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