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