##// END OF EJS Templates
subrepo: move git version check into a separate method...
Mathias De Maré -
r23521:f5de2a82 default
parent child Browse files
Show More
@@ -1,1592 +1,1599 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 4 #
5 5 # This software may be used and distributed according to the terms of the
6 6 # GNU General Public License version 2 or any later version.
7 7
8 8 import errno, os, re, shutil, posixpath, sys
9 9 import xml.dom.minidom
10 10 import stat, subprocess, tarfile
11 11 from i18n import _
12 12 import config, util, node, error, cmdutil, 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 def _gitversion(out):
1139 m = re.search(r'^git version (\d+)\.(\d+)', out)
1140 if m:
1141 return (int(m.group(1)), int(m.group(2)))
1142
1143 return -1
1144
1145 @staticmethod
1138 1146 def _checkversion(out):
1139 1147 '''ensure git version is new enough
1140 1148
1141 1149 >>> _checkversion = gitsubrepo._checkversion
1142 1150 >>> _checkversion('git version 1.6.0')
1143 1151 'ok'
1144 1152 >>> _checkversion('git version 1.8.5')
1145 1153 'ok'
1146 1154 >>> _checkversion('git version 1.4.0')
1147 1155 'abort'
1148 1156 >>> _checkversion('git version 1.5.0')
1149 1157 'warning'
1150 1158 >>> _checkversion('git version 1.9-rc0')
1151 1159 'ok'
1152 1160 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1153 1161 'ok'
1154 1162 >>> _checkversion('git version 1.9.0.GIT')
1155 1163 'ok'
1156 1164 >>> _checkversion('git version 12345')
1157 1165 'unknown'
1158 1166 >>> _checkversion('no')
1159 1167 'unknown'
1160 1168 '''
1161 m = re.search(r'^git version (\d+)\.(\d+)', out)
1162 if not m:
1163 return 'unknown'
1164 version = (int(m.group(1)), int(m.group(2)))
1169 version = gitsubrepo._gitversion(out)
1165 1170 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1166 1171 # despite the docstring comment. For now, error on 1.4.0, warn on
1167 1172 # 1.5.0 but attempt to continue.
1173 if version == -1:
1174 return 'unknown'
1168 1175 if version < (1, 5):
1169 1176 return 'abort'
1170 1177 elif version < (1, 6):
1171 1178 return 'warning'
1172 1179 return 'ok'
1173 1180
1174 1181 def _gitcommand(self, commands, env=None, stream=False):
1175 1182 return self._gitdir(commands, env=env, stream=stream)[0]
1176 1183
1177 1184 def _gitdir(self, commands, env=None, stream=False):
1178 1185 return self._gitnodir(commands, env=env, stream=stream,
1179 1186 cwd=self._abspath)
1180 1187
1181 1188 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1182 1189 """Calls the git command
1183 1190
1184 1191 The methods tries to call the git command. versions prior to 1.6.0
1185 1192 are not supported and very probably fail.
1186 1193 """
1187 1194 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1188 1195 # unless ui.quiet is set, print git's stderr,
1189 1196 # which is mostly progress and useful info
1190 1197 errpipe = None
1191 1198 if self._ui.quiet:
1192 1199 errpipe = open(os.devnull, 'w')
1193 1200 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1194 1201 cwd=cwd, env=env, close_fds=util.closefds,
1195 1202 stdout=subprocess.PIPE, stderr=errpipe)
1196 1203 if stream:
1197 1204 return p.stdout, None
1198 1205
1199 1206 retdata = p.stdout.read().strip()
1200 1207 # wait for the child to exit to avoid race condition.
1201 1208 p.wait()
1202 1209
1203 1210 if p.returncode != 0 and p.returncode != 1:
1204 1211 # there are certain error codes that are ok
1205 1212 command = commands[0]
1206 1213 if command in ('cat-file', 'symbolic-ref'):
1207 1214 return retdata, p.returncode
1208 1215 # for all others, abort
1209 1216 raise util.Abort('git %s error %d in %s' %
1210 1217 (command, p.returncode, self._relpath))
1211 1218
1212 1219 return retdata, p.returncode
1213 1220
1214 1221 def _gitmissing(self):
1215 1222 return not os.path.exists(os.path.join(self._abspath, '.git'))
1216 1223
1217 1224 def _gitstate(self):
1218 1225 return self._gitcommand(['rev-parse', 'HEAD'])
1219 1226
1220 1227 def _gitcurrentbranch(self):
1221 1228 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1222 1229 if err:
1223 1230 current = None
1224 1231 return current
1225 1232
1226 1233 def _gitremote(self, remote):
1227 1234 out = self._gitcommand(['remote', 'show', '-n', remote])
1228 1235 line = out.split('\n')[1]
1229 1236 i = line.index('URL: ') + len('URL: ')
1230 1237 return line[i:]
1231 1238
1232 1239 def _githavelocally(self, revision):
1233 1240 out, code = self._gitdir(['cat-file', '-e', revision])
1234 1241 return code == 0
1235 1242
1236 1243 def _gitisancestor(self, r1, r2):
1237 1244 base = self._gitcommand(['merge-base', r1, r2])
1238 1245 return base == r1
1239 1246
1240 1247 def _gitisbare(self):
1241 1248 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1242 1249
1243 1250 def _gitupdatestat(self):
1244 1251 """This must be run before git diff-index.
1245 1252 diff-index only looks at changes to file stat;
1246 1253 this command looks at file contents and updates the stat."""
1247 1254 self._gitcommand(['update-index', '-q', '--refresh'])
1248 1255
1249 1256 def _gitbranchmap(self):
1250 1257 '''returns 2 things:
1251 1258 a map from git branch to revision
1252 1259 a map from revision to branches'''
1253 1260 branch2rev = {}
1254 1261 rev2branch = {}
1255 1262
1256 1263 out = self._gitcommand(['for-each-ref', '--format',
1257 1264 '%(objectname) %(refname)'])
1258 1265 for line in out.split('\n'):
1259 1266 revision, ref = line.split(' ')
1260 1267 if (not ref.startswith('refs/heads/') and
1261 1268 not ref.startswith('refs/remotes/')):
1262 1269 continue
1263 1270 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1264 1271 continue # ignore remote/HEAD redirects
1265 1272 branch2rev[ref] = revision
1266 1273 rev2branch.setdefault(revision, []).append(ref)
1267 1274 return branch2rev, rev2branch
1268 1275
1269 1276 def _gittracking(self, branches):
1270 1277 'return map of remote branch to local tracking branch'
1271 1278 # assumes no more than one local tracking branch for each remote
1272 1279 tracking = {}
1273 1280 for b in branches:
1274 1281 if b.startswith('refs/remotes/'):
1275 1282 continue
1276 1283 bname = b.split('/', 2)[2]
1277 1284 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1278 1285 if remote:
1279 1286 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1280 1287 tracking['refs/remotes/%s/%s' %
1281 1288 (remote, ref.split('/', 2)[2])] = b
1282 1289 return tracking
1283 1290
1284 1291 def _abssource(self, source):
1285 1292 if '://' not in source:
1286 1293 # recognize the scp syntax as an absolute source
1287 1294 colon = source.find(':')
1288 1295 if colon != -1 and '/' not in source[:colon]:
1289 1296 return source
1290 1297 self._subsource = source
1291 1298 return _abssource(self)
1292 1299
1293 1300 def _fetch(self, source, revision):
1294 1301 if self._gitmissing():
1295 1302 source = self._abssource(source)
1296 1303 self._ui.status(_('cloning subrepo %s from %s\n') %
1297 1304 (self._relpath, source))
1298 1305 self._gitnodir(['clone', source, self._abspath])
1299 1306 if self._githavelocally(revision):
1300 1307 return
1301 1308 self._ui.status(_('pulling subrepo %s from %s\n') %
1302 1309 (self._relpath, self._gitremote('origin')))
1303 1310 # try only origin: the originally cloned repo
1304 1311 self._gitcommand(['fetch'])
1305 1312 if not self._githavelocally(revision):
1306 1313 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1307 1314 (revision, self._relpath))
1308 1315
1309 1316 @annotatesubrepoerror
1310 1317 def dirty(self, ignoreupdate=False):
1311 1318 if self._gitmissing():
1312 1319 return self._state[1] != ''
1313 1320 if self._gitisbare():
1314 1321 return True
1315 1322 if not ignoreupdate and self._state[1] != self._gitstate():
1316 1323 # different version checked out
1317 1324 return True
1318 1325 # check for staged changes or modified files; ignore untracked files
1319 1326 self._gitupdatestat()
1320 1327 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1321 1328 return code == 1
1322 1329
1323 1330 def basestate(self):
1324 1331 return self._gitstate()
1325 1332
1326 1333 @annotatesubrepoerror
1327 1334 def get(self, state, overwrite=False):
1328 1335 source, revision, kind = state
1329 1336 if not revision:
1330 1337 self.remove()
1331 1338 return
1332 1339 self._fetch(source, revision)
1333 1340 # if the repo was set to be bare, unbare it
1334 1341 if self._gitisbare():
1335 1342 self._gitcommand(['config', 'core.bare', 'false'])
1336 1343 if self._gitstate() == revision:
1337 1344 self._gitcommand(['reset', '--hard', 'HEAD'])
1338 1345 return
1339 1346 elif self._gitstate() == revision:
1340 1347 if overwrite:
1341 1348 # first reset the index to unmark new files for commit, because
1342 1349 # reset --hard will otherwise throw away files added for commit,
1343 1350 # not just unmark them.
1344 1351 self._gitcommand(['reset', 'HEAD'])
1345 1352 self._gitcommand(['reset', '--hard', 'HEAD'])
1346 1353 return
1347 1354 branch2rev, rev2branch = self._gitbranchmap()
1348 1355
1349 1356 def checkout(args):
1350 1357 cmd = ['checkout']
1351 1358 if overwrite:
1352 1359 # first reset the index to unmark new files for commit, because
1353 1360 # the -f option will otherwise throw away files added for
1354 1361 # commit, not just unmark them.
1355 1362 self._gitcommand(['reset', 'HEAD'])
1356 1363 cmd.append('-f')
1357 1364 self._gitcommand(cmd + args)
1358 1365 _sanitize(self._ui, self._abspath, '.git')
1359 1366
1360 1367 def rawcheckout():
1361 1368 # no branch to checkout, check it out with no branch
1362 1369 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1363 1370 self._relpath)
1364 1371 self._ui.warn(_('check out a git branch if you intend '
1365 1372 'to make changes\n'))
1366 1373 checkout(['-q', revision])
1367 1374
1368 1375 if revision not in rev2branch:
1369 1376 rawcheckout()
1370 1377 return
1371 1378 branches = rev2branch[revision]
1372 1379 firstlocalbranch = None
1373 1380 for b in branches:
1374 1381 if b == 'refs/heads/master':
1375 1382 # master trumps all other branches
1376 1383 checkout(['refs/heads/master'])
1377 1384 return
1378 1385 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1379 1386 firstlocalbranch = b
1380 1387 if firstlocalbranch:
1381 1388 checkout([firstlocalbranch])
1382 1389 return
1383 1390
1384 1391 tracking = self._gittracking(branch2rev.keys())
1385 1392 # choose a remote branch already tracked if possible
1386 1393 remote = branches[0]
1387 1394 if remote not in tracking:
1388 1395 for b in branches:
1389 1396 if b in tracking:
1390 1397 remote = b
1391 1398 break
1392 1399
1393 1400 if remote not in tracking:
1394 1401 # create a new local tracking branch
1395 1402 local = remote.split('/', 3)[3]
1396 1403 checkout(['-b', local, remote])
1397 1404 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1398 1405 # When updating to a tracked remote branch,
1399 1406 # if the local tracking branch is downstream of it,
1400 1407 # a normal `git pull` would have performed a "fast-forward merge"
1401 1408 # which is equivalent to updating the local branch to the remote.
1402 1409 # Since we are only looking at branching at update, we need to
1403 1410 # detect this situation and perform this action lazily.
1404 1411 if tracking[remote] != self._gitcurrentbranch():
1405 1412 checkout([tracking[remote]])
1406 1413 self._gitcommand(['merge', '--ff', remote])
1407 1414 _sanitize(self._ui, self._abspath, '.git')
1408 1415 else:
1409 1416 # a real merge would be required, just checkout the revision
1410 1417 rawcheckout()
1411 1418
1412 1419 @annotatesubrepoerror
1413 1420 def commit(self, text, user, date):
1414 1421 if self._gitmissing():
1415 1422 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1416 1423 cmd = ['commit', '-a', '-m', text]
1417 1424 env = os.environ.copy()
1418 1425 if user:
1419 1426 cmd += ['--author', user]
1420 1427 if date:
1421 1428 # git's date parser silently ignores when seconds < 1e9
1422 1429 # convert to ISO8601
1423 1430 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1424 1431 '%Y-%m-%dT%H:%M:%S %1%2')
1425 1432 self._gitcommand(cmd, env=env)
1426 1433 # make sure commit works otherwise HEAD might not exist under certain
1427 1434 # circumstances
1428 1435 return self._gitstate()
1429 1436
1430 1437 @annotatesubrepoerror
1431 1438 def merge(self, state):
1432 1439 source, revision, kind = state
1433 1440 self._fetch(source, revision)
1434 1441 base = self._gitcommand(['merge-base', revision, self._state[1]])
1435 1442 self._gitupdatestat()
1436 1443 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1437 1444
1438 1445 def mergefunc():
1439 1446 if base == revision:
1440 1447 self.get(state) # fast forward merge
1441 1448 elif base != self._state[1]:
1442 1449 self._gitcommand(['merge', '--no-commit', revision])
1443 1450 _sanitize(self._ui, self._abspath, '.git')
1444 1451
1445 1452 if self.dirty():
1446 1453 if self._gitstate() != revision:
1447 1454 dirty = self._gitstate() == self._state[1] or code != 0
1448 1455 if _updateprompt(self._ui, self, dirty,
1449 1456 self._state[1][:7], revision[:7]):
1450 1457 mergefunc()
1451 1458 else:
1452 1459 mergefunc()
1453 1460
1454 1461 @annotatesubrepoerror
1455 1462 def push(self, opts):
1456 1463 force = opts.get('force')
1457 1464
1458 1465 if not self._state[1]:
1459 1466 return True
1460 1467 if self._gitmissing():
1461 1468 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1462 1469 # if a branch in origin contains the revision, nothing to do
1463 1470 branch2rev, rev2branch = self._gitbranchmap()
1464 1471 if self._state[1] in rev2branch:
1465 1472 for b in rev2branch[self._state[1]]:
1466 1473 if b.startswith('refs/remotes/origin/'):
1467 1474 return True
1468 1475 for b, revision in branch2rev.iteritems():
1469 1476 if b.startswith('refs/remotes/origin/'):
1470 1477 if self._gitisancestor(self._state[1], revision):
1471 1478 return True
1472 1479 # otherwise, try to push the currently checked out branch
1473 1480 cmd = ['push']
1474 1481 if force:
1475 1482 cmd.append('--force')
1476 1483
1477 1484 current = self._gitcurrentbranch()
1478 1485 if current:
1479 1486 # determine if the current branch is even useful
1480 1487 if not self._gitisancestor(self._state[1], current):
1481 1488 self._ui.warn(_('unrelated git branch checked out '
1482 1489 'in subrepo %s\n') % self._relpath)
1483 1490 return False
1484 1491 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1485 1492 (current.split('/', 2)[2], self._relpath))
1486 1493 ret = self._gitdir(cmd + ['origin', current])
1487 1494 return ret[1] == 0
1488 1495 else:
1489 1496 self._ui.warn(_('no branch checked out in subrepo %s\n'
1490 1497 'cannot push revision %s\n') %
1491 1498 (self._relpath, self._state[1]))
1492 1499 return False
1493 1500
1494 1501 @annotatesubrepoerror
1495 1502 def remove(self):
1496 1503 if self._gitmissing():
1497 1504 return
1498 1505 if self.dirty():
1499 1506 self._ui.warn(_('not removing repo %s because '
1500 1507 'it has changes.\n') % self._relpath)
1501 1508 return
1502 1509 # we can't fully delete the repository as it may contain
1503 1510 # local-only history
1504 1511 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1505 1512 self._gitcommand(['config', 'core.bare', 'true'])
1506 1513 for f in os.listdir(self._abspath):
1507 1514 if f == '.git':
1508 1515 continue
1509 1516 path = os.path.join(self._abspath, f)
1510 1517 if os.path.isdir(path) and not os.path.islink(path):
1511 1518 shutil.rmtree(path)
1512 1519 else:
1513 1520 os.remove(path)
1514 1521
1515 1522 def archive(self, ui, archiver, prefix, match=None):
1516 1523 total = 0
1517 1524 source, revision = self._state
1518 1525 if not revision:
1519 1526 return total
1520 1527 self._fetch(source, revision)
1521 1528
1522 1529 # Parse git's native archive command.
1523 1530 # This should be much faster than manually traversing the trees
1524 1531 # and objects with many subprocess calls.
1525 1532 tarstream = self._gitcommand(['archive', revision], stream=True)
1526 1533 tar = tarfile.open(fileobj=tarstream, mode='r|')
1527 1534 relpath = subrelpath(self)
1528 1535 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1529 1536 for i, info in enumerate(tar):
1530 1537 if info.isdir():
1531 1538 continue
1532 1539 if match and not match(info.name):
1533 1540 continue
1534 1541 if info.issym():
1535 1542 data = info.linkname
1536 1543 else:
1537 1544 data = tar.extractfile(info).read()
1538 1545 archiver.addfile(os.path.join(prefix, self._path, info.name),
1539 1546 info.mode, info.issym(), data)
1540 1547 total += 1
1541 1548 ui.progress(_('archiving (%s)') % relpath, i + 1,
1542 1549 unit=_('files'))
1543 1550 ui.progress(_('archiving (%s)') % relpath, None)
1544 1551 return total
1545 1552
1546 1553
1547 1554 @annotatesubrepoerror
1548 1555 def status(self, rev2, **opts):
1549 1556 rev1 = self._state[1]
1550 1557 if self._gitmissing() or not rev1:
1551 1558 # if the repo is missing, return no results
1552 1559 return [], [], [], [], [], [], []
1553 1560 modified, added, removed = [], [], []
1554 1561 self._gitupdatestat()
1555 1562 if rev2:
1556 1563 command = ['diff-tree', rev1, rev2]
1557 1564 else:
1558 1565 command = ['diff-index', rev1]
1559 1566 out = self._gitcommand(command)
1560 1567 for line in out.split('\n'):
1561 1568 tab = line.find('\t')
1562 1569 if tab == -1:
1563 1570 continue
1564 1571 status, f = line[tab - 1], line[tab + 1:]
1565 1572 if status == 'M':
1566 1573 modified.append(f)
1567 1574 elif status == 'A':
1568 1575 added.append(f)
1569 1576 elif status == 'D':
1570 1577 removed.append(f)
1571 1578
1572 1579 deleted, unknown, ignored, clean = [], [], [], []
1573 1580
1574 1581 if not rev2:
1575 1582 command = ['ls-files', '--others', '--exclude-standard']
1576 1583 out = self._gitcommand(command)
1577 1584 for line in out.split('\n'):
1578 1585 if len(line) == 0:
1579 1586 continue
1580 1587 unknown.append(line)
1581 1588
1582 1589 return scmutil.status(modified, added, removed, deleted,
1583 1590 unknown, ignored, clean)
1584 1591
1585 1592 def shortid(self, revid):
1586 1593 return revid[:7]
1587 1594
1588 1595 types = {
1589 1596 'hg': hgsubrepo,
1590 1597 'svn': svnsubrepo,
1591 1598 'git': gitsubrepo,
1592 1599 }
General Comments 0
You need to be logged in to leave comments. Login now