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