Replace / with \ on windows.
[jelmer/dulwich.git] / dulwich / porcelain.py
1 # porcelain.py -- Porcelain-like layer on top of Dulwich
2 # Copyright (C) 2013 Jelmer Vernooij <jelmer@jelmer.uk>
3 #
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.
8 #
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.
14 #
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.
19 #
20
21 """Simple wrapper that provides porcelain-like functions on top of Dulwich.
22
23 Currently implemented:
24  * archive
25  * add
26  * branch{_create,_delete,_list}
27  * check-ignore
28  * checkout
29  * clone
30  * commit
31  * commit-tree
32  * daemon
33  * describe
34  * diff-tree
35  * fetch
36  * init
37  * ls-files
38  * ls-remote
39  * ls-tree
40  * pull
41  * push
42  * rm
43  * remote{_add}
44  * receive-pack
45  * reset
46  * rev-list
47  * tag{_create,_delete,_list}
48  * upload-pack
49  * update-server-info
50  * status
51  * symbolic-ref
52
53 These functions are meant to behave similarly to the git subcommands.
54 Differences in behaviour are considered bugs.
55
56 Functions should generally accept both unicode strings and bytestrings
57 """
58
59 from collections import namedtuple
60 from contextlib import (
61     closing,
62     contextmanager,
63 )
64 from io import BytesIO
65 import datetime
66 import os
67 import posixpath
68 import stat
69 import sys
70 import time
71
72 from dulwich.archive import (
73     tar_stream,
74     )
75 from dulwich.client import (
76     get_transport_and_path,
77     )
78 from dulwich.config import (
79     StackedConfig,
80     )
81 from dulwich.diff_tree import (
82     CHANGE_ADD,
83     CHANGE_DELETE,
84     CHANGE_MODIFY,
85     CHANGE_RENAME,
86     CHANGE_COPY,
87     RENAME_CHANGE_TYPES,
88     )
89 from dulwich.errors import (
90     SendPackError,
91     UpdateRefsError,
92     )
93 from dulwich.ignore import IgnoreFilterManager
94 from dulwich.index import (
95     blob_from_path_and_stat,
96     get_unstaged_changes,
97     )
98 from dulwich.object_store import (
99     tree_lookup_path,
100     )
101 from dulwich.objects import (
102     Commit,
103     Tag,
104     format_timezone,
105     parse_timezone,
106     pretty_format_tree_entry,
107     )
108 from dulwich.objectspec import (
109     parse_commit,
110     parse_object,
111     parse_ref,
112     parse_reftuples,
113     parse_tree,
114     )
115 from dulwich.pack import (
116     write_pack_index,
117     write_pack_objects,
118     )
119 from dulwich.patch import write_tree_diff
120 from dulwich.protocol import (
121     Protocol,
122     ZERO_SHA,
123     )
124 from dulwich.refs import (
125     ANNOTATED_TAG_SUFFIX,
126     strip_peeled_refs,
127 )
128 from dulwich.repo import (BaseRepo, Repo)
129 from dulwich.server import (
130     FileSystemBackend,
131     TCPGitServer,
132     ReceivePackHandler,
133     UploadPackHandler,
134     update_server_info as server_update_server_info,
135     )
136
137
138 # Module level tuple definition for status output
139 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
140
141
142 default_bytes_out_stream = getattr(sys.stdout, 'buffer', sys.stdout)
143 default_bytes_err_stream = getattr(sys.stderr, 'buffer', sys.stderr)
144
145
146 DEFAULT_ENCODING = 'utf-8'
147
148
149 class RemoteExists(Exception):
150     """Raised when the remote already exists."""
151
152
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):
156         return path_or_repo
157     return Repo(path_or_repo)
158
159
160 @contextmanager
161 def _noop_context_manager(obj):
162     """Context manager that has the same api as closing but does nothing."""
163     yield obj
164
165
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.
170     """
171     if isinstance(path_or_repo, BaseRepo):
172         return _noop_context_manager(path_or_repo)
173     return closing(Repo(path_or_repo))
174
175
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
178     the repository root.
179
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
183     """
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'/')
193     return treepath
194
195
196 def archive(repo, committish=None, outstream=default_bytes_out_stream,
197             errstream=default_bytes_err_stream):
198     """Create an archive.
199
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)
204     """
205
206     if committish is None:
207         committish = "HEAD"
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],
212                 c.commit_time):
213             outstream.write(chunk)
214
215
216 def update_server_info(repo="."):
217     """Update server info files for a repository.
218
219     :param repo: path to the repository
220     """
221     with open_repo_closing(repo) as r:
222         server_update_server_info(r)
223
224
225 def symbolic_ref(repo, ref_name, force=False):
226     """Set git symbolic ref into HEAD.
227
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
231     """
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)
237
238
239 def commit(repo=".", message=None, author=None, committer=None, encoding=None):
240     """Create a new commit.
241
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
247     """
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:
257         return r.do_commit(
258                 message=message, author=author, committer=committer,
259                 encoding=encoding)
260
261
262 def commit_tree(repo, tree, message=None, author=None, committer=None):
263     """Create a new commit object.
264
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
269     """
270     with open_repo_closing(repo) as r:
271         return r.do_commit(
272             message=message, tree=tree, committer=committer, author=author)
273
274
275 def init(path=".", bare=False):
276     """Create a new git repository.
277
278     :param path: Path to repository.
279     :param bare: Whether to create a bare repository.
280     :return: A Repo instance
281     """
282     if not os.path.exists(path):
283         os.mkdir(path)
284
285     if bare:
286         return Repo.init_bare(path)
287     else:
288         return Repo.init(path)
289
290
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.
295
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
304     """
305     # TODO(jelmer): This code overlaps quite a bit with Repo.clone
306     if outstream is not None:
307         import warnings
308         warnings.warn(
309             "outstream= has been deprecated in favour of errstream=.",
310             DeprecationWarning, stacklevel=3)
311         errstream = outstream
312
313     if checkout is None:
314         checkout = (not bare)
315     if checkout and bare:
316         raise ValueError("checkout and bare are incompatible")
317
318     if target is None:
319         target = source.split("/")[-1]
320
321     if not os.path.exists(target):
322         os.mkdir(target)
323
324     if bare:
325         r = Repo.init_bare(target)
326     else:
327         r = Repo.init(target)
328
329     reflog_message = b'clone: from ' + source.encode('utf-8')
330     try:
331         fetch_result = fetch(
332             r, source, origin, errstream=errstream, message=reflog_message,
333             **kwargs)
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)
338         target_config.set(
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
344         try:
345             head = r[fetch_result[b'HEAD']]
346         except KeyError:
347             head = None
348         else:
349             r[b'HEAD'] = head.id
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:
354         r.close()
355         raise
356
357     return r
358
359
360 def add(repo=".", paths=None):
361     """Add files to the staging area.
362
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
366     """
367     ignored = set()
368     with open_repo_closing(repo) as r:
369         ignore_manager = IgnoreFilterManager.from_repo(r)
370         if not paths:
371             paths = list(
372                 get_untracked_paths(os.getcwd(), r.path, r.open_index()))
373         relpaths = []
374         if not isinstance(paths, list):
375             paths = [paths]
376         for p in paths:
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):
382                 ignored.add(relpath)
383                 continue
384             relpaths.append(relpath)
385         r.stage(relpaths)
386     return (relpaths, ignored)
387
388
389 def remove(repo=".", paths=None, cached=False):
390     """Remove files from the staging area.
391
392     :param repo: Repository for the files
393     :param paths: Paths to remove
394     """
395     with open_repo_closing(repo) as r:
396         index = r.open_index()
397         for p in paths:
398             full_path = os.path.abspath(p).encode(sys.getfilesystemencoding())
399             tree_path = path_to_tree_path(r.path, p)
400             try:
401                 index_sha = index[tree_path].sha
402             except KeyError:
403                 raise Exception('%s did not match any files' % p)
404
405             if not cached:
406                 try:
407                     st = os.lstat(full_path)
408                 except OSError:
409                     pass
410                 else:
411                     try:
412                         blob = blob_from_path_and_stat(full_path, st)
413                     except IOError:
414                         pass
415                     else:
416                         try:
417                             committed_sha = tree_lookup_path(
418                                 r.__getitem__, r[r.head()].tree, tree_path)[1]
419                         except KeyError:
420                             committed_sha = None
421
422                         if blob.id != index_sha and index_sha != committed_sha:
423                             raise Exception(
424                                 'file has staged content differing '
425                                 'from both the file and head: %s' % p)
426
427                         if index_sha != committed_sha:
428                             raise Exception(
429                                 'file has staged changes: %s' % p)
430                         os.remove(full_path)
431             del index[tree_path]
432         index.write()
433
434
435 rm = remove
436
437
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")
442
443
444 def print_commit(commit, decode, outstream=sys.stdout):
445     """Write a human-readable commit log entry.
446
447     :param commit: A `Commit` object
448     :param outstream: A stream file to write to
449     """
450     outstream.write("-" * 50 + "\n")
451     outstream.write("commit: " + commit.id.decode('ascii') + "\n")
452     if len(commit.parents) > 1:
453         outstream.write(
454             "merge: " +
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")
459
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")
467
468
469 def print_tag(tag, decode, outstream=sys.stdout):
470     """Write a human-readable tag.
471
472     :param tag: A `Tag` object
473     :param decode: Function for decoding bytes to unicode string
474     :param outstream: A stream to write to
475     """
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")
481
482
483 def show_blob(repo, blob, decode, outstream=sys.stdout):
484     """Write a blob to a stream.
485
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
490     """
491     outstream.write(decode(blob.data))
492
493
494 def show_commit(repo, commit, decode, outstream=sys.stdout):
495     """Show a commit to a stream.
496
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
501     """
502     print_commit(commit, decode=decode, outstream=outstream)
503     if commit.parents:
504         parent_commit = repo[commit.parents[0]]
505         base_tree = parent_commit.tree
506     else:
507         base_tree = None
508     diffstream = BytesIO()
509     write_tree_diff(
510         diffstream,
511         repo.object_store, base_tree, commit.tree)
512     diffstream.seek(0)
513     outstream.write(
514         diffstream.getvalue().decode(
515                 commit.encoding or DEFAULT_ENCODING, 'replace'))
516
517
518 def show_tree(repo, tree, decode, outstream=sys.stdout):
519     """Print a tree to a stream.
520
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
525     """
526     for n in tree:
527         outstream.write(decode(n) + "\n")
528
529
530 def show_tag(repo, tag, decode, outstream=sys.stdout):
531     """Print a tag to a stream.
532
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
537     """
538     print_tag(tag, decode, outstream)
539     show_object(repo, repo[tag.object[1]], outstream)
540
541
542 def show_object(repo, obj, decode, outstream):
543     return {
544         b"tree": show_tree,
545         b"blob": show_blob,
546         b"commit": show_commit,
547         b"tag": show_tag,
548             }[obj.type_name](repo, obj, decode, outstream)
549
550
551 def print_name_status(changes):
552     """Print a simple status summary, listing changed files.
553     """
554     for change in changes:
555         if not change:
556             continue
557         if isinstance(change, list):
558             change = change[0]
559         if change.type == CHANGE_ADD:
560             path1 = change.new.path
561             path2 = ''
562             kind = 'A'
563         elif change.type == CHANGE_DELETE:
564             path1 = change.old.path
565             path2 = ''
566             kind = 'D'
567         elif change.type == CHANGE_MODIFY:
568             path1 = change.new.path
569             path2 = ''
570             kind = 'M'
571         elif change.type in RENAME_CHANGE_TYPES:
572             path1 = change.old.path
573             path2 = change.new.path
574             if change.type == CHANGE_RENAME:
575                 kind = 'R'
576             elif change.type == CHANGE_COPY:
577                 kind = 'C'
578         yield '%-8s%-20s%-20s' % (kind, path1, path2)
579
580
581 def log(repo=".", paths=None, outstream=sys.stdout, max_entries=None,
582         reverse=False, name_status=False):
583     """Write commit logs.
584
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
591     """
592     with open_repo_closing(repo) as r:
593         walker = r.get_walker(
594             max_entries=max_entries, paths=paths, reverse=reverse)
595         for entry in walker:
596             def decode(x):
597                 return commit_decode(entry.commit, x)
598             print_commit(entry.commit, decode, outstream)
599             if name_status:
600                 outstream.writelines(
601                     [l+'\n' for l in print_name_status(entry.changes())])
602
603
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.
608
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
613         commit
614     """
615     if objects is None:
616         objects = ["HEAD"]
617     if not isinstance(objects, list):
618         objects = [objects]
619     with open_repo_closing(repo) as r:
620         for objectish in objects:
621             o = parse_object(r, objectish)
622             if isinstance(o, Commit):
623                 def decode(x):
624                     return commit_decode(o, x, default_encoding)
625             else:
626                 def decode(x):
627                     return x.decode(default_encoding)
628             show_object(r, o, decode, outstream)
629
630
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.
633
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
638     """
639     with open_repo_closing(repo) as r:
640         write_tree_diff(outstream, r.object_store, old_tree, new_tree)
641
642
643 def rev_list(repo, commits, outstream=sys.stdout):
644     """Lists commit objects in reverse chronological order.
645
646     :param repo: Path to repository
647     :param commits: Commits over which to iterate
648     :param outstream: Stream to write to
649     """
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")
653
654
655 def tag(*args, **kwargs):
656     import warnings
657     warnings.warn("tag has been deprecated in favour of tag_create.",
658                   DeprecationWarning)
659     return tag_create(*args, **kwargs)
660
661
662 def tag_create(
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:
666
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
675     """
676
677     with open_repo_closing(repo) as r:
678         object = parse_object(r, objectish)
679
680         if annotated:
681             # Create the tag object
682             tag_obj = Tag()
683             if author is None:
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
688             tag_obj.name = tag
689             tag_obj.object = (type(object), object.id)
690             if tag_time is None:
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
695                 tag_timezone = 0
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)
700             tag_id = tag_obj.id
701         else:
702             tag_id = object.id
703
704         r.refs[_make_tag_ref(tag)] = tag_id
705
706
707 def list_tags(*args, **kwargs):
708     import warnings
709     warnings.warn("list_tags has been deprecated in favour of tag_list.",
710                   DeprecationWarning)
711     return tag_list(*args, **kwargs)
712
713
714 def tag_list(repo, outstream=sys.stdout):
715     """List all tags.
716
717     :param repo: Path to repository
718     :param outstream: Stream to write tags to
719     """
720     with open_repo_closing(repo) as r:
721         tags = sorted(r.refs.as_dict(b"refs/tags"))
722         return tags
723
724
725 def tag_delete(repo, name):
726     """Remove a tag.
727
728     :param repo: Path to repository
729     :param name: Name of tag to remove
730     """
731     with open_repo_closing(repo) as r:
732         if isinstance(name, bytes):
733             names = [name]
734         elif isinstance(name, list):
735             names = name
736         else:
737             raise TypeError("Unexpected tag name type %r" % name)
738         for name in names:
739             del r.refs[_make_tag_ref(name)]
740
741
742 def reset(repo, mode, treeish="HEAD"):
743     """Reset current HEAD to the specified state.
744
745     :param repo: Path to repository
746     :param mode: Mode ("hard", "soft", "mixed")
747     :param treeish: Treeish to reset to
748     """
749
750     if mode != "hard":
751         raise ValueError("hard is the only mode currently supported")
752
753     with open_repo_closing(repo) as r:
754         tree = parse_tree(r, treeish)
755         r.reset_index(tree.id)
756
757
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
762
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
768     """
769
770     # Open the repo
771     with open_repo_closing(repo) as r:
772
773         # Get the client and path
774         client, path = get_transport_and_path(
775                 remote_location, config=r.get_config_stack(), **kwargs)
776
777         selected_refs = []
778
779         def update_refs(refs):
780             selected_refs.extend(parse_reftuples(r.refs, refs, refspecs))
781             new_refs = {}
782             # TODO: Handle selected_refs == {None: None}
783             for (lh, rh, force) in selected_refs:
784                 if lh is None:
785                     new_refs[rh] = ZERO_SHA
786                 else:
787                     new_refs[rh] = r.refs[lh]
788             return new_refs
789
790         err_encoding = getattr(errstream, 'encoding', None) or DEFAULT_ENCODING
791         remote_location_bytes = client.get_url(path).encode(err_encoding)
792         try:
793             client.send_pack(
794                 path, update_refs,
795                 generate_pack_data=r.object_store.generate_pack_data,
796                 progress=errstream.write)
797             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) +
802                             b"\n")
803
804
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
809
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
815     """
816     # Open the repo
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")
822         if refspecs is None:
823             refspecs = [b"HEAD"]
824         selected_refs = []
825
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]
836         if selected_refs:
837             r[b'HEAD'] = fetch_result.refs[selected_refs[0][1]]
838
839         # Perform 'git checkout .' - syncs staged changes
840         tree = r[b"HEAD"].tree
841         r.reset_index(tree=tree)
842
843
844 def status(repo=".", ignored=False):
845     """Returns staged, unstaged, and untracked changes relative to the HEAD.
846
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
853     """
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)
862         if ignored:
863             untracked_changes = list(untracked_paths)
864         else:
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)
869
870
871 def get_untracked_paths(frompath, basepath, index):
872     """Get untracked paths.
873
874     ;param frompath: Path to walk
875     :param basepath: Path to compare to
876     :param index: Index to check against
877     """
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:
884                 continue
885         if '.git' in filenames:
886             filenames.remove('.git')
887             if dirpath != basepath:
888                 continue
889         for filename in filenames:
890             ap = os.path.join(dirpath, filename)
891             ip = path_to_tree_path(basepath, ap)
892             if ip not in index:
893                 yield os.path.relpath(ap, frompath)
894
895
896 def get_tree_changes(repo):
897     """Return add/delete/modify changes to tree by comparing index to HEAD.
898
899     :param repo: repo path or object
900     :return: dict with lists for each type of change
901     """
902     with open_repo_closing(repo) as r:
903         index = r.open_index()
904
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.
908         tracked_changes = {
909             'add': [],
910             'delete': [],
911             'modify': [],
912         }
913         try:
914             tree_id = r[b'HEAD'].tree
915         except KeyError:
916             tree_id = None
917
918         for change in index.changes_from_tree(r.object_store, tree_id):
919             if not change[0][0]:
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])
925             else:
926                 raise AssertionError('git mv ops not yet supported')
927         return tracked_changes
928
929
930 def daemon(path=".", address=None, port=None):
931     """Run a daemon serving Git requests over TCP/IP.
932
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)
936     """
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()
941
942
943 def web_daemon(path=".", address=None, port=None):
944     """Run a daemon serving Git requests over HTTP.
945
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)
949     """
950     from dulwich.web import (
951         make_wsgi_chain,
952         make_server,
953         WSGIRequestHandlerLogger,
954         WSGIServerLogger)
955
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()
962
963
964 def upload_pack(path=".", inf=None, outf=None):
965     """Upload a pack file after negotiating its contents using smart protocol.
966
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
970     """
971     if outf is None:
972         outf = getattr(sys.stdout, 'buffer', sys.stdout)
973     if inf is None:
974         inf = getattr(sys.stdin, 'buffer', sys.stdin)
975     path = os.path.expanduser(path)
976     backend = FileSystemBackend(path)
977
978     def send_fn(data):
979         outf.write(data)
980         outf.flush()
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.
984     handler.handle()
985     return 0
986
987
988 def receive_pack(path=".", inf=None, outf=None):
989     """Receive a pack file after negotiating its contents using smart protocol.
990
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
994     """
995     if outf is None:
996         outf = getattr(sys.stdout, 'buffer', sys.stdout)
997     if inf is None:
998         inf = getattr(sys.stdin, 'buffer', sys.stdin)
999     path = os.path.expanduser(path)
1000     backend = FileSystemBackend(path)
1001
1002     def send_fn(data):
1003         outf.write(data)
1004         outf.flush()
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.
1008     handler.handle()
1009     return 0
1010
1011
1012 def _make_branch_ref(name):
1013     if getattr(name, 'encode', None):
1014         name = name.encode(DEFAULT_ENCODING)
1015     return b"refs/heads/" + name
1016
1017
1018 def _make_tag_ref(name):
1019     if getattr(name, 'encode', None):
1020         name = name.encode(DEFAULT_ENCODING)
1021     return b"refs/tags/" + name
1022
1023
1024 def branch_delete(repo, name):
1025     """Delete a branch.
1026
1027     :param repo: Path to the repository
1028     :param name: Name of the branch
1029     """
1030     with open_repo_closing(repo) as r:
1031         if isinstance(name, list):
1032             names = name
1033         else:
1034             names = [name]
1035         for name in names:
1036             del r.refs[_make_branch_ref(name)]
1037
1038
1039 def branch_create(repo, name, objectish=None, force=False):
1040     """Create a branch.
1041
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
1046     """
1047     with open_repo_closing(repo) as r:
1048         if objectish is None:
1049             objectish = "HEAD"
1050         object = parse_object(r, objectish)
1051         refname = _make_branch_ref(name)
1052         ref_message = b"branch: Created from " + objectish.encode('utf-8')
1053         if force:
1054             r.refs.set_if_equals(refname, None, object.id, message=ref_message)
1055         else:
1056             if not r.refs.add_if_new(refname, object.id, message=ref_message):
1057                 raise KeyError("Branch with name %s already exists." % name)
1058
1059
1060 def branch_list(repo):
1061     """List all branches.
1062
1063     :param repo: Path to the repository
1064     """
1065     with open_repo_closing(repo) as r:
1066         return r.refs.keys(base=b"refs/heads/")
1067
1068
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.
1072
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
1080     """
1081     if message is None:
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)
1088         branches = {
1089             n[len(b'refs/heads/'):]: v for (n, v) in stripped_refs.items()
1090             if n.startswith(b'refs/heads/')}
1091         r.refs.import_refs(
1092             b'refs/remotes/' + remote_name, branches, message=message)
1093         tags = {
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
1099
1100
1101 def ls_remote(remote, config=None, **kwargs):
1102     """List the refs in a remote.
1103
1104     :param remote: Remote repository location
1105     :param config: Configuration to use
1106     :return: Dictionary with remote refs
1107     """
1108     if config is None:
1109         config = StackedConfig.default()
1110     client, host_path = get_transport_and_path(remote, config=config, **kwargs)
1111     return client.get_refs(host_path)
1112
1113
1114 def repack(repo):
1115     """Repack loose files in a repository.
1116
1117     Currently this only packs loose objects.
1118
1119     :param repo: Path to the repository
1120     """
1121     with open_repo_closing(repo) as r:
1122         r.object_store.pack_loose_objects()
1123
1124
1125 def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None):
1126     """Pack objects into a file.
1127
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)
1132     """
1133     with open_repo_closing(repo) as r:
1134         entries, data_sum = write_pack_objects(
1135             packf,
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)
1141
1142
1143 def ls_tree(repo, treeish=b"HEAD", outstream=sys.stdout, recursive=False,
1144             name_only=False):
1145     """List contents of a tree.
1146
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
1152     """
1153     def list_tree(store, treeid, base):
1154         for (name, mode, sha) in store[treeid].iteritems():
1155             if base:
1156                 name = posixpath.join(base, name)
1157             if name_only:
1158                 outstream.write(name + b"\n")
1159             else:
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, "")
1166
1167
1168 def remote_add(repo, name, url):
1169     """Add a remote.
1170
1171     :param repo: Path to the repository
1172     :param name: Remote name
1173     :param url: Remote URL
1174     """
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:
1180         c = r.get_config()
1181         section = (b'remote', name)
1182         if c.has_section(section):
1183             raise RemoteExists(section)
1184         c.set(section, b"url", url)
1185         c.write_to_path()
1186
1187
1188 def check_ignore(repo, paths, no_index=False):
1189     """Debug gitignore files.
1190
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
1195     """
1196     with open_repo_closing(repo) as r:
1197         index = r.open_index()
1198         ignore_manager = IgnoreFilterManager.from_repo(r)
1199         for path in paths:
1200             if not no_index and path_to_tree_path(r.path, path) in index:
1201                 continue
1202             if os.path.isabs(path):
1203                 path = os.path.relpath(path, r.path)
1204             if ignore_manager.is_ignored(path):
1205                 yield path
1206
1207
1208 def update_head(repo, target, detached=False, new_branch=None):
1209     """Update HEAD to point at a new branch/commit.
1210
1211     Note that this does not actually update the working tree.
1212
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
1217     """
1218     with open_repo_closing(repo) as r:
1219         if new_branch is not None:
1220             to_set = _make_branch_ref(new_branch)
1221         else:
1222             to_set = b"HEAD"
1223         if detached:
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
1226             # necessary.
1227             del r.refs[to_set]
1228             r.refs[to_set] = parse_commit(r, target).id
1229         else:
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)
1233
1234
1235 def check_mailmap(repo, contact):
1236     """Check canonical name and email of contact.
1237
1238     :param repo: Path to the repository
1239     :param contact: Contact name and/or email
1240     :return: Canonical contact data
1241     """
1242     with open_repo_closing(repo) as r:
1243         from dulwich.mailmap import Mailmap
1244         import errno
1245         try:
1246             mailmap = Mailmap.from_path(os.path.join(r.path, '.mailmap'))
1247         except IOError as e:
1248             if e.errno != errno.ENOENT:
1249                 raise
1250             mailmap = Mailmap()
1251         return mailmap.lookup(contact)
1252
1253
1254 def fsck(repo):
1255     """Check a repository.
1256
1257     :param repo: A path to the repository
1258     :return: Iterator over errors/warnings
1259     """
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]
1266             try:
1267                 o.check()
1268             except Exception as e:
1269                 yield (sha, e)
1270
1271
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()))
1278
1279
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)
1285         stash.push()
1286
1287
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)
1293         stash.pop()
1294
1295
1296 def ls_files(repo):
1297     """List all files in an index."""
1298     with open_repo_closing(repo) as r:
1299         return sorted(r.open_index())
1300
1301
1302 def describe(repo):
1303     """Describe the repository version.
1304
1305     :param projdir: git repository root
1306     :returns: a string description of the current git revision
1307
1308     Examples: "gabcdefh", "v0.1" or "v0.1-5-gabcdefh".
1309     """
1310     # Get the repository
1311     with open_repo_closing(repo) as r:
1312         # Get a list of all tags
1313         refs = r.get_refs()
1314         tags = {}
1315         for key, value in refs.items():
1316             key = key.decode()
1317             obj = r.get_object(value)
1318             if u'tags' not in key:
1319                 continue
1320
1321             _, tag = key.rsplit(u'/', 1)
1322
1323             try:
1324                 commit = obj.object
1325             except AttributeError:
1326                 continue
1327             else:
1328                 commit = r.get_object(commit[1])
1329             tags[tag] = [
1330                 datetime.datetime(*time.gmtime(commit.commit_time)[:6]),
1331                 commit.id.decode('ascii'),
1332             ]
1333
1334         sorted_tags = sorted(tags.items(),
1335                              key=lambda tag: tag[1][0],
1336                              reverse=True)
1337
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])
1341
1342         # We're now 0 commits from the top
1343         commit_count = 0
1344
1345         # Get the latest commit
1346         latest_commit = r[r.head()]
1347
1348         # Walk through all commits
1349         walker = r.get_walker()
1350         for entry in walker:
1351             # Check if tag
1352             commit_id = entry.commit.id.decode('ascii')
1353             for tag in sorted_tags:
1354                 tag_name = tag[0]
1355                 tag_commit = tag[1][1]
1356                 if commit_id == tag_commit:
1357                     if commit_count == 0:
1358                         return tag_name
1359                     else:
1360                         return '{}-{}-g{}'.format(
1361                                 tag_name,
1362                                 commit_count,
1363                                 latest_commit.id.decode('ascii')[:7])
1364
1365             commit_count += 1
1366
1367         # Return plain commit if no parent tag can be found
1368         return 'g{}'.format(latest_commit.id.decode('ascii')[:7])