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