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