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