cab02ed6c224207a9a128af524ccb3a2ac551e0c
[jelmer/dulwich.git] / dulwich / porcelain.py
1 # porcelain.py -- Porcelain-like layer on top of Dulwich
2 # Copyright (C) 2013 Jelmer Vernooij <jelmer@samba.org>
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  * clone
28  * commit
29  * commit-tree
30  * daemon
31  * diff-tree
32  * fetch
33  * init
34  * ls-remote
35  * ls-tree
36  * pull
37  * push
38  * rm
39  * receive-pack
40  * reset
41  * rev-list
42  * tag{_create,_delete,_list}
43  * upload-pack
44  * update-server-info
45  * status
46  * symbolic-ref
47
48 These functions are meant to behave similarly to the git subcommands.
49 Differences in behaviour are considered bugs.
50 """
51
52 __docformat__ = 'restructuredText'
53
54 from collections import namedtuple
55 from contextlib import (
56     closing,
57     contextmanager,
58 )
59 import os
60 import posixpath
61 import stat
62 import sys
63 import time
64
65 from dulwich.archive import (
66     tar_stream,
67     )
68 from dulwich.client import (
69     get_transport_and_path,
70     )
71 from dulwich.diff_tree import (
72     CHANGE_ADD,
73     CHANGE_DELETE,
74     CHANGE_MODIFY,
75     CHANGE_RENAME,
76     CHANGE_COPY,
77     RENAME_CHANGE_TYPES,
78     )
79 from dulwich.errors import (
80     SendPackError,
81     UpdateRefsError,
82     )
83 from dulwich.index import get_unstaged_changes
84 from dulwich.objects import (
85     Commit,
86     Tag,
87     format_timezone,
88     parse_timezone,
89     pretty_format_tree_entry,
90     )
91 from dulwich.objectspec import (
92     parse_object,
93     parse_reftuples,
94     )
95 from dulwich.pack import (
96     write_pack_index,
97     write_pack_objects,
98     )
99 from dulwich.patch import write_tree_diff
100 from dulwich.protocol import (
101     Protocol,
102     ZERO_SHA,
103     )
104 from dulwich.refs import ANNOTATED_TAG_SUFFIX
105 from dulwich.repo import (BaseRepo, Repo)
106 from dulwich.server import (
107     FileSystemBackend,
108     TCPGitServer,
109     ReceivePackHandler,
110     UploadPackHandler,
111     update_server_info as server_update_server_info,
112     )
113
114
115 # Module level tuple definition for status output
116 GitStatus = namedtuple('GitStatus', 'staged unstaged untracked')
117
118
119 default_bytes_out_stream = getattr(sys.stdout, 'buffer', sys.stdout)
120 default_bytes_err_stream = getattr(sys.stderr, 'buffer', sys.stderr)
121
122
123 DEFAULT_ENCODING = 'utf-8'
124
125
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):
129         return path_or_repo
130     return Repo(path_or_repo)
131
132
133 @contextmanager
134 def _noop_context_manager(obj):
135     """Context manager that has the same api as closing but does nothing."""
136     yield obj
137
138
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.
143     """
144     if isinstance(path_or_repo, BaseRepo):
145         return _noop_context_manager(path_or_repo)
146     return closing(Repo(path_or_repo))
147
148
149 def archive(repo, committish=None, outstream=default_bytes_out_stream,
150             errstream=default_bytes_err_stream):
151     """Create an archive.
152
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)
157     """
158
159     if committish is None:
160         committish = "HEAD"
161     with open_repo_closing(repo) as repo_obj:
162         c = repo_obj[committish]
163         tree = c.tree
164         for chunk in tar_stream(repo_obj.object_store,
165                 repo_obj.object_store[c.tree], c.commit_time):
166             outstream.write(chunk)
167
168
169 def update_server_info(repo="."):
170     """Update server info files for a repository.
171
172     :param repo: path to the repository
173     """
174     with open_repo_closing(repo) as r:
175         server_update_server_info(r)
176
177
178 def symbolic_ref(repo, ref_name, force=False):
179     """Set git symbolic ref into HEAD.
180
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
184     """
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)
190
191
192 def commit(repo=".", message=None, author=None, committer=None):
193     """Create a new commit.
194
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
200     """
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,
205             committer=committer)
206
207
208 def commit_tree(repo, tree, message=None, author=None, committer=None):
209     """Create a new commit object.
210
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
215     """
216     with open_repo_closing(repo) as r:
217         return r.do_commit(message=message, tree=tree, committer=committer,
218                 author=author)
219
220
221 def init(path=".", bare=False):
222     """Create a new git repository.
223
224     :param path: Path to repository.
225     :param bare: Whether to create a bare repository.
226     :return: A Repo instance
227     """
228     if not os.path.exists(path):
229         os.mkdir(path)
230
231     if bare:
232         return Repo.init_bare(path)
233     else:
234         return Repo.init(path)
235
236
237 def clone(source, target=None, bare=False, checkout=None,
238           errstream=default_bytes_err_stream, outstream=None,
239           origin=b"origin"):
240     """Clone a local or remote git repository.
241
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
248     """
249     if outstream is not None:
250         import warnings
251         warnings.warn("outstream= has been deprecated in favour of errstream=.", DeprecationWarning,
252                 stacklevel=3)
253         errstream = outstream
254
255     if checkout is None:
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)
260
261     if target is None:
262         target = host_path.split("/")[-1]
263
264     if not os.path.exists(target):
265         os.mkdir(target)
266
267     if bare:
268         r = Repo.init_bare(target)
269     else:
270         r = Repo.init(target)
271     try:
272         remote_refs = client.fetch(host_path, r,
273             determine_wants=r.object_store.determine_wants_all,
274             progress=errstream.write)
275         r.refs.import_refs(
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/')})
279         r.refs.import_refs(
280             b'refs/tags',
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"]
285         if checkout:
286             errstream.write(b'Checking out HEAD\n')
287             r.reset_index()
288     except:
289         r.close()
290         raise
291
292     return r
293
294
295 def add(repo=".", paths=None):
296     """Add files to the staging area.
297
298     :param repo: Repository for the files
299     :param paths: Paths to add.  No value passed stages all modified files.
300     """
301     # FIXME: Support patterns, directories.
302     with open_repo_closing(repo) as r:
303         if not paths:
304             # If nothing is specified, add all non-ignored files.
305             paths = []
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))
312         r.stage(paths)
313
314
315 def rm(repo=".", paths=None):
316     """Remove files from the staging area.
317
318     :param repo: Repository for the files
319     :param paths: Paths to remove
320     """
321     with open_repo_closing(repo) as r:
322         index = r.open_index()
323         for p in paths:
324             del index[p.encode(sys.getfilesystemencoding())]
325         index.write()
326
327
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")
332
333
334 def print_commit(commit, decode, outstream=sys.stdout):
335     """Write a human-readable commit log entry.
336
337     :param commit: A `Commit` object
338     :param outstream: A stream file to write to
339     """
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")
348
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")
356
357
358 def print_tag(tag, decode, outstream=sys.stdout):
359     """Write a human-readable tag.
360
361     :param tag: A `Tag` object
362     :param decode: Function for decoding bytes to unicode string
363     :param outstream: A stream to write to
364     """
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")
370
371
372 def show_blob(repo, blob, decode, outstream=sys.stdout):
373     """Write a blob to a stream.
374
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
379     """
380     outstream.write(decode(blob.data))
381
382
383 def show_commit(repo, commit, decode, outstream=sys.stdout):
384     """Show a commit to a stream.
385
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
390     """
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)
394
395
396 def show_tree(repo, tree, decode, outstream=sys.stdout):
397     """Print a tree to a stream.
398
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
403     """
404     for n in tree:
405         outstream.write(decode(n) + "\n")
406
407
408 def show_tag(repo, tag, decode, outstream=sys.stdout):
409     """Print a tag to a stream.
410
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
415     """
416     print_tag(tag, decode, outstream)
417     show_object(repo, repo[tag.object[1]], outstream)
418
419
420 def show_object(repo, obj, decode, outstream):
421     return {
422         b"tree": show_tree,
423         b"blob": show_blob,
424         b"commit": show_commit,
425         b"tag": show_tag,
426             }[obj.type_name](repo, obj, decode, outstream)
427
428
429 def print_name_status(changes):
430     """Print a simple status summary, listing changed files.
431     """
432     for change in changes:
433         if not change:
434             continue
435         if type(change) is list:
436             change = change[0]
437         if change.type == CHANGE_ADD:
438             path1 = change.new.path
439             path2 = ''
440             kind = 'A'
441         elif change.type == CHANGE_DELETE:
442             path1 = change.old.path
443             path2 = ''
444             kind = 'D'
445         elif change.type == CHANGE_MODIFY:
446             path1 = change.new.path
447             path2 = ''
448             kind = 'M'
449         elif change.type in RENAME_CHANGE_TYPES:
450             path1 = change.old.path
451             path2 = change.new.path
452             if change.type == CHANGE_RENAME:
453                 kind = 'R'
454             elif change.type == CHANGE_COPY:
455                 kind = 'C'
456         yield '%-8s%-20s%-20s' % (kind, path1, path2)
457
458
459 def log(repo=".", paths=None, outstream=sys.stdout, max_entries=None,
460         reverse=False, name_status=False):
461     """Write commit logs.
462
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
469     """
470     with open_repo_closing(repo) as r:
471         walker = r.get_walker(
472             max_entries=max_entries, paths=paths, reverse=reverse)
473         for entry in walker:
474             decode = lambda x: commit_decode(entry.commit, x)
475             print_commit(entry.commit, decode, outstream)
476             if name_status:
477                 outstream.writelines(
478                     [l+'\n' for l in print_name_status(entry.changes())])
479
480
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.
485
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
490     """
491     if objects is None:
492         objects = ["HEAD"]
493     if not isinstance(objects, list):
494         objects = [objects]
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)
500             else:
501                 decode = lambda x: x.decode(default_encoding)
502             show_object(r, o, decode, outstream)
503
504
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.
507
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
512     """
513     with open_repo_closing(repo) as r:
514         write_tree_diff(outstream, r.object_store, old_tree, new_tree)
515
516
517 def rev_list(repo, commits, outstream=sys.stdout):
518     """Lists commit objects in reverse chronological order.
519
520     :param repo: Path to repository
521     :param commits: Commits over which to iterate
522     :param outstream: Stream to write to
523     """
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")
527
528
529 def tag(*args, **kwargs):
530     import warnings
531     warnings.warn("tag has been deprecated in favour of tag_create.", DeprecationWarning)
532     return tag_create(*args, **kwargs)
533
534
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:
538
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
547     """
548
549     with open_repo_closing(repo) as r:
550         object = parse_object(r, objectish)
551
552         if annotated:
553             # Create the tag object
554             tag_obj = Tag()
555             if author is None:
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
560             tag_obj.name = tag
561             tag_obj.object = (type(object), object.id)
562             if tag_time is None:
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
567                 tag_timezone = 0
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)
572             tag_id = tag_obj.id
573         else:
574             tag_id = object.id
575
576         r.refs[b'refs/tags/' + tag] = tag_id
577
578
579 def list_tags(*args, **kwargs):
580     import warnings
581     warnings.warn("list_tags has been deprecated in favour of tag_list.", DeprecationWarning)
582     return tag_list(*args, **kwargs)
583
584
585 def tag_list(repo, outstream=sys.stdout):
586     """List all tags.
587
588     :param repo: Path to repository
589     :param outstream: Stream to write tags to
590     """
591     with open_repo_closing(repo) as r:
592         tags = list(r.refs.as_dict(b"refs/tags"))
593         tags.sort()
594         return tags
595
596
597 def tag_delete(repo, name):
598     """Remove a tag.
599
600     :param repo: Path to repository
601     :param name: Name of tag to remove
602     """
603     with open_repo_closing(repo) as r:
604         if isinstance(name, bytes):
605             names = [name]
606         elif isinstance(name, list):
607             names = name
608         else:
609             raise TypeError("Unexpected tag name type %r" % name)
610         for name in names:
611             del r.refs[b"refs/tags/" + name]
612
613
614 def reset(repo, mode, committish="HEAD"):
615     """Reset current HEAD to the specified state.
616
617     :param repo: Path to repository
618     :param mode: Mode ("hard", "soft", "mixed")
619     """
620
621     if mode != "hard":
622         raise ValueError("hard is the only mode currently supported")
623
624     with open_repo_closing(repo) as r:
625         tree = r[committish].tree
626         r.reset_index(tree)
627
628
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
632
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
638     """
639
640     # Open the repo
641     with open_repo_closing(repo) as r:
642
643         # Get the client and path
644         client, path = get_transport_and_path(remote_location)
645
646         selected_refs = []
647
648         def update_refs(refs):
649             selected_refs.extend(parse_reftuples(r.refs, refs, refspecs))
650             new_refs = {}
651             # TODO: Handle selected_refs == {None: None}
652             for (lh, rh, force) in selected_refs:
653                 if lh is None:
654                     new_refs[rh] = ZERO_SHA
655                 else:
656                     new_refs[rh] = r.refs[lh]
657             return new_refs
658
659         err_encoding = getattr(errstream, 'encoding', None) or DEFAULT_ENCODING
660         remote_location_bytes = client.get_url(path).encode(err_encoding)
661         try:
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 +
665                             b" successful.\n")
666         except (UpdateRefsError, SendPackError) as e:
667             errstream.write(b"Push to " + remote_location_bytes +
668                             b" failed -> " + e.message.encode(err_encoding) +
669                             b"\n")
670
671
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
675
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
681     """
682     # Open the repo
683     with open_repo_closing(repo) as r:
684         if refspecs is None:
685             refspecs = [b"HEAD"]
686         selected_refs = []
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]
695         if selected_refs:
696             r[b'HEAD'] = remote_refs[selected_refs[0][1]]
697
698         # Perform 'git checkout .' - syncs staged changes
699         tree = r[b"HEAD"].tree
700         r.reset_index()
701
702
703 def status(repo="."):
704     """Returns staged, unstaged, and untracked changes relative to the HEAD.
705
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
711     """
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)
720
721
722 def get_tree_changes(repo):
723     """Return add/delete/modify changes to tree by comparing index to HEAD.
724
725     :param repo: repo path or object
726     :return: dict with lists for each type of change
727     """
728     with open_repo_closing(repo) as r:
729         index = r.open_index()
730
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.
734         tracked_changes = {
735             'add': [],
736             'delete': [],
737             'modify': [],
738         }
739         try:
740             tree_id = r[b'HEAD'].tree
741         except KeyError:
742             tree_id = None
743
744         for change in index.changes_from_tree(r.object_store, tree_id):
745             if not change[0][0]:
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])
751             else:
752                 raise AssertionError('git mv ops not yet supported')
753         return tracked_changes
754
755
756 def daemon(path=".", address=None, port=None):
757     """Run a daemon serving Git requests over TCP/IP.
758
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)
762     """
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()
767
768
769 def web_daemon(path=".", address=None, port=None):
770     """Run a daemon serving Git requests over HTTP.
771
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)
775     """
776     from dulwich.web import (
777         make_wsgi_chain,
778         make_server,
779         WSGIRequestHandlerLogger,
780         WSGIServerLogger)
781
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()
788
789
790 def upload_pack(path=".", inf=None, outf=None):
791     """Upload a pack file after negotiating its contents using smart protocol.
792
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
796     """
797     if outf is None:
798         outf = getattr(sys.stdout, 'buffer', sys.stdout)
799     if inf is None:
800         inf = getattr(sys.stdin, 'buffer', sys.stdin)
801     backend = FileSystemBackend(path)
802     def send_fn(data):
803         outf.write(data)
804         outf.flush()
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.
808     handler.handle()
809     return 0
810
811
812 def receive_pack(path=".", inf=None, outf=None):
813     """Receive a pack file after negotiating its contents using smart protocol.
814
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
818     """
819     if outf is None:
820         outf = getattr(sys.stdout, 'buffer', sys.stdout)
821     if inf is None:
822         inf = getattr(sys.stdin, 'buffer', sys.stdin)
823     backend = FileSystemBackend(path)
824     def send_fn(data):
825         outf.write(data)
826         outf.flush()
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.
830     handler.handle()
831     return 0
832
833
834 def branch_delete(repo, name):
835     """Delete a branch.
836
837     :param repo: Path to the repository
838     :param name: Name of the branch
839     """
840     with open_repo_closing(repo) as r:
841         if isinstance(name, bytes):
842             names = [name]
843         elif isinstance(name, list):
844             names = name
845         else:
846             raise TypeError("Unexpected branch name type %r" % name)
847         for name in names:
848             del r.refs[b"refs/heads/" + name]
849
850
851 def branch_create(repo, name, objectish=None, force=False):
852     """Create a branch.
853
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
858     """
859     with open_repo_closing(repo) as r:
860         if isinstance(name, bytes):
861             names = [name]
862         elif isinstance(name, list):
863             names = name
864         else:
865             raise TypeError("Unexpected branch name type %r" % name)
866         if objectish is None:
867             objectish = "HEAD"
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
873
874
875 def branch_list(repo):
876     """List all branches.
877
878     :param repo: Path to the repository
879     """
880     with open_repo_closing(repo) as r:
881         return r.refs.keys(base=b"refs/heads/")
882
883
884 def fetch(repo, remote_location, outstream=sys.stdout,
885         errstream=default_bytes_err_stream):
886     """Fetch objects from a remote server.
887
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
893     """
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)
897     return remote_refs
898
899
900 def ls_remote(remote):
901     """List the refs in a remote.
902
903     :param remote: Remote repository location
904     :return: Dictionary with remote refs
905     """
906     client, host_path = get_transport_and_path(remote)
907     return client.get_refs(host_path)
908
909
910 def repack(repo):
911     """Repack loose files in a repository.
912
913     Currently this only packs loose objects.
914
915     :param repo: Path to the repository
916     """
917     with open_repo_closing(repo) as r:
918         r.object_store.pack_loose_objects()
919
920
921 def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None):
922     """Pack objects into a file.
923
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)
928     """
929     with open_repo_closing(repo) as r:
930         entries, data_sum = write_pack_objects(
931             packf,
932             r.object_store.iter_shas((oid, None) for oid in object_ids),
933             delta_window_size=delta_window_size)
934     if idxf is not None:
935         entries = [(k, v[0], v[1]) for (k, v) in entries.items()]
936         entries.sort()
937         write_pack_index(idxf, entries, data_sum)
938
939
940 def ls_tree(repo, tree_ish=None, outstream=sys.stdout, recursive=False,
941         name_only=False):
942     """List contents of a tree.
943
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
949     """
950     def list_tree(store, treeid, base):
951         for (name, mode, sha) in store[treeid].iteritems():
952             if base:
953                 name = posixpath.join(base, name)
954             if name_only:
955                 outstream.write(name + b"\n")
956             else:
957                 outstream.write(pretty_format_tree_entry(name, mode, sha))
958             if stat.S_ISDIR(mode):
959                 list_tree(store, sha, name)
960     if tree_ish is None:
961         tree_ish = "HEAD"
962     with open_repo_closing(repo) as r:
963         c = r[tree_ish]
964         treeid = c.tree
965         list_tree(r.object_store, treeid, "")