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