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