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