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