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