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