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