[PATCH 1 of 2] subrepo: split non-core functions to new module
Yuya Nishihara
yuya at tcha.org
Thu Feb 8 12:52:35 UTC 2018
# HG changeset patch
# User Yuya Nishihara <yuya at tcha.org>
# Date 1517924198 -32400
# Tue Feb 06 22:36:38 2018 +0900
# Node ID 64230c22a92152b70f41dfa429184cad84ac8eac
# Parent 3e64e5f4d038708b258cec15c17a78d9d49adad3
subrepo: split non-core functions to new module
Resolves import cycle caused by subrepo -> cmdutil. Still we have another
cycle, cmdutil -> context -> subrepo, but where I think importing context
is wrong. Perhaps we'll need repo.makememctx().
diff --git a/hgext/mq.py b/hgext/mq.py
--- a/hgext/mq.py
+++ b/hgext/mq.py
@@ -94,7 +94,7 @@ from mercurial import (
revsetlang,
scmutil,
smartset,
- subrepo,
+ subrepoutil,
util,
vfs as vfsmod,
)
@@ -970,8 +970,8 @@ class queue(object):
wctx = repo[None]
pctx = repo['.']
overwrite = False
- mergedsubstate = subrepo.submerge(repo, pctx, wctx, wctx,
- overwrite)
+ mergedsubstate = subrepoutil.submerge(repo, pctx, wctx, wctx,
+ overwrite)
files += mergedsubstate.keys()
match = scmutil.matchfiles(repo, files or [])
diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py
--- a/mercurial/cmdutil.py
+++ b/mercurial/cmdutil.py
@@ -40,6 +40,7 @@ from . import (
rewriteutil,
scmutil,
smartset,
+ subrepoutil,
templater,
util,
vfs as vfsmod,
@@ -2307,13 +2308,12 @@ def amend(ui, repo, old, extra, pats, op
# subrepo.precommit(). To minimize the risk of this hack, we do
# nothing if .hgsub does not exist.
if '.hgsub' in wctx or '.hgsub' in old:
- from . import subrepo # avoid cycle: cmdutil -> subrepo -> cmdutil
- subs, commitsubs, newsubstate = subrepo.precommit(
+ subs, commitsubs, newsubstate = subrepoutil.precommit(
ui, wctx, wctx._status, matcher)
# amend should abort if commitsubrepos is enabled
assert not commitsubs
if subs:
- subrepo.writestate(repo, newsubstate)
+ subrepoutil.writestate(repo, newsubstate)
filestoamend = set(f for f in wctx.files() if matcher(f))
diff --git a/mercurial/context.py b/mercurial/context.py
--- a/mercurial/context.py
+++ b/mercurial/context.py
@@ -46,6 +46,7 @@ from . import (
scmutil,
sparse,
subrepo,
+ subrepoutil,
util,
)
@@ -173,7 +174,7 @@ class basectx(object):
@propertycache
def substate(self):
- return subrepo.state(self, self._repo.ui)
+ return subrepoutil.state(self, self._repo.ui)
def subrev(self, subpath):
return self.substate[subpath][1]
diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py
--- a/mercurial/localrepo.py
+++ b/mercurial/localrepo.py
@@ -57,7 +57,7 @@ from . import (
scmutil,
sparse,
store,
- subrepo,
+ subrepoutil,
tags as tagsmod,
transaction,
txnutil,
@@ -1833,7 +1833,7 @@ class localrepository(object):
status.modified.extend(status.clean) # mq may commit clean files
# check subrepos
- subs, commitsubs, newstate = subrepo.precommit(
+ subs, commitsubs, newstate = subrepoutil.precommit(
self.ui, wctx, status, match, force=force)
# make sure all explicit patterns are matched
@@ -1870,10 +1870,10 @@ class localrepository(object):
for s in sorted(commitsubs):
sub = wctx.sub(s)
self.ui.status(_('committing subrepository %s\n') %
- subrepo.subrelpath(sub))
+ subrepoutil.subrelpath(sub))
sr = sub.commit(cctx._text, user, date)
newstate[s] = (newstate[s][0], sr)
- subrepo.writestate(self, newstate)
+ subrepoutil.writestate(self, newstate)
p1, p2 = self.dirstate.parents()
hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or '')
@@ -1983,7 +1983,7 @@ class localrepository(object):
self.hook('pretxncommit', throw=True, node=hex(n), parent1=xp1,
parent2=xp2)
# set the new commit is proper phase
- targetphase = subrepo.newcommitphase(self.ui, ctx)
+ targetphase = subrepoutil.newcommitphase(self.ui, ctx)
if targetphase:
# retract boundary do not alter parent changeset.
# if a parent have higher the resulting phase will
diff --git a/mercurial/merge.py b/mercurial/merge.py
--- a/mercurial/merge.py
+++ b/mercurial/merge.py
@@ -31,7 +31,7 @@ from . import (
obsutil,
pycompat,
scmutil,
- subrepo,
+ subrepoutil,
util,
worker,
)
@@ -1445,7 +1445,7 @@ def applyupdates(repo, actions, wctx, mc
z = 0
if [a for a in actions['r'] if a[0] == '.hgsubstate']:
- subrepo.submerge(repo, wctx, mctx, wctx, overwrite, labels)
+ subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
# record path conflicts
for f, args, msg in actions['p']:
@@ -1495,7 +1495,7 @@ def applyupdates(repo, actions, wctx, mc
updated = len(actions['g'])
if [a for a in actions['g'] if a[0] == '.hgsubstate']:
- subrepo.submerge(repo, wctx, mctx, wctx, overwrite, labels)
+ subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
# forget (manifest only, just log it) (must come first)
for f, args, msg in actions['f']:
@@ -1583,8 +1583,8 @@ def applyupdates(repo, actions, wctx, mc
z += 1
progress(_updating, z, item=f, total=numupdates, unit=_files)
if f == '.hgsubstate': # subrepo states need updating
- subrepo.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
- overwrite, labels)
+ subrepoutil.submerge(repo, wctx, mctx, wctx.ancestor(mctx),
+ overwrite, labels)
continue
wctx[f].audit()
complete, r = ms.preresolve(f, wctx)
@@ -1913,7 +1913,7 @@ def update(repo, node, branchmerge, forc
# Prompt and create actions. Most of this is in the resolve phase
# already, but we can't handle .hgsubstate in filemerge or
- # subrepo.submerge yet so we have to keep prompting for it.
+ # subrepoutil.submerge yet so we have to keep prompting for it.
if '.hgsubstate' in actionbyfile:
f = '.hgsubstate'
m, args, msg = actionbyfile[f]
diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py
--- a/mercurial/subrepo.py
+++ b/mercurial/subrepo.py
@@ -1,4 +1,4 @@
-# subrepo.py - sub-repository handling for Mercurial
+# subrepo.py - sub-repository classes and factory
#
# Copyright 2009-2010 Matt Mackall <mpm at selenic.com>
#
@@ -19,15 +19,12 @@ import sys
import tarfile
import xml.dom.minidom
-
from .i18n import _
from . import (
cmdutil,
- config,
encoding,
error,
exchange,
- filemerge,
logcmdutil,
match as matchmod,
node,
@@ -35,15 +32,17 @@ from . import (
phases,
pycompat,
scmutil,
+ subrepoutil,
util,
vfs as vfsmod,
)
hg = None
+reporelpath = subrepoutil.reporelpath
+subrelpath = subrepoutil.subrelpath
+_abssource = subrepoutil._abssource
propertycache = util.propertycache
-nullstate = ('', '', 'empty')
-
def _expandedabspath(path):
'''
get a path or url and if it is a path expand it and return an absolute path
@@ -81,284 +80,6 @@ def annotatesubrepoerror(func):
return res
return decoratedmethod
-def state(ctx, ui):
- """return a state dict, mapping subrepo paths configured in .hgsub
- to tuple: (source from .hgsub, revision from .hgsubstate, kind
- (key in types dict))
- """
- p = config.config()
- repo = ctx.repo()
- def read(f, sections=None, remap=None):
- if f in ctx:
- try:
- data = ctx[f].data()
- except IOError as err:
- if err.errno != errno.ENOENT:
- raise
- # handle missing subrepo spec files as removed
- ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
- repo.pathto(f))
- return
- p.parse(f, data, sections, remap, read)
- else:
- raise error.Abort(_("subrepo spec file \'%s\' not found") %
- repo.pathto(f))
- if '.hgsub' in ctx:
- read('.hgsub')
-
- for path, src in ui.configitems('subpaths'):
- p.set('subpaths', path, src, ui.configsource('subpaths', path))
-
- rev = {}
- if '.hgsubstate' in ctx:
- try:
- for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
- l = l.lstrip()
- if not l:
- continue
- try:
- revision, path = l.split(" ", 1)
- except ValueError:
- raise error.Abort(_("invalid subrepository revision "
- "specifier in \'%s\' line %d")
- % (repo.pathto('.hgsubstate'), (i + 1)))
- rev[path] = revision
- except IOError as err:
- if err.errno != errno.ENOENT:
- raise
-
- def remap(src):
- for pattern, repl in p.items('subpaths'):
- # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
- # does a string decode.
- repl = util.escapestr(repl)
- # However, we still want to allow back references to go
- # through unharmed, so we turn r'\\1' into r'\1'. Again,
- # extra escapes are needed because re.sub string decodes.
- repl = re.sub(br'\\\\([0-9]+)', br'\\\1', repl)
- try:
- src = re.sub(pattern, repl, src, 1)
- except re.error as e:
- raise error.Abort(_("bad subrepository pattern in %s: %s")
- % (p.source('subpaths', pattern), e))
- return src
-
- state = {}
- for path, src in p[''].items():
- kind = 'hg'
- if src.startswith('['):
- if ']' not in src:
- raise error.Abort(_('missing ] in subrepository source'))
- kind, src = src.split(']', 1)
- kind = kind[1:]
- src = src.lstrip() # strip any extra whitespace after ']'
-
- if not util.url(src).isabs():
- parent = _abssource(repo, abort=False)
- if parent:
- parent = util.url(parent)
- parent.path = posixpath.join(parent.path or '', src)
- parent.path = posixpath.normpath(parent.path)
- joined = str(parent)
- # Remap the full joined path and use it if it changes,
- # else remap the original source.
- remapped = remap(joined)
- if remapped == joined:
- src = remap(src)
- else:
- src = remapped
-
- src = remap(src)
- state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
-
- return state
-
-def writestate(repo, state):
- """rewrite .hgsubstate in (outer) repo with these subrepo states"""
- lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
- if state[s][1] != nullstate[1]]
- repo.wwrite('.hgsubstate', ''.join(lines), '')
-
-def submerge(repo, wctx, mctx, actx, overwrite, labels=None):
- """delegated from merge.applyupdates: merging of .hgsubstate file
- in working context, merging context and ancestor context"""
- if mctx == actx: # backwards?
- actx = wctx.p1()
- s1 = wctx.substate
- s2 = mctx.substate
- sa = actx.substate
- sm = {}
-
- repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
-
- def debug(s, msg, r=""):
- if r:
- r = "%s:%s:%s" % r
- repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
-
- promptssrc = filemerge.partextras(labels)
- for s, l in sorted(s1.iteritems()):
- prompts = None
- a = sa.get(s, nullstate)
- ld = l # local state with possible dirty flag for compares
- if wctx.sub(s).dirty():
- ld = (l[0], l[1] + "+")
- if wctx == actx: # overwrite
- a = ld
-
- prompts = promptssrc.copy()
- prompts['s'] = s
- if s in s2:
- r = s2[s]
- if ld == r or r == a: # no change or local is newer
- sm[s] = l
- continue
- elif ld == a: # other side changed
- debug(s, "other changed, get", r)
- wctx.sub(s).get(r, overwrite)
- sm[s] = r
- elif ld[0] != r[0]: # sources differ
- prompts['lo'] = l[0]
- prompts['ro'] = r[0]
- if repo.ui.promptchoice(
- _(' subrepository sources for %(s)s differ\n'
- 'use (l)ocal%(l)s source (%(lo)s)'
- ' or (r)emote%(o)s source (%(ro)s)?'
- '$$ &Local $$ &Remote') % prompts, 0):
- debug(s, "prompt changed, get", r)
- wctx.sub(s).get(r, overwrite)
- sm[s] = r
- elif ld[1] == a[1]: # local side is unchanged
- debug(s, "other side changed, get", r)
- wctx.sub(s).get(r, overwrite)
- sm[s] = r
- else:
- debug(s, "both sides changed")
- srepo = wctx.sub(s)
- prompts['sl'] = srepo.shortid(l[1])
- prompts['sr'] = srepo.shortid(r[1])
- option = repo.ui.promptchoice(
- _(' subrepository %(s)s diverged (local revision: %(sl)s, '
- 'remote revision: %(sr)s)\n'
- '(M)erge, keep (l)ocal%(l)s or keep (r)emote%(o)s?'
- '$$ &Merge $$ &Local $$ &Remote')
- % prompts, 0)
- if option == 0:
- wctx.sub(s).merge(r)
- sm[s] = l
- debug(s, "merge with", r)
- elif option == 1:
- sm[s] = l
- debug(s, "keep local subrepo revision", l)
- else:
- wctx.sub(s).get(r, overwrite)
- sm[s] = r
- debug(s, "get remote subrepo revision", r)
- elif ld == a: # remote removed, local unchanged
- debug(s, "remote removed, remove")
- wctx.sub(s).remove()
- elif a == nullstate: # not present in remote or ancestor
- debug(s, "local added, keep")
- sm[s] = l
- continue
- else:
- if repo.ui.promptchoice(
- _(' local%(l)s changed subrepository %(s)s'
- ' which remote%(o)s removed\n'
- 'use (c)hanged version or (d)elete?'
- '$$ &Changed $$ &Delete') % prompts, 0):
- debug(s, "prompt remove")
- wctx.sub(s).remove()
-
- for s, r in sorted(s2.items()):
- prompts = None
- if s in s1:
- continue
- elif s not in sa:
- debug(s, "remote added, get", r)
- mctx.sub(s).get(r)
- sm[s] = r
- elif r != sa[s]:
- prompts = promptssrc.copy()
- prompts['s'] = s
- if repo.ui.promptchoice(
- _(' remote%(o)s changed subrepository %(s)s'
- ' which local%(l)s removed\n'
- 'use (c)hanged version or (d)elete?'
- '$$ &Changed $$ &Delete') % prompts, 0) == 0:
- debug(s, "prompt recreate", r)
- mctx.sub(s).get(r)
- sm[s] = r
-
- # record merged .hgsubstate
- writestate(repo, sm)
- return sm
-
-def precommit(ui, wctx, status, match, force=False):
- """Calculate .hgsubstate changes that should be applied before committing
-
- Returns (subs, commitsubs, newstate) where
- - subs: changed subrepos (including dirty ones)
- - commitsubs: dirty subrepos which the caller needs to commit recursively
- - newstate: new state dict which the caller must write to .hgsubstate
-
- This also updates the given status argument.
- """
- subs = []
- commitsubs = set()
- newstate = wctx.substate.copy()
-
- # only manage subrepos and .hgsubstate if .hgsub is present
- if '.hgsub' in wctx:
- # we'll decide whether to track this ourselves, thanks
- for c in status.modified, status.added, status.removed:
- if '.hgsubstate' in c:
- c.remove('.hgsubstate')
-
- # compare current state to last committed state
- # build new substate based on last committed state
- oldstate = wctx.p1().substate
- for s in sorted(newstate.keys()):
- if not match(s):
- # ignore working copy, use old state if present
- if s in oldstate:
- newstate[s] = oldstate[s]
- continue
- if not force:
- raise error.Abort(
- _("commit with new subrepo %s excluded") % s)
- dirtyreason = wctx.sub(s).dirtyreason(True)
- if dirtyreason:
- if not ui.configbool('ui', 'commitsubrepos'):
- raise error.Abort(dirtyreason,
- hint=_("use --subrepos for recursive commit"))
- subs.append(s)
- commitsubs.add(s)
- else:
- bs = wctx.sub(s).basestate()
- newstate[s] = (newstate[s][0], bs, newstate[s][2])
- if oldstate.get(s, (None, None, None))[1] != bs:
- subs.append(s)
-
- # check for removed subrepos
- for p in wctx.parents():
- r = [s for s in p.substate if s not in newstate]
- subs += [s for s in r if match(s)]
- if subs:
- if (not match('.hgsub') and
- '.hgsub' in (wctx.modified() + wctx.added())):
- raise error.Abort(_("can't commit subrepos without .hgsub"))
- status.modified.insert(0, '.hgsubstate')
-
- elif '.hgsub' in status.removed:
- # clean up .hgsubstate when .hgsub is removed
- if ('.hgsubstate' in wctx and
- '.hgsubstate' not in (status.modified + status.added +
- status.removed)):
- status.removed.insert(0, '.hgsubstate')
-
- return subs, commitsubs, newstate
-
def _updateprompt(ui, sub, dirty, local, remote):
if dirty:
msg = (_(' subrepository sources for %s differ\n'
@@ -373,64 +94,6 @@ def _updateprompt(ui, sub, dirty, local,
% (subrelpath(sub), local, remote))
return ui.promptchoice(msg, 0)
-def reporelpath(repo):
- """return path to this (sub)repo as seen from outermost repo"""
- parent = repo
- while util.safehasattr(parent, '_subparent'):
- parent = parent._subparent
- return repo.root[len(pathutil.normasprefix(parent.root)):]
-
-def subrelpath(sub):
- """return path to this subrepo as seen from outermost repo"""
- return sub._relpath
-
-def _abssource(repo, push=False, abort=True):
- """return pull/push path of repo - either based on parent repo .hgsub info
- or on the top repo config. Abort or return None if no source found."""
- if util.safehasattr(repo, '_subparent'):
- source = util.url(repo._subsource)
- if source.isabs():
- return bytes(source)
- source.path = posixpath.normpath(source.path)
- parent = _abssource(repo._subparent, push, abort=False)
- if parent:
- parent = util.url(util.pconvert(parent))
- parent.path = posixpath.join(parent.path or '', source.path)
- parent.path = posixpath.normpath(parent.path)
- return bytes(parent)
- else: # recursion reached top repo
- path = None
- if util.safehasattr(repo, '_subtoppath'):
- path = repo._subtoppath
- elif push and repo.ui.config('paths', 'default-push'):
- path = repo.ui.config('paths', 'default-push')
- elif repo.ui.config('paths', 'default'):
- path = repo.ui.config('paths', 'default')
- elif repo.shared():
- # chop off the .hg component to get the default path form. This has
- # already run through vfsmod.vfs(..., realpath=True), so it doesn't
- # have problems with 'C:'
- return os.path.dirname(repo.sharedpath)
- if path:
- # issue5770: 'C:\' and 'C:' are not equivalent paths. The former is
- # as expected: an absolute path to the root of the C: drive. The
- # latter is a relative path, and works like so:
- #
- # C:\>cd C:\some\path
- # C:\>D:
- # D:\>python -c "import os; print os.path.abspath('C:')"
- # C:\some\path
- #
- # D:\>python -c "import os; print os.path.abspath('C:relative')"
- # C:\some\path\relative
- if util.hasdriveletter(path):
- if len(path) == 2 or path[2:3] not in br'\/':
- path = os.path.abspath(path)
- return path
-
- if abort:
- raise error.Abort(_("default path for subrepository not found"))
-
def _sanitize(ui, vfs, ignore):
for dirname, dirs, names in vfs.walk():
for i, d in enumerate(dirs):
@@ -509,37 +172,6 @@ def nullsubrepo(ctx, path, pctx):
subrev = "0" * 40
return types[state[2]](pctx, path, (state[0], subrev), True)
-def newcommitphase(ui, ctx):
- commitphase = phases.newcommitphase(ui)
- substate = getattr(ctx, "substate", None)
- if not substate:
- return commitphase
- check = ui.config('phases', 'checksubrepos')
- if check not in ('ignore', 'follow', 'abort'):
- raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
- % (check))
- if check == 'ignore':
- return commitphase
- maxphase = phases.public
- maxsub = None
- for s in sorted(substate):
- sub = ctx.sub(s)
- subphase = sub.phase(substate[s][1])
- if maxphase < subphase:
- maxphase = subphase
- maxsub = s
- if commitphase < maxphase:
- if check == 'abort':
- raise error.Abort(_("can't commit in %s phase"
- " conflicting %s from subrepository %s") %
- (phases.phasenames[commitphase],
- phases.phasenames[maxphase], maxsub))
- ui.warn(_("warning: changes are committed in"
- " %s phase from subrepository %s\n") %
- (phases.phasenames[maxphase], maxsub))
- return maxphase
- return commitphase
-
# subrepo classes need to implement the following abstract class:
class abstractsubrepo(object):
diff --git a/mercurial/subrepo.py b/mercurial/subrepoutil.py
copy from mercurial/subrepo.py
copy to mercurial/subrepoutil.py
--- a/mercurial/subrepo.py
+++ b/mercurial/subrepoutil.py
@@ -1,4 +1,4 @@
-# subrepo.py - sub-repository handling for Mercurial
+# subrepoutil.py - sub-repository operations and substate handling
#
# Copyright 2009-2010 Matt Mackall <mpm at selenic.com>
#
@@ -7,80 +7,23 @@
from __future__ import absolute_import
-import copy
import errno
-import hashlib
import os
import posixpath
import re
-import stat
-import subprocess
-import sys
-import tarfile
-import xml.dom.minidom
-
from .i18n import _
from . import (
- cmdutil,
config,
- encoding,
error,
- exchange,
filemerge,
- logcmdutil,
- match as matchmod,
- node,
pathutil,
phases,
- pycompat,
- scmutil,
util,
- vfs as vfsmod,
)
-hg = None
-propertycache = util.propertycache
-
nullstate = ('', '', 'empty')
-def _expandedabspath(path):
- '''
- get a path or url and if it is a path expand it and return an absolute path
- '''
- expandedpath = util.urllocalpath(util.expandpath(path))
- u = util.url(expandedpath)
- if not u.scheme:
- path = util.normpath(os.path.abspath(u.path))
- return path
-
-def _getstorehashcachename(remotepath):
- '''get a unique filename for the store hash cache of a remote repository'''
- return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
-
-class SubrepoAbort(error.Abort):
- """Exception class used to avoid handling a subrepo error more than once"""
- def __init__(self, *args, **kw):
- self.subrepo = kw.pop(r'subrepo', None)
- self.cause = kw.pop(r'cause', None)
- error.Abort.__init__(self, *args, **kw)
-
-def annotatesubrepoerror(func):
- def decoratedmethod(self, *args, **kargs):
- try:
- res = func(self, *args, **kargs)
- except SubrepoAbort as ex:
- # This exception has already been handled
- raise ex
- except error.Abort as ex:
- subrepo = subrelpath(self)
- errormsg = str(ex) + ' ' + _('(in subrepository "%s")') % subrepo
- # avoid handling this exception by raising a SubrepoAbort exception
- raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
- cause=sys.exc_info())
- return res
- return decoratedmethod
-
def state(ctx, ui):
"""return a state dict, mapping subrepo paths configured in .hgsub
to tuple: (source from .hgsub, revision from .hgsubstate, kind
@@ -359,20 +302,6 @@ def precommit(ui, wctx, status, match, f
return subs, commitsubs, newstate
-def _updateprompt(ui, sub, dirty, local, remote):
- if dirty:
- msg = (_(' subrepository sources for %s differ\n'
- 'use (l)ocal source (%s) or (r)emote source (%s)?'
- '$$ &Local $$ &Remote')
- % (subrelpath(sub), local, remote))
- else:
- msg = (_(' subrepository sources for %s differ (in checked out '
- 'version)\n'
- 'use (l)ocal source (%s) or (r)emote source (%s)?'
- '$$ &Local $$ &Remote')
- % (subrelpath(sub), local, remote))
- return ui.promptchoice(msg, 0)
-
def reporelpath(repo):
"""return path to this (sub)repo as seen from outermost repo"""
parent = repo
@@ -431,84 +360,6 @@ def _abssource(repo, push=False, abort=T
if abort:
raise error.Abort(_("default path for subrepository not found"))
-def _sanitize(ui, vfs, ignore):
- for dirname, dirs, names in vfs.walk():
- for i, d in enumerate(dirs):
- if d.lower() == ignore:
- del dirs[i]
- break
- if vfs.basename(dirname).lower() != '.hg':
- continue
- for f in names:
- if f.lower() == 'hgrc':
- ui.warn(_("warning: removing potentially hostile 'hgrc' "
- "in '%s'\n") % vfs.join(dirname))
- vfs.unlink(vfs.reljoin(dirname, f))
-
-def _auditsubrepopath(repo, path):
- # auditor doesn't check if the path itself is a symlink
- pathutil.pathauditor(repo.root)(path)
- if repo.wvfs.islink(path):
- raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
-
-SUBREPO_ALLOWED_DEFAULTS = {
- 'hg': True,
- 'git': False,
- 'svn': False,
-}
-
-def _checktype(ui, kind):
- # subrepos.allowed is a master kill switch. If disabled, subrepos are
- # disabled period.
- if not ui.configbool('subrepos', 'allowed', True):
- raise error.Abort(_('subrepos not enabled'),
- hint=_("see 'hg help config.subrepos' for details"))
-
- default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
- if not ui.configbool('subrepos', '%s:allowed' % kind, default):
- raise error.Abort(_('%s subrepos not allowed') % kind,
- hint=_("see 'hg help config.subrepos' for details"))
-
- if kind not in types:
- raise error.Abort(_('unknown subrepo type %s') % kind)
-
-def subrepo(ctx, path, allowwdir=False, allowcreate=True):
- """return instance of the right subrepo class for subrepo in path"""
- # subrepo inherently violates our import layering rules
- # because it wants to make repo objects from deep inside the stack
- # so we manually delay the circular imports to not break
- # scripts that don't use our demand-loading
- global hg
- from . import hg as h
- hg = h
-
- repo = ctx.repo()
- _auditsubrepopath(repo, path)
- state = ctx.substate[path]
- _checktype(repo.ui, state[2])
- if allowwdir:
- state = (state[0], ctx.subrev(path), state[2])
- return types[state[2]](ctx, path, state[:2], allowcreate)
-
-def nullsubrepo(ctx, path, pctx):
- """return an empty subrepo in pctx for the extant subrepo in ctx"""
- # subrepo inherently violates our import layering rules
- # because it wants to make repo objects from deep inside the stack
- # so we manually delay the circular imports to not break
- # scripts that don't use our demand-loading
- global hg
- from . import hg as h
- hg = h
-
- repo = ctx.repo()
- _auditsubrepopath(repo, path)
- state = ctx.substate[path]
- _checktype(repo.ui, state[2])
- subrev = ''
- if state[2] == 'hg':
- subrev = "0" * 40
- return types[state[2]](pctx, path, (state[0], subrev), True)
-
def newcommitphase(ui, ctx):
commitphase = phases.newcommitphase(ui)
substate = getattr(ctx, "substate", None)
@@ -539,1617 +390,3 @@ def newcommitphase(ui, ctx):
(phases.phasenames[maxphase], maxsub))
return maxphase
return commitphase
-
-# subrepo classes need to implement the following abstract class:
-
-class abstractsubrepo(object):
-
- def __init__(self, ctx, path):
- """Initialize abstractsubrepo part
-
- ``ctx`` is the context referring this subrepository in the
- parent repository.
-
- ``path`` is the path to this subrepository as seen from
- innermost repository.
- """
- self.ui = ctx.repo().ui
- self._ctx = ctx
- self._path = path
-
- def addwebdirpath(self, serverpath, webconf):
- """Add the hgwebdir entries for this subrepo, and any of its subrepos.
-
- ``serverpath`` is the path component of the URL for this repo.
-
- ``webconf`` is the dictionary of hgwebdir entries.
- """
- pass
-
- def storeclean(self, path):
- """
- returns true if the repository has not changed since it was last
- cloned from or pushed to a given repository.
- """
- return False
-
- def dirty(self, ignoreupdate=False, missing=False):
- """returns true if the dirstate of the subrepo is dirty or does not
- match current stored state. If ignoreupdate is true, only check
- whether the subrepo has uncommitted changes in its dirstate. If missing
- is true, check for deleted files.
- """
- raise NotImplementedError
-
- def dirtyreason(self, ignoreupdate=False, missing=False):
- """return reason string if it is ``dirty()``
-
- Returned string should have enough information for the message
- of exception.
-
- This returns None, otherwise.
- """
- if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
- return _('uncommitted changes in subrepository "%s"'
- ) % subrelpath(self)
-
- def bailifchanged(self, ignoreupdate=False, hint=None):
- """raise Abort if subrepository is ``dirty()``
- """
- dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
- missing=True)
- if dirtyreason:
- raise error.Abort(dirtyreason, hint=hint)
-
- def basestate(self):
- """current working directory base state, disregarding .hgsubstate
- state and working directory modifications"""
- raise NotImplementedError
-
- def checknested(self, path):
- """check if path is a subrepository within this repository"""
- return False
-
- def commit(self, text, user, date):
- """commit the current changes to the subrepo with the given
- log message. Use given user and date if possible. Return the
- new state of the subrepo.
- """
- raise NotImplementedError
-
- def phase(self, state):
- """returns phase of specified state in the subrepository.
- """
- return phases.public
-
- def remove(self):
- """remove the subrepo
-
- (should verify the dirstate is not dirty first)
- """
- raise NotImplementedError
-
- def get(self, state, overwrite=False):
- """run whatever commands are needed to put the subrepo into
- this state
- """
- raise NotImplementedError
-
- def merge(self, state):
- """merge currently-saved state with the new state."""
- raise NotImplementedError
-
- def push(self, opts):
- """perform whatever action is analogous to 'hg push'
-
- This may be a no-op on some systems.
- """
- raise NotImplementedError
-
- def add(self, ui, match, prefix, explicitonly, **opts):
- return []
-
- def addremove(self, matcher, prefix, opts, dry_run, similarity):
- self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
- return 1
-
- def cat(self, match, fm, fntemplate, prefix, **opts):
- return 1
-
- def status(self, rev2, **opts):
- return scmutil.status([], [], [], [], [], [], [])
-
- def diff(self, ui, diffopts, node2, match, prefix, **opts):
- pass
-
- def outgoing(self, ui, dest, opts):
- return 1
-
- def incoming(self, ui, source, opts):
- return 1
-
- def files(self):
- """return filename iterator"""
- raise NotImplementedError
-
- def filedata(self, name, decode):
- """return file data, optionally passed through repo decoders"""
- raise NotImplementedError
-
- def fileflags(self, name):
- """return file flags"""
- return ''
-
- def getfileset(self, expr):
- """Resolve the fileset expression for this repo"""
- return set()
-
- def printfiles(self, ui, m, fm, fmt, subrepos):
- """handle the files command for this subrepo"""
- return 1
-
- def archive(self, archiver, prefix, match=None, decode=True):
- if match is not None:
- files = [f for f in self.files() if match(f)]
- else:
- files = self.files()
- total = len(files)
- relpath = subrelpath(self)
- self.ui.progress(_('archiving (%s)') % relpath, 0,
- unit=_('files'), total=total)
- for i, name in enumerate(files):
- flags = self.fileflags(name)
- mode = 'x' in flags and 0o755 or 0o644
- symlink = 'l' in flags
- archiver.addfile(prefix + self._path + '/' + name,
- mode, symlink, self.filedata(name, decode))
- self.ui.progress(_('archiving (%s)') % relpath, i + 1,
- unit=_('files'), total=total)
- self.ui.progress(_('archiving (%s)') % relpath, None)
- return total
-
- def walk(self, match):
- '''
- walk recursively through the directory tree, finding all files
- matched by the match function
- '''
-
- def forget(self, match, prefix):
- return ([], [])
-
- def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
- """remove the matched files from the subrepository and the filesystem,
- possibly by force and/or after the file has been removed from the
- filesystem. Return 0 on success, 1 on any warning.
- """
- warnings.append(_("warning: removefiles not implemented (%s)")
- % self._path)
- return 1
-
- def revert(self, substate, *pats, **opts):
- self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
- % (substate[0], substate[2]))
- return []
-
- def shortid(self, revid):
- return revid
-
- def unshare(self):
- '''
- convert this repository from shared to normal storage.
- '''
-
- def verify(self):
- '''verify the integrity of the repository. Return 0 on success or
- warning, 1 on any error.
- '''
- return 0
-
- @propertycache
- def wvfs(self):
- """return vfs to access the working directory of this subrepository
- """
- return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
-
- @propertycache
- def _relpath(self):
- """return path to this subrepository as seen from outermost repository
- """
- return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
-
-class hgsubrepo(abstractsubrepo):
- def __init__(self, ctx, path, state, allowcreate):
- super(hgsubrepo, self).__init__(ctx, path)
- self._state = state
- r = ctx.repo()
- root = r.wjoin(path)
- create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
- self._repo = hg.repository(r.baseui, root, create=create)
-
- # Propagate the parent's --hidden option
- if r is r.unfiltered():
- self._repo = self._repo.unfiltered()
-
- self.ui = self._repo.ui
- for s, k in [('ui', 'commitsubrepos')]:
- v = r.ui.config(s, k)
- if v:
- self.ui.setconfig(s, k, v, 'subrepo')
- # internal config: ui._usedassubrepo
- self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
- self._initrepo(r, state[0], create)
-
- @annotatesubrepoerror
- def addwebdirpath(self, serverpath, webconf):
- cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
-
- def storeclean(self, path):
- with self._repo.lock():
- return self._storeclean(path)
-
- def _storeclean(self, path):
- clean = True
- itercache = self._calcstorehash(path)
- for filehash in self._readstorehashcache(path):
- if filehash != next(itercache, None):
- clean = False
- break
- if clean:
- # if not empty:
- # the cached and current pull states have a different size
- clean = next(itercache, None) is None
- return clean
-
- def _calcstorehash(self, remotepath):
- '''calculate a unique "store hash"
-
- This method is used to to detect when there are changes that may
- require a push to a given remote path.'''
- # sort the files that will be hashed in increasing (likely) file size
- filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
- yield '# %s\n' % _expandedabspath(remotepath)
- vfs = self._repo.vfs
- for relname in filelist:
- filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
- yield '%s = %s\n' % (relname, filehash)
-
- @propertycache
- def _cachestorehashvfs(self):
- return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
-
- def _readstorehashcache(self, remotepath):
- '''read the store hash cache for a given remote repository'''
- cachefile = _getstorehashcachename(remotepath)
- return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
-
- def _cachestorehash(self, remotepath):
- '''cache the current store hash
-
- Each remote repo requires its own store hash cache, because a subrepo
- store may be "clean" versus a given remote repo, but not versus another
- '''
- cachefile = _getstorehashcachename(remotepath)
- with self._repo.lock():
- storehash = list(self._calcstorehash(remotepath))
- vfs = self._cachestorehashvfs
- vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
-
- def _getctx(self):
- '''fetch the context for this subrepo revision, possibly a workingctx
- '''
- if self._ctx.rev() is None:
- return self._repo[None] # workingctx if parent is workingctx
- else:
- rev = self._state[1]
- return self._repo[rev]
-
- @annotatesubrepoerror
- def _initrepo(self, parentrepo, source, create):
- self._repo._subparent = parentrepo
- self._repo._subsource = source
-
- if create:
- lines = ['[paths]\n']
-
- def addpathconfig(key, value):
- if value:
- lines.append('%s = %s\n' % (key, value))
- self.ui.setconfig('paths', key, value, 'subrepo')
-
- defpath = _abssource(self._repo, abort=False)
- defpushpath = _abssource(self._repo, True, abort=False)
- addpathconfig('default', defpath)
- if defpath != defpushpath:
- addpathconfig('default-push', defpushpath)
-
- self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
-
- @annotatesubrepoerror
- def add(self, ui, match, prefix, explicitonly, **opts):
- return cmdutil.add(ui, self._repo, match,
- self.wvfs.reljoin(prefix, self._path),
- explicitonly, **opts)
-
- @annotatesubrepoerror
- def addremove(self, m, prefix, opts, dry_run, similarity):
- # In the same way as sub directories are processed, once in a subrepo,
- # always entry any of its subrepos. Don't corrupt the options that will
- # be used to process sibling subrepos however.
- opts = copy.copy(opts)
- opts['subrepos'] = True
- return scmutil.addremove(self._repo, m,
- self.wvfs.reljoin(prefix, self._path), opts,
- dry_run, similarity)
-
- @annotatesubrepoerror
- def cat(self, match, fm, fntemplate, prefix, **opts):
- rev = self._state[1]
- ctx = self._repo[rev]
- return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
- prefix, **opts)
-
- @annotatesubrepoerror
- def status(self, rev2, **opts):
- try:
- rev1 = self._state[1]
- ctx1 = self._repo[rev1]
- ctx2 = self._repo[rev2]
- return self._repo.status(ctx1, ctx2, **opts)
- except error.RepoLookupError as inst:
- self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
- % (inst, subrelpath(self)))
- return scmutil.status([], [], [], [], [], [], [])
-
- @annotatesubrepoerror
- def diff(self, ui, diffopts, node2, match, prefix, **opts):
- try:
- node1 = node.bin(self._state[1])
- # We currently expect node2 to come from substate and be
- # in hex format
- if node2 is not None:
- node2 = node.bin(node2)
- logcmdutil.diffordiffstat(ui, self._repo, diffopts,
- node1, node2, match,
- prefix=posixpath.join(prefix, self._path),
- listsubrepos=True, **opts)
- except error.RepoLookupError as inst:
- self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
- % (inst, subrelpath(self)))
-
- @annotatesubrepoerror
- def archive(self, archiver, prefix, match=None, decode=True):
- self._get(self._state + ('hg',))
- files = self.files()
- if match:
- files = [f for f in files if match(f)]
- rev = self._state[1]
- ctx = self._repo[rev]
- cmdutil._prefetchfiles(self._repo, ctx, files)
- total = abstractsubrepo.archive(self, archiver, prefix, match)
- for subpath in ctx.substate:
- s = subrepo(ctx, subpath, True)
- submatch = matchmod.subdirmatcher(subpath, match)
- total += s.archive(archiver, prefix + self._path + '/', submatch,
- decode)
- return total
-
- @annotatesubrepoerror
- def dirty(self, ignoreupdate=False, missing=False):
- r = self._state[1]
- if r == '' and not ignoreupdate: # no state recorded
- return True
- w = self._repo[None]
- if r != w.p1().hex() and not ignoreupdate:
- # different version checked out
- return True
- return w.dirty(missing=missing) # working directory changed
-
- def basestate(self):
- return self._repo['.'].hex()
-
- def checknested(self, path):
- return self._repo._checknested(self._repo.wjoin(path))
-
- @annotatesubrepoerror
- def commit(self, text, user, date):
- # don't bother committing in the subrepo if it's only been
- # updated
- if not self.dirty(True):
- return self._repo['.'].hex()
- self.ui.debug("committing subrepo %s\n" % subrelpath(self))
- n = self._repo.commit(text, user, date)
- if not n:
- return self._repo['.'].hex() # different version checked out
- return node.hex(n)
-
- @annotatesubrepoerror
- def phase(self, state):
- return self._repo[state].phase()
-
- @annotatesubrepoerror
- def remove(self):
- # we can't fully delete the repository as it may contain
- # local-only history
- self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
- hg.clean(self._repo, node.nullid, False)
-
- def _get(self, state):
- source, revision, kind = state
- parentrepo = self._repo._subparent
-
- if revision in self._repo.unfiltered():
- # Allow shared subrepos tracked at null to setup the sharedpath
- if len(self._repo) != 0 or not parentrepo.shared():
- return True
- self._repo._subsource = source
- srcurl = _abssource(self._repo)
- other = hg.peer(self._repo, {}, srcurl)
- if len(self._repo) == 0:
- # use self._repo.vfs instead of self.wvfs to remove .hg only
- self._repo.vfs.rmtree()
- if parentrepo.shared():
- self.ui.status(_('sharing subrepo %s from %s\n')
- % (subrelpath(self), srcurl))
- shared = hg.share(self._repo._subparent.baseui,
- other, self._repo.root,
- update=False, bookmarks=False)
- self._repo = shared.local()
- else:
- self.ui.status(_('cloning subrepo %s from %s\n')
- % (subrelpath(self), srcurl))
- other, cloned = hg.clone(self._repo._subparent.baseui, {},
- other, self._repo.root,
- update=False)
- self._repo = cloned.local()
- self._initrepo(parentrepo, source, create=True)
- self._cachestorehash(srcurl)
- else:
- self.ui.status(_('pulling subrepo %s from %s\n')
- % (subrelpath(self), srcurl))
- cleansub = self.storeclean(srcurl)
- exchange.pull(self._repo, other)
- if cleansub:
- # keep the repo clean after pull
- self._cachestorehash(srcurl)
- return False
-
- @annotatesubrepoerror
- def get(self, state, overwrite=False):
- inrepo = self._get(state)
- source, revision, kind = state
- repo = self._repo
- repo.ui.debug("getting subrepo %s\n" % self._path)
- if inrepo:
- urepo = repo.unfiltered()
- ctx = urepo[revision]
- if ctx.hidden():
- urepo.ui.warn(
- _('revision %s in subrepository "%s" is hidden\n') \
- % (revision[0:12], self._path))
- repo = urepo
- hg.updaterepo(repo, revision, overwrite)
-
- @annotatesubrepoerror
- def merge(self, state):
- self._get(state)
- cur = self._repo['.']
- dst = self._repo[state[1]]
- anc = dst.ancestor(cur)
-
- def mergefunc():
- if anc == cur and dst.branch() == cur.branch():
- self.ui.debug('updating subrepository "%s"\n'
- % subrelpath(self))
- hg.update(self._repo, state[1])
- elif anc == dst:
- self.ui.debug('skipping subrepository "%s"\n'
- % subrelpath(self))
- else:
- self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
- hg.merge(self._repo, state[1], remind=False)
-
- wctx = self._repo[None]
- if self.dirty():
- if anc != dst:
- if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
- mergefunc()
- else:
- mergefunc()
- else:
- mergefunc()
-
- @annotatesubrepoerror
- def push(self, opts):
- force = opts.get('force')
- newbranch = opts.get('new_branch')
- ssh = opts.get('ssh')
-
- # push subrepos depth-first for coherent ordering
- c = self._repo['']
- subs = c.substate # only repos that are committed
- for s in sorted(subs):
- if c.sub(s).push(opts) == 0:
- return False
-
- dsturl = _abssource(self._repo, True)
- if not force:
- if self.storeclean(dsturl):
- self.ui.status(
- _('no changes made to subrepo %s since last push to %s\n')
- % (subrelpath(self), dsturl))
- return None
- self.ui.status(_('pushing subrepo %s to %s\n') %
- (subrelpath(self), dsturl))
- other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
- res = exchange.push(self._repo, other, force, newbranch=newbranch)
-
- # the repo is now clean
- self._cachestorehash(dsturl)
- return res.cgresult
-
- @annotatesubrepoerror
- def outgoing(self, ui, dest, opts):
- if 'rev' in opts or 'branch' in opts:
- opts = copy.copy(opts)
- opts.pop('rev', None)
- opts.pop('branch', None)
- return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
-
- @annotatesubrepoerror
- def incoming(self, ui, source, opts):
- if 'rev' in opts or 'branch' in opts:
- opts = copy.copy(opts)
- opts.pop('rev', None)
- opts.pop('branch', None)
- return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
-
- @annotatesubrepoerror
- def files(self):
- rev = self._state[1]
- ctx = self._repo[rev]
- return ctx.manifest().keys()
-
- def filedata(self, name, decode):
- rev = self._state[1]
- data = self._repo[rev][name].data()
- if decode:
- data = self._repo.wwritedata(name, data)
- return data
-
- def fileflags(self, name):
- rev = self._state[1]
- ctx = self._repo[rev]
- return ctx.flags(name)
-
- @annotatesubrepoerror
- def printfiles(self, ui, m, fm, fmt, subrepos):
- # If the parent context is a workingctx, use the workingctx here for
- # consistency.
- if self._ctx.rev() is None:
- ctx = self._repo[None]
- else:
- rev = self._state[1]
- ctx = self._repo[rev]
- return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
-
- @annotatesubrepoerror
- def getfileset(self, expr):
- if self._ctx.rev() is None:
- ctx = self._repo[None]
- else:
- rev = self._state[1]
- ctx = self._repo[rev]
-
- files = ctx.getfileset(expr)
-
- for subpath in ctx.substate:
- sub = ctx.sub(subpath)
-
- try:
- files.extend(subpath + '/' + f for f in sub.getfileset(expr))
- except error.LookupError:
- self.ui.status(_("skipping missing subrepository: %s\n")
- % self.wvfs.reljoin(reporelpath(self), subpath))
- return files
-
- def walk(self, match):
- ctx = self._repo[None]
- return ctx.walk(match)
-
- @annotatesubrepoerror
- def forget(self, match, prefix):
- return cmdutil.forget(self.ui, self._repo, match,
- self.wvfs.reljoin(prefix, self._path), True)
-
- @annotatesubrepoerror
- def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
- return cmdutil.remove(self.ui, self._repo, matcher,
- self.wvfs.reljoin(prefix, self._path),
- after, force, subrepos)
-
- @annotatesubrepoerror
- def revert(self, substate, *pats, **opts):
- # reverting a subrepo is a 2 step process:
- # 1. if the no_backup is not set, revert all modified
- # files inside the subrepo
- # 2. update the subrepo to the revision specified in
- # the corresponding substate dictionary
- self.ui.status(_('reverting subrepo %s\n') % substate[0])
- if not opts.get(r'no_backup'):
- # Revert all files on the subrepo, creating backups
- # Note that this will not recursively revert subrepos
- # We could do it if there was a set:subrepos() predicate
- opts = opts.copy()
- opts[r'date'] = None
- opts[r'rev'] = substate[1]
-
- self.filerevert(*pats, **opts)
-
- # Update the repo to the revision specified in the given substate
- if not opts.get(r'dry_run'):
- self.get(substate, overwrite=True)
-
- def filerevert(self, *pats, **opts):
- ctx = self._repo[opts[r'rev']]
- parents = self._repo.dirstate.parents()
- if opts.get(r'all'):
- pats = ['set:modified()']
- else:
- pats = []
- cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
-
- def shortid(self, revid):
- return revid[:12]
-
- @annotatesubrepoerror
- def unshare(self):
- # subrepo inherently violates our import layering rules
- # because it wants to make repo objects from deep inside the stack
- # so we manually delay the circular imports to not break
- # scripts that don't use our demand-loading
- global hg
- from . import hg as h
- hg = h
-
- # Nothing prevents a user from sharing in a repo, and then making that a
- # subrepo. Alternately, the previous unshare attempt may have failed
- # part way through. So recurse whether or not this layer is shared.
- if self._repo.shared():
- self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
-
- hg.unshare(self.ui, self._repo)
-
- def verify(self):
- try:
- rev = self._state[1]
- ctx = self._repo.unfiltered()[rev]
- if ctx.hidden():
- # Since hidden revisions aren't pushed/pulled, it seems worth an
- # explicit warning.
- ui = self._repo.ui
- ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
- (self._relpath, node.short(self._ctx.node())))
- return 0
- except error.RepoLookupError:
- # A missing subrepo revision may be a case of needing to pull it, so
- # don't treat this as an error.
- self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
- (self._relpath, node.short(self._ctx.node())))
- return 0
-
- @propertycache
- def wvfs(self):
- """return own wvfs for efficiency and consistency
- """
- return self._repo.wvfs
-
- @propertycache
- def _relpath(self):
- """return path to this subrepository as seen from outermost repository
- """
- # Keep consistent dir separators by avoiding vfs.join(self._path)
- return reporelpath(self._repo)
-
-class svnsubrepo(abstractsubrepo):
- def __init__(self, ctx, path, state, allowcreate):
- super(svnsubrepo, self).__init__(ctx, path)
- self._state = state
- self._exe = util.findexe('svn')
- if not self._exe:
- raise error.Abort(_("'svn' executable not found for subrepo '%s'")
- % self._path)
-
- def _svncommand(self, commands, filename='', failok=False):
- cmd = [self._exe]
- extrakw = {}
- if not self.ui.interactive():
- # Making stdin be a pipe should prevent svn from behaving
- # interactively even if we can't pass --non-interactive.
- extrakw[r'stdin'] = subprocess.PIPE
- # Starting in svn 1.5 --non-interactive is a global flag
- # instead of being per-command, but we need to support 1.4 so
- # we have to be intelligent about what commands take
- # --non-interactive.
- if commands[0] in ('update', 'checkout', 'commit'):
- cmd.append('--non-interactive')
- cmd.extend(commands)
- if filename is not None:
- path = self.wvfs.reljoin(self._ctx.repo().origroot,
- self._path, filename)
- cmd.append(path)
- env = dict(encoding.environ)
- # Avoid localized output, preserve current locale for everything else.
- lc_all = env.get('LC_ALL')
- if lc_all:
- env['LANG'] = lc_all
- del env['LC_ALL']
- env['LC_MESSAGES'] = 'C'
- p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
- stdout=subprocess.PIPE, stderr=subprocess.PIPE,
- universal_newlines=True, env=env, **extrakw)
- stdout, stderr = p.communicate()
- stderr = stderr.strip()
- if not failok:
- if p.returncode:
- raise error.Abort(stderr or 'exited with code %d'
- % p.returncode)
- if stderr:
- self.ui.warn(stderr + '\n')
- return stdout, stderr
-
- @propertycache
- def _svnversion(self):
- output, err = self._svncommand(['--version', '--quiet'], filename=None)
- m = re.search(br'^(\d+)\.(\d+)', output)
- if not m:
- raise error.Abort(_('cannot retrieve svn tool version'))
- return (int(m.group(1)), int(m.group(2)))
-
- def _svnmissing(self):
- return not self.wvfs.exists('.svn')
-
- def _wcrevs(self):
- # Get the working directory revision as well as the last
- # commit revision so we can compare the subrepo state with
- # both. We used to store the working directory one.
- output, err = self._svncommand(['info', '--xml'])
- doc = xml.dom.minidom.parseString(output)
- entries = doc.getElementsByTagName('entry')
- lastrev, rev = '0', '0'
- if entries:
- rev = str(entries[0].getAttribute('revision')) or '0'
- commits = entries[0].getElementsByTagName('commit')
- if commits:
- lastrev = str(commits[0].getAttribute('revision')) or '0'
- return (lastrev, rev)
-
- def _wcrev(self):
- return self._wcrevs()[0]
-
- def _wcchanged(self):
- """Return (changes, extchanges, missing) where changes is True
- if the working directory was changed, extchanges is
- True if any of these changes concern an external entry and missing
- is True if any change is a missing entry.
- """
- output, err = self._svncommand(['status', '--xml'])
- externals, changes, missing = [], [], []
- doc = xml.dom.minidom.parseString(output)
- for e in doc.getElementsByTagName('entry'):
- s = e.getElementsByTagName('wc-status')
- if not s:
- continue
- item = s[0].getAttribute('item')
- props = s[0].getAttribute('props')
- path = e.getAttribute('path')
- if item == 'external':
- externals.append(path)
- elif item == 'missing':
- missing.append(path)
- if (item not in ('', 'normal', 'unversioned', 'external')
- or props not in ('', 'none', 'normal')):
- changes.append(path)
- for path in changes:
- for ext in externals:
- if path == ext or path.startswith(ext + pycompat.ossep):
- return True, True, bool(missing)
- return bool(changes), False, bool(missing)
-
- @annotatesubrepoerror
- def dirty(self, ignoreupdate=False, missing=False):
- if self._svnmissing():
- return self._state[1] != ''
- wcchanged = self._wcchanged()
- changed = wcchanged[0] or (missing and wcchanged[2])
- if not changed:
- if self._state[1] in self._wcrevs() or ignoreupdate:
- return False
- return True
-
- def basestate(self):
- lastrev, rev = self._wcrevs()
- if lastrev != rev:
- # Last committed rev is not the same than rev. We would
- # like to take lastrev but we do not know if the subrepo
- # URL exists at lastrev. Test it and fallback to rev it
- # is not there.
- try:
- self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
- return lastrev
- except error.Abort:
- pass
- return rev
-
- @annotatesubrepoerror
- def commit(self, text, user, date):
- # user and date are out of our hands since svn is centralized
- changed, extchanged, missing = self._wcchanged()
- if not changed:
- return self.basestate()
- if extchanged:
- # Do not try to commit externals
- raise error.Abort(_('cannot commit svn externals'))
- if missing:
- # svn can commit with missing entries but aborting like hg
- # seems a better approach.
- raise error.Abort(_('cannot commit missing svn entries'))
- commitinfo, err = self._svncommand(['commit', '-m', text])
- self.ui.status(commitinfo)
- newrev = re.search('Committed revision ([0-9]+).', commitinfo)
- if not newrev:
- if not commitinfo.strip():
- # Sometimes, our definition of "changed" differs from
- # svn one. For instance, svn ignores missing files
- # when committing. If there are only missing files, no
- # commit is made, no output and no error code.
- raise error.Abort(_('failed to commit svn changes'))
- raise error.Abort(commitinfo.splitlines()[-1])
- newrev = newrev.groups()[0]
- self.ui.status(self._svncommand(['update', '-r', newrev])[0])
- return newrev
-
- @annotatesubrepoerror
- def remove(self):
- if self.dirty():
- self.ui.warn(_('not removing repo %s because '
- 'it has changes.\n') % self._path)
- return
- self.ui.note(_('removing subrepo %s\n') % self._path)
-
- self.wvfs.rmtree(forcibly=True)
- try:
- pwvfs = self._ctx.repo().wvfs
- pwvfs.removedirs(pwvfs.dirname(self._path))
- except OSError:
- pass
-
- @annotatesubrepoerror
- def get(self, state, overwrite=False):
- if overwrite:
- self._svncommand(['revert', '--recursive'])
- args = ['checkout']
- if self._svnversion >= (1, 5):
- args.append('--force')
- # The revision must be specified at the end of the URL to properly
- # update to a directory which has since been deleted and recreated.
- args.append('%s@%s' % (state[0], state[1]))
-
- # SEC: check that the ssh url is safe
- util.checksafessh(state[0])
-
- status, err = self._svncommand(args, failok=True)
- _sanitize(self.ui, self.wvfs, '.svn')
- if not re.search('Checked out revision [0-9]+.', status):
- if ('is already a working copy for a different URL' in err
- and (self._wcchanged()[:2] == (False, False))):
- # obstructed but clean working copy, so just blow it away.
- self.remove()
- self.get(state, overwrite=False)
- return
- raise error.Abort((status or err).splitlines()[-1])
- self.ui.status(status)
-
- @annotatesubrepoerror
- def merge(self, state):
- old = self._state[1]
- new = state[1]
- wcrev = self._wcrev()
- if new != wcrev:
- dirty = old == wcrev or self._wcchanged()[0]
- if _updateprompt(self.ui, self, dirty, wcrev, new):
- self.get(state, False)
-
- def push(self, opts):
- # push is a no-op for SVN
- return True
-
- @annotatesubrepoerror
- def files(self):
- output = self._svncommand(['list', '--recursive', '--xml'])[0]
- doc = xml.dom.minidom.parseString(output)
- paths = []
- for e in doc.getElementsByTagName('entry'):
- kind = str(e.getAttribute('kind'))
- if kind != 'file':
- continue
- name = ''.join(c.data for c
- in e.getElementsByTagName('name')[0].childNodes
- if c.nodeType == c.TEXT_NODE)
- paths.append(name.encode('utf-8'))
- return paths
-
- def filedata(self, name, decode):
- return self._svncommand(['cat'], name)[0]
-
-
-class gitsubrepo(abstractsubrepo):
- def __init__(self, ctx, path, state, allowcreate):
- super(gitsubrepo, self).__init__(ctx, path)
- self._state = state
- self._abspath = ctx.repo().wjoin(path)
- self._subparent = ctx.repo()
- self._ensuregit()
-
- def _ensuregit(self):
- try:
- self._gitexecutable = 'git'
- out, err = self._gitnodir(['--version'])
- except OSError as e:
- genericerror = _("error executing git for subrepo '%s': %s")
- notfoundhint = _("check git is installed and in your PATH")
- if e.errno != errno.ENOENT:
- raise error.Abort(genericerror % (
- self._path, encoding.strtolocal(e.strerror)))
- elif pycompat.iswindows:
- try:
- self._gitexecutable = 'git.cmd'
- out, err = self._gitnodir(['--version'])
- except OSError as e2:
- if e2.errno == errno.ENOENT:
- raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
- " for subrepo '%s'") % self._path,
- hint=notfoundhint)
- else:
- raise error.Abort(genericerror % (self._path,
- encoding.strtolocal(e2.strerror)))
- else:
- raise error.Abort(_("couldn't find git for subrepo '%s'")
- % self._path, hint=notfoundhint)
- versionstatus = self._checkversion(out)
- if versionstatus == 'unknown':
- self.ui.warn(_('cannot retrieve git version\n'))
- elif versionstatus == 'abort':
- raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
- elif versionstatus == 'warning':
- self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
-
- @staticmethod
- def _gitversion(out):
- m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
- if m:
- return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
-
- m = re.search(br'^git version (\d+)\.(\d+)', out)
- if m:
- return (int(m.group(1)), int(m.group(2)), 0)
-
- return -1
-
- @staticmethod
- def _checkversion(out):
- '''ensure git version is new enough
-
- >>> _checkversion = gitsubrepo._checkversion
- >>> _checkversion(b'git version 1.6.0')
- 'ok'
- >>> _checkversion(b'git version 1.8.5')
- 'ok'
- >>> _checkversion(b'git version 1.4.0')
- 'abort'
- >>> _checkversion(b'git version 1.5.0')
- 'warning'
- >>> _checkversion(b'git version 1.9-rc0')
- 'ok'
- >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
- 'ok'
- >>> _checkversion(b'git version 1.9.0.GIT')
- 'ok'
- >>> _checkversion(b'git version 12345')
- 'unknown'
- >>> _checkversion(b'no')
- 'unknown'
- '''
- version = gitsubrepo._gitversion(out)
- # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
- # despite the docstring comment. For now, error on 1.4.0, warn on
- # 1.5.0 but attempt to continue.
- if version == -1:
- return 'unknown'
- if version < (1, 5, 0):
- return 'abort'
- elif version < (1, 6, 0):
- return 'warning'
- return 'ok'
-
- def _gitcommand(self, commands, env=None, stream=False):
- return self._gitdir(commands, env=env, stream=stream)[0]
-
- def _gitdir(self, commands, env=None, stream=False):
- return self._gitnodir(commands, env=env, stream=stream,
- cwd=self._abspath)
-
- def _gitnodir(self, commands, env=None, stream=False, cwd=None):
- """Calls the git command
-
- The methods tries to call the git command. versions prior to 1.6.0
- are not supported and very probably fail.
- """
- self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
- if env is None:
- env = encoding.environ.copy()
- # disable localization for Git output (issue5176)
- env['LC_ALL'] = 'C'
- # fix for Git CVE-2015-7545
- if 'GIT_ALLOW_PROTOCOL' not in env:
- env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
- # unless ui.quiet is set, print git's stderr,
- # which is mostly progress and useful info
- errpipe = None
- if self.ui.quiet:
- errpipe = open(os.devnull, 'w')
- if self.ui._colormode and len(commands) and commands[0] == "diff":
- # insert the argument in the front,
- # the end of git diff arguments is used for paths
- commands.insert(1, '--color')
- p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
- cwd=cwd, env=env, close_fds=util.closefds,
- stdout=subprocess.PIPE, stderr=errpipe)
- if stream:
- return p.stdout, None
-
- retdata = p.stdout.read().strip()
- # wait for the child to exit to avoid race condition.
- p.wait()
-
- if p.returncode != 0 and p.returncode != 1:
- # there are certain error codes that are ok
- command = commands[0]
- if command in ('cat-file', 'symbolic-ref'):
- return retdata, p.returncode
- # for all others, abort
- raise error.Abort(_('git %s error %d in %s') %
- (command, p.returncode, self._relpath))
-
- return retdata, p.returncode
-
- def _gitmissing(self):
- return not self.wvfs.exists('.git')
-
- def _gitstate(self):
- return self._gitcommand(['rev-parse', 'HEAD'])
-
- def _gitcurrentbranch(self):
- current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
- if err:
- current = None
- return current
-
- def _gitremote(self, remote):
- out = self._gitcommand(['remote', 'show', '-n', remote])
- line = out.split('\n')[1]
- i = line.index('URL: ') + len('URL: ')
- return line[i:]
-
- def _githavelocally(self, revision):
- out, code = self._gitdir(['cat-file', '-e', revision])
- return code == 0
-
- def _gitisancestor(self, r1, r2):
- base = self._gitcommand(['merge-base', r1, r2])
- return base == r1
-
- def _gitisbare(self):
- return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
-
- def _gitupdatestat(self):
- """This must be run before git diff-index.
- diff-index only looks at changes to file stat;
- this command looks at file contents and updates the stat."""
- self._gitcommand(['update-index', '-q', '--refresh'])
-
- def _gitbranchmap(self):
- '''returns 2 things:
- a map from git branch to revision
- a map from revision to branches'''
- branch2rev = {}
- rev2branch = {}
-
- out = self._gitcommand(['for-each-ref', '--format',
- '%(objectname) %(refname)'])
- for line in out.split('\n'):
- revision, ref = line.split(' ')
- if (not ref.startswith('refs/heads/') and
- not ref.startswith('refs/remotes/')):
- continue
- if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
- continue # ignore remote/HEAD redirects
- branch2rev[ref] = revision
- rev2branch.setdefault(revision, []).append(ref)
- return branch2rev, rev2branch
-
- def _gittracking(self, branches):
- 'return map of remote branch to local tracking branch'
- # assumes no more than one local tracking branch for each remote
- tracking = {}
- for b in branches:
- if b.startswith('refs/remotes/'):
- continue
- bname = b.split('/', 2)[2]
- remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
- if remote:
- ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
- tracking['refs/remotes/%s/%s' %
- (remote, ref.split('/', 2)[2])] = b
- return tracking
-
- def _abssource(self, source):
- if '://' not in source:
- # recognize the scp syntax as an absolute source
- colon = source.find(':')
- if colon != -1 and '/' not in source[:colon]:
- return source
- self._subsource = source
- return _abssource(self)
-
- def _fetch(self, source, revision):
- if self._gitmissing():
- # SEC: check for safe ssh url
- util.checksafessh(source)
-
- source = self._abssource(source)
- self.ui.status(_('cloning subrepo %s from %s\n') %
- (self._relpath, source))
- self._gitnodir(['clone', source, self._abspath])
- if self._githavelocally(revision):
- return
- self.ui.status(_('pulling subrepo %s from %s\n') %
- (self._relpath, self._gitremote('origin')))
- # try only origin: the originally cloned repo
- self._gitcommand(['fetch'])
- if not self._githavelocally(revision):
- raise error.Abort(_('revision %s does not exist in subrepository '
- '"%s"\n') % (revision, self._relpath))
-
- @annotatesubrepoerror
- def dirty(self, ignoreupdate=False, missing=False):
- if self._gitmissing():
- return self._state[1] != ''
- if self._gitisbare():
- return True
- if not ignoreupdate and self._state[1] != self._gitstate():
- # different version checked out
- return True
- # check for staged changes or modified files; ignore untracked files
- self._gitupdatestat()
- out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
- return code == 1
-
- def basestate(self):
- return self._gitstate()
-
- @annotatesubrepoerror
- def get(self, state, overwrite=False):
- source, revision, kind = state
- if not revision:
- self.remove()
- return
- self._fetch(source, revision)
- # if the repo was set to be bare, unbare it
- if self._gitisbare():
- self._gitcommand(['config', 'core.bare', 'false'])
- if self._gitstate() == revision:
- self._gitcommand(['reset', '--hard', 'HEAD'])
- return
- elif self._gitstate() == revision:
- if overwrite:
- # first reset the index to unmark new files for commit, because
- # reset --hard will otherwise throw away files added for commit,
- # not just unmark them.
- self._gitcommand(['reset', 'HEAD'])
- self._gitcommand(['reset', '--hard', 'HEAD'])
- return
- branch2rev, rev2branch = self._gitbranchmap()
-
- def checkout(args):
- cmd = ['checkout']
- if overwrite:
- # first reset the index to unmark new files for commit, because
- # the -f option will otherwise throw away files added for
- # commit, not just unmark them.
- self._gitcommand(['reset', 'HEAD'])
- cmd.append('-f')
- self._gitcommand(cmd + args)
- _sanitize(self.ui, self.wvfs, '.git')
-
- def rawcheckout():
- # no branch to checkout, check it out with no branch
- self.ui.warn(_('checking out detached HEAD in '
- 'subrepository "%s"\n') % self._relpath)
- self.ui.warn(_('check out a git branch if you intend '
- 'to make changes\n'))
- checkout(['-q', revision])
-
- if revision not in rev2branch:
- rawcheckout()
- return
- branches = rev2branch[revision]
- firstlocalbranch = None
- for b in branches:
- if b == 'refs/heads/master':
- # master trumps all other branches
- checkout(['refs/heads/master'])
- return
- if not firstlocalbranch and not b.startswith('refs/remotes/'):
- firstlocalbranch = b
- if firstlocalbranch:
- checkout([firstlocalbranch])
- return
-
- tracking = self._gittracking(branch2rev.keys())
- # choose a remote branch already tracked if possible
- remote = branches[0]
- if remote not in tracking:
- for b in branches:
- if b in tracking:
- remote = b
- break
-
- if remote not in tracking:
- # create a new local tracking branch
- local = remote.split('/', 3)[3]
- checkout(['-b', local, remote])
- elif self._gitisancestor(branch2rev[tracking[remote]], remote):
- # When updating to a tracked remote branch,
- # if the local tracking branch is downstream of it,
- # a normal `git pull` would have performed a "fast-forward merge"
- # which is equivalent to updating the local branch to the remote.
- # Since we are only looking at branching at update, we need to
- # detect this situation and perform this action lazily.
- if tracking[remote] != self._gitcurrentbranch():
- checkout([tracking[remote]])
- self._gitcommand(['merge', '--ff', remote])
- _sanitize(self.ui, self.wvfs, '.git')
- else:
- # a real merge would be required, just checkout the revision
- rawcheckout()
-
- @annotatesubrepoerror
- def commit(self, text, user, date):
- if self._gitmissing():
- raise error.Abort(_("subrepo %s is missing") % self._relpath)
- cmd = ['commit', '-a', '-m', text]
- env = encoding.environ.copy()
- if user:
- cmd += ['--author', user]
- if date:
- # git's date parser silently ignores when seconds < 1e9
- # convert to ISO8601
- env['GIT_AUTHOR_DATE'] = util.datestr(date,
- '%Y-%m-%dT%H:%M:%S %1%2')
- self._gitcommand(cmd, env=env)
- # make sure commit works otherwise HEAD might not exist under certain
- # circumstances
- return self._gitstate()
-
- @annotatesubrepoerror
- def merge(self, state):
- source, revision, kind = state
- self._fetch(source, revision)
- base = self._gitcommand(['merge-base', revision, self._state[1]])
- self._gitupdatestat()
- out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
-
- def mergefunc():
- if base == revision:
- self.get(state) # fast forward merge
- elif base != self._state[1]:
- self._gitcommand(['merge', '--no-commit', revision])
- _sanitize(self.ui, self.wvfs, '.git')
-
- if self.dirty():
- if self._gitstate() != revision:
- dirty = self._gitstate() == self._state[1] or code != 0
- if _updateprompt(self.ui, self, dirty,
- self._state[1][:7], revision[:7]):
- mergefunc()
- else:
- mergefunc()
-
- @annotatesubrepoerror
- def push(self, opts):
- force = opts.get('force')
-
- if not self._state[1]:
- return True
- if self._gitmissing():
- raise error.Abort(_("subrepo %s is missing") % self._relpath)
- # if a branch in origin contains the revision, nothing to do
- branch2rev, rev2branch = self._gitbranchmap()
- if self._state[1] in rev2branch:
- for b in rev2branch[self._state[1]]:
- if b.startswith('refs/remotes/origin/'):
- return True
- for b, revision in branch2rev.iteritems():
- if b.startswith('refs/remotes/origin/'):
- if self._gitisancestor(self._state[1], revision):
- return True
- # otherwise, try to push the currently checked out branch
- cmd = ['push']
- if force:
- cmd.append('--force')
-
- current = self._gitcurrentbranch()
- if current:
- # determine if the current branch is even useful
- if not self._gitisancestor(self._state[1], current):
- self.ui.warn(_('unrelated git branch checked out '
- 'in subrepository "%s"\n') % self._relpath)
- return False
- self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
- (current.split('/', 2)[2], self._relpath))
- ret = self._gitdir(cmd + ['origin', current])
- return ret[1] == 0
- else:
- self.ui.warn(_('no branch checked out in subrepository "%s"\n'
- 'cannot push revision %s\n') %
- (self._relpath, self._state[1]))
- return False
-
- @annotatesubrepoerror
- def add(self, ui, match, prefix, explicitonly, **opts):
- if self._gitmissing():
- return []
-
- (modified, added, removed,
- deleted, unknown, ignored, clean) = self.status(None, unknown=True,
- clean=True)
-
- tracked = set()
- # dirstates 'amn' warn, 'r' is added again
- for l in (modified, added, deleted, clean):
- tracked.update(l)
-
- # Unknown files not of interest will be rejected by the matcher
- files = unknown
- files.extend(match.files())
-
- rejected = []
-
- files = [f for f in sorted(set(files)) if match(f)]
- for f in files:
- exact = match.exact(f)
- command = ["add"]
- if exact:
- command.append("-f") #should be added, even if ignored
- if ui.verbose or not exact:
- ui.status(_('adding %s\n') % match.rel(f))
-
- if f in tracked: # hg prints 'adding' even if already tracked
- if exact:
- rejected.append(f)
- continue
- if not opts.get(r'dry_run'):
- self._gitcommand(command + [f])
-
- for f in rejected:
- ui.warn(_("%s already tracked!\n") % match.abs(f))
-
- return rejected
-
- @annotatesubrepoerror
- def remove(self):
- if self._gitmissing():
- return
- if self.dirty():
- self.ui.warn(_('not removing repo %s because '
- 'it has changes.\n') % self._relpath)
- return
- # we can't fully delete the repository as it may contain
- # local-only history
- self.ui.note(_('removing subrepo %s\n') % self._relpath)
- self._gitcommand(['config', 'core.bare', 'true'])
- for f, kind in self.wvfs.readdir():
- if f == '.git':
- continue
- if kind == stat.S_IFDIR:
- self.wvfs.rmtree(f)
- else:
- self.wvfs.unlink(f)
-
- def archive(self, archiver, prefix, match=None, decode=True):
- total = 0
- source, revision = self._state
- if not revision:
- return total
- self._fetch(source, revision)
-
- # Parse git's native archive command.
- # This should be much faster than manually traversing the trees
- # and objects with many subprocess calls.
- tarstream = self._gitcommand(['archive', revision], stream=True)
- tar = tarfile.open(fileobj=tarstream, mode='r|')
- relpath = subrelpath(self)
- self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
- for i, info in enumerate(tar):
- if info.isdir():
- continue
- if match and not match(info.name):
- continue
- if info.issym():
- data = info.linkname
- else:
- data = tar.extractfile(info).read()
- archiver.addfile(prefix + self._path + '/' + info.name,
- info.mode, info.issym(), data)
- total += 1
- self.ui.progress(_('archiving (%s)') % relpath, i + 1,
- unit=_('files'))
- self.ui.progress(_('archiving (%s)') % relpath, None)
- return total
-
-
- @annotatesubrepoerror
- def cat(self, match, fm, fntemplate, prefix, **opts):
- rev = self._state[1]
- if match.anypats():
- return 1 #No support for include/exclude yet
-
- if not match.files():
- return 1
-
- # TODO: add support for non-plain formatter (see cmdutil.cat())
- for f in match.files():
- output = self._gitcommand(["show", "%s:%s" % (rev, f)])
- fp = cmdutil.makefileobj(self._subparent, fntemplate,
- self._ctx.node(),
- pathname=self.wvfs.reljoin(prefix, f))
- fp.write(output)
- fp.close()
- return 0
-
-
- @annotatesubrepoerror
- def status(self, rev2, **opts):
- rev1 = self._state[1]
- if self._gitmissing() or not rev1:
- # if the repo is missing, return no results
- return scmutil.status([], [], [], [], [], [], [])
- modified, added, removed = [], [], []
- self._gitupdatestat()
- if rev2:
- command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
- else:
- command = ['diff-index', '--no-renames', rev1]
- out = self._gitcommand(command)
- for line in out.split('\n'):
- tab = line.find('\t')
- if tab == -1:
- continue
- status, f = line[tab - 1], line[tab + 1:]
- if status == 'M':
- modified.append(f)
- elif status == 'A':
- added.append(f)
- elif status == 'D':
- removed.append(f)
-
- deleted, unknown, ignored, clean = [], [], [], []
-
- command = ['status', '--porcelain', '-z']
- if opts.get(r'unknown'):
- command += ['--untracked-files=all']
- if opts.get(r'ignored'):
- command += ['--ignored']
- out = self._gitcommand(command)
-
- changedfiles = set()
- changedfiles.update(modified)
- changedfiles.update(added)
- changedfiles.update(removed)
- for line in out.split('\0'):
- if not line:
- continue
- st = line[0:2]
- #moves and copies show 2 files on one line
- if line.find('\0') >= 0:
- filename1, filename2 = line[3:].split('\0')
- else:
- filename1 = line[3:]
- filename2 = None
-
- changedfiles.add(filename1)
- if filename2:
- changedfiles.add(filename2)
-
- if st == '??':
- unknown.append(filename1)
- elif st == '!!':
- ignored.append(filename1)
-
- if opts.get(r'clean'):
- out = self._gitcommand(['ls-files'])
- for f in out.split('\n'):
- if not f in changedfiles:
- clean.append(f)
-
- return scmutil.status(modified, added, removed, deleted,
- unknown, ignored, clean)
-
- @annotatesubrepoerror
- def diff(self, ui, diffopts, node2, match, prefix, **opts):
- node1 = self._state[1]
- cmd = ['diff', '--no-renames']
- if opts[r'stat']:
- cmd.append('--stat')
- else:
- # for Git, this also implies '-p'
- cmd.append('-U%d' % diffopts.context)
-
- gitprefix = self.wvfs.reljoin(prefix, self._path)
-
- if diffopts.noprefix:
- cmd.extend(['--src-prefix=%s/' % gitprefix,
- '--dst-prefix=%s/' % gitprefix])
- else:
- cmd.extend(['--src-prefix=a/%s/' % gitprefix,
- '--dst-prefix=b/%s/' % gitprefix])
-
- if diffopts.ignorews:
- cmd.append('--ignore-all-space')
- if diffopts.ignorewsamount:
- cmd.append('--ignore-space-change')
- if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
- and diffopts.ignoreblanklines:
- cmd.append('--ignore-blank-lines')
-
- cmd.append(node1)
- if node2:
- cmd.append(node2)
-
- output = ""
- if match.always():
- output += self._gitcommand(cmd) + '\n'
- else:
- st = self.status(node2)[:3]
- files = [f for sublist in st for f in sublist]
- for f in files:
- if match(f):
- output += self._gitcommand(cmd + ['--', f]) + '\n'
-
- if output.strip():
- ui.write(output)
-
- @annotatesubrepoerror
- def revert(self, substate, *pats, **opts):
- self.ui.status(_('reverting subrepo %s\n') % substate[0])
- if not opts.get(r'no_backup'):
- status = self.status(None)
- names = status.modified
- for name in names:
- bakname = scmutil.origpath(self.ui, self._subparent, name)
- self.ui.note(_('saving current version of %s as %s\n') %
- (name, bakname))
- self.wvfs.rename(name, bakname)
-
- if not opts.get(r'dry_run'):
- self.get(substate, overwrite=True)
- return []
-
- def shortid(self, revid):
- return revid[:7]
-
-types = {
- 'hg': hgsubrepo,
- 'svn': svnsubrepo,
- 'git': gitsubrepo,
- }
More information about the Mercurial-devel
mailing list