##// END OF EJS Templates
subrepo/svn: improve error message on missing files...
Patrick Mezard -
r16530:e37199a1 stable
parent child Browse files
Show More
@@ -1,1215 +1,1222
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 """Return (changes, extchanges) where changes is True
684 if the working directory was changed, and extchanges is
685 True if any of these changes concern an external entry.
683 """Return (changes, extchanges, missing) where changes is True
684 if the working directory was changed, extchanges is
685 True if any of these changes concern an external entry and missing
686 is True if any change is a missing entry.
686 687 """
687 688 output, err = self._svncommand(['status', '--xml'])
688 externals, changes = [], []
689 externals, changes, missing = [], [], []
689 690 doc = xml.dom.minidom.parseString(output)
690 691 for e in doc.getElementsByTagName('entry'):
691 692 s = e.getElementsByTagName('wc-status')
692 693 if not s:
693 694 continue
694 695 item = s[0].getAttribute('item')
695 696 props = s[0].getAttribute('props')
696 697 path = e.getAttribute('path')
697 698 if item == 'external':
698 699 externals.append(path)
700 elif item == 'missing':
701 missing.append(path)
699 702 if (item not in ('', 'normal', 'unversioned', 'external')
700 703 or props not in ('', 'none', 'normal')):
701 704 changes.append(path)
702 705 for path in changes:
703 706 for ext in externals:
704 707 if path == ext or path.startswith(ext + os.sep):
705 return True, True
706 return bool(changes), False
708 return True, True, bool(missing)
709 return bool(changes), False, bool(missing)
707 710
708 711 def dirty(self, ignoreupdate=False):
709 712 if not self._wcchanged()[0]:
710 713 if self._state[1] in self._wcrevs() or ignoreupdate:
711 714 return False
712 715 return True
713 716
714 717 def basestate(self):
715 718 return self._wcrev()
716 719
717 720 def commit(self, text, user, date):
718 721 # user and date are out of our hands since svn is centralized
719 changed, extchanged = self._wcchanged()
722 changed, extchanged, missing = self._wcchanged()
720 723 if not changed:
721 724 return self._wcrev()
722 725 if extchanged:
723 726 # Do not try to commit externals
724 727 raise util.Abort(_('cannot commit svn externals'))
728 if missing:
729 # svn can commit with missing entries but aborting like hg
730 # seems a better approach.
731 raise util.Abort(_('cannot commit missing svn entries'))
725 732 commitinfo, err = self._svncommand(['commit', '-m', text])
726 733 self._ui.status(commitinfo)
727 734 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
728 735 if not newrev:
729 736 if not commitinfo.strip():
730 737 # Sometimes, our definition of "changed" differs from
731 738 # svn one. For instance, svn ignores missing files
732 739 # when committing. If there are only missing files, no
733 740 # commit is made, no output and no error code.
734 741 raise util.Abort(_('failed to commit svn changes'))
735 742 raise util.Abort(commitinfo.splitlines()[-1])
736 743 newrev = newrev.groups()[0]
737 744 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
738 745 return newrev
739 746
740 747 def remove(self):
741 748 if self.dirty():
742 749 self._ui.warn(_('not removing repo %s because '
743 750 'it has changes.\n' % self._path))
744 751 return
745 752 self._ui.note(_('removing subrepo %s\n') % self._path)
746 753
747 754 def onerror(function, path, excinfo):
748 755 if function is not os.remove:
749 756 raise
750 757 # read-only files cannot be unlinked under Windows
751 758 s = os.stat(path)
752 759 if (s.st_mode & stat.S_IWRITE) != 0:
753 760 raise
754 761 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
755 762 os.remove(path)
756 763
757 764 path = self._ctx._repo.wjoin(self._path)
758 765 shutil.rmtree(path, onerror=onerror)
759 766 try:
760 767 os.removedirs(os.path.dirname(path))
761 768 except OSError:
762 769 pass
763 770
764 771 def get(self, state, overwrite=False):
765 772 if overwrite:
766 773 self._svncommand(['revert', '--recursive'])
767 774 args = ['checkout']
768 775 if self._svnversion >= (1, 5):
769 776 args.append('--force')
770 777 # The revision must be specified at the end of the URL to properly
771 778 # update to a directory which has since been deleted and recreated.
772 779 args.append('%s@%s' % (state[0], state[1]))
773 780 status, err = self._svncommand(args, failok=True)
774 781 if not re.search('Checked out revision [0-9]+.', status):
775 782 if ('is already a working copy for a different URL' in err
776 and (self._wcchanged() == (False, False))):
783 and (self._wcchanged()[:2] == (False, False))):
777 784 # obstructed but clean working copy, so just blow it away.
778 785 self.remove()
779 786 self.get(state, overwrite=False)
780 787 return
781 788 raise util.Abort((status or err).splitlines()[-1])
782 789 self._ui.status(status)
783 790
784 791 def merge(self, state):
785 792 old = self._state[1]
786 793 new = state[1]
787 794 if new != self._wcrev():
788 795 dirty = old == self._wcrev() or self._wcchanged()[0]
789 796 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
790 797 self.get(state, False)
791 798
792 799 def push(self, opts):
793 800 # push is a no-op for SVN
794 801 return True
795 802
796 803 def files(self):
797 804 output = self._svncommand(['list', '--recursive', '--xml'])[0]
798 805 doc = xml.dom.minidom.parseString(output)
799 806 paths = []
800 807 for e in doc.getElementsByTagName('entry'):
801 808 kind = str(e.getAttribute('kind'))
802 809 if kind != 'file':
803 810 continue
804 811 name = ''.join(c.data for c
805 812 in e.getElementsByTagName('name')[0].childNodes
806 813 if c.nodeType == c.TEXT_NODE)
807 814 paths.append(name)
808 815 return paths
809 816
810 817 def filedata(self, name):
811 818 return self._svncommand(['cat'], name)[0]
812 819
813 820
814 821 class gitsubrepo(abstractsubrepo):
815 822 def __init__(self, ctx, path, state):
816 823 # TODO add git version check.
817 824 self._state = state
818 825 self._ctx = ctx
819 826 self._path = path
820 827 self._relpath = os.path.join(reporelpath(ctx._repo), path)
821 828 self._abspath = ctx._repo.wjoin(path)
822 829 self._subparent = ctx._repo
823 830 self._ui = ctx._repo.ui
824 831
825 832 def _gitcommand(self, commands, env=None, stream=False):
826 833 return self._gitdir(commands, env=env, stream=stream)[0]
827 834
828 835 def _gitdir(self, commands, env=None, stream=False):
829 836 return self._gitnodir(commands, env=env, stream=stream,
830 837 cwd=self._abspath)
831 838
832 839 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
833 840 """Calls the git command
834 841
835 842 The methods tries to call the git command. versions previor to 1.6.0
836 843 are not supported and very probably fail.
837 844 """
838 845 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
839 846 # unless ui.quiet is set, print git's stderr,
840 847 # which is mostly progress and useful info
841 848 errpipe = None
842 849 if self._ui.quiet:
843 850 errpipe = open(os.devnull, 'w')
844 851 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
845 852 close_fds=util.closefds,
846 853 stdout=subprocess.PIPE, stderr=errpipe)
847 854 if stream:
848 855 return p.stdout, None
849 856
850 857 retdata = p.stdout.read().strip()
851 858 # wait for the child to exit to avoid race condition.
852 859 p.wait()
853 860
854 861 if p.returncode != 0 and p.returncode != 1:
855 862 # there are certain error codes that are ok
856 863 command = commands[0]
857 864 if command in ('cat-file', 'symbolic-ref'):
858 865 return retdata, p.returncode
859 866 # for all others, abort
860 867 raise util.Abort('git %s error %d in %s' %
861 868 (command, p.returncode, self._relpath))
862 869
863 870 return retdata, p.returncode
864 871
865 872 def _gitmissing(self):
866 873 return not os.path.exists(os.path.join(self._abspath, '.git'))
867 874
868 875 def _gitstate(self):
869 876 return self._gitcommand(['rev-parse', 'HEAD'])
870 877
871 878 def _gitcurrentbranch(self):
872 879 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
873 880 if err:
874 881 current = None
875 882 return current
876 883
877 884 def _gitremote(self, remote):
878 885 out = self._gitcommand(['remote', 'show', '-n', remote])
879 886 line = out.split('\n')[1]
880 887 i = line.index('URL: ') + len('URL: ')
881 888 return line[i:]
882 889
883 890 def _githavelocally(self, revision):
884 891 out, code = self._gitdir(['cat-file', '-e', revision])
885 892 return code == 0
886 893
887 894 def _gitisancestor(self, r1, r2):
888 895 base = self._gitcommand(['merge-base', r1, r2])
889 896 return base == r1
890 897
891 898 def _gitisbare(self):
892 899 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
893 900
894 901 def _gitupdatestat(self):
895 902 """This must be run before git diff-index.
896 903 diff-index only looks at changes to file stat;
897 904 this command looks at file contents and updates the stat."""
898 905 self._gitcommand(['update-index', '-q', '--refresh'])
899 906
900 907 def _gitbranchmap(self):
901 908 '''returns 2 things:
902 909 a map from git branch to revision
903 910 a map from revision to branches'''
904 911 branch2rev = {}
905 912 rev2branch = {}
906 913
907 914 out = self._gitcommand(['for-each-ref', '--format',
908 915 '%(objectname) %(refname)'])
909 916 for line in out.split('\n'):
910 917 revision, ref = line.split(' ')
911 918 if (not ref.startswith('refs/heads/') and
912 919 not ref.startswith('refs/remotes/')):
913 920 continue
914 921 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
915 922 continue # ignore remote/HEAD redirects
916 923 branch2rev[ref] = revision
917 924 rev2branch.setdefault(revision, []).append(ref)
918 925 return branch2rev, rev2branch
919 926
920 927 def _gittracking(self, branches):
921 928 'return map of remote branch to local tracking branch'
922 929 # assumes no more than one local tracking branch for each remote
923 930 tracking = {}
924 931 for b in branches:
925 932 if b.startswith('refs/remotes/'):
926 933 continue
927 934 bname = b.split('/', 2)[2]
928 935 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
929 936 if remote:
930 937 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
931 938 tracking['refs/remotes/%s/%s' %
932 939 (remote, ref.split('/', 2)[2])] = b
933 940 return tracking
934 941
935 942 def _abssource(self, source):
936 943 if '://' not in source:
937 944 # recognize the scp syntax as an absolute source
938 945 colon = source.find(':')
939 946 if colon != -1 and '/' not in source[:colon]:
940 947 return source
941 948 self._subsource = source
942 949 return _abssource(self)
943 950
944 951 def _fetch(self, source, revision):
945 952 if self._gitmissing():
946 953 source = self._abssource(source)
947 954 self._ui.status(_('cloning subrepo %s from %s\n') %
948 955 (self._relpath, source))
949 956 self._gitnodir(['clone', source, self._abspath])
950 957 if self._githavelocally(revision):
951 958 return
952 959 self._ui.status(_('pulling subrepo %s from %s\n') %
953 960 (self._relpath, self._gitremote('origin')))
954 961 # try only origin: the originally cloned repo
955 962 self._gitcommand(['fetch'])
956 963 if not self._githavelocally(revision):
957 964 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
958 965 (revision, self._relpath))
959 966
960 967 def dirty(self, ignoreupdate=False):
961 968 if self._gitmissing():
962 969 return self._state[1] != ''
963 970 if self._gitisbare():
964 971 return True
965 972 if not ignoreupdate and self._state[1] != self._gitstate():
966 973 # different version checked out
967 974 return True
968 975 # check for staged changes or modified files; ignore untracked files
969 976 self._gitupdatestat()
970 977 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
971 978 return code == 1
972 979
973 980 def basestate(self):
974 981 return self._gitstate()
975 982
976 983 def get(self, state, overwrite=False):
977 984 source, revision, kind = state
978 985 if not revision:
979 986 self.remove()
980 987 return
981 988 self._fetch(source, revision)
982 989 # if the repo was set to be bare, unbare it
983 990 if self._gitisbare():
984 991 self._gitcommand(['config', 'core.bare', 'false'])
985 992 if self._gitstate() == revision:
986 993 self._gitcommand(['reset', '--hard', 'HEAD'])
987 994 return
988 995 elif self._gitstate() == revision:
989 996 if overwrite:
990 997 # first reset the index to unmark new files for commit, because
991 998 # reset --hard will otherwise throw away files added for commit,
992 999 # not just unmark them.
993 1000 self._gitcommand(['reset', 'HEAD'])
994 1001 self._gitcommand(['reset', '--hard', 'HEAD'])
995 1002 return
996 1003 branch2rev, rev2branch = self._gitbranchmap()
997 1004
998 1005 def checkout(args):
999 1006 cmd = ['checkout']
1000 1007 if overwrite:
1001 1008 # first reset the index to unmark new files for commit, because
1002 1009 # the -f option will otherwise throw away files added for
1003 1010 # commit, not just unmark them.
1004 1011 self._gitcommand(['reset', 'HEAD'])
1005 1012 cmd.append('-f')
1006 1013 self._gitcommand(cmd + args)
1007 1014
1008 1015 def rawcheckout():
1009 1016 # no branch to checkout, check it out with no branch
1010 1017 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1011 1018 self._relpath)
1012 1019 self._ui.warn(_('check out a git branch if you intend '
1013 1020 'to make changes\n'))
1014 1021 checkout(['-q', revision])
1015 1022
1016 1023 if revision not in rev2branch:
1017 1024 rawcheckout()
1018 1025 return
1019 1026 branches = rev2branch[revision]
1020 1027 firstlocalbranch = None
1021 1028 for b in branches:
1022 1029 if b == 'refs/heads/master':
1023 1030 # master trumps all other branches
1024 1031 checkout(['refs/heads/master'])
1025 1032 return
1026 1033 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1027 1034 firstlocalbranch = b
1028 1035 if firstlocalbranch:
1029 1036 checkout([firstlocalbranch])
1030 1037 return
1031 1038
1032 1039 tracking = self._gittracking(branch2rev.keys())
1033 1040 # choose a remote branch already tracked if possible
1034 1041 remote = branches[0]
1035 1042 if remote not in tracking:
1036 1043 for b in branches:
1037 1044 if b in tracking:
1038 1045 remote = b
1039 1046 break
1040 1047
1041 1048 if remote not in tracking:
1042 1049 # create a new local tracking branch
1043 1050 local = remote.split('/', 2)[2]
1044 1051 checkout(['-b', local, remote])
1045 1052 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1046 1053 # When updating to a tracked remote branch,
1047 1054 # if the local tracking branch is downstream of it,
1048 1055 # a normal `git pull` would have performed a "fast-forward merge"
1049 1056 # which is equivalent to updating the local branch to the remote.
1050 1057 # Since we are only looking at branching at update, we need to
1051 1058 # detect this situation and perform this action lazily.
1052 1059 if tracking[remote] != self._gitcurrentbranch():
1053 1060 checkout([tracking[remote]])
1054 1061 self._gitcommand(['merge', '--ff', remote])
1055 1062 else:
1056 1063 # a real merge would be required, just checkout the revision
1057 1064 rawcheckout()
1058 1065
1059 1066 def commit(self, text, user, date):
1060 1067 if self._gitmissing():
1061 1068 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1062 1069 cmd = ['commit', '-a', '-m', text]
1063 1070 env = os.environ.copy()
1064 1071 if user:
1065 1072 cmd += ['--author', user]
1066 1073 if date:
1067 1074 # git's date parser silently ignores when seconds < 1e9
1068 1075 # convert to ISO8601
1069 1076 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1070 1077 '%Y-%m-%dT%H:%M:%S %1%2')
1071 1078 self._gitcommand(cmd, env=env)
1072 1079 # make sure commit works otherwise HEAD might not exist under certain
1073 1080 # circumstances
1074 1081 return self._gitstate()
1075 1082
1076 1083 def merge(self, state):
1077 1084 source, revision, kind = state
1078 1085 self._fetch(source, revision)
1079 1086 base = self._gitcommand(['merge-base', revision, self._state[1]])
1080 1087 self._gitupdatestat()
1081 1088 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1082 1089
1083 1090 def mergefunc():
1084 1091 if base == revision:
1085 1092 self.get(state) # fast forward merge
1086 1093 elif base != self._state[1]:
1087 1094 self._gitcommand(['merge', '--no-commit', revision])
1088 1095
1089 1096 if self.dirty():
1090 1097 if self._gitstate() != revision:
1091 1098 dirty = self._gitstate() == self._state[1] or code != 0
1092 1099 if _updateprompt(self._ui, self, dirty,
1093 1100 self._state[1][:7], revision[:7]):
1094 1101 mergefunc()
1095 1102 else:
1096 1103 mergefunc()
1097 1104
1098 1105 def push(self, opts):
1099 1106 force = opts.get('force')
1100 1107
1101 1108 if not self._state[1]:
1102 1109 return True
1103 1110 if self._gitmissing():
1104 1111 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1105 1112 # if a branch in origin contains the revision, nothing to do
1106 1113 branch2rev, rev2branch = self._gitbranchmap()
1107 1114 if self._state[1] in rev2branch:
1108 1115 for b in rev2branch[self._state[1]]:
1109 1116 if b.startswith('refs/remotes/origin/'):
1110 1117 return True
1111 1118 for b, revision in branch2rev.iteritems():
1112 1119 if b.startswith('refs/remotes/origin/'):
1113 1120 if self._gitisancestor(self._state[1], revision):
1114 1121 return True
1115 1122 # otherwise, try to push the currently checked out branch
1116 1123 cmd = ['push']
1117 1124 if force:
1118 1125 cmd.append('--force')
1119 1126
1120 1127 current = self._gitcurrentbranch()
1121 1128 if current:
1122 1129 # determine if the current branch is even useful
1123 1130 if not self._gitisancestor(self._state[1], current):
1124 1131 self._ui.warn(_('unrelated git branch checked out '
1125 1132 'in subrepo %s\n') % self._relpath)
1126 1133 return False
1127 1134 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1128 1135 (current.split('/', 2)[2], self._relpath))
1129 1136 self._gitcommand(cmd + ['origin', current])
1130 1137 return True
1131 1138 else:
1132 1139 self._ui.warn(_('no branch checked out in subrepo %s\n'
1133 1140 'cannot push revision %s') %
1134 1141 (self._relpath, self._state[1]))
1135 1142 return False
1136 1143
1137 1144 def remove(self):
1138 1145 if self._gitmissing():
1139 1146 return
1140 1147 if self.dirty():
1141 1148 self._ui.warn(_('not removing repo %s because '
1142 1149 'it has changes.\n') % self._relpath)
1143 1150 return
1144 1151 # we can't fully delete the repository as it may contain
1145 1152 # local-only history
1146 1153 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1147 1154 self._gitcommand(['config', 'core.bare', 'true'])
1148 1155 for f in os.listdir(self._abspath):
1149 1156 if f == '.git':
1150 1157 continue
1151 1158 path = os.path.join(self._abspath, f)
1152 1159 if os.path.isdir(path) and not os.path.islink(path):
1153 1160 shutil.rmtree(path)
1154 1161 else:
1155 1162 os.remove(path)
1156 1163
1157 1164 def archive(self, ui, archiver, prefix):
1158 1165 source, revision = self._state
1159 1166 if not revision:
1160 1167 return
1161 1168 self._fetch(source, revision)
1162 1169
1163 1170 # Parse git's native archive command.
1164 1171 # This should be much faster than manually traversing the trees
1165 1172 # and objects with many subprocess calls.
1166 1173 tarstream = self._gitcommand(['archive', revision], stream=True)
1167 1174 tar = tarfile.open(fileobj=tarstream, mode='r|')
1168 1175 relpath = subrelpath(self)
1169 1176 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1170 1177 for i, info in enumerate(tar):
1171 1178 if info.isdir():
1172 1179 continue
1173 1180 if info.issym():
1174 1181 data = info.linkname
1175 1182 else:
1176 1183 data = tar.extractfile(info).read()
1177 1184 archiver.addfile(os.path.join(prefix, self._path, info.name),
1178 1185 info.mode, info.issym(), data)
1179 1186 ui.progress(_('archiving (%s)') % relpath, i + 1,
1180 1187 unit=_('files'))
1181 1188 ui.progress(_('archiving (%s)') % relpath, None)
1182 1189
1183 1190
1184 1191 def status(self, rev2, **opts):
1185 1192 rev1 = self._state[1]
1186 1193 if self._gitmissing() or not rev1:
1187 1194 # if the repo is missing, return no results
1188 1195 return [], [], [], [], [], [], []
1189 1196 modified, added, removed = [], [], []
1190 1197 self._gitupdatestat()
1191 1198 if rev2:
1192 1199 command = ['diff-tree', rev1, rev2]
1193 1200 else:
1194 1201 command = ['diff-index', rev1]
1195 1202 out = self._gitcommand(command)
1196 1203 for line in out.split('\n'):
1197 1204 tab = line.find('\t')
1198 1205 if tab == -1:
1199 1206 continue
1200 1207 status, f = line[tab - 1], line[tab + 1:]
1201 1208 if status == 'M':
1202 1209 modified.append(f)
1203 1210 elif status == 'A':
1204 1211 added.append(f)
1205 1212 elif status == 'D':
1206 1213 removed.append(f)
1207 1214
1208 1215 deleted = unknown = ignored = clean = []
1209 1216 return modified, added, removed, deleted, unknown, ignored, clean
1210 1217
1211 1218 types = {
1212 1219 'hg': hgsubrepo,
1213 1220 'svn': svnsubrepo,
1214 1221 'git': gitsubrepo,
1215 1222 }
@@ -1,566 +1,566
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 abort: failed to commit svn changes
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]
General Comments 0
You need to be logged in to leave comments. Login now