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