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