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