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