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