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