##// END OF EJS Templates
subrepo: compare svn subrepo state to last committed revision...
Patrick Mezard -
r13287:d0e0d3d4 stable
parent child Browse files
Show More
@@ -1,610 +1,620
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, urlparse, posixpath
9 9 import stat, subprocess
10 10 from i18n import _
11 11 import config, util, node, error, cmdutil
12 12 hg = None
13 13
14 14 nullstate = ('', '', 'empty')
15 15
16 16 def state(ctx, ui):
17 17 """return a state dict, mapping subrepo paths configured in .hgsub
18 18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 19 (key in types dict))
20 20 """
21 21 p = config.config()
22 22 def read(f, sections=None, remap=None):
23 23 if f in ctx:
24 24 try:
25 25 data = ctx[f].data()
26 26 except IOError, err:
27 27 if err.errno != errno.ENOENT:
28 28 raise
29 29 # handle missing subrepo spec files as removed
30 30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 31 return
32 32 p.parse(f, data, sections, remap, read)
33 33 else:
34 34 raise util.Abort(_("subrepo spec file %s not found") % f)
35 35
36 36 if '.hgsub' in ctx:
37 37 read('.hgsub')
38 38
39 39 for path, src in ui.configitems('subpaths'):
40 40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 41
42 42 rev = {}
43 43 if '.hgsubstate' in ctx:
44 44 try:
45 45 for l in ctx['.hgsubstate'].data().splitlines():
46 46 revision, path = l.split(" ", 1)
47 47 rev[path] = revision
48 48 except IOError, err:
49 49 if err.errno != errno.ENOENT:
50 50 raise
51 51
52 52 state = {}
53 53 for path, src in p[''].items():
54 54 kind = 'hg'
55 55 if src.startswith('['):
56 56 if ']' not in src:
57 57 raise util.Abort(_('missing ] in subrepo source'))
58 58 kind, src = src.split(']', 1)
59 59 kind = kind[1:]
60 60
61 61 for pattern, repl in p.items('subpaths'):
62 62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 63 # does a string decode.
64 64 repl = repl.encode('string-escape')
65 65 # However, we still want to allow back references to go
66 66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 67 # extra escapes are needed because re.sub string decodes.
68 68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 69 try:
70 70 src = re.sub(pattern, repl, src, 1)
71 71 except re.error, e:
72 72 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 73 % (p.source('subpaths', pattern), e))
74 74
75 75 state[path] = (src.strip(), rev.get(path, ''), kind)
76 76
77 77 return state
78 78
79 79 def writestate(repo, state):
80 80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 81 repo.wwrite('.hgsubstate',
82 82 ''.join(['%s %s\n' % (state[s][1], s)
83 83 for s in sorted(state)]), '')
84 84
85 85 def submerge(repo, wctx, mctx, actx):
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)
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)
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)
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 else:
140 140 if repo.ui.promptchoice(
141 141 _(' local changed subrepository %s which remote removed\n'
142 142 'use (c)hanged version or (d)elete?') % s,
143 143 (_('&Changed'), _('&Delete')), 0):
144 144 debug(s, "prompt remove")
145 145 wctx.sub(s).remove()
146 146
147 147 for s, r in s2.items():
148 148 if s in s1:
149 149 continue
150 150 elif s not in sa:
151 151 debug(s, "remote added, get", r)
152 152 mctx.sub(s).get(r)
153 153 sm[s] = r
154 154 elif r != sa[s]:
155 155 if repo.ui.promptchoice(
156 156 _(' remote changed subrepository %s which local removed\n'
157 157 'use (c)hanged version or (d)elete?') % s,
158 158 (_('&Changed'), _('&Delete')), 0) == 0:
159 159 debug(s, "prompt recreate", r)
160 160 wctx.sub(s).get(r)
161 161 sm[s] = r
162 162
163 163 # record merged .hgsubstate
164 164 writestate(repo, sm)
165 165
166 166 def reporelpath(repo):
167 167 """return path to this (sub)repo as seen from outermost repo"""
168 168 parent = repo
169 169 while hasattr(parent, '_subparent'):
170 170 parent = parent._subparent
171 171 return repo.root[len(parent.root)+1:]
172 172
173 173 def subrelpath(sub):
174 174 """return path to this subrepo as seen from outermost repo"""
175 175 if not hasattr(sub, '_repo'):
176 176 return sub._path
177 177 return reporelpath(sub._repo)
178 178
179 179 def _abssource(repo, push=False, abort=True):
180 180 """return pull/push path of repo - either based on parent repo .hgsub info
181 181 or on the top repo config. Abort or return None if no source found."""
182 182 if hasattr(repo, '_subparent'):
183 183 source = repo._subsource
184 184 if source.startswith('/') or '://' in source:
185 185 return source
186 186 parent = _abssource(repo._subparent, push, abort=False)
187 187 if parent:
188 188 if '://' in parent:
189 189 if parent[-1] == '/':
190 190 parent = parent[:-1]
191 191 r = urlparse.urlparse(parent + '/' + source)
192 192 r = urlparse.urlunparse((r[0], r[1],
193 193 posixpath.normpath(r[2]),
194 194 r[3], r[4], r[5]))
195 195 return r
196 196 else: # plain file system path
197 197 return posixpath.normpath(os.path.join(parent, repo._subsource))
198 198 else: # recursion reached top repo
199 199 if hasattr(repo, '_subtoppath'):
200 200 return repo._subtoppath
201 201 if push and repo.ui.config('paths', 'default-push'):
202 202 return repo.ui.config('paths', 'default-push')
203 203 if repo.ui.config('paths', 'default'):
204 204 return repo.ui.config('paths', 'default')
205 205 if abort:
206 206 raise util.Abort(_("default path for subrepository %s not found") %
207 207 reporelpath(repo))
208 208
209 209 def itersubrepos(ctx1, ctx2):
210 210 """find subrepos in ctx1 or ctx2"""
211 211 # Create a (subpath, ctx) mapping where we prefer subpaths from
212 212 # ctx1. The subpaths from ctx2 are important when the .hgsub file
213 213 # has been modified (in ctx2) but not yet committed (in ctx1).
214 214 subpaths = dict.fromkeys(ctx2.substate, ctx2)
215 215 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
216 216 for subpath, ctx in sorted(subpaths.iteritems()):
217 217 yield subpath, ctx.sub(subpath)
218 218
219 219 def subrepo(ctx, path):
220 220 """return instance of the right subrepo class for subrepo in path"""
221 221 # subrepo inherently violates our import layering rules
222 222 # because it wants to make repo objects from deep inside the stack
223 223 # so we manually delay the circular imports to not break
224 224 # scripts that don't use our demand-loading
225 225 global hg
226 226 import hg as h
227 227 hg = h
228 228
229 229 util.path_auditor(ctx._repo.root)(path)
230 230 state = ctx.substate.get(path, nullstate)
231 231 if state[2] not in types:
232 232 raise util.Abort(_('unknown subrepo type %s') % state[2])
233 233 return types[state[2]](ctx, path, state[:2])
234 234
235 235 # subrepo classes need to implement the following abstract class:
236 236
237 237 class abstractsubrepo(object):
238 238
239 239 def dirty(self):
240 240 """returns true if the dirstate of the subrepo does not match
241 241 current stored state
242 242 """
243 243 raise NotImplementedError
244 244
245 245 def checknested(self, path):
246 246 """check if path is a subrepository within this repository"""
247 247 return False
248 248
249 249 def commit(self, text, user, date):
250 250 """commit the current changes to the subrepo with the given
251 251 log message. Use given user and date if possible. Return the
252 252 new state of the subrepo.
253 253 """
254 254 raise NotImplementedError
255 255
256 256 def remove(self):
257 257 """remove the subrepo
258 258
259 259 (should verify the dirstate is not dirty first)
260 260 """
261 261 raise NotImplementedError
262 262
263 263 def get(self, state):
264 264 """run whatever commands are needed to put the subrepo into
265 265 this state
266 266 """
267 267 raise NotImplementedError
268 268
269 269 def merge(self, state):
270 270 """merge currently-saved state with the new state."""
271 271 raise NotImplementedError
272 272
273 273 def push(self, force):
274 274 """perform whatever action is analogous to 'hg push'
275 275
276 276 This may be a no-op on some systems.
277 277 """
278 278 raise NotImplementedError
279 279
280 280 def add(self, ui, match, dryrun, prefix):
281 281 return []
282 282
283 283 def status(self, rev2, **opts):
284 284 return [], [], [], [], [], [], []
285 285
286 286 def diff(self, diffopts, node2, match, prefix, **opts):
287 287 pass
288 288
289 289 def outgoing(self, ui, dest, opts):
290 290 return 1
291 291
292 292 def incoming(self, ui, source, opts):
293 293 return 1
294 294
295 295 def files(self):
296 296 """return filename iterator"""
297 297 raise NotImplementedError
298 298
299 299 def filedata(self, name):
300 300 """return file data"""
301 301 raise NotImplementedError
302 302
303 303 def fileflags(self, name):
304 304 """return file flags"""
305 305 return ''
306 306
307 307 def archive(self, archiver, prefix):
308 308 for name in self.files():
309 309 flags = self.fileflags(name)
310 310 mode = 'x' in flags and 0755 or 0644
311 311 symlink = 'l' in flags
312 312 archiver.addfile(os.path.join(prefix, self._path, name),
313 313 mode, symlink, self.filedata(name))
314 314
315 315
316 316 class hgsubrepo(abstractsubrepo):
317 317 def __init__(self, ctx, path, state):
318 318 self._path = path
319 319 self._state = state
320 320 r = ctx._repo
321 321 root = r.wjoin(path)
322 322 create = False
323 323 if not os.path.exists(os.path.join(root, '.hg')):
324 324 create = True
325 325 util.makedirs(root)
326 326 self._repo = hg.repository(r.ui, root, create=create)
327 327 self._repo._subparent = r
328 328 self._repo._subsource = state[0]
329 329
330 330 if create:
331 331 fp = self._repo.opener("hgrc", "w", text=True)
332 332 fp.write('[paths]\n')
333 333
334 334 def addpathconfig(key, value):
335 335 if value:
336 336 fp.write('%s = %s\n' % (key, value))
337 337 self._repo.ui.setconfig('paths', key, value)
338 338
339 339 defpath = _abssource(self._repo, abort=False)
340 340 defpushpath = _abssource(self._repo, True, abort=False)
341 341 addpathconfig('default', defpath)
342 342 if defpath != defpushpath:
343 343 addpathconfig('default-push', defpushpath)
344 344 fp.close()
345 345
346 346 def add(self, ui, match, dryrun, prefix):
347 347 return cmdutil.add(ui, self._repo, match, dryrun, True,
348 348 os.path.join(prefix, self._path))
349 349
350 350 def status(self, rev2, **opts):
351 351 try:
352 352 rev1 = self._state[1]
353 353 ctx1 = self._repo[rev1]
354 354 ctx2 = self._repo[rev2]
355 355 return self._repo.status(ctx1, ctx2, **opts)
356 356 except error.RepoLookupError, inst:
357 357 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
358 358 % (inst, subrelpath(self)))
359 359 return [], [], [], [], [], [], []
360 360
361 361 def diff(self, diffopts, node2, match, prefix, **opts):
362 362 try:
363 363 node1 = node.bin(self._state[1])
364 364 # We currently expect node2 to come from substate and be
365 365 # in hex format
366 366 if node2 is not None:
367 367 node2 = node.bin(node2)
368 368 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
369 369 node1, node2, match,
370 370 prefix=os.path.join(prefix, self._path),
371 371 listsubrepos=True, **opts)
372 372 except error.RepoLookupError, inst:
373 373 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
374 374 % (inst, subrelpath(self)))
375 375
376 376 def archive(self, archiver, prefix):
377 377 abstractsubrepo.archive(self, archiver, prefix)
378 378
379 379 rev = self._state[1]
380 380 ctx = self._repo[rev]
381 381 for subpath in ctx.substate:
382 382 s = subrepo(ctx, subpath)
383 383 s.archive(archiver, os.path.join(prefix, self._path))
384 384
385 385 def dirty(self):
386 386 r = self._state[1]
387 387 if r == '':
388 388 return True
389 389 w = self._repo[None]
390 390 if w.p1() != self._repo[r]: # version checked out change
391 391 return True
392 392 return w.dirty() # working directory changed
393 393
394 394 def checknested(self, path):
395 395 return self._repo._checknested(self._repo.wjoin(path))
396 396
397 397 def commit(self, text, user, date):
398 398 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
399 399 n = self._repo.commit(text, user, date)
400 400 if not n:
401 401 return self._repo['.'].hex() # different version checked out
402 402 return node.hex(n)
403 403
404 404 def remove(self):
405 405 # we can't fully delete the repository as it may contain
406 406 # local-only history
407 407 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
408 408 hg.clean(self._repo, node.nullid, False)
409 409
410 410 def _get(self, state):
411 411 source, revision, kind = state
412 412 try:
413 413 self._repo.lookup(revision)
414 414 except error.RepoError:
415 415 self._repo._subsource = source
416 416 srcurl = _abssource(self._repo)
417 417 self._repo.ui.status(_('pulling subrepo %s from %s\n')
418 418 % (subrelpath(self), srcurl))
419 419 other = hg.repository(self._repo.ui, srcurl)
420 420 self._repo.pull(other)
421 421
422 422 def get(self, state):
423 423 self._get(state)
424 424 source, revision, kind = state
425 425 self._repo.ui.debug("getting subrepo %s\n" % self._path)
426 426 hg.clean(self._repo, revision, False)
427 427
428 428 def merge(self, state):
429 429 self._get(state)
430 430 cur = self._repo['.']
431 431 dst = self._repo[state[1]]
432 432 anc = dst.ancestor(cur)
433 433 if anc == cur:
434 434 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
435 435 hg.update(self._repo, state[1])
436 436 elif anc == dst:
437 437 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
438 438 else:
439 439 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
440 440 hg.merge(self._repo, state[1], remind=False)
441 441
442 442 def push(self, force):
443 443 # push subrepos depth-first for coherent ordering
444 444 c = self._repo['']
445 445 subs = c.substate # only repos that are committed
446 446 for s in sorted(subs):
447 447 if not c.sub(s).push(force):
448 448 return False
449 449
450 450 dsturl = _abssource(self._repo, True)
451 451 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
452 452 (subrelpath(self), dsturl))
453 453 other = hg.repository(self._repo.ui, dsturl)
454 454 return self._repo.push(other, force)
455 455
456 456 def outgoing(self, ui, dest, opts):
457 457 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
458 458
459 459 def incoming(self, ui, source, opts):
460 460 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
461 461
462 462 def files(self):
463 463 rev = self._state[1]
464 464 ctx = self._repo[rev]
465 465 return ctx.manifest()
466 466
467 467 def filedata(self, name):
468 468 rev = self._state[1]
469 469 return self._repo[rev][name].data()
470 470
471 471 def fileflags(self, name):
472 472 rev = self._state[1]
473 473 ctx = self._repo[rev]
474 474 return ctx.flags(name)
475 475
476 476
477 477 class svnsubrepo(abstractsubrepo):
478 478 def __init__(self, ctx, path, state):
479 479 self._path = path
480 480 self._state = state
481 481 self._ctx = ctx
482 482 self._ui = ctx._repo.ui
483 483
484 484 def _svncommand(self, commands, filename=''):
485 485 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
486 486 cmd = ['svn'] + commands + [path]
487 487 cmd = [util.shellquote(arg) for arg in cmd]
488 488 cmd = util.quotecommand(' '.join(cmd))
489 489 env = dict(os.environ)
490 490 # Avoid localized output, preserve current locale for everything else.
491 491 env['LC_MESSAGES'] = 'C'
492 492 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
493 493 close_fds=util.closefds,
494 494 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
495 495 universal_newlines=True, env=env)
496 496 stdout, stderr = p.communicate()
497 497 stderr = stderr.strip()
498 498 if stderr:
499 499 raise util.Abort(stderr)
500 500 return stdout
501 501
502 def _wcrev(self):
502 def _wcrevs(self):
503 # Get the working directory revision as well as the last
504 # commit revision so we can compare the subrepo state with
505 # both. We used to store the working directory one.
503 506 output = self._svncommand(['info', '--xml'])
504 507 doc = xml.dom.minidom.parseString(output)
505 508 entries = doc.getElementsByTagName('entry')
506 if not entries:
507 return '0'
508 return str(entries[0].getAttribute('revision')) or '0'
509 lastrev, rev = '0', '0'
510 if entries:
511 rev = str(entries[0].getAttribute('revision')) or '0'
512 commits = entries[0].getElementsByTagName('commit')
513 if commits:
514 lastrev = str(commits[0].getAttribute('revision')) or '0'
515 return (lastrev, rev)
516
517 def _wcrev(self):
518 return self._wcrevs()[0]
509 519
510 520 def _wcchanged(self):
511 521 """Return (changes, extchanges) where changes is True
512 522 if the working directory was changed, and extchanges is
513 523 True if any of these changes concern an external entry.
514 524 """
515 525 output = self._svncommand(['status', '--xml'])
516 526 externals, changes = [], []
517 527 doc = xml.dom.minidom.parseString(output)
518 528 for e in doc.getElementsByTagName('entry'):
519 529 s = e.getElementsByTagName('wc-status')
520 530 if not s:
521 531 continue
522 532 item = s[0].getAttribute('item')
523 533 props = s[0].getAttribute('props')
524 534 path = e.getAttribute('path')
525 535 if item == 'external':
526 536 externals.append(path)
527 537 if (item not in ('', 'normal', 'unversioned', 'external')
528 538 or props not in ('', 'none')):
529 539 changes.append(path)
530 540 for path in changes:
531 541 for ext in externals:
532 542 if path == ext or path.startswith(ext + os.sep):
533 543 return True, True
534 544 return bool(changes), False
535 545
536 546 def dirty(self):
537 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
547 if self._state[1] in self._wcrevs() and not self._wcchanged()[0]:
538 548 return False
539 549 return True
540 550
541 551 def commit(self, text, user, date):
542 552 # user and date are out of our hands since svn is centralized
543 553 changed, extchanged = self._wcchanged()
544 554 if not changed:
545 555 return self._wcrev()
546 556 if extchanged:
547 557 # Do not try to commit externals
548 558 raise util.Abort(_('cannot commit svn externals'))
549 559 commitinfo = self._svncommand(['commit', '-m', text])
550 560 self._ui.status(commitinfo)
551 561 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
552 562 if not newrev:
553 563 raise util.Abort(commitinfo.splitlines()[-1])
554 564 newrev = newrev.groups()[0]
555 565 self._ui.status(self._svncommand(['update', '-r', newrev]))
556 566 return newrev
557 567
558 568 def remove(self):
559 569 if self.dirty():
560 570 self._ui.warn(_('not removing repo %s because '
561 571 'it has changes.\n' % self._path))
562 572 return
563 573 self._ui.note(_('removing subrepo %s\n') % self._path)
564 574
565 575 def onerror(function, path, excinfo):
566 576 if function is not os.remove:
567 577 raise
568 578 # read-only files cannot be unlinked under Windows
569 579 s = os.stat(path)
570 580 if (s.st_mode & stat.S_IWRITE) != 0:
571 581 raise
572 582 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
573 583 os.remove(path)
574 584
575 585 path = self._ctx._repo.wjoin(self._path)
576 586 shutil.rmtree(path, onerror=onerror)
577 587 try:
578 588 os.removedirs(os.path.dirname(path))
579 589 except OSError:
580 590 pass
581 591
582 592 def get(self, state):
583 593 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
584 594 if not re.search('Checked out revision [0-9]+.', status):
585 595 raise util.Abort(status.splitlines()[-1])
586 596 self._ui.status(status)
587 597
588 598 def merge(self, state):
589 599 old = int(self._state[1])
590 600 new = int(state[1])
591 601 if new > old:
592 602 self.get(state)
593 603
594 604 def push(self, force):
595 605 # push is a no-op for SVN
596 606 return True
597 607
598 608 def files(self):
599 609 output = self._svncommand(['list'])
600 610 # This works because svn forbids \n in filenames.
601 611 return output.splitlines()
602 612
603 613 def filedata(self, name):
604 614 return self._svncommand(['cat'], name)
605 615
606 616
607 617 types = {
608 618 'hg': hgsubrepo,
609 619 'svn': svnsubrepo,
610 620 }
@@ -1,247 +1,266
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 add an unrelated revision in svn and update the subrepo to without
127 bringing any changes.
128
129 $ svn mkdir --parents "$SVNREPO/unrelated" -m 'create unrelated'
130
131 Committed revision 4.
132 $ svn up s
133
134 Fetching external item into 's/externals'
135 External at revision 1.
136
137 At revision 4.
138 $ hg sum
139 parent: 2:* tip (glob)
140 Message!
141 branch: default
142 commit: (clean)
143 update: (current)
144
126 145 $ echo a > s/a
127 146
128 147 should be empty despite change to s/a
129 148
130 149 $ hg st
131 150
132 151 add a commit from svn
133 152
134 153 $ cd "$WCROOT"/src
135 154 $ svn up
136 155 U alpha
137 156
138 157 Fetching external item into 'externals'
139 158 A externals/other
140 159 Updated external to revision 1.
141 160
142 Updated to revision 3.
161 Updated to revision 4.
143 162 $ echo xyz >> alpha
144 163 $ svn propset svn:mime-type 'text/xml' alpha
145 164 property 'svn:mime-type' set on 'alpha'
146 165 $ svn ci -m 'amend a from svn'
147 166 Sending src/alpha
148 167 Transmitting file data .
149 Committed revision 4.
168 Committed revision 5.
150 169 $ cd ../../sub/t
151 170
152 171 this commit from hg will fail
153 172
154 173 $ echo zzz >> s/alpha
155 174 $ hg ci -m 'amend alpha from hg'
156 175 committing subrepository s
157 176 abort: svn: Commit failed (details follow):
158 177 svn: (Out of date)?.*/src/alpha.*(is out of date)? (re)
159 178 [255]
160 179 $ svn revert -q s/alpha
161 180
162 181 this commit fails because of meta changes
163 182
164 183 $ svn propset svn:mime-type 'text/html' s/alpha
165 184 property 'svn:mime-type' set on 's/alpha'
166 185 $ hg ci -m 'amend alpha from hg'
167 186 committing subrepository s
168 187 abort: svn: Commit failed (details follow):
169 188 svn: (Out of date)?.*/src/alpha.*(is out of date)? (re)
170 189 [255]
171 190 $ svn revert -q s/alpha
172 191
173 192 this commit fails because of externals changes
174 193
175 194 $ echo zzz > s/externals/other
176 195 $ hg ci -m 'amend externals from hg'
177 196 committing subrepository s
178 197 abort: cannot commit svn externals
179 198 [255]
180 199 $ hg diff --subrepos -r 1:2 | grep -v diff
181 200 --- a/.hgsubstate Thu Jan 01 00:00:00 1970 +0000
182 201 +++ b/.hgsubstate Thu Jan 01 00:00:00 1970 +0000
183 202 @@ -1,2 +1,2 @@
184 203 -2 s
185 204 +3 s
186 205 2 subdir/s
187 206 --- a/a Thu Jan 01 00:00:00 1970 +0000
188 207 +++ b/a Thu Jan 01 00:00:00 1970 +0000
189 208 @@ -1,1 +1,2 @@
190 209 a
191 210 +a
192 211 $ svn revert -q s/externals/other
193 212
194 213 this commit fails because of externals meta changes
195 214
196 215 $ svn propset svn:mime-type 'text/html' s/externals/other
197 216 property 'svn:mime-type' set on 's/externals/other'
198 217 $ hg ci -m 'amend externals from hg'
199 218 committing subrepository s
200 219 abort: cannot commit svn externals
201 220 [255]
202 221 $ svn revert -q s/externals/other
203 222
204 223 clone
205 224
206 225 $ cd ..
207 226 $ hg clone t tc | fix_path
208 227 updating to branch default
209 228 A tc/subdir/s/alpha
210 229 U tc/subdir/s
211 230
212 231 Fetching external item into 'tc/subdir/s/externals'
213 232 A tc/subdir/s/externals/other
214 233 Checked out external at revision 1.
215 234
216 235 Checked out revision 2.
217 236 A tc/s/alpha
218 237 U tc/s
219 238
220 239 Fetching external item into 'tc/s/externals'
221 240 A tc/s/externals/other
222 241 Checked out external at revision 1.
223 242
224 243 Checked out revision 3.
225 244 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
226 245 $ cd tc
227 246
228 247 debugsub in clone
229 248
230 249 $ hg debugsub
231 250 path s
232 251 source file://*/svn-repo/src (glob)
233 252 revision 3
234 253 path subdir/s
235 254 source file://*/svn-repo/src (glob)
236 255 revision 2
237 256
238 257 verify subrepo is contained within the repo directory
239 258
240 259 $ python -c "import os.path; print os.path.exists('s')"
241 260 True
242 261
243 262 update to nullrev (must delete the subrepo)
244 263
245 264 $ hg up null
246 265 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
247 266 $ ls
General Comments 0
You need to be logged in to leave comments. Login now