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