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