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