1 # porcelain.py -- Porcelain-like layer on top of Dulwich
2 # Copyright (C) 2013 Jelmer Vernooij <jelmer@samba.org>
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}
42 * tag{_create,_delete,_list}
48 These functions are meant to behave similarly to the git subcommands.
49 Differences in behaviour are considered bugs.
52 __docformat__ = 'restructuredText'
54 from collections import namedtuple
55 from contextlib import (
65 from dulwich.archive import (
68 from dulwich.client import (
69 get_transport_and_path,
71 from dulwich.diff_tree import (
79 from dulwich.errors import (
83 from dulwich.index import get_unstaged_changes
84 from dulwich.objects import (
89 pretty_format_tree_entry,
91 from dulwich.objectspec import (
95 from dulwich.pack import (
99 from dulwich.patch import write_tree_diff
100 from dulwich.protocol import (
104 from dulwich.refs import ANNOTATED_TAG_SUFFIX
105 from dulwich.repo import (BaseRepo, Repo)
106 from dulwich.server import (
111 update_server_info as server_update_server_info,
115 # Module level tuple definition for status output
116 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
119 default_bytes_out_stream = getattr(sys.stdout, 'buffer', sys.stdout)
120 default_bytes_err_stream = getattr(sys.stderr, 'buffer', sys.stderr)
123 DEFAULT_ENCODING = 'utf-8'
126 def open_repo(path_or_repo):
127 """Open an argument that can be a repository or a path for a repository."""
128 if isinstance(path_or_repo, BaseRepo):
130 return Repo(path_or_repo)
134 def _noop_context_manager(obj):
135 """Context manager that has the same api as closing but does nothing."""
139 def open_repo_closing(path_or_repo):
140 """Open an argument that can be a repository or a path for a repository.
141 returns a context manager that will close the repo on exit if the argument
142 is a path, else does nothing if the argument is a repo.
144 if isinstance(path_or_repo, BaseRepo):
145 return _noop_context_manager(path_or_repo)
146 return closing(Repo(path_or_repo))
149 def archive(repo, committish=None, outstream=default_bytes_out_stream,
150 errstream=default_bytes_err_stream):
151 """Create an archive.
153 :param repo: Path of repository for which to generate an archive.
154 :param committish: Commit SHA1 or ref to use
155 :param outstream: Output stream (defaults to stdout)
156 :param errstream: Error stream (defaults to stderr)
159 if committish is None:
161 with open_repo_closing(repo) as repo_obj:
162 c = repo_obj[committish]
164 for chunk in tar_stream(repo_obj.object_store,
165 repo_obj.object_store[c.tree], c.commit_time):
166 outstream.write(chunk)
169 def update_server_info(repo="."):
170 """Update server info files for a repository.
172 :param repo: path to the repository
174 with open_repo_closing(repo) as r:
175 server_update_server_info(r)
178 def symbolic_ref(repo, ref_name, force=False):
179 """Set git symbolic ref into HEAD.
181 :param repo: path to the repository
182 :param ref_name: short name of the new ref
183 :param force: force settings without checking if it exists in refs/heads
185 with open_repo_closing(repo) as repo_obj:
186 ref_path = b'refs/heads/' + ref_name
187 if not force and ref_path not in repo_obj.refs.keys():
188 raise ValueError('fatal: ref `%s` is not a ref' % ref_name)
189 repo_obj.refs.set_symbolic_ref(b'HEAD', ref_path)
192 def commit(repo=".", message=None, author=None, committer=None):
193 """Create a new commit.
195 :param repo: Path to repository
196 :param message: Optional commit message
197 :param author: Optional author name and email
198 :param committer: Optional committer name and email
199 :return: SHA1 of the new commit
201 # FIXME: Support --all argument
202 # FIXME: Support --signoff argument
203 with open_repo_closing(repo) as r:
204 return r.do_commit(message=message, author=author,
208 def commit_tree(repo, tree, message=None, author=None, committer=None):
209 """Create a new commit object.
211 :param repo: Path to repository
212 :param tree: An existing tree object
213 :param author: Optional author name and email
214 :param committer: Optional committer name and email
216 with open_repo_closing(repo) as r:
217 return r.do_commit(message=message, tree=tree, committer=committer,
221 def init(path=".", bare=False):
222 """Create a new git repository.
224 :param path: Path to repository.
225 :param bare: Whether to create a bare repository.
226 :return: A Repo instance
228 if not os.path.exists(path):
232 return Repo.init_bare(path)
234 return Repo.init(path)
237 def clone(source, target=None, bare=False, checkout=None,
238 errstream=default_bytes_err_stream, outstream=None,
240 """Clone a local or remote git repository.
242 :param source: Path or URL for source repository
243 :param target: Path to target repository (optional)
244 :param bare: Whether or not to create a bare repository
245 :param errstream: Optional stream to write progress to
246 :param outstream: Optional stream to write progress to (deprecated)
247 :return: The new repository
249 if outstream is not None:
251 warnings.warn("outstream= has been deprecated in favour of errstream=.", DeprecationWarning,
253 errstream = outstream
256 checkout = (not bare)
257 if checkout and bare:
258 raise ValueError("checkout and bare are incompatible")
259 client, host_path = get_transport_and_path(source)
262 target = host_path.split("/")[-1]
264 if not os.path.exists(target):
268 r = Repo.init_bare(target)
270 r = Repo.init(target)
272 remote_refs = client.fetch(host_path, r,
273 determine_wants=r.object_store.determine_wants_all,
274 progress=errstream.write)
276 b'refs/remotes/' + origin,
277 {n[len(b'refs/heads/'):]: v for (n, v) in remote_refs.items()
278 if n.startswith(b'refs/heads/')})
281 {n[len(b'refs/tags/'):]: v for (n, v) in remote_refs.items()
282 if n.startswith(b'refs/tags/') and
283 not n.endswith(ANNOTATED_TAG_SUFFIX)})
284 r[b"HEAD"] = remote_refs[b"HEAD"]
286 errstream.write(b'Checking out HEAD\n')
295 def add(repo=".", paths=None):
296 """Add files to the staging area.
298 :param repo: Repository for the files
299 :param paths: Paths to add. No value passed stages all modified files.
301 # FIXME: Support patterns, directories.
302 with open_repo_closing(repo) as r:
304 # If nothing is specified, add all non-ignored files.
306 for dirpath, dirnames, filenames in os.walk(r.path):
307 # Skip .git and below.
308 if '.git' in dirnames:
309 dirnames.remove('.git')
310 for filename in filenames:
311 paths.append(os.path.join(dirpath[len(r.path)+1:], filename))
315 def rm(repo=".", paths=None):
316 """Remove files from the staging area.
318 :param repo: Repository for the files
319 :param paths: Paths to remove
321 with open_repo_closing(repo) as r:
322 index = r.open_index()
324 del index[p.encode(sys.getfilesystemencoding())]
328 def commit_decode(commit, contents, default_encoding=DEFAULT_ENCODING):
329 if commit.encoding is not None:
330 return contents.decode(commit.encoding, "replace")
331 return contents.decode(default_encoding, "replace")
334 def print_commit(commit, decode, outstream=sys.stdout):
335 """Write a human-readable commit log entry.
337 :param commit: A `Commit` object
338 :param outstream: A stream file to write to
340 outstream.write("-" * 50 + "\n")
341 outstream.write("commit: " + commit.id.decode('ascii') + "\n")
342 if len(commit.parents) > 1:
343 outstream.write("merge: " +
344 "...".join([c.decode('ascii') for c in commit.parents[1:]]) + "\n")
345 outstream.write("Author: " + decode(commit.author) + "\n")
346 if commit.author != commit.committer:
347 outstream.write("Committer: " + decode(commit.committer) + "\n")
349 time_tuple = time.gmtime(commit.author_time + commit.author_timezone)
350 time_str = time.strftime("%a %b %d %Y %H:%M:%S", time_tuple)
351 timezone_str = format_timezone(commit.author_timezone).decode('ascii')
352 outstream.write("Date: " + time_str + " " + timezone_str + "\n")
353 outstream.write("\n")
354 outstream.write(decode(commit.message) + "\n")
355 outstream.write("\n")
358 def print_tag(tag, decode, outstream=sys.stdout):
359 """Write a human-readable tag.
361 :param tag: A `Tag` object
362 :param decode: Function for decoding bytes to unicode string
363 :param outstream: A stream to write to
365 outstream.write("Tagger: " + decode(tag.tagger) + "\n")
366 outstream.write("Date: " + decode(tag.tag_time) + "\n")
367 outstream.write("\n")
368 outstream.write(decode(tag.message) + "\n")
369 outstream.write("\n")
372 def show_blob(repo, blob, decode, outstream=sys.stdout):
373 """Write a blob to a stream.
375 :param repo: A `Repo` object
376 :param blob: A `Blob` object
377 :param decode: Function for decoding bytes to unicode string
378 :param outstream: A stream file to write to
380 outstream.write(decode(blob.data))
383 def show_commit(repo, commit, decode, outstream=sys.stdout):
384 """Show a commit to a stream.
386 :param repo: A `Repo` object
387 :param commit: A `Commit` object
388 :param decode: Function for decoding bytes to unicode string
389 :param outstream: Stream to write to
391 print_commit(commit, decode=decode, outstream=outstream)
392 parent_commit = repo[commit.parents[0]]
393 write_tree_diff(outstream, repo.object_store, parent_commit.tree, commit.tree)
396 def show_tree(repo, tree, decode, outstream=sys.stdout):
397 """Print a tree to a stream.
399 :param repo: A `Repo` object
400 :param tree: A `Tree` object
401 :param decode: Function for decoding bytes to unicode string
402 :param outstream: Stream to write to
405 outstream.write(decode(n) + "\n")
408 def show_tag(repo, tag, decode, outstream=sys.stdout):
409 """Print a tag to a stream.
411 :param repo: A `Repo` object
412 :param tag: A `Tag` object
413 :param decode: Function for decoding bytes to unicode string
414 :param outstream: Stream to write to
416 print_tag(tag, decode, outstream)
417 show_object(repo, repo[tag.object[1]], outstream)
420 def show_object(repo, obj, decode, outstream):
424 b"commit": show_commit,
426 }[obj.type_name](repo, obj, decode, outstream)
429 def print_name_status(changes):
430 """Print a simple status summary, listing changed files.
432 for change in changes:
435 if type(change) is list:
437 if change.type == CHANGE_ADD:
438 path1 = change.new.path
441 elif change.type == CHANGE_DELETE:
442 path1 = change.old.path
445 elif change.type == CHANGE_MODIFY:
446 path1 = change.new.path
449 elif change.type in RENAME_CHANGE_TYPES:
450 path1 = change.old.path
451 path2 = change.new.path
452 if change.type == CHANGE_RENAME:
454 elif change.type == CHANGE_COPY:
456 yield '%-8s%-20s%-20s' % (kind, path1, path2)
459 def log(repo=".", paths=None, outstream=sys.stdout, max_entries=None,
460 reverse=False, name_status=False):
461 """Write commit logs.
463 :param repo: Path to repository
464 :param paths: Optional set of specific paths to print entries for
465 :param outstream: Stream to write log output to
466 :param reverse: Reverse order in which entries are printed
467 :param name_status: Print name status
468 :param max_entries: Optional maximum number of entries to display
470 with open_repo_closing(repo) as r:
471 walker = r.get_walker(
472 max_entries=max_entries, paths=paths, reverse=reverse)
474 decode = lambda x: commit_decode(entry.commit, x)
475 print_commit(entry.commit, decode, outstream)
477 outstream.writelines(
478 [l+'\n' for l in print_name_status(entry.changes())])
481 # TODO(jelmer): better default for encoding?
482 def show(repo=".", objects=None, outstream=sys.stdout,
483 default_encoding=DEFAULT_ENCODING):
484 """Print the changes in a commit.
486 :param repo: Path to repository
487 :param objects: Objects to show (defaults to [HEAD])
488 :param outstream: Stream to write to
489 :param default_encoding: Default encoding to use if none is set in the commit
493 if not isinstance(objects, list):
495 with open_repo_closing(repo) as r:
496 for objectish in objects:
497 o = parse_object(r, objectish)
498 if isinstance(o, Commit):
499 decode = lambda x: commit_decode(o, x, default_encoding)
501 decode = lambda x: x.decode(default_encoding)
502 show_object(r, o, decode, outstream)
505 def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout):
506 """Compares the content and mode of blobs found via two tree objects.
508 :param repo: Path to repository
509 :param old_tree: Id of old tree
510 :param new_tree: Id of new tree
511 :param outstream: Stream to write to
513 with open_repo_closing(repo) as r:
514 write_tree_diff(outstream, r.object_store, old_tree, new_tree)
517 def rev_list(repo, commits, outstream=sys.stdout):
518 """Lists commit objects in reverse chronological order.
520 :param repo: Path to repository
521 :param commits: Commits over which to iterate
522 :param outstream: Stream to write to
524 with open_repo_closing(repo) as r:
525 for entry in r.get_walker(include=[r[c].id for c in commits]):
526 outstream.write(entry.commit.id + b"\n")
529 def tag(*args, **kwargs):
531 warnings.warn("tag has been deprecated in favour of tag_create.", DeprecationWarning)
532 return tag_create(*args, **kwargs)
535 def tag_create(repo, tag, author=None, message=None, annotated=False,
536 objectish="HEAD", tag_time=None, tag_timezone=None):
537 """Creates a tag in git via dulwich calls:
539 :param repo: Path to repository
540 :param tag: tag string
541 :param author: tag author (optional, if annotated is set)
542 :param message: tag message (optional)
543 :param annotated: whether to create an annotated tag
544 :param objectish: object the tag should point at, defaults to HEAD
545 :param tag_time: Optional time for annotated tag
546 :param tag_timezone: Optional timezone for annotated tag
549 with open_repo_closing(repo) as r:
550 object = parse_object(r, objectish)
553 # Create the tag object
556 # TODO(jelmer): Don't use repo private method.
557 author = r._get_user_identity()
558 tag_obj.tagger = author
559 tag_obj.message = message
561 tag_obj.object = (type(object), object.id)
563 tag_time = int(time.time())
564 tag_obj.tag_time = tag_time
565 if tag_timezone is None:
566 # TODO(jelmer) Use current user timezone rather than UTC
568 elif isinstance(tag_timezone, str):
569 tag_timezone = parse_timezone(tag_timezone)
570 tag_obj.tag_timezone = tag_timezone
571 r.object_store.add_object(tag_obj)
576 r.refs[b'refs/tags/' + tag] = tag_id
579 def list_tags(*args, **kwargs):
581 warnings.warn("list_tags has been deprecated in favour of tag_list.", DeprecationWarning)
582 return tag_list(*args, **kwargs)
585 def tag_list(repo, outstream=sys.stdout):
588 :param repo: Path to repository
589 :param outstream: Stream to write tags to
591 with open_repo_closing(repo) as r:
592 tags = list(r.refs.as_dict(b"refs/tags"))
597 def tag_delete(repo, name):
600 :param repo: Path to repository
601 :param name: Name of tag to remove
603 with open_repo_closing(repo) as r:
604 if isinstance(name, bytes):
606 elif isinstance(name, list):
609 raise TypeError("Unexpected tag name type %r" % name)
611 del r.refs[b"refs/tags/" + name]
614 def reset(repo, mode, committish="HEAD"):
615 """Reset current HEAD to the specified state.
617 :param repo: Path to repository
618 :param mode: Mode ("hard", "soft", "mixed")
622 raise ValueError("hard is the only mode currently supported")
624 with open_repo_closing(repo) as r:
625 tree = r[committish].tree
629 def push(repo, remote_location, refspecs=None,
630 outstream=default_bytes_out_stream, errstream=default_bytes_err_stream):
631 """Remote push with dulwich via dulwich.client
633 :param repo: Path to repository
634 :param remote_location: Location of the remote
635 :param refspecs: relative path to the refs to push to remote
636 :param outstream: A stream file to write output
637 :param errstream: A stream file to write errors
641 with open_repo_closing(repo) as r:
643 # Get the client and path
644 client, path = get_transport_and_path(remote_location)
648 def update_refs(refs):
649 selected_refs.extend(parse_reftuples(r.refs, refs, refspecs))
651 # TODO: Handle selected_refs == {None: None}
652 for (lh, rh, force) in selected_refs:
654 new_refs[rh] = ZERO_SHA
656 new_refs[rh] = r.refs[lh]
659 err_encoding = getattr(errstream, 'encoding', None) or DEFAULT_ENCODING
660 remote_location_bytes = client.get_url(path).encode(err_encoding)
662 client.send_pack(path, update_refs,
663 r.object_store.generate_pack_contents, progress=errstream.write)
664 errstream.write(b"Push to " + remote_location_bytes +
666 except (UpdateRefsError, SendPackError) as e:
667 errstream.write(b"Push to " + remote_location_bytes +
668 b" failed -> " + e.message.encode(err_encoding) +
672 def pull(repo, remote_location, refspecs=None,
673 outstream=default_bytes_out_stream, errstream=default_bytes_err_stream):
674 """Pull from remote via dulwich.client
676 :param repo: Path to repository
677 :param remote_location: Location of the remote
678 :param refspec: refspecs to fetch
679 :param outstream: A stream file to write to output
680 :param errstream: A stream file to write to errors
683 with open_repo_closing(repo) as r:
687 def determine_wants(remote_refs):
688 selected_refs.extend(parse_reftuples(remote_refs, r.refs, refspecs))
689 return [remote_refs[lh] for (lh, rh, force) in selected_refs]
690 client, path = get_transport_and_path(remote_location)
691 remote_refs = client.fetch(path, r, progress=errstream.write,
692 determine_wants=determine_wants)
693 for (lh, rh, force) in selected_refs:
694 r.refs[rh] = remote_refs[lh]
696 r[b'HEAD'] = remote_refs[selected_refs[0][1]]
698 # Perform 'git checkout .' - syncs staged changes
699 tree = r[b"HEAD"].tree
703 def status(repo="."):
704 """Returns staged, unstaged, and untracked changes relative to the HEAD.
706 :param repo: Path to repository or repository object
707 :return: GitStatus tuple,
708 staged - list of staged paths (diff index/HEAD)
709 unstaged - list of unstaged paths (diff index/working-tree)
710 untracked - list of untracked, un-ignored & non-.git paths
712 with open_repo_closing(repo) as r:
713 # 1. Get status of staged
714 tracked_changes = get_tree_changes(r)
715 # 2. Get status of unstaged
716 unstaged_changes = list(get_unstaged_changes(r.open_index(), r.path))
717 # TODO - Status of untracked - add untracked changes, need gitignore.
718 untracked_changes = []
719 return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
722 def get_tree_changes(repo):
723 """Return add/delete/modify changes to tree by comparing index to HEAD.
725 :param repo: repo path or object
726 :return: dict with lists for each type of change
728 with open_repo_closing(repo) as r:
729 index = r.open_index()
731 # Compares the Index to the HEAD & determines changes
732 # Iterate through the changes and report add/delete/modify
733 # TODO: call out to dulwich.diff_tree somehow.
740 tree_id = r[b'HEAD'].tree
744 for change in index.changes_from_tree(r.object_store, tree_id):
746 tracked_changes['add'].append(change[0][1])
747 elif not change[0][1]:
748 tracked_changes['delete'].append(change[0][0])
749 elif change[0][0] == change[0][1]:
750 tracked_changes['modify'].append(change[0][0])
752 raise AssertionError('git mv ops not yet supported')
753 return tracked_changes
756 def daemon(path=".", address=None, port=None):
757 """Run a daemon serving Git requests over TCP/IP.
759 :param path: Path to the directory to serve.
760 :param address: Optional address to listen on (defaults to ::)
761 :param port: Optional port to listen on (defaults to TCP_GIT_PORT)
763 # TODO(jelmer): Support git-daemon-export-ok and --export-all.
764 backend = FileSystemBackend(path)
765 server = TCPGitServer(backend, address, port)
766 server.serve_forever()
769 def web_daemon(path=".", address=None, port=None):
770 """Run a daemon serving Git requests over HTTP.
772 :param path: Path to the directory to serve
773 :param address: Optional address to listen on (defaults to ::)
774 :param port: Optional port to listen on (defaults to 80)
776 from dulwich.web import (
779 WSGIRequestHandlerLogger,
782 backend = FileSystemBackend(path)
783 app = make_wsgi_chain(backend)
784 server = make_server(address, port, app,
785 handler_class=WSGIRequestHandlerLogger,
786 server_class=WSGIServerLogger)
787 server.serve_forever()
790 def upload_pack(path=".", inf=None, outf=None):
791 """Upload a pack file after negotiating its contents using smart protocol.
793 :param path: Path to the repository
794 :param inf: Input stream to communicate with client
795 :param outf: Output stream to communicate with client
798 outf = getattr(sys.stdout, 'buffer', sys.stdout)
800 inf = getattr(sys.stdin, 'buffer', sys.stdin)
801 backend = FileSystemBackend(path)
805 proto = Protocol(inf.read, send_fn)
806 handler = UploadPackHandler(backend, [path], proto)
807 # FIXME: Catch exceptions and write a single-line summary to outf.
812 def receive_pack(path=".", inf=None, outf=None):
813 """Receive a pack file after negotiating its contents using smart protocol.
815 :param path: Path to the repository
816 :param inf: Input stream to communicate with client
817 :param outf: Output stream to communicate with client
820 outf = getattr(sys.stdout, 'buffer', sys.stdout)
822 inf = getattr(sys.stdin, 'buffer', sys.stdin)
823 backend = FileSystemBackend(path)
827 proto = Protocol(inf.read, send_fn)
828 handler = ReceivePackHandler(backend, [path], proto)
829 # FIXME: Catch exceptions and write a single-line summary to outf.
834 def branch_delete(repo, name):
837 :param repo: Path to the repository
838 :param name: Name of the branch
840 with open_repo_closing(repo) as r:
841 if isinstance(name, bytes):
843 elif isinstance(name, list):
846 raise TypeError("Unexpected branch name type %r" % name)
848 del r.refs[b"refs/heads/" + name]
851 def branch_create(repo, name, objectish=None, force=False):
854 :param repo: Path to the repository
855 :param name: Name of the new branch
856 :param objectish: Target object to point new branch at (defaults to HEAD)
857 :param force: Force creation of branch, even if it already exists
859 with open_repo_closing(repo) as r:
860 if isinstance(name, bytes):
862 elif isinstance(name, list):
865 raise TypeError("Unexpected branch name type %r" % name)
866 if objectish is None:
868 object = parse_object(r, objectish)
869 refname = b"refs/heads/" + name
870 if refname in r.refs and not force:
871 raise KeyError("Branch with name %s already exists." % name)
872 r.refs[refname] = object.id
875 def branch_list(repo):
876 """List all branches.
878 :param repo: Path to the repository
880 with open_repo_closing(repo) as r:
881 return r.refs.keys(base=b"refs/heads/")
884 def fetch(repo, remote_location, outstream=sys.stdout,
885 errstream=default_bytes_err_stream):
886 """Fetch objects from a remote server.
888 :param repo: Path to the repository
889 :param remote_location: String identifying a remote server
890 :param outstream: Output stream (defaults to stdout)
891 :param errstream: Error stream (defaults to stderr)
892 :return: Dictionary with refs on the remote
894 with open_repo_closing(repo) as r:
895 client, path = get_transport_and_path(remote_location)
896 remote_refs = client.fetch(path, r, progress=errstream.write)
900 def ls_remote(remote):
901 """List the refs in a remote.
903 :param remote: Remote repository location
904 :return: Dictionary with remote refs
906 client, host_path = get_transport_and_path(remote)
907 return client.get_refs(host_path)
911 """Repack loose files in a repository.
913 Currently this only packs loose objects.
915 :param repo: Path to the repository
917 with open_repo_closing(repo) as r:
918 r.object_store.pack_loose_objects()
921 def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None):
922 """Pack objects into a file.
924 :param repo: Path to the repository
925 :param object_ids: List of object ids to write
926 :param packf: File-like object to write to
927 :param idxf: File-like object to write to (can be None)
929 with open_repo_closing(repo) as r:
930 entries, data_sum = write_pack_objects(
932 r.object_store.iter_shas((oid, None) for oid in object_ids),
933 delta_window_size=delta_window_size)
935 entries = [(k, v[0], v[1]) for (k, v) in entries.items()]
937 write_pack_index(idxf, entries, data_sum)
940 def ls_tree(repo, tree_ish=None, outstream=sys.stdout, recursive=False,
942 """List contents of a tree.
944 :param repo: Path to the repository
945 :param tree_ish: Tree id to list
946 :param outstream: Output stream (defaults to stdout)
947 :param recursive: Whether to recursively list files
948 :param name_only: Only print item name
950 def list_tree(store, treeid, base):
951 for (name, mode, sha) in store[treeid].iteritems():
953 name = posixpath.join(base, name)
955 outstream.write(name + b"\n")
957 outstream.write(pretty_format_tree_entry(name, mode, sha))
958 if stat.S_ISDIR(mode):
959 list_tree(store, sha, name)
962 with open_repo_closing(repo) as r:
965 list_tree(r.object_store, treeid, "")