1 # porcelain.py -- Porcelain-like layer on top of Dulwich
2 # Copyright (C) 2013 Jelmer Vernooij <jelmer@jelmer.uk>
4 # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
5 # General Public License as public by the Free Software Foundation; version 2.0
6 # or (at your option) any later version. You can redistribute it and/or
7 # modify it under the terms of either of these two licenses.
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
15 # You should have received a copy of the licenses; if not, see
16 # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
17 # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
18 # License, Version 2.0.
21 """Simple wrapper that provides porcelain-like functions on top of Dulwich.
23 Currently implemented:
26 * branch{_create,_delete,_list}
47 * tag{_create,_delete,_list}
53 These functions are meant to behave similarly to the git subcommands.
54 Differences in behaviour are considered bugs.
56 Functions should generally accept both unicode strings and bytestrings
59 from collections import namedtuple
60 from contextlib import (
64 from io import BytesIO
72 from dulwich.archive import (
75 from dulwich.client import (
76 get_transport_and_path,
78 from dulwich.config import (
81 from dulwich.diff_tree import (
89 from dulwich.errors import (
93 from dulwich.ignore import IgnoreFilterManager
94 from dulwich.index import (
95 blob_from_path_and_stat,
98 from dulwich.object_store import (
101 from dulwich.objects import (
106 pretty_format_tree_entry,
108 from dulwich.objectspec import (
115 from dulwich.pack import (
119 from dulwich.patch import write_tree_diff
120 from dulwich.protocol import (
124 from dulwich.refs import (
125 ANNOTATED_TAG_SUFFIX,
128 from dulwich.repo import (BaseRepo, Repo)
129 from dulwich.server import (
134 update_server_info as server_update_server_info,
138 # Module level tuple definition for status output
139 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
142 default_bytes_out_stream = getattr(sys.stdout, 'buffer', sys.stdout)
143 default_bytes_err_stream = getattr(sys.stderr, 'buffer', sys.stderr)
146 DEFAULT_ENCODING = 'utf-8'
149 class RemoteExists(Exception):
150 """Raised when the remote already exists."""
153 def open_repo(path_or_repo):
154 """Open an argument that can be a repository or a path for a repository."""
155 if isinstance(path_or_repo, BaseRepo):
157 return Repo(path_or_repo)
161 def _noop_context_manager(obj):
162 """Context manager that has the same api as closing but does nothing."""
166 def open_repo_closing(path_or_repo):
167 """Open an argument that can be a repository or a path for a repository.
168 returns a context manager that will close the repo on exit if the argument
169 is a path, else does nothing if the argument is a repo.
171 if isinstance(path_or_repo, BaseRepo):
172 return _noop_context_manager(path_or_repo)
173 return closing(Repo(path_or_repo))
176 def path_to_tree_path(repopath, path):
177 """Convert a path to a path usable in an index, e.g. bytes and relative to
180 :param repopath: Repository path, absolute or relative to the cwd
181 :param path: A path, absolute or relative to the cwd
182 :return: A path formatted for use in e.g. an index
184 if not isinstance(path, bytes):
185 path = path.encode(sys.getfilesystemencoding())
186 if not isinstance(repopath, bytes):
187 repopath = repopath.encode(sys.getfilesystemencoding())
188 treepath = os.path.relpath(path, repopath)
189 if treepath.startswith(b'..'):
190 raise ValueError('Path not in repo')
191 if os.path.sep != '/':
192 treepath = treepath.replace(os.path.sep.encode('ascii'), b'/')
196 def archive(repo, committish=None, outstream=default_bytes_out_stream,
197 errstream=default_bytes_err_stream):
198 """Create an archive.
200 :param repo: Path of repository for which to generate an archive.
201 :param committish: Commit SHA1 or ref to use
202 :param outstream: Output stream (defaults to stdout)
203 :param errstream: Error stream (defaults to stderr)
206 if committish is None:
208 with open_repo_closing(repo) as repo_obj:
209 c = repo_obj[committish]
210 for chunk in tar_stream(
211 repo_obj.object_store, repo_obj.object_store[c.tree],
213 outstream.write(chunk)
216 def update_server_info(repo="."):
217 """Update server info files for a repository.
219 :param repo: path to the repository
221 with open_repo_closing(repo) as r:
222 server_update_server_info(r)
225 def symbolic_ref(repo, ref_name, force=False):
226 """Set git symbolic ref into HEAD.
228 :param repo: path to the repository
229 :param ref_name: short name of the new ref
230 :param force: force settings without checking if it exists in refs/heads
232 with open_repo_closing(repo) as repo_obj:
233 ref_path = _make_branch_ref(ref_name)
234 if not force and ref_path not in repo_obj.refs.keys():
235 raise ValueError('fatal: ref `%s` is not a ref' % ref_name)
236 repo_obj.refs.set_symbolic_ref(b'HEAD', ref_path)
239 def commit(repo=".", message=None, author=None, committer=None, encoding=None):
240 """Create a new commit.
242 :param repo: Path to repository
243 :param message: Optional commit message
244 :param author: Optional author name and email
245 :param committer: Optional committer name and email
246 :return: SHA1 of the new commit
248 # FIXME: Support --all argument
249 # FIXME: Support --signoff argument
250 if getattr(message, 'encode', None):
251 message = message.encode(encoding or DEFAULT_ENCODING)
252 if getattr(author, 'encode', None):
253 author = author.encode(encoding or DEFAULT_ENCODING)
254 if getattr(committer, 'encode', None):
255 committer = committer.encode(encoding or DEFAULT_ENCODING)
256 with open_repo_closing(repo) as r:
258 message=message, author=author, committer=committer,
262 def commit_tree(repo, tree, message=None, author=None, committer=None):
263 """Create a new commit object.
265 :param repo: Path to repository
266 :param tree: An existing tree object
267 :param author: Optional author name and email
268 :param committer: Optional committer name and email
270 with open_repo_closing(repo) as r:
272 message=message, tree=tree, committer=committer, author=author)
275 def init(path=".", bare=False):
276 """Create a new git repository.
278 :param path: Path to repository.
279 :param bare: Whether to create a bare repository.
280 :return: A Repo instance
282 if not os.path.exists(path):
286 return Repo.init_bare(path)
288 return Repo.init(path)
291 def clone(source, target=None, bare=False, checkout=None,
292 errstream=default_bytes_err_stream, outstream=None,
293 origin=b"origin", **kwargs):
294 """Clone a local or remote git repository.
296 :param source: Path or URL for source repository
297 :param target: Path to target repository (optional)
298 :param bare: Whether or not to create a bare repository
299 :param checkout: Whether or not to check-out HEAD after cloning
300 :param errstream: Optional stream to write progress to
301 :param outstream: Optional stream to write progress to (deprecated)
302 :param origin: Name of remote from the repository used to clone
303 :return: The new repository
305 # TODO(jelmer): This code overlaps quite a bit with Repo.clone
306 if outstream is not None:
309 "outstream= has been deprecated in favour of errstream=.",
310 DeprecationWarning, stacklevel=3)
311 errstream = outstream
314 checkout = (not bare)
315 if checkout and bare:
316 raise ValueError("checkout and bare are incompatible")
319 target = source.split("/")[-1]
321 if not os.path.exists(target):
325 r = Repo.init_bare(target)
327 r = Repo.init(target)
329 reflog_message = b'clone: from ' + source.encode('utf-8')
331 fetch_result = fetch(
332 r, source, origin, errstream=errstream, message=reflog_message,
334 target_config = r.get_config()
335 if not isinstance(source, bytes):
336 source = source.encode(DEFAULT_ENCODING)
337 target_config.set((b'remote', origin), b'url', source)
339 (b'remote', origin), b'fetch',
340 b'+refs/heads/*:refs/remotes/' + origin + b'/*')
341 target_config.write_to_path()
342 # TODO(jelmer): Support symref capability,
343 # https://github.com/jelmer/dulwich/issues/485
345 head = r[fetch_result[b'HEAD']]
350 if checkout and not bare and head is not None:
351 errstream.write(b'Checking out ' + head.id + b'\n')
352 r.reset_index(head.tree)
353 except BaseException:
360 def add(repo=".", paths=None):
361 """Add files to the staging area.
363 :param repo: Repository for the files
364 :param paths: Paths to add. No value passed stages all modified files.
365 :return: Tuple with set of added files and ignored files
368 with open_repo_closing(repo) as r:
369 ignore_manager = IgnoreFilterManager.from_repo(r)
372 get_untracked_paths(os.getcwd(), r.path, r.open_index()))
374 if not isinstance(paths, list):
377 relpath = os.path.relpath(p, r.path)
378 if relpath.startswith('..' + os.path.sep):
379 raise ValueError('path %r is not in repo' % relpath)
380 # FIXME: Support patterns, directories.
381 if ignore_manager.is_ignored(relpath):
384 relpaths.append(relpath)
386 return (relpaths, ignored)
389 def remove(repo=".", paths=None, cached=False):
390 """Remove files from the staging area.
392 :param repo: Repository for the files
393 :param paths: Paths to remove
395 with open_repo_closing(repo) as r:
396 index = r.open_index()
398 full_path = os.path.abspath(p).encode(sys.getfilesystemencoding())
399 tree_path = path_to_tree_path(r.path, p)
401 index_sha = index[tree_path].sha
403 raise Exception('%s did not match any files' % p)
407 st = os.lstat(full_path)
412 blob = blob_from_path_and_stat(full_path, st)
417 committed_sha = tree_lookup_path(
418 r.__getitem__, r[r.head()].tree, tree_path)[1]
422 if blob.id != index_sha and index_sha != committed_sha:
424 'file has staged content differing '
425 'from both the file and head: %s' % p)
427 if index_sha != committed_sha:
429 'file has staged changes: %s' % p)
438 def commit_decode(commit, contents, default_encoding=DEFAULT_ENCODING):
439 if commit.encoding is not None:
440 return contents.decode(commit.encoding, "replace")
441 return contents.decode(default_encoding, "replace")
444 def print_commit(commit, decode, outstream=sys.stdout):
445 """Write a human-readable commit log entry.
447 :param commit: A `Commit` object
448 :param outstream: A stream file to write to
450 outstream.write("-" * 50 + "\n")
451 outstream.write("commit: " + commit.id.decode('ascii') + "\n")
452 if len(commit.parents) > 1:
455 "...".join([c.decode('ascii') for c in commit.parents[1:]]) + "\n")
456 outstream.write("Author: " + decode(commit.author) + "\n")
457 if commit.author != commit.committer:
458 outstream.write("Committer: " + decode(commit.committer) + "\n")
460 time_tuple = time.gmtime(commit.author_time + commit.author_timezone)
461 time_str = time.strftime("%a %b %d %Y %H:%M:%S", time_tuple)
462 timezone_str = format_timezone(commit.author_timezone).decode('ascii')
463 outstream.write("Date: " + time_str + " " + timezone_str + "\n")
464 outstream.write("\n")
465 outstream.write(decode(commit.message) + "\n")
466 outstream.write("\n")
469 def print_tag(tag, decode, outstream=sys.stdout):
470 """Write a human-readable tag.
472 :param tag: A `Tag` object
473 :param decode: Function for decoding bytes to unicode string
474 :param outstream: A stream to write to
476 outstream.write("Tagger: " + decode(tag.tagger) + "\n")
477 outstream.write("Date: " + decode(tag.tag_time) + "\n")
478 outstream.write("\n")
479 outstream.write(decode(tag.message) + "\n")
480 outstream.write("\n")
483 def show_blob(repo, blob, decode, outstream=sys.stdout):
484 """Write a blob to a stream.
486 :param repo: A `Repo` object
487 :param blob: A `Blob` object
488 :param decode: Function for decoding bytes to unicode string
489 :param outstream: A stream file to write to
491 outstream.write(decode(blob.data))
494 def show_commit(repo, commit, decode, outstream=sys.stdout):
495 """Show a commit to a stream.
497 :param repo: A `Repo` object
498 :param commit: A `Commit` object
499 :param decode: Function for decoding bytes to unicode string
500 :param outstream: Stream to write to
502 print_commit(commit, decode=decode, outstream=outstream)
504 parent_commit = repo[commit.parents[0]]
505 base_tree = parent_commit.tree
508 diffstream = BytesIO()
511 repo.object_store, base_tree, commit.tree)
514 diffstream.getvalue().decode(
515 commit.encoding or DEFAULT_ENCODING, 'replace'))
518 def show_tree(repo, tree, decode, outstream=sys.stdout):
519 """Print a tree to a stream.
521 :param repo: A `Repo` object
522 :param tree: A `Tree` object
523 :param decode: Function for decoding bytes to unicode string
524 :param outstream: Stream to write to
527 outstream.write(decode(n) + "\n")
530 def show_tag(repo, tag, decode, outstream=sys.stdout):
531 """Print a tag to a stream.
533 :param repo: A `Repo` object
534 :param tag: A `Tag` object
535 :param decode: Function for decoding bytes to unicode string
536 :param outstream: Stream to write to
538 print_tag(tag, decode, outstream)
539 show_object(repo, repo[tag.object[1]], outstream)
542 def show_object(repo, obj, decode, outstream):
546 b"commit": show_commit,
548 }[obj.type_name](repo, obj, decode, outstream)
551 def print_name_status(changes):
552 """Print a simple status summary, listing changed files.
554 for change in changes:
557 if isinstance(change, list):
559 if change.type == CHANGE_ADD:
560 path1 = change.new.path
563 elif change.type == CHANGE_DELETE:
564 path1 = change.old.path
567 elif change.type == CHANGE_MODIFY:
568 path1 = change.new.path
571 elif change.type in RENAME_CHANGE_TYPES:
572 path1 = change.old.path
573 path2 = change.new.path
574 if change.type == CHANGE_RENAME:
576 elif change.type == CHANGE_COPY:
578 yield '%-8s%-20s%-20s' % (kind, path1, path2)
581 def log(repo=".", paths=None, outstream=sys.stdout, max_entries=None,
582 reverse=False, name_status=False):
583 """Write commit logs.
585 :param repo: Path to repository
586 :param paths: Optional set of specific paths to print entries for
587 :param outstream: Stream to write log output to
588 :param reverse: Reverse order in which entries are printed
589 :param name_status: Print name status
590 :param max_entries: Optional maximum number of entries to display
592 with open_repo_closing(repo) as r:
593 walker = r.get_walker(
594 max_entries=max_entries, paths=paths, reverse=reverse)
597 return commit_decode(entry.commit, x)
598 print_commit(entry.commit, decode, outstream)
600 outstream.writelines(
601 [l+'\n' for l in print_name_status(entry.changes())])
604 # TODO(jelmer): better default for encoding?
605 def show(repo=".", objects=None, outstream=sys.stdout,
606 default_encoding=DEFAULT_ENCODING):
607 """Print the changes in a commit.
609 :param repo: Path to repository
610 :param objects: Objects to show (defaults to [HEAD])
611 :param outstream: Stream to write to
612 :param default_encoding: Default encoding to use if none is set in the
617 if not isinstance(objects, list):
619 with open_repo_closing(repo) as r:
620 for objectish in objects:
621 o = parse_object(r, objectish)
622 if isinstance(o, Commit):
624 return commit_decode(o, x, default_encoding)
627 return x.decode(default_encoding)
628 show_object(r, o, decode, outstream)
631 def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout):
632 """Compares the content and mode of blobs found via two tree objects.
634 :param repo: Path to repository
635 :param old_tree: Id of old tree
636 :param new_tree: Id of new tree
637 :param outstream: Stream to write to
639 with open_repo_closing(repo) as r:
640 write_tree_diff(outstream, r.object_store, old_tree, new_tree)
643 def rev_list(repo, commits, outstream=sys.stdout):
644 """Lists commit objects in reverse chronological order.
646 :param repo: Path to repository
647 :param commits: Commits over which to iterate
648 :param outstream: Stream to write to
650 with open_repo_closing(repo) as r:
651 for entry in r.get_walker(include=[r[c].id for c in commits]):
652 outstream.write(entry.commit.id + b"\n")
655 def tag(*args, **kwargs):
657 warnings.warn("tag has been deprecated in favour of tag_create.",
659 return tag_create(*args, **kwargs)
663 repo, tag, author=None, message=None, annotated=False,
664 objectish="HEAD", tag_time=None, tag_timezone=None):
665 """Creates a tag in git via dulwich calls:
667 :param repo: Path to repository
668 :param tag: tag string
669 :param author: tag author (optional, if annotated is set)
670 :param message: tag message (optional)
671 :param annotated: whether to create an annotated tag
672 :param objectish: object the tag should point at, defaults to HEAD
673 :param tag_time: Optional time for annotated tag
674 :param tag_timezone: Optional timezone for annotated tag
677 with open_repo_closing(repo) as r:
678 object = parse_object(r, objectish)
681 # Create the tag object
684 # TODO(jelmer): Don't use repo private method.
685 author = r._get_user_identity()
686 tag_obj.tagger = author
687 tag_obj.message = message
689 tag_obj.object = (type(object), object.id)
691 tag_time = int(time.time())
692 tag_obj.tag_time = tag_time
693 if tag_timezone is None:
694 # TODO(jelmer) Use current user timezone rather than UTC
696 elif isinstance(tag_timezone, str):
697 tag_timezone = parse_timezone(tag_timezone)
698 tag_obj.tag_timezone = tag_timezone
699 r.object_store.add_object(tag_obj)
704 r.refs[_make_tag_ref(tag)] = tag_id
707 def list_tags(*args, **kwargs):
709 warnings.warn("list_tags has been deprecated in favour of tag_list.",
711 return tag_list(*args, **kwargs)
714 def tag_list(repo, outstream=sys.stdout):
717 :param repo: Path to repository
718 :param outstream: Stream to write tags to
720 with open_repo_closing(repo) as r:
721 tags = sorted(r.refs.as_dict(b"refs/tags"))
725 def tag_delete(repo, name):
728 :param repo: Path to repository
729 :param name: Name of tag to remove
731 with open_repo_closing(repo) as r:
732 if isinstance(name, bytes):
734 elif isinstance(name, list):
737 raise TypeError("Unexpected tag name type %r" % name)
739 del r.refs[_make_tag_ref(name)]
742 def reset(repo, mode, treeish="HEAD"):
743 """Reset current HEAD to the specified state.
745 :param repo: Path to repository
746 :param mode: Mode ("hard", "soft", "mixed")
747 :param treeish: Treeish to reset to
751 raise ValueError("hard is the only mode currently supported")
753 with open_repo_closing(repo) as r:
754 tree = parse_tree(r, treeish)
755 r.reset_index(tree.id)
758 def push(repo, remote_location, refspecs,
759 outstream=default_bytes_out_stream,
760 errstream=default_bytes_err_stream, **kwargs):
761 """Remote push with dulwich via dulwich.client
763 :param repo: Path to repository
764 :param remote_location: Location of the remote
765 :param refspecs: Refs to push to remote
766 :param outstream: A stream file to write output
767 :param errstream: A stream file to write errors
771 with open_repo_closing(repo) as r:
773 # Get the client and path
774 client, path = get_transport_and_path(
775 remote_location, config=r.get_config_stack(), **kwargs)
779 def update_refs(refs):
780 selected_refs.extend(parse_reftuples(r.refs, refs, refspecs))
782 # TODO: Handle selected_refs == {None: None}
783 for (lh, rh, force) in selected_refs:
785 new_refs[rh] = ZERO_SHA
787 new_refs[rh] = r.refs[lh]
790 err_encoding = getattr(errstream, 'encoding', None) or DEFAULT_ENCODING
791 remote_location_bytes = client.get_url(path).encode(err_encoding)
795 generate_pack_data=r.object_store.generate_pack_data,
796 progress=errstream.write)
798 b"Push to " + remote_location_bytes + b" successful.\n")
799 except (UpdateRefsError, SendPackError) as e:
800 errstream.write(b"Push to " + remote_location_bytes +
801 b" failed -> " + e.message.encode(err_encoding) +
805 def pull(repo, remote_location=None, refspecs=None,
806 outstream=default_bytes_out_stream,
807 errstream=default_bytes_err_stream, **kwargs):
808 """Pull from remote via dulwich.client
810 :param repo: Path to repository
811 :param remote_location: Location of the remote
812 :param refspec: refspecs to fetch
813 :param outstream: A stream file to write to output
814 :param errstream: A stream file to write to errors
817 with open_repo_closing(repo) as r:
818 if remote_location is None:
819 # TODO(jelmer): Lookup 'remote' for current branch in config
820 raise NotImplementedError(
821 "looking up remote from branch config not supported yet")
826 def determine_wants(remote_refs):
827 selected_refs.extend(
828 parse_reftuples(remote_refs, r.refs, refspecs))
829 return [remote_refs[lh] for (lh, rh, force) in selected_refs]
830 client, path = get_transport_and_path(
831 remote_location, config=r.get_config_stack(), **kwargs)
832 fetch_result = client.fetch(
833 path, r, progress=errstream.write, determine_wants=determine_wants)
834 for (lh, rh, force) in selected_refs:
835 r.refs[rh] = fetch_result.refs[lh]
837 r[b'HEAD'] = fetch_result.refs[selected_refs[0][1]]
839 # Perform 'git checkout .' - syncs staged changes
840 tree = r[b"HEAD"].tree
841 r.reset_index(tree=tree)
844 def status(repo=".", ignored=False):
845 """Returns staged, unstaged, and untracked changes relative to the HEAD.
847 :param repo: Path to repository or repository object
848 :param ignored: Whether to include ignored files in `untracked`
849 :return: GitStatus tuple,
850 staged - list of staged paths (diff index/HEAD)
851 unstaged - list of unstaged paths (diff index/working-tree)
852 untracked - list of untracked, un-ignored & non-.git paths
854 with open_repo_closing(repo) as r:
855 # 1. Get status of staged
856 tracked_changes = get_tree_changes(r)
857 # 2. Get status of unstaged
858 index = r.open_index()
859 unstaged_changes = list(get_unstaged_changes(index, r.path))
860 ignore_manager = IgnoreFilterManager.from_repo(r)
861 untracked_paths = get_untracked_paths(r.path, r.path, index)
863 untracked_changes = list(untracked_paths)
865 untracked_changes = [
866 p for p in untracked_paths
867 if not ignore_manager.is_ignored(p)]
868 return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
871 def get_untracked_paths(frompath, basepath, index):
872 """Get untracked paths.
874 ;param frompath: Path to walk
875 :param basepath: Path to compare to
876 :param index: Index to check against
878 # If nothing is specified, add all non-ignored files.
879 for dirpath, dirnames, filenames in os.walk(frompath):
880 # Skip .git and below.
881 if '.git' in dirnames:
882 dirnames.remove('.git')
883 if dirpath != basepath:
885 if '.git' in filenames:
886 filenames.remove('.git')
887 if dirpath != basepath:
889 for filename in filenames:
890 ap = os.path.join(dirpath, filename)
891 ip = path_to_tree_path(basepath, ap)
893 yield os.path.relpath(ap, frompath)
896 def get_tree_changes(repo):
897 """Return add/delete/modify changes to tree by comparing index to HEAD.
899 :param repo: repo path or object
900 :return: dict with lists for each type of change
902 with open_repo_closing(repo) as r:
903 index = r.open_index()
905 # Compares the Index to the HEAD & determines changes
906 # Iterate through the changes and report add/delete/modify
907 # TODO: call out to dulwich.diff_tree somehow.
914 tree_id = r[b'HEAD'].tree
918 for change in index.changes_from_tree(r.object_store, tree_id):
920 tracked_changes['add'].append(change[0][1])
921 elif not change[0][1]:
922 tracked_changes['delete'].append(change[0][0])
923 elif change[0][0] == change[0][1]:
924 tracked_changes['modify'].append(change[0][0])
926 raise AssertionError('git mv ops not yet supported')
927 return tracked_changes
930 def daemon(path=".", address=None, port=None):
931 """Run a daemon serving Git requests over TCP/IP.
933 :param path: Path to the directory to serve.
934 :param address: Optional address to listen on (defaults to ::)
935 :param port: Optional port to listen on (defaults to TCP_GIT_PORT)
937 # TODO(jelmer): Support git-daemon-export-ok and --export-all.
938 backend = FileSystemBackend(path)
939 server = TCPGitServer(backend, address, port)
940 server.serve_forever()
943 def web_daemon(path=".", address=None, port=None):
944 """Run a daemon serving Git requests over HTTP.
946 :param path: Path to the directory to serve
947 :param address: Optional address to listen on (defaults to ::)
948 :param port: Optional port to listen on (defaults to 80)
950 from dulwich.web import (
953 WSGIRequestHandlerLogger,
956 backend = FileSystemBackend(path)
957 app = make_wsgi_chain(backend)
958 server = make_server(address, port, app,
959 handler_class=WSGIRequestHandlerLogger,
960 server_class=WSGIServerLogger)
961 server.serve_forever()
964 def upload_pack(path=".", inf=None, outf=None):
965 """Upload a pack file after negotiating its contents using smart protocol.
967 :param path: Path to the repository
968 :param inf: Input stream to communicate with client
969 :param outf: Output stream to communicate with client
972 outf = getattr(sys.stdout, 'buffer', sys.stdout)
974 inf = getattr(sys.stdin, 'buffer', sys.stdin)
975 path = os.path.expanduser(path)
976 backend = FileSystemBackend(path)
981 proto = Protocol(inf.read, send_fn)
982 handler = UploadPackHandler(backend, [path], proto)
983 # FIXME: Catch exceptions and write a single-line summary to outf.
988 def receive_pack(path=".", inf=None, outf=None):
989 """Receive a pack file after negotiating its contents using smart protocol.
991 :param path: Path to the repository
992 :param inf: Input stream to communicate with client
993 :param outf: Output stream to communicate with client
996 outf = getattr(sys.stdout, 'buffer', sys.stdout)
998 inf = getattr(sys.stdin, 'buffer', sys.stdin)
999 path = os.path.expanduser(path)
1000 backend = FileSystemBackend(path)
1005 proto = Protocol(inf.read, send_fn)
1006 handler = ReceivePackHandler(backend, [path], proto)
1007 # FIXME: Catch exceptions and write a single-line summary to outf.
1012 def _make_branch_ref(name):
1013 if getattr(name, 'encode', None):
1014 name = name.encode(DEFAULT_ENCODING)
1015 return b"refs/heads/" + name
1018 def _make_tag_ref(name):
1019 if getattr(name, 'encode', None):
1020 name = name.encode(DEFAULT_ENCODING)
1021 return b"refs/tags/" + name
1024 def branch_delete(repo, name):
1027 :param repo: Path to the repository
1028 :param name: Name of the branch
1030 with open_repo_closing(repo) as r:
1031 if isinstance(name, list):
1036 del r.refs[_make_branch_ref(name)]
1039 def branch_create(repo, name, objectish=None, force=False):
1042 :param repo: Path to the repository
1043 :param name: Name of the new branch
1044 :param objectish: Target object to point new branch at (defaults to HEAD)
1045 :param force: Force creation of branch, even if it already exists
1047 with open_repo_closing(repo) as r:
1048 if objectish is None:
1050 object = parse_object(r, objectish)
1051 refname = _make_branch_ref(name)
1052 ref_message = b"branch: Created from " + objectish.encode('utf-8')
1054 r.refs.set_if_equals(refname, None, object.id, message=ref_message)
1056 if not r.refs.add_if_new(refname, object.id, message=ref_message):
1057 raise KeyError("Branch with name %s already exists." % name)
1060 def branch_list(repo):
1061 """List all branches.
1063 :param repo: Path to the repository
1065 with open_repo_closing(repo) as r:
1066 return r.refs.keys(base=b"refs/heads/")
1069 def fetch(repo, remote_location, remote_name=b'origin', outstream=sys.stdout,
1070 errstream=default_bytes_err_stream, message=None, **kwargs):
1071 """Fetch objects from a remote server.
1073 :param repo: Path to the repository
1074 :param remote_location: String identifying a remote server
1075 :param remote_name: Name for remote server
1076 :param outstream: Output stream (defaults to stdout)
1077 :param errstream: Error stream (defaults to stderr)
1078 :param message: Reflog message (defaults to b"fetch: from <remote_name>")
1079 :return: Dictionary with refs on the remote
1082 message = b'fetch: from ' + remote_location.encode("utf-8")
1083 with open_repo_closing(repo) as r:
1084 client, path = get_transport_and_path(
1085 remote_location, config=r.get_config_stack(), **kwargs)
1086 fetch_result = client.fetch(path, r, progress=errstream.write)
1087 stripped_refs = strip_peeled_refs(fetch_result.refs)
1089 n[len(b'refs/heads/'):]: v for (n, v) in stripped_refs.items()
1090 if n.startswith(b'refs/heads/')}
1092 b'refs/remotes/' + remote_name, branches, message=message)
1094 n[len(b'refs/tags/'):]: v for (n, v) in stripped_refs.items()
1095 if n.startswith(b'refs/tags/') and
1096 not n.endswith(ANNOTATED_TAG_SUFFIX)}
1097 r.refs.import_refs(b'refs/tags', tags, message=message)
1098 return fetch_result.refs
1101 def ls_remote(remote, config=None, **kwargs):
1102 """List the refs in a remote.
1104 :param remote: Remote repository location
1105 :param config: Configuration to use
1106 :return: Dictionary with remote refs
1109 config = StackedConfig.default()
1110 client, host_path = get_transport_and_path(remote, config=config, **kwargs)
1111 return client.get_refs(host_path)
1115 """Repack loose files in a repository.
1117 Currently this only packs loose objects.
1119 :param repo: Path to the repository
1121 with open_repo_closing(repo) as r:
1122 r.object_store.pack_loose_objects()
1125 def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None):
1126 """Pack objects into a file.
1128 :param repo: Path to the repository
1129 :param object_ids: List of object ids to write
1130 :param packf: File-like object to write to
1131 :param idxf: File-like object to write to (can be None)
1133 with open_repo_closing(repo) as r:
1134 entries, data_sum = write_pack_objects(
1136 r.object_store.iter_shas((oid, None) for oid in object_ids),
1137 delta_window_size=delta_window_size)
1138 if idxf is not None:
1139 entries = sorted([(k, v[0], v[1]) for (k, v) in entries.items()])
1140 write_pack_index(idxf, entries, data_sum)
1143 def ls_tree(repo, treeish=b"HEAD", outstream=sys.stdout, recursive=False,
1145 """List contents of a tree.
1147 :param repo: Path to the repository
1148 :param tree_ish: Tree id to list
1149 :param outstream: Output stream (defaults to stdout)
1150 :param recursive: Whether to recursively list files
1151 :param name_only: Only print item name
1153 def list_tree(store, treeid, base):
1154 for (name, mode, sha) in store[treeid].iteritems():
1156 name = posixpath.join(base, name)
1158 outstream.write(name + b"\n")
1160 outstream.write(pretty_format_tree_entry(name, mode, sha))
1161 if stat.S_ISDIR(mode) and recursive:
1162 list_tree(store, sha, name)
1163 with open_repo_closing(repo) as r:
1164 tree = parse_tree(r, treeish)
1165 list_tree(r.object_store, tree.id, "")
1168 def remote_add(repo, name, url):
1171 :param repo: Path to the repository
1172 :param name: Remote name
1173 :param url: Remote URL
1175 if not isinstance(name, bytes):
1176 name = name.encode(DEFAULT_ENCODING)
1177 if not isinstance(url, bytes):
1178 url = url.encode(DEFAULT_ENCODING)
1179 with open_repo_closing(repo) as r:
1181 section = (b'remote', name)
1182 if c.has_section(section):
1183 raise RemoteExists(section)
1184 c.set(section, b"url", url)
1188 def check_ignore(repo, paths, no_index=False):
1189 """Debug gitignore files.
1191 :param repo: Path to the repository
1192 :param paths: List of paths to check for
1193 :param no_index: Don't check index
1194 :return: List of ignored files
1196 with open_repo_closing(repo) as r:
1197 index = r.open_index()
1198 ignore_manager = IgnoreFilterManager.from_repo(r)
1200 if not no_index and path_to_tree_path(r.path, path) in index:
1202 if os.path.isabs(path):
1203 path = os.path.relpath(path, r.path)
1204 if ignore_manager.is_ignored(path):
1208 def update_head(repo, target, detached=False, new_branch=None):
1209 """Update HEAD to point at a new branch/commit.
1211 Note that this does not actually update the working tree.
1213 :param repo: Path to the repository
1214 :param detach: Create a detached head
1215 :param target: Branch or committish to switch to
1216 :param new_branch: New branch to create
1218 with open_repo_closing(repo) as r:
1219 if new_branch is not None:
1220 to_set = _make_branch_ref(new_branch)
1224 # TODO(jelmer): Provide some way so that the actual ref gets
1225 # updated rather than what it points to, so the delete isn't
1228 r.refs[to_set] = parse_commit(r, target).id
1230 r.refs.set_symbolic_ref(to_set, parse_ref(r, target))
1231 if new_branch is not None:
1232 r.refs.set_symbolic_ref(b"HEAD", to_set)
1235 def check_mailmap(repo, contact):
1236 """Check canonical name and email of contact.
1238 :param repo: Path to the repository
1239 :param contact: Contact name and/or email
1240 :return: Canonical contact data
1242 with open_repo_closing(repo) as r:
1243 from dulwich.mailmap import Mailmap
1246 mailmap = Mailmap.from_path(os.path.join(r.path, '.mailmap'))
1247 except IOError as e:
1248 if e.errno != errno.ENOENT:
1251 return mailmap.lookup(contact)
1255 """Check a repository.
1257 :param repo: A path to the repository
1258 :return: Iterator over errors/warnings
1260 with open_repo_closing(repo) as r:
1261 # TODO(jelmer): check pack files
1262 # TODO(jelmer): check graph
1263 # TODO(jelmer): check refs
1264 for sha in r.object_store:
1265 o = r.object_store[sha]
1268 except Exception as e:
1272 def stash_list(repo):
1273 """List all stashes in a repository."""
1274 with open_repo_closing(repo) as r:
1275 from dulwich.stash import Stash
1276 stash = Stash.from_repo(r)
1277 return enumerate(list(stash.stashes()))
1280 def stash_push(repo):
1281 """Push a new stash onto the stack."""
1282 with open_repo_closing(repo) as r:
1283 from dulwich.stash import Stash
1284 stash = Stash.from_repo(r)
1288 def stash_pop(repo):
1289 """Pop a new stash from the stack."""
1290 with open_repo_closing(repo) as r:
1291 from dulwich.stash import Stash
1292 stash = Stash.from_repo(r)
1297 """List all files in an index."""
1298 with open_repo_closing(repo) as r:
1299 return sorted(r.open_index())
1303 """Describe the repository version.
1305 :param projdir: git repository root
1306 :returns: a string description of the current git revision
1308 Examples: "gabcdefh", "v0.1" or "v0.1-5-gabcdefh".
1310 # Get the repository
1311 with open_repo_closing(repo) as r:
1312 # Get a list of all tags
1315 for key, value in refs.items():
1317 obj = r.get_object(value)
1318 if u'tags' not in key:
1321 _, tag = key.rsplit(u'/', 1)
1325 except AttributeError:
1328 commit = r.get_object(commit[1])
1330 datetime.datetime(*time.gmtime(commit.commit_time)[:6]),
1331 commit.id.decode('ascii'),
1334 sorted_tags = sorted(tags.items(),
1335 key=lambda tag: tag[1][0],
1338 # If there are no tags, return the current commit
1339 if len(sorted_tags) == 0:
1340 return 'g{}'.format(r[r.head()].id.decode('ascii')[:7])
1342 # We're now 0 commits from the top
1345 # Get the latest commit
1346 latest_commit = r[r.head()]
1348 # Walk through all commits
1349 walker = r.get_walker()
1350 for entry in walker:
1352 commit_id = entry.commit.id.decode('ascii')
1353 for tag in sorted_tags:
1355 tag_commit = tag[1][1]
1356 if commit_id == tag_commit:
1357 if commit_count == 0:
1360 return '{}-{}-g{}'.format(
1363 latest_commit.id.decode('ascii')[:7])
1367 # Return plain commit if no parent tag can be found
1368 return 'g{}'.format(latest_commit.id.decode('ascii')[:7])