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