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 / repo.py @ 959

History | View | Annotate | Download (38.4 KB)

1
# repo.py -- For dealing with git repositories.
2
# Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
3
# Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
4
#
5
# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
6
# General Public License as public by the Free Software Foundation; version 2.0
7
# or (at your option) any later version. You can redistribute it and/or
8
# modify it under the terms of either of these two licenses.
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
#
16
# You should have received a copy of the licenses; if not, see
17
# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
18
# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
19
# License, Version 2.0.
20
#
21

    
22

    
23
"""Repository access.
24

25
This module contains the base class for git repositories
26
(BaseRepo) and an implementation which uses a repository on
27
local disk (Repo).
28

29
"""
30

    
31
from io import BytesIO
32
import errno
33
import os
34
import sys
35
import stat
36

    
37
from dulwich.errors import (
38
    NoIndexPresent,
39
    NotBlobError,
40
    NotCommitError,
41
    NotGitRepository,
42
    NotTreeError,
43
    NotTagError,
44
    CommitError,
45
    RefFormatError,
46
    HookError,
47
    )
48
from dulwich.file import (
49
    GitFile,
50
    )
51
from dulwich.object_store import (
52
    DiskObjectStore,
53
    MemoryObjectStore,
54
    ObjectStoreGraphWalker,
55
    )
56
from dulwich.objects import (
57
    check_hexsha,
58
    Blob,
59
    Commit,
60
    ShaFile,
61
    Tag,
62
    Tree,
63
    )
64

    
65
from dulwich.hooks import (
66
    PreCommitShellHook,
67
    PostCommitShellHook,
68
    CommitMsgShellHook,
69
    )
70

    
71
from dulwich.refs import (
72
    check_ref_format,
73
    RefsContainer,
74
    DictRefsContainer,
75
    InfoRefsContainer,
76
    DiskRefsContainer,
77
    read_packed_refs,
78
    read_packed_refs_with_peeled,
79
    write_packed_refs,
80
    SYMREF,
81
    )
82

    
83

    
84
import warnings
85

    
86

    
87
CONTROLDIR = '.git'
88
OBJECTDIR = 'objects'
89
REFSDIR = 'refs'
90
REFSDIR_TAGS = 'tags'
91
REFSDIR_HEADS = 'heads'
92
INDEX_FILENAME = "index"
93
COMMONDIR = 'commondir'
94
GITDIR = 'gitdir'
95
WORKTREES = 'worktrees'
96

    
97
BASE_DIRECTORIES = [
98
    ["branches"],
99
    [REFSDIR],
100
    [REFSDIR, REFSDIR_TAGS],
101
    [REFSDIR, REFSDIR_HEADS],
102
    ["hooks"],
103
    ["info"]
104
    ]
105

    
106
DEFAULT_REF = b'refs/heads/master'
107

    
108

    
109
def parse_graftpoints(graftpoints):
110
    """Convert a list of graftpoints into a dict
111

112
    :param graftpoints: Iterator of graftpoint lines
113

114
    Each line is formatted as:
115
        <commit sha1> <parent sha1> [<parent sha1>]*
116

117
    Resulting dictionary is:
118
        <commit sha1>: [<parent sha1>*]
119

120
    https://git.wiki.kernel.org/index.php/GraftPoint
121
    """
122
    grafts = {}
123
    for l in graftpoints:
124
        raw_graft = l.split(None, 1)
125

    
126
        commit = raw_graft[0]
127
        if len(raw_graft) == 2:
128
            parents = raw_graft[1].split()
129
        else:
130
            parents = []
131

    
132
        for sha in [commit] + parents:
133
            check_hexsha(sha, 'Invalid graftpoint')
134

    
135
        grafts[commit] = parents
136
    return grafts
137

    
138

    
139
def serialize_graftpoints(graftpoints):
140
    """Convert a dictionary of grafts into string
141

142
    The graft dictionary is:
143
        <commit sha1>: [<parent sha1>*]
144

145
    Each line is formatted as:
146
        <commit sha1> <parent sha1> [<parent sha1>]*
147

148
    https://git.wiki.kernel.org/index.php/GraftPoint
149

150
    """
151
    graft_lines = []
152
    for commit, parents in graftpoints.items():
153
        if parents:
154
            graft_lines.append(commit + b' ' + b' '.join(parents))
155
        else:
156
            graft_lines.append(commit)
157
    return b'\n'.join(graft_lines)
158

    
159

    
160
class BaseRepo(object):
161
    """Base class for a git repository.
162

163
    :ivar object_store: Dictionary-like object for accessing
164
        the objects
165
    :ivar refs: Dictionary-like object with the refs in this
166
        repository
167
    """
168

    
169
    def __init__(self, object_store, refs):
170
        """Open a repository.
171

172
        This shouldn't be called directly, but rather through one of the
173
        base classes, such as MemoryRepo or Repo.
174

175
        :param object_store: Object store to use
176
        :param refs: Refs container to use
177
        """
178
        self.object_store = object_store
179
        self.refs = refs
180

    
181
        self._graftpoints = {}
182
        self.hooks = {}
183

    
184
    def _determine_file_mode(self):
185
        """Probe the file-system to determine whether permissions can be trusted.
186

187
        :return: True if permissions can be trusted, False otherwise.
188
        """
189
        raise NotImplementedError(self._determine_file_mode)
190

    
191
    def _init_files(self, bare):
192
        """Initialize a default set of named files."""
193
        from dulwich.config import ConfigFile
194
        self._put_named_file('description', b"Unnamed repository")
195
        f = BytesIO()
196
        cf = ConfigFile()
197
        cf.set(b"core", b"repositoryformatversion", b"0")
198
        if self._determine_file_mode():
199
            cf.set(b"core", b"filemode", True)
200
        else:
201
            cf.set(b"core", b"filemode", False)
202

    
203
        cf.set(b"core", b"bare", bare)
204
        cf.set(b"core", b"logallrefupdates", True)
205
        cf.write_to_file(f)
206
        self._put_named_file('config', f.getvalue())
207
        self._put_named_file(os.path.join('info', 'exclude'), b'')
208

    
209
    def get_named_file(self, path):
210
        """Get a file from the control dir with a specific name.
211

212
        Although the filename should be interpreted as a filename relative to
213
        the control dir in a disk-based Repo, the object returned need not be
214
        pointing to a file in that location.
215

216
        :param path: The path to the file, relative to the control dir.
217
        :return: An open file object, or None if the file does not exist.
218
        """
219
        raise NotImplementedError(self.get_named_file)
220

    
221
    def _put_named_file(self, path, contents):
222
        """Write a file to the control dir with the given name and contents.
223

224
        :param path: The path to the file, relative to the control dir.
225
        :param contents: A string to write to the file.
226
        """
227
        raise NotImplementedError(self._put_named_file)
228

    
229
    def open_index(self):
230
        """Open the index for this repository.
231

232
        :raise NoIndexPresent: If no index is present
233
        :return: The matching `Index`
234
        """
235
        raise NotImplementedError(self.open_index)
236

    
237
    def fetch(self, target, determine_wants=None, progress=None):
238
        """Fetch objects into another repository.
239

240
        :param target: The target repository
241
        :param determine_wants: Optional function to determine what refs to
242
            fetch.
243
        :param progress: Optional progress function
244
        :return: The local refs
245
        """
246
        if determine_wants is None:
247
            determine_wants = target.object_store.determine_wants_all
248
        target.object_store.add_objects(
249
            self.fetch_objects(determine_wants, target.get_graph_walker(),
250
                               progress))
251
        return self.get_refs()
252

    
253
    def fetch_objects(self, determine_wants, graph_walker, progress,
254
                      get_tagged=None):
255
        """Fetch the missing objects required for a set of revisions.
256

257
        :param determine_wants: Function that takes a dictionary with heads
258
            and returns the list of heads to fetch.
259
        :param graph_walker: Object that can iterate over the list of revisions
260
            to fetch and has an "ack" method that will be called to acknowledge
261
            that a revision is present.
262
        :param progress: Simple progress function that will be called with
263
            updated progress strings.
264
        :param get_tagged: Function that returns a dict of pointed-to sha -> tag
265
            sha for including tags.
266
        :return: iterator over objects, with __len__ implemented
267
        """
268
        wants = determine_wants(self.get_refs())
269
        if not isinstance(wants, list):
270
            raise TypeError("determine_wants() did not return a list")
271

    
272
        shallows = getattr(graph_walker, 'shallow', frozenset())
273
        unshallows = getattr(graph_walker, 'unshallow', frozenset())
274

    
275
        if wants == []:
276
            # TODO(dborowitz): find a way to short-circuit that doesn't change
277
            # this interface.
278

    
279
            if shallows or unshallows:
280
                # Do not send a pack in shallow short-circuit path
281
                return None
282

    
283
            return []
284

    
285
        # If the graph walker is set up with an implementation that can
286
        # ACK/NAK to the wire, it will write data to the client through
287
        # this call as a side-effect.
288
        haves = self.object_store.find_common_revisions(graph_walker)
289

    
290
        # Deal with shallow requests separately because the haves do
291
        # not reflect what objects are missing
292
        if shallows or unshallows:
293
            haves = []  # TODO: filter the haves commits from iter_shas.
294
                        # the specific commits aren't missing.
295

    
296
        def get_parents(commit):
297
            if commit.id in shallows:
298
                return []
299
            return self.get_parents(commit.id, commit)
300

    
301
        return self.object_store.iter_shas(
302
          self.object_store.find_missing_objects(
303
              haves, wants, progress,
304
              get_tagged,
305
              get_parents=get_parents))
306

    
307
    def get_graph_walker(self, heads=None):
308
        """Retrieve a graph walker.
309

310
        A graph walker is used by a remote repository (or proxy)
311
        to find out which objects are present in this repository.
312

313
        :param heads: Repository heads to use (optional)
314
        :return: A graph walker object
315
        """
316
        if heads is None:
317
            heads = self.refs.as_dict(b'refs/heads').values()
318
        return ObjectStoreGraphWalker(heads, self.get_parents)
319

    
320
    def get_refs(self):
321
        """Get dictionary with all refs.
322

323
        :return: A ``dict`` mapping ref names to SHA1s
324
        """
325
        return self.refs.as_dict()
326

    
327
    def head(self):
328
        """Return the SHA1 pointed at by HEAD."""
329
        return self.refs[b'HEAD']
330

    
331
    def _get_object(self, sha, cls):
332
        assert len(sha) in (20, 40)
333
        ret = self.get_object(sha)
334
        if not isinstance(ret, cls):
335
            if cls is Commit:
336
                raise NotCommitError(ret)
337
            elif cls is Blob:
338
                raise NotBlobError(ret)
339
            elif cls is Tree:
340
                raise NotTreeError(ret)
341
            elif cls is Tag:
342
                raise NotTagError(ret)
343
            else:
344
                raise Exception("Type invalid: %r != %r" % (
345
                  ret.type_name, cls.type_name))
346
        return ret
347

    
348
    def get_object(self, sha):
349
        """Retrieve the object with the specified SHA.
350

351
        :param sha: SHA to retrieve
352
        :return: A ShaFile object
353
        :raise KeyError: when the object can not be found
354
        """
355
        return self.object_store[sha]
356

    
357
    def get_parents(self, sha, commit=None):
358
        """Retrieve the parents of a specific commit.
359

360
        If the specific commit is a graftpoint, the graft parents
361
        will be returned instead.
362

363
        :param sha: SHA of the commit for which to retrieve the parents
364
        :param commit: Optional commit matching the sha
365
        :return: List of parents
366
        """
367

    
368
        try:
369
            return self._graftpoints[sha]
370
        except KeyError:
371
            if commit is None:
372
                commit = self[sha]
373
            return commit.parents
374

    
375
    def get_config(self):
376
        """Retrieve the config object.
377

378
        :return: `ConfigFile` object for the ``.git/config`` file.
379
        """
380
        raise NotImplementedError(self.get_config)
381

    
382
    def get_description(self):
383
        """Retrieve the description for this repository.
384

385
        :return: String with the description of the repository
386
            as set by the user.
387
        """
388
        raise NotImplementedError(self.get_description)
389

    
390
    def set_description(self, description):
391
        """Set the description for this repository.
392

393
        :param description: Text to set as description for this repository.
394
        """
395
        raise NotImplementedError(self.set_description)
396

    
397
    def get_config_stack(self):
398
        """Return a config stack for this repository.
399

400
        This stack accesses the configuration for both this repository
401
        itself (.git/config) and the global configuration, which usually
402
        lives in ~/.gitconfig.
403

404
        :return: `Config` instance for this repository
405
        """
406
        from dulwich.config import StackedConfig
407
        backends = [self.get_config()] + StackedConfig.default_backends()
408
        return StackedConfig(backends, writable=backends[0])
409

    
410
    def get_peeled(self, ref):
411
        """Get the peeled value of a ref.
412

413
        :param ref: The refname to peel.
414
        :return: The fully-peeled SHA1 of a tag object, after peeling all
415
            intermediate tags; if the original ref does not point to a tag, this
416
            will equal the original SHA1.
417
        """
418
        cached = self.refs.get_peeled(ref)
419
        if cached is not None:
420
            return cached
421
        return self.object_store.peel_sha(self.refs[ref]).id
422

    
423
    def get_walker(self, include=None, *args, **kwargs):
424
        """Obtain a walker for this repository.
425

426
        :param include: Iterable of SHAs of commits to include along with their
427
            ancestors. Defaults to [HEAD]
428
        :param exclude: Iterable of SHAs of commits to exclude along with their
429
            ancestors, overriding includes.
430
        :param order: ORDER_* constant specifying the order of results. Anything
431
            other than ORDER_DATE may result in O(n) memory usage.
432
        :param reverse: If True, reverse the order of output, requiring O(n)
433
            memory.
434
        :param max_entries: The maximum number of entries to yield, or None for
435
            no limit.
436
        :param paths: Iterable of file or subtree paths to show entries for.
437
        :param rename_detector: diff.RenameDetector object for detecting
438
            renames.
439
        :param follow: If True, follow path across renames/copies. Forces a
440
            default rename_detector.
441
        :param since: Timestamp to list commits after.
442
        :param until: Timestamp to list commits before.
443
        :param queue_cls: A class to use for a queue of commits, supporting the
444
            iterator protocol. The constructor takes a single argument, the
445
            Walker.
446
        :return: A `Walker` object
447
        """
448
        from dulwich.walk import Walker
449
        if include is None:
450
            include = [self.head()]
451
        if isinstance(include, str):
452
            include = [include]
453

    
454
        kwargs['get_parents'] = lambda commit: self.get_parents(commit.id, commit)
455

    
456
        return Walker(self.object_store, include, *args, **kwargs)
457

    
458
    def __getitem__(self, name):
459
        """Retrieve a Git object by SHA1 or ref.
460

461
        :param name: A Git object SHA1 or a ref name
462
        :return: A `ShaFile` object, such as a Commit or Blob
463
        :raise KeyError: when the specified ref or object does not exist
464
        """
465
        if not isinstance(name, bytes):
466
            raise TypeError("'name' must be bytestring, not %.80s" %
467
                    type(name).__name__)
468
        if len(name) in (20, 40):
469
            try:
470
                return self.object_store[name]
471
            except (KeyError, ValueError):
472
                pass
473
        try:
474
            return self.object_store[self.refs[name]]
475
        except RefFormatError:
476
            raise KeyError(name)
477

    
478
    def __contains__(self, name):
479
        """Check if a specific Git object or ref is present.
480

481
        :param name: Git object SHA1 or ref name
482
        """
483
        if len(name) in (20, 40):
484
            return name in self.object_store or name in self.refs
485
        else:
486
            return name in self.refs
487

    
488
    def __setitem__(self, name, value):
489
        """Set a ref.
490

491
        :param name: ref name
492
        :param value: Ref value - either a ShaFile object, or a hex sha
493
        """
494
        if name.startswith(b"refs/") or name == b'HEAD':
495
            if isinstance(value, ShaFile):
496
                self.refs[name] = value.id
497
            elif isinstance(value, bytes):
498
                self.refs[name] = value
499
            else:
500
                raise TypeError(value)
501
        else:
502
            raise ValueError(name)
503

    
504
    def __delitem__(self, name):
505
        """Remove a ref.
506

507
        :param name: Name of the ref to remove
508
        """
509
        if name.startswith(b"refs/") or name == b"HEAD":
510
            del self.refs[name]
511
        else:
512
            raise ValueError(name)
513

    
514
    def _get_user_identity(self):
515
        """Determine the identity to use for new commits.
516
        """
517
        config = self.get_config_stack()
518
        return (config.get((b"user", ), b"name") + b" <" +
519
                config.get((b"user", ), b"email") + b">")
520

    
521
    def _add_graftpoints(self, updated_graftpoints):
522
        """Add or modify graftpoints
523

524
        :param updated_graftpoints: Dict of commit shas to list of parent shas
525
        """
526

    
527
        # Simple validation
528
        for commit, parents in updated_graftpoints.items():
529
            for sha in [commit] + parents:
530
                check_hexsha(sha, 'Invalid graftpoint')
531

    
532
        self._graftpoints.update(updated_graftpoints)
533

    
534
    def _remove_graftpoints(self, to_remove=[]):
535
        """Remove graftpoints
536

537
        :param to_remove: List of commit shas
538
        """
539
        for sha in to_remove:
540
            del self._graftpoints[sha]
541

    
542
    def do_commit(self, message=None, committer=None,
543
                  author=None, commit_timestamp=None,
544
                  commit_timezone=None, author_timestamp=None,
545
                  author_timezone=None, tree=None, encoding=None,
546
                  ref=b'HEAD', merge_heads=None):
547
        """Create a new commit.
548

549
        :param message: Commit message
550
        :param committer: Committer fullname
551
        :param author: Author fullname (defaults to committer)
552
        :param commit_timestamp: Commit timestamp (defaults to now)
553
        :param commit_timezone: Commit timestamp timezone (defaults to GMT)
554
        :param author_timestamp: Author timestamp (defaults to commit timestamp)
555
        :param author_timezone: Author timestamp timezone
556
            (defaults to commit timestamp timezone)
557
        :param tree: SHA1 of the tree root to use (if not specified the
558
            current index will be committed).
559
        :param encoding: Encoding
560
        :param ref: Optional ref to commit to (defaults to current branch)
561
        :param merge_heads: Merge heads (defaults to .git/MERGE_HEADS)
562
        :return: New commit SHA1
563
        """
564
        import time
565
        c = Commit()
566
        if tree is None:
567
            index = self.open_index()
568
            c.tree = index.commit(self.object_store)
569
        else:
570
            if len(tree) != 40:
571
                raise ValueError("tree must be a 40-byte hex sha string")
572
            c.tree = tree
573

    
574
        try:
575
            self.hooks['pre-commit'].execute()
576
        except HookError as e:
577
            raise CommitError(e)
578
        except KeyError:  # no hook defined, silent fallthrough
579
            pass
580

    
581
        if merge_heads is None:
582
            # FIXME: Read merge heads from .git/MERGE_HEADS
583
            merge_heads = []
584
        if committer is None:
585
            # FIXME: Support GIT_COMMITTER_NAME/GIT_COMMITTER_EMAIL environment
586
            # variables
587
            committer = self._get_user_identity()
588
        c.committer = committer
589
        if commit_timestamp is None:
590
            # FIXME: Support GIT_COMMITTER_DATE environment variable
591
            commit_timestamp = time.time()
592
        c.commit_time = int(commit_timestamp)
593
        if commit_timezone is None:
594
            # FIXME: Use current user timezone rather than UTC
595
            commit_timezone = 0
596
        c.commit_timezone = commit_timezone
597
        if author is None:
598
            # FIXME: Support GIT_AUTHOR_NAME/GIT_AUTHOR_EMAIL environment
599
            # variables
600
            author = committer
601
        c.author = author
602
        if author_timestamp is None:
603
            # FIXME: Support GIT_AUTHOR_DATE environment variable
604
            author_timestamp = commit_timestamp
605
        c.author_time = int(author_timestamp)
606
        if author_timezone is None:
607
            author_timezone = commit_timezone
608
        c.author_timezone = author_timezone
609
        if encoding is not None:
610
            c.encoding = encoding
611
        if message is None:
612
            # FIXME: Try to read commit message from .git/MERGE_MSG
613
            raise ValueError("No commit message specified")
614

    
615
        try:
616
            c.message = self.hooks['commit-msg'].execute(message)
617
            if c.message is None:
618
                c.message = message
619
        except HookError as e:
620
            raise CommitError(e)
621
        except KeyError:  # no hook defined, message not modified
622
            c.message = message
623

    
624
        if ref is None:
625
            # Create a dangling commit
626
            c.parents = merge_heads
627
            self.object_store.add_object(c)
628
        else:
629
            try:
630
                old_head = self.refs[ref]
631
                c.parents = [old_head] + merge_heads
632
                self.object_store.add_object(c)
633
                ok = self.refs.set_if_equals(ref, old_head, c.id)
634
            except KeyError:
635
                c.parents = merge_heads
636
                self.object_store.add_object(c)
637
                ok = self.refs.add_if_new(ref, c.id)
638
            if not ok:
639
                # Fail if the atomic compare-and-swap failed, leaving the commit and
640
                # all its objects as garbage.
641
                raise CommitError("%s changed during commit" % (ref,))
642

    
643
        try:
644
            self.hooks['post-commit'].execute()
645
        except HookError as e:  # silent failure
646
            warnings.warn("post-commit hook failed: %s" % e, UserWarning)
647
        except KeyError:  # no hook defined, silent fallthrough
648
            pass
649

    
650
        return c.id
651

    
652

    
653

    
654
def read_gitfile(f):
655
    """Read a ``.git`` file.
656

657
    The first line of the file should start with "gitdir: "
658

659
    :param f: File-like object to read from
660
    :return: A path
661
    """
662
    cs = f.read()
663
    if not cs.startswith("gitdir: "):
664
        raise ValueError("Expected file to start with 'gitdir: '")
665
    return cs[len("gitdir: "):].rstrip("\n")
666

    
667

    
668
class Repo(BaseRepo):
669
    """A git repository backed by local disk.
670

671
    To open an existing repository, call the contructor with
672
    the path of the repository.
673

674
    To create a new repository, use the Repo.init class method.
675
    """
676

    
677
    def __init__(self, root):
678
        hidden_path = os.path.join(root, CONTROLDIR)
679
        if os.path.isdir(os.path.join(hidden_path, OBJECTDIR)):
680
            self.bare = False
681
            self._controldir = hidden_path
682
        elif (os.path.isdir(os.path.join(root, OBJECTDIR)) and
683
              os.path.isdir(os.path.join(root, REFSDIR))):
684
            self.bare = True
685
            self._controldir = root
686
        elif os.path.isfile(hidden_path):
687
            self.bare = False
688
            with open(hidden_path, 'r') as f:
689
                path = read_gitfile(f)
690
            self.bare = False
691
            self._controldir = os.path.join(root, path)
692
        else:
693
            raise NotGitRepository(
694
                "No git repository was found at %(path)s" % dict(path=root)
695
            )
696
        commondir = self.get_named_file(COMMONDIR)
697
        if commondir is not None:
698
            with commondir:
699
                self._commondir = os.path.join(
700
                    self.controldir(),
701
                    commondir.read().rstrip(b"\r\n").decode(sys.getfilesystemencoding()))
702
        else:
703
            self._commondir = self._controldir
704
        self.path = root
705
        object_store = DiskObjectStore(
706
            os.path.join(self.commondir(), OBJECTDIR))
707
        refs = DiskRefsContainer(self.commondir(), self._controldir)
708
        BaseRepo.__init__(self, object_store, refs)
709

    
710
        self._graftpoints = {}
711
        graft_file = self.get_named_file(os.path.join("info", "grafts"),
712
                                         basedir=self.commondir())
713
        if graft_file:
714
            with graft_file:
715
                self._graftpoints.update(parse_graftpoints(graft_file))
716
        graft_file = self.get_named_file("shallow",
717
                                         basedir=self.commondir())
718
        if graft_file:
719
            with graft_file:
720
                self._graftpoints.update(parse_graftpoints(graft_file))
721

    
722
        self.hooks['pre-commit'] = PreCommitShellHook(self.controldir())
723
        self.hooks['commit-msg'] = CommitMsgShellHook(self.controldir())
724
        self.hooks['post-commit'] = PostCommitShellHook(self.controldir())
725

    
726
    @classmethod
727
    def discover(cls, start='.'):
728
        """Iterate parent directories to discover a repository
729

730
        Return a Repo object for the first parent directory that looks like a
731
        Git repository.
732

733
        :param start: The directory to start discovery from (defaults to '.')
734
        """
735
        remaining = True
736
        path = os.path.abspath(start)
737
        while remaining:
738
            try:
739
                return cls(path)
740
            except NotGitRepository:
741
                path, remaining = os.path.split(path)
742
        raise NotGitRepository(
743
            "No git repository was found at %(path)s" % dict(path=start)
744
        )
745

    
746
    def controldir(self):
747
        """Return the path of the control directory."""
748
        return self._controldir
749

    
750
    def commondir(self):
751
        """Return the path of the common directory.
752

753
        For a main working tree, it is identical to `controldir()`.
754

755
        For a linked working tree, it is the control directory of the
756
        main working tree."""
757

    
758
        return self._commondir
759

    
760
    def _determine_file_mode(self):
761
        """Probe the file-system to determine whether permissions can be trusted.
762

763
        :return: True if permissions can be trusted, False otherwise.
764
        """
765
        fname = os.path.join(self.path, '.probe-permissions')
766
        with open(fname, 'w') as f:
767
            f.write('')
768

    
769
        st1 = os.lstat(fname)
770
        os.chmod(fname, st1.st_mode ^ stat.S_IXUSR)
771
        st2 = os.lstat(fname)
772

    
773
        os.unlink(fname)
774

    
775
        mode_differs = st1.st_mode != st2.st_mode
776
        st2_has_exec = (st2.st_mode & stat.S_IXUSR) != 0
777

    
778
        return mode_differs and st2_has_exec
779

    
780
    def _put_named_file(self, path, contents):
781
        """Write a file to the control dir with the given name and contents.
782

783
        :param path: The path to the file, relative to the control dir.
784
        :param contents: A string to write to the file.
785
        """
786
        path = path.lstrip(os.path.sep)
787
        with GitFile(os.path.join(self.controldir(), path), 'wb') as f:
788
            f.write(contents)
789

    
790
    def get_named_file(self, path, basedir=None):
791
        """Get a file from the control dir with a specific name.
792

793
        Although the filename should be interpreted as a filename relative to
794
        the control dir in a disk-based Repo, the object returned need not be
795
        pointing to a file in that location.
796

797
        :param path: The path to the file, relative to the control dir.
798
        :param basedir: Optional argument that specifies an alternative to the control dir.
799
        :return: An open file object, or None if the file does not exist.
800
        """
801
        # TODO(dborowitz): sanitize filenames, since this is used directly by
802
        # the dumb web serving code.
803
        if basedir is None:
804
            basedir = self.controldir()
805
        path = path.lstrip(os.path.sep)
806
        try:
807
            return open(os.path.join(basedir, path), 'rb')
808
        except (IOError, OSError) as e:
809
            if e.errno == errno.ENOENT:
810
                return None
811
            raise
812

    
813
    def index_path(self):
814
        """Return path to the index file."""
815
        return os.path.join(self.controldir(), INDEX_FILENAME)
816

    
817
    def open_index(self):
818
        """Open the index for this repository.
819

820
        :raise NoIndexPresent: If no index is present
821
        :return: The matching `Index`
822
        """
823
        from dulwich.index import Index
824
        if not self.has_index():
825
            raise NoIndexPresent()
826
        return Index(self.index_path())
827

    
828
    def has_index(self):
829
        """Check if an index is present."""
830
        # Bare repos must never have index files; non-bare repos may have a
831
        # missing index file, which is treated as empty.
832
        return not self.bare
833

    
834
    def stage(self, fs_paths):
835
        """Stage a set of paths.
836

837
        :param fs_paths: List of paths, relative to the repository path
838
        """
839

    
840
        root_path_bytes = self.path.encode(sys.getfilesystemencoding())
841

    
842
        if not isinstance(fs_paths, list):
843
            fs_paths = [fs_paths]
844
        from dulwich.index import (
845
            blob_from_path_and_stat,
846
            index_entry_from_stat,
847
            _fs_to_tree_path,
848
            )
849
        index = self.open_index()
850
        for fs_path in fs_paths:
851
            if not isinstance(fs_path, bytes):
852
                fs_path = fs_path.encode(sys.getfilesystemencoding())
853
            tree_path = _fs_to_tree_path(fs_path)
854
            full_path = os.path.join(root_path_bytes, fs_path)
855
            try:
856
                st = os.lstat(full_path)
857
            except OSError:
858
                # File no longer exists
859
                try:
860
                    del index[tree_path]
861
                except KeyError:
862
                    pass  # already removed
863
            else:
864
                blob = blob_from_path_and_stat(full_path, st)
865
                self.object_store.add_object(blob)
866
                index[tree_path] = index_entry_from_stat(st, blob.id, 0)
867
        index.write()
868

    
869
    def clone(self, target_path, mkdir=True, bare=False,
870
            origin=b"origin"):
871
        """Clone this repository.
872

873
        :param target_path: Target path
874
        :param mkdir: Create the target directory
875
        :param bare: Whether to create a bare repository
876
        :param origin: Base name for refs in target repository
877
            cloned from this repository
878
        :return: Created repository as `Repo`
879
        """
880
        if not bare:
881
            target = self.init(target_path, mkdir=mkdir)
882
        else:
883
            target = self.init_bare(target_path)
884
        self.fetch(target)
885
        target.refs.import_refs(
886
            b'refs/remotes/' + origin, self.refs.as_dict(b'refs/heads'))
887
        target.refs.import_refs(
888
            b'refs/tags', self.refs.as_dict(b'refs/tags'))
889
        try:
890
            target.refs.add_if_new(DEFAULT_REF, self.refs[DEFAULT_REF])
891
        except KeyError:
892
            pass
893
        target_config = target.get_config()
894
        encoded_path = self.path
895
        if not isinstance(encoded_path, bytes):
896
            encoded_path = encoded_path.encode(sys.getfilesystemencoding())
897
        target_config.set((b'remote', b'origin'), b'url', encoded_path)
898
        target_config.set((b'remote', b'origin'), b'fetch',
899
            b'+refs/heads/*:refs/remotes/origin/*')
900
        target_config.write_to_path()
901

    
902
        # Update target head
903
        head_chain, head_sha = self.refs.follow(b'HEAD')
904
        if head_chain and head_sha is not None:
905
            target.refs.set_symbolic_ref(b'HEAD', head_chain[-1])
906
            target[b'HEAD'] = head_sha
907

    
908
            if not bare:
909
                # Checkout HEAD to target dir
910
                target.reset_index()
911

    
912
        return target
913

    
914
    def reset_index(self, tree=None):
915
        """Reset the index back to a specific tree.
916

917
        :param tree: Tree SHA to reset to, None for current HEAD tree.
918
        """
919
        from dulwich.index import (
920
            build_index_from_tree,
921
            validate_path_element_default,
922
            validate_path_element_ntfs,
923
            )
924
        if tree is None:
925
            tree = self[b'HEAD'].tree
926
        config = self.get_config()
927
        honor_filemode = config.get_boolean('core', 'filemode', os.name != "nt")
928
        if config.get_boolean('core', 'core.protectNTFS', os.name == "nt"):
929
            validate_path_element = validate_path_element_ntfs
930
        else:
931
            validate_path_element = validate_path_element_default
932
        return build_index_from_tree(self.path, self.index_path(),
933
                self.object_store, tree, honor_filemode=honor_filemode,
934
                validate_path_element=validate_path_element)
935

    
936
    def get_config(self):
937
        """Retrieve the config object.
938

939
        :return: `ConfigFile` object for the ``.git/config`` file.
940
        """
941
        from dulwich.config import ConfigFile
942
        path = os.path.join(self._controldir, 'config')
943
        try:
944
            return ConfigFile.from_path(path)
945
        except (IOError, OSError) as e:
946
            if e.errno != errno.ENOENT:
947
                raise
948
            ret = ConfigFile()
949
            ret.path = path
950
            return ret
951

    
952
    def get_description(self):
953
        """Retrieve the description of this repository.
954

955
        :return: A string describing the repository or None.
956
        """
957
        path = os.path.join(self._controldir, 'description')
958
        try:
959
            with GitFile(path, 'rb') as f:
960
                return f.read()
961
        except (IOError, OSError) as e:
962
            if e.errno != errno.ENOENT:
963
                raise
964
            return None
965

    
966
    def __repr__(self):
967
        return "<Repo at %r>" % self.path
968

    
969
    def set_description(self, description):
970
        """Set the description for this repository.
971

972
        :param description: Text to set as description for this repository.
973
        """
974

    
975
        self._put_named_file('description', description)
976

    
977
    @classmethod
978
    def _init_maybe_bare(cls, path, bare):
979
        for d in BASE_DIRECTORIES:
980
            os.mkdir(os.path.join(path, *d))
981
        DiskObjectStore.init(os.path.join(path, OBJECTDIR))
982
        ret = cls(path)
983
        ret.refs.set_symbolic_ref(b'HEAD', DEFAULT_REF)
984
        ret._init_files(bare)
985
        return ret
986

    
987
    @classmethod
988
    def init(cls, path, mkdir=False):
989
        """Create a new repository.
990

991
        :param path: Path in which to create the repository
992
        :param mkdir: Whether to create the directory
993
        :return: `Repo` instance
994
        """
995
        if mkdir:
996
            os.mkdir(path)
997
        controldir = os.path.join(path, CONTROLDIR)
998
        os.mkdir(controldir)
999
        cls._init_maybe_bare(controldir, False)
1000
        return cls(path)
1001

    
1002
    @classmethod
1003
    def _init_new_working_directory(cls, path, main_repo, identifier=None, mkdir=False):
1004
        """Create a new working directory linked to a repository.
1005

1006
        :param path: Path in which to create the working tree.
1007
        :param main_repo: Main repository to reference
1008
        :param identifier: Worktree identifier
1009
        :param mkdir: Whether to create the directory
1010
        :return: `Repo` instance
1011
        """
1012
        if mkdir:
1013
            os.mkdir(path)
1014
        if identifier is None:
1015
            identifier = os.path.basename(path)
1016
        main_worktreesdir = os.path.join(main_repo.controldir(), WORKTREES)
1017
        worktree_controldir = os.path.join(main_worktreesdir, identifier)
1018
        gitdirfile = os.path.join(path, CONTROLDIR)
1019
        with open(gitdirfile, 'wb') as f:
1020
            f.write(b'gitdir: ' +
1021
                    worktree_controldir.encode(sys.getfilesystemencoding()) +
1022
                    b'\n')
1023
        try:
1024
            os.mkdir(main_worktreesdir)
1025
        except OSError as e:
1026
            if e.errno != errno.EEXIST:
1027
                raise
1028
        try:
1029
            os.mkdir(worktree_controldir)
1030
        except OSError as e:
1031
            if e.errno != errno.EEXIST:
1032
                raise
1033
        with open(os.path.join(worktree_controldir, GITDIR), 'wb') as f:
1034
            f.write(gitdirfile.encode(sys.getfilesystemencoding()) + b'\n')
1035
        with open(os.path.join(worktree_controldir, COMMONDIR), 'wb') as f:
1036
            f.write(b'../..\n')
1037
        with open(os.path.join(worktree_controldir, 'HEAD'), 'wb') as f:
1038
            f.write(main_repo.head() + b'\n')
1039
        r = cls(path)
1040
        r.reset_index()
1041
        return r
1042

    
1043
    @classmethod
1044
    def init_bare(cls, path):
1045
        """Create a new bare repository.
1046

1047
        ``path`` should already exist and be an emty directory.
1048

1049
        :param path: Path to create bare repository in
1050
        :return: a `Repo` instance
1051
        """
1052
        return cls._init_maybe_bare(path, True)
1053

    
1054
    create = init_bare
1055

    
1056
    def close(self):
1057
        """Close any files opened by this repository."""
1058
        self.object_store.close()
1059

    
1060
    def __enter__(self):
1061
        return self
1062

    
1063
    def __exit__(self, exc_type, exc_val, exc_tb):
1064
        self.close()
1065

    
1066

    
1067
class MemoryRepo(BaseRepo):
1068
    """Repo that stores refs, objects, and named files in memory.
1069

1070
    MemoryRepos are always bare: they have no working tree and no index, since
1071
    those have a stronger dependency on the filesystem.
1072
    """
1073

    
1074
    def __init__(self):
1075
        from dulwich.config import ConfigFile
1076
        BaseRepo.__init__(self, MemoryObjectStore(), DictRefsContainer({}))
1077
        self._named_files = {}
1078
        self.bare = True
1079
        self._config = ConfigFile()
1080

    
1081
    def _determine_file_mode(self):
1082
        """Probe the file-system to determine whether permissions can be trusted.
1083

1084
        :return: True if permissions can be trusted, False otherwise.
1085
        """
1086
        return sys.platform != 'win32'
1087

    
1088
    def _put_named_file(self, path, contents):
1089
        """Write a file to the control dir with the given name and contents.
1090

1091
        :param path: The path to the file, relative to the control dir.
1092
        :param contents: A string to write to the file.
1093
        """
1094
        self._named_files[path] = contents
1095

    
1096
    def get_named_file(self, path):
1097
        """Get a file from the control dir with a specific name.
1098

1099
        Although the filename should be interpreted as a filename relative to
1100
        the control dir in a disk-baked Repo, the object returned need not be
1101
        pointing to a file in that location.
1102

1103
        :param path: The path to the file, relative to the control dir.
1104
        :return: An open file object, or None if the file does not exist.
1105
        """
1106
        contents = self._named_files.get(path, None)
1107
        if contents is None:
1108
            return None
1109
        return BytesIO(contents)
1110

    
1111
    def open_index(self):
1112
        """Fail to open index for this repo, since it is bare.
1113

1114
        :raise NoIndexPresent: Raised when no index is present
1115
        """
1116
        raise NoIndexPresent()
1117

    
1118
    def get_config(self):
1119
        """Retrieve the config object.
1120

1121
        :return: `ConfigFile` object.
1122
        """
1123
        return self._config
1124

    
1125
    def get_description(self):
1126
        """Retrieve the repository description.
1127

1128
        This defaults to None, for no description.
1129
        """
1130
        return None
1131

    
1132
    @classmethod
1133
    def init_bare(cls, objects, refs):
1134
        """Create a new bare repository in memory.
1135

1136
        :param objects: Objects for the new repository,
1137
            as iterable
1138
        :param refs: Refs as dictionary, mapping names
1139
            to object SHA1s
1140
        """
1141
        ret = cls()
1142
        for obj in objects:
1143
            ret.object_store.add_object(obj)
1144
        for refname, sha in refs.items():
1145
            ret.refs[refname] = sha
1146
        ret._init_files(bare=True)
1147
        return ret