##// END OF EJS Templates
subrepo: stop pulling bookmark manually...
Pierre-Yves David -
r22657:f4e39ceb default
parent child Browse files
Show More
@@ -1,1598 +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 import config, util, node, error, cmdutil, bookmarks, match as matchmod
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 remotebookmarks = other.listkeys('bookmarks')
747 746 self._repo.pull(other)
748 bookmarks.updatefromremote(self._repo.ui, self._repo,
749 remotebookmarks, srcurl)
750 747 if cleansub:
751 748 # keep the repo clean after pull
752 749 self._cachestorehash(srcurl)
753 750 return False
754 751
755 752 @annotatesubrepoerror
756 753 def get(self, state, overwrite=False):
757 754 inrepo = self._get(state)
758 755 source, revision, kind = state
759 756 repo = self._repo
760 757 repo.ui.debug("getting subrepo %s\n" % self._path)
761 758 if inrepo:
762 759 urepo = repo.unfiltered()
763 760 ctx = urepo[revision]
764 761 if ctx.hidden():
765 762 urepo.ui.warn(
766 763 _('revision %s in subrepo %s is hidden\n') \
767 764 % (revision[0:12], self._path))
768 765 repo = urepo
769 766 hg.updaterepo(repo, revision, overwrite)
770 767
771 768 @annotatesubrepoerror
772 769 def merge(self, state):
773 770 self._get(state)
774 771 cur = self._repo['.']
775 772 dst = self._repo[state[1]]
776 773 anc = dst.ancestor(cur)
777 774
778 775 def mergefunc():
779 776 if anc == cur and dst.branch() == cur.branch():
780 777 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
781 778 hg.update(self._repo, state[1])
782 779 elif anc == dst:
783 780 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
784 781 else:
785 782 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
786 783 hg.merge(self._repo, state[1], remind=False)
787 784
788 785 wctx = self._repo[None]
789 786 if self.dirty():
790 787 if anc != dst:
791 788 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
792 789 mergefunc()
793 790 else:
794 791 mergefunc()
795 792 else:
796 793 mergefunc()
797 794
798 795 @annotatesubrepoerror
799 796 def push(self, opts):
800 797 force = opts.get('force')
801 798 newbranch = opts.get('new_branch')
802 799 ssh = opts.get('ssh')
803 800
804 801 # push subrepos depth-first for coherent ordering
805 802 c = self._repo['']
806 803 subs = c.substate # only repos that are committed
807 804 for s in sorted(subs):
808 805 if c.sub(s).push(opts) == 0:
809 806 return False
810 807
811 808 dsturl = _abssource(self._repo, True)
812 809 if not force:
813 810 if self.storeclean(dsturl):
814 811 self._repo.ui.status(
815 812 _('no changes made to subrepo %s since last push to %s\n')
816 813 % (subrelpath(self), dsturl))
817 814 return None
818 815 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
819 816 (subrelpath(self), dsturl))
820 817 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
821 818 res = exchange.push(self._repo, other, force, newbranch=newbranch)
822 819
823 820 # the repo is now clean
824 821 self._cachestorehash(dsturl)
825 822 return res.cgresult
826 823
827 824 @annotatesubrepoerror
828 825 def outgoing(self, ui, dest, opts):
829 826 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
830 827
831 828 @annotatesubrepoerror
832 829 def incoming(self, ui, source, opts):
833 830 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
834 831
835 832 @annotatesubrepoerror
836 833 def files(self):
837 834 rev = self._state[1]
838 835 ctx = self._repo[rev]
839 836 return ctx.manifest()
840 837
841 838 def filedata(self, name):
842 839 rev = self._state[1]
843 840 return self._repo[rev][name].data()
844 841
845 842 def fileflags(self, name):
846 843 rev = self._state[1]
847 844 ctx = self._repo[rev]
848 845 return ctx.flags(name)
849 846
850 847 def walk(self, match):
851 848 ctx = self._repo[None]
852 849 return ctx.walk(match)
853 850
854 851 @annotatesubrepoerror
855 852 def forget(self, ui, match, prefix):
856 853 return cmdutil.forget(ui, self._repo, match,
857 854 os.path.join(prefix, self._path), True)
858 855
859 856 @annotatesubrepoerror
860 857 def revert(self, ui, substate, *pats, **opts):
861 858 # reverting a subrepo is a 2 step process:
862 859 # 1. if the no_backup is not set, revert all modified
863 860 # files inside the subrepo
864 861 # 2. update the subrepo to the revision specified in
865 862 # the corresponding substate dictionary
866 863 ui.status(_('reverting subrepo %s\n') % substate[0])
867 864 if not opts.get('no_backup'):
868 865 # Revert all files on the subrepo, creating backups
869 866 # Note that this will not recursively revert subrepos
870 867 # We could do it if there was a set:subrepos() predicate
871 868 opts = opts.copy()
872 869 opts['date'] = None
873 870 opts['rev'] = substate[1]
874 871
875 872 pats = []
876 873 if not opts.get('all'):
877 874 pats = ['set:modified()']
878 875 self.filerevert(ui, *pats, **opts)
879 876
880 877 # Update the repo to the revision specified in the given substate
881 878 self.get(substate, overwrite=True)
882 879
883 880 def filerevert(self, ui, *pats, **opts):
884 881 ctx = self._repo[opts['rev']]
885 882 parents = self._repo.dirstate.parents()
886 883 if opts.get('all'):
887 884 pats = ['set:modified()']
888 885 else:
889 886 pats = []
890 887 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
891 888
892 889 def shortid(self, revid):
893 890 return revid[:12]
894 891
895 892 class svnsubrepo(abstractsubrepo):
896 893 def __init__(self, ctx, path, state):
897 894 self._path = path
898 895 self._state = state
899 896 self._ctx = ctx
900 897 self._ui = ctx._repo.ui
901 898 self._exe = util.findexe('svn')
902 899 if not self._exe:
903 900 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
904 901 % self._path)
905 902
906 903 def _svncommand(self, commands, filename='', failok=False):
907 904 cmd = [self._exe]
908 905 extrakw = {}
909 906 if not self._ui.interactive():
910 907 # Making stdin be a pipe should prevent svn from behaving
911 908 # interactively even if we can't pass --non-interactive.
912 909 extrakw['stdin'] = subprocess.PIPE
913 910 # Starting in svn 1.5 --non-interactive is a global flag
914 911 # instead of being per-command, but we need to support 1.4 so
915 912 # we have to be intelligent about what commands take
916 913 # --non-interactive.
917 914 if commands[0] in ('update', 'checkout', 'commit'):
918 915 cmd.append('--non-interactive')
919 916 cmd.extend(commands)
920 917 if filename is not None:
921 918 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
922 919 cmd.append(path)
923 920 env = dict(os.environ)
924 921 # Avoid localized output, preserve current locale for everything else.
925 922 lc_all = env.get('LC_ALL')
926 923 if lc_all:
927 924 env['LANG'] = lc_all
928 925 del env['LC_ALL']
929 926 env['LC_MESSAGES'] = 'C'
930 927 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
931 928 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
932 929 universal_newlines=True, env=env, **extrakw)
933 930 stdout, stderr = p.communicate()
934 931 stderr = stderr.strip()
935 932 if not failok:
936 933 if p.returncode:
937 934 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
938 935 if stderr:
939 936 self._ui.warn(stderr + '\n')
940 937 return stdout, stderr
941 938
942 939 @propertycache
943 940 def _svnversion(self):
944 941 output, err = self._svncommand(['--version', '--quiet'], filename=None)
945 942 m = re.search(r'^(\d+)\.(\d+)', output)
946 943 if not m:
947 944 raise util.Abort(_('cannot retrieve svn tool version'))
948 945 return (int(m.group(1)), int(m.group(2)))
949 946
950 947 def _wcrevs(self):
951 948 # Get the working directory revision as well as the last
952 949 # commit revision so we can compare the subrepo state with
953 950 # both. We used to store the working directory one.
954 951 output, err = self._svncommand(['info', '--xml'])
955 952 doc = xml.dom.minidom.parseString(output)
956 953 entries = doc.getElementsByTagName('entry')
957 954 lastrev, rev = '0', '0'
958 955 if entries:
959 956 rev = str(entries[0].getAttribute('revision')) or '0'
960 957 commits = entries[0].getElementsByTagName('commit')
961 958 if commits:
962 959 lastrev = str(commits[0].getAttribute('revision')) or '0'
963 960 return (lastrev, rev)
964 961
965 962 def _wcrev(self):
966 963 return self._wcrevs()[0]
967 964
968 965 def _wcchanged(self):
969 966 """Return (changes, extchanges, missing) where changes is True
970 967 if the working directory was changed, extchanges is
971 968 True if any of these changes concern an external entry and missing
972 969 is True if any change is a missing entry.
973 970 """
974 971 output, err = self._svncommand(['status', '--xml'])
975 972 externals, changes, missing = [], [], []
976 973 doc = xml.dom.minidom.parseString(output)
977 974 for e in doc.getElementsByTagName('entry'):
978 975 s = e.getElementsByTagName('wc-status')
979 976 if not s:
980 977 continue
981 978 item = s[0].getAttribute('item')
982 979 props = s[0].getAttribute('props')
983 980 path = e.getAttribute('path')
984 981 if item == 'external':
985 982 externals.append(path)
986 983 elif item == 'missing':
987 984 missing.append(path)
988 985 if (item not in ('', 'normal', 'unversioned', 'external')
989 986 or props not in ('', 'none', 'normal')):
990 987 changes.append(path)
991 988 for path in changes:
992 989 for ext in externals:
993 990 if path == ext or path.startswith(ext + os.sep):
994 991 return True, True, bool(missing)
995 992 return bool(changes), False, bool(missing)
996 993
997 994 def dirty(self, ignoreupdate=False):
998 995 if not self._wcchanged()[0]:
999 996 if self._state[1] in self._wcrevs() or ignoreupdate:
1000 997 return False
1001 998 return True
1002 999
1003 1000 def basestate(self):
1004 1001 lastrev, rev = self._wcrevs()
1005 1002 if lastrev != rev:
1006 1003 # Last committed rev is not the same than rev. We would
1007 1004 # like to take lastrev but we do not know if the subrepo
1008 1005 # URL exists at lastrev. Test it and fallback to rev it
1009 1006 # is not there.
1010 1007 try:
1011 1008 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1012 1009 return lastrev
1013 1010 except error.Abort:
1014 1011 pass
1015 1012 return rev
1016 1013
1017 1014 @annotatesubrepoerror
1018 1015 def commit(self, text, user, date):
1019 1016 # user and date are out of our hands since svn is centralized
1020 1017 changed, extchanged, missing = self._wcchanged()
1021 1018 if not changed:
1022 1019 return self.basestate()
1023 1020 if extchanged:
1024 1021 # Do not try to commit externals
1025 1022 raise util.Abort(_('cannot commit svn externals'))
1026 1023 if missing:
1027 1024 # svn can commit with missing entries but aborting like hg
1028 1025 # seems a better approach.
1029 1026 raise util.Abort(_('cannot commit missing svn entries'))
1030 1027 commitinfo, err = self._svncommand(['commit', '-m', text])
1031 1028 self._ui.status(commitinfo)
1032 1029 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1033 1030 if not newrev:
1034 1031 if not commitinfo.strip():
1035 1032 # Sometimes, our definition of "changed" differs from
1036 1033 # svn one. For instance, svn ignores missing files
1037 1034 # when committing. If there are only missing files, no
1038 1035 # commit is made, no output and no error code.
1039 1036 raise util.Abort(_('failed to commit svn changes'))
1040 1037 raise util.Abort(commitinfo.splitlines()[-1])
1041 1038 newrev = newrev.groups()[0]
1042 1039 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
1043 1040 return newrev
1044 1041
1045 1042 @annotatesubrepoerror
1046 1043 def remove(self):
1047 1044 if self.dirty():
1048 1045 self._ui.warn(_('not removing repo %s because '
1049 1046 'it has changes.\n') % self._path)
1050 1047 return
1051 1048 self._ui.note(_('removing subrepo %s\n') % self._path)
1052 1049
1053 1050 def onerror(function, path, excinfo):
1054 1051 if function is not os.remove:
1055 1052 raise
1056 1053 # read-only files cannot be unlinked under Windows
1057 1054 s = os.stat(path)
1058 1055 if (s.st_mode & stat.S_IWRITE) != 0:
1059 1056 raise
1060 1057 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1061 1058 os.remove(path)
1062 1059
1063 1060 path = self._ctx._repo.wjoin(self._path)
1064 1061 shutil.rmtree(path, onerror=onerror)
1065 1062 try:
1066 1063 os.removedirs(os.path.dirname(path))
1067 1064 except OSError:
1068 1065 pass
1069 1066
1070 1067 @annotatesubrepoerror
1071 1068 def get(self, state, overwrite=False):
1072 1069 if overwrite:
1073 1070 self._svncommand(['revert', '--recursive'])
1074 1071 args = ['checkout']
1075 1072 if self._svnversion >= (1, 5):
1076 1073 args.append('--force')
1077 1074 # The revision must be specified at the end of the URL to properly
1078 1075 # update to a directory which has since been deleted and recreated.
1079 1076 args.append('%s@%s' % (state[0], state[1]))
1080 1077 status, err = self._svncommand(args, failok=True)
1081 1078 _sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn')
1082 1079 if not re.search('Checked out revision [0-9]+.', status):
1083 1080 if ('is already a working copy for a different URL' in err
1084 1081 and (self._wcchanged()[:2] == (False, False))):
1085 1082 # obstructed but clean working copy, so just blow it away.
1086 1083 self.remove()
1087 1084 self.get(state, overwrite=False)
1088 1085 return
1089 1086 raise util.Abort((status or err).splitlines()[-1])
1090 1087 self._ui.status(status)
1091 1088
1092 1089 @annotatesubrepoerror
1093 1090 def merge(self, state):
1094 1091 old = self._state[1]
1095 1092 new = state[1]
1096 1093 wcrev = self._wcrev()
1097 1094 if new != wcrev:
1098 1095 dirty = old == wcrev or self._wcchanged()[0]
1099 1096 if _updateprompt(self._ui, self, dirty, wcrev, new):
1100 1097 self.get(state, False)
1101 1098
1102 1099 def push(self, opts):
1103 1100 # push is a no-op for SVN
1104 1101 return True
1105 1102
1106 1103 @annotatesubrepoerror
1107 1104 def files(self):
1108 1105 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1109 1106 doc = xml.dom.minidom.parseString(output)
1110 1107 paths = []
1111 1108 for e in doc.getElementsByTagName('entry'):
1112 1109 kind = str(e.getAttribute('kind'))
1113 1110 if kind != 'file':
1114 1111 continue
1115 1112 name = ''.join(c.data for c
1116 1113 in e.getElementsByTagName('name')[0].childNodes
1117 1114 if c.nodeType == c.TEXT_NODE)
1118 1115 paths.append(name.encode('utf-8'))
1119 1116 return paths
1120 1117
1121 1118 def filedata(self, name):
1122 1119 return self._svncommand(['cat'], name)[0]
1123 1120
1124 1121
1125 1122 class gitsubrepo(abstractsubrepo):
1126 1123 def __init__(self, ctx, path, state):
1127 1124 self._state = state
1128 1125 self._ctx = ctx
1129 1126 self._path = path
1130 1127 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1131 1128 self._abspath = ctx._repo.wjoin(path)
1132 1129 self._subparent = ctx._repo
1133 1130 self._ui = ctx._repo.ui
1134 1131 self._ensuregit()
1135 1132
1136 1133 def _ensuregit(self):
1137 1134 try:
1138 1135 self._gitexecutable = 'git'
1139 1136 out, err = self._gitnodir(['--version'])
1140 1137 except OSError, e:
1141 1138 if e.errno != 2 or os.name != 'nt':
1142 1139 raise
1143 1140 self._gitexecutable = 'git.cmd'
1144 1141 out, err = self._gitnodir(['--version'])
1145 1142 versionstatus = self._checkversion(out)
1146 1143 if versionstatus == 'unknown':
1147 1144 self._ui.warn(_('cannot retrieve git version\n'))
1148 1145 elif versionstatus == 'abort':
1149 1146 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1150 1147 elif versionstatus == 'warning':
1151 1148 self._ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1152 1149
1153 1150 @staticmethod
1154 1151 def _checkversion(out):
1155 1152 '''ensure git version is new enough
1156 1153
1157 1154 >>> _checkversion = gitsubrepo._checkversion
1158 1155 >>> _checkversion('git version 1.6.0')
1159 1156 'ok'
1160 1157 >>> _checkversion('git version 1.8.5')
1161 1158 'ok'
1162 1159 >>> _checkversion('git version 1.4.0')
1163 1160 'abort'
1164 1161 >>> _checkversion('git version 1.5.0')
1165 1162 'warning'
1166 1163 >>> _checkversion('git version 1.9-rc0')
1167 1164 'ok'
1168 1165 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1169 1166 'ok'
1170 1167 >>> _checkversion('git version 1.9.0.GIT')
1171 1168 'ok'
1172 1169 >>> _checkversion('git version 12345')
1173 1170 'unknown'
1174 1171 >>> _checkversion('no')
1175 1172 'unknown'
1176 1173 '''
1177 1174 m = re.search(r'^git version (\d+)\.(\d+)', out)
1178 1175 if not m:
1179 1176 return 'unknown'
1180 1177 version = (int(m.group(1)), int(m.group(2)))
1181 1178 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1182 1179 # despite the docstring comment. For now, error on 1.4.0, warn on
1183 1180 # 1.5.0 but attempt to continue.
1184 1181 if version < (1, 5):
1185 1182 return 'abort'
1186 1183 elif version < (1, 6):
1187 1184 return 'warning'
1188 1185 return 'ok'
1189 1186
1190 1187 def _gitcommand(self, commands, env=None, stream=False):
1191 1188 return self._gitdir(commands, env=env, stream=stream)[0]
1192 1189
1193 1190 def _gitdir(self, commands, env=None, stream=False):
1194 1191 return self._gitnodir(commands, env=env, stream=stream,
1195 1192 cwd=self._abspath)
1196 1193
1197 1194 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1198 1195 """Calls the git command
1199 1196
1200 1197 The methods tries to call the git command. versions prior to 1.6.0
1201 1198 are not supported and very probably fail.
1202 1199 """
1203 1200 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1204 1201 # unless ui.quiet is set, print git's stderr,
1205 1202 # which is mostly progress and useful info
1206 1203 errpipe = None
1207 1204 if self._ui.quiet:
1208 1205 errpipe = open(os.devnull, 'w')
1209 1206 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1210 1207 cwd=cwd, env=env, close_fds=util.closefds,
1211 1208 stdout=subprocess.PIPE, stderr=errpipe)
1212 1209 if stream:
1213 1210 return p.stdout, None
1214 1211
1215 1212 retdata = p.stdout.read().strip()
1216 1213 # wait for the child to exit to avoid race condition.
1217 1214 p.wait()
1218 1215
1219 1216 if p.returncode != 0 and p.returncode != 1:
1220 1217 # there are certain error codes that are ok
1221 1218 command = commands[0]
1222 1219 if command in ('cat-file', 'symbolic-ref'):
1223 1220 return retdata, p.returncode
1224 1221 # for all others, abort
1225 1222 raise util.Abort('git %s error %d in %s' %
1226 1223 (command, p.returncode, self._relpath))
1227 1224
1228 1225 return retdata, p.returncode
1229 1226
1230 1227 def _gitmissing(self):
1231 1228 return not os.path.exists(os.path.join(self._abspath, '.git'))
1232 1229
1233 1230 def _gitstate(self):
1234 1231 return self._gitcommand(['rev-parse', 'HEAD'])
1235 1232
1236 1233 def _gitcurrentbranch(self):
1237 1234 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1238 1235 if err:
1239 1236 current = None
1240 1237 return current
1241 1238
1242 1239 def _gitremote(self, remote):
1243 1240 out = self._gitcommand(['remote', 'show', '-n', remote])
1244 1241 line = out.split('\n')[1]
1245 1242 i = line.index('URL: ') + len('URL: ')
1246 1243 return line[i:]
1247 1244
1248 1245 def _githavelocally(self, revision):
1249 1246 out, code = self._gitdir(['cat-file', '-e', revision])
1250 1247 return code == 0
1251 1248
1252 1249 def _gitisancestor(self, r1, r2):
1253 1250 base = self._gitcommand(['merge-base', r1, r2])
1254 1251 return base == r1
1255 1252
1256 1253 def _gitisbare(self):
1257 1254 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1258 1255
1259 1256 def _gitupdatestat(self):
1260 1257 """This must be run before git diff-index.
1261 1258 diff-index only looks at changes to file stat;
1262 1259 this command looks at file contents and updates the stat."""
1263 1260 self._gitcommand(['update-index', '-q', '--refresh'])
1264 1261
1265 1262 def _gitbranchmap(self):
1266 1263 '''returns 2 things:
1267 1264 a map from git branch to revision
1268 1265 a map from revision to branches'''
1269 1266 branch2rev = {}
1270 1267 rev2branch = {}
1271 1268
1272 1269 out = self._gitcommand(['for-each-ref', '--format',
1273 1270 '%(objectname) %(refname)'])
1274 1271 for line in out.split('\n'):
1275 1272 revision, ref = line.split(' ')
1276 1273 if (not ref.startswith('refs/heads/') and
1277 1274 not ref.startswith('refs/remotes/')):
1278 1275 continue
1279 1276 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1280 1277 continue # ignore remote/HEAD redirects
1281 1278 branch2rev[ref] = revision
1282 1279 rev2branch.setdefault(revision, []).append(ref)
1283 1280 return branch2rev, rev2branch
1284 1281
1285 1282 def _gittracking(self, branches):
1286 1283 'return map of remote branch to local tracking branch'
1287 1284 # assumes no more than one local tracking branch for each remote
1288 1285 tracking = {}
1289 1286 for b in branches:
1290 1287 if b.startswith('refs/remotes/'):
1291 1288 continue
1292 1289 bname = b.split('/', 2)[2]
1293 1290 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1294 1291 if remote:
1295 1292 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1296 1293 tracking['refs/remotes/%s/%s' %
1297 1294 (remote, ref.split('/', 2)[2])] = b
1298 1295 return tracking
1299 1296
1300 1297 def _abssource(self, source):
1301 1298 if '://' not in source:
1302 1299 # recognize the scp syntax as an absolute source
1303 1300 colon = source.find(':')
1304 1301 if colon != -1 and '/' not in source[:colon]:
1305 1302 return source
1306 1303 self._subsource = source
1307 1304 return _abssource(self)
1308 1305
1309 1306 def _fetch(self, source, revision):
1310 1307 if self._gitmissing():
1311 1308 source = self._abssource(source)
1312 1309 self._ui.status(_('cloning subrepo %s from %s\n') %
1313 1310 (self._relpath, source))
1314 1311 self._gitnodir(['clone', source, self._abspath])
1315 1312 if self._githavelocally(revision):
1316 1313 return
1317 1314 self._ui.status(_('pulling subrepo %s from %s\n') %
1318 1315 (self._relpath, self._gitremote('origin')))
1319 1316 # try only origin: the originally cloned repo
1320 1317 self._gitcommand(['fetch'])
1321 1318 if not self._githavelocally(revision):
1322 1319 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1323 1320 (revision, self._relpath))
1324 1321
1325 1322 @annotatesubrepoerror
1326 1323 def dirty(self, ignoreupdate=False):
1327 1324 if self._gitmissing():
1328 1325 return self._state[1] != ''
1329 1326 if self._gitisbare():
1330 1327 return True
1331 1328 if not ignoreupdate and self._state[1] != self._gitstate():
1332 1329 # different version checked out
1333 1330 return True
1334 1331 # check for staged changes or modified files; ignore untracked files
1335 1332 self._gitupdatestat()
1336 1333 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1337 1334 return code == 1
1338 1335
1339 1336 def basestate(self):
1340 1337 return self._gitstate()
1341 1338
1342 1339 @annotatesubrepoerror
1343 1340 def get(self, state, overwrite=False):
1344 1341 source, revision, kind = state
1345 1342 if not revision:
1346 1343 self.remove()
1347 1344 return
1348 1345 self._fetch(source, revision)
1349 1346 # if the repo was set to be bare, unbare it
1350 1347 if self._gitisbare():
1351 1348 self._gitcommand(['config', 'core.bare', 'false'])
1352 1349 if self._gitstate() == revision:
1353 1350 self._gitcommand(['reset', '--hard', 'HEAD'])
1354 1351 return
1355 1352 elif self._gitstate() == revision:
1356 1353 if overwrite:
1357 1354 # first reset the index to unmark new files for commit, because
1358 1355 # reset --hard will otherwise throw away files added for commit,
1359 1356 # not just unmark them.
1360 1357 self._gitcommand(['reset', 'HEAD'])
1361 1358 self._gitcommand(['reset', '--hard', 'HEAD'])
1362 1359 return
1363 1360 branch2rev, rev2branch = self._gitbranchmap()
1364 1361
1365 1362 def checkout(args):
1366 1363 cmd = ['checkout']
1367 1364 if overwrite:
1368 1365 # first reset the index to unmark new files for commit, because
1369 1366 # the -f option will otherwise throw away files added for
1370 1367 # commit, not just unmark them.
1371 1368 self._gitcommand(['reset', 'HEAD'])
1372 1369 cmd.append('-f')
1373 1370 self._gitcommand(cmd + args)
1374 1371 _sanitize(self._ui, self._abspath, '.git')
1375 1372
1376 1373 def rawcheckout():
1377 1374 # no branch to checkout, check it out with no branch
1378 1375 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1379 1376 self._relpath)
1380 1377 self._ui.warn(_('check out a git branch if you intend '
1381 1378 'to make changes\n'))
1382 1379 checkout(['-q', revision])
1383 1380
1384 1381 if revision not in rev2branch:
1385 1382 rawcheckout()
1386 1383 return
1387 1384 branches = rev2branch[revision]
1388 1385 firstlocalbranch = None
1389 1386 for b in branches:
1390 1387 if b == 'refs/heads/master':
1391 1388 # master trumps all other branches
1392 1389 checkout(['refs/heads/master'])
1393 1390 return
1394 1391 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1395 1392 firstlocalbranch = b
1396 1393 if firstlocalbranch:
1397 1394 checkout([firstlocalbranch])
1398 1395 return
1399 1396
1400 1397 tracking = self._gittracking(branch2rev.keys())
1401 1398 # choose a remote branch already tracked if possible
1402 1399 remote = branches[0]
1403 1400 if remote not in tracking:
1404 1401 for b in branches:
1405 1402 if b in tracking:
1406 1403 remote = b
1407 1404 break
1408 1405
1409 1406 if remote not in tracking:
1410 1407 # create a new local tracking branch
1411 1408 local = remote.split('/', 3)[3]
1412 1409 checkout(['-b', local, remote])
1413 1410 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1414 1411 # When updating to a tracked remote branch,
1415 1412 # if the local tracking branch is downstream of it,
1416 1413 # a normal `git pull` would have performed a "fast-forward merge"
1417 1414 # which is equivalent to updating the local branch to the remote.
1418 1415 # Since we are only looking at branching at update, we need to
1419 1416 # detect this situation and perform this action lazily.
1420 1417 if tracking[remote] != self._gitcurrentbranch():
1421 1418 checkout([tracking[remote]])
1422 1419 self._gitcommand(['merge', '--ff', remote])
1423 1420 _sanitize(self._ui, self._abspath, '.git')
1424 1421 else:
1425 1422 # a real merge would be required, just checkout the revision
1426 1423 rawcheckout()
1427 1424
1428 1425 @annotatesubrepoerror
1429 1426 def commit(self, text, user, date):
1430 1427 if self._gitmissing():
1431 1428 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1432 1429 cmd = ['commit', '-a', '-m', text]
1433 1430 env = os.environ.copy()
1434 1431 if user:
1435 1432 cmd += ['--author', user]
1436 1433 if date:
1437 1434 # git's date parser silently ignores when seconds < 1e9
1438 1435 # convert to ISO8601
1439 1436 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1440 1437 '%Y-%m-%dT%H:%M:%S %1%2')
1441 1438 self._gitcommand(cmd, env=env)
1442 1439 # make sure commit works otherwise HEAD might not exist under certain
1443 1440 # circumstances
1444 1441 return self._gitstate()
1445 1442
1446 1443 @annotatesubrepoerror
1447 1444 def merge(self, state):
1448 1445 source, revision, kind = state
1449 1446 self._fetch(source, revision)
1450 1447 base = self._gitcommand(['merge-base', revision, self._state[1]])
1451 1448 self._gitupdatestat()
1452 1449 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1453 1450
1454 1451 def mergefunc():
1455 1452 if base == revision:
1456 1453 self.get(state) # fast forward merge
1457 1454 elif base != self._state[1]:
1458 1455 self._gitcommand(['merge', '--no-commit', revision])
1459 1456 _sanitize(self._ui, self._abspath, '.git')
1460 1457
1461 1458 if self.dirty():
1462 1459 if self._gitstate() != revision:
1463 1460 dirty = self._gitstate() == self._state[1] or code != 0
1464 1461 if _updateprompt(self._ui, self, dirty,
1465 1462 self._state[1][:7], revision[:7]):
1466 1463 mergefunc()
1467 1464 else:
1468 1465 mergefunc()
1469 1466
1470 1467 @annotatesubrepoerror
1471 1468 def push(self, opts):
1472 1469 force = opts.get('force')
1473 1470
1474 1471 if not self._state[1]:
1475 1472 return True
1476 1473 if self._gitmissing():
1477 1474 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1478 1475 # if a branch in origin contains the revision, nothing to do
1479 1476 branch2rev, rev2branch = self._gitbranchmap()
1480 1477 if self._state[1] in rev2branch:
1481 1478 for b in rev2branch[self._state[1]]:
1482 1479 if b.startswith('refs/remotes/origin/'):
1483 1480 return True
1484 1481 for b, revision in branch2rev.iteritems():
1485 1482 if b.startswith('refs/remotes/origin/'):
1486 1483 if self._gitisancestor(self._state[1], revision):
1487 1484 return True
1488 1485 # otherwise, try to push the currently checked out branch
1489 1486 cmd = ['push']
1490 1487 if force:
1491 1488 cmd.append('--force')
1492 1489
1493 1490 current = self._gitcurrentbranch()
1494 1491 if current:
1495 1492 # determine if the current branch is even useful
1496 1493 if not self._gitisancestor(self._state[1], current):
1497 1494 self._ui.warn(_('unrelated git branch checked out '
1498 1495 'in subrepo %s\n') % self._relpath)
1499 1496 return False
1500 1497 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1501 1498 (current.split('/', 2)[2], self._relpath))
1502 1499 ret = self._gitdir(cmd + ['origin', current])
1503 1500 return ret[1] == 0
1504 1501 else:
1505 1502 self._ui.warn(_('no branch checked out in subrepo %s\n'
1506 1503 'cannot push revision %s\n') %
1507 1504 (self._relpath, self._state[1]))
1508 1505 return False
1509 1506
1510 1507 @annotatesubrepoerror
1511 1508 def remove(self):
1512 1509 if self._gitmissing():
1513 1510 return
1514 1511 if self.dirty():
1515 1512 self._ui.warn(_('not removing repo %s because '
1516 1513 'it has changes.\n') % self._relpath)
1517 1514 return
1518 1515 # we can't fully delete the repository as it may contain
1519 1516 # local-only history
1520 1517 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1521 1518 self._gitcommand(['config', 'core.bare', 'true'])
1522 1519 for f in os.listdir(self._abspath):
1523 1520 if f == '.git':
1524 1521 continue
1525 1522 path = os.path.join(self._abspath, f)
1526 1523 if os.path.isdir(path) and not os.path.islink(path):
1527 1524 shutil.rmtree(path)
1528 1525 else:
1529 1526 os.remove(path)
1530 1527
1531 1528 def archive(self, ui, archiver, prefix, match=None):
1532 1529 total = 0
1533 1530 source, revision = self._state
1534 1531 if not revision:
1535 1532 return total
1536 1533 self._fetch(source, revision)
1537 1534
1538 1535 # Parse git's native archive command.
1539 1536 # This should be much faster than manually traversing the trees
1540 1537 # and objects with many subprocess calls.
1541 1538 tarstream = self._gitcommand(['archive', revision], stream=True)
1542 1539 tar = tarfile.open(fileobj=tarstream, mode='r|')
1543 1540 relpath = subrelpath(self)
1544 1541 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1545 1542 for i, info in enumerate(tar):
1546 1543 if info.isdir():
1547 1544 continue
1548 1545 if match and not match(info.name):
1549 1546 continue
1550 1547 if info.issym():
1551 1548 data = info.linkname
1552 1549 else:
1553 1550 data = tar.extractfile(info).read()
1554 1551 archiver.addfile(os.path.join(prefix, self._path, info.name),
1555 1552 info.mode, info.issym(), data)
1556 1553 total += 1
1557 1554 ui.progress(_('archiving (%s)') % relpath, i + 1,
1558 1555 unit=_('files'))
1559 1556 ui.progress(_('archiving (%s)') % relpath, None)
1560 1557 return total
1561 1558
1562 1559
1563 1560 @annotatesubrepoerror
1564 1561 def status(self, rev2, **opts):
1565 1562 rev1 = self._state[1]
1566 1563 if self._gitmissing() or not rev1:
1567 1564 # if the repo is missing, return no results
1568 1565 return [], [], [], [], [], [], []
1569 1566 modified, added, removed = [], [], []
1570 1567 self._gitupdatestat()
1571 1568 if rev2:
1572 1569 command = ['diff-tree', rev1, rev2]
1573 1570 else:
1574 1571 command = ['diff-index', rev1]
1575 1572 out = self._gitcommand(command)
1576 1573 for line in out.split('\n'):
1577 1574 tab = line.find('\t')
1578 1575 if tab == -1:
1579 1576 continue
1580 1577 status, f = line[tab - 1], line[tab + 1:]
1581 1578 if status == 'M':
1582 1579 modified.append(f)
1583 1580 elif status == 'A':
1584 1581 added.append(f)
1585 1582 elif status == 'D':
1586 1583 removed.append(f)
1587 1584
1588 1585 deleted = unknown = ignored = clean = []
1589 1586 return modified, added, removed, deleted, unknown, ignored, clean
1590 1587
1591 1588 def shortid(self, revid):
1592 1589 return revid[:7]
1593 1590
1594 1591 types = {
1595 1592 'hg': hgsubrepo,
1596 1593 'svn': svnsubrepo,
1597 1594 'git': gitsubrepo,
1598 1595 }
General Comments 0
You need to be logged in to leave comments. Login now