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