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