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