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