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