##// END OF EJS Templates
subrepo: extend git version check to 3 digits...
Mathias De Maré -
r23522:49a58b33 default
parent child Browse files
Show More
@@ -1,1599 +1,1603
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 375 def storeclean(self, path):
376 376 """
377 377 returns true if the repository has not changed since it was last
378 378 cloned from or pushed to a given repository.
379 379 """
380 380 return False
381 381
382 382 def dirty(self, ignoreupdate=False):
383 383 """returns true if the dirstate of the subrepo is dirty or does not
384 384 match current stored state. If ignoreupdate is true, only check
385 385 whether the subrepo has uncommitted changes in its dirstate.
386 386 """
387 387 raise NotImplementedError
388 388
389 389 def basestate(self):
390 390 """current working directory base state, disregarding .hgsubstate
391 391 state and working directory modifications"""
392 392 raise NotImplementedError
393 393
394 394 def checknested(self, path):
395 395 """check if path is a subrepository within this repository"""
396 396 return False
397 397
398 398 def commit(self, text, user, date):
399 399 """commit the current changes to the subrepo with the given
400 400 log message. Use given user and date if possible. Return the
401 401 new state of the subrepo.
402 402 """
403 403 raise NotImplementedError
404 404
405 405 def phase(self, state):
406 406 """returns phase of specified state in the subrepository.
407 407 """
408 408 return phases.public
409 409
410 410 def remove(self):
411 411 """remove the subrepo
412 412
413 413 (should verify the dirstate is not dirty first)
414 414 """
415 415 raise NotImplementedError
416 416
417 417 def get(self, state, overwrite=False):
418 418 """run whatever commands are needed to put the subrepo into
419 419 this state
420 420 """
421 421 raise NotImplementedError
422 422
423 423 def merge(self, state):
424 424 """merge currently-saved state with the new state."""
425 425 raise NotImplementedError
426 426
427 427 def push(self, opts):
428 428 """perform whatever action is analogous to 'hg push'
429 429
430 430 This may be a no-op on some systems.
431 431 """
432 432 raise NotImplementedError
433 433
434 434 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
435 435 return []
436 436
437 437 def cat(self, ui, match, prefix, **opts):
438 438 return 1
439 439
440 440 def status(self, rev2, **opts):
441 441 return scmutil.status([], [], [], [], [], [], [])
442 442
443 443 def diff(self, ui, diffopts, node2, match, prefix, **opts):
444 444 pass
445 445
446 446 def outgoing(self, ui, dest, opts):
447 447 return 1
448 448
449 449 def incoming(self, ui, source, opts):
450 450 return 1
451 451
452 452 def files(self):
453 453 """return filename iterator"""
454 454 raise NotImplementedError
455 455
456 456 def filedata(self, name):
457 457 """return file data"""
458 458 raise NotImplementedError
459 459
460 460 def fileflags(self, name):
461 461 """return file flags"""
462 462 return ''
463 463
464 464 def archive(self, ui, archiver, prefix, match=None):
465 465 if match is not None:
466 466 files = [f for f in self.files() if match(f)]
467 467 else:
468 468 files = self.files()
469 469 total = len(files)
470 470 relpath = subrelpath(self)
471 471 ui.progress(_('archiving (%s)') % relpath, 0,
472 472 unit=_('files'), total=total)
473 473 for i, name in enumerate(files):
474 474 flags = self.fileflags(name)
475 475 mode = 'x' in flags and 0755 or 0644
476 476 symlink = 'l' in flags
477 477 archiver.addfile(os.path.join(prefix, self._path, name),
478 478 mode, symlink, self.filedata(name))
479 479 ui.progress(_('archiving (%s)') % relpath, i + 1,
480 480 unit=_('files'), total=total)
481 481 ui.progress(_('archiving (%s)') % relpath, None)
482 482 return total
483 483
484 484 def walk(self, match):
485 485 '''
486 486 walk recursively through the directory tree, finding all files
487 487 matched by the match function
488 488 '''
489 489 pass
490 490
491 491 def forget(self, ui, match, prefix):
492 492 return ([], [])
493 493
494 494 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
495 495 """remove the matched files from the subrepository and the filesystem,
496 496 possibly by force and/or after the file has been removed from the
497 497 filesystem. Return 0 on success, 1 on any warning.
498 498 """
499 499 return 1
500 500
501 501 def revert(self, ui, substate, *pats, **opts):
502 502 ui.warn('%s: reverting %s subrepos is unsupported\n' \
503 503 % (substate[0], substate[2]))
504 504 return []
505 505
506 506 def shortid(self, revid):
507 507 return revid
508 508
509 509 class hgsubrepo(abstractsubrepo):
510 510 def __init__(self, ctx, path, state):
511 511 self._path = path
512 512 self._state = state
513 513 r = ctx._repo
514 514 root = r.wjoin(path)
515 515 create = not r.wvfs.exists('%s/.hg' % path)
516 516 self._repo = hg.repository(r.baseui, root, create=create)
517 517 for s, k in [('ui', 'commitsubrepos')]:
518 518 v = r.ui.config(s, k)
519 519 if v:
520 520 self._repo.ui.setconfig(s, k, v, 'subrepo')
521 521 self._repo.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
522 522 self._initrepo(r, state[0], create)
523 523
524 524 def storeclean(self, path):
525 525 lock = self._repo.lock()
526 526 try:
527 527 return self._storeclean(path)
528 528 finally:
529 529 lock.release()
530 530
531 531 def _storeclean(self, path):
532 532 clean = True
533 533 itercache = self._calcstorehash(path)
534 534 try:
535 535 for filehash in self._readstorehashcache(path):
536 536 if filehash != itercache.next():
537 537 clean = False
538 538 break
539 539 except StopIteration:
540 540 # the cached and current pull states have a different size
541 541 clean = False
542 542 if clean:
543 543 try:
544 544 itercache.next()
545 545 # the cached and current pull states have a different size
546 546 clean = False
547 547 except StopIteration:
548 548 pass
549 549 return clean
550 550
551 551 def _calcstorehash(self, remotepath):
552 552 '''calculate a unique "store hash"
553 553
554 554 This method is used to to detect when there are changes that may
555 555 require a push to a given remote path.'''
556 556 # sort the files that will be hashed in increasing (likely) file size
557 557 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
558 558 yield '# %s\n' % _expandedabspath(remotepath)
559 559 vfs = self._repo.vfs
560 560 for relname in filelist:
561 561 filehash = util.sha1(vfs.tryread(relname)).hexdigest()
562 562 yield '%s = %s\n' % (relname, filehash)
563 563
564 564 @propertycache
565 565 def _cachestorehashvfs(self):
566 566 return scmutil.vfs(self._repo.join('cache/storehash'))
567 567
568 568 def _readstorehashcache(self, remotepath):
569 569 '''read the store hash cache for a given remote repository'''
570 570 cachefile = _getstorehashcachename(remotepath)
571 571 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
572 572
573 573 def _cachestorehash(self, remotepath):
574 574 '''cache the current store hash
575 575
576 576 Each remote repo requires its own store hash cache, because a subrepo
577 577 store may be "clean" versus a given remote repo, but not versus another
578 578 '''
579 579 cachefile = _getstorehashcachename(remotepath)
580 580 lock = self._repo.lock()
581 581 try:
582 582 storehash = list(self._calcstorehash(remotepath))
583 583 vfs = self._cachestorehashvfs
584 584 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
585 585 finally:
586 586 lock.release()
587 587
588 588 @annotatesubrepoerror
589 589 def _initrepo(self, parentrepo, source, create):
590 590 self._repo._subparent = parentrepo
591 591 self._repo._subsource = source
592 592
593 593 if create:
594 594 lines = ['[paths]\n']
595 595
596 596 def addpathconfig(key, value):
597 597 if value:
598 598 lines.append('%s = %s\n' % (key, value))
599 599 self._repo.ui.setconfig('paths', key, value, 'subrepo')
600 600
601 601 defpath = _abssource(self._repo, abort=False)
602 602 defpushpath = _abssource(self._repo, True, abort=False)
603 603 addpathconfig('default', defpath)
604 604 if defpath != defpushpath:
605 605 addpathconfig('default-push', defpushpath)
606 606
607 607 fp = self._repo.opener("hgrc", "w", text=True)
608 608 try:
609 609 fp.write(''.join(lines))
610 610 finally:
611 611 fp.close()
612 612
613 613 @annotatesubrepoerror
614 614 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
615 615 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
616 616 os.path.join(prefix, self._path), explicitonly)
617 617
618 618 @annotatesubrepoerror
619 619 def cat(self, ui, match, prefix, **opts):
620 620 rev = self._state[1]
621 621 ctx = self._repo[rev]
622 622 return cmdutil.cat(ui, self._repo, ctx, match, prefix, **opts)
623 623
624 624 @annotatesubrepoerror
625 625 def status(self, rev2, **opts):
626 626 try:
627 627 rev1 = self._state[1]
628 628 ctx1 = self._repo[rev1]
629 629 ctx2 = self._repo[rev2]
630 630 return self._repo.status(ctx1, ctx2, **opts)
631 631 except error.RepoLookupError, inst:
632 632 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
633 633 % (inst, subrelpath(self)))
634 634 return scmutil.status([], [], [], [], [], [], [])
635 635
636 636 @annotatesubrepoerror
637 637 def diff(self, ui, diffopts, node2, match, prefix, **opts):
638 638 try:
639 639 node1 = node.bin(self._state[1])
640 640 # We currently expect node2 to come from substate and be
641 641 # in hex format
642 642 if node2 is not None:
643 643 node2 = node.bin(node2)
644 644 cmdutil.diffordiffstat(ui, self._repo, diffopts,
645 645 node1, node2, match,
646 646 prefix=posixpath.join(prefix, self._path),
647 647 listsubrepos=True, **opts)
648 648 except error.RepoLookupError, inst:
649 649 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
650 650 % (inst, subrelpath(self)))
651 651
652 652 @annotatesubrepoerror
653 653 def archive(self, ui, archiver, prefix, match=None):
654 654 self._get(self._state + ('hg',))
655 655 total = abstractsubrepo.archive(self, ui, archiver, prefix, match)
656 656 rev = self._state[1]
657 657 ctx = self._repo[rev]
658 658 for subpath in ctx.substate:
659 659 s = subrepo(ctx, subpath)
660 660 submatch = matchmod.narrowmatcher(subpath, match)
661 661 total += s.archive(
662 662 ui, archiver, os.path.join(prefix, self._path), submatch)
663 663 return total
664 664
665 665 @annotatesubrepoerror
666 666 def dirty(self, ignoreupdate=False):
667 667 r = self._state[1]
668 668 if r == '' and not ignoreupdate: # no state recorded
669 669 return True
670 670 w = self._repo[None]
671 671 if r != w.p1().hex() and not ignoreupdate:
672 672 # different version checked out
673 673 return True
674 674 return w.dirty() # working directory changed
675 675
676 676 def basestate(self):
677 677 return self._repo['.'].hex()
678 678
679 679 def checknested(self, path):
680 680 return self._repo._checknested(self._repo.wjoin(path))
681 681
682 682 @annotatesubrepoerror
683 683 def commit(self, text, user, date):
684 684 # don't bother committing in the subrepo if it's only been
685 685 # updated
686 686 if not self.dirty(True):
687 687 return self._repo['.'].hex()
688 688 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
689 689 n = self._repo.commit(text, user, date)
690 690 if not n:
691 691 return self._repo['.'].hex() # different version checked out
692 692 return node.hex(n)
693 693
694 694 @annotatesubrepoerror
695 695 def phase(self, state):
696 696 return self._repo[state].phase()
697 697
698 698 @annotatesubrepoerror
699 699 def remove(self):
700 700 # we can't fully delete the repository as it may contain
701 701 # local-only history
702 702 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
703 703 hg.clean(self._repo, node.nullid, False)
704 704
705 705 def _get(self, state):
706 706 source, revision, kind = state
707 707 if revision in self._repo.unfiltered():
708 708 return True
709 709 self._repo._subsource = source
710 710 srcurl = _abssource(self._repo)
711 711 other = hg.peer(self._repo, {}, srcurl)
712 712 if len(self._repo) == 0:
713 713 self._repo.ui.status(_('cloning subrepo %s from %s\n')
714 714 % (subrelpath(self), srcurl))
715 715 parentrepo = self._repo._subparent
716 716 shutil.rmtree(self._repo.path)
717 717 other, cloned = hg.clone(self._repo._subparent.baseui, {},
718 718 other, self._repo.root,
719 719 update=False)
720 720 self._repo = cloned.local()
721 721 self._initrepo(parentrepo, source, create=True)
722 722 self._cachestorehash(srcurl)
723 723 else:
724 724 self._repo.ui.status(_('pulling subrepo %s from %s\n')
725 725 % (subrelpath(self), srcurl))
726 726 cleansub = self.storeclean(srcurl)
727 727 exchange.pull(self._repo, other)
728 728 if cleansub:
729 729 # keep the repo clean after pull
730 730 self._cachestorehash(srcurl)
731 731 return False
732 732
733 733 @annotatesubrepoerror
734 734 def get(self, state, overwrite=False):
735 735 inrepo = self._get(state)
736 736 source, revision, kind = state
737 737 repo = self._repo
738 738 repo.ui.debug("getting subrepo %s\n" % self._path)
739 739 if inrepo:
740 740 urepo = repo.unfiltered()
741 741 ctx = urepo[revision]
742 742 if ctx.hidden():
743 743 urepo.ui.warn(
744 744 _('revision %s in subrepo %s is hidden\n') \
745 745 % (revision[0:12], self._path))
746 746 repo = urepo
747 747 hg.updaterepo(repo, revision, overwrite)
748 748
749 749 @annotatesubrepoerror
750 750 def merge(self, state):
751 751 self._get(state)
752 752 cur = self._repo['.']
753 753 dst = self._repo[state[1]]
754 754 anc = dst.ancestor(cur)
755 755
756 756 def mergefunc():
757 757 if anc == cur and dst.branch() == cur.branch():
758 758 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
759 759 hg.update(self._repo, state[1])
760 760 elif anc == dst:
761 761 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
762 762 else:
763 763 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
764 764 hg.merge(self._repo, state[1], remind=False)
765 765
766 766 wctx = self._repo[None]
767 767 if self.dirty():
768 768 if anc != dst:
769 769 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
770 770 mergefunc()
771 771 else:
772 772 mergefunc()
773 773 else:
774 774 mergefunc()
775 775
776 776 @annotatesubrepoerror
777 777 def push(self, opts):
778 778 force = opts.get('force')
779 779 newbranch = opts.get('new_branch')
780 780 ssh = opts.get('ssh')
781 781
782 782 # push subrepos depth-first for coherent ordering
783 783 c = self._repo['']
784 784 subs = c.substate # only repos that are committed
785 785 for s in sorted(subs):
786 786 if c.sub(s).push(opts) == 0:
787 787 return False
788 788
789 789 dsturl = _abssource(self._repo, True)
790 790 if not force:
791 791 if self.storeclean(dsturl):
792 792 self._repo.ui.status(
793 793 _('no changes made to subrepo %s since last push to %s\n')
794 794 % (subrelpath(self), dsturl))
795 795 return None
796 796 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
797 797 (subrelpath(self), dsturl))
798 798 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
799 799 res = exchange.push(self._repo, other, force, newbranch=newbranch)
800 800
801 801 # the repo is now clean
802 802 self._cachestorehash(dsturl)
803 803 return res.cgresult
804 804
805 805 @annotatesubrepoerror
806 806 def outgoing(self, ui, dest, opts):
807 807 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
808 808
809 809 @annotatesubrepoerror
810 810 def incoming(self, ui, source, opts):
811 811 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
812 812
813 813 @annotatesubrepoerror
814 814 def files(self):
815 815 rev = self._state[1]
816 816 ctx = self._repo[rev]
817 817 return ctx.manifest()
818 818
819 819 def filedata(self, name):
820 820 rev = self._state[1]
821 821 return self._repo[rev][name].data()
822 822
823 823 def fileflags(self, name):
824 824 rev = self._state[1]
825 825 ctx = self._repo[rev]
826 826 return ctx.flags(name)
827 827
828 828 def walk(self, match):
829 829 ctx = self._repo[None]
830 830 return ctx.walk(match)
831 831
832 832 @annotatesubrepoerror
833 833 def forget(self, ui, match, prefix):
834 834 return cmdutil.forget(ui, self._repo, match,
835 835 os.path.join(prefix, self._path), True)
836 836
837 837 @annotatesubrepoerror
838 838 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
839 839 return cmdutil.remove(ui, self._repo, matcher,
840 840 os.path.join(prefix, self._path), after, force,
841 841 subrepos)
842 842
843 843 @annotatesubrepoerror
844 844 def revert(self, ui, substate, *pats, **opts):
845 845 # reverting a subrepo is a 2 step process:
846 846 # 1. if the no_backup is not set, revert all modified
847 847 # files inside the subrepo
848 848 # 2. update the subrepo to the revision specified in
849 849 # the corresponding substate dictionary
850 850 ui.status(_('reverting subrepo %s\n') % substate[0])
851 851 if not opts.get('no_backup'):
852 852 # Revert all files on the subrepo, creating backups
853 853 # Note that this will not recursively revert subrepos
854 854 # We could do it if there was a set:subrepos() predicate
855 855 opts = opts.copy()
856 856 opts['date'] = None
857 857 opts['rev'] = substate[1]
858 858
859 859 pats = []
860 860 if not opts.get('all'):
861 861 pats = ['set:modified()']
862 862 self.filerevert(ui, *pats, **opts)
863 863
864 864 # Update the repo to the revision specified in the given substate
865 865 self.get(substate, overwrite=True)
866 866
867 867 def filerevert(self, ui, *pats, **opts):
868 868 ctx = self._repo[opts['rev']]
869 869 parents = self._repo.dirstate.parents()
870 870 if opts.get('all'):
871 871 pats = ['set:modified()']
872 872 else:
873 873 pats = []
874 874 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
875 875
876 876 def shortid(self, revid):
877 877 return revid[:12]
878 878
879 879 class svnsubrepo(abstractsubrepo):
880 880 def __init__(self, ctx, path, state):
881 881 self._path = path
882 882 self._state = state
883 883 self._ctx = ctx
884 884 self._ui = ctx._repo.ui
885 885 self._exe = util.findexe('svn')
886 886 if not self._exe:
887 887 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
888 888 % self._path)
889 889
890 890 def _svncommand(self, commands, filename='', failok=False):
891 891 cmd = [self._exe]
892 892 extrakw = {}
893 893 if not self._ui.interactive():
894 894 # Making stdin be a pipe should prevent svn from behaving
895 895 # interactively even if we can't pass --non-interactive.
896 896 extrakw['stdin'] = subprocess.PIPE
897 897 # Starting in svn 1.5 --non-interactive is a global flag
898 898 # instead of being per-command, but we need to support 1.4 so
899 899 # we have to be intelligent about what commands take
900 900 # --non-interactive.
901 901 if commands[0] in ('update', 'checkout', 'commit'):
902 902 cmd.append('--non-interactive')
903 903 cmd.extend(commands)
904 904 if filename is not None:
905 905 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
906 906 cmd.append(path)
907 907 env = dict(os.environ)
908 908 # Avoid localized output, preserve current locale for everything else.
909 909 lc_all = env.get('LC_ALL')
910 910 if lc_all:
911 911 env['LANG'] = lc_all
912 912 del env['LC_ALL']
913 913 env['LC_MESSAGES'] = 'C'
914 914 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
915 915 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
916 916 universal_newlines=True, env=env, **extrakw)
917 917 stdout, stderr = p.communicate()
918 918 stderr = stderr.strip()
919 919 if not failok:
920 920 if p.returncode:
921 921 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
922 922 if stderr:
923 923 self._ui.warn(stderr + '\n')
924 924 return stdout, stderr
925 925
926 926 @propertycache
927 927 def _svnversion(self):
928 928 output, err = self._svncommand(['--version', '--quiet'], filename=None)
929 929 m = re.search(r'^(\d+)\.(\d+)', output)
930 930 if not m:
931 931 raise util.Abort(_('cannot retrieve svn tool version'))
932 932 return (int(m.group(1)), int(m.group(2)))
933 933
934 934 def _wcrevs(self):
935 935 # Get the working directory revision as well as the last
936 936 # commit revision so we can compare the subrepo state with
937 937 # both. We used to store the working directory one.
938 938 output, err = self._svncommand(['info', '--xml'])
939 939 doc = xml.dom.minidom.parseString(output)
940 940 entries = doc.getElementsByTagName('entry')
941 941 lastrev, rev = '0', '0'
942 942 if entries:
943 943 rev = str(entries[0].getAttribute('revision')) or '0'
944 944 commits = entries[0].getElementsByTagName('commit')
945 945 if commits:
946 946 lastrev = str(commits[0].getAttribute('revision')) or '0'
947 947 return (lastrev, rev)
948 948
949 949 def _wcrev(self):
950 950 return self._wcrevs()[0]
951 951
952 952 def _wcchanged(self):
953 953 """Return (changes, extchanges, missing) where changes is True
954 954 if the working directory was changed, extchanges is
955 955 True if any of these changes concern an external entry and missing
956 956 is True if any change is a missing entry.
957 957 """
958 958 output, err = self._svncommand(['status', '--xml'])
959 959 externals, changes, missing = [], [], []
960 960 doc = xml.dom.minidom.parseString(output)
961 961 for e in doc.getElementsByTagName('entry'):
962 962 s = e.getElementsByTagName('wc-status')
963 963 if not s:
964 964 continue
965 965 item = s[0].getAttribute('item')
966 966 props = s[0].getAttribute('props')
967 967 path = e.getAttribute('path')
968 968 if item == 'external':
969 969 externals.append(path)
970 970 elif item == 'missing':
971 971 missing.append(path)
972 972 if (item not in ('', 'normal', 'unversioned', 'external')
973 973 or props not in ('', 'none', 'normal')):
974 974 changes.append(path)
975 975 for path in changes:
976 976 for ext in externals:
977 977 if path == ext or path.startswith(ext + os.sep):
978 978 return True, True, bool(missing)
979 979 return bool(changes), False, bool(missing)
980 980
981 981 def dirty(self, ignoreupdate=False):
982 982 if not self._wcchanged()[0]:
983 983 if self._state[1] in self._wcrevs() or ignoreupdate:
984 984 return False
985 985 return True
986 986
987 987 def basestate(self):
988 988 lastrev, rev = self._wcrevs()
989 989 if lastrev != rev:
990 990 # Last committed rev is not the same than rev. We would
991 991 # like to take lastrev but we do not know if the subrepo
992 992 # URL exists at lastrev. Test it and fallback to rev it
993 993 # is not there.
994 994 try:
995 995 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
996 996 return lastrev
997 997 except error.Abort:
998 998 pass
999 999 return rev
1000 1000
1001 1001 @annotatesubrepoerror
1002 1002 def commit(self, text, user, date):
1003 1003 # user and date are out of our hands since svn is centralized
1004 1004 changed, extchanged, missing = self._wcchanged()
1005 1005 if not changed:
1006 1006 return self.basestate()
1007 1007 if extchanged:
1008 1008 # Do not try to commit externals
1009 1009 raise util.Abort(_('cannot commit svn externals'))
1010 1010 if missing:
1011 1011 # svn can commit with missing entries but aborting like hg
1012 1012 # seems a better approach.
1013 1013 raise util.Abort(_('cannot commit missing svn entries'))
1014 1014 commitinfo, err = self._svncommand(['commit', '-m', text])
1015 1015 self._ui.status(commitinfo)
1016 1016 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1017 1017 if not newrev:
1018 1018 if not commitinfo.strip():
1019 1019 # Sometimes, our definition of "changed" differs from
1020 1020 # svn one. For instance, svn ignores missing files
1021 1021 # when committing. If there are only missing files, no
1022 1022 # commit is made, no output and no error code.
1023 1023 raise util.Abort(_('failed to commit svn changes'))
1024 1024 raise util.Abort(commitinfo.splitlines()[-1])
1025 1025 newrev = newrev.groups()[0]
1026 1026 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
1027 1027 return newrev
1028 1028
1029 1029 @annotatesubrepoerror
1030 1030 def remove(self):
1031 1031 if self.dirty():
1032 1032 self._ui.warn(_('not removing repo %s because '
1033 1033 'it has changes.\n') % self._path)
1034 1034 return
1035 1035 self._ui.note(_('removing subrepo %s\n') % self._path)
1036 1036
1037 1037 def onerror(function, path, excinfo):
1038 1038 if function is not os.remove:
1039 1039 raise
1040 1040 # read-only files cannot be unlinked under Windows
1041 1041 s = os.stat(path)
1042 1042 if (s.st_mode & stat.S_IWRITE) != 0:
1043 1043 raise
1044 1044 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1045 1045 os.remove(path)
1046 1046
1047 1047 path = self._ctx._repo.wjoin(self._path)
1048 1048 shutil.rmtree(path, onerror=onerror)
1049 1049 try:
1050 1050 os.removedirs(os.path.dirname(path))
1051 1051 except OSError:
1052 1052 pass
1053 1053
1054 1054 @annotatesubrepoerror
1055 1055 def get(self, state, overwrite=False):
1056 1056 if overwrite:
1057 1057 self._svncommand(['revert', '--recursive'])
1058 1058 args = ['checkout']
1059 1059 if self._svnversion >= (1, 5):
1060 1060 args.append('--force')
1061 1061 # The revision must be specified at the end of the URL to properly
1062 1062 # update to a directory which has since been deleted and recreated.
1063 1063 args.append('%s@%s' % (state[0], state[1]))
1064 1064 status, err = self._svncommand(args, failok=True)
1065 1065 _sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn')
1066 1066 if not re.search('Checked out revision [0-9]+.', status):
1067 1067 if ('is already a working copy for a different URL' in err
1068 1068 and (self._wcchanged()[:2] == (False, False))):
1069 1069 # obstructed but clean working copy, so just blow it away.
1070 1070 self.remove()
1071 1071 self.get(state, overwrite=False)
1072 1072 return
1073 1073 raise util.Abort((status or err).splitlines()[-1])
1074 1074 self._ui.status(status)
1075 1075
1076 1076 @annotatesubrepoerror
1077 1077 def merge(self, state):
1078 1078 old = self._state[1]
1079 1079 new = state[1]
1080 1080 wcrev = self._wcrev()
1081 1081 if new != wcrev:
1082 1082 dirty = old == wcrev or self._wcchanged()[0]
1083 1083 if _updateprompt(self._ui, self, dirty, wcrev, new):
1084 1084 self.get(state, False)
1085 1085
1086 1086 def push(self, opts):
1087 1087 # push is a no-op for SVN
1088 1088 return True
1089 1089
1090 1090 @annotatesubrepoerror
1091 1091 def files(self):
1092 1092 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1093 1093 doc = xml.dom.minidom.parseString(output)
1094 1094 paths = []
1095 1095 for e in doc.getElementsByTagName('entry'):
1096 1096 kind = str(e.getAttribute('kind'))
1097 1097 if kind != 'file':
1098 1098 continue
1099 1099 name = ''.join(c.data for c
1100 1100 in e.getElementsByTagName('name')[0].childNodes
1101 1101 if c.nodeType == c.TEXT_NODE)
1102 1102 paths.append(name.encode('utf-8'))
1103 1103 return paths
1104 1104
1105 1105 def filedata(self, name):
1106 1106 return self._svncommand(['cat'], name)[0]
1107 1107
1108 1108
1109 1109 class gitsubrepo(abstractsubrepo):
1110 1110 def __init__(self, ctx, path, state):
1111 1111 self._state = state
1112 1112 self._ctx = ctx
1113 1113 self._path = path
1114 1114 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1115 1115 self._abspath = ctx._repo.wjoin(path)
1116 1116 self._subparent = ctx._repo
1117 1117 self._ui = ctx._repo.ui
1118 1118 self._ensuregit()
1119 1119
1120 1120 def _ensuregit(self):
1121 1121 try:
1122 1122 self._gitexecutable = 'git'
1123 1123 out, err = self._gitnodir(['--version'])
1124 1124 except OSError, e:
1125 1125 if e.errno != 2 or os.name != 'nt':
1126 1126 raise
1127 1127 self._gitexecutable = 'git.cmd'
1128 1128 out, err = self._gitnodir(['--version'])
1129 1129 versionstatus = self._checkversion(out)
1130 1130 if versionstatus == 'unknown':
1131 1131 self._ui.warn(_('cannot retrieve git version\n'))
1132 1132 elif versionstatus == 'abort':
1133 1133 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1134 1134 elif versionstatus == 'warning':
1135 1135 self._ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1136 1136
1137 1137 @staticmethod
1138 1138 def _gitversion(out):
1139 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1140 if m:
1141 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1142
1139 1143 m = re.search(r'^git version (\d+)\.(\d+)', out)
1140 1144 if m:
1141 return (int(m.group(1)), int(m.group(2)))
1145 return (int(m.group(1)), int(m.group(2)), 0)
1142 1146
1143 1147 return -1
1144 1148
1145 1149 @staticmethod
1146 1150 def _checkversion(out):
1147 1151 '''ensure git version is new enough
1148 1152
1149 1153 >>> _checkversion = gitsubrepo._checkversion
1150 1154 >>> _checkversion('git version 1.6.0')
1151 1155 'ok'
1152 1156 >>> _checkversion('git version 1.8.5')
1153 1157 'ok'
1154 1158 >>> _checkversion('git version 1.4.0')
1155 1159 'abort'
1156 1160 >>> _checkversion('git version 1.5.0')
1157 1161 'warning'
1158 1162 >>> _checkversion('git version 1.9-rc0')
1159 1163 'ok'
1160 1164 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1161 1165 'ok'
1162 1166 >>> _checkversion('git version 1.9.0.GIT')
1163 1167 'ok'
1164 1168 >>> _checkversion('git version 12345')
1165 1169 'unknown'
1166 1170 >>> _checkversion('no')
1167 1171 'unknown'
1168 1172 '''
1169 1173 version = gitsubrepo._gitversion(out)
1170 1174 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1171 1175 # despite the docstring comment. For now, error on 1.4.0, warn on
1172 1176 # 1.5.0 but attempt to continue.
1173 1177 if version == -1:
1174 1178 return 'unknown'
1175 if version < (1, 5):
1179 if version < (1, 5, 0):
1176 1180 return 'abort'
1177 elif version < (1, 6):
1181 elif version < (1, 6, 0):
1178 1182 return 'warning'
1179 1183 return 'ok'
1180 1184
1181 1185 def _gitcommand(self, commands, env=None, stream=False):
1182 1186 return self._gitdir(commands, env=env, stream=stream)[0]
1183 1187
1184 1188 def _gitdir(self, commands, env=None, stream=False):
1185 1189 return self._gitnodir(commands, env=env, stream=stream,
1186 1190 cwd=self._abspath)
1187 1191
1188 1192 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1189 1193 """Calls the git command
1190 1194
1191 1195 The methods tries to call the git command. versions prior to 1.6.0
1192 1196 are not supported and very probably fail.
1193 1197 """
1194 1198 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1195 1199 # unless ui.quiet is set, print git's stderr,
1196 1200 # which is mostly progress and useful info
1197 1201 errpipe = None
1198 1202 if self._ui.quiet:
1199 1203 errpipe = open(os.devnull, 'w')
1200 1204 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1201 1205 cwd=cwd, env=env, close_fds=util.closefds,
1202 1206 stdout=subprocess.PIPE, stderr=errpipe)
1203 1207 if stream:
1204 1208 return p.stdout, None
1205 1209
1206 1210 retdata = p.stdout.read().strip()
1207 1211 # wait for the child to exit to avoid race condition.
1208 1212 p.wait()
1209 1213
1210 1214 if p.returncode != 0 and p.returncode != 1:
1211 1215 # there are certain error codes that are ok
1212 1216 command = commands[0]
1213 1217 if command in ('cat-file', 'symbolic-ref'):
1214 1218 return retdata, p.returncode
1215 1219 # for all others, abort
1216 1220 raise util.Abort('git %s error %d in %s' %
1217 1221 (command, p.returncode, self._relpath))
1218 1222
1219 1223 return retdata, p.returncode
1220 1224
1221 1225 def _gitmissing(self):
1222 1226 return not os.path.exists(os.path.join(self._abspath, '.git'))
1223 1227
1224 1228 def _gitstate(self):
1225 1229 return self._gitcommand(['rev-parse', 'HEAD'])
1226 1230
1227 1231 def _gitcurrentbranch(self):
1228 1232 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1229 1233 if err:
1230 1234 current = None
1231 1235 return current
1232 1236
1233 1237 def _gitremote(self, remote):
1234 1238 out = self._gitcommand(['remote', 'show', '-n', remote])
1235 1239 line = out.split('\n')[1]
1236 1240 i = line.index('URL: ') + len('URL: ')
1237 1241 return line[i:]
1238 1242
1239 1243 def _githavelocally(self, revision):
1240 1244 out, code = self._gitdir(['cat-file', '-e', revision])
1241 1245 return code == 0
1242 1246
1243 1247 def _gitisancestor(self, r1, r2):
1244 1248 base = self._gitcommand(['merge-base', r1, r2])
1245 1249 return base == r1
1246 1250
1247 1251 def _gitisbare(self):
1248 1252 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1249 1253
1250 1254 def _gitupdatestat(self):
1251 1255 """This must be run before git diff-index.
1252 1256 diff-index only looks at changes to file stat;
1253 1257 this command looks at file contents and updates the stat."""
1254 1258 self._gitcommand(['update-index', '-q', '--refresh'])
1255 1259
1256 1260 def _gitbranchmap(self):
1257 1261 '''returns 2 things:
1258 1262 a map from git branch to revision
1259 1263 a map from revision to branches'''
1260 1264 branch2rev = {}
1261 1265 rev2branch = {}
1262 1266
1263 1267 out = self._gitcommand(['for-each-ref', '--format',
1264 1268 '%(objectname) %(refname)'])
1265 1269 for line in out.split('\n'):
1266 1270 revision, ref = line.split(' ')
1267 1271 if (not ref.startswith('refs/heads/') and
1268 1272 not ref.startswith('refs/remotes/')):
1269 1273 continue
1270 1274 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1271 1275 continue # ignore remote/HEAD redirects
1272 1276 branch2rev[ref] = revision
1273 1277 rev2branch.setdefault(revision, []).append(ref)
1274 1278 return branch2rev, rev2branch
1275 1279
1276 1280 def _gittracking(self, branches):
1277 1281 'return map of remote branch to local tracking branch'
1278 1282 # assumes no more than one local tracking branch for each remote
1279 1283 tracking = {}
1280 1284 for b in branches:
1281 1285 if b.startswith('refs/remotes/'):
1282 1286 continue
1283 1287 bname = b.split('/', 2)[2]
1284 1288 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1285 1289 if remote:
1286 1290 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1287 1291 tracking['refs/remotes/%s/%s' %
1288 1292 (remote, ref.split('/', 2)[2])] = b
1289 1293 return tracking
1290 1294
1291 1295 def _abssource(self, source):
1292 1296 if '://' not in source:
1293 1297 # recognize the scp syntax as an absolute source
1294 1298 colon = source.find(':')
1295 1299 if colon != -1 and '/' not in source[:colon]:
1296 1300 return source
1297 1301 self._subsource = source
1298 1302 return _abssource(self)
1299 1303
1300 1304 def _fetch(self, source, revision):
1301 1305 if self._gitmissing():
1302 1306 source = self._abssource(source)
1303 1307 self._ui.status(_('cloning subrepo %s from %s\n') %
1304 1308 (self._relpath, source))
1305 1309 self._gitnodir(['clone', source, self._abspath])
1306 1310 if self._githavelocally(revision):
1307 1311 return
1308 1312 self._ui.status(_('pulling subrepo %s from %s\n') %
1309 1313 (self._relpath, self._gitremote('origin')))
1310 1314 # try only origin: the originally cloned repo
1311 1315 self._gitcommand(['fetch'])
1312 1316 if not self._githavelocally(revision):
1313 1317 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1314 1318 (revision, self._relpath))
1315 1319
1316 1320 @annotatesubrepoerror
1317 1321 def dirty(self, ignoreupdate=False):
1318 1322 if self._gitmissing():
1319 1323 return self._state[1] != ''
1320 1324 if self._gitisbare():
1321 1325 return True
1322 1326 if not ignoreupdate and self._state[1] != self._gitstate():
1323 1327 # different version checked out
1324 1328 return True
1325 1329 # check for staged changes or modified files; ignore untracked files
1326 1330 self._gitupdatestat()
1327 1331 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1328 1332 return code == 1
1329 1333
1330 1334 def basestate(self):
1331 1335 return self._gitstate()
1332 1336
1333 1337 @annotatesubrepoerror
1334 1338 def get(self, state, overwrite=False):
1335 1339 source, revision, kind = state
1336 1340 if not revision:
1337 1341 self.remove()
1338 1342 return
1339 1343 self._fetch(source, revision)
1340 1344 # if the repo was set to be bare, unbare it
1341 1345 if self._gitisbare():
1342 1346 self._gitcommand(['config', 'core.bare', 'false'])
1343 1347 if self._gitstate() == revision:
1344 1348 self._gitcommand(['reset', '--hard', 'HEAD'])
1345 1349 return
1346 1350 elif self._gitstate() == revision:
1347 1351 if overwrite:
1348 1352 # first reset the index to unmark new files for commit, because
1349 1353 # reset --hard will otherwise throw away files added for commit,
1350 1354 # not just unmark them.
1351 1355 self._gitcommand(['reset', 'HEAD'])
1352 1356 self._gitcommand(['reset', '--hard', 'HEAD'])
1353 1357 return
1354 1358 branch2rev, rev2branch = self._gitbranchmap()
1355 1359
1356 1360 def checkout(args):
1357 1361 cmd = ['checkout']
1358 1362 if overwrite:
1359 1363 # first reset the index to unmark new files for commit, because
1360 1364 # the -f option will otherwise throw away files added for
1361 1365 # commit, not just unmark them.
1362 1366 self._gitcommand(['reset', 'HEAD'])
1363 1367 cmd.append('-f')
1364 1368 self._gitcommand(cmd + args)
1365 1369 _sanitize(self._ui, self._abspath, '.git')
1366 1370
1367 1371 def rawcheckout():
1368 1372 # no branch to checkout, check it out with no branch
1369 1373 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1370 1374 self._relpath)
1371 1375 self._ui.warn(_('check out a git branch if you intend '
1372 1376 'to make changes\n'))
1373 1377 checkout(['-q', revision])
1374 1378
1375 1379 if revision not in rev2branch:
1376 1380 rawcheckout()
1377 1381 return
1378 1382 branches = rev2branch[revision]
1379 1383 firstlocalbranch = None
1380 1384 for b in branches:
1381 1385 if b == 'refs/heads/master':
1382 1386 # master trumps all other branches
1383 1387 checkout(['refs/heads/master'])
1384 1388 return
1385 1389 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1386 1390 firstlocalbranch = b
1387 1391 if firstlocalbranch:
1388 1392 checkout([firstlocalbranch])
1389 1393 return
1390 1394
1391 1395 tracking = self._gittracking(branch2rev.keys())
1392 1396 # choose a remote branch already tracked if possible
1393 1397 remote = branches[0]
1394 1398 if remote not in tracking:
1395 1399 for b in branches:
1396 1400 if b in tracking:
1397 1401 remote = b
1398 1402 break
1399 1403
1400 1404 if remote not in tracking:
1401 1405 # create a new local tracking branch
1402 1406 local = remote.split('/', 3)[3]
1403 1407 checkout(['-b', local, remote])
1404 1408 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1405 1409 # When updating to a tracked remote branch,
1406 1410 # if the local tracking branch is downstream of it,
1407 1411 # a normal `git pull` would have performed a "fast-forward merge"
1408 1412 # which is equivalent to updating the local branch to the remote.
1409 1413 # Since we are only looking at branching at update, we need to
1410 1414 # detect this situation and perform this action lazily.
1411 1415 if tracking[remote] != self._gitcurrentbranch():
1412 1416 checkout([tracking[remote]])
1413 1417 self._gitcommand(['merge', '--ff', remote])
1414 1418 _sanitize(self._ui, self._abspath, '.git')
1415 1419 else:
1416 1420 # a real merge would be required, just checkout the revision
1417 1421 rawcheckout()
1418 1422
1419 1423 @annotatesubrepoerror
1420 1424 def commit(self, text, user, date):
1421 1425 if self._gitmissing():
1422 1426 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1423 1427 cmd = ['commit', '-a', '-m', text]
1424 1428 env = os.environ.copy()
1425 1429 if user:
1426 1430 cmd += ['--author', user]
1427 1431 if date:
1428 1432 # git's date parser silently ignores when seconds < 1e9
1429 1433 # convert to ISO8601
1430 1434 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1431 1435 '%Y-%m-%dT%H:%M:%S %1%2')
1432 1436 self._gitcommand(cmd, env=env)
1433 1437 # make sure commit works otherwise HEAD might not exist under certain
1434 1438 # circumstances
1435 1439 return self._gitstate()
1436 1440
1437 1441 @annotatesubrepoerror
1438 1442 def merge(self, state):
1439 1443 source, revision, kind = state
1440 1444 self._fetch(source, revision)
1441 1445 base = self._gitcommand(['merge-base', revision, self._state[1]])
1442 1446 self._gitupdatestat()
1443 1447 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1444 1448
1445 1449 def mergefunc():
1446 1450 if base == revision:
1447 1451 self.get(state) # fast forward merge
1448 1452 elif base != self._state[1]:
1449 1453 self._gitcommand(['merge', '--no-commit', revision])
1450 1454 _sanitize(self._ui, self._abspath, '.git')
1451 1455
1452 1456 if self.dirty():
1453 1457 if self._gitstate() != revision:
1454 1458 dirty = self._gitstate() == self._state[1] or code != 0
1455 1459 if _updateprompt(self._ui, self, dirty,
1456 1460 self._state[1][:7], revision[:7]):
1457 1461 mergefunc()
1458 1462 else:
1459 1463 mergefunc()
1460 1464
1461 1465 @annotatesubrepoerror
1462 1466 def push(self, opts):
1463 1467 force = opts.get('force')
1464 1468
1465 1469 if not self._state[1]:
1466 1470 return True
1467 1471 if self._gitmissing():
1468 1472 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1469 1473 # if a branch in origin contains the revision, nothing to do
1470 1474 branch2rev, rev2branch = self._gitbranchmap()
1471 1475 if self._state[1] in rev2branch:
1472 1476 for b in rev2branch[self._state[1]]:
1473 1477 if b.startswith('refs/remotes/origin/'):
1474 1478 return True
1475 1479 for b, revision in branch2rev.iteritems():
1476 1480 if b.startswith('refs/remotes/origin/'):
1477 1481 if self._gitisancestor(self._state[1], revision):
1478 1482 return True
1479 1483 # otherwise, try to push the currently checked out branch
1480 1484 cmd = ['push']
1481 1485 if force:
1482 1486 cmd.append('--force')
1483 1487
1484 1488 current = self._gitcurrentbranch()
1485 1489 if current:
1486 1490 # determine if the current branch is even useful
1487 1491 if not self._gitisancestor(self._state[1], current):
1488 1492 self._ui.warn(_('unrelated git branch checked out '
1489 1493 'in subrepo %s\n') % self._relpath)
1490 1494 return False
1491 1495 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1492 1496 (current.split('/', 2)[2], self._relpath))
1493 1497 ret = self._gitdir(cmd + ['origin', current])
1494 1498 return ret[1] == 0
1495 1499 else:
1496 1500 self._ui.warn(_('no branch checked out in subrepo %s\n'
1497 1501 'cannot push revision %s\n') %
1498 1502 (self._relpath, self._state[1]))
1499 1503 return False
1500 1504
1501 1505 @annotatesubrepoerror
1502 1506 def remove(self):
1503 1507 if self._gitmissing():
1504 1508 return
1505 1509 if self.dirty():
1506 1510 self._ui.warn(_('not removing repo %s because '
1507 1511 'it has changes.\n') % self._relpath)
1508 1512 return
1509 1513 # we can't fully delete the repository as it may contain
1510 1514 # local-only history
1511 1515 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1512 1516 self._gitcommand(['config', 'core.bare', 'true'])
1513 1517 for f in os.listdir(self._abspath):
1514 1518 if f == '.git':
1515 1519 continue
1516 1520 path = os.path.join(self._abspath, f)
1517 1521 if os.path.isdir(path) and not os.path.islink(path):
1518 1522 shutil.rmtree(path)
1519 1523 else:
1520 1524 os.remove(path)
1521 1525
1522 1526 def archive(self, ui, archiver, prefix, match=None):
1523 1527 total = 0
1524 1528 source, revision = self._state
1525 1529 if not revision:
1526 1530 return total
1527 1531 self._fetch(source, revision)
1528 1532
1529 1533 # Parse git's native archive command.
1530 1534 # This should be much faster than manually traversing the trees
1531 1535 # and objects with many subprocess calls.
1532 1536 tarstream = self._gitcommand(['archive', revision], stream=True)
1533 1537 tar = tarfile.open(fileobj=tarstream, mode='r|')
1534 1538 relpath = subrelpath(self)
1535 1539 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1536 1540 for i, info in enumerate(tar):
1537 1541 if info.isdir():
1538 1542 continue
1539 1543 if match and not match(info.name):
1540 1544 continue
1541 1545 if info.issym():
1542 1546 data = info.linkname
1543 1547 else:
1544 1548 data = tar.extractfile(info).read()
1545 1549 archiver.addfile(os.path.join(prefix, self._path, info.name),
1546 1550 info.mode, info.issym(), data)
1547 1551 total += 1
1548 1552 ui.progress(_('archiving (%s)') % relpath, i + 1,
1549 1553 unit=_('files'))
1550 1554 ui.progress(_('archiving (%s)') % relpath, None)
1551 1555 return total
1552 1556
1553 1557
1554 1558 @annotatesubrepoerror
1555 1559 def status(self, rev2, **opts):
1556 1560 rev1 = self._state[1]
1557 1561 if self._gitmissing() or not rev1:
1558 1562 # if the repo is missing, return no results
1559 1563 return [], [], [], [], [], [], []
1560 1564 modified, added, removed = [], [], []
1561 1565 self._gitupdatestat()
1562 1566 if rev2:
1563 1567 command = ['diff-tree', rev1, rev2]
1564 1568 else:
1565 1569 command = ['diff-index', rev1]
1566 1570 out = self._gitcommand(command)
1567 1571 for line in out.split('\n'):
1568 1572 tab = line.find('\t')
1569 1573 if tab == -1:
1570 1574 continue
1571 1575 status, f = line[tab - 1], line[tab + 1:]
1572 1576 if status == 'M':
1573 1577 modified.append(f)
1574 1578 elif status == 'A':
1575 1579 added.append(f)
1576 1580 elif status == 'D':
1577 1581 removed.append(f)
1578 1582
1579 1583 deleted, unknown, ignored, clean = [], [], [], []
1580 1584
1581 1585 if not rev2:
1582 1586 command = ['ls-files', '--others', '--exclude-standard']
1583 1587 out = self._gitcommand(command)
1584 1588 for line in out.split('\n'):
1585 1589 if len(line) == 0:
1586 1590 continue
1587 1591 unknown.append(line)
1588 1592
1589 1593 return scmutil.status(modified, added, removed, deleted,
1590 1594 unknown, ignored, clean)
1591 1595
1592 1596 def shortid(self, revid):
1593 1597 return revid[:7]
1594 1598
1595 1599 types = {
1596 1600 'hg': hgsubrepo,
1597 1601 'svn': svnsubrepo,
1598 1602 'git': gitsubrepo,
1599 1603 }
General Comments 0
You need to be logged in to leave comments. Login now