##// END OF EJS Templates
subrepo: use repo['.'] instead of repo['']...
Martin von Zweigbergk -
r37283:32857300 default
parent child Browse files
Show More
@@ -1,1815 +1,1815 b''
1 # subrepo.py - sub-repository classes and factory
1 # subrepo.py - sub-repository classes and factory
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import posixpath
14 import posixpath
15 import re
15 import re
16 import stat
16 import stat
17 import subprocess
17 import subprocess
18 import sys
18 import sys
19 import tarfile
19 import tarfile
20 import xml.dom.minidom
20 import xml.dom.minidom
21
21
22 from .i18n import _
22 from .i18n import _
23 from . import (
23 from . import (
24 cmdutil,
24 cmdutil,
25 encoding,
25 encoding,
26 error,
26 error,
27 exchange,
27 exchange,
28 logcmdutil,
28 logcmdutil,
29 match as matchmod,
29 match as matchmod,
30 node,
30 node,
31 pathutil,
31 pathutil,
32 phases,
32 phases,
33 pycompat,
33 pycompat,
34 scmutil,
34 scmutil,
35 subrepoutil,
35 subrepoutil,
36 util,
36 util,
37 vfs as vfsmod,
37 vfs as vfsmod,
38 )
38 )
39 from .utils import (
39 from .utils import (
40 dateutil,
40 dateutil,
41 procutil,
41 procutil,
42 stringutil,
42 stringutil,
43 )
43 )
44
44
45 hg = None
45 hg = None
46 reporelpath = subrepoutil.reporelpath
46 reporelpath = subrepoutil.reporelpath
47 subrelpath = subrepoutil.subrelpath
47 subrelpath = subrepoutil.subrelpath
48 _abssource = subrepoutil._abssource
48 _abssource = subrepoutil._abssource
49 propertycache = util.propertycache
49 propertycache = util.propertycache
50
50
51 def _expandedabspath(path):
51 def _expandedabspath(path):
52 '''
52 '''
53 get a path or url and if it is a path expand it and return an absolute path
53 get a path or url and if it is a path expand it and return an absolute path
54 '''
54 '''
55 expandedpath = util.urllocalpath(util.expandpath(path))
55 expandedpath = util.urllocalpath(util.expandpath(path))
56 u = util.url(expandedpath)
56 u = util.url(expandedpath)
57 if not u.scheme:
57 if not u.scheme:
58 path = util.normpath(os.path.abspath(u.path))
58 path = util.normpath(os.path.abspath(u.path))
59 return path
59 return path
60
60
61 def _getstorehashcachename(remotepath):
61 def _getstorehashcachename(remotepath):
62 '''get a unique filename for the store hash cache of a remote repository'''
62 '''get a unique filename for the store hash cache of a remote repository'''
63 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
63 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
64
64
65 class SubrepoAbort(error.Abort):
65 class SubrepoAbort(error.Abort):
66 """Exception class used to avoid handling a subrepo error more than once"""
66 """Exception class used to avoid handling a subrepo error more than once"""
67 def __init__(self, *args, **kw):
67 def __init__(self, *args, **kw):
68 self.subrepo = kw.pop(r'subrepo', None)
68 self.subrepo = kw.pop(r'subrepo', None)
69 self.cause = kw.pop(r'cause', None)
69 self.cause = kw.pop(r'cause', None)
70 error.Abort.__init__(self, *args, **kw)
70 error.Abort.__init__(self, *args, **kw)
71
71
72 def annotatesubrepoerror(func):
72 def annotatesubrepoerror(func):
73 def decoratedmethod(self, *args, **kargs):
73 def decoratedmethod(self, *args, **kargs):
74 try:
74 try:
75 res = func(self, *args, **kargs)
75 res = func(self, *args, **kargs)
76 except SubrepoAbort as ex:
76 except SubrepoAbort as ex:
77 # This exception has already been handled
77 # This exception has already been handled
78 raise ex
78 raise ex
79 except error.Abort as ex:
79 except error.Abort as ex:
80 subrepo = subrelpath(self)
80 subrepo = subrelpath(self)
81 errormsg = (stringutil.forcebytestr(ex) + ' '
81 errormsg = (stringutil.forcebytestr(ex) + ' '
82 + _('(in subrepository "%s")') % subrepo)
82 + _('(in subrepository "%s")') % subrepo)
83 # avoid handling this exception by raising a SubrepoAbort exception
83 # avoid handling this exception by raising a SubrepoAbort exception
84 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
84 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
85 cause=sys.exc_info())
85 cause=sys.exc_info())
86 return res
86 return res
87 return decoratedmethod
87 return decoratedmethod
88
88
89 def _updateprompt(ui, sub, dirty, local, remote):
89 def _updateprompt(ui, sub, dirty, local, remote):
90 if dirty:
90 if dirty:
91 msg = (_(' subrepository sources for %s differ\n'
91 msg = (_(' subrepository sources for %s differ\n'
92 'use (l)ocal source (%s) or (r)emote source (%s)?'
92 'use (l)ocal source (%s) or (r)emote source (%s)?'
93 '$$ &Local $$ &Remote')
93 '$$ &Local $$ &Remote')
94 % (subrelpath(sub), local, remote))
94 % (subrelpath(sub), local, remote))
95 else:
95 else:
96 msg = (_(' subrepository sources for %s differ (in checked out '
96 msg = (_(' subrepository sources for %s differ (in checked out '
97 'version)\n'
97 'version)\n'
98 'use (l)ocal source (%s) or (r)emote source (%s)?'
98 'use (l)ocal source (%s) or (r)emote source (%s)?'
99 '$$ &Local $$ &Remote')
99 '$$ &Local $$ &Remote')
100 % (subrelpath(sub), local, remote))
100 % (subrelpath(sub), local, remote))
101 return ui.promptchoice(msg, 0)
101 return ui.promptchoice(msg, 0)
102
102
103 def _sanitize(ui, vfs, ignore):
103 def _sanitize(ui, vfs, ignore):
104 for dirname, dirs, names in vfs.walk():
104 for dirname, dirs, names in vfs.walk():
105 for i, d in enumerate(dirs):
105 for i, d in enumerate(dirs):
106 if d.lower() == ignore:
106 if d.lower() == ignore:
107 del dirs[i]
107 del dirs[i]
108 break
108 break
109 if vfs.basename(dirname).lower() != '.hg':
109 if vfs.basename(dirname).lower() != '.hg':
110 continue
110 continue
111 for f in names:
111 for f in names:
112 if f.lower() == 'hgrc':
112 if f.lower() == 'hgrc':
113 ui.warn(_("warning: removing potentially hostile 'hgrc' "
113 ui.warn(_("warning: removing potentially hostile 'hgrc' "
114 "in '%s'\n") % vfs.join(dirname))
114 "in '%s'\n") % vfs.join(dirname))
115 vfs.unlink(vfs.reljoin(dirname, f))
115 vfs.unlink(vfs.reljoin(dirname, f))
116
116
117 def _auditsubrepopath(repo, path):
117 def _auditsubrepopath(repo, path):
118 # auditor doesn't check if the path itself is a symlink
118 # auditor doesn't check if the path itself is a symlink
119 pathutil.pathauditor(repo.root)(path)
119 pathutil.pathauditor(repo.root)(path)
120 if repo.wvfs.islink(path):
120 if repo.wvfs.islink(path):
121 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
121 raise error.Abort(_("subrepo '%s' traverses symbolic link") % path)
122
122
123 SUBREPO_ALLOWED_DEFAULTS = {
123 SUBREPO_ALLOWED_DEFAULTS = {
124 'hg': True,
124 'hg': True,
125 'git': False,
125 'git': False,
126 'svn': False,
126 'svn': False,
127 }
127 }
128
128
129 def _checktype(ui, kind):
129 def _checktype(ui, kind):
130 # subrepos.allowed is a master kill switch. If disabled, subrepos are
130 # subrepos.allowed is a master kill switch. If disabled, subrepos are
131 # disabled period.
131 # disabled period.
132 if not ui.configbool('subrepos', 'allowed', True):
132 if not ui.configbool('subrepos', 'allowed', True):
133 raise error.Abort(_('subrepos not enabled'),
133 raise error.Abort(_('subrepos not enabled'),
134 hint=_("see 'hg help config.subrepos' for details"))
134 hint=_("see 'hg help config.subrepos' for details"))
135
135
136 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
136 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
137 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
137 if not ui.configbool('subrepos', '%s:allowed' % kind, default):
138 raise error.Abort(_('%s subrepos not allowed') % kind,
138 raise error.Abort(_('%s subrepos not allowed') % kind,
139 hint=_("see 'hg help config.subrepos' for details"))
139 hint=_("see 'hg help config.subrepos' for details"))
140
140
141 if kind not in types:
141 if kind not in types:
142 raise error.Abort(_('unknown subrepo type %s') % kind)
142 raise error.Abort(_('unknown subrepo type %s') % kind)
143
143
144 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
144 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
145 """return instance of the right subrepo class for subrepo in path"""
145 """return instance of the right subrepo class for subrepo in path"""
146 # subrepo inherently violates our import layering rules
146 # subrepo inherently violates our import layering rules
147 # because it wants to make repo objects from deep inside the stack
147 # because it wants to make repo objects from deep inside the stack
148 # so we manually delay the circular imports to not break
148 # so we manually delay the circular imports to not break
149 # scripts that don't use our demand-loading
149 # scripts that don't use our demand-loading
150 global hg
150 global hg
151 from . import hg as h
151 from . import hg as h
152 hg = h
152 hg = h
153
153
154 repo = ctx.repo()
154 repo = ctx.repo()
155 _auditsubrepopath(repo, path)
155 _auditsubrepopath(repo, path)
156 state = ctx.substate[path]
156 state = ctx.substate[path]
157 _checktype(repo.ui, state[2])
157 _checktype(repo.ui, state[2])
158 if allowwdir:
158 if allowwdir:
159 state = (state[0], ctx.subrev(path), state[2])
159 state = (state[0], ctx.subrev(path), state[2])
160 return types[state[2]](ctx, path, state[:2], allowcreate)
160 return types[state[2]](ctx, path, state[:2], allowcreate)
161
161
162 def nullsubrepo(ctx, path, pctx):
162 def nullsubrepo(ctx, path, pctx):
163 """return an empty subrepo in pctx for the extant subrepo in ctx"""
163 """return an empty subrepo in pctx for the extant subrepo in ctx"""
164 # subrepo inherently violates our import layering rules
164 # subrepo inherently violates our import layering rules
165 # because it wants to make repo objects from deep inside the stack
165 # because it wants to make repo objects from deep inside the stack
166 # so we manually delay the circular imports to not break
166 # so we manually delay the circular imports to not break
167 # scripts that don't use our demand-loading
167 # scripts that don't use our demand-loading
168 global hg
168 global hg
169 from . import hg as h
169 from . import hg as h
170 hg = h
170 hg = h
171
171
172 repo = ctx.repo()
172 repo = ctx.repo()
173 _auditsubrepopath(repo, path)
173 _auditsubrepopath(repo, path)
174 state = ctx.substate[path]
174 state = ctx.substate[path]
175 _checktype(repo.ui, state[2])
175 _checktype(repo.ui, state[2])
176 subrev = ''
176 subrev = ''
177 if state[2] == 'hg':
177 if state[2] == 'hg':
178 subrev = "0" * 40
178 subrev = "0" * 40
179 return types[state[2]](pctx, path, (state[0], subrev), True)
179 return types[state[2]](pctx, path, (state[0], subrev), True)
180
180
181 # subrepo classes need to implement the following abstract class:
181 # subrepo classes need to implement the following abstract class:
182
182
183 class abstractsubrepo(object):
183 class abstractsubrepo(object):
184
184
185 def __init__(self, ctx, path):
185 def __init__(self, ctx, path):
186 """Initialize abstractsubrepo part
186 """Initialize abstractsubrepo part
187
187
188 ``ctx`` is the context referring this subrepository in the
188 ``ctx`` is the context referring this subrepository in the
189 parent repository.
189 parent repository.
190
190
191 ``path`` is the path to this subrepository as seen from
191 ``path`` is the path to this subrepository as seen from
192 innermost repository.
192 innermost repository.
193 """
193 """
194 self.ui = ctx.repo().ui
194 self.ui = ctx.repo().ui
195 self._ctx = ctx
195 self._ctx = ctx
196 self._path = path
196 self._path = path
197
197
198 def addwebdirpath(self, serverpath, webconf):
198 def addwebdirpath(self, serverpath, webconf):
199 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
199 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
200
200
201 ``serverpath`` is the path component of the URL for this repo.
201 ``serverpath`` is the path component of the URL for this repo.
202
202
203 ``webconf`` is the dictionary of hgwebdir entries.
203 ``webconf`` is the dictionary of hgwebdir entries.
204 """
204 """
205 pass
205 pass
206
206
207 def storeclean(self, path):
207 def storeclean(self, path):
208 """
208 """
209 returns true if the repository has not changed since it was last
209 returns true if the repository has not changed since it was last
210 cloned from or pushed to a given repository.
210 cloned from or pushed to a given repository.
211 """
211 """
212 return False
212 return False
213
213
214 def dirty(self, ignoreupdate=False, missing=False):
214 def dirty(self, ignoreupdate=False, missing=False):
215 """returns true if the dirstate of the subrepo is dirty or does not
215 """returns true if the dirstate of the subrepo is dirty or does not
216 match current stored state. If ignoreupdate is true, only check
216 match current stored state. If ignoreupdate is true, only check
217 whether the subrepo has uncommitted changes in its dirstate. If missing
217 whether the subrepo has uncommitted changes in its dirstate. If missing
218 is true, check for deleted files.
218 is true, check for deleted files.
219 """
219 """
220 raise NotImplementedError
220 raise NotImplementedError
221
221
222 def dirtyreason(self, ignoreupdate=False, missing=False):
222 def dirtyreason(self, ignoreupdate=False, missing=False):
223 """return reason string if it is ``dirty()``
223 """return reason string if it is ``dirty()``
224
224
225 Returned string should have enough information for the message
225 Returned string should have enough information for the message
226 of exception.
226 of exception.
227
227
228 This returns None, otherwise.
228 This returns None, otherwise.
229 """
229 """
230 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
230 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
231 return _('uncommitted changes in subrepository "%s"'
231 return _('uncommitted changes in subrepository "%s"'
232 ) % subrelpath(self)
232 ) % subrelpath(self)
233
233
234 def bailifchanged(self, ignoreupdate=False, hint=None):
234 def bailifchanged(self, ignoreupdate=False, hint=None):
235 """raise Abort if subrepository is ``dirty()``
235 """raise Abort if subrepository is ``dirty()``
236 """
236 """
237 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
237 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate,
238 missing=True)
238 missing=True)
239 if dirtyreason:
239 if dirtyreason:
240 raise error.Abort(dirtyreason, hint=hint)
240 raise error.Abort(dirtyreason, hint=hint)
241
241
242 def basestate(self):
242 def basestate(self):
243 """current working directory base state, disregarding .hgsubstate
243 """current working directory base state, disregarding .hgsubstate
244 state and working directory modifications"""
244 state and working directory modifications"""
245 raise NotImplementedError
245 raise NotImplementedError
246
246
247 def checknested(self, path):
247 def checknested(self, path):
248 """check if path is a subrepository within this repository"""
248 """check if path is a subrepository within this repository"""
249 return False
249 return False
250
250
251 def commit(self, text, user, date):
251 def commit(self, text, user, date):
252 """commit the current changes to the subrepo with the given
252 """commit the current changes to the subrepo with the given
253 log message. Use given user and date if possible. Return the
253 log message. Use given user and date if possible. Return the
254 new state of the subrepo.
254 new state of the subrepo.
255 """
255 """
256 raise NotImplementedError
256 raise NotImplementedError
257
257
258 def phase(self, state):
258 def phase(self, state):
259 """returns phase of specified state in the subrepository.
259 """returns phase of specified state in the subrepository.
260 """
260 """
261 return phases.public
261 return phases.public
262
262
263 def remove(self):
263 def remove(self):
264 """remove the subrepo
264 """remove the subrepo
265
265
266 (should verify the dirstate is not dirty first)
266 (should verify the dirstate is not dirty first)
267 """
267 """
268 raise NotImplementedError
268 raise NotImplementedError
269
269
270 def get(self, state, overwrite=False):
270 def get(self, state, overwrite=False):
271 """run whatever commands are needed to put the subrepo into
271 """run whatever commands are needed to put the subrepo into
272 this state
272 this state
273 """
273 """
274 raise NotImplementedError
274 raise NotImplementedError
275
275
276 def merge(self, state):
276 def merge(self, state):
277 """merge currently-saved state with the new state."""
277 """merge currently-saved state with the new state."""
278 raise NotImplementedError
278 raise NotImplementedError
279
279
280 def push(self, opts):
280 def push(self, opts):
281 """perform whatever action is analogous to 'hg push'
281 """perform whatever action is analogous to 'hg push'
282
282
283 This may be a no-op on some systems.
283 This may be a no-op on some systems.
284 """
284 """
285 raise NotImplementedError
285 raise NotImplementedError
286
286
287 def add(self, ui, match, prefix, explicitonly, **opts):
287 def add(self, ui, match, prefix, explicitonly, **opts):
288 return []
288 return []
289
289
290 def addremove(self, matcher, prefix, opts, dry_run, similarity):
290 def addremove(self, matcher, prefix, opts, dry_run, similarity):
291 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
291 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
292 return 1
292 return 1
293
293
294 def cat(self, match, fm, fntemplate, prefix, **opts):
294 def cat(self, match, fm, fntemplate, prefix, **opts):
295 return 1
295 return 1
296
296
297 def status(self, rev2, **opts):
297 def status(self, rev2, **opts):
298 return scmutil.status([], [], [], [], [], [], [])
298 return scmutil.status([], [], [], [], [], [], [])
299
299
300 def diff(self, ui, diffopts, node2, match, prefix, **opts):
300 def diff(self, ui, diffopts, node2, match, prefix, **opts):
301 pass
301 pass
302
302
303 def outgoing(self, ui, dest, opts):
303 def outgoing(self, ui, dest, opts):
304 return 1
304 return 1
305
305
306 def incoming(self, ui, source, opts):
306 def incoming(self, ui, source, opts):
307 return 1
307 return 1
308
308
309 def files(self):
309 def files(self):
310 """return filename iterator"""
310 """return filename iterator"""
311 raise NotImplementedError
311 raise NotImplementedError
312
312
313 def filedata(self, name, decode):
313 def filedata(self, name, decode):
314 """return file data, optionally passed through repo decoders"""
314 """return file data, optionally passed through repo decoders"""
315 raise NotImplementedError
315 raise NotImplementedError
316
316
317 def fileflags(self, name):
317 def fileflags(self, name):
318 """return file flags"""
318 """return file flags"""
319 return ''
319 return ''
320
320
321 def getfileset(self, expr):
321 def getfileset(self, expr):
322 """Resolve the fileset expression for this repo"""
322 """Resolve the fileset expression for this repo"""
323 return set()
323 return set()
324
324
325 def printfiles(self, ui, m, fm, fmt, subrepos):
325 def printfiles(self, ui, m, fm, fmt, subrepos):
326 """handle the files command for this subrepo"""
326 """handle the files command for this subrepo"""
327 return 1
327 return 1
328
328
329 def archive(self, archiver, prefix, match=None, decode=True):
329 def archive(self, archiver, prefix, match=None, decode=True):
330 if match is not None:
330 if match is not None:
331 files = [f for f in self.files() if match(f)]
331 files = [f for f in self.files() if match(f)]
332 else:
332 else:
333 files = self.files()
333 files = self.files()
334 total = len(files)
334 total = len(files)
335 relpath = subrelpath(self)
335 relpath = subrelpath(self)
336 self.ui.progress(_('archiving (%s)') % relpath, 0,
336 self.ui.progress(_('archiving (%s)') % relpath, 0,
337 unit=_('files'), total=total)
337 unit=_('files'), total=total)
338 for i, name in enumerate(files):
338 for i, name in enumerate(files):
339 flags = self.fileflags(name)
339 flags = self.fileflags(name)
340 mode = 'x' in flags and 0o755 or 0o644
340 mode = 'x' in flags and 0o755 or 0o644
341 symlink = 'l' in flags
341 symlink = 'l' in flags
342 archiver.addfile(prefix + self._path + '/' + name,
342 archiver.addfile(prefix + self._path + '/' + name,
343 mode, symlink, self.filedata(name, decode))
343 mode, symlink, self.filedata(name, decode))
344 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
344 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
345 unit=_('files'), total=total)
345 unit=_('files'), total=total)
346 self.ui.progress(_('archiving (%s)') % relpath, None)
346 self.ui.progress(_('archiving (%s)') % relpath, None)
347 return total
347 return total
348
348
349 def walk(self, match):
349 def walk(self, match):
350 '''
350 '''
351 walk recursively through the directory tree, finding all files
351 walk recursively through the directory tree, finding all files
352 matched by the match function
352 matched by the match function
353 '''
353 '''
354
354
355 def forget(self, match, prefix, dryrun):
355 def forget(self, match, prefix, dryrun):
356 return ([], [])
356 return ([], [])
357
357
358 def removefiles(self, matcher, prefix, after, force, subrepos,
358 def removefiles(self, matcher, prefix, after, force, subrepos,
359 dryrun, warnings):
359 dryrun, warnings):
360 """remove the matched files from the subrepository and the filesystem,
360 """remove the matched files from the subrepository and the filesystem,
361 possibly by force and/or after the file has been removed from the
361 possibly by force and/or after the file has been removed from the
362 filesystem. Return 0 on success, 1 on any warning.
362 filesystem. Return 0 on success, 1 on any warning.
363 """
363 """
364 warnings.append(_("warning: removefiles not implemented (%s)")
364 warnings.append(_("warning: removefiles not implemented (%s)")
365 % self._path)
365 % self._path)
366 return 1
366 return 1
367
367
368 def revert(self, substate, *pats, **opts):
368 def revert(self, substate, *pats, **opts):
369 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
369 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
370 % (substate[0], substate[2]))
370 % (substate[0], substate[2]))
371 return []
371 return []
372
372
373 def shortid(self, revid):
373 def shortid(self, revid):
374 return revid
374 return revid
375
375
376 def unshare(self):
376 def unshare(self):
377 '''
377 '''
378 convert this repository from shared to normal storage.
378 convert this repository from shared to normal storage.
379 '''
379 '''
380
380
381 def verify(self):
381 def verify(self):
382 '''verify the integrity of the repository. Return 0 on success or
382 '''verify the integrity of the repository. Return 0 on success or
383 warning, 1 on any error.
383 warning, 1 on any error.
384 '''
384 '''
385 return 0
385 return 0
386
386
387 @propertycache
387 @propertycache
388 def wvfs(self):
388 def wvfs(self):
389 """return vfs to access the working directory of this subrepository
389 """return vfs to access the working directory of this subrepository
390 """
390 """
391 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
391 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
392
392
393 @propertycache
393 @propertycache
394 def _relpath(self):
394 def _relpath(self):
395 """return path to this subrepository as seen from outermost repository
395 """return path to this subrepository as seen from outermost repository
396 """
396 """
397 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
397 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
398
398
399 class hgsubrepo(abstractsubrepo):
399 class hgsubrepo(abstractsubrepo):
400 def __init__(self, ctx, path, state, allowcreate):
400 def __init__(self, ctx, path, state, allowcreate):
401 super(hgsubrepo, self).__init__(ctx, path)
401 super(hgsubrepo, self).__init__(ctx, path)
402 self._state = state
402 self._state = state
403 r = ctx.repo()
403 r = ctx.repo()
404 root = r.wjoin(path)
404 root = r.wjoin(path)
405 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
405 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
406 self._repo = hg.repository(r.baseui, root, create=create)
406 self._repo = hg.repository(r.baseui, root, create=create)
407
407
408 # Propagate the parent's --hidden option
408 # Propagate the parent's --hidden option
409 if r is r.unfiltered():
409 if r is r.unfiltered():
410 self._repo = self._repo.unfiltered()
410 self._repo = self._repo.unfiltered()
411
411
412 self.ui = self._repo.ui
412 self.ui = self._repo.ui
413 for s, k in [('ui', 'commitsubrepos')]:
413 for s, k in [('ui', 'commitsubrepos')]:
414 v = r.ui.config(s, k)
414 v = r.ui.config(s, k)
415 if v:
415 if v:
416 self.ui.setconfig(s, k, v, 'subrepo')
416 self.ui.setconfig(s, k, v, 'subrepo')
417 # internal config: ui._usedassubrepo
417 # internal config: ui._usedassubrepo
418 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
418 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
419 self._initrepo(r, state[0], create)
419 self._initrepo(r, state[0], create)
420
420
421 @annotatesubrepoerror
421 @annotatesubrepoerror
422 def addwebdirpath(self, serverpath, webconf):
422 def addwebdirpath(self, serverpath, webconf):
423 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
423 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
424
424
425 def storeclean(self, path):
425 def storeclean(self, path):
426 with self._repo.lock():
426 with self._repo.lock():
427 return self._storeclean(path)
427 return self._storeclean(path)
428
428
429 def _storeclean(self, path):
429 def _storeclean(self, path):
430 clean = True
430 clean = True
431 itercache = self._calcstorehash(path)
431 itercache = self._calcstorehash(path)
432 for filehash in self._readstorehashcache(path):
432 for filehash in self._readstorehashcache(path):
433 if filehash != next(itercache, None):
433 if filehash != next(itercache, None):
434 clean = False
434 clean = False
435 break
435 break
436 if clean:
436 if clean:
437 # if not empty:
437 # if not empty:
438 # the cached and current pull states have a different size
438 # the cached and current pull states have a different size
439 clean = next(itercache, None) is None
439 clean = next(itercache, None) is None
440 return clean
440 return clean
441
441
442 def _calcstorehash(self, remotepath):
442 def _calcstorehash(self, remotepath):
443 '''calculate a unique "store hash"
443 '''calculate a unique "store hash"
444
444
445 This method is used to to detect when there are changes that may
445 This method is used to to detect when there are changes that may
446 require a push to a given remote path.'''
446 require a push to a given remote path.'''
447 # sort the files that will be hashed in increasing (likely) file size
447 # sort the files that will be hashed in increasing (likely) file size
448 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
448 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
449 yield '# %s\n' % _expandedabspath(remotepath)
449 yield '# %s\n' % _expandedabspath(remotepath)
450 vfs = self._repo.vfs
450 vfs = self._repo.vfs
451 for relname in filelist:
451 for relname in filelist:
452 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
452 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
453 yield '%s = %s\n' % (relname, filehash)
453 yield '%s = %s\n' % (relname, filehash)
454
454
455 @propertycache
455 @propertycache
456 def _cachestorehashvfs(self):
456 def _cachestorehashvfs(self):
457 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
457 return vfsmod.vfs(self._repo.vfs.join('cache/storehash'))
458
458
459 def _readstorehashcache(self, remotepath):
459 def _readstorehashcache(self, remotepath):
460 '''read the store hash cache for a given remote repository'''
460 '''read the store hash cache for a given remote repository'''
461 cachefile = _getstorehashcachename(remotepath)
461 cachefile = _getstorehashcachename(remotepath)
462 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
462 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
463
463
464 def _cachestorehash(self, remotepath):
464 def _cachestorehash(self, remotepath):
465 '''cache the current store hash
465 '''cache the current store hash
466
466
467 Each remote repo requires its own store hash cache, because a subrepo
467 Each remote repo requires its own store hash cache, because a subrepo
468 store may be "clean" versus a given remote repo, but not versus another
468 store may be "clean" versus a given remote repo, but not versus another
469 '''
469 '''
470 cachefile = _getstorehashcachename(remotepath)
470 cachefile = _getstorehashcachename(remotepath)
471 with self._repo.lock():
471 with self._repo.lock():
472 storehash = list(self._calcstorehash(remotepath))
472 storehash = list(self._calcstorehash(remotepath))
473 vfs = self._cachestorehashvfs
473 vfs = self._cachestorehashvfs
474 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
474 vfs.writelines(cachefile, storehash, mode='wb', notindexed=True)
475
475
476 def _getctx(self):
476 def _getctx(self):
477 '''fetch the context for this subrepo revision, possibly a workingctx
477 '''fetch the context for this subrepo revision, possibly a workingctx
478 '''
478 '''
479 if self._ctx.rev() is None:
479 if self._ctx.rev() is None:
480 return self._repo[None] # workingctx if parent is workingctx
480 return self._repo[None] # workingctx if parent is workingctx
481 else:
481 else:
482 rev = self._state[1]
482 rev = self._state[1]
483 return self._repo[rev]
483 return self._repo[rev]
484
484
485 @annotatesubrepoerror
485 @annotatesubrepoerror
486 def _initrepo(self, parentrepo, source, create):
486 def _initrepo(self, parentrepo, source, create):
487 self._repo._subparent = parentrepo
487 self._repo._subparent = parentrepo
488 self._repo._subsource = source
488 self._repo._subsource = source
489
489
490 if create:
490 if create:
491 lines = ['[paths]\n']
491 lines = ['[paths]\n']
492
492
493 def addpathconfig(key, value):
493 def addpathconfig(key, value):
494 if value:
494 if value:
495 lines.append('%s = %s\n' % (key, value))
495 lines.append('%s = %s\n' % (key, value))
496 self.ui.setconfig('paths', key, value, 'subrepo')
496 self.ui.setconfig('paths', key, value, 'subrepo')
497
497
498 defpath = _abssource(self._repo, abort=False)
498 defpath = _abssource(self._repo, abort=False)
499 defpushpath = _abssource(self._repo, True, abort=False)
499 defpushpath = _abssource(self._repo, True, abort=False)
500 addpathconfig('default', defpath)
500 addpathconfig('default', defpath)
501 if defpath != defpushpath:
501 if defpath != defpushpath:
502 addpathconfig('default-push', defpushpath)
502 addpathconfig('default-push', defpushpath)
503
503
504 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
504 self._repo.vfs.write('hgrc', util.tonativeeol(''.join(lines)))
505
505
506 @annotatesubrepoerror
506 @annotatesubrepoerror
507 def add(self, ui, match, prefix, explicitonly, **opts):
507 def add(self, ui, match, prefix, explicitonly, **opts):
508 return cmdutil.add(ui, self._repo, match,
508 return cmdutil.add(ui, self._repo, match,
509 self.wvfs.reljoin(prefix, self._path),
509 self.wvfs.reljoin(prefix, self._path),
510 explicitonly, **opts)
510 explicitonly, **opts)
511
511
512 @annotatesubrepoerror
512 @annotatesubrepoerror
513 def addremove(self, m, prefix, opts, dry_run, similarity):
513 def addremove(self, m, prefix, opts, dry_run, similarity):
514 # In the same way as sub directories are processed, once in a subrepo,
514 # In the same way as sub directories are processed, once in a subrepo,
515 # always entry any of its subrepos. Don't corrupt the options that will
515 # always entry any of its subrepos. Don't corrupt the options that will
516 # be used to process sibling subrepos however.
516 # be used to process sibling subrepos however.
517 opts = copy.copy(opts)
517 opts = copy.copy(opts)
518 opts['subrepos'] = True
518 opts['subrepos'] = True
519 return scmutil.addremove(self._repo, m,
519 return scmutil.addremove(self._repo, m,
520 self.wvfs.reljoin(prefix, self._path), opts,
520 self.wvfs.reljoin(prefix, self._path), opts,
521 dry_run, similarity)
521 dry_run, similarity)
522
522
523 @annotatesubrepoerror
523 @annotatesubrepoerror
524 def cat(self, match, fm, fntemplate, prefix, **opts):
524 def cat(self, match, fm, fntemplate, prefix, **opts):
525 rev = self._state[1]
525 rev = self._state[1]
526 ctx = self._repo[rev]
526 ctx = self._repo[rev]
527 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
527 return cmdutil.cat(self.ui, self._repo, ctx, match, fm, fntemplate,
528 prefix, **opts)
528 prefix, **opts)
529
529
530 @annotatesubrepoerror
530 @annotatesubrepoerror
531 def status(self, rev2, **opts):
531 def status(self, rev2, **opts):
532 try:
532 try:
533 rev1 = self._state[1]
533 rev1 = self._state[1]
534 ctx1 = self._repo[rev1]
534 ctx1 = self._repo[rev1]
535 ctx2 = self._repo[rev2]
535 ctx2 = self._repo[rev2]
536 return self._repo.status(ctx1, ctx2, **opts)
536 return self._repo.status(ctx1, ctx2, **opts)
537 except error.RepoLookupError as inst:
537 except error.RepoLookupError as inst:
538 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
538 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
539 % (inst, subrelpath(self)))
539 % (inst, subrelpath(self)))
540 return scmutil.status([], [], [], [], [], [], [])
540 return scmutil.status([], [], [], [], [], [], [])
541
541
542 @annotatesubrepoerror
542 @annotatesubrepoerror
543 def diff(self, ui, diffopts, node2, match, prefix, **opts):
543 def diff(self, ui, diffopts, node2, match, prefix, **opts):
544 try:
544 try:
545 node1 = node.bin(self._state[1])
545 node1 = node.bin(self._state[1])
546 # We currently expect node2 to come from substate and be
546 # We currently expect node2 to come from substate and be
547 # in hex format
547 # in hex format
548 if node2 is not None:
548 if node2 is not None:
549 node2 = node.bin(node2)
549 node2 = node.bin(node2)
550 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
550 logcmdutil.diffordiffstat(ui, self._repo, diffopts,
551 node1, node2, match,
551 node1, node2, match,
552 prefix=posixpath.join(prefix, self._path),
552 prefix=posixpath.join(prefix, self._path),
553 listsubrepos=True, **opts)
553 listsubrepos=True, **opts)
554 except error.RepoLookupError as inst:
554 except error.RepoLookupError as inst:
555 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
555 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
556 % (inst, subrelpath(self)))
556 % (inst, subrelpath(self)))
557
557
558 @annotatesubrepoerror
558 @annotatesubrepoerror
559 def archive(self, archiver, prefix, match=None, decode=True):
559 def archive(self, archiver, prefix, match=None, decode=True):
560 self._get(self._state + ('hg',))
560 self._get(self._state + ('hg',))
561 files = self.files()
561 files = self.files()
562 if match:
562 if match:
563 files = [f for f in files if match(f)]
563 files = [f for f in files if match(f)]
564 rev = self._state[1]
564 rev = self._state[1]
565 ctx = self._repo[rev]
565 ctx = self._repo[rev]
566 scmutil.fileprefetchhooks(self._repo, ctx, files)
566 scmutil.fileprefetchhooks(self._repo, ctx, files)
567 total = abstractsubrepo.archive(self, archiver, prefix, match)
567 total = abstractsubrepo.archive(self, archiver, prefix, match)
568 for subpath in ctx.substate:
568 for subpath in ctx.substate:
569 s = subrepo(ctx, subpath, True)
569 s = subrepo(ctx, subpath, True)
570 submatch = matchmod.subdirmatcher(subpath, match)
570 submatch = matchmod.subdirmatcher(subpath, match)
571 total += s.archive(archiver, prefix + self._path + '/', submatch,
571 total += s.archive(archiver, prefix + self._path + '/', submatch,
572 decode)
572 decode)
573 return total
573 return total
574
574
575 @annotatesubrepoerror
575 @annotatesubrepoerror
576 def dirty(self, ignoreupdate=False, missing=False):
576 def dirty(self, ignoreupdate=False, missing=False):
577 r = self._state[1]
577 r = self._state[1]
578 if r == '' and not ignoreupdate: # no state recorded
578 if r == '' and not ignoreupdate: # no state recorded
579 return True
579 return True
580 w = self._repo[None]
580 w = self._repo[None]
581 if r != w.p1().hex() and not ignoreupdate:
581 if r != w.p1().hex() and not ignoreupdate:
582 # different version checked out
582 # different version checked out
583 return True
583 return True
584 return w.dirty(missing=missing) # working directory changed
584 return w.dirty(missing=missing) # working directory changed
585
585
586 def basestate(self):
586 def basestate(self):
587 return self._repo['.'].hex()
587 return self._repo['.'].hex()
588
588
589 def checknested(self, path):
589 def checknested(self, path):
590 return self._repo._checknested(self._repo.wjoin(path))
590 return self._repo._checknested(self._repo.wjoin(path))
591
591
592 @annotatesubrepoerror
592 @annotatesubrepoerror
593 def commit(self, text, user, date):
593 def commit(self, text, user, date):
594 # don't bother committing in the subrepo if it's only been
594 # don't bother committing in the subrepo if it's only been
595 # updated
595 # updated
596 if not self.dirty(True):
596 if not self.dirty(True):
597 return self._repo['.'].hex()
597 return self._repo['.'].hex()
598 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
598 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
599 n = self._repo.commit(text, user, date)
599 n = self._repo.commit(text, user, date)
600 if not n:
600 if not n:
601 return self._repo['.'].hex() # different version checked out
601 return self._repo['.'].hex() # different version checked out
602 return node.hex(n)
602 return node.hex(n)
603
603
604 @annotatesubrepoerror
604 @annotatesubrepoerror
605 def phase(self, state):
605 def phase(self, state):
606 return self._repo[state].phase()
606 return self._repo[state or '.'].phase()
607
607
608 @annotatesubrepoerror
608 @annotatesubrepoerror
609 def remove(self):
609 def remove(self):
610 # we can't fully delete the repository as it may contain
610 # we can't fully delete the repository as it may contain
611 # local-only history
611 # local-only history
612 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
612 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
613 hg.clean(self._repo, node.nullid, False)
613 hg.clean(self._repo, node.nullid, False)
614
614
615 def _get(self, state):
615 def _get(self, state):
616 source, revision, kind = state
616 source, revision, kind = state
617 parentrepo = self._repo._subparent
617 parentrepo = self._repo._subparent
618
618
619 if revision in self._repo.unfiltered():
619 if revision in self._repo.unfiltered():
620 # Allow shared subrepos tracked at null to setup the sharedpath
620 # Allow shared subrepos tracked at null to setup the sharedpath
621 if len(self._repo) != 0 or not parentrepo.shared():
621 if len(self._repo) != 0 or not parentrepo.shared():
622 return True
622 return True
623 self._repo._subsource = source
623 self._repo._subsource = source
624 srcurl = _abssource(self._repo)
624 srcurl = _abssource(self._repo)
625 other = hg.peer(self._repo, {}, srcurl)
625 other = hg.peer(self._repo, {}, srcurl)
626 if len(self._repo) == 0:
626 if len(self._repo) == 0:
627 # use self._repo.vfs instead of self.wvfs to remove .hg only
627 # use self._repo.vfs instead of self.wvfs to remove .hg only
628 self._repo.vfs.rmtree()
628 self._repo.vfs.rmtree()
629
629
630 # A remote subrepo could be shared if there is a local copy
630 # A remote subrepo could be shared if there is a local copy
631 # relative to the parent's share source. But clone pooling doesn't
631 # relative to the parent's share source. But clone pooling doesn't
632 # assemble the repos in a tree, so that can't be consistently done.
632 # assemble the repos in a tree, so that can't be consistently done.
633 # A simpler option is for the user to configure clone pooling, and
633 # A simpler option is for the user to configure clone pooling, and
634 # work with that.
634 # work with that.
635 if parentrepo.shared() and hg.islocal(srcurl):
635 if parentrepo.shared() and hg.islocal(srcurl):
636 self.ui.status(_('sharing subrepo %s from %s\n')
636 self.ui.status(_('sharing subrepo %s from %s\n')
637 % (subrelpath(self), srcurl))
637 % (subrelpath(self), srcurl))
638 shared = hg.share(self._repo._subparent.baseui,
638 shared = hg.share(self._repo._subparent.baseui,
639 other, self._repo.root,
639 other, self._repo.root,
640 update=False, bookmarks=False)
640 update=False, bookmarks=False)
641 self._repo = shared.local()
641 self._repo = shared.local()
642 else:
642 else:
643 # TODO: find a common place for this and this code in the
643 # TODO: find a common place for this and this code in the
644 # share.py wrap of the clone command.
644 # share.py wrap of the clone command.
645 if parentrepo.shared():
645 if parentrepo.shared():
646 pool = self.ui.config('share', 'pool')
646 pool = self.ui.config('share', 'pool')
647 if pool:
647 if pool:
648 pool = util.expandpath(pool)
648 pool = util.expandpath(pool)
649
649
650 shareopts = {
650 shareopts = {
651 'pool': pool,
651 'pool': pool,
652 'mode': self.ui.config('share', 'poolnaming'),
652 'mode': self.ui.config('share', 'poolnaming'),
653 }
653 }
654 else:
654 else:
655 shareopts = {}
655 shareopts = {}
656
656
657 self.ui.status(_('cloning subrepo %s from %s\n')
657 self.ui.status(_('cloning subrepo %s from %s\n')
658 % (subrelpath(self), srcurl))
658 % (subrelpath(self), srcurl))
659 other, cloned = hg.clone(self._repo._subparent.baseui, {},
659 other, cloned = hg.clone(self._repo._subparent.baseui, {},
660 other, self._repo.root,
660 other, self._repo.root,
661 update=False, shareopts=shareopts)
661 update=False, shareopts=shareopts)
662 self._repo = cloned.local()
662 self._repo = cloned.local()
663 self._initrepo(parentrepo, source, create=True)
663 self._initrepo(parentrepo, source, create=True)
664 self._cachestorehash(srcurl)
664 self._cachestorehash(srcurl)
665 else:
665 else:
666 self.ui.status(_('pulling subrepo %s from %s\n')
666 self.ui.status(_('pulling subrepo %s from %s\n')
667 % (subrelpath(self), srcurl))
667 % (subrelpath(self), srcurl))
668 cleansub = self.storeclean(srcurl)
668 cleansub = self.storeclean(srcurl)
669 exchange.pull(self._repo, other)
669 exchange.pull(self._repo, other)
670 if cleansub:
670 if cleansub:
671 # keep the repo clean after pull
671 # keep the repo clean after pull
672 self._cachestorehash(srcurl)
672 self._cachestorehash(srcurl)
673 return False
673 return False
674
674
675 @annotatesubrepoerror
675 @annotatesubrepoerror
676 def get(self, state, overwrite=False):
676 def get(self, state, overwrite=False):
677 inrepo = self._get(state)
677 inrepo = self._get(state)
678 source, revision, kind = state
678 source, revision, kind = state
679 repo = self._repo
679 repo = self._repo
680 repo.ui.debug("getting subrepo %s\n" % self._path)
680 repo.ui.debug("getting subrepo %s\n" % self._path)
681 if inrepo:
681 if inrepo:
682 urepo = repo.unfiltered()
682 urepo = repo.unfiltered()
683 ctx = urepo[revision]
683 ctx = urepo[revision]
684 if ctx.hidden():
684 if ctx.hidden():
685 urepo.ui.warn(
685 urepo.ui.warn(
686 _('revision %s in subrepository "%s" is hidden\n') \
686 _('revision %s in subrepository "%s" is hidden\n') \
687 % (revision[0:12], self._path))
687 % (revision[0:12], self._path))
688 repo = urepo
688 repo = urepo
689 hg.updaterepo(repo, revision, overwrite)
689 hg.updaterepo(repo, revision, overwrite)
690
690
691 @annotatesubrepoerror
691 @annotatesubrepoerror
692 def merge(self, state):
692 def merge(self, state):
693 self._get(state)
693 self._get(state)
694 cur = self._repo['.']
694 cur = self._repo['.']
695 dst = self._repo[state[1]]
695 dst = self._repo[state[1]]
696 anc = dst.ancestor(cur)
696 anc = dst.ancestor(cur)
697
697
698 def mergefunc():
698 def mergefunc():
699 if anc == cur and dst.branch() == cur.branch():
699 if anc == cur and dst.branch() == cur.branch():
700 self.ui.debug('updating subrepository "%s"\n'
700 self.ui.debug('updating subrepository "%s"\n'
701 % subrelpath(self))
701 % subrelpath(self))
702 hg.update(self._repo, state[1])
702 hg.update(self._repo, state[1])
703 elif anc == dst:
703 elif anc == dst:
704 self.ui.debug('skipping subrepository "%s"\n'
704 self.ui.debug('skipping subrepository "%s"\n'
705 % subrelpath(self))
705 % subrelpath(self))
706 else:
706 else:
707 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
707 self.ui.debug('merging subrepository "%s"\n' % subrelpath(self))
708 hg.merge(self._repo, state[1], remind=False)
708 hg.merge(self._repo, state[1], remind=False)
709
709
710 wctx = self._repo[None]
710 wctx = self._repo[None]
711 if self.dirty():
711 if self.dirty():
712 if anc != dst:
712 if anc != dst:
713 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
713 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
714 mergefunc()
714 mergefunc()
715 else:
715 else:
716 mergefunc()
716 mergefunc()
717 else:
717 else:
718 mergefunc()
718 mergefunc()
719
719
720 @annotatesubrepoerror
720 @annotatesubrepoerror
721 def push(self, opts):
721 def push(self, opts):
722 force = opts.get('force')
722 force = opts.get('force')
723 newbranch = opts.get('new_branch')
723 newbranch = opts.get('new_branch')
724 ssh = opts.get('ssh')
724 ssh = opts.get('ssh')
725
725
726 # push subrepos depth-first for coherent ordering
726 # push subrepos depth-first for coherent ordering
727 c = self._repo['.']
727 c = self._repo['.']
728 subs = c.substate # only repos that are committed
728 subs = c.substate # only repos that are committed
729 for s in sorted(subs):
729 for s in sorted(subs):
730 if c.sub(s).push(opts) == 0:
730 if c.sub(s).push(opts) == 0:
731 return False
731 return False
732
732
733 dsturl = _abssource(self._repo, True)
733 dsturl = _abssource(self._repo, True)
734 if not force:
734 if not force:
735 if self.storeclean(dsturl):
735 if self.storeclean(dsturl):
736 self.ui.status(
736 self.ui.status(
737 _('no changes made to subrepo %s since last push to %s\n')
737 _('no changes made to subrepo %s since last push to %s\n')
738 % (subrelpath(self), dsturl))
738 % (subrelpath(self), dsturl))
739 return None
739 return None
740 self.ui.status(_('pushing subrepo %s to %s\n') %
740 self.ui.status(_('pushing subrepo %s to %s\n') %
741 (subrelpath(self), dsturl))
741 (subrelpath(self), dsturl))
742 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
742 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
743 res = exchange.push(self._repo, other, force, newbranch=newbranch)
743 res = exchange.push(self._repo, other, force, newbranch=newbranch)
744
744
745 # the repo is now clean
745 # the repo is now clean
746 self._cachestorehash(dsturl)
746 self._cachestorehash(dsturl)
747 return res.cgresult
747 return res.cgresult
748
748
749 @annotatesubrepoerror
749 @annotatesubrepoerror
750 def outgoing(self, ui, dest, opts):
750 def outgoing(self, ui, dest, opts):
751 if 'rev' in opts or 'branch' in opts:
751 if 'rev' in opts or 'branch' in opts:
752 opts = copy.copy(opts)
752 opts = copy.copy(opts)
753 opts.pop('rev', None)
753 opts.pop('rev', None)
754 opts.pop('branch', None)
754 opts.pop('branch', None)
755 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
755 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
756
756
757 @annotatesubrepoerror
757 @annotatesubrepoerror
758 def incoming(self, ui, source, opts):
758 def incoming(self, ui, source, opts):
759 if 'rev' in opts or 'branch' in opts:
759 if 'rev' in opts or 'branch' in opts:
760 opts = copy.copy(opts)
760 opts = copy.copy(opts)
761 opts.pop('rev', None)
761 opts.pop('rev', None)
762 opts.pop('branch', None)
762 opts.pop('branch', None)
763 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
763 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
764
764
765 @annotatesubrepoerror
765 @annotatesubrepoerror
766 def files(self):
766 def files(self):
767 rev = self._state[1]
767 rev = self._state[1]
768 ctx = self._repo[rev]
768 ctx = self._repo[rev]
769 return ctx.manifest().keys()
769 return ctx.manifest().keys()
770
770
771 def filedata(self, name, decode):
771 def filedata(self, name, decode):
772 rev = self._state[1]
772 rev = self._state[1]
773 data = self._repo[rev][name].data()
773 data = self._repo[rev][name].data()
774 if decode:
774 if decode:
775 data = self._repo.wwritedata(name, data)
775 data = self._repo.wwritedata(name, data)
776 return data
776 return data
777
777
778 def fileflags(self, name):
778 def fileflags(self, name):
779 rev = self._state[1]
779 rev = self._state[1]
780 ctx = self._repo[rev]
780 ctx = self._repo[rev]
781 return ctx.flags(name)
781 return ctx.flags(name)
782
782
783 @annotatesubrepoerror
783 @annotatesubrepoerror
784 def printfiles(self, ui, m, fm, fmt, subrepos):
784 def printfiles(self, ui, m, fm, fmt, subrepos):
785 # If the parent context is a workingctx, use the workingctx here for
785 # If the parent context is a workingctx, use the workingctx here for
786 # consistency.
786 # consistency.
787 if self._ctx.rev() is None:
787 if self._ctx.rev() is None:
788 ctx = self._repo[None]
788 ctx = self._repo[None]
789 else:
789 else:
790 rev = self._state[1]
790 rev = self._state[1]
791 ctx = self._repo[rev]
791 ctx = self._repo[rev]
792 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
792 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
793
793
794 @annotatesubrepoerror
794 @annotatesubrepoerror
795 def getfileset(self, expr):
795 def getfileset(self, expr):
796 if self._ctx.rev() is None:
796 if self._ctx.rev() is None:
797 ctx = self._repo[None]
797 ctx = self._repo[None]
798 else:
798 else:
799 rev = self._state[1]
799 rev = self._state[1]
800 ctx = self._repo[rev]
800 ctx = self._repo[rev]
801
801
802 files = ctx.getfileset(expr)
802 files = ctx.getfileset(expr)
803
803
804 for subpath in ctx.substate:
804 for subpath in ctx.substate:
805 sub = ctx.sub(subpath)
805 sub = ctx.sub(subpath)
806
806
807 try:
807 try:
808 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
808 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
809 except error.LookupError:
809 except error.LookupError:
810 self.ui.status(_("skipping missing subrepository: %s\n")
810 self.ui.status(_("skipping missing subrepository: %s\n")
811 % self.wvfs.reljoin(reporelpath(self), subpath))
811 % self.wvfs.reljoin(reporelpath(self), subpath))
812 return files
812 return files
813
813
814 def walk(self, match):
814 def walk(self, match):
815 ctx = self._repo[None]
815 ctx = self._repo[None]
816 return ctx.walk(match)
816 return ctx.walk(match)
817
817
818 @annotatesubrepoerror
818 @annotatesubrepoerror
819 def forget(self, match, prefix, dryrun):
819 def forget(self, match, prefix, dryrun):
820 return cmdutil.forget(self.ui, self._repo, match,
820 return cmdutil.forget(self.ui, self._repo, match,
821 self.wvfs.reljoin(prefix, self._path),
821 self.wvfs.reljoin(prefix, self._path),
822 True, dryrun=dryrun)
822 True, dryrun=dryrun)
823
823
824 @annotatesubrepoerror
824 @annotatesubrepoerror
825 def removefiles(self, matcher, prefix, after, force, subrepos,
825 def removefiles(self, matcher, prefix, after, force, subrepos,
826 dryrun, warnings):
826 dryrun, warnings):
827 return cmdutil.remove(self.ui, self._repo, matcher,
827 return cmdutil.remove(self.ui, self._repo, matcher,
828 self.wvfs.reljoin(prefix, self._path),
828 self.wvfs.reljoin(prefix, self._path),
829 after, force, subrepos, dryrun)
829 after, force, subrepos, dryrun)
830
830
831 @annotatesubrepoerror
831 @annotatesubrepoerror
832 def revert(self, substate, *pats, **opts):
832 def revert(self, substate, *pats, **opts):
833 # reverting a subrepo is a 2 step process:
833 # reverting a subrepo is a 2 step process:
834 # 1. if the no_backup is not set, revert all modified
834 # 1. if the no_backup is not set, revert all modified
835 # files inside the subrepo
835 # files inside the subrepo
836 # 2. update the subrepo to the revision specified in
836 # 2. update the subrepo to the revision specified in
837 # the corresponding substate dictionary
837 # the corresponding substate dictionary
838 self.ui.status(_('reverting subrepo %s\n') % substate[0])
838 self.ui.status(_('reverting subrepo %s\n') % substate[0])
839 if not opts.get(r'no_backup'):
839 if not opts.get(r'no_backup'):
840 # Revert all files on the subrepo, creating backups
840 # Revert all files on the subrepo, creating backups
841 # Note that this will not recursively revert subrepos
841 # Note that this will not recursively revert subrepos
842 # We could do it if there was a set:subrepos() predicate
842 # We could do it if there was a set:subrepos() predicate
843 opts = opts.copy()
843 opts = opts.copy()
844 opts[r'date'] = None
844 opts[r'date'] = None
845 opts[r'rev'] = substate[1]
845 opts[r'rev'] = substate[1]
846
846
847 self.filerevert(*pats, **opts)
847 self.filerevert(*pats, **opts)
848
848
849 # Update the repo to the revision specified in the given substate
849 # Update the repo to the revision specified in the given substate
850 if not opts.get(r'dry_run'):
850 if not opts.get(r'dry_run'):
851 self.get(substate, overwrite=True)
851 self.get(substate, overwrite=True)
852
852
853 def filerevert(self, *pats, **opts):
853 def filerevert(self, *pats, **opts):
854 ctx = self._repo[opts[r'rev']]
854 ctx = self._repo[opts[r'rev']]
855 parents = self._repo.dirstate.parents()
855 parents = self._repo.dirstate.parents()
856 if opts.get(r'all'):
856 if opts.get(r'all'):
857 pats = ['set:modified()']
857 pats = ['set:modified()']
858 else:
858 else:
859 pats = []
859 pats = []
860 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
860 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
861
861
862 def shortid(self, revid):
862 def shortid(self, revid):
863 return revid[:12]
863 return revid[:12]
864
864
865 @annotatesubrepoerror
865 @annotatesubrepoerror
866 def unshare(self):
866 def unshare(self):
867 # subrepo inherently violates our import layering rules
867 # subrepo inherently violates our import layering rules
868 # because it wants to make repo objects from deep inside the stack
868 # because it wants to make repo objects from deep inside the stack
869 # so we manually delay the circular imports to not break
869 # so we manually delay the circular imports to not break
870 # scripts that don't use our demand-loading
870 # scripts that don't use our demand-loading
871 global hg
871 global hg
872 from . import hg as h
872 from . import hg as h
873 hg = h
873 hg = h
874
874
875 # Nothing prevents a user from sharing in a repo, and then making that a
875 # Nothing prevents a user from sharing in a repo, and then making that a
876 # subrepo. Alternately, the previous unshare attempt may have failed
876 # subrepo. Alternately, the previous unshare attempt may have failed
877 # part way through. So recurse whether or not this layer is shared.
877 # part way through. So recurse whether or not this layer is shared.
878 if self._repo.shared():
878 if self._repo.shared():
879 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
879 self.ui.status(_("unsharing subrepo '%s'\n") % self._relpath)
880
880
881 hg.unshare(self.ui, self._repo)
881 hg.unshare(self.ui, self._repo)
882
882
883 def verify(self):
883 def verify(self):
884 try:
884 try:
885 rev = self._state[1]
885 rev = self._state[1]
886 ctx = self._repo.unfiltered()[rev]
886 ctx = self._repo.unfiltered()[rev]
887 if ctx.hidden():
887 if ctx.hidden():
888 # Since hidden revisions aren't pushed/pulled, it seems worth an
888 # Since hidden revisions aren't pushed/pulled, it seems worth an
889 # explicit warning.
889 # explicit warning.
890 ui = self._repo.ui
890 ui = self._repo.ui
891 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
891 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
892 (self._relpath, node.short(self._ctx.node())))
892 (self._relpath, node.short(self._ctx.node())))
893 return 0
893 return 0
894 except error.RepoLookupError:
894 except error.RepoLookupError:
895 # A missing subrepo revision may be a case of needing to pull it, so
895 # A missing subrepo revision may be a case of needing to pull it, so
896 # don't treat this as an error.
896 # don't treat this as an error.
897 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
897 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
898 (self._relpath, node.short(self._ctx.node())))
898 (self._relpath, node.short(self._ctx.node())))
899 return 0
899 return 0
900
900
901 @propertycache
901 @propertycache
902 def wvfs(self):
902 def wvfs(self):
903 """return own wvfs for efficiency and consistency
903 """return own wvfs for efficiency and consistency
904 """
904 """
905 return self._repo.wvfs
905 return self._repo.wvfs
906
906
907 @propertycache
907 @propertycache
908 def _relpath(self):
908 def _relpath(self):
909 """return path to this subrepository as seen from outermost repository
909 """return path to this subrepository as seen from outermost repository
910 """
910 """
911 # Keep consistent dir separators by avoiding vfs.join(self._path)
911 # Keep consistent dir separators by avoiding vfs.join(self._path)
912 return reporelpath(self._repo)
912 return reporelpath(self._repo)
913
913
914 class svnsubrepo(abstractsubrepo):
914 class svnsubrepo(abstractsubrepo):
915 def __init__(self, ctx, path, state, allowcreate):
915 def __init__(self, ctx, path, state, allowcreate):
916 super(svnsubrepo, self).__init__(ctx, path)
916 super(svnsubrepo, self).__init__(ctx, path)
917 self._state = state
917 self._state = state
918 self._exe = procutil.findexe('svn')
918 self._exe = procutil.findexe('svn')
919 if not self._exe:
919 if not self._exe:
920 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
920 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
921 % self._path)
921 % self._path)
922
922
923 def _svncommand(self, commands, filename='', failok=False):
923 def _svncommand(self, commands, filename='', failok=False):
924 cmd = [self._exe]
924 cmd = [self._exe]
925 extrakw = {}
925 extrakw = {}
926 if not self.ui.interactive():
926 if not self.ui.interactive():
927 # Making stdin be a pipe should prevent svn from behaving
927 # Making stdin be a pipe should prevent svn from behaving
928 # interactively even if we can't pass --non-interactive.
928 # interactively even if we can't pass --non-interactive.
929 extrakw[r'stdin'] = subprocess.PIPE
929 extrakw[r'stdin'] = subprocess.PIPE
930 # Starting in svn 1.5 --non-interactive is a global flag
930 # Starting in svn 1.5 --non-interactive is a global flag
931 # instead of being per-command, but we need to support 1.4 so
931 # instead of being per-command, but we need to support 1.4 so
932 # we have to be intelligent about what commands take
932 # we have to be intelligent about what commands take
933 # --non-interactive.
933 # --non-interactive.
934 if commands[0] in ('update', 'checkout', 'commit'):
934 if commands[0] in ('update', 'checkout', 'commit'):
935 cmd.append('--non-interactive')
935 cmd.append('--non-interactive')
936 cmd.extend(commands)
936 cmd.extend(commands)
937 if filename is not None:
937 if filename is not None:
938 path = self.wvfs.reljoin(self._ctx.repo().origroot,
938 path = self.wvfs.reljoin(self._ctx.repo().origroot,
939 self._path, filename)
939 self._path, filename)
940 cmd.append(path)
940 cmd.append(path)
941 env = dict(encoding.environ)
941 env = dict(encoding.environ)
942 # Avoid localized output, preserve current locale for everything else.
942 # Avoid localized output, preserve current locale for everything else.
943 lc_all = env.get('LC_ALL')
943 lc_all = env.get('LC_ALL')
944 if lc_all:
944 if lc_all:
945 env['LANG'] = lc_all
945 env['LANG'] = lc_all
946 del env['LC_ALL']
946 del env['LC_ALL']
947 env['LC_MESSAGES'] = 'C'
947 env['LC_MESSAGES'] = 'C'
948 p = subprocess.Popen(cmd, bufsize=-1, close_fds=procutil.closefds,
948 p = subprocess.Popen(cmd, bufsize=-1, close_fds=procutil.closefds,
949 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
949 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
950 universal_newlines=True, env=env, **extrakw)
950 universal_newlines=True, env=env, **extrakw)
951 stdout, stderr = p.communicate()
951 stdout, stderr = p.communicate()
952 stderr = stderr.strip()
952 stderr = stderr.strip()
953 if not failok:
953 if not failok:
954 if p.returncode:
954 if p.returncode:
955 raise error.Abort(stderr or 'exited with code %d'
955 raise error.Abort(stderr or 'exited with code %d'
956 % p.returncode)
956 % p.returncode)
957 if stderr:
957 if stderr:
958 self.ui.warn(stderr + '\n')
958 self.ui.warn(stderr + '\n')
959 return stdout, stderr
959 return stdout, stderr
960
960
961 @propertycache
961 @propertycache
962 def _svnversion(self):
962 def _svnversion(self):
963 output, err = self._svncommand(['--version', '--quiet'], filename=None)
963 output, err = self._svncommand(['--version', '--quiet'], filename=None)
964 m = re.search(br'^(\d+)\.(\d+)', output)
964 m = re.search(br'^(\d+)\.(\d+)', output)
965 if not m:
965 if not m:
966 raise error.Abort(_('cannot retrieve svn tool version'))
966 raise error.Abort(_('cannot retrieve svn tool version'))
967 return (int(m.group(1)), int(m.group(2)))
967 return (int(m.group(1)), int(m.group(2)))
968
968
969 def _svnmissing(self):
969 def _svnmissing(self):
970 return not self.wvfs.exists('.svn')
970 return not self.wvfs.exists('.svn')
971
971
972 def _wcrevs(self):
972 def _wcrevs(self):
973 # Get the working directory revision as well as the last
973 # Get the working directory revision as well as the last
974 # commit revision so we can compare the subrepo state with
974 # commit revision so we can compare the subrepo state with
975 # both. We used to store the working directory one.
975 # both. We used to store the working directory one.
976 output, err = self._svncommand(['info', '--xml'])
976 output, err = self._svncommand(['info', '--xml'])
977 doc = xml.dom.minidom.parseString(output)
977 doc = xml.dom.minidom.parseString(output)
978 entries = doc.getElementsByTagName('entry')
978 entries = doc.getElementsByTagName('entry')
979 lastrev, rev = '0', '0'
979 lastrev, rev = '0', '0'
980 if entries:
980 if entries:
981 rev = str(entries[0].getAttribute('revision')) or '0'
981 rev = str(entries[0].getAttribute('revision')) or '0'
982 commits = entries[0].getElementsByTagName('commit')
982 commits = entries[0].getElementsByTagName('commit')
983 if commits:
983 if commits:
984 lastrev = str(commits[0].getAttribute('revision')) or '0'
984 lastrev = str(commits[0].getAttribute('revision')) or '0'
985 return (lastrev, rev)
985 return (lastrev, rev)
986
986
987 def _wcrev(self):
987 def _wcrev(self):
988 return self._wcrevs()[0]
988 return self._wcrevs()[0]
989
989
990 def _wcchanged(self):
990 def _wcchanged(self):
991 """Return (changes, extchanges, missing) where changes is True
991 """Return (changes, extchanges, missing) where changes is True
992 if the working directory was changed, extchanges is
992 if the working directory was changed, extchanges is
993 True if any of these changes concern an external entry and missing
993 True if any of these changes concern an external entry and missing
994 is True if any change is a missing entry.
994 is True if any change is a missing entry.
995 """
995 """
996 output, err = self._svncommand(['status', '--xml'])
996 output, err = self._svncommand(['status', '--xml'])
997 externals, changes, missing = [], [], []
997 externals, changes, missing = [], [], []
998 doc = xml.dom.minidom.parseString(output)
998 doc = xml.dom.minidom.parseString(output)
999 for e in doc.getElementsByTagName('entry'):
999 for e in doc.getElementsByTagName('entry'):
1000 s = e.getElementsByTagName('wc-status')
1000 s = e.getElementsByTagName('wc-status')
1001 if not s:
1001 if not s:
1002 continue
1002 continue
1003 item = s[0].getAttribute('item')
1003 item = s[0].getAttribute('item')
1004 props = s[0].getAttribute('props')
1004 props = s[0].getAttribute('props')
1005 path = e.getAttribute('path')
1005 path = e.getAttribute('path')
1006 if item == 'external':
1006 if item == 'external':
1007 externals.append(path)
1007 externals.append(path)
1008 elif item == 'missing':
1008 elif item == 'missing':
1009 missing.append(path)
1009 missing.append(path)
1010 if (item not in ('', 'normal', 'unversioned', 'external')
1010 if (item not in ('', 'normal', 'unversioned', 'external')
1011 or props not in ('', 'none', 'normal')):
1011 or props not in ('', 'none', 'normal')):
1012 changes.append(path)
1012 changes.append(path)
1013 for path in changes:
1013 for path in changes:
1014 for ext in externals:
1014 for ext in externals:
1015 if path == ext or path.startswith(ext + pycompat.ossep):
1015 if path == ext or path.startswith(ext + pycompat.ossep):
1016 return True, True, bool(missing)
1016 return True, True, bool(missing)
1017 return bool(changes), False, bool(missing)
1017 return bool(changes), False, bool(missing)
1018
1018
1019 @annotatesubrepoerror
1019 @annotatesubrepoerror
1020 def dirty(self, ignoreupdate=False, missing=False):
1020 def dirty(self, ignoreupdate=False, missing=False):
1021 if self._svnmissing():
1021 if self._svnmissing():
1022 return self._state[1] != ''
1022 return self._state[1] != ''
1023 wcchanged = self._wcchanged()
1023 wcchanged = self._wcchanged()
1024 changed = wcchanged[0] or (missing and wcchanged[2])
1024 changed = wcchanged[0] or (missing and wcchanged[2])
1025 if not changed:
1025 if not changed:
1026 if self._state[1] in self._wcrevs() or ignoreupdate:
1026 if self._state[1] in self._wcrevs() or ignoreupdate:
1027 return False
1027 return False
1028 return True
1028 return True
1029
1029
1030 def basestate(self):
1030 def basestate(self):
1031 lastrev, rev = self._wcrevs()
1031 lastrev, rev = self._wcrevs()
1032 if lastrev != rev:
1032 if lastrev != rev:
1033 # Last committed rev is not the same than rev. We would
1033 # Last committed rev is not the same than rev. We would
1034 # like to take lastrev but we do not know if the subrepo
1034 # like to take lastrev but we do not know if the subrepo
1035 # URL exists at lastrev. Test it and fallback to rev it
1035 # URL exists at lastrev. Test it and fallback to rev it
1036 # is not there.
1036 # is not there.
1037 try:
1037 try:
1038 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1038 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1039 return lastrev
1039 return lastrev
1040 except error.Abort:
1040 except error.Abort:
1041 pass
1041 pass
1042 return rev
1042 return rev
1043
1043
1044 @annotatesubrepoerror
1044 @annotatesubrepoerror
1045 def commit(self, text, user, date):
1045 def commit(self, text, user, date):
1046 # user and date are out of our hands since svn is centralized
1046 # user and date are out of our hands since svn is centralized
1047 changed, extchanged, missing = self._wcchanged()
1047 changed, extchanged, missing = self._wcchanged()
1048 if not changed:
1048 if not changed:
1049 return self.basestate()
1049 return self.basestate()
1050 if extchanged:
1050 if extchanged:
1051 # Do not try to commit externals
1051 # Do not try to commit externals
1052 raise error.Abort(_('cannot commit svn externals'))
1052 raise error.Abort(_('cannot commit svn externals'))
1053 if missing:
1053 if missing:
1054 # svn can commit with missing entries but aborting like hg
1054 # svn can commit with missing entries but aborting like hg
1055 # seems a better approach.
1055 # seems a better approach.
1056 raise error.Abort(_('cannot commit missing svn entries'))
1056 raise error.Abort(_('cannot commit missing svn entries'))
1057 commitinfo, err = self._svncommand(['commit', '-m', text])
1057 commitinfo, err = self._svncommand(['commit', '-m', text])
1058 self.ui.status(commitinfo)
1058 self.ui.status(commitinfo)
1059 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1059 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1060 if not newrev:
1060 if not newrev:
1061 if not commitinfo.strip():
1061 if not commitinfo.strip():
1062 # Sometimes, our definition of "changed" differs from
1062 # Sometimes, our definition of "changed" differs from
1063 # svn one. For instance, svn ignores missing files
1063 # svn one. For instance, svn ignores missing files
1064 # when committing. If there are only missing files, no
1064 # when committing. If there are only missing files, no
1065 # commit is made, no output and no error code.
1065 # commit is made, no output and no error code.
1066 raise error.Abort(_('failed to commit svn changes'))
1066 raise error.Abort(_('failed to commit svn changes'))
1067 raise error.Abort(commitinfo.splitlines()[-1])
1067 raise error.Abort(commitinfo.splitlines()[-1])
1068 newrev = newrev.groups()[0]
1068 newrev = newrev.groups()[0]
1069 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1069 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1070 return newrev
1070 return newrev
1071
1071
1072 @annotatesubrepoerror
1072 @annotatesubrepoerror
1073 def remove(self):
1073 def remove(self):
1074 if self.dirty():
1074 if self.dirty():
1075 self.ui.warn(_('not removing repo %s because '
1075 self.ui.warn(_('not removing repo %s because '
1076 'it has changes.\n') % self._path)
1076 'it has changes.\n') % self._path)
1077 return
1077 return
1078 self.ui.note(_('removing subrepo %s\n') % self._path)
1078 self.ui.note(_('removing subrepo %s\n') % self._path)
1079
1079
1080 self.wvfs.rmtree(forcibly=True)
1080 self.wvfs.rmtree(forcibly=True)
1081 try:
1081 try:
1082 pwvfs = self._ctx.repo().wvfs
1082 pwvfs = self._ctx.repo().wvfs
1083 pwvfs.removedirs(pwvfs.dirname(self._path))
1083 pwvfs.removedirs(pwvfs.dirname(self._path))
1084 except OSError:
1084 except OSError:
1085 pass
1085 pass
1086
1086
1087 @annotatesubrepoerror
1087 @annotatesubrepoerror
1088 def get(self, state, overwrite=False):
1088 def get(self, state, overwrite=False):
1089 if overwrite:
1089 if overwrite:
1090 self._svncommand(['revert', '--recursive'])
1090 self._svncommand(['revert', '--recursive'])
1091 args = ['checkout']
1091 args = ['checkout']
1092 if self._svnversion >= (1, 5):
1092 if self._svnversion >= (1, 5):
1093 args.append('--force')
1093 args.append('--force')
1094 # The revision must be specified at the end of the URL to properly
1094 # The revision must be specified at the end of the URL to properly
1095 # update to a directory which has since been deleted and recreated.
1095 # update to a directory which has since been deleted and recreated.
1096 args.append('%s@%s' % (state[0], state[1]))
1096 args.append('%s@%s' % (state[0], state[1]))
1097
1097
1098 # SEC: check that the ssh url is safe
1098 # SEC: check that the ssh url is safe
1099 util.checksafessh(state[0])
1099 util.checksafessh(state[0])
1100
1100
1101 status, err = self._svncommand(args, failok=True)
1101 status, err = self._svncommand(args, failok=True)
1102 _sanitize(self.ui, self.wvfs, '.svn')
1102 _sanitize(self.ui, self.wvfs, '.svn')
1103 if not re.search('Checked out revision [0-9]+.', status):
1103 if not re.search('Checked out revision [0-9]+.', status):
1104 if ('is already a working copy for a different URL' in err
1104 if ('is already a working copy for a different URL' in err
1105 and (self._wcchanged()[:2] == (False, False))):
1105 and (self._wcchanged()[:2] == (False, False))):
1106 # obstructed but clean working copy, so just blow it away.
1106 # obstructed but clean working copy, so just blow it away.
1107 self.remove()
1107 self.remove()
1108 self.get(state, overwrite=False)
1108 self.get(state, overwrite=False)
1109 return
1109 return
1110 raise error.Abort((status or err).splitlines()[-1])
1110 raise error.Abort((status or err).splitlines()[-1])
1111 self.ui.status(status)
1111 self.ui.status(status)
1112
1112
1113 @annotatesubrepoerror
1113 @annotatesubrepoerror
1114 def merge(self, state):
1114 def merge(self, state):
1115 old = self._state[1]
1115 old = self._state[1]
1116 new = state[1]
1116 new = state[1]
1117 wcrev = self._wcrev()
1117 wcrev = self._wcrev()
1118 if new != wcrev:
1118 if new != wcrev:
1119 dirty = old == wcrev or self._wcchanged()[0]
1119 dirty = old == wcrev or self._wcchanged()[0]
1120 if _updateprompt(self.ui, self, dirty, wcrev, new):
1120 if _updateprompt(self.ui, self, dirty, wcrev, new):
1121 self.get(state, False)
1121 self.get(state, False)
1122
1122
1123 def push(self, opts):
1123 def push(self, opts):
1124 # push is a no-op for SVN
1124 # push is a no-op for SVN
1125 return True
1125 return True
1126
1126
1127 @annotatesubrepoerror
1127 @annotatesubrepoerror
1128 def files(self):
1128 def files(self):
1129 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1129 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1130 doc = xml.dom.minidom.parseString(output)
1130 doc = xml.dom.minidom.parseString(output)
1131 paths = []
1131 paths = []
1132 for e in doc.getElementsByTagName('entry'):
1132 for e in doc.getElementsByTagName('entry'):
1133 kind = pycompat.bytestr(e.getAttribute('kind'))
1133 kind = pycompat.bytestr(e.getAttribute('kind'))
1134 if kind != 'file':
1134 if kind != 'file':
1135 continue
1135 continue
1136 name = ''.join(c.data for c
1136 name = ''.join(c.data for c
1137 in e.getElementsByTagName('name')[0].childNodes
1137 in e.getElementsByTagName('name')[0].childNodes
1138 if c.nodeType == c.TEXT_NODE)
1138 if c.nodeType == c.TEXT_NODE)
1139 paths.append(name.encode('utf-8'))
1139 paths.append(name.encode('utf-8'))
1140 return paths
1140 return paths
1141
1141
1142 def filedata(self, name, decode):
1142 def filedata(self, name, decode):
1143 return self._svncommand(['cat'], name)[0]
1143 return self._svncommand(['cat'], name)[0]
1144
1144
1145
1145
1146 class gitsubrepo(abstractsubrepo):
1146 class gitsubrepo(abstractsubrepo):
1147 def __init__(self, ctx, path, state, allowcreate):
1147 def __init__(self, ctx, path, state, allowcreate):
1148 super(gitsubrepo, self).__init__(ctx, path)
1148 super(gitsubrepo, self).__init__(ctx, path)
1149 self._state = state
1149 self._state = state
1150 self._abspath = ctx.repo().wjoin(path)
1150 self._abspath = ctx.repo().wjoin(path)
1151 self._subparent = ctx.repo()
1151 self._subparent = ctx.repo()
1152 self._ensuregit()
1152 self._ensuregit()
1153
1153
1154 def _ensuregit(self):
1154 def _ensuregit(self):
1155 try:
1155 try:
1156 self._gitexecutable = 'git'
1156 self._gitexecutable = 'git'
1157 out, err = self._gitnodir(['--version'])
1157 out, err = self._gitnodir(['--version'])
1158 except OSError as e:
1158 except OSError as e:
1159 genericerror = _("error executing git for subrepo '%s': %s")
1159 genericerror = _("error executing git for subrepo '%s': %s")
1160 notfoundhint = _("check git is installed and in your PATH")
1160 notfoundhint = _("check git is installed and in your PATH")
1161 if e.errno != errno.ENOENT:
1161 if e.errno != errno.ENOENT:
1162 raise error.Abort(genericerror % (
1162 raise error.Abort(genericerror % (
1163 self._path, encoding.strtolocal(e.strerror)))
1163 self._path, encoding.strtolocal(e.strerror)))
1164 elif pycompat.iswindows:
1164 elif pycompat.iswindows:
1165 try:
1165 try:
1166 self._gitexecutable = 'git.cmd'
1166 self._gitexecutable = 'git.cmd'
1167 out, err = self._gitnodir(['--version'])
1167 out, err = self._gitnodir(['--version'])
1168 except OSError as e2:
1168 except OSError as e2:
1169 if e2.errno == errno.ENOENT:
1169 if e2.errno == errno.ENOENT:
1170 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1170 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1171 " for subrepo '%s'") % self._path,
1171 " for subrepo '%s'") % self._path,
1172 hint=notfoundhint)
1172 hint=notfoundhint)
1173 else:
1173 else:
1174 raise error.Abort(genericerror % (self._path,
1174 raise error.Abort(genericerror % (self._path,
1175 encoding.strtolocal(e2.strerror)))
1175 encoding.strtolocal(e2.strerror)))
1176 else:
1176 else:
1177 raise error.Abort(_("couldn't find git for subrepo '%s'")
1177 raise error.Abort(_("couldn't find git for subrepo '%s'")
1178 % self._path, hint=notfoundhint)
1178 % self._path, hint=notfoundhint)
1179 versionstatus = self._checkversion(out)
1179 versionstatus = self._checkversion(out)
1180 if versionstatus == 'unknown':
1180 if versionstatus == 'unknown':
1181 self.ui.warn(_('cannot retrieve git version\n'))
1181 self.ui.warn(_('cannot retrieve git version\n'))
1182 elif versionstatus == 'abort':
1182 elif versionstatus == 'abort':
1183 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1183 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1184 elif versionstatus == 'warning':
1184 elif versionstatus == 'warning':
1185 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1185 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1186
1186
1187 @staticmethod
1187 @staticmethod
1188 def _gitversion(out):
1188 def _gitversion(out):
1189 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1189 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1190 if m:
1190 if m:
1191 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1191 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1192
1192
1193 m = re.search(br'^git version (\d+)\.(\d+)', out)
1193 m = re.search(br'^git version (\d+)\.(\d+)', out)
1194 if m:
1194 if m:
1195 return (int(m.group(1)), int(m.group(2)), 0)
1195 return (int(m.group(1)), int(m.group(2)), 0)
1196
1196
1197 return -1
1197 return -1
1198
1198
1199 @staticmethod
1199 @staticmethod
1200 def _checkversion(out):
1200 def _checkversion(out):
1201 '''ensure git version is new enough
1201 '''ensure git version is new enough
1202
1202
1203 >>> _checkversion = gitsubrepo._checkversion
1203 >>> _checkversion = gitsubrepo._checkversion
1204 >>> _checkversion(b'git version 1.6.0')
1204 >>> _checkversion(b'git version 1.6.0')
1205 'ok'
1205 'ok'
1206 >>> _checkversion(b'git version 1.8.5')
1206 >>> _checkversion(b'git version 1.8.5')
1207 'ok'
1207 'ok'
1208 >>> _checkversion(b'git version 1.4.0')
1208 >>> _checkversion(b'git version 1.4.0')
1209 'abort'
1209 'abort'
1210 >>> _checkversion(b'git version 1.5.0')
1210 >>> _checkversion(b'git version 1.5.0')
1211 'warning'
1211 'warning'
1212 >>> _checkversion(b'git version 1.9-rc0')
1212 >>> _checkversion(b'git version 1.9-rc0')
1213 'ok'
1213 'ok'
1214 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1214 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1215 'ok'
1215 'ok'
1216 >>> _checkversion(b'git version 1.9.0.GIT')
1216 >>> _checkversion(b'git version 1.9.0.GIT')
1217 'ok'
1217 'ok'
1218 >>> _checkversion(b'git version 12345')
1218 >>> _checkversion(b'git version 12345')
1219 'unknown'
1219 'unknown'
1220 >>> _checkversion(b'no')
1220 >>> _checkversion(b'no')
1221 'unknown'
1221 'unknown'
1222 '''
1222 '''
1223 version = gitsubrepo._gitversion(out)
1223 version = gitsubrepo._gitversion(out)
1224 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1224 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1225 # despite the docstring comment. For now, error on 1.4.0, warn on
1225 # despite the docstring comment. For now, error on 1.4.0, warn on
1226 # 1.5.0 but attempt to continue.
1226 # 1.5.0 but attempt to continue.
1227 if version == -1:
1227 if version == -1:
1228 return 'unknown'
1228 return 'unknown'
1229 if version < (1, 5, 0):
1229 if version < (1, 5, 0):
1230 return 'abort'
1230 return 'abort'
1231 elif version < (1, 6, 0):
1231 elif version < (1, 6, 0):
1232 return 'warning'
1232 return 'warning'
1233 return 'ok'
1233 return 'ok'
1234
1234
1235 def _gitcommand(self, commands, env=None, stream=False):
1235 def _gitcommand(self, commands, env=None, stream=False):
1236 return self._gitdir(commands, env=env, stream=stream)[0]
1236 return self._gitdir(commands, env=env, stream=stream)[0]
1237
1237
1238 def _gitdir(self, commands, env=None, stream=False):
1238 def _gitdir(self, commands, env=None, stream=False):
1239 return self._gitnodir(commands, env=env, stream=stream,
1239 return self._gitnodir(commands, env=env, stream=stream,
1240 cwd=self._abspath)
1240 cwd=self._abspath)
1241
1241
1242 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1242 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1243 """Calls the git command
1243 """Calls the git command
1244
1244
1245 The methods tries to call the git command. versions prior to 1.6.0
1245 The methods tries to call the git command. versions prior to 1.6.0
1246 are not supported and very probably fail.
1246 are not supported and very probably fail.
1247 """
1247 """
1248 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1248 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1249 if env is None:
1249 if env is None:
1250 env = encoding.environ.copy()
1250 env = encoding.environ.copy()
1251 # disable localization for Git output (issue5176)
1251 # disable localization for Git output (issue5176)
1252 env['LC_ALL'] = 'C'
1252 env['LC_ALL'] = 'C'
1253 # fix for Git CVE-2015-7545
1253 # fix for Git CVE-2015-7545
1254 if 'GIT_ALLOW_PROTOCOL' not in env:
1254 if 'GIT_ALLOW_PROTOCOL' not in env:
1255 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1255 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1256 # unless ui.quiet is set, print git's stderr,
1256 # unless ui.quiet is set, print git's stderr,
1257 # which is mostly progress and useful info
1257 # which is mostly progress and useful info
1258 errpipe = None
1258 errpipe = None
1259 if self.ui.quiet:
1259 if self.ui.quiet:
1260 errpipe = open(os.devnull, 'w')
1260 errpipe = open(os.devnull, 'w')
1261 if self.ui._colormode and len(commands) and commands[0] == "diff":
1261 if self.ui._colormode and len(commands) and commands[0] == "diff":
1262 # insert the argument in the front,
1262 # insert the argument in the front,
1263 # the end of git diff arguments is used for paths
1263 # the end of git diff arguments is used for paths
1264 commands.insert(1, '--color')
1264 commands.insert(1, '--color')
1265 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1265 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1266 cwd=cwd, env=env, close_fds=procutil.closefds,
1266 cwd=cwd, env=env, close_fds=procutil.closefds,
1267 stdout=subprocess.PIPE, stderr=errpipe)
1267 stdout=subprocess.PIPE, stderr=errpipe)
1268 if stream:
1268 if stream:
1269 return p.stdout, None
1269 return p.stdout, None
1270
1270
1271 retdata = p.stdout.read().strip()
1271 retdata = p.stdout.read().strip()
1272 # wait for the child to exit to avoid race condition.
1272 # wait for the child to exit to avoid race condition.
1273 p.wait()
1273 p.wait()
1274
1274
1275 if p.returncode != 0 and p.returncode != 1:
1275 if p.returncode != 0 and p.returncode != 1:
1276 # there are certain error codes that are ok
1276 # there are certain error codes that are ok
1277 command = commands[0]
1277 command = commands[0]
1278 if command in ('cat-file', 'symbolic-ref'):
1278 if command in ('cat-file', 'symbolic-ref'):
1279 return retdata, p.returncode
1279 return retdata, p.returncode
1280 # for all others, abort
1280 # for all others, abort
1281 raise error.Abort(_('git %s error %d in %s') %
1281 raise error.Abort(_('git %s error %d in %s') %
1282 (command, p.returncode, self._relpath))
1282 (command, p.returncode, self._relpath))
1283
1283
1284 return retdata, p.returncode
1284 return retdata, p.returncode
1285
1285
1286 def _gitmissing(self):
1286 def _gitmissing(self):
1287 return not self.wvfs.exists('.git')
1287 return not self.wvfs.exists('.git')
1288
1288
1289 def _gitstate(self):
1289 def _gitstate(self):
1290 return self._gitcommand(['rev-parse', 'HEAD'])
1290 return self._gitcommand(['rev-parse', 'HEAD'])
1291
1291
1292 def _gitcurrentbranch(self):
1292 def _gitcurrentbranch(self):
1293 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1293 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1294 if err:
1294 if err:
1295 current = None
1295 current = None
1296 return current
1296 return current
1297
1297
1298 def _gitremote(self, remote):
1298 def _gitremote(self, remote):
1299 out = self._gitcommand(['remote', 'show', '-n', remote])
1299 out = self._gitcommand(['remote', 'show', '-n', remote])
1300 line = out.split('\n')[1]
1300 line = out.split('\n')[1]
1301 i = line.index('URL: ') + len('URL: ')
1301 i = line.index('URL: ') + len('URL: ')
1302 return line[i:]
1302 return line[i:]
1303
1303
1304 def _githavelocally(self, revision):
1304 def _githavelocally(self, revision):
1305 out, code = self._gitdir(['cat-file', '-e', revision])
1305 out, code = self._gitdir(['cat-file', '-e', revision])
1306 return code == 0
1306 return code == 0
1307
1307
1308 def _gitisancestor(self, r1, r2):
1308 def _gitisancestor(self, r1, r2):
1309 base = self._gitcommand(['merge-base', r1, r2])
1309 base = self._gitcommand(['merge-base', r1, r2])
1310 return base == r1
1310 return base == r1
1311
1311
1312 def _gitisbare(self):
1312 def _gitisbare(self):
1313 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1313 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1314
1314
1315 def _gitupdatestat(self):
1315 def _gitupdatestat(self):
1316 """This must be run before git diff-index.
1316 """This must be run before git diff-index.
1317 diff-index only looks at changes to file stat;
1317 diff-index only looks at changes to file stat;
1318 this command looks at file contents and updates the stat."""
1318 this command looks at file contents and updates the stat."""
1319 self._gitcommand(['update-index', '-q', '--refresh'])
1319 self._gitcommand(['update-index', '-q', '--refresh'])
1320
1320
1321 def _gitbranchmap(self):
1321 def _gitbranchmap(self):
1322 '''returns 2 things:
1322 '''returns 2 things:
1323 a map from git branch to revision
1323 a map from git branch to revision
1324 a map from revision to branches'''
1324 a map from revision to branches'''
1325 branch2rev = {}
1325 branch2rev = {}
1326 rev2branch = {}
1326 rev2branch = {}
1327
1327
1328 out = self._gitcommand(['for-each-ref', '--format',
1328 out = self._gitcommand(['for-each-ref', '--format',
1329 '%(objectname) %(refname)'])
1329 '%(objectname) %(refname)'])
1330 for line in out.split('\n'):
1330 for line in out.split('\n'):
1331 revision, ref = line.split(' ')
1331 revision, ref = line.split(' ')
1332 if (not ref.startswith('refs/heads/') and
1332 if (not ref.startswith('refs/heads/') and
1333 not ref.startswith('refs/remotes/')):
1333 not ref.startswith('refs/remotes/')):
1334 continue
1334 continue
1335 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1335 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1336 continue # ignore remote/HEAD redirects
1336 continue # ignore remote/HEAD redirects
1337 branch2rev[ref] = revision
1337 branch2rev[ref] = revision
1338 rev2branch.setdefault(revision, []).append(ref)
1338 rev2branch.setdefault(revision, []).append(ref)
1339 return branch2rev, rev2branch
1339 return branch2rev, rev2branch
1340
1340
1341 def _gittracking(self, branches):
1341 def _gittracking(self, branches):
1342 'return map of remote branch to local tracking branch'
1342 'return map of remote branch to local tracking branch'
1343 # assumes no more than one local tracking branch for each remote
1343 # assumes no more than one local tracking branch for each remote
1344 tracking = {}
1344 tracking = {}
1345 for b in branches:
1345 for b in branches:
1346 if b.startswith('refs/remotes/'):
1346 if b.startswith('refs/remotes/'):
1347 continue
1347 continue
1348 bname = b.split('/', 2)[2]
1348 bname = b.split('/', 2)[2]
1349 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1349 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1350 if remote:
1350 if remote:
1351 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1351 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1352 tracking['refs/remotes/%s/%s' %
1352 tracking['refs/remotes/%s/%s' %
1353 (remote, ref.split('/', 2)[2])] = b
1353 (remote, ref.split('/', 2)[2])] = b
1354 return tracking
1354 return tracking
1355
1355
1356 def _abssource(self, source):
1356 def _abssource(self, source):
1357 if '://' not in source:
1357 if '://' not in source:
1358 # recognize the scp syntax as an absolute source
1358 # recognize the scp syntax as an absolute source
1359 colon = source.find(':')
1359 colon = source.find(':')
1360 if colon != -1 and '/' not in source[:colon]:
1360 if colon != -1 and '/' not in source[:colon]:
1361 return source
1361 return source
1362 self._subsource = source
1362 self._subsource = source
1363 return _abssource(self)
1363 return _abssource(self)
1364
1364
1365 def _fetch(self, source, revision):
1365 def _fetch(self, source, revision):
1366 if self._gitmissing():
1366 if self._gitmissing():
1367 # SEC: check for safe ssh url
1367 # SEC: check for safe ssh url
1368 util.checksafessh(source)
1368 util.checksafessh(source)
1369
1369
1370 source = self._abssource(source)
1370 source = self._abssource(source)
1371 self.ui.status(_('cloning subrepo %s from %s\n') %
1371 self.ui.status(_('cloning subrepo %s from %s\n') %
1372 (self._relpath, source))
1372 (self._relpath, source))
1373 self._gitnodir(['clone', source, self._abspath])
1373 self._gitnodir(['clone', source, self._abspath])
1374 if self._githavelocally(revision):
1374 if self._githavelocally(revision):
1375 return
1375 return
1376 self.ui.status(_('pulling subrepo %s from %s\n') %
1376 self.ui.status(_('pulling subrepo %s from %s\n') %
1377 (self._relpath, self._gitremote('origin')))
1377 (self._relpath, self._gitremote('origin')))
1378 # try only origin: the originally cloned repo
1378 # try only origin: the originally cloned repo
1379 self._gitcommand(['fetch'])
1379 self._gitcommand(['fetch'])
1380 if not self._githavelocally(revision):
1380 if not self._githavelocally(revision):
1381 raise error.Abort(_('revision %s does not exist in subrepository '
1381 raise error.Abort(_('revision %s does not exist in subrepository '
1382 '"%s"\n') % (revision, self._relpath))
1382 '"%s"\n') % (revision, self._relpath))
1383
1383
1384 @annotatesubrepoerror
1384 @annotatesubrepoerror
1385 def dirty(self, ignoreupdate=False, missing=False):
1385 def dirty(self, ignoreupdate=False, missing=False):
1386 if self._gitmissing():
1386 if self._gitmissing():
1387 return self._state[1] != ''
1387 return self._state[1] != ''
1388 if self._gitisbare():
1388 if self._gitisbare():
1389 return True
1389 return True
1390 if not ignoreupdate and self._state[1] != self._gitstate():
1390 if not ignoreupdate and self._state[1] != self._gitstate():
1391 # different version checked out
1391 # different version checked out
1392 return True
1392 return True
1393 # check for staged changes or modified files; ignore untracked files
1393 # check for staged changes or modified files; ignore untracked files
1394 self._gitupdatestat()
1394 self._gitupdatestat()
1395 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1395 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1396 return code == 1
1396 return code == 1
1397
1397
1398 def basestate(self):
1398 def basestate(self):
1399 return self._gitstate()
1399 return self._gitstate()
1400
1400
1401 @annotatesubrepoerror
1401 @annotatesubrepoerror
1402 def get(self, state, overwrite=False):
1402 def get(self, state, overwrite=False):
1403 source, revision, kind = state
1403 source, revision, kind = state
1404 if not revision:
1404 if not revision:
1405 self.remove()
1405 self.remove()
1406 return
1406 return
1407 self._fetch(source, revision)
1407 self._fetch(source, revision)
1408 # if the repo was set to be bare, unbare it
1408 # if the repo was set to be bare, unbare it
1409 if self._gitisbare():
1409 if self._gitisbare():
1410 self._gitcommand(['config', 'core.bare', 'false'])
1410 self._gitcommand(['config', 'core.bare', 'false'])
1411 if self._gitstate() == revision:
1411 if self._gitstate() == revision:
1412 self._gitcommand(['reset', '--hard', 'HEAD'])
1412 self._gitcommand(['reset', '--hard', 'HEAD'])
1413 return
1413 return
1414 elif self._gitstate() == revision:
1414 elif self._gitstate() == revision:
1415 if overwrite:
1415 if overwrite:
1416 # first reset the index to unmark new files for commit, because
1416 # first reset the index to unmark new files for commit, because
1417 # reset --hard will otherwise throw away files added for commit,
1417 # reset --hard will otherwise throw away files added for commit,
1418 # not just unmark them.
1418 # not just unmark them.
1419 self._gitcommand(['reset', 'HEAD'])
1419 self._gitcommand(['reset', 'HEAD'])
1420 self._gitcommand(['reset', '--hard', 'HEAD'])
1420 self._gitcommand(['reset', '--hard', 'HEAD'])
1421 return
1421 return
1422 branch2rev, rev2branch = self._gitbranchmap()
1422 branch2rev, rev2branch = self._gitbranchmap()
1423
1423
1424 def checkout(args):
1424 def checkout(args):
1425 cmd = ['checkout']
1425 cmd = ['checkout']
1426 if overwrite:
1426 if overwrite:
1427 # first reset the index to unmark new files for commit, because
1427 # first reset the index to unmark new files for commit, because
1428 # the -f option will otherwise throw away files added for
1428 # the -f option will otherwise throw away files added for
1429 # commit, not just unmark them.
1429 # commit, not just unmark them.
1430 self._gitcommand(['reset', 'HEAD'])
1430 self._gitcommand(['reset', 'HEAD'])
1431 cmd.append('-f')
1431 cmd.append('-f')
1432 self._gitcommand(cmd + args)
1432 self._gitcommand(cmd + args)
1433 _sanitize(self.ui, self.wvfs, '.git')
1433 _sanitize(self.ui, self.wvfs, '.git')
1434
1434
1435 def rawcheckout():
1435 def rawcheckout():
1436 # no branch to checkout, check it out with no branch
1436 # no branch to checkout, check it out with no branch
1437 self.ui.warn(_('checking out detached HEAD in '
1437 self.ui.warn(_('checking out detached HEAD in '
1438 'subrepository "%s"\n') % self._relpath)
1438 'subrepository "%s"\n') % self._relpath)
1439 self.ui.warn(_('check out a git branch if you intend '
1439 self.ui.warn(_('check out a git branch if you intend '
1440 'to make changes\n'))
1440 'to make changes\n'))
1441 checkout(['-q', revision])
1441 checkout(['-q', revision])
1442
1442
1443 if revision not in rev2branch:
1443 if revision not in rev2branch:
1444 rawcheckout()
1444 rawcheckout()
1445 return
1445 return
1446 branches = rev2branch[revision]
1446 branches = rev2branch[revision]
1447 firstlocalbranch = None
1447 firstlocalbranch = None
1448 for b in branches:
1448 for b in branches:
1449 if b == 'refs/heads/master':
1449 if b == 'refs/heads/master':
1450 # master trumps all other branches
1450 # master trumps all other branches
1451 checkout(['refs/heads/master'])
1451 checkout(['refs/heads/master'])
1452 return
1452 return
1453 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1453 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1454 firstlocalbranch = b
1454 firstlocalbranch = b
1455 if firstlocalbranch:
1455 if firstlocalbranch:
1456 checkout([firstlocalbranch])
1456 checkout([firstlocalbranch])
1457 return
1457 return
1458
1458
1459 tracking = self._gittracking(branch2rev.keys())
1459 tracking = self._gittracking(branch2rev.keys())
1460 # choose a remote branch already tracked if possible
1460 # choose a remote branch already tracked if possible
1461 remote = branches[0]
1461 remote = branches[0]
1462 if remote not in tracking:
1462 if remote not in tracking:
1463 for b in branches:
1463 for b in branches:
1464 if b in tracking:
1464 if b in tracking:
1465 remote = b
1465 remote = b
1466 break
1466 break
1467
1467
1468 if remote not in tracking:
1468 if remote not in tracking:
1469 # create a new local tracking branch
1469 # create a new local tracking branch
1470 local = remote.split('/', 3)[3]
1470 local = remote.split('/', 3)[3]
1471 checkout(['-b', local, remote])
1471 checkout(['-b', local, remote])
1472 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1472 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1473 # When updating to a tracked remote branch,
1473 # When updating to a tracked remote branch,
1474 # if the local tracking branch is downstream of it,
1474 # if the local tracking branch is downstream of it,
1475 # a normal `git pull` would have performed a "fast-forward merge"
1475 # a normal `git pull` would have performed a "fast-forward merge"
1476 # which is equivalent to updating the local branch to the remote.
1476 # which is equivalent to updating the local branch to the remote.
1477 # Since we are only looking at branching at update, we need to
1477 # Since we are only looking at branching at update, we need to
1478 # detect this situation and perform this action lazily.
1478 # detect this situation and perform this action lazily.
1479 if tracking[remote] != self._gitcurrentbranch():
1479 if tracking[remote] != self._gitcurrentbranch():
1480 checkout([tracking[remote]])
1480 checkout([tracking[remote]])
1481 self._gitcommand(['merge', '--ff', remote])
1481 self._gitcommand(['merge', '--ff', remote])
1482 _sanitize(self.ui, self.wvfs, '.git')
1482 _sanitize(self.ui, self.wvfs, '.git')
1483 else:
1483 else:
1484 # a real merge would be required, just checkout the revision
1484 # a real merge would be required, just checkout the revision
1485 rawcheckout()
1485 rawcheckout()
1486
1486
1487 @annotatesubrepoerror
1487 @annotatesubrepoerror
1488 def commit(self, text, user, date):
1488 def commit(self, text, user, date):
1489 if self._gitmissing():
1489 if self._gitmissing():
1490 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1490 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1491 cmd = ['commit', '-a', '-m', text]
1491 cmd = ['commit', '-a', '-m', text]
1492 env = encoding.environ.copy()
1492 env = encoding.environ.copy()
1493 if user:
1493 if user:
1494 cmd += ['--author', user]
1494 cmd += ['--author', user]
1495 if date:
1495 if date:
1496 # git's date parser silently ignores when seconds < 1e9
1496 # git's date parser silently ignores when seconds < 1e9
1497 # convert to ISO8601
1497 # convert to ISO8601
1498 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1498 env['GIT_AUTHOR_DATE'] = dateutil.datestr(date,
1499 '%Y-%m-%dT%H:%M:%S %1%2')
1499 '%Y-%m-%dT%H:%M:%S %1%2')
1500 self._gitcommand(cmd, env=env)
1500 self._gitcommand(cmd, env=env)
1501 # make sure commit works otherwise HEAD might not exist under certain
1501 # make sure commit works otherwise HEAD might not exist under certain
1502 # circumstances
1502 # circumstances
1503 return self._gitstate()
1503 return self._gitstate()
1504
1504
1505 @annotatesubrepoerror
1505 @annotatesubrepoerror
1506 def merge(self, state):
1506 def merge(self, state):
1507 source, revision, kind = state
1507 source, revision, kind = state
1508 self._fetch(source, revision)
1508 self._fetch(source, revision)
1509 base = self._gitcommand(['merge-base', revision, self._state[1]])
1509 base = self._gitcommand(['merge-base', revision, self._state[1]])
1510 self._gitupdatestat()
1510 self._gitupdatestat()
1511 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1511 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1512
1512
1513 def mergefunc():
1513 def mergefunc():
1514 if base == revision:
1514 if base == revision:
1515 self.get(state) # fast forward merge
1515 self.get(state) # fast forward merge
1516 elif base != self._state[1]:
1516 elif base != self._state[1]:
1517 self._gitcommand(['merge', '--no-commit', revision])
1517 self._gitcommand(['merge', '--no-commit', revision])
1518 _sanitize(self.ui, self.wvfs, '.git')
1518 _sanitize(self.ui, self.wvfs, '.git')
1519
1519
1520 if self.dirty():
1520 if self.dirty():
1521 if self._gitstate() != revision:
1521 if self._gitstate() != revision:
1522 dirty = self._gitstate() == self._state[1] or code != 0
1522 dirty = self._gitstate() == self._state[1] or code != 0
1523 if _updateprompt(self.ui, self, dirty,
1523 if _updateprompt(self.ui, self, dirty,
1524 self._state[1][:7], revision[:7]):
1524 self._state[1][:7], revision[:7]):
1525 mergefunc()
1525 mergefunc()
1526 else:
1526 else:
1527 mergefunc()
1527 mergefunc()
1528
1528
1529 @annotatesubrepoerror
1529 @annotatesubrepoerror
1530 def push(self, opts):
1530 def push(self, opts):
1531 force = opts.get('force')
1531 force = opts.get('force')
1532
1532
1533 if not self._state[1]:
1533 if not self._state[1]:
1534 return True
1534 return True
1535 if self._gitmissing():
1535 if self._gitmissing():
1536 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1536 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1537 # if a branch in origin contains the revision, nothing to do
1537 # if a branch in origin contains the revision, nothing to do
1538 branch2rev, rev2branch = self._gitbranchmap()
1538 branch2rev, rev2branch = self._gitbranchmap()
1539 if self._state[1] in rev2branch:
1539 if self._state[1] in rev2branch:
1540 for b in rev2branch[self._state[1]]:
1540 for b in rev2branch[self._state[1]]:
1541 if b.startswith('refs/remotes/origin/'):
1541 if b.startswith('refs/remotes/origin/'):
1542 return True
1542 return True
1543 for b, revision in branch2rev.iteritems():
1543 for b, revision in branch2rev.iteritems():
1544 if b.startswith('refs/remotes/origin/'):
1544 if b.startswith('refs/remotes/origin/'):
1545 if self._gitisancestor(self._state[1], revision):
1545 if self._gitisancestor(self._state[1], revision):
1546 return True
1546 return True
1547 # otherwise, try to push the currently checked out branch
1547 # otherwise, try to push the currently checked out branch
1548 cmd = ['push']
1548 cmd = ['push']
1549 if force:
1549 if force:
1550 cmd.append('--force')
1550 cmd.append('--force')
1551
1551
1552 current = self._gitcurrentbranch()
1552 current = self._gitcurrentbranch()
1553 if current:
1553 if current:
1554 # determine if the current branch is even useful
1554 # determine if the current branch is even useful
1555 if not self._gitisancestor(self._state[1], current):
1555 if not self._gitisancestor(self._state[1], current):
1556 self.ui.warn(_('unrelated git branch checked out '
1556 self.ui.warn(_('unrelated git branch checked out '
1557 'in subrepository "%s"\n') % self._relpath)
1557 'in subrepository "%s"\n') % self._relpath)
1558 return False
1558 return False
1559 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1559 self.ui.status(_('pushing branch %s of subrepository "%s"\n') %
1560 (current.split('/', 2)[2], self._relpath))
1560 (current.split('/', 2)[2], self._relpath))
1561 ret = self._gitdir(cmd + ['origin', current])
1561 ret = self._gitdir(cmd + ['origin', current])
1562 return ret[1] == 0
1562 return ret[1] == 0
1563 else:
1563 else:
1564 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1564 self.ui.warn(_('no branch checked out in subrepository "%s"\n'
1565 'cannot push revision %s\n') %
1565 'cannot push revision %s\n') %
1566 (self._relpath, self._state[1]))
1566 (self._relpath, self._state[1]))
1567 return False
1567 return False
1568
1568
1569 @annotatesubrepoerror
1569 @annotatesubrepoerror
1570 def add(self, ui, match, prefix, explicitonly, **opts):
1570 def add(self, ui, match, prefix, explicitonly, **opts):
1571 if self._gitmissing():
1571 if self._gitmissing():
1572 return []
1572 return []
1573
1573
1574 (modified, added, removed,
1574 (modified, added, removed,
1575 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1575 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1576 clean=True)
1576 clean=True)
1577
1577
1578 tracked = set()
1578 tracked = set()
1579 # dirstates 'amn' warn, 'r' is added again
1579 # dirstates 'amn' warn, 'r' is added again
1580 for l in (modified, added, deleted, clean):
1580 for l in (modified, added, deleted, clean):
1581 tracked.update(l)
1581 tracked.update(l)
1582
1582
1583 # Unknown files not of interest will be rejected by the matcher
1583 # Unknown files not of interest will be rejected by the matcher
1584 files = unknown
1584 files = unknown
1585 files.extend(match.files())
1585 files.extend(match.files())
1586
1586
1587 rejected = []
1587 rejected = []
1588
1588
1589 files = [f for f in sorted(set(files)) if match(f)]
1589 files = [f for f in sorted(set(files)) if match(f)]
1590 for f in files:
1590 for f in files:
1591 exact = match.exact(f)
1591 exact = match.exact(f)
1592 command = ["add"]
1592 command = ["add"]
1593 if exact:
1593 if exact:
1594 command.append("-f") #should be added, even if ignored
1594 command.append("-f") #should be added, even if ignored
1595 if ui.verbose or not exact:
1595 if ui.verbose or not exact:
1596 ui.status(_('adding %s\n') % match.rel(f))
1596 ui.status(_('adding %s\n') % match.rel(f))
1597
1597
1598 if f in tracked: # hg prints 'adding' even if already tracked
1598 if f in tracked: # hg prints 'adding' even if already tracked
1599 if exact:
1599 if exact:
1600 rejected.append(f)
1600 rejected.append(f)
1601 continue
1601 continue
1602 if not opts.get(r'dry_run'):
1602 if not opts.get(r'dry_run'):
1603 self._gitcommand(command + [f])
1603 self._gitcommand(command + [f])
1604
1604
1605 for f in rejected:
1605 for f in rejected:
1606 ui.warn(_("%s already tracked!\n") % match.abs(f))
1606 ui.warn(_("%s already tracked!\n") % match.abs(f))
1607
1607
1608 return rejected
1608 return rejected
1609
1609
1610 @annotatesubrepoerror
1610 @annotatesubrepoerror
1611 def remove(self):
1611 def remove(self):
1612 if self._gitmissing():
1612 if self._gitmissing():
1613 return
1613 return
1614 if self.dirty():
1614 if self.dirty():
1615 self.ui.warn(_('not removing repo %s because '
1615 self.ui.warn(_('not removing repo %s because '
1616 'it has changes.\n') % self._relpath)
1616 'it has changes.\n') % self._relpath)
1617 return
1617 return
1618 # we can't fully delete the repository as it may contain
1618 # we can't fully delete the repository as it may contain
1619 # local-only history
1619 # local-only history
1620 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1620 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1621 self._gitcommand(['config', 'core.bare', 'true'])
1621 self._gitcommand(['config', 'core.bare', 'true'])
1622 for f, kind in self.wvfs.readdir():
1622 for f, kind in self.wvfs.readdir():
1623 if f == '.git':
1623 if f == '.git':
1624 continue
1624 continue
1625 if kind == stat.S_IFDIR:
1625 if kind == stat.S_IFDIR:
1626 self.wvfs.rmtree(f)
1626 self.wvfs.rmtree(f)
1627 else:
1627 else:
1628 self.wvfs.unlink(f)
1628 self.wvfs.unlink(f)
1629
1629
1630 def archive(self, archiver, prefix, match=None, decode=True):
1630 def archive(self, archiver, prefix, match=None, decode=True):
1631 total = 0
1631 total = 0
1632 source, revision = self._state
1632 source, revision = self._state
1633 if not revision:
1633 if not revision:
1634 return total
1634 return total
1635 self._fetch(source, revision)
1635 self._fetch(source, revision)
1636
1636
1637 # Parse git's native archive command.
1637 # Parse git's native archive command.
1638 # This should be much faster than manually traversing the trees
1638 # This should be much faster than manually traversing the trees
1639 # and objects with many subprocess calls.
1639 # and objects with many subprocess calls.
1640 tarstream = self._gitcommand(['archive', revision], stream=True)
1640 tarstream = self._gitcommand(['archive', revision], stream=True)
1641 tar = tarfile.open(fileobj=tarstream, mode='r|')
1641 tar = tarfile.open(fileobj=tarstream, mode='r|')
1642 relpath = subrelpath(self)
1642 relpath = subrelpath(self)
1643 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1643 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1644 for i, info in enumerate(tar):
1644 for i, info in enumerate(tar):
1645 if info.isdir():
1645 if info.isdir():
1646 continue
1646 continue
1647 if match and not match(info.name):
1647 if match and not match(info.name):
1648 continue
1648 continue
1649 if info.issym():
1649 if info.issym():
1650 data = info.linkname
1650 data = info.linkname
1651 else:
1651 else:
1652 data = tar.extractfile(info).read()
1652 data = tar.extractfile(info).read()
1653 archiver.addfile(prefix + self._path + '/' + info.name,
1653 archiver.addfile(prefix + self._path + '/' + info.name,
1654 info.mode, info.issym(), data)
1654 info.mode, info.issym(), data)
1655 total += 1
1655 total += 1
1656 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1656 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1657 unit=_('files'))
1657 unit=_('files'))
1658 self.ui.progress(_('archiving (%s)') % relpath, None)
1658 self.ui.progress(_('archiving (%s)') % relpath, None)
1659 return total
1659 return total
1660
1660
1661
1661
1662 @annotatesubrepoerror
1662 @annotatesubrepoerror
1663 def cat(self, match, fm, fntemplate, prefix, **opts):
1663 def cat(self, match, fm, fntemplate, prefix, **opts):
1664 rev = self._state[1]
1664 rev = self._state[1]
1665 if match.anypats():
1665 if match.anypats():
1666 return 1 #No support for include/exclude yet
1666 return 1 #No support for include/exclude yet
1667
1667
1668 if not match.files():
1668 if not match.files():
1669 return 1
1669 return 1
1670
1670
1671 # TODO: add support for non-plain formatter (see cmdutil.cat())
1671 # TODO: add support for non-plain formatter (see cmdutil.cat())
1672 for f in match.files():
1672 for f in match.files():
1673 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1673 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1674 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1674 fp = cmdutil.makefileobj(self._ctx, fntemplate,
1675 pathname=self.wvfs.reljoin(prefix, f))
1675 pathname=self.wvfs.reljoin(prefix, f))
1676 fp.write(output)
1676 fp.write(output)
1677 fp.close()
1677 fp.close()
1678 return 0
1678 return 0
1679
1679
1680
1680
1681 @annotatesubrepoerror
1681 @annotatesubrepoerror
1682 def status(self, rev2, **opts):
1682 def status(self, rev2, **opts):
1683 rev1 = self._state[1]
1683 rev1 = self._state[1]
1684 if self._gitmissing() or not rev1:
1684 if self._gitmissing() or not rev1:
1685 # if the repo is missing, return no results
1685 # if the repo is missing, return no results
1686 return scmutil.status([], [], [], [], [], [], [])
1686 return scmutil.status([], [], [], [], [], [], [])
1687 modified, added, removed = [], [], []
1687 modified, added, removed = [], [], []
1688 self._gitupdatestat()
1688 self._gitupdatestat()
1689 if rev2:
1689 if rev2:
1690 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1690 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1691 else:
1691 else:
1692 command = ['diff-index', '--no-renames', rev1]
1692 command = ['diff-index', '--no-renames', rev1]
1693 out = self._gitcommand(command)
1693 out = self._gitcommand(command)
1694 for line in out.split('\n'):
1694 for line in out.split('\n'):
1695 tab = line.find('\t')
1695 tab = line.find('\t')
1696 if tab == -1:
1696 if tab == -1:
1697 continue
1697 continue
1698 status, f = line[tab - 1], line[tab + 1:]
1698 status, f = line[tab - 1], line[tab + 1:]
1699 if status == 'M':
1699 if status == 'M':
1700 modified.append(f)
1700 modified.append(f)
1701 elif status == 'A':
1701 elif status == 'A':
1702 added.append(f)
1702 added.append(f)
1703 elif status == 'D':
1703 elif status == 'D':
1704 removed.append(f)
1704 removed.append(f)
1705
1705
1706 deleted, unknown, ignored, clean = [], [], [], []
1706 deleted, unknown, ignored, clean = [], [], [], []
1707
1707
1708 command = ['status', '--porcelain', '-z']
1708 command = ['status', '--porcelain', '-z']
1709 if opts.get(r'unknown'):
1709 if opts.get(r'unknown'):
1710 command += ['--untracked-files=all']
1710 command += ['--untracked-files=all']
1711 if opts.get(r'ignored'):
1711 if opts.get(r'ignored'):
1712 command += ['--ignored']
1712 command += ['--ignored']
1713 out = self._gitcommand(command)
1713 out = self._gitcommand(command)
1714
1714
1715 changedfiles = set()
1715 changedfiles = set()
1716 changedfiles.update(modified)
1716 changedfiles.update(modified)
1717 changedfiles.update(added)
1717 changedfiles.update(added)
1718 changedfiles.update(removed)
1718 changedfiles.update(removed)
1719 for line in out.split('\0'):
1719 for line in out.split('\0'):
1720 if not line:
1720 if not line:
1721 continue
1721 continue
1722 st = line[0:2]
1722 st = line[0:2]
1723 #moves and copies show 2 files on one line
1723 #moves and copies show 2 files on one line
1724 if line.find('\0') >= 0:
1724 if line.find('\0') >= 0:
1725 filename1, filename2 = line[3:].split('\0')
1725 filename1, filename2 = line[3:].split('\0')
1726 else:
1726 else:
1727 filename1 = line[3:]
1727 filename1 = line[3:]
1728 filename2 = None
1728 filename2 = None
1729
1729
1730 changedfiles.add(filename1)
1730 changedfiles.add(filename1)
1731 if filename2:
1731 if filename2:
1732 changedfiles.add(filename2)
1732 changedfiles.add(filename2)
1733
1733
1734 if st == '??':
1734 if st == '??':
1735 unknown.append(filename1)
1735 unknown.append(filename1)
1736 elif st == '!!':
1736 elif st == '!!':
1737 ignored.append(filename1)
1737 ignored.append(filename1)
1738
1738
1739 if opts.get(r'clean'):
1739 if opts.get(r'clean'):
1740 out = self._gitcommand(['ls-files'])
1740 out = self._gitcommand(['ls-files'])
1741 for f in out.split('\n'):
1741 for f in out.split('\n'):
1742 if not f in changedfiles:
1742 if not f in changedfiles:
1743 clean.append(f)
1743 clean.append(f)
1744
1744
1745 return scmutil.status(modified, added, removed, deleted,
1745 return scmutil.status(modified, added, removed, deleted,
1746 unknown, ignored, clean)
1746 unknown, ignored, clean)
1747
1747
1748 @annotatesubrepoerror
1748 @annotatesubrepoerror
1749 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1749 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1750 node1 = self._state[1]
1750 node1 = self._state[1]
1751 cmd = ['diff', '--no-renames']
1751 cmd = ['diff', '--no-renames']
1752 if opts[r'stat']:
1752 if opts[r'stat']:
1753 cmd.append('--stat')
1753 cmd.append('--stat')
1754 else:
1754 else:
1755 # for Git, this also implies '-p'
1755 # for Git, this also implies '-p'
1756 cmd.append('-U%d' % diffopts.context)
1756 cmd.append('-U%d' % diffopts.context)
1757
1757
1758 gitprefix = self.wvfs.reljoin(prefix, self._path)
1758 gitprefix = self.wvfs.reljoin(prefix, self._path)
1759
1759
1760 if diffopts.noprefix:
1760 if diffopts.noprefix:
1761 cmd.extend(['--src-prefix=%s/' % gitprefix,
1761 cmd.extend(['--src-prefix=%s/' % gitprefix,
1762 '--dst-prefix=%s/' % gitprefix])
1762 '--dst-prefix=%s/' % gitprefix])
1763 else:
1763 else:
1764 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1764 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1765 '--dst-prefix=b/%s/' % gitprefix])
1765 '--dst-prefix=b/%s/' % gitprefix])
1766
1766
1767 if diffopts.ignorews:
1767 if diffopts.ignorews:
1768 cmd.append('--ignore-all-space')
1768 cmd.append('--ignore-all-space')
1769 if diffopts.ignorewsamount:
1769 if diffopts.ignorewsamount:
1770 cmd.append('--ignore-space-change')
1770 cmd.append('--ignore-space-change')
1771 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1771 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1772 and diffopts.ignoreblanklines:
1772 and diffopts.ignoreblanklines:
1773 cmd.append('--ignore-blank-lines')
1773 cmd.append('--ignore-blank-lines')
1774
1774
1775 cmd.append(node1)
1775 cmd.append(node1)
1776 if node2:
1776 if node2:
1777 cmd.append(node2)
1777 cmd.append(node2)
1778
1778
1779 output = ""
1779 output = ""
1780 if match.always():
1780 if match.always():
1781 output += self._gitcommand(cmd) + '\n'
1781 output += self._gitcommand(cmd) + '\n'
1782 else:
1782 else:
1783 st = self.status(node2)[:3]
1783 st = self.status(node2)[:3]
1784 files = [f for sublist in st for f in sublist]
1784 files = [f for sublist in st for f in sublist]
1785 for f in files:
1785 for f in files:
1786 if match(f):
1786 if match(f):
1787 output += self._gitcommand(cmd + ['--', f]) + '\n'
1787 output += self._gitcommand(cmd + ['--', f]) + '\n'
1788
1788
1789 if output.strip():
1789 if output.strip():
1790 ui.write(output)
1790 ui.write(output)
1791
1791
1792 @annotatesubrepoerror
1792 @annotatesubrepoerror
1793 def revert(self, substate, *pats, **opts):
1793 def revert(self, substate, *pats, **opts):
1794 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1794 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1795 if not opts.get(r'no_backup'):
1795 if not opts.get(r'no_backup'):
1796 status = self.status(None)
1796 status = self.status(None)
1797 names = status.modified
1797 names = status.modified
1798 for name in names:
1798 for name in names:
1799 bakname = scmutil.origpath(self.ui, self._subparent, name)
1799 bakname = scmutil.origpath(self.ui, self._subparent, name)
1800 self.ui.note(_('saving current version of %s as %s\n') %
1800 self.ui.note(_('saving current version of %s as %s\n') %
1801 (name, bakname))
1801 (name, bakname))
1802 self.wvfs.rename(name, bakname)
1802 self.wvfs.rename(name, bakname)
1803
1803
1804 if not opts.get(r'dry_run'):
1804 if not opts.get(r'dry_run'):
1805 self.get(substate, overwrite=True)
1805 self.get(substate, overwrite=True)
1806 return []
1806 return []
1807
1807
1808 def shortid(self, revid):
1808 def shortid(self, revid):
1809 return revid[:7]
1809 return revid[:7]
1810
1810
1811 types = {
1811 types = {
1812 'hg': hgsubrepo,
1812 'hg': hgsubrepo,
1813 'svn': svnsubrepo,
1813 'svn': svnsubrepo,
1814 'git': gitsubrepo,
1814 'git': gitsubrepo,
1815 }
1815 }
General Comments 0
You need to be logged in to leave comments. Login now