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