##// END OF EJS Templates
subrepo: replace direct file APIs around "readlines" by "vfs.tryreadlines"...
FUJIWARA Katsunori -
r23369:22e00674 default
parent child Browse files
Show More
@@ -1,1601 +1,1594
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 cachefile = self._getstorehashcachepath(remotepath)
576 if not os.path.exists(cachefile):
577 return ''
578 fd = open(cachefile, 'r')
579 try:
580 pullstate = fd.readlines()
581 finally:
582 fd.close()
583 return pullstate
575 cachefile = _getstorehashcachename(remotepath)
576 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
584 577
585 578 def _cachestorehash(self, remotepath):
586 579 '''cache the current store hash
587 580
588 581 Each remote repo requires its own store hash cache, because a subrepo
589 582 store may be "clean" versus a given remote repo, but not versus another
590 583 '''
591 584 cachefile = self._getstorehashcachepath(remotepath)
592 585 lock = self._repo.lock()
593 586 try:
594 587 storehash = list(self._calcstorehash(remotepath))
595 588 cachedir = os.path.dirname(cachefile)
596 589 if not os.path.exists(cachedir):
597 590 util.makedirs(cachedir, notindexed=True)
598 591 fd = open(cachefile, 'w')
599 592 try:
600 593 fd.writelines(storehash)
601 594 finally:
602 595 fd.close()
603 596 finally:
604 597 lock.release()
605 598
606 599 @annotatesubrepoerror
607 600 def _initrepo(self, parentrepo, source, create):
608 601 self._repo._subparent = parentrepo
609 602 self._repo._subsource = source
610 603
611 604 if create:
612 605 lines = ['[paths]\n']
613 606
614 607 def addpathconfig(key, value):
615 608 if value:
616 609 lines.append('%s = %s\n' % (key, value))
617 610 self._repo.ui.setconfig('paths', key, value, 'subrepo')
618 611
619 612 defpath = _abssource(self._repo, abort=False)
620 613 defpushpath = _abssource(self._repo, True, abort=False)
621 614 addpathconfig('default', defpath)
622 615 if defpath != defpushpath:
623 616 addpathconfig('default-push', defpushpath)
624 617
625 618 fp = self._repo.opener("hgrc", "w", text=True)
626 619 try:
627 620 fp.write(''.join(lines))
628 621 finally:
629 622 fp.close()
630 623
631 624 @annotatesubrepoerror
632 625 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
633 626 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
634 627 os.path.join(prefix, self._path), explicitonly)
635 628
636 629 @annotatesubrepoerror
637 630 def cat(self, ui, match, prefix, **opts):
638 631 rev = self._state[1]
639 632 ctx = self._repo[rev]
640 633 return cmdutil.cat(ui, self._repo, ctx, match, prefix, **opts)
641 634
642 635 @annotatesubrepoerror
643 636 def status(self, rev2, **opts):
644 637 try:
645 638 rev1 = self._state[1]
646 639 ctx1 = self._repo[rev1]
647 640 ctx2 = self._repo[rev2]
648 641 return self._repo.status(ctx1, ctx2, **opts)
649 642 except error.RepoLookupError, inst:
650 643 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
651 644 % (inst, subrelpath(self)))
652 645 return scmutil.status([], [], [], [], [], [], [])
653 646
654 647 @annotatesubrepoerror
655 648 def diff(self, ui, diffopts, node2, match, prefix, **opts):
656 649 try:
657 650 node1 = node.bin(self._state[1])
658 651 # We currently expect node2 to come from substate and be
659 652 # in hex format
660 653 if node2 is not None:
661 654 node2 = node.bin(node2)
662 655 cmdutil.diffordiffstat(ui, self._repo, diffopts,
663 656 node1, node2, match,
664 657 prefix=posixpath.join(prefix, self._path),
665 658 listsubrepos=True, **opts)
666 659 except error.RepoLookupError, inst:
667 660 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
668 661 % (inst, subrelpath(self)))
669 662
670 663 @annotatesubrepoerror
671 664 def archive(self, ui, archiver, prefix, match=None):
672 665 self._get(self._state + ('hg',))
673 666 total = abstractsubrepo.archive(self, ui, archiver, prefix, match)
674 667 rev = self._state[1]
675 668 ctx = self._repo[rev]
676 669 for subpath in ctx.substate:
677 670 s = subrepo(ctx, subpath)
678 671 submatch = matchmod.narrowmatcher(subpath, match)
679 672 total += s.archive(
680 673 ui, archiver, os.path.join(prefix, self._path), submatch)
681 674 return total
682 675
683 676 @annotatesubrepoerror
684 677 def dirty(self, ignoreupdate=False):
685 678 r = self._state[1]
686 679 if r == '' and not ignoreupdate: # no state recorded
687 680 return True
688 681 w = self._repo[None]
689 682 if r != w.p1().hex() and not ignoreupdate:
690 683 # different version checked out
691 684 return True
692 685 return w.dirty() # working directory changed
693 686
694 687 def basestate(self):
695 688 return self._repo['.'].hex()
696 689
697 690 def checknested(self, path):
698 691 return self._repo._checknested(self._repo.wjoin(path))
699 692
700 693 @annotatesubrepoerror
701 694 def commit(self, text, user, date):
702 695 # don't bother committing in the subrepo if it's only been
703 696 # updated
704 697 if not self.dirty(True):
705 698 return self._repo['.'].hex()
706 699 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
707 700 n = self._repo.commit(text, user, date)
708 701 if not n:
709 702 return self._repo['.'].hex() # different version checked out
710 703 return node.hex(n)
711 704
712 705 @annotatesubrepoerror
713 706 def phase(self, state):
714 707 return self._repo[state].phase()
715 708
716 709 @annotatesubrepoerror
717 710 def remove(self):
718 711 # we can't fully delete the repository as it may contain
719 712 # local-only history
720 713 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
721 714 hg.clean(self._repo, node.nullid, False)
722 715
723 716 def _get(self, state):
724 717 source, revision, kind = state
725 718 if revision in self._repo.unfiltered():
726 719 return True
727 720 self._repo._subsource = source
728 721 srcurl = _abssource(self._repo)
729 722 other = hg.peer(self._repo, {}, srcurl)
730 723 if len(self._repo) == 0:
731 724 self._repo.ui.status(_('cloning subrepo %s from %s\n')
732 725 % (subrelpath(self), srcurl))
733 726 parentrepo = self._repo._subparent
734 727 shutil.rmtree(self._repo.path)
735 728 other, cloned = hg.clone(self._repo._subparent.baseui, {},
736 729 other, self._repo.root,
737 730 update=False)
738 731 self._repo = cloned.local()
739 732 self._initrepo(parentrepo, source, create=True)
740 733 self._cachestorehash(srcurl)
741 734 else:
742 735 self._repo.ui.status(_('pulling subrepo %s from %s\n')
743 736 % (subrelpath(self), srcurl))
744 737 cleansub = self.storeclean(srcurl)
745 738 exchange.pull(self._repo, other)
746 739 if cleansub:
747 740 # keep the repo clean after pull
748 741 self._cachestorehash(srcurl)
749 742 return False
750 743
751 744 @annotatesubrepoerror
752 745 def get(self, state, overwrite=False):
753 746 inrepo = self._get(state)
754 747 source, revision, kind = state
755 748 repo = self._repo
756 749 repo.ui.debug("getting subrepo %s\n" % self._path)
757 750 if inrepo:
758 751 urepo = repo.unfiltered()
759 752 ctx = urepo[revision]
760 753 if ctx.hidden():
761 754 urepo.ui.warn(
762 755 _('revision %s in subrepo %s is hidden\n') \
763 756 % (revision[0:12], self._path))
764 757 repo = urepo
765 758 hg.updaterepo(repo, revision, overwrite)
766 759
767 760 @annotatesubrepoerror
768 761 def merge(self, state):
769 762 self._get(state)
770 763 cur = self._repo['.']
771 764 dst = self._repo[state[1]]
772 765 anc = dst.ancestor(cur)
773 766
774 767 def mergefunc():
775 768 if anc == cur and dst.branch() == cur.branch():
776 769 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
777 770 hg.update(self._repo, state[1])
778 771 elif anc == dst:
779 772 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
780 773 else:
781 774 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
782 775 hg.merge(self._repo, state[1], remind=False)
783 776
784 777 wctx = self._repo[None]
785 778 if self.dirty():
786 779 if anc != dst:
787 780 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
788 781 mergefunc()
789 782 else:
790 783 mergefunc()
791 784 else:
792 785 mergefunc()
793 786
794 787 @annotatesubrepoerror
795 788 def push(self, opts):
796 789 force = opts.get('force')
797 790 newbranch = opts.get('new_branch')
798 791 ssh = opts.get('ssh')
799 792
800 793 # push subrepos depth-first for coherent ordering
801 794 c = self._repo['']
802 795 subs = c.substate # only repos that are committed
803 796 for s in sorted(subs):
804 797 if c.sub(s).push(opts) == 0:
805 798 return False
806 799
807 800 dsturl = _abssource(self._repo, True)
808 801 if not force:
809 802 if self.storeclean(dsturl):
810 803 self._repo.ui.status(
811 804 _('no changes made to subrepo %s since last push to %s\n')
812 805 % (subrelpath(self), dsturl))
813 806 return None
814 807 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
815 808 (subrelpath(self), dsturl))
816 809 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
817 810 res = exchange.push(self._repo, other, force, newbranch=newbranch)
818 811
819 812 # the repo is now clean
820 813 self._cachestorehash(dsturl)
821 814 return res.cgresult
822 815
823 816 @annotatesubrepoerror
824 817 def outgoing(self, ui, dest, opts):
825 818 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
826 819
827 820 @annotatesubrepoerror
828 821 def incoming(self, ui, source, opts):
829 822 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
830 823
831 824 @annotatesubrepoerror
832 825 def files(self):
833 826 rev = self._state[1]
834 827 ctx = self._repo[rev]
835 828 return ctx.manifest()
836 829
837 830 def filedata(self, name):
838 831 rev = self._state[1]
839 832 return self._repo[rev][name].data()
840 833
841 834 def fileflags(self, name):
842 835 rev = self._state[1]
843 836 ctx = self._repo[rev]
844 837 return ctx.flags(name)
845 838
846 839 def walk(self, match):
847 840 ctx = self._repo[None]
848 841 return ctx.walk(match)
849 842
850 843 @annotatesubrepoerror
851 844 def forget(self, ui, match, prefix):
852 845 return cmdutil.forget(ui, self._repo, match,
853 846 os.path.join(prefix, self._path), True)
854 847
855 848 @annotatesubrepoerror
856 849 def removefiles(self, ui, matcher, prefix, after, force, subrepos):
857 850 return cmdutil.remove(ui, self._repo, matcher,
858 851 os.path.join(prefix, self._path), after, force,
859 852 subrepos)
860 853
861 854 @annotatesubrepoerror
862 855 def revert(self, ui, substate, *pats, **opts):
863 856 # reverting a subrepo is a 2 step process:
864 857 # 1. if the no_backup is not set, revert all modified
865 858 # files inside the subrepo
866 859 # 2. update the subrepo to the revision specified in
867 860 # the corresponding substate dictionary
868 861 ui.status(_('reverting subrepo %s\n') % substate[0])
869 862 if not opts.get('no_backup'):
870 863 # Revert all files on the subrepo, creating backups
871 864 # Note that this will not recursively revert subrepos
872 865 # We could do it if there was a set:subrepos() predicate
873 866 opts = opts.copy()
874 867 opts['date'] = None
875 868 opts['rev'] = substate[1]
876 869
877 870 pats = []
878 871 if not opts.get('all'):
879 872 pats = ['set:modified()']
880 873 self.filerevert(ui, *pats, **opts)
881 874
882 875 # Update the repo to the revision specified in the given substate
883 876 self.get(substate, overwrite=True)
884 877
885 878 def filerevert(self, ui, *pats, **opts):
886 879 ctx = self._repo[opts['rev']]
887 880 parents = self._repo.dirstate.parents()
888 881 if opts.get('all'):
889 882 pats = ['set:modified()']
890 883 else:
891 884 pats = []
892 885 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
893 886
894 887 def shortid(self, revid):
895 888 return revid[:12]
896 889
897 890 class svnsubrepo(abstractsubrepo):
898 891 def __init__(self, ctx, path, state):
899 892 self._path = path
900 893 self._state = state
901 894 self._ctx = ctx
902 895 self._ui = ctx._repo.ui
903 896 self._exe = util.findexe('svn')
904 897 if not self._exe:
905 898 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
906 899 % self._path)
907 900
908 901 def _svncommand(self, commands, filename='', failok=False):
909 902 cmd = [self._exe]
910 903 extrakw = {}
911 904 if not self._ui.interactive():
912 905 # Making stdin be a pipe should prevent svn from behaving
913 906 # interactively even if we can't pass --non-interactive.
914 907 extrakw['stdin'] = subprocess.PIPE
915 908 # Starting in svn 1.5 --non-interactive is a global flag
916 909 # instead of being per-command, but we need to support 1.4 so
917 910 # we have to be intelligent about what commands take
918 911 # --non-interactive.
919 912 if commands[0] in ('update', 'checkout', 'commit'):
920 913 cmd.append('--non-interactive')
921 914 cmd.extend(commands)
922 915 if filename is not None:
923 916 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
924 917 cmd.append(path)
925 918 env = dict(os.environ)
926 919 # Avoid localized output, preserve current locale for everything else.
927 920 lc_all = env.get('LC_ALL')
928 921 if lc_all:
929 922 env['LANG'] = lc_all
930 923 del env['LC_ALL']
931 924 env['LC_MESSAGES'] = 'C'
932 925 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
933 926 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
934 927 universal_newlines=True, env=env, **extrakw)
935 928 stdout, stderr = p.communicate()
936 929 stderr = stderr.strip()
937 930 if not failok:
938 931 if p.returncode:
939 932 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
940 933 if stderr:
941 934 self._ui.warn(stderr + '\n')
942 935 return stdout, stderr
943 936
944 937 @propertycache
945 938 def _svnversion(self):
946 939 output, err = self._svncommand(['--version', '--quiet'], filename=None)
947 940 m = re.search(r'^(\d+)\.(\d+)', output)
948 941 if not m:
949 942 raise util.Abort(_('cannot retrieve svn tool version'))
950 943 return (int(m.group(1)), int(m.group(2)))
951 944
952 945 def _wcrevs(self):
953 946 # Get the working directory revision as well as the last
954 947 # commit revision so we can compare the subrepo state with
955 948 # both. We used to store the working directory one.
956 949 output, err = self._svncommand(['info', '--xml'])
957 950 doc = xml.dom.minidom.parseString(output)
958 951 entries = doc.getElementsByTagName('entry')
959 952 lastrev, rev = '0', '0'
960 953 if entries:
961 954 rev = str(entries[0].getAttribute('revision')) or '0'
962 955 commits = entries[0].getElementsByTagName('commit')
963 956 if commits:
964 957 lastrev = str(commits[0].getAttribute('revision')) or '0'
965 958 return (lastrev, rev)
966 959
967 960 def _wcrev(self):
968 961 return self._wcrevs()[0]
969 962
970 963 def _wcchanged(self):
971 964 """Return (changes, extchanges, missing) where changes is True
972 965 if the working directory was changed, extchanges is
973 966 True if any of these changes concern an external entry and missing
974 967 is True if any change is a missing entry.
975 968 """
976 969 output, err = self._svncommand(['status', '--xml'])
977 970 externals, changes, missing = [], [], []
978 971 doc = xml.dom.minidom.parseString(output)
979 972 for e in doc.getElementsByTagName('entry'):
980 973 s = e.getElementsByTagName('wc-status')
981 974 if not s:
982 975 continue
983 976 item = s[0].getAttribute('item')
984 977 props = s[0].getAttribute('props')
985 978 path = e.getAttribute('path')
986 979 if item == 'external':
987 980 externals.append(path)
988 981 elif item == 'missing':
989 982 missing.append(path)
990 983 if (item not in ('', 'normal', 'unversioned', 'external')
991 984 or props not in ('', 'none', 'normal')):
992 985 changes.append(path)
993 986 for path in changes:
994 987 for ext in externals:
995 988 if path == ext or path.startswith(ext + os.sep):
996 989 return True, True, bool(missing)
997 990 return bool(changes), False, bool(missing)
998 991
999 992 def dirty(self, ignoreupdate=False):
1000 993 if not self._wcchanged()[0]:
1001 994 if self._state[1] in self._wcrevs() or ignoreupdate:
1002 995 return False
1003 996 return True
1004 997
1005 998 def basestate(self):
1006 999 lastrev, rev = self._wcrevs()
1007 1000 if lastrev != rev:
1008 1001 # Last committed rev is not the same than rev. We would
1009 1002 # like to take lastrev but we do not know if the subrepo
1010 1003 # URL exists at lastrev. Test it and fallback to rev it
1011 1004 # is not there.
1012 1005 try:
1013 1006 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1014 1007 return lastrev
1015 1008 except error.Abort:
1016 1009 pass
1017 1010 return rev
1018 1011
1019 1012 @annotatesubrepoerror
1020 1013 def commit(self, text, user, date):
1021 1014 # user and date are out of our hands since svn is centralized
1022 1015 changed, extchanged, missing = self._wcchanged()
1023 1016 if not changed:
1024 1017 return self.basestate()
1025 1018 if extchanged:
1026 1019 # Do not try to commit externals
1027 1020 raise util.Abort(_('cannot commit svn externals'))
1028 1021 if missing:
1029 1022 # svn can commit with missing entries but aborting like hg
1030 1023 # seems a better approach.
1031 1024 raise util.Abort(_('cannot commit missing svn entries'))
1032 1025 commitinfo, err = self._svncommand(['commit', '-m', text])
1033 1026 self._ui.status(commitinfo)
1034 1027 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1035 1028 if not newrev:
1036 1029 if not commitinfo.strip():
1037 1030 # Sometimes, our definition of "changed" differs from
1038 1031 # svn one. For instance, svn ignores missing files
1039 1032 # when committing. If there are only missing files, no
1040 1033 # commit is made, no output and no error code.
1041 1034 raise util.Abort(_('failed to commit svn changes'))
1042 1035 raise util.Abort(commitinfo.splitlines()[-1])
1043 1036 newrev = newrev.groups()[0]
1044 1037 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
1045 1038 return newrev
1046 1039
1047 1040 @annotatesubrepoerror
1048 1041 def remove(self):
1049 1042 if self.dirty():
1050 1043 self._ui.warn(_('not removing repo %s because '
1051 1044 'it has changes.\n') % self._path)
1052 1045 return
1053 1046 self._ui.note(_('removing subrepo %s\n') % self._path)
1054 1047
1055 1048 def onerror(function, path, excinfo):
1056 1049 if function is not os.remove:
1057 1050 raise
1058 1051 # read-only files cannot be unlinked under Windows
1059 1052 s = os.stat(path)
1060 1053 if (s.st_mode & stat.S_IWRITE) != 0:
1061 1054 raise
1062 1055 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
1063 1056 os.remove(path)
1064 1057
1065 1058 path = self._ctx._repo.wjoin(self._path)
1066 1059 shutil.rmtree(path, onerror=onerror)
1067 1060 try:
1068 1061 os.removedirs(os.path.dirname(path))
1069 1062 except OSError:
1070 1063 pass
1071 1064
1072 1065 @annotatesubrepoerror
1073 1066 def get(self, state, overwrite=False):
1074 1067 if overwrite:
1075 1068 self._svncommand(['revert', '--recursive'])
1076 1069 args = ['checkout']
1077 1070 if self._svnversion >= (1, 5):
1078 1071 args.append('--force')
1079 1072 # The revision must be specified at the end of the URL to properly
1080 1073 # update to a directory which has since been deleted and recreated.
1081 1074 args.append('%s@%s' % (state[0], state[1]))
1082 1075 status, err = self._svncommand(args, failok=True)
1083 1076 _sanitize(self._ui, self._ctx._repo.wjoin(self._path), '.svn')
1084 1077 if not re.search('Checked out revision [0-9]+.', status):
1085 1078 if ('is already a working copy for a different URL' in err
1086 1079 and (self._wcchanged()[:2] == (False, False))):
1087 1080 # obstructed but clean working copy, so just blow it away.
1088 1081 self.remove()
1089 1082 self.get(state, overwrite=False)
1090 1083 return
1091 1084 raise util.Abort((status or err).splitlines()[-1])
1092 1085 self._ui.status(status)
1093 1086
1094 1087 @annotatesubrepoerror
1095 1088 def merge(self, state):
1096 1089 old = self._state[1]
1097 1090 new = state[1]
1098 1091 wcrev = self._wcrev()
1099 1092 if new != wcrev:
1100 1093 dirty = old == wcrev or self._wcchanged()[0]
1101 1094 if _updateprompt(self._ui, self, dirty, wcrev, new):
1102 1095 self.get(state, False)
1103 1096
1104 1097 def push(self, opts):
1105 1098 # push is a no-op for SVN
1106 1099 return True
1107 1100
1108 1101 @annotatesubrepoerror
1109 1102 def files(self):
1110 1103 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1111 1104 doc = xml.dom.minidom.parseString(output)
1112 1105 paths = []
1113 1106 for e in doc.getElementsByTagName('entry'):
1114 1107 kind = str(e.getAttribute('kind'))
1115 1108 if kind != 'file':
1116 1109 continue
1117 1110 name = ''.join(c.data for c
1118 1111 in e.getElementsByTagName('name')[0].childNodes
1119 1112 if c.nodeType == c.TEXT_NODE)
1120 1113 paths.append(name.encode('utf-8'))
1121 1114 return paths
1122 1115
1123 1116 def filedata(self, name):
1124 1117 return self._svncommand(['cat'], name)[0]
1125 1118
1126 1119
1127 1120 class gitsubrepo(abstractsubrepo):
1128 1121 def __init__(self, ctx, path, state):
1129 1122 self._state = state
1130 1123 self._ctx = ctx
1131 1124 self._path = path
1132 1125 self._relpath = os.path.join(reporelpath(ctx._repo), path)
1133 1126 self._abspath = ctx._repo.wjoin(path)
1134 1127 self._subparent = ctx._repo
1135 1128 self._ui = ctx._repo.ui
1136 1129 self._ensuregit()
1137 1130
1138 1131 def _ensuregit(self):
1139 1132 try:
1140 1133 self._gitexecutable = 'git'
1141 1134 out, err = self._gitnodir(['--version'])
1142 1135 except OSError, e:
1143 1136 if e.errno != 2 or os.name != 'nt':
1144 1137 raise
1145 1138 self._gitexecutable = 'git.cmd'
1146 1139 out, err = self._gitnodir(['--version'])
1147 1140 versionstatus = self._checkversion(out)
1148 1141 if versionstatus == 'unknown':
1149 1142 self._ui.warn(_('cannot retrieve git version\n'))
1150 1143 elif versionstatus == 'abort':
1151 1144 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
1152 1145 elif versionstatus == 'warning':
1153 1146 self._ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1154 1147
1155 1148 @staticmethod
1156 1149 def _checkversion(out):
1157 1150 '''ensure git version is new enough
1158 1151
1159 1152 >>> _checkversion = gitsubrepo._checkversion
1160 1153 >>> _checkversion('git version 1.6.0')
1161 1154 'ok'
1162 1155 >>> _checkversion('git version 1.8.5')
1163 1156 'ok'
1164 1157 >>> _checkversion('git version 1.4.0')
1165 1158 'abort'
1166 1159 >>> _checkversion('git version 1.5.0')
1167 1160 'warning'
1168 1161 >>> _checkversion('git version 1.9-rc0')
1169 1162 'ok'
1170 1163 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1171 1164 'ok'
1172 1165 >>> _checkversion('git version 1.9.0.GIT')
1173 1166 'ok'
1174 1167 >>> _checkversion('git version 12345')
1175 1168 'unknown'
1176 1169 >>> _checkversion('no')
1177 1170 'unknown'
1178 1171 '''
1179 1172 m = re.search(r'^git version (\d+)\.(\d+)', out)
1180 1173 if not m:
1181 1174 return 'unknown'
1182 1175 version = (int(m.group(1)), int(m.group(2)))
1183 1176 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1184 1177 # despite the docstring comment. For now, error on 1.4.0, warn on
1185 1178 # 1.5.0 but attempt to continue.
1186 1179 if version < (1, 5):
1187 1180 return 'abort'
1188 1181 elif version < (1, 6):
1189 1182 return 'warning'
1190 1183 return 'ok'
1191 1184
1192 1185 def _gitcommand(self, commands, env=None, stream=False):
1193 1186 return self._gitdir(commands, env=env, stream=stream)[0]
1194 1187
1195 1188 def _gitdir(self, commands, env=None, stream=False):
1196 1189 return self._gitnodir(commands, env=env, stream=stream,
1197 1190 cwd=self._abspath)
1198 1191
1199 1192 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1200 1193 """Calls the git command
1201 1194
1202 1195 The methods tries to call the git command. versions prior to 1.6.0
1203 1196 are not supported and very probably fail.
1204 1197 """
1205 1198 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1206 1199 # unless ui.quiet is set, print git's stderr,
1207 1200 # which is mostly progress and useful info
1208 1201 errpipe = None
1209 1202 if self._ui.quiet:
1210 1203 errpipe = open(os.devnull, 'w')
1211 1204 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1212 1205 cwd=cwd, env=env, close_fds=util.closefds,
1213 1206 stdout=subprocess.PIPE, stderr=errpipe)
1214 1207 if stream:
1215 1208 return p.stdout, None
1216 1209
1217 1210 retdata = p.stdout.read().strip()
1218 1211 # wait for the child to exit to avoid race condition.
1219 1212 p.wait()
1220 1213
1221 1214 if p.returncode != 0 and p.returncode != 1:
1222 1215 # there are certain error codes that are ok
1223 1216 command = commands[0]
1224 1217 if command in ('cat-file', 'symbolic-ref'):
1225 1218 return retdata, p.returncode
1226 1219 # for all others, abort
1227 1220 raise util.Abort('git %s error %d in %s' %
1228 1221 (command, p.returncode, self._relpath))
1229 1222
1230 1223 return retdata, p.returncode
1231 1224
1232 1225 def _gitmissing(self):
1233 1226 return not os.path.exists(os.path.join(self._abspath, '.git'))
1234 1227
1235 1228 def _gitstate(self):
1236 1229 return self._gitcommand(['rev-parse', 'HEAD'])
1237 1230
1238 1231 def _gitcurrentbranch(self):
1239 1232 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1240 1233 if err:
1241 1234 current = None
1242 1235 return current
1243 1236
1244 1237 def _gitremote(self, remote):
1245 1238 out = self._gitcommand(['remote', 'show', '-n', remote])
1246 1239 line = out.split('\n')[1]
1247 1240 i = line.index('URL: ') + len('URL: ')
1248 1241 return line[i:]
1249 1242
1250 1243 def _githavelocally(self, revision):
1251 1244 out, code = self._gitdir(['cat-file', '-e', revision])
1252 1245 return code == 0
1253 1246
1254 1247 def _gitisancestor(self, r1, r2):
1255 1248 base = self._gitcommand(['merge-base', r1, r2])
1256 1249 return base == r1
1257 1250
1258 1251 def _gitisbare(self):
1259 1252 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1260 1253
1261 1254 def _gitupdatestat(self):
1262 1255 """This must be run before git diff-index.
1263 1256 diff-index only looks at changes to file stat;
1264 1257 this command looks at file contents and updates the stat."""
1265 1258 self._gitcommand(['update-index', '-q', '--refresh'])
1266 1259
1267 1260 def _gitbranchmap(self):
1268 1261 '''returns 2 things:
1269 1262 a map from git branch to revision
1270 1263 a map from revision to branches'''
1271 1264 branch2rev = {}
1272 1265 rev2branch = {}
1273 1266
1274 1267 out = self._gitcommand(['for-each-ref', '--format',
1275 1268 '%(objectname) %(refname)'])
1276 1269 for line in out.split('\n'):
1277 1270 revision, ref = line.split(' ')
1278 1271 if (not ref.startswith('refs/heads/') and
1279 1272 not ref.startswith('refs/remotes/')):
1280 1273 continue
1281 1274 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1282 1275 continue # ignore remote/HEAD redirects
1283 1276 branch2rev[ref] = revision
1284 1277 rev2branch.setdefault(revision, []).append(ref)
1285 1278 return branch2rev, rev2branch
1286 1279
1287 1280 def _gittracking(self, branches):
1288 1281 'return map of remote branch to local tracking branch'
1289 1282 # assumes no more than one local tracking branch for each remote
1290 1283 tracking = {}
1291 1284 for b in branches:
1292 1285 if b.startswith('refs/remotes/'):
1293 1286 continue
1294 1287 bname = b.split('/', 2)[2]
1295 1288 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1296 1289 if remote:
1297 1290 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1298 1291 tracking['refs/remotes/%s/%s' %
1299 1292 (remote, ref.split('/', 2)[2])] = b
1300 1293 return tracking
1301 1294
1302 1295 def _abssource(self, source):
1303 1296 if '://' not in source:
1304 1297 # recognize the scp syntax as an absolute source
1305 1298 colon = source.find(':')
1306 1299 if colon != -1 and '/' not in source[:colon]:
1307 1300 return source
1308 1301 self._subsource = source
1309 1302 return _abssource(self)
1310 1303
1311 1304 def _fetch(self, source, revision):
1312 1305 if self._gitmissing():
1313 1306 source = self._abssource(source)
1314 1307 self._ui.status(_('cloning subrepo %s from %s\n') %
1315 1308 (self._relpath, source))
1316 1309 self._gitnodir(['clone', source, self._abspath])
1317 1310 if self._githavelocally(revision):
1318 1311 return
1319 1312 self._ui.status(_('pulling subrepo %s from %s\n') %
1320 1313 (self._relpath, self._gitremote('origin')))
1321 1314 # try only origin: the originally cloned repo
1322 1315 self._gitcommand(['fetch'])
1323 1316 if not self._githavelocally(revision):
1324 1317 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1325 1318 (revision, self._relpath))
1326 1319
1327 1320 @annotatesubrepoerror
1328 1321 def dirty(self, ignoreupdate=False):
1329 1322 if self._gitmissing():
1330 1323 return self._state[1] != ''
1331 1324 if self._gitisbare():
1332 1325 return True
1333 1326 if not ignoreupdate and self._state[1] != self._gitstate():
1334 1327 # different version checked out
1335 1328 return True
1336 1329 # check for staged changes or modified files; ignore untracked files
1337 1330 self._gitupdatestat()
1338 1331 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1339 1332 return code == 1
1340 1333
1341 1334 def basestate(self):
1342 1335 return self._gitstate()
1343 1336
1344 1337 @annotatesubrepoerror
1345 1338 def get(self, state, overwrite=False):
1346 1339 source, revision, kind = state
1347 1340 if not revision:
1348 1341 self.remove()
1349 1342 return
1350 1343 self._fetch(source, revision)
1351 1344 # if the repo was set to be bare, unbare it
1352 1345 if self._gitisbare():
1353 1346 self._gitcommand(['config', 'core.bare', 'false'])
1354 1347 if self._gitstate() == revision:
1355 1348 self._gitcommand(['reset', '--hard', 'HEAD'])
1356 1349 return
1357 1350 elif self._gitstate() == revision:
1358 1351 if overwrite:
1359 1352 # first reset the index to unmark new files for commit, because
1360 1353 # reset --hard will otherwise throw away files added for commit,
1361 1354 # not just unmark them.
1362 1355 self._gitcommand(['reset', 'HEAD'])
1363 1356 self._gitcommand(['reset', '--hard', 'HEAD'])
1364 1357 return
1365 1358 branch2rev, rev2branch = self._gitbranchmap()
1366 1359
1367 1360 def checkout(args):
1368 1361 cmd = ['checkout']
1369 1362 if overwrite:
1370 1363 # first reset the index to unmark new files for commit, because
1371 1364 # the -f option will otherwise throw away files added for
1372 1365 # commit, not just unmark them.
1373 1366 self._gitcommand(['reset', 'HEAD'])
1374 1367 cmd.append('-f')
1375 1368 self._gitcommand(cmd + args)
1376 1369 _sanitize(self._ui, self._abspath, '.git')
1377 1370
1378 1371 def rawcheckout():
1379 1372 # no branch to checkout, check it out with no branch
1380 1373 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1381 1374 self._relpath)
1382 1375 self._ui.warn(_('check out a git branch if you intend '
1383 1376 'to make changes\n'))
1384 1377 checkout(['-q', revision])
1385 1378
1386 1379 if revision not in rev2branch:
1387 1380 rawcheckout()
1388 1381 return
1389 1382 branches = rev2branch[revision]
1390 1383 firstlocalbranch = None
1391 1384 for b in branches:
1392 1385 if b == 'refs/heads/master':
1393 1386 # master trumps all other branches
1394 1387 checkout(['refs/heads/master'])
1395 1388 return
1396 1389 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1397 1390 firstlocalbranch = b
1398 1391 if firstlocalbranch:
1399 1392 checkout([firstlocalbranch])
1400 1393 return
1401 1394
1402 1395 tracking = self._gittracking(branch2rev.keys())
1403 1396 # choose a remote branch already tracked if possible
1404 1397 remote = branches[0]
1405 1398 if remote not in tracking:
1406 1399 for b in branches:
1407 1400 if b in tracking:
1408 1401 remote = b
1409 1402 break
1410 1403
1411 1404 if remote not in tracking:
1412 1405 # create a new local tracking branch
1413 1406 local = remote.split('/', 3)[3]
1414 1407 checkout(['-b', local, remote])
1415 1408 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1416 1409 # When updating to a tracked remote branch,
1417 1410 # if the local tracking branch is downstream of it,
1418 1411 # a normal `git pull` would have performed a "fast-forward merge"
1419 1412 # which is equivalent to updating the local branch to the remote.
1420 1413 # Since we are only looking at branching at update, we need to
1421 1414 # detect this situation and perform this action lazily.
1422 1415 if tracking[remote] != self._gitcurrentbranch():
1423 1416 checkout([tracking[remote]])
1424 1417 self._gitcommand(['merge', '--ff', remote])
1425 1418 _sanitize(self._ui, self._abspath, '.git')
1426 1419 else:
1427 1420 # a real merge would be required, just checkout the revision
1428 1421 rawcheckout()
1429 1422
1430 1423 @annotatesubrepoerror
1431 1424 def commit(self, text, user, date):
1432 1425 if self._gitmissing():
1433 1426 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1434 1427 cmd = ['commit', '-a', '-m', text]
1435 1428 env = os.environ.copy()
1436 1429 if user:
1437 1430 cmd += ['--author', user]
1438 1431 if date:
1439 1432 # git's date parser silently ignores when seconds < 1e9
1440 1433 # convert to ISO8601
1441 1434 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1442 1435 '%Y-%m-%dT%H:%M:%S %1%2')
1443 1436 self._gitcommand(cmd, env=env)
1444 1437 # make sure commit works otherwise HEAD might not exist under certain
1445 1438 # circumstances
1446 1439 return self._gitstate()
1447 1440
1448 1441 @annotatesubrepoerror
1449 1442 def merge(self, state):
1450 1443 source, revision, kind = state
1451 1444 self._fetch(source, revision)
1452 1445 base = self._gitcommand(['merge-base', revision, self._state[1]])
1453 1446 self._gitupdatestat()
1454 1447 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1455 1448
1456 1449 def mergefunc():
1457 1450 if base == revision:
1458 1451 self.get(state) # fast forward merge
1459 1452 elif base != self._state[1]:
1460 1453 self._gitcommand(['merge', '--no-commit', revision])
1461 1454 _sanitize(self._ui, self._abspath, '.git')
1462 1455
1463 1456 if self.dirty():
1464 1457 if self._gitstate() != revision:
1465 1458 dirty = self._gitstate() == self._state[1] or code != 0
1466 1459 if _updateprompt(self._ui, self, dirty,
1467 1460 self._state[1][:7], revision[:7]):
1468 1461 mergefunc()
1469 1462 else:
1470 1463 mergefunc()
1471 1464
1472 1465 @annotatesubrepoerror
1473 1466 def push(self, opts):
1474 1467 force = opts.get('force')
1475 1468
1476 1469 if not self._state[1]:
1477 1470 return True
1478 1471 if self._gitmissing():
1479 1472 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1480 1473 # if a branch in origin contains the revision, nothing to do
1481 1474 branch2rev, rev2branch = self._gitbranchmap()
1482 1475 if self._state[1] in rev2branch:
1483 1476 for b in rev2branch[self._state[1]]:
1484 1477 if b.startswith('refs/remotes/origin/'):
1485 1478 return True
1486 1479 for b, revision in branch2rev.iteritems():
1487 1480 if b.startswith('refs/remotes/origin/'):
1488 1481 if self._gitisancestor(self._state[1], revision):
1489 1482 return True
1490 1483 # otherwise, try to push the currently checked out branch
1491 1484 cmd = ['push']
1492 1485 if force:
1493 1486 cmd.append('--force')
1494 1487
1495 1488 current = self._gitcurrentbranch()
1496 1489 if current:
1497 1490 # determine if the current branch is even useful
1498 1491 if not self._gitisancestor(self._state[1], current):
1499 1492 self._ui.warn(_('unrelated git branch checked out '
1500 1493 'in subrepo %s\n') % self._relpath)
1501 1494 return False
1502 1495 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1503 1496 (current.split('/', 2)[2], self._relpath))
1504 1497 ret = self._gitdir(cmd + ['origin', current])
1505 1498 return ret[1] == 0
1506 1499 else:
1507 1500 self._ui.warn(_('no branch checked out in subrepo %s\n'
1508 1501 'cannot push revision %s\n') %
1509 1502 (self._relpath, self._state[1]))
1510 1503 return False
1511 1504
1512 1505 @annotatesubrepoerror
1513 1506 def remove(self):
1514 1507 if self._gitmissing():
1515 1508 return
1516 1509 if self.dirty():
1517 1510 self._ui.warn(_('not removing repo %s because '
1518 1511 'it has changes.\n') % self._relpath)
1519 1512 return
1520 1513 # we can't fully delete the repository as it may contain
1521 1514 # local-only history
1522 1515 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1523 1516 self._gitcommand(['config', 'core.bare', 'true'])
1524 1517 for f in os.listdir(self._abspath):
1525 1518 if f == '.git':
1526 1519 continue
1527 1520 path = os.path.join(self._abspath, f)
1528 1521 if os.path.isdir(path) and not os.path.islink(path):
1529 1522 shutil.rmtree(path)
1530 1523 else:
1531 1524 os.remove(path)
1532 1525
1533 1526 def archive(self, ui, archiver, prefix, match=None):
1534 1527 total = 0
1535 1528 source, revision = self._state
1536 1529 if not revision:
1537 1530 return total
1538 1531 self._fetch(source, revision)
1539 1532
1540 1533 # Parse git's native archive command.
1541 1534 # This should be much faster than manually traversing the trees
1542 1535 # and objects with many subprocess calls.
1543 1536 tarstream = self._gitcommand(['archive', revision], stream=True)
1544 1537 tar = tarfile.open(fileobj=tarstream, mode='r|')
1545 1538 relpath = subrelpath(self)
1546 1539 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1547 1540 for i, info in enumerate(tar):
1548 1541 if info.isdir():
1549 1542 continue
1550 1543 if match and not match(info.name):
1551 1544 continue
1552 1545 if info.issym():
1553 1546 data = info.linkname
1554 1547 else:
1555 1548 data = tar.extractfile(info).read()
1556 1549 archiver.addfile(os.path.join(prefix, self._path, info.name),
1557 1550 info.mode, info.issym(), data)
1558 1551 total += 1
1559 1552 ui.progress(_('archiving (%s)') % relpath, i + 1,
1560 1553 unit=_('files'))
1561 1554 ui.progress(_('archiving (%s)') % relpath, None)
1562 1555 return total
1563 1556
1564 1557
1565 1558 @annotatesubrepoerror
1566 1559 def status(self, rev2, **opts):
1567 1560 rev1 = self._state[1]
1568 1561 if self._gitmissing() or not rev1:
1569 1562 # if the repo is missing, return no results
1570 1563 return [], [], [], [], [], [], []
1571 1564 modified, added, removed = [], [], []
1572 1565 self._gitupdatestat()
1573 1566 if rev2:
1574 1567 command = ['diff-tree', rev1, rev2]
1575 1568 else:
1576 1569 command = ['diff-index', rev1]
1577 1570 out = self._gitcommand(command)
1578 1571 for line in out.split('\n'):
1579 1572 tab = line.find('\t')
1580 1573 if tab == -1:
1581 1574 continue
1582 1575 status, f = line[tab - 1], line[tab + 1:]
1583 1576 if status == 'M':
1584 1577 modified.append(f)
1585 1578 elif status == 'A':
1586 1579 added.append(f)
1587 1580 elif status == 'D':
1588 1581 removed.append(f)
1589 1582
1590 1583 deleted, unknown, ignored, clean = [], [], [], []
1591 1584 return scmutil.status(modified, added, removed, deleted,
1592 1585 unknown, ignored, clean)
1593 1586
1594 1587 def shortid(self, revid):
1595 1588 return revid[:7]
1596 1589
1597 1590 types = {
1598 1591 'hg': hgsubrepo,
1599 1592 'svn': svnsubrepo,
1600 1593 'git': gitsubrepo,
1601 1594 }
General Comments 0
You need to be logged in to leave comments. Login now