hgext/git/dirstate.py
changeset 44477 ad718271a9eb
child 44484 ec54b3d2af0b
equal deleted inserted replaced
44470:a08bbdf839ae 44477:ad718271a9eb
       
     1 from __future__ import absolute_import
       
     2 
       
     3 import contextlib
       
     4 import errno
       
     5 import os
       
     6 
       
     7 import pygit2
       
     8 
       
     9 from mercurial import (
       
    10     error,
       
    11     extensions,
       
    12     match as matchmod,
       
    13     node as nodemod,
       
    14     pycompat,
       
    15     scmutil,
       
    16     util,
       
    17 )
       
    18 from mercurial.interfaces import (
       
    19     dirstate as intdirstate,
       
    20     util as interfaceutil,
       
    21 )
       
    22 
       
    23 from . import gitutil
       
    24 
       
    25 
       
    26 def readpatternfile(orig, filepath, warn, sourceinfo=False):
       
    27     if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')):
       
    28         return orig(filepath, warn, sourceinfo=False)
       
    29     result = []
       
    30     warnings = []
       
    31     with open(filepath, b'rb') as fp:
       
    32         for l in fp:
       
    33             l = l.strip()
       
    34             if not l or l.startswith(b'#'):
       
    35                 continue
       
    36             if l.startswith(b'!'):
       
    37                 warnings.append(b'unsupported ignore pattern %s' % l)
       
    38                 continue
       
    39             if l.startswith(b'/'):
       
    40                 result.append(b'rootglob:' + l[1:])
       
    41             else:
       
    42                 result.append(b'relglob:' + l)
       
    43     return result, warnings
       
    44 
       
    45 
       
    46 extensions.wrapfunction(matchmod, b'readpatternfile', readpatternfile)
       
    47 
       
    48 
       
    49 _STATUS_MAP = {
       
    50     pygit2.GIT_STATUS_CONFLICTED: b'm',
       
    51     pygit2.GIT_STATUS_CURRENT: b'n',
       
    52     pygit2.GIT_STATUS_IGNORED: b'?',
       
    53     pygit2.GIT_STATUS_INDEX_DELETED: b'r',
       
    54     pygit2.GIT_STATUS_INDEX_MODIFIED: b'n',
       
    55     pygit2.GIT_STATUS_INDEX_NEW: b'a',
       
    56     pygit2.GIT_STATUS_INDEX_RENAMED: b'a',
       
    57     pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n',
       
    58     pygit2.GIT_STATUS_WT_DELETED: b'r',
       
    59     pygit2.GIT_STATUS_WT_MODIFIED: b'n',
       
    60     pygit2.GIT_STATUS_WT_NEW: b'?',
       
    61     pygit2.GIT_STATUS_WT_RENAMED: b'a',
       
    62     pygit2.GIT_STATUS_WT_TYPECHANGE: b'n',
       
    63     pygit2.GIT_STATUS_WT_UNREADABLE: b'?',
       
    64     pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: 'm',
       
    65 }
       
    66 
       
    67 
       
    68 @interfaceutil.implementer(intdirstate.idirstate)
       
    69 class gitdirstate(object):
       
    70     def __init__(self, ui, root, gitrepo):
       
    71         self._ui = ui
       
    72         self._root = os.path.dirname(root)
       
    73         self.git = gitrepo
       
    74         self._plchangecallbacks = {}
       
    75 
       
    76     def p1(self):
       
    77         return self.git.head.peel().id.raw
       
    78 
       
    79     def p2(self):
       
    80         # TODO: MERGE_HEAD? something like that, right?
       
    81         return nodemod.nullid
       
    82 
       
    83     def setparents(self, p1, p2=nodemod.nullid):
       
    84         assert p2 == nodemod.nullid, b'TODO merging support'
       
    85         self.git.head.set_target(gitutil.togitnode(p1))
       
    86 
       
    87     @util.propertycache
       
    88     def identity(self):
       
    89         return util.filestat.frompath(
       
    90             os.path.join(self._root, b'.git', b'index')
       
    91         )
       
    92 
       
    93     def branch(self):
       
    94         return b'default'
       
    95 
       
    96     def parents(self):
       
    97         # TODO how on earth do we find p2 if a merge is in flight?
       
    98         return self.p1(), nodemod.nullid
       
    99 
       
   100     def __iter__(self):
       
   101         return (pycompat.fsencode(f.path) for f in self.git.index)
       
   102 
       
   103     def items(self):
       
   104         for ie in self.git.index:
       
   105             yield ie.path, None  # value should be a dirstatetuple
       
   106 
       
   107     # py2,3 compat forward
       
   108     iteritems = items
       
   109 
       
   110     def __getitem__(self, filename):
       
   111         try:
       
   112             gs = self.git.status_file(filename)
       
   113         except KeyError:
       
   114             return b'?'
       
   115         return _STATUS_MAP[gs]
       
   116 
       
   117     def __contains__(self, filename):
       
   118         try:
       
   119             gs = self.git.status_file(filename)
       
   120             return _STATUS_MAP[gs] != b'?'
       
   121         except KeyError:
       
   122             return False
       
   123 
       
   124     def status(self, match, subrepos, ignored, clean, unknown):
       
   125         # TODO handling of clean files - can we get that from git.status()?
       
   126         modified, added, removed, deleted, unknown, ignored, clean = (
       
   127             [],
       
   128             [],
       
   129             [],
       
   130             [],
       
   131             [],
       
   132             [],
       
   133             [],
       
   134         )
       
   135         gstatus = self.git.status()
       
   136         for path, status in gstatus.items():
       
   137             path = pycompat.fsencode(path)
       
   138             if status == pygit2.GIT_STATUS_IGNORED:
       
   139                 if path.endswith(b'/'):
       
   140                     continue
       
   141                 ignored.append(path)
       
   142             elif status in (
       
   143                 pygit2.GIT_STATUS_WT_MODIFIED,
       
   144                 pygit2.GIT_STATUS_INDEX_MODIFIED,
       
   145                 pygit2.GIT_STATUS_WT_MODIFIED
       
   146                 | pygit2.GIT_STATUS_INDEX_MODIFIED,
       
   147             ):
       
   148                 modified.append(path)
       
   149             elif status == pygit2.GIT_STATUS_INDEX_NEW:
       
   150                 added.append(path)
       
   151             elif status == pygit2.GIT_STATUS_WT_NEW:
       
   152                 unknown.append(path)
       
   153             elif status == pygit2.GIT_STATUS_WT_DELETED:
       
   154                 deleted.append(path)
       
   155             elif status == pygit2.GIT_STATUS_INDEX_DELETED:
       
   156                 removed.append(path)
       
   157             else:
       
   158                 raise error.Abort(
       
   159                     b'unhandled case: status for %r is %r' % (path, status)
       
   160                 )
       
   161 
       
   162         # TODO are we really always sure of status here?
       
   163         return (
       
   164             False,
       
   165             scmutil.status(
       
   166                 modified, added, removed, deleted, unknown, ignored, clean
       
   167             ),
       
   168         )
       
   169 
       
   170     def flagfunc(self, buildfallback):
       
   171         # TODO we can do better
       
   172         return buildfallback()
       
   173 
       
   174     def getcwd(self):
       
   175         # TODO is this a good way to do this?
       
   176         return os.path.dirname(
       
   177             os.path.dirname(pycompat.fsencode(self.git.path))
       
   178         )
       
   179 
       
   180     def normalize(self, path):
       
   181         normed = util.normcase(path)
       
   182         assert normed == path, b"TODO handling of case folding: %s != %s" % (
       
   183             normed,
       
   184             path,
       
   185         )
       
   186         return path
       
   187 
       
   188     @property
       
   189     def _checklink(self):
       
   190         return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path)))
       
   191 
       
   192     def copies(self):
       
   193         # TODO support copies?
       
   194         return {}
       
   195 
       
   196     # # TODO what the heck is this
       
   197     _filecache = set()
       
   198 
       
   199     def pendingparentchange(self):
       
   200         # TODO: we need to implement the context manager bits and
       
   201         # correctly stage/revert index edits.
       
   202         return False
       
   203 
       
   204     def write(self, tr):
       
   205         # TODO: call parent change callbacks
       
   206 
       
   207         if tr:
       
   208 
       
   209             def writeinner(category):
       
   210                 self.git.index.write()
       
   211 
       
   212             tr.addpending(b'gitdirstate', writeinner)
       
   213         else:
       
   214             self.git.index.write()
       
   215 
       
   216     def pathto(self, f, cwd=None):
       
   217         if cwd is None:
       
   218             cwd = self.getcwd()
       
   219         # TODO core dirstate does something about slashes here
       
   220         assert isinstance(f, bytes)
       
   221         r = util.pathto(self._root, cwd, f)
       
   222         return r
       
   223 
       
   224     def matches(self, match):
       
   225         for x in self.git.index:
       
   226             p = pycompat.fsencode(x.path)
       
   227             if match(p):
       
   228                 yield p
       
   229 
       
   230     def normal(self, f, parentfiledata=None):
       
   231         """Mark a file normal and clean."""
       
   232         # TODO: for now we just let libgit2 re-stat the file. We can
       
   233         # clearly do better.
       
   234 
       
   235     def normallookup(self, f):
       
   236         """Mark a file normal, but possibly dirty."""
       
   237         # TODO: for now we just let libgit2 re-stat the file. We can
       
   238         # clearly do better.
       
   239 
       
   240     def walk(self, match, subrepos, unknown, ignored, full=True):
       
   241         # TODO: we need to use .status() and not iterate the index,
       
   242         # because the index doesn't force a re-walk and so `hg add` of
       
   243         # a new file without an intervening call to status will
       
   244         # silently do nothing.
       
   245         r = {}
       
   246         cwd = self.getcwd()
       
   247         for path, status in self.git.status().items():
       
   248             if path.startswith('.hg/'):
       
   249                 continue
       
   250             path = pycompat.fsencode(path)
       
   251             if not match(path):
       
   252                 continue
       
   253             # TODO construct the stat info from the status object?
       
   254             try:
       
   255                 s = os.stat(os.path.join(cwd, path))
       
   256             except OSError as e:
       
   257                 if e.errno != errno.ENOENT:
       
   258                     raise
       
   259                 continue
       
   260             r[path] = s
       
   261         return r
       
   262 
       
   263     def savebackup(self, tr, backupname):
       
   264         # TODO: figure out a strategy for saving index backups.
       
   265         pass
       
   266 
       
   267     def restorebackup(self, tr, backupname):
       
   268         # TODO: figure out a strategy for saving index backups.
       
   269         pass
       
   270 
       
   271     def add(self, f):
       
   272         self.git.index.add(pycompat.fsdecode(f))
       
   273 
       
   274     def drop(self, f):
       
   275         self.git.index.remove(pycompat.fsdecode(f))
       
   276 
       
   277     def remove(self, f):
       
   278         self.git.index.remove(pycompat.fsdecode(f))
       
   279 
       
   280     def copied(self, path):
       
   281         # TODO: track copies?
       
   282         return None
       
   283 
       
   284     @contextlib.contextmanager
       
   285     def parentchange(self):
       
   286         # TODO: track this maybe?
       
   287         yield
       
   288 
       
   289     def addparentchangecallback(self, category, callback):
       
   290         # TODO: should this be added to the dirstate interface?
       
   291         self._plchangecallbacks[category] = callback
       
   292 
       
   293     def clearbackup(self, tr, backupname):
       
   294         # TODO
       
   295         pass