##// END OF EJS Templates
subrepo: backout 67fbe566eff1, --force requires svn >= 1.5
Patrick Mezard -
r13559:42a34c0a stable
parent child Browse files
Show More
@@ -1,1015 +1,1014 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, urlparse, posixpath
9 9 import stat, subprocess, tarfile
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, 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 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 _updateprompt(ui, sub, dirty, local, remote):
167 167 if dirty:
168 168 msg = (_(' subrepository sources for %s differ\n'
169 169 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
170 170 % (subrelpath(sub), local, remote))
171 171 else:
172 172 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
173 173 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
174 174 % (subrelpath(sub), local, remote))
175 175 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
176 176
177 177 def reporelpath(repo):
178 178 """return path to this (sub)repo as seen from outermost repo"""
179 179 parent = repo
180 180 while hasattr(parent, '_subparent'):
181 181 parent = parent._subparent
182 182 return repo.root[len(parent.root)+1:]
183 183
184 184 def subrelpath(sub):
185 185 """return path to this subrepo as seen from outermost repo"""
186 186 if hasattr(sub, '_relpath'):
187 187 return sub._relpath
188 188 if not hasattr(sub, '_repo'):
189 189 return sub._path
190 190 return reporelpath(sub._repo)
191 191
192 192 def _abssource(repo, push=False, abort=True):
193 193 """return pull/push path of repo - either based on parent repo .hgsub info
194 194 or on the top repo config. Abort or return None if no source found."""
195 195 if hasattr(repo, '_subparent'):
196 196 source = repo._subsource
197 197 if source.startswith('/') or '://' in source:
198 198 return source
199 199 parent = _abssource(repo._subparent, push, abort=False)
200 200 if parent:
201 201 if '://' in parent:
202 202 if parent[-1] == '/':
203 203 parent = parent[:-1]
204 204 r = urlparse.urlparse(parent + '/' + source)
205 205 r = urlparse.urlunparse((r[0], r[1],
206 206 posixpath.normpath(r[2]),
207 207 r[3], r[4], r[5]))
208 208 return r
209 209 else: # plain file system path
210 210 return posixpath.normpath(os.path.join(parent, repo._subsource))
211 211 else: # recursion reached top repo
212 212 if hasattr(repo, '_subtoppath'):
213 213 return repo._subtoppath
214 214 if push and repo.ui.config('paths', 'default-push'):
215 215 return repo.ui.config('paths', 'default-push')
216 216 if repo.ui.config('paths', 'default'):
217 217 return repo.ui.config('paths', 'default')
218 218 if abort:
219 219 raise util.Abort(_("default path for subrepository %s not found") %
220 220 reporelpath(repo))
221 221
222 222 def itersubrepos(ctx1, ctx2):
223 223 """find subrepos in ctx1 or ctx2"""
224 224 # Create a (subpath, ctx) mapping where we prefer subpaths from
225 225 # ctx1. The subpaths from ctx2 are important when the .hgsub file
226 226 # has been modified (in ctx2) but not yet committed (in ctx1).
227 227 subpaths = dict.fromkeys(ctx2.substate, ctx2)
228 228 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
229 229 for subpath, ctx in sorted(subpaths.iteritems()):
230 230 yield subpath, ctx.sub(subpath)
231 231
232 232 def subrepo(ctx, path):
233 233 """return instance of the right subrepo class for subrepo in path"""
234 234 # subrepo inherently violates our import layering rules
235 235 # because it wants to make repo objects from deep inside the stack
236 236 # so we manually delay the circular imports to not break
237 237 # scripts that don't use our demand-loading
238 238 global hg
239 239 import hg as h
240 240 hg = h
241 241
242 242 util.path_auditor(ctx._repo.root)(path)
243 243 state = ctx.substate.get(path, nullstate)
244 244 if state[2] not in types:
245 245 raise util.Abort(_('unknown subrepo type %s') % state[2])
246 246 return types[state[2]](ctx, path, state[:2])
247 247
248 248 # subrepo classes need to implement the following abstract class:
249 249
250 250 class abstractsubrepo(object):
251 251
252 252 def dirty(self, ignoreupdate=False):
253 253 """returns true if the dirstate of the subrepo is dirty or does not
254 254 match current stored state. If ignoreupdate is true, only check
255 255 whether the subrepo has uncommitted changes in its dirstate.
256 256 """
257 257 raise NotImplementedError
258 258
259 259 def checknested(self, path):
260 260 """check if path is a subrepository within this repository"""
261 261 return False
262 262
263 263 def commit(self, text, user, date):
264 264 """commit the current changes to the subrepo with the given
265 265 log message. Use given user and date if possible. Return the
266 266 new state of the subrepo.
267 267 """
268 268 raise NotImplementedError
269 269
270 270 def remove(self):
271 271 """remove the subrepo
272 272
273 273 (should verify the dirstate is not dirty first)
274 274 """
275 275 raise NotImplementedError
276 276
277 277 def get(self, state, overwrite=False):
278 278 """run whatever commands are needed to put the subrepo into
279 279 this state
280 280 """
281 281 raise NotImplementedError
282 282
283 283 def merge(self, state):
284 284 """merge currently-saved state with the new state."""
285 285 raise NotImplementedError
286 286
287 287 def push(self, force):
288 288 """perform whatever action is analogous to 'hg push'
289 289
290 290 This may be a no-op on some systems.
291 291 """
292 292 raise NotImplementedError
293 293
294 294 def add(self, ui, match, dryrun, prefix):
295 295 return []
296 296
297 297 def status(self, rev2, **opts):
298 298 return [], [], [], [], [], [], []
299 299
300 300 def diff(self, diffopts, node2, match, prefix, **opts):
301 301 pass
302 302
303 303 def outgoing(self, ui, dest, opts):
304 304 return 1
305 305
306 306 def incoming(self, ui, source, opts):
307 307 return 1
308 308
309 309 def files(self):
310 310 """return filename iterator"""
311 311 raise NotImplementedError
312 312
313 313 def filedata(self, name):
314 314 """return file data"""
315 315 raise NotImplementedError
316 316
317 317 def fileflags(self, name):
318 318 """return file flags"""
319 319 return ''
320 320
321 321 def archive(self, ui, archiver, prefix):
322 322 files = self.files()
323 323 total = len(files)
324 324 relpath = subrelpath(self)
325 325 ui.progress(_('archiving (%s)') % relpath, 0,
326 326 unit=_('files'), total=total)
327 327 for i, name in enumerate(files):
328 328 flags = self.fileflags(name)
329 329 mode = 'x' in flags and 0755 or 0644
330 330 symlink = 'l' in flags
331 331 archiver.addfile(os.path.join(prefix, self._path, name),
332 332 mode, symlink, self.filedata(name))
333 333 ui.progress(_('archiving (%s)') % relpath, i + 1,
334 334 unit=_('files'), total=total)
335 335 ui.progress(_('archiving (%s)') % relpath, None)
336 336
337 337
338 338 class hgsubrepo(abstractsubrepo):
339 339 def __init__(self, ctx, path, state):
340 340 self._path = path
341 341 self._state = state
342 342 r = ctx._repo
343 343 root = r.wjoin(path)
344 344 create = False
345 345 if not os.path.exists(os.path.join(root, '.hg')):
346 346 create = True
347 347 util.makedirs(root)
348 348 self._repo = hg.repository(r.ui, root, create=create)
349 349 self._repo._subparent = r
350 350 self._repo._subsource = state[0]
351 351
352 352 if create:
353 353 fp = self._repo.opener("hgrc", "w", text=True)
354 354 fp.write('[paths]\n')
355 355
356 356 def addpathconfig(key, value):
357 357 if value:
358 358 fp.write('%s = %s\n' % (key, value))
359 359 self._repo.ui.setconfig('paths', key, value)
360 360
361 361 defpath = _abssource(self._repo, abort=False)
362 362 defpushpath = _abssource(self._repo, True, abort=False)
363 363 addpathconfig('default', defpath)
364 364 if defpath != defpushpath:
365 365 addpathconfig('default-push', defpushpath)
366 366 fp.close()
367 367
368 368 def add(self, ui, match, dryrun, prefix):
369 369 return cmdutil.add(ui, self._repo, match, dryrun, True,
370 370 os.path.join(prefix, self._path))
371 371
372 372 def status(self, rev2, **opts):
373 373 try:
374 374 rev1 = self._state[1]
375 375 ctx1 = self._repo[rev1]
376 376 ctx2 = self._repo[rev2]
377 377 return self._repo.status(ctx1, ctx2, **opts)
378 378 except error.RepoLookupError, inst:
379 379 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
380 380 % (inst, subrelpath(self)))
381 381 return [], [], [], [], [], [], []
382 382
383 383 def diff(self, diffopts, node2, match, prefix, **opts):
384 384 try:
385 385 node1 = node.bin(self._state[1])
386 386 # We currently expect node2 to come from substate and be
387 387 # in hex format
388 388 if node2 is not None:
389 389 node2 = node.bin(node2)
390 390 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
391 391 node1, node2, match,
392 392 prefix=os.path.join(prefix, self._path),
393 393 listsubrepos=True, **opts)
394 394 except error.RepoLookupError, inst:
395 395 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
396 396 % (inst, subrelpath(self)))
397 397
398 398 def archive(self, ui, archiver, prefix):
399 399 abstractsubrepo.archive(self, ui, archiver, prefix)
400 400
401 401 rev = self._state[1]
402 402 ctx = self._repo[rev]
403 403 for subpath in ctx.substate:
404 404 s = subrepo(ctx, subpath)
405 405 s.archive(ui, archiver, os.path.join(prefix, self._path))
406 406
407 407 def dirty(self, ignoreupdate=False):
408 408 r = self._state[1]
409 409 if r == '' and not ignoreupdate: # no state recorded
410 410 return True
411 411 w = self._repo[None]
412 412 if w.p1() != self._repo[r] and not ignoreupdate:
413 413 # different version checked out
414 414 return True
415 415 return w.dirty() # working directory changed
416 416
417 417 def checknested(self, path):
418 418 return self._repo._checknested(self._repo.wjoin(path))
419 419
420 420 def commit(self, text, user, date):
421 421 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
422 422 n = self._repo.commit(text, user, date)
423 423 if not n:
424 424 return self._repo['.'].hex() # different version checked out
425 425 return node.hex(n)
426 426
427 427 def remove(self):
428 428 # we can't fully delete the repository as it may contain
429 429 # local-only history
430 430 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
431 431 hg.clean(self._repo, node.nullid, False)
432 432
433 433 def _get(self, state):
434 434 source, revision, kind = state
435 435 try:
436 436 self._repo.lookup(revision)
437 437 except error.RepoError:
438 438 self._repo._subsource = source
439 439 srcurl = _abssource(self._repo)
440 440 self._repo.ui.status(_('pulling subrepo %s from %s\n')
441 441 % (subrelpath(self), srcurl))
442 442 other = hg.repository(self._repo.ui, srcurl)
443 443 self._repo.pull(other)
444 444
445 445 def get(self, state, overwrite=False):
446 446 self._get(state)
447 447 source, revision, kind = state
448 448 self._repo.ui.debug("getting subrepo %s\n" % self._path)
449 449 hg.clean(self._repo, revision, False)
450 450
451 451 def merge(self, state):
452 452 self._get(state)
453 453 cur = self._repo['.']
454 454 dst = self._repo[state[1]]
455 455 anc = dst.ancestor(cur)
456 456
457 457 def mergefunc():
458 458 if anc == cur:
459 459 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
460 460 hg.update(self._repo, state[1])
461 461 elif anc == dst:
462 462 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
463 463 else:
464 464 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
465 465 hg.merge(self._repo, state[1], remind=False)
466 466
467 467 wctx = self._repo[None]
468 468 if self.dirty():
469 469 if anc != dst:
470 470 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
471 471 mergefunc()
472 472 else:
473 473 mergefunc()
474 474 else:
475 475 mergefunc()
476 476
477 477 def push(self, force):
478 478 # push subrepos depth-first for coherent ordering
479 479 c = self._repo['']
480 480 subs = c.substate # only repos that are committed
481 481 for s in sorted(subs):
482 482 if not c.sub(s).push(force):
483 483 return False
484 484
485 485 dsturl = _abssource(self._repo, True)
486 486 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
487 487 (subrelpath(self), dsturl))
488 488 other = hg.repository(self._repo.ui, dsturl)
489 489 return self._repo.push(other, force)
490 490
491 491 def outgoing(self, ui, dest, opts):
492 492 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
493 493
494 494 def incoming(self, ui, source, opts):
495 495 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
496 496
497 497 def files(self):
498 498 rev = self._state[1]
499 499 ctx = self._repo[rev]
500 500 return ctx.manifest()
501 501
502 502 def filedata(self, name):
503 503 rev = self._state[1]
504 504 return self._repo[rev][name].data()
505 505
506 506 def fileflags(self, name):
507 507 rev = self._state[1]
508 508 ctx = self._repo[rev]
509 509 return ctx.flags(name)
510 510
511 511
512 512 class svnsubrepo(abstractsubrepo):
513 513 def __init__(self, ctx, path, state):
514 514 self._path = path
515 515 self._state = state
516 516 self._ctx = ctx
517 517 self._ui = ctx._repo.ui
518 518
519 519 def _svncommand(self, commands, filename=''):
520 520 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
521 521 cmd = ['svn'] + commands + [path]
522 522 env = dict(os.environ)
523 523 # Avoid localized output, preserve current locale for everything else.
524 524 env['LC_MESSAGES'] = 'C'
525 525 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
526 526 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
527 527 universal_newlines=True, env=env)
528 528 stdout, stderr = p.communicate()
529 529 stderr = stderr.strip()
530 530 if stderr:
531 531 raise util.Abort(stderr)
532 532 return stdout
533 533
534 534 def _wcrevs(self):
535 535 # Get the working directory revision as well as the last
536 536 # commit revision so we can compare the subrepo state with
537 537 # both. We used to store the working directory one.
538 538 output = self._svncommand(['info', '--xml'])
539 539 doc = xml.dom.minidom.parseString(output)
540 540 entries = doc.getElementsByTagName('entry')
541 541 lastrev, rev = '0', '0'
542 542 if entries:
543 543 rev = str(entries[0].getAttribute('revision')) or '0'
544 544 commits = entries[0].getElementsByTagName('commit')
545 545 if commits:
546 546 lastrev = str(commits[0].getAttribute('revision')) or '0'
547 547 return (lastrev, rev)
548 548
549 549 def _wcrev(self):
550 550 return self._wcrevs()[0]
551 551
552 552 def _wcchanged(self):
553 553 """Return (changes, extchanges) where changes is True
554 554 if the working directory was changed, and extchanges is
555 555 True if any of these changes concern an external entry.
556 556 """
557 557 output = self._svncommand(['status', '--xml'])
558 558 externals, changes = [], []
559 559 doc = xml.dom.minidom.parseString(output)
560 560 for e in doc.getElementsByTagName('entry'):
561 561 s = e.getElementsByTagName('wc-status')
562 562 if not s:
563 563 continue
564 564 item = s[0].getAttribute('item')
565 565 props = s[0].getAttribute('props')
566 566 path = e.getAttribute('path')
567 567 if item == 'external':
568 568 externals.append(path)
569 569 if (item not in ('', 'normal', 'unversioned', 'external')
570 570 or props not in ('', 'none')):
571 571 changes.append(path)
572 572 for path in changes:
573 573 for ext in externals:
574 574 if path == ext or path.startswith(ext + os.sep):
575 575 return True, True
576 576 return bool(changes), False
577 577
578 578 def dirty(self, ignoreupdate=False):
579 579 if not self._wcchanged()[0]:
580 580 if self._state[1] in self._wcrevs() or ignoreupdate:
581 581 return False
582 582 return True
583 583
584 584 def commit(self, text, user, date):
585 585 # user and date are out of our hands since svn is centralized
586 586 changed, extchanged = self._wcchanged()
587 587 if not changed:
588 588 return self._wcrev()
589 589 if extchanged:
590 590 # Do not try to commit externals
591 591 raise util.Abort(_('cannot commit svn externals'))
592 592 commitinfo = self._svncommand(['commit', '-m', text])
593 593 self._ui.status(commitinfo)
594 594 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
595 595 if not newrev:
596 596 raise util.Abort(commitinfo.splitlines()[-1])
597 597 newrev = newrev.groups()[0]
598 598 self._ui.status(self._svncommand(['update', '-r', newrev]))
599 599 return newrev
600 600
601 601 def remove(self):
602 602 if self.dirty():
603 603 self._ui.warn(_('not removing repo %s because '
604 604 'it has changes.\n' % self._path))
605 605 return
606 606 self._ui.note(_('removing subrepo %s\n') % self._path)
607 607
608 608 def onerror(function, path, excinfo):
609 609 if function is not os.remove:
610 610 raise
611 611 # read-only files cannot be unlinked under Windows
612 612 s = os.stat(path)
613 613 if (s.st_mode & stat.S_IWRITE) != 0:
614 614 raise
615 615 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
616 616 os.remove(path)
617 617
618 618 path = self._ctx._repo.wjoin(self._path)
619 619 shutil.rmtree(path, onerror=onerror)
620 620 try:
621 621 os.removedirs(os.path.dirname(path))
622 622 except OSError:
623 623 pass
624 624
625 625 def get(self, state, overwrite=False):
626 626 if overwrite:
627 627 self._svncommand(['revert', '--recursive'])
628 status = self._svncommand(['checkout', '--force', state[0],
629 '--revision', state[1]])
628 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
630 629 if not re.search('Checked out revision [0-9]+.', status):
631 630 raise util.Abort(status.splitlines()[-1])
632 631 self._ui.status(status)
633 632
634 633 def merge(self, state):
635 634 old = self._state[1]
636 635 new = state[1]
637 636 if new != self._wcrev():
638 637 dirty = old == self._wcrev() or self._wcchanged()[0]
639 638 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
640 639 self.get(state, False)
641 640
642 641 def push(self, force):
643 642 # push is a no-op for SVN
644 643 return True
645 644
646 645 def files(self):
647 646 output = self._svncommand(['list'])
648 647 # This works because svn forbids \n in filenames.
649 648 return output.splitlines()
650 649
651 650 def filedata(self, name):
652 651 return self._svncommand(['cat'], name)
653 652
654 653
655 654 class gitsubrepo(abstractsubrepo):
656 655 def __init__(self, ctx, path, state):
657 656 # TODO add git version check.
658 657 self._state = state
659 658 self._ctx = ctx
660 659 self._path = path
661 660 self._relpath = os.path.join(reporelpath(ctx._repo), path)
662 661 self._abspath = ctx._repo.wjoin(path)
663 662 self._subparent = ctx._repo
664 663 self._ui = ctx._repo.ui
665 664
666 665 def _gitcommand(self, commands, env=None, stream=False):
667 666 return self._gitdir(commands, env=env, stream=stream)[0]
668 667
669 668 def _gitdir(self, commands, env=None, stream=False):
670 669 return self._gitnodir(commands, env=env, stream=stream,
671 670 cwd=self._abspath)
672 671
673 672 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
674 673 """Calls the git command
675 674
676 675 The methods tries to call the git command. versions previor to 1.6.0
677 676 are not supported and very probably fail.
678 677 """
679 678 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
680 679 # unless ui.quiet is set, print git's stderr,
681 680 # which is mostly progress and useful info
682 681 errpipe = None
683 682 if self._ui.quiet:
684 683 errpipe = open(os.devnull, 'w')
685 684 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
686 685 close_fds=util.closefds,
687 686 stdout=subprocess.PIPE, stderr=errpipe)
688 687 if stream:
689 688 return p.stdout, None
690 689
691 690 retdata = p.stdout.read().strip()
692 691 # wait for the child to exit to avoid race condition.
693 692 p.wait()
694 693
695 694 if p.returncode != 0 and p.returncode != 1:
696 695 # there are certain error codes that are ok
697 696 command = commands[0]
698 697 if command in ('cat-file', 'symbolic-ref'):
699 698 return retdata, p.returncode
700 699 # for all others, abort
701 700 raise util.Abort('git %s error %d in %s' %
702 701 (command, p.returncode, self._relpath))
703 702
704 703 return retdata, p.returncode
705 704
706 705 def _gitmissing(self):
707 706 return not os.path.exists(os.path.join(self._abspath, '.git'))
708 707
709 708 def _gitstate(self):
710 709 return self._gitcommand(['rev-parse', 'HEAD'])
711 710
712 711 def _gitcurrentbranch(self):
713 712 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
714 713 if err:
715 714 current = None
716 715 return current
717 716
718 717 def _githavelocally(self, revision):
719 718 out, code = self._gitdir(['cat-file', '-e', revision])
720 719 return code == 0
721 720
722 721 def _gitisancestor(self, r1, r2):
723 722 base = self._gitcommand(['merge-base', r1, r2])
724 723 return base == r1
725 724
726 725 def _gitbranchmap(self):
727 726 '''returns 2 things:
728 727 a map from git branch to revision
729 728 a map from revision to branches'''
730 729 branch2rev = {}
731 730 rev2branch = {}
732 731
733 732 out = self._gitcommand(['for-each-ref', '--format',
734 733 '%(objectname) %(refname)'])
735 734 for line in out.split('\n'):
736 735 revision, ref = line.split(' ')
737 736 if (not ref.startswith('refs/heads/') and
738 737 not ref.startswith('refs/remotes/')):
739 738 continue
740 739 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
741 740 continue # ignore remote/HEAD redirects
742 741 branch2rev[ref] = revision
743 742 rev2branch.setdefault(revision, []).append(ref)
744 743 return branch2rev, rev2branch
745 744
746 745 def _gittracking(self, branches):
747 746 'return map of remote branch to local tracking branch'
748 747 # assumes no more than one local tracking branch for each remote
749 748 tracking = {}
750 749 for b in branches:
751 750 if b.startswith('refs/remotes/'):
752 751 continue
753 752 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
754 753 if remote:
755 754 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
756 755 tracking['refs/remotes/%s/%s' %
757 756 (remote, ref.split('/', 2)[2])] = b
758 757 return tracking
759 758
760 759 def _abssource(self, source):
761 760 self._subsource = source
762 761 return _abssource(self)
763 762
764 763 def _fetch(self, source, revision):
765 764 if self._gitmissing():
766 765 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
767 766 self._gitnodir(['clone', self._abssource(source), self._abspath])
768 767 if self._githavelocally(revision):
769 768 return
770 769 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
771 770 # try only origin: the originally cloned repo
772 771 self._gitcommand(['fetch'])
773 772 if not self._githavelocally(revision):
774 773 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
775 774 (revision, self._relpath))
776 775
777 776 def dirty(self, ignoreupdate=False):
778 777 if self._gitmissing():
779 778 return True
780 779 if not ignoreupdate and self._state[1] != self._gitstate():
781 780 # different version checked out
782 781 return True
783 782 # check for staged changes or modified files; ignore untracked files
784 783 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
785 784 return code == 1
786 785
787 786 def get(self, state, overwrite=False):
788 787 source, revision, kind = state
789 788 self._fetch(source, revision)
790 789 # if the repo was set to be bare, unbare it
791 790 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
792 791 self._gitcommand(['config', 'core.bare', 'false'])
793 792 if self._gitstate() == revision:
794 793 self._gitcommand(['reset', '--hard', 'HEAD'])
795 794 return
796 795 elif self._gitstate() == revision:
797 796 if overwrite:
798 797 # first reset the index to unmark new files for commit, because
799 798 # reset --hard will otherwise throw away files added for commit,
800 799 # not just unmark them.
801 800 self._gitcommand(['reset', 'HEAD'])
802 801 self._gitcommand(['reset', '--hard', 'HEAD'])
803 802 return
804 803 branch2rev, rev2branch = self._gitbranchmap()
805 804
806 805 def checkout(args):
807 806 cmd = ['checkout']
808 807 if overwrite:
809 808 # first reset the index to unmark new files for commit, because
810 809 # the -f option will otherwise throw away files added for
811 810 # commit, not just unmark them.
812 811 self._gitcommand(['reset', 'HEAD'])
813 812 cmd.append('-f')
814 813 self._gitcommand(cmd + args)
815 814
816 815 def rawcheckout():
817 816 # no branch to checkout, check it out with no branch
818 817 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
819 818 self._relpath)
820 819 self._ui.warn(_('check out a git branch if you intend '
821 820 'to make changes\n'))
822 821 checkout(['-q', revision])
823 822
824 823 if revision not in rev2branch:
825 824 rawcheckout()
826 825 return
827 826 branches = rev2branch[revision]
828 827 firstlocalbranch = None
829 828 for b in branches:
830 829 if b == 'refs/heads/master':
831 830 # master trumps all other branches
832 831 checkout(['refs/heads/master'])
833 832 return
834 833 if not firstlocalbranch and not b.startswith('refs/remotes/'):
835 834 firstlocalbranch = b
836 835 if firstlocalbranch:
837 836 checkout([firstlocalbranch])
838 837 return
839 838
840 839 tracking = self._gittracking(branch2rev.keys())
841 840 # choose a remote branch already tracked if possible
842 841 remote = branches[0]
843 842 if remote not in tracking:
844 843 for b in branches:
845 844 if b in tracking:
846 845 remote = b
847 846 break
848 847
849 848 if remote not in tracking:
850 849 # create a new local tracking branch
851 850 local = remote.split('/', 2)[2]
852 851 checkout(['-b', local, remote])
853 852 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
854 853 # When updating to a tracked remote branch,
855 854 # if the local tracking branch is downstream of it,
856 855 # a normal `git pull` would have performed a "fast-forward merge"
857 856 # which is equivalent to updating the local branch to the remote.
858 857 # Since we are only looking at branching at update, we need to
859 858 # detect this situation and perform this action lazily.
860 859 if tracking[remote] != self._gitcurrentbranch():
861 860 checkout([tracking[remote]])
862 861 self._gitcommand(['merge', '--ff', remote])
863 862 else:
864 863 # a real merge would be required, just checkout the revision
865 864 rawcheckout()
866 865
867 866 def commit(self, text, user, date):
868 867 if self._gitmissing():
869 868 raise util.Abort(_("subrepo %s is missing") % self._relpath)
870 869 cmd = ['commit', '-a', '-m', text]
871 870 env = os.environ.copy()
872 871 if user:
873 872 cmd += ['--author', user]
874 873 if date:
875 874 # git's date parser silently ignores when seconds < 1e9
876 875 # convert to ISO8601
877 876 env['GIT_AUTHOR_DATE'] = util.datestr(date,
878 877 '%Y-%m-%dT%H:%M:%S %1%2')
879 878 self._gitcommand(cmd, env=env)
880 879 # make sure commit works otherwise HEAD might not exist under certain
881 880 # circumstances
882 881 return self._gitstate()
883 882
884 883 def merge(self, state):
885 884 source, revision, kind = state
886 885 self._fetch(source, revision)
887 886 base = self._gitcommand(['merge-base', revision, self._state[1]])
888 887 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
889 888
890 889 def mergefunc():
891 890 if base == revision:
892 891 self.get(state) # fast forward merge
893 892 elif base != self._state[1]:
894 893 self._gitcommand(['merge', '--no-commit', revision])
895 894
896 895 if self.dirty():
897 896 if self._gitstate() != revision:
898 897 dirty = self._gitstate() == self._state[1] or code != 0
899 898 if _updateprompt(self._ui, self, dirty,
900 899 self._state[1][:7], revision[:7]):
901 900 mergefunc()
902 901 else:
903 902 mergefunc()
904 903
905 904 def push(self, force):
906 905 if self._gitmissing():
907 906 raise util.Abort(_("subrepo %s is missing") % self._relpath)
908 907 # if a branch in origin contains the revision, nothing to do
909 908 branch2rev, rev2branch = self._gitbranchmap()
910 909 if self._state[1] in rev2branch:
911 910 for b in rev2branch[self._state[1]]:
912 911 if b.startswith('refs/remotes/origin/'):
913 912 return True
914 913 for b, revision in branch2rev.iteritems():
915 914 if b.startswith('refs/remotes/origin/'):
916 915 if self._gitisancestor(self._state[1], revision):
917 916 return True
918 917 # otherwise, try to push the currently checked out branch
919 918 cmd = ['push']
920 919 if force:
921 920 cmd.append('--force')
922 921
923 922 current = self._gitcurrentbranch()
924 923 if current:
925 924 # determine if the current branch is even useful
926 925 if not self._gitisancestor(self._state[1], current):
927 926 self._ui.warn(_('unrelated git branch checked out '
928 927 'in subrepo %s\n') % self._relpath)
929 928 return False
930 929 self._ui.status(_('pushing branch %s of subrepo %s\n') %
931 930 (current.split('/', 2)[2], self._relpath))
932 931 self._gitcommand(cmd + ['origin', current])
933 932 return True
934 933 else:
935 934 self._ui.warn(_('no branch checked out in subrepo %s\n'
936 935 'cannot push revision %s') %
937 936 (self._relpath, self._state[1]))
938 937 return False
939 938
940 939 def remove(self):
941 940 if self._gitmissing():
942 941 return
943 942 if self.dirty():
944 943 self._ui.warn(_('not removing repo %s because '
945 944 'it has changes.\n') % self._relpath)
946 945 return
947 946 # we can't fully delete the repository as it may contain
948 947 # local-only history
949 948 self._ui.note(_('removing subrepo %s\n') % self._relpath)
950 949 self._gitcommand(['config', 'core.bare', 'true'])
951 950 for f in os.listdir(self._abspath):
952 951 if f == '.git':
953 952 continue
954 953 path = os.path.join(self._abspath, f)
955 954 if os.path.isdir(path) and not os.path.islink(path):
956 955 shutil.rmtree(path)
957 956 else:
958 957 os.remove(path)
959 958
960 959 def archive(self, ui, archiver, prefix):
961 960 source, revision = self._state
962 961 self._fetch(source, revision)
963 962
964 963 # Parse git's native archive command.
965 964 # This should be much faster than manually traversing the trees
966 965 # and objects with many subprocess calls.
967 966 tarstream = self._gitcommand(['archive', revision], stream=True)
968 967 tar = tarfile.open(fileobj=tarstream, mode='r|')
969 968 relpath = subrelpath(self)
970 969 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
971 970 for i, info in enumerate(tar):
972 971 if info.isdir():
973 972 continue
974 973 if info.issym():
975 974 data = info.linkname
976 975 else:
977 976 data = tar.extractfile(info).read()
978 977 archiver.addfile(os.path.join(prefix, self._path, info.name),
979 978 info.mode, info.issym(), data)
980 979 ui.progress(_('archiving (%s)') % relpath, i + 1,
981 980 unit=_('files'))
982 981 ui.progress(_('archiving (%s)') % relpath, None)
983 982
984 983
985 984 def status(self, rev2, **opts):
986 985 if self._gitmissing():
987 986 # if the repo is missing, return no results
988 987 return [], [], [], [], [], [], []
989 988 rev1 = self._state[1]
990 989 modified, added, removed = [], [], []
991 990 if rev2:
992 991 command = ['diff-tree', rev1, rev2]
993 992 else:
994 993 command = ['diff-index', rev1]
995 994 out = self._gitcommand(command)
996 995 for line in out.split('\n'):
997 996 tab = line.find('\t')
998 997 if tab == -1:
999 998 continue
1000 999 status, f = line[tab - 1], line[tab + 1:]
1001 1000 if status == 'M':
1002 1001 modified.append(f)
1003 1002 elif status == 'A':
1004 1003 added.append(f)
1005 1004 elif status == 'D':
1006 1005 removed.append(f)
1007 1006
1008 1007 deleted = unknown = ignored = clean = []
1009 1008 return modified, added, removed, deleted, unknown, ignored, clean
1010 1009
1011 1010 types = {
1012 1011 'hg': hgsubrepo,
1013 1012 'svn': svnsubrepo,
1014 1013 'git': gitsubrepo,
1015 1014 }
@@ -1,497 +1,440 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/subdir/s/alpha
229 229 U tc/subdir/s
230 230
231 231 Fetching external item into 'tc/subdir/s/externals'
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 A tc/s/alpha
237 237 U tc/s
238 238
239 239 Fetching external item into 'tc/s/externals'
240 240 A tc/s/externals/other
241 241 Checked out external at revision 1.
242 242
243 243 Checked out revision 3.
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
442 Test case where subversion would fail to update the subrepo because there
443 are unknown directories being replaced by tracked ones (happens with rebase).
444
445 $ cd $WCROOT/src
446 $ mkdir dir
447 $ echo epsilon.py > dir/epsilon.py
448 $ svn add dir
449 A dir
450 A dir/epsilon.py
451 $ svn ci -m 'Add dir/epsilon.py'
452 Adding src/dir
453 Adding src/dir/epsilon.py
454 Transmitting file data .
455 Committed revision 6.
456 $ cd ../..
457 $ hg init rebaserepo
458 $ cd rebaserepo
459 $ svn co -r5 --quiet "$SVNREPO"/src s
460 $ echo "s = [svn] $SVNREPO/src" >> .hgsub
461 $ hg add .hgsub
462 $ hg ci -m addsub
463 committing subrepository s
464 $ echo a > a
465 $ hg ci -Am adda
466 adding a
467 $ hg up 0
468 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
469 $ svn up -r6 s
470 A s/dir
471 A s/dir/epsilon.py
472
473 Fetching external item into 's/externals'
474 Updated external to revision 1.
475
476 Updated to revision 6.
477 $ hg ci -m updatesub
478 committing subrepository s
479 created new head
480 $ echo pyc > s/dir/epsilon.pyc
481 $ hg up 1
482 D $TESTTMP/rebaserepo/s/dir
483
484 Fetching external item into '$TESTTMP/rebaserepo/s/externals'
485 Checked out external at revision 1.
486
487 Checked out revision 5.
488 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
489 $ hg up 2
490 E $TESTTMP/rebaserepo/s/dir
491 A $TESTTMP/rebaserepo/s/dir/epsilon.py
492
493 Fetching external item into '$TESTTMP/rebaserepo/s/externals'
494 Checked out external at revision 1.
495
496 Checked out revision 6.
497 1 files updated, 0 files merged, 1 files removed, 0 files unresolved
General Comments 0
You need to be logged in to leave comments. Login now