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