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