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