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