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