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