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