dirstate.py
429 lines
| 12.5 KiB
| text/x-python
|
PythonLexer
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Augie Fackler
|
r44961 | import contextlib | ||
import os | ||||
Matt Harbison
|
r52822 | from typing import ( | ||
Any, | ||||
Dict, | ||||
Iterable, | ||||
Iterator, | ||||
List, | ||||
Optional, | ||||
Tuple, | ||||
) | ||||
Joerg Sonnenberger
|
r47771 | from mercurial.node import sha1nodeconstants | ||
Augie Fackler
|
r44961 | from mercurial import ( | ||
Matt Harbison
|
r49968 | dirstatemap, | ||
Augie Fackler
|
r44961 | error, | ||
extensions, | ||||
match as matchmod, | ||||
pycompat, | ||||
scmutil, | ||||
util, | ||||
) | ||||
Matt Harbison
|
r49968 | from mercurial.dirstateutils import ( | ||
timestamp, | ||||
) | ||||
Augie Fackler
|
r44961 | from mercurial.interfaces import ( | ||
dirstate as intdirstate, | ||||
) | ||||
from . import gitutil | ||||
Matt Harbison
|
r49968 | |||
DirstateItem = dirstatemap.DirstateItem | ||||
propertycache = util.propertycache | ||||
Martin von Zweigbergk
|
r44968 | pygit2 = gitutil.get_pygit2() | ||
Augie Fackler
|
r44961 | |||
def readpatternfile(orig, filepath, warn, sourceinfo=False): | ||||
if not (b'info/exclude' in filepath or filepath.endswith(b'.gitignore')): | ||||
return orig(filepath, warn, sourceinfo=False) | ||||
result = [] | ||||
warnings = [] | ||||
Matt Harbison
|
r49969 | with open(filepath, 'rb') as fp: | ||
Augie Fackler
|
r44961 | for l in fp: | ||
l = l.strip() | ||||
if not l or l.startswith(b'#'): | ||||
continue | ||||
if l.startswith(b'!'): | ||||
warnings.append(b'unsupported ignore pattern %s' % l) | ||||
continue | ||||
if l.startswith(b'/'): | ||||
result.append(b'rootglob:' + l[1:]) | ||||
else: | ||||
result.append(b'relglob:' + l) | ||||
return result, warnings | ||||
r51672 | extensions.wrapfunction(matchmod, 'readpatternfile', readpatternfile) | |||
Augie Fackler
|
r44961 | |||
Martin von Zweigbergk
|
r44968 | _STATUS_MAP = {} | ||
if pygit2: | ||||
_STATUS_MAP = { | ||||
pygit2.GIT_STATUS_CONFLICTED: b'm', | ||||
pygit2.GIT_STATUS_CURRENT: b'n', | ||||
pygit2.GIT_STATUS_IGNORED: b'?', | ||||
pygit2.GIT_STATUS_INDEX_DELETED: b'r', | ||||
pygit2.GIT_STATUS_INDEX_MODIFIED: b'n', | ||||
pygit2.GIT_STATUS_INDEX_NEW: b'a', | ||||
pygit2.GIT_STATUS_INDEX_RENAMED: b'a', | ||||
pygit2.GIT_STATUS_INDEX_TYPECHANGE: b'n', | ||||
pygit2.GIT_STATUS_WT_DELETED: b'r', | ||||
pygit2.GIT_STATUS_WT_MODIFIED: b'n', | ||||
pygit2.GIT_STATUS_WT_NEW: b'?', | ||||
pygit2.GIT_STATUS_WT_RENAMED: b'a', | ||||
pygit2.GIT_STATUS_WT_TYPECHANGE: b'n', | ||||
pygit2.GIT_STATUS_WT_UNREADABLE: b'?', | ||||
Matt Harbison
|
r47828 | pygit2.GIT_STATUS_INDEX_MODIFIED | pygit2.GIT_STATUS_WT_MODIFIED: b'm', | ||
Martin von Zweigbergk
|
r44968 | } | ||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r52818 | class gitdirstate(intdirstate.idirstate): | ||
Matt Harbison
|
r49968 | def __init__(self, ui, vfs, gitrepo, use_dirstate_v2): | ||
Augie Fackler
|
r44961 | self._ui = ui | ||
Matt Harbison
|
r49968 | self._root = os.path.dirname(vfs.base) | ||
self._opener = vfs | ||||
Augie Fackler
|
r44961 | self.git = gitrepo | ||
self._plchangecallbacks = {} | ||||
Augie Fackler
|
r48571 | # TODO: context.poststatusfixup is bad and uses this attribute | ||
self._dirty = False | ||||
Matt Harbison
|
r49968 | self._mapcls = dirstatemap.dirstatemap | ||
self._use_dirstate_v2 = use_dirstate_v2 | ||||
@propertycache | ||||
def _map(self): | ||||
"""Return the dirstate contents (see documentation for dirstatemap).""" | ||||
self._map = self._mapcls( | ||||
self._ui, | ||||
self._opener, | ||||
self._root, | ||||
sha1nodeconstants, | ||||
self._use_dirstate_v2, | ||||
) | ||||
return self._map | ||||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r52822 | def p1(self) -> bytes: | ||
Augie Fackler
|
r44976 | try: | ||
return self.git.head.peel().id.raw | ||||
except pygit2.GitError: | ||||
# Typically happens when peeling HEAD fails, as in an | ||||
# empty repository. | ||||
Joerg Sonnenberger
|
r47771 | return sha1nodeconstants.nullid | ||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r52822 | def p2(self) -> bytes: | ||
Augie Fackler
|
r44961 | # TODO: MERGE_HEAD? something like that, right? | ||
Joerg Sonnenberger
|
r47771 | return sha1nodeconstants.nullid | ||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r52822 | def setparents(self, p1: bytes, p2: Optional[bytes] = None): | ||
Joerg Sonnenberger
|
r47771 | if p2 is None: | ||
p2 = sha1nodeconstants.nullid | ||||
assert p2 == sha1nodeconstants.nullid, b'TODO merging support' | ||||
Augie Fackler
|
r44961 | self.git.head.set_target(gitutil.togitnode(p1)) | ||
@util.propertycache | ||||
def identity(self): | ||||
return util.filestat.frompath( | ||||
os.path.join(self._root, b'.git', b'index') | ||||
) | ||||
Matt Harbison
|
r52822 | def branch(self) -> bytes: | ||
Augie Fackler
|
r44961 | return b'default' | ||
Matt Harbison
|
r52822 | def parents(self) -> List[bytes]: | ||
Augie Fackler
|
r44961 | # TODO how on earth do we find p2 if a merge is in flight? | ||
Matt Harbison
|
r52820 | return [self.p1(), sha1nodeconstants.nullid] | ||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r52822 | def __iter__(self) -> Iterator[bytes]: | ||
Augie Fackler
|
r44961 | return (pycompat.fsencode(f.path) for f in self.git.index) | ||
Matt Harbison
|
r52822 | def items(self) -> Iterator[Tuple[bytes, intdirstate.DirstateItemT]]: | ||
Augie Fackler
|
r44961 | for ie in self.git.index: | ||
r48328 | yield ie.path, None # value should be a DirstateItem | |||
Augie Fackler
|
r44961 | |||
# py2,3 compat forward | ||||
iteritems = items | ||||
def __getitem__(self, filename): | ||||
try: | ||||
gs = self.git.status_file(filename) | ||||
except KeyError: | ||||
return b'?' | ||||
return _STATUS_MAP[gs] | ||||
Matt Harbison
|
r52822 | def __contains__(self, filename: Any) -> bool: | ||
Augie Fackler
|
r44961 | try: | ||
gs = self.git.status_file(filename) | ||||
return _STATUS_MAP[gs] != b'?' | ||||
except KeyError: | ||||
return False | ||||
Matt Harbison
|
r52822 | def status( | ||
self, | ||||
match: matchmod.basematcher, | ||||
subrepos: bool, | ||||
ignored: bool, | ||||
clean: bool, | ||||
unknown: bool, | ||||
) -> intdirstate.StatusReturnT: | ||||
Pulkit Goyal
|
r46001 | listclean = clean | ||
Augie Fackler
|
r44961 | # TODO handling of clean files - can we get that from git.status()? | ||
modified, added, removed, deleted, unknown, ignored, clean = ( | ||||
[], | ||||
[], | ||||
[], | ||||
[], | ||||
[], | ||||
[], | ||||
[], | ||||
) | ||||
Matt Harbison
|
r49968 | |||
try: | ||||
mtime_boundary = timestamp.get_fs_now(self._opener) | ||||
except OSError: | ||||
# In largefiles or readonly context | ||||
mtime_boundary = None | ||||
Augie Fackler
|
r44961 | gstatus = self.git.status() | ||
for path, status in gstatus.items(): | ||||
path = pycompat.fsencode(path) | ||||
Augie Fackler
|
r45990 | if not match(path): | ||
continue | ||||
Augie Fackler
|
r44961 | if status == pygit2.GIT_STATUS_IGNORED: | ||
if path.endswith(b'/'): | ||||
continue | ||||
ignored.append(path) | ||||
elif status in ( | ||||
pygit2.GIT_STATUS_WT_MODIFIED, | ||||
pygit2.GIT_STATUS_INDEX_MODIFIED, | ||||
pygit2.GIT_STATUS_WT_MODIFIED | ||||
| pygit2.GIT_STATUS_INDEX_MODIFIED, | ||||
): | ||||
modified.append(path) | ||||
elif status == pygit2.GIT_STATUS_INDEX_NEW: | ||||
added.append(path) | ||||
elif status == pygit2.GIT_STATUS_WT_NEW: | ||||
unknown.append(path) | ||||
elif status == pygit2.GIT_STATUS_WT_DELETED: | ||||
deleted.append(path) | ||||
elif status == pygit2.GIT_STATUS_INDEX_DELETED: | ||||
removed.append(path) | ||||
else: | ||||
raise error.Abort( | ||||
b'unhandled case: status for %r is %r' % (path, status) | ||||
) | ||||
Augie Fackler
|
r45991 | if listclean: | ||
observed = set( | ||||
modified + added + removed + deleted + unknown + ignored | ||||
) | ||||
index = self.git.index | ||||
index.read() | ||||
for entry in index: | ||||
path = pycompat.fsencode(entry.path) | ||||
if not match(path): | ||||
continue | ||||
if path in observed: | ||||
continue # already in some other set | ||||
if path[-1] == b'/': | ||||
continue # directory | ||||
clean.append(path) | ||||
Augie Fackler
|
r44961 | # TODO are we really always sure of status here? | ||
return ( | ||||
False, | ||||
scmutil.status( | ||||
modified, added, removed, deleted, unknown, ignored, clean | ||||
), | ||||
Matt Harbison
|
r49968 | mtime_boundary, | ||
Augie Fackler
|
r44961 | ) | ||
Matt Harbison
|
r52822 | def flagfunc( | ||
self, buildfallback: intdirstate.FlagFuncFallbackT | ||||
) -> intdirstate.FlagFuncReturnT: | ||||
Augie Fackler
|
r44961 | # TODO we can do better | ||
return buildfallback() | ||||
Matt Harbison
|
r52822 | def getcwd(self) -> bytes: | ||
Augie Fackler
|
r44961 | # TODO is this a good way to do this? | ||
return os.path.dirname( | ||||
os.path.dirname(pycompat.fsencode(self.git.path)) | ||||
) | ||||
Matt Harbison
|
r52822 | def get_entry(self, path: bytes) -> intdirstate.DirstateItemT: | ||
Matt Harbison
|
r49968 | """return a DirstateItem for the associated path""" | ||
entry = self._map.get(path) | ||||
if entry is None: | ||||
return DirstateItem() | ||||
return entry | ||||
Matt Harbison
|
r52822 | def normalize( | ||
self, path: bytes, isknown: bool = False, ignoremissing: bool = False | ||||
) -> bytes: | ||||
Augie Fackler
|
r44961 | normed = util.normcase(path) | ||
assert normed == path, b"TODO handling of case folding: %s != %s" % ( | ||||
normed, | ||||
path, | ||||
) | ||||
return path | ||||
@property | ||||
Matt Harbison
|
r52822 | def _checklink(self) -> bool: | ||
Augie Fackler
|
r44961 | return util.checklink(os.path.dirname(pycompat.fsencode(self.git.path))) | ||
Matt Harbison
|
r52822 | def copies(self) -> Dict[bytes, bytes]: | ||
Augie Fackler
|
r44961 | # TODO support copies? | ||
return {} | ||||
# # TODO what the heck is this | ||||
_filecache = set() | ||||
Matt Harbison
|
r52817 | @property | ||
Matt Harbison
|
r52822 | def is_changing_parents(self) -> bool: | ||
Augie Fackler
|
r44961 | # TODO: we need to implement the context manager bits and | ||
# correctly stage/revert index edits. | ||||
return False | ||||
Matt Harbison
|
r52817 | @property | ||
Matt Harbison
|
r52822 | def is_changing_any(self) -> bool: | ||
r50918 | # TODO: we need to implement the context manager bits and | |||
# correctly stage/revert index edits. | ||||
return False | ||||
Matt Harbison
|
r52822 | def write(self, tr: Optional[intdirstate.TransactionT]) -> None: | ||
Augie Fackler
|
r44961 | # TODO: call parent change callbacks | ||
if tr: | ||||
def writeinner(category): | ||||
self.git.index.write() | ||||
tr.addpending(b'gitdirstate', writeinner) | ||||
else: | ||||
self.git.index.write() | ||||
Matt Harbison
|
r52822 | def pathto(self, f: bytes, cwd: Optional[bytes] = None) -> bytes: | ||
Augie Fackler
|
r44961 | if cwd is None: | ||
cwd = self.getcwd() | ||||
# TODO core dirstate does something about slashes here | ||||
assert isinstance(f, bytes) | ||||
r = util.pathto(self._root, cwd, f) | ||||
return r | ||||
Matt Harbison
|
r52822 | def matches(self, match: matchmod.basematcher) -> Iterable[bytes]: | ||
Augie Fackler
|
r44961 | for x in self.git.index: | ||
p = pycompat.fsencode(x.path) | ||||
if match(p): | ||||
Matt Harbison
|
r52822 | yield p # TODO: return list instead of yielding? | ||
Augie Fackler
|
r44961 | |||
r49208 | def set_clean(self, f, parentfiledata): | |||
Augie Fackler
|
r44961 | """Mark a file normal and clean.""" | ||
# TODO: for now we just let libgit2 re-stat the file. We can | ||||
# clearly do better. | ||||
Augie Fackler
|
r48571 | def set_possibly_dirty(self, f): | ||
Augie Fackler
|
r44961 | """Mark a file normal, but possibly dirty.""" | ||
# TODO: for now we just let libgit2 re-stat the file. We can | ||||
# clearly do better. | ||||
Matt Harbison
|
r52822 | def walk( | ||
self, | ||||
match: matchmod.basematcher, | ||||
subrepos: Any, | ||||
unknown: bool, | ||||
ignored: bool, | ||||
full: bool = True, | ||||
) -> intdirstate.WalkReturnT: | ||||
Augie Fackler
|
r44961 | # TODO: we need to use .status() and not iterate the index, | ||
# because the index doesn't force a re-walk and so `hg add` of | ||||
# a new file without an intervening call to status will | ||||
# silently do nothing. | ||||
r = {} | ||||
cwd = self.getcwd() | ||||
for path, status in self.git.status().items(): | ||||
if path.startswith('.hg/'): | ||||
continue | ||||
path = pycompat.fsencode(path) | ||||
if not match(path): | ||||
continue | ||||
# TODO construct the stat info from the status object? | ||||
try: | ||||
s = os.stat(os.path.join(cwd, path)) | ||||
Manuel Jacob
|
r50201 | except FileNotFoundError: | ||
Augie Fackler
|
r44961 | continue | ||
r[path] = s | ||||
return r | ||||
r50280 | def set_tracked(self, f, reset_copy=False): | |||
# TODO: support copies and reset_copy=True | ||||
Augie Fackler
|
r48571 | uf = pycompat.fsdecode(f) | ||
if uf in self.git.index: | ||||
return False | ||||
index = self.git.index | ||||
index.read() | ||||
index.add(uf) | ||||
index.write() | ||||
return True | ||||
Augie Fackler
|
r44961 | def add(self, f): | ||
Augie Fackler
|
r45989 | index = self.git.index | ||
index.read() | ||||
index.add(pycompat.fsdecode(f)) | ||||
index.write() | ||||
Augie Fackler
|
r44961 | |||
def drop(self, f): | ||||
Augie Fackler
|
r45989 | index = self.git.index | ||
index.read() | ||||
Augie Fackler
|
r45992 | fs = pycompat.fsdecode(f) | ||
if fs in index: | ||||
index.remove(fs) | ||||
index.write() | ||||
Augie Fackler
|
r44961 | |||
Augie Fackler
|
r48571 | def set_untracked(self, f): | ||
index = self.git.index | ||||
index.read() | ||||
fs = pycompat.fsdecode(f) | ||||
if fs in index: | ||||
index.remove(fs) | ||||
index.write() | ||||
return True | ||||
return False | ||||
Augie Fackler
|
r44961 | def remove(self, f): | ||
Augie Fackler
|
r45989 | index = self.git.index | ||
index.read() | ||||
index.remove(pycompat.fsdecode(f)) | ||||
index.write() | ||||
Augie Fackler
|
r44961 | |||
Matt Harbison
|
r52822 | def copied(self, file: bytes) -> Optional[bytes]: | ||
Augie Fackler
|
r44961 | # TODO: track copies? | ||
return None | ||||
Josef 'Jeff' Sipek
|
r45446 | def prefetch_parents(self): | ||
# TODO | ||||
pass | ||||
Augie Fackler
|
r48571 | def update_file(self, *args, **kwargs): | ||
# TODO | ||||
pass | ||||
Augie Fackler
|
r44961 | @contextlib.contextmanager | ||
r50855 | def changing_parents(self, repo): | |||
Augie Fackler
|
r44961 | # TODO: track this maybe? | ||
yield | ||||
Matt Harbison
|
r52822 | def addparentchangecallback( | ||
self, category: bytes, callback: intdirstate.AddParentChangeCallbackT | ||||
) -> None: | ||||
Augie Fackler
|
r44961 | # TODO: should this be added to the dirstate interface? | ||
self._plchangecallbacks[category] = callback | ||||
Matt Harbison
|
r52822 | def setbranch( | ||
self, branch: bytes, transaction: Optional[intdirstate.TransactionT] | ||||
) -> None: | ||||
Josef 'Jeff' Sipek
|
r45112 | raise error.Abort( | ||
b'git repos do not support branches. try using bookmarks' | ||||
) | ||||