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