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