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