Statistics
| Revision:

gvsig-scripting / org.gvsig.scripting / trunk / org.gvsig.scripting / org.gvsig.scripting.app / org.gvsig.scripting.app.mainplugin / src / main / resources-plugin / scripting / lib / dulwich / porcelain.py @ 959

History | View | Annotate | Download (31.2 KB)

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 checkout: Whether or not to check-out HEAD after cloning
246
    :param errstream: Optional stream to write progress to
247
    :param outstream: Optional stream to write progress to (deprecated)
248
    :return: The new repository
249
    """
250
    if outstream is not None:
251
        import warnings
252
        warnings.warn("outstream= has been deprecated in favour of errstream=.", DeprecationWarning,
253
                stacklevel=3)
254
        errstream = outstream
255

    
256
    if checkout is None:
257
        checkout = (not bare)
258
    if checkout and bare:
259
        raise ValueError("checkout and bare are incompatible")
260
    client, host_path = get_transport_and_path(source)
261

    
262
    if target is None:
263
        target = host_path.split("/")[-1]
264

    
265
    if not os.path.exists(target):
266
        os.mkdir(target)
267

    
268
    if bare:
269
        r = Repo.init_bare(target)
270
    else:
271
        r = Repo.init(target)
272
    try:
273
        remote_refs = client.fetch(host_path, r,
274
            determine_wants=r.object_store.determine_wants_all,
275
            progress=errstream.write)
276
        r.refs.import_refs(
277
            b'refs/remotes/' + origin,
278
            {n[len(b'refs/heads/'):]: v for (n, v) in remote_refs.items()
279
                if n.startswith(b'refs/heads/')})
280
        r.refs.import_refs(
281
            b'refs/tags',
282
            {n[len(b'refs/tags/'):]: v for (n, v) in remote_refs.items()
283
                if n.startswith(b'refs/tags/') and
284
                not n.endswith(ANNOTATED_TAG_SUFFIX)})
285
        r[b"HEAD"] = remote_refs[b"HEAD"]
286
        target_config = r.get_config()
287
        if not isinstance(source, bytes):
288
            source = source.encode(DEFAULT_ENCODING)
289
        target_config.set((b'remote', b'origin'), b'url', source)
290
        target_config.set((b'remote', b'origin'), b'fetch',
291
            b'+refs/heads/*:refs/remotes/origin/*')
292
        target_config.write_to_path()
293
        if checkout:
294
            errstream.write(b'Checking out HEAD\n')
295
            r.reset_index()
296
    except:
297
        r.close()
298
        raise
299

    
300
    return r
301

    
302

    
303
def add(repo=".", paths=None):
304
    """Add files to the staging area.
305

306
    :param repo: Repository for the files
307
    :param paths: Paths to add.  No value passed stages all modified files.
308
    """
309
    # FIXME: Support patterns, directories.
310
    with open_repo_closing(repo) as r:
311
        if not paths:
312
            # If nothing is specified, add all non-ignored files.
313
            paths = []
314
            for dirpath, dirnames, filenames in os.walk(r.path):
315
                # Skip .git and below.
316
                if '.git' in dirnames:
317
                    dirnames.remove('.git')
318
                for filename in filenames:
319
                    paths.append(os.path.join(dirpath[len(r.path)+1:], filename))
320
        r.stage(paths)
321

    
322

    
323
def rm(repo=".", paths=None):
324
    """Remove files from the staging area.
325

326
    :param repo: Repository for the files
327
    :param paths: Paths to remove
328
    """
329
    with open_repo_closing(repo) as r:
330
        index = r.open_index()
331
        for p in paths:
332
            del index[p.encode(sys.getfilesystemencoding())]
333
        index.write()
334

    
335

    
336
def commit_decode(commit, contents, default_encoding=DEFAULT_ENCODING):
337
    if commit.encoding is not None:
338
        return contents.decode(commit.encoding, "replace")
339
    return contents.decode(default_encoding, "replace")
340

    
341

    
342
def print_commit(commit, decode, outstream=sys.stdout):
343
    """Write a human-readable commit log entry.
344

345
    :param commit: A `Commit` object
346
    :param outstream: A stream file to write to
347
    """
348
    outstream.write("-" * 50 + "\n")
349
    outstream.write("commit: " + commit.id.decode('ascii') + "\n")
350
    if len(commit.parents) > 1:
351
        outstream.write("merge: " +
352
            "...".join([c.decode('ascii') for c in commit.parents[1:]]) + "\n")
353
    outstream.write("Author: " + decode(commit.author) + "\n")
354
    if commit.author != commit.committer:
355
        outstream.write("Committer: " + decode(commit.committer) + "\n")
356

    
357
    time_tuple = time.gmtime(commit.author_time + commit.author_timezone)
358
    time_str = time.strftime("%a %b %d %Y %H:%M:%S", time_tuple)
359
    timezone_str = format_timezone(commit.author_timezone).decode('ascii')
360
    outstream.write("Date:   " + time_str + " " + timezone_str + "\n")
361
    outstream.write("\n")
362
    outstream.write(decode(commit.message) + "\n")
363
    outstream.write("\n")
364

    
365

    
366
def print_tag(tag, decode, outstream=sys.stdout):
367
    """Write a human-readable tag.
368

369
    :param tag: A `Tag` object
370
    :param decode: Function for decoding bytes to unicode string
371
    :param outstream: A stream to write to
372
    """
373
    outstream.write("Tagger: " + decode(tag.tagger) + "\n")
374
    outstream.write("Date:   " + decode(tag.tag_time) + "\n")
375
    outstream.write("\n")
376
    outstream.write(decode(tag.message) + "\n")
377
    outstream.write("\n")
378

    
379

    
380
def show_blob(repo, blob, decode, outstream=sys.stdout):
381
    """Write a blob to a stream.
382

383
    :param repo: A `Repo` object
384
    :param blob: A `Blob` object
385
    :param decode: Function for decoding bytes to unicode string
386
    :param outstream: A stream file to write to
387
    """
388
    outstream.write(decode(blob.data))
389

    
390

    
391
def show_commit(repo, commit, decode, outstream=sys.stdout):
392
    """Show a commit to a stream.
393

394
    :param repo: A `Repo` object
395
    :param commit: A `Commit` object
396
    :param decode: Function for decoding bytes to unicode string
397
    :param outstream: Stream to write to
398
    """
399
    print_commit(commit, decode=decode, outstream=outstream)
400
    parent_commit = repo[commit.parents[0]]
401
    write_tree_diff(outstream, repo.object_store, parent_commit.tree, commit.tree)
402

    
403

    
404
def show_tree(repo, tree, decode, outstream=sys.stdout):
405
    """Print a tree to a stream.
406

407
    :param repo: A `Repo` object
408
    :param tree: A `Tree` object
409
    :param decode: Function for decoding bytes to unicode string
410
    :param outstream: Stream to write to
411
    """
412
    for n in tree:
413
        outstream.write(decode(n) + "\n")
414

    
415

    
416
def show_tag(repo, tag, decode, outstream=sys.stdout):
417
    """Print a tag to a stream.
418

419
    :param repo: A `Repo` object
420
    :param tag: A `Tag` object
421
    :param decode: Function for decoding bytes to unicode string
422
    :param outstream: Stream to write to
423
    """
424
    print_tag(tag, decode, outstream)
425
    show_object(repo, repo[tag.object[1]], outstream)
426

    
427

    
428
def show_object(repo, obj, decode, outstream):
429
    return {
430
        b"tree": show_tree,
431
        b"blob": show_blob,
432
        b"commit": show_commit,
433
        b"tag": show_tag,
434
            }[obj.type_name](repo, obj, decode, outstream)
435

    
436

    
437
def print_name_status(changes):
438
    """Print a simple status summary, listing changed files.
439
    """
440
    for change in changes:
441
        if not change:
442
            continue
443
        if type(change) is list:
444
            change = change[0]
445
        if change.type == CHANGE_ADD:
446
            path1 = change.new.path
447
            path2 = ''
448
            kind = 'A'
449
        elif change.type == CHANGE_DELETE:
450
            path1 = change.old.path
451
            path2 = ''
452
            kind = 'D'
453
        elif change.type == CHANGE_MODIFY:
454
            path1 = change.new.path
455
            path2 = ''
456
            kind = 'M'
457
        elif change.type in RENAME_CHANGE_TYPES:
458
            path1 = change.old.path
459
            path2 = change.new.path
460
            if change.type == CHANGE_RENAME:
461
                kind = 'R'
462
            elif change.type == CHANGE_COPY:
463
                kind = 'C'
464
        yield '%-8s%-20s%-20s' % (kind, path1, path2)
465

    
466

    
467
def log(repo=".", paths=None, outstream=sys.stdout, max_entries=None,
468
        reverse=False, name_status=False):
469
    """Write commit logs.
470

471
    :param repo: Path to repository
472
    :param paths: Optional set of specific paths to print entries for
473
    :param outstream: Stream to write log output to
474
    :param reverse: Reverse order in which entries are printed
475
    :param name_status: Print name status
476
    :param max_entries: Optional maximum number of entries to display
477
    """
478
    with open_repo_closing(repo) as r:
479
        walker = r.get_walker(
480
            max_entries=max_entries, paths=paths, reverse=reverse)
481
        for entry in walker:
482
            decode = lambda x: commit_decode(entry.commit, x)
483
            print_commit(entry.commit, decode, outstream)
484
            if name_status:
485
                outstream.writelines(
486
                    [l+'\n' for l in print_name_status(entry.changes())])
487

    
488

    
489
# TODO(jelmer): better default for encoding?
490
def show(repo=".", objects=None, outstream=sys.stdout,
491
         default_encoding=DEFAULT_ENCODING):
492
    """Print the changes in a commit.
493

494
    :param repo: Path to repository
495
    :param objects: Objects to show (defaults to [HEAD])
496
    :param outstream: Stream to write to
497
    :param default_encoding: Default encoding to use if none is set in the commit
498
    """
499
    if objects is None:
500
        objects = ["HEAD"]
501
    if not isinstance(objects, list):
502
        objects = [objects]
503
    with open_repo_closing(repo) as r:
504
        for objectish in objects:
505
            o = parse_object(r, objectish)
506
            if isinstance(o, Commit):
507
                decode = lambda x: commit_decode(o, x, default_encoding)
508
            else:
509
                decode = lambda x: x.decode(default_encoding)
510
            show_object(r, o, decode, outstream)
511

    
512

    
513
def diff_tree(repo, old_tree, new_tree, outstream=sys.stdout):
514
    """Compares the content and mode of blobs found via two tree objects.
515

516
    :param repo: Path to repository
517
    :param old_tree: Id of old tree
518
    :param new_tree: Id of new tree
519
    :param outstream: Stream to write to
520
    """
521
    with open_repo_closing(repo) as r:
522
        write_tree_diff(outstream, r.object_store, old_tree, new_tree)
523

    
524

    
525
def rev_list(repo, commits, outstream=sys.stdout):
526
    """Lists commit objects in reverse chronological order.
527

528
    :param repo: Path to repository
529
    :param commits: Commits over which to iterate
530
    :param outstream: Stream to write to
531
    """
532
    with open_repo_closing(repo) as r:
533
        for entry in r.get_walker(include=[r[c].id for c in commits]):
534
            outstream.write(entry.commit.id + b"\n")
535

    
536

    
537
def tag(*args, **kwargs):
538
    import warnings
539
    warnings.warn("tag has been deprecated in favour of tag_create.", DeprecationWarning)
540
    return tag_create(*args, **kwargs)
541

    
542

    
543
def tag_create(repo, tag, author=None, message=None, annotated=False,
544
        objectish="HEAD", tag_time=None, tag_timezone=None):
545
    """Creates a tag in git via dulwich calls:
546

547
    :param repo: Path to repository
548
    :param tag: tag string
549
    :param author: tag author (optional, if annotated is set)
550
    :param message: tag message (optional)
551
    :param annotated: whether to create an annotated tag
552
    :param objectish: object the tag should point at, defaults to HEAD
553
    :param tag_time: Optional time for annotated tag
554
    :param tag_timezone: Optional timezone for annotated tag
555
    """
556

    
557
    with open_repo_closing(repo) as r:
558
        object = parse_object(r, objectish)
559

    
560
        if annotated:
561
            # Create the tag object
562
            tag_obj = Tag()
563
            if author is None:
564
                # TODO(jelmer): Don't use repo private method.
565
                author = r._get_user_identity()
566
            tag_obj.tagger = author
567
            tag_obj.message = message
568
            tag_obj.name = tag
569
            tag_obj.object = (type(object), object.id)
570
            if tag_time is None:
571
                tag_time = int(time.time())
572
            tag_obj.tag_time = tag_time
573
            if tag_timezone is None:
574
                # TODO(jelmer) Use current user timezone rather than UTC
575
                tag_timezone = 0
576
            elif isinstance(tag_timezone, str):
577
                tag_timezone = parse_timezone(tag_timezone)
578
            tag_obj.tag_timezone = tag_timezone
579
            r.object_store.add_object(tag_obj)
580
            tag_id = tag_obj.id
581
        else:
582
            tag_id = object.id
583

    
584
        r.refs[b'refs/tags/' + tag] = tag_id
585

    
586

    
587
def list_tags(*args, **kwargs):
588
    import warnings
589
    warnings.warn("list_tags has been deprecated in favour of tag_list.", DeprecationWarning)
590
    return tag_list(*args, **kwargs)
591

    
592

    
593
def tag_list(repo, outstream=sys.stdout):
594
    """List all tags.
595

596
    :param repo: Path to repository
597
    :param outstream: Stream to write tags to
598
    """
599
    with open_repo_closing(repo) as r:
600
        tags = list(r.refs.as_dict(b"refs/tags"))
601
        tags.sort()
602
        return tags
603

    
604

    
605
def tag_delete(repo, name):
606
    """Remove a tag.
607

608
    :param repo: Path to repository
609
    :param name: Name of tag to remove
610
    """
611
    with open_repo_closing(repo) as r:
612
        if isinstance(name, bytes):
613
            names = [name]
614
        elif isinstance(name, list):
615
            names = name
616
        else:
617
            raise TypeError("Unexpected tag name type %r" % name)
618
        for name in names:
619
            del r.refs[b"refs/tags/" + name]
620

    
621

    
622
def reset(repo, mode, committish="HEAD"):
623
    """Reset current HEAD to the specified state.
624

625
    :param repo: Path to repository
626
    :param mode: Mode ("hard", "soft", "mixed")
627
    """
628

    
629
    if mode != "hard":
630
        raise ValueError("hard is the only mode currently supported")
631

    
632
    with open_repo_closing(repo) as r:
633
        tree = r[committish].tree
634
        r.reset_index(tree)
635

    
636

    
637
def push(repo, remote_location, refspecs=None,
638
         outstream=default_bytes_out_stream, errstream=default_bytes_err_stream):
639
    """Remote push with dulwich via dulwich.client
640

641
    :param repo: Path to repository
642
    :param remote_location: Location of the remote
643
    :param refspecs: relative path to the refs to push to remote
644
    :param outstream: A stream file to write output
645
    :param errstream: A stream file to write errors
646
    """
647

    
648
    # Open the repo
649
    with open_repo_closing(repo) as r:
650

    
651
        # Get the client and path
652
        client, path = get_transport_and_path(remote_location)
653

    
654
        selected_refs = []
655

    
656
        def update_refs(refs):
657
            selected_refs.extend(parse_reftuples(r.refs, refs, refspecs))
658
            new_refs = {}
659
            # TODO: Handle selected_refs == {None: None}
660
            for (lh, rh, force) in selected_refs:
661
                if lh is None:
662
                    new_refs[rh] = ZERO_SHA
663
                else:
664
                    new_refs[rh] = r.refs[lh]
665
            return new_refs
666

    
667
        err_encoding = getattr(errstream, 'encoding', None) or DEFAULT_ENCODING
668
        remote_location_bytes = client.get_url(path).encode(err_encoding)
669
        try:
670
            client.send_pack(path, update_refs,
671
                r.object_store.generate_pack_contents, progress=errstream.write)
672
            errstream.write(b"Push to " + remote_location_bytes +
673
                            b" successful.\n")
674
        except (UpdateRefsError, SendPackError) as e:
675
            errstream.write(b"Push to " + remote_location_bytes +
676
                            b" failed -> " + e.message.encode(err_encoding) +
677
                            b"\n")
678

    
679

    
680
def pull(repo, remote_location, refspecs=None,
681
         outstream=default_bytes_out_stream, errstream=default_bytes_err_stream):
682
    """Pull from remote via dulwich.client
683

684
    :param repo: Path to repository
685
    :param remote_location: Location of the remote
686
    :param refspec: refspecs to fetch
687
    :param outstream: A stream file to write to output
688
    :param errstream: A stream file to write to errors
689
    """
690
    # Open the repo
691
    with open_repo_closing(repo) as r:
692
        if refspecs is None:
693
            refspecs = [b"HEAD"]
694
        selected_refs = []
695
        def determine_wants(remote_refs):
696
            selected_refs.extend(parse_reftuples(remote_refs, r.refs, refspecs))
697
            return [remote_refs[lh] for (lh, rh, force) in selected_refs]
698
        client, path = get_transport_and_path(remote_location)
699
        remote_refs = client.fetch(path, r, progress=errstream.write,
700
                determine_wants=determine_wants)
701
        for (lh, rh, force) in selected_refs:
702
            r.refs[rh] = remote_refs[lh]
703
        if selected_refs:
704
            r[b'HEAD'] = remote_refs[selected_refs[0][1]]
705

    
706
        # Perform 'git checkout .' - syncs staged changes
707
        tree = r[b"HEAD"].tree
708
        r.reset_index()
709

    
710

    
711
def status(repo="."):
712
    """Returns staged, unstaged, and untracked changes relative to the HEAD.
713

714
    :param repo: Path to repository or repository object
715
    :return: GitStatus tuple,
716
        staged -    list of staged paths (diff index/HEAD)
717
        unstaged -  list of unstaged paths (diff index/working-tree)
718
        untracked - list of untracked, un-ignored & non-.git paths
719
    """
720
    with open_repo_closing(repo) as r:
721
        # 1. Get status of staged
722
        tracked_changes = get_tree_changes(r)
723
        # 2. Get status of unstaged
724
        unstaged_changes = list(get_unstaged_changes(r.open_index(), r.path))
725
        # TODO - Status of untracked - add untracked changes, need gitignore.
726
        untracked_changes = []
727
        return GitStatus(tracked_changes, unstaged_changes, untracked_changes)
728

    
729

    
730
def get_tree_changes(repo):
731
    """Return add/delete/modify changes to tree by comparing index to HEAD.
732

733
    :param repo: repo path or object
734
    :return: dict with lists for each type of change
735
    """
736
    with open_repo_closing(repo) as r:
737
        index = r.open_index()
738

    
739
        # Compares the Index to the HEAD & determines changes
740
        # Iterate through the changes and report add/delete/modify
741
        # TODO: call out to dulwich.diff_tree somehow.
742
        tracked_changes = {
743
            'add': [],
744
            'delete': [],
745
            'modify': [],
746
        }
747
        try:
748
            tree_id = r[b'HEAD'].tree
749
        except KeyError:
750
            tree_id = None
751

    
752
        for change in index.changes_from_tree(r.object_store, tree_id):
753
            if not change[0][0]:
754
                tracked_changes['add'].append(change[0][1])
755
            elif not change[0][1]:
756
                tracked_changes['delete'].append(change[0][0])
757
            elif change[0][0] == change[0][1]:
758
                tracked_changes['modify'].append(change[0][0])
759
            else:
760
                raise AssertionError('git mv ops not yet supported')
761
        return tracked_changes
762

    
763

    
764
def daemon(path=".", address=None, port=None):
765
    """Run a daemon serving Git requests over TCP/IP.
766

767
    :param path: Path to the directory to serve.
768
    :param address: Optional address to listen on (defaults to ::)
769
    :param port: Optional port to listen on (defaults to TCP_GIT_PORT)
770
    """
771
    # TODO(jelmer): Support git-daemon-export-ok and --export-all.
772
    backend = FileSystemBackend(path)
773
    server = TCPGitServer(backend, address, port)
774
    server.serve_forever()
775

    
776

    
777
def web_daemon(path=".", address=None, port=None):
778
    """Run a daemon serving Git requests over HTTP.
779

780
    :param path: Path to the directory to serve
781
    :param address: Optional address to listen on (defaults to ::)
782
    :param port: Optional port to listen on (defaults to 80)
783
    """
784
    from dulwich.web import (
785
        make_wsgi_chain,
786
        make_server,
787
        WSGIRequestHandlerLogger,
788
        WSGIServerLogger)
789

    
790
    backend = FileSystemBackend(path)
791
    app = make_wsgi_chain(backend)
792
    server = make_server(address, port, app,
793
                         handler_class=WSGIRequestHandlerLogger,
794
                         server_class=WSGIServerLogger)
795
    server.serve_forever()
796

    
797

    
798
def upload_pack(path=".", inf=None, outf=None):
799
    """Upload a pack file after negotiating its contents using smart protocol.
800

801
    :param path: Path to the repository
802
    :param inf: Input stream to communicate with client
803
    :param outf: Output stream to communicate with client
804
    """
805
    if outf is None:
806
        outf = getattr(sys.stdout, 'buffer', sys.stdout)
807
    if inf is None:
808
        inf = getattr(sys.stdin, 'buffer', sys.stdin)
809
    backend = FileSystemBackend(path)
810
    def send_fn(data):
811
        outf.write(data)
812
        outf.flush()
813
    proto = Protocol(inf.read, send_fn)
814
    handler = UploadPackHandler(backend, [path], proto)
815
    # FIXME: Catch exceptions and write a single-line summary to outf.
816
    handler.handle()
817
    return 0
818

    
819

    
820
def receive_pack(path=".", inf=None, outf=None):
821
    """Receive a pack file after negotiating its contents using smart protocol.
822

823
    :param path: Path to the repository
824
    :param inf: Input stream to communicate with client
825
    :param outf: Output stream to communicate with client
826
    """
827
    if outf is None:
828
        outf = getattr(sys.stdout, 'buffer', sys.stdout)
829
    if inf is None:
830
        inf = getattr(sys.stdin, 'buffer', sys.stdin)
831
    backend = FileSystemBackend(path)
832
    def send_fn(data):
833
        outf.write(data)
834
        outf.flush()
835
    proto = Protocol(inf.read, send_fn)
836
    handler = ReceivePackHandler(backend, [path], proto)
837
    # FIXME: Catch exceptions and write a single-line summary to outf.
838
    handler.handle()
839
    return 0
840

    
841

    
842
def branch_delete(repo, name):
843
    """Delete a branch.
844

845
    :param repo: Path to the repository
846
    :param name: Name of the branch
847
    """
848
    with open_repo_closing(repo) as r:
849
        if isinstance(name, bytes):
850
            names = [name]
851
        elif isinstance(name, list):
852
            names = name
853
        else:
854
            raise TypeError("Unexpected branch name type %r" % name)
855
        for name in names:
856
            del r.refs[b"refs/heads/" + name]
857

    
858

    
859
def branch_create(repo, name, objectish=None, force=False):
860
    """Create a branch.
861

862
    :param repo: Path to the repository
863
    :param name: Name of the new branch
864
    :param objectish: Target object to point new branch at (defaults to HEAD)
865
    :param force: Force creation of branch, even if it already exists
866
    """
867
    with open_repo_closing(repo) as r:
868
        if isinstance(name, bytes):
869
            names = [name]
870
        elif isinstance(name, list):
871
            names = name
872
        else:
873
            raise TypeError("Unexpected branch name type %r" % name)
874
        if objectish is None:
875
            objectish = "HEAD"
876
        object = parse_object(r, objectish)
877
        refname = b"refs/heads/" + name
878
        if refname in r.refs and not force:
879
            raise KeyError("Branch with name %s already exists." % name)
880
        r.refs[refname] = object.id
881

    
882

    
883
def branch_list(repo):
884
    """List all branches.
885

886
    :param repo: Path to the repository
887
    """
888
    with open_repo_closing(repo) as r:
889
        return r.refs.keys(base=b"refs/heads/")
890

    
891

    
892
def fetch(repo, remote_location, outstream=sys.stdout,
893
        errstream=default_bytes_err_stream):
894
    """Fetch objects from a remote server.
895

896
    :param repo: Path to the repository
897
    :param remote_location: String identifying a remote server
898
    :param outstream: Output stream (defaults to stdout)
899
    :param errstream: Error stream (defaults to stderr)
900
    :return: Dictionary with refs on the remote
901
    """
902
    with open_repo_closing(repo) as r:
903
        client, path = get_transport_and_path(remote_location)
904
        remote_refs = client.fetch(path, r, progress=errstream.write)
905
    return remote_refs
906

    
907

    
908
def ls_remote(remote):
909
    """List the refs in a remote.
910

911
    :param remote: Remote repository location
912
    :return: Dictionary with remote refs
913
    """
914
    client, host_path = get_transport_and_path(remote)
915
    return client.get_refs(host_path)
916

    
917

    
918
def repack(repo):
919
    """Repack loose files in a repository.
920

921
    Currently this only packs loose objects.
922

923
    :param repo: Path to the repository
924
    """
925
    with open_repo_closing(repo) as r:
926
        r.object_store.pack_loose_objects()
927

    
928

    
929
def pack_objects(repo, object_ids, packf, idxf, delta_window_size=None):
930
    """Pack objects into a file.
931

932
    :param repo: Path to the repository
933
    :param object_ids: List of object ids to write
934
    :param packf: File-like object to write to
935
    :param idxf: File-like object to write to (can be None)
936
    """
937
    with open_repo_closing(repo) as r:
938
        entries, data_sum = write_pack_objects(
939
            packf,
940
            r.object_store.iter_shas((oid, None) for oid in object_ids),
941
            delta_window_size=delta_window_size)
942
    if idxf is not None:
943
        entries = [(k, v[0], v[1]) for (k, v) in entries.items()]
944
        entries.sort()
945
        write_pack_index(idxf, entries, data_sum)
946

    
947

    
948
def ls_tree(repo, tree_ish=None, outstream=sys.stdout, recursive=False,
949
        name_only=False):
950
    """List contents of a tree.
951

952
    :param repo: Path to the repository
953
    :param tree_ish: Tree id to list
954
    :param outstream: Output stream (defaults to stdout)
955
    :param recursive: Whether to recursively list files
956
    :param name_only: Only print item name
957
    """
958
    def list_tree(store, treeid, base):
959
        for (name, mode, sha) in store[treeid].iteritems():
960
            if base:
961
                name = posixpath.join(base, name)
962
            if name_only:
963
                outstream.write(name + b"\n")
964
            else:
965
                outstream.write(pretty_format_tree_entry(name, mode, sha))
966
            if stat.S_ISDIR(mode):
967
                list_tree(store, sha, name)
968
    if tree_ish is None:
969
        tree_ish = "HEAD"
970
    with open_repo_closing(repo) as r:
971
        c = r[tree_ish]
972
        treeid = c.tree
973
        list_tree(r.object_store, treeid, "")