##// END OF EJS Templates
subrepo: update and merge works with any git branch
Eric Eisner -
r12995:d90fc91c default
parent child Browse files
Show More
@@ -1,704 +1,749 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, subprocess, urlparse, posixpath
9 9 from i18n import _
10 10 import config, util, node, error, cmdutil
11 11 hg = None
12 12
13 13 nullstate = ('', '', 'empty')
14 14
15 15 def state(ctx, ui):
16 16 """return a state dict, mapping subrepo paths configured in .hgsub
17 17 to tuple: (source from .hgsub, revision from .hgsubstate, kind
18 18 (key in types dict))
19 19 """
20 20 p = config.config()
21 21 def read(f, sections=None, remap=None):
22 22 if f in ctx:
23 23 p.parse(f, ctx[f].data(), sections, remap, read)
24 24 else:
25 25 raise util.Abort(_("subrepo spec file %s not found") % f)
26 26
27 27 if '.hgsub' in ctx:
28 28 read('.hgsub')
29 29
30 30 for path, src in ui.configitems('subpaths'):
31 31 p.set('subpaths', path, src, ui.configsource('subpaths', path))
32 32
33 33 rev = {}
34 34 if '.hgsubstate' in ctx:
35 35 try:
36 36 for l in ctx['.hgsubstate'].data().splitlines():
37 37 revision, path = l.split(" ", 1)
38 38 rev[path] = revision
39 39 except IOError, err:
40 40 if err.errno != errno.ENOENT:
41 41 raise
42 42
43 43 state = {}
44 44 for path, src in p[''].items():
45 45 kind = 'hg'
46 46 if src.startswith('['):
47 47 if ']' not in src:
48 48 raise util.Abort(_('missing ] in subrepo source'))
49 49 kind, src = src.split(']', 1)
50 50 kind = kind[1:]
51 51
52 52 for pattern, repl in p.items('subpaths'):
53 53 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
54 54 # does a string decode.
55 55 repl = repl.encode('string-escape')
56 56 # However, we still want to allow back references to go
57 57 # through unharmed, so we turn r'\\1' into r'\1'. Again,
58 58 # extra escapes are needed because re.sub string decodes.
59 59 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
60 60 try:
61 61 src = re.sub(pattern, repl, src, 1)
62 62 except re.error, e:
63 63 raise util.Abort(_("bad subrepository pattern in %s: %s")
64 64 % (p.source('subpaths', pattern), e))
65 65
66 66 state[path] = (src.strip(), rev.get(path, ''), kind)
67 67
68 68 return state
69 69
70 70 def writestate(repo, state):
71 71 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
72 72 repo.wwrite('.hgsubstate',
73 73 ''.join(['%s %s\n' % (state[s][1], s)
74 74 for s in sorted(state)]), '')
75 75
76 76 def submerge(repo, wctx, mctx, actx):
77 77 """delegated from merge.applyupdates: merging of .hgsubstate file
78 78 in working context, merging context and ancestor context"""
79 79 if mctx == actx: # backwards?
80 80 actx = wctx.p1()
81 81 s1 = wctx.substate
82 82 s2 = mctx.substate
83 83 sa = actx.substate
84 84 sm = {}
85 85
86 86 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
87 87
88 88 def debug(s, msg, r=""):
89 89 if r:
90 90 r = "%s:%s:%s" % r
91 91 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
92 92
93 93 for s, l in s1.items():
94 94 a = sa.get(s, nullstate)
95 95 ld = l # local state with possible dirty flag for compares
96 96 if wctx.sub(s).dirty():
97 97 ld = (l[0], l[1] + "+")
98 98 if wctx == actx: # overwrite
99 99 a = ld
100 100
101 101 if s in s2:
102 102 r = s2[s]
103 103 if ld == r or r == a: # no change or local is newer
104 104 sm[s] = l
105 105 continue
106 106 elif ld == a: # other side changed
107 107 debug(s, "other changed, get", r)
108 108 wctx.sub(s).get(r)
109 109 sm[s] = r
110 110 elif ld[0] != r[0]: # sources differ
111 111 if repo.ui.promptchoice(
112 112 _(' subrepository sources for %s differ\n'
113 113 'use (l)ocal source (%s) or (r)emote source (%s)?')
114 114 % (s, l[0], r[0]),
115 115 (_('&Local'), _('&Remote')), 0):
116 116 debug(s, "prompt changed, get", r)
117 117 wctx.sub(s).get(r)
118 118 sm[s] = r
119 119 elif ld[1] == a[1]: # local side is unchanged
120 120 debug(s, "other side changed, get", r)
121 121 wctx.sub(s).get(r)
122 122 sm[s] = r
123 123 else:
124 124 debug(s, "both sides changed, merge with", r)
125 125 wctx.sub(s).merge(r)
126 126 sm[s] = l
127 127 elif ld == a: # remote removed, local unchanged
128 128 debug(s, "remote removed, remove")
129 129 wctx.sub(s).remove()
130 130 else:
131 131 if repo.ui.promptchoice(
132 132 _(' local changed subrepository %s which remote removed\n'
133 133 'use (c)hanged version or (d)elete?') % s,
134 134 (_('&Changed'), _('&Delete')), 0):
135 135 debug(s, "prompt remove")
136 136 wctx.sub(s).remove()
137 137
138 138 for s, r in s2.items():
139 139 if s in s1:
140 140 continue
141 141 elif s not in sa:
142 142 debug(s, "remote added, get", r)
143 143 mctx.sub(s).get(r)
144 144 sm[s] = r
145 145 elif r != sa[s]:
146 146 if repo.ui.promptchoice(
147 147 _(' remote changed subrepository %s which local removed\n'
148 148 'use (c)hanged version or (d)elete?') % s,
149 149 (_('&Changed'), _('&Delete')), 0) == 0:
150 150 debug(s, "prompt recreate", r)
151 151 wctx.sub(s).get(r)
152 152 sm[s] = r
153 153
154 154 # record merged .hgsubstate
155 155 writestate(repo, sm)
156 156
157 157 def reporelpath(repo):
158 158 """return path to this (sub)repo as seen from outermost repo"""
159 159 parent = repo
160 160 while hasattr(parent, '_subparent'):
161 161 parent = parent._subparent
162 162 return repo.root[len(parent.root)+1:]
163 163
164 164 def subrelpath(sub):
165 165 """return path to this subrepo as seen from outermost repo"""
166 166 if not hasattr(sub, '_repo'):
167 167 return sub._path
168 168 return reporelpath(sub._repo)
169 169
170 170 def _abssource(repo, push=False, abort=True):
171 171 """return pull/push path of repo - either based on parent repo .hgsub info
172 172 or on the top repo config. Abort or return None if no source found."""
173 173 if hasattr(repo, '_subparent'):
174 174 source = repo._subsource
175 175 if source.startswith('/') or '://' in source:
176 176 return source
177 177 parent = _abssource(repo._subparent, push, abort=False)
178 178 if parent:
179 179 if '://' in parent:
180 180 if parent[-1] == '/':
181 181 parent = parent[:-1]
182 182 r = urlparse.urlparse(parent + '/' + source)
183 183 r = urlparse.urlunparse((r[0], r[1],
184 184 posixpath.normpath(r[2]),
185 185 r[3], r[4], r[5]))
186 186 return r
187 187 else: # plain file system path
188 188 return posixpath.normpath(os.path.join(parent, repo._subsource))
189 189 else: # recursion reached top repo
190 190 if hasattr(repo, '_subtoppath'):
191 191 return repo._subtoppath
192 192 if push and repo.ui.config('paths', 'default-push'):
193 193 return repo.ui.config('paths', 'default-push')
194 194 if repo.ui.config('paths', 'default'):
195 195 return repo.ui.config('paths', 'default')
196 196 if abort:
197 197 raise util.Abort(_("default path for subrepository %s not found") %
198 198 reporelpath(repo))
199 199
200 200 def itersubrepos(ctx1, ctx2):
201 201 """find subrepos in ctx1 or ctx2"""
202 202 # Create a (subpath, ctx) mapping where we prefer subpaths from
203 203 # ctx1. The subpaths from ctx2 are important when the .hgsub file
204 204 # has been modified (in ctx2) but not yet committed (in ctx1).
205 205 subpaths = dict.fromkeys(ctx2.substate, ctx2)
206 206 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
207 207 for subpath, ctx in sorted(subpaths.iteritems()):
208 208 yield subpath, ctx.sub(subpath)
209 209
210 210 def subrepo(ctx, path):
211 211 """return instance of the right subrepo class for subrepo in path"""
212 212 # subrepo inherently violates our import layering rules
213 213 # because it wants to make repo objects from deep inside the stack
214 214 # so we manually delay the circular imports to not break
215 215 # scripts that don't use our demand-loading
216 216 global hg
217 217 import hg as h
218 218 hg = h
219 219
220 220 util.path_auditor(ctx._repo.root)(path)
221 221 state = ctx.substate.get(path, nullstate)
222 222 if state[2] not in types:
223 223 raise util.Abort(_('unknown subrepo type %s') % state[2])
224 224 return types[state[2]](ctx, path, state[:2])
225 225
226 226 # subrepo classes need to implement the following abstract class:
227 227
228 228 class abstractsubrepo(object):
229 229
230 230 def dirty(self):
231 231 """returns true if the dirstate of the subrepo does not match
232 232 current stored state
233 233 """
234 234 raise NotImplementedError
235 235
236 236 def checknested(self, path):
237 237 """check if path is a subrepository within this repository"""
238 238 return False
239 239
240 240 def commit(self, text, user, date):
241 241 """commit the current changes to the subrepo with the given
242 242 log message. Use given user and date if possible. Return the
243 243 new state of the subrepo.
244 244 """
245 245 raise NotImplementedError
246 246
247 247 def remove(self):
248 248 """remove the subrepo
249 249
250 250 (should verify the dirstate is not dirty first)
251 251 """
252 252 raise NotImplementedError
253 253
254 254 def get(self, state):
255 255 """run whatever commands are needed to put the subrepo into
256 256 this state
257 257 """
258 258 raise NotImplementedError
259 259
260 260 def merge(self, state):
261 261 """merge currently-saved state with the new state."""
262 262 raise NotImplementedError
263 263
264 264 def push(self, force):
265 265 """perform whatever action is analogous to 'hg push'
266 266
267 267 This may be a no-op on some systems.
268 268 """
269 269 raise NotImplementedError
270 270
271 271 def add(self, ui, match, dryrun, prefix):
272 272 return []
273 273
274 274 def status(self, rev2, **opts):
275 275 return [], [], [], [], [], [], []
276 276
277 277 def diff(self, diffopts, node2, match, prefix, **opts):
278 278 pass
279 279
280 280 def outgoing(self, ui, dest, opts):
281 281 return 1
282 282
283 283 def incoming(self, ui, source, opts):
284 284 return 1
285 285
286 286 def files(self):
287 287 """return filename iterator"""
288 288 raise NotImplementedError
289 289
290 290 def filedata(self, name):
291 291 """return file data"""
292 292 raise NotImplementedError
293 293
294 294 def fileflags(self, name):
295 295 """return file flags"""
296 296 return ''
297 297
298 298 def archive(self, archiver, prefix):
299 299 for name in self.files():
300 300 flags = self.fileflags(name)
301 301 mode = 'x' in flags and 0755 or 0644
302 302 symlink = 'l' in flags
303 303 archiver.addfile(os.path.join(prefix, self._path, name),
304 304 mode, symlink, self.filedata(name))
305 305
306 306
307 307 class hgsubrepo(abstractsubrepo):
308 308 def __init__(self, ctx, path, state):
309 309 self._path = path
310 310 self._state = state
311 311 r = ctx._repo
312 312 root = r.wjoin(path)
313 313 create = False
314 314 if not os.path.exists(os.path.join(root, '.hg')):
315 315 create = True
316 316 util.makedirs(root)
317 317 self._repo = hg.repository(r.ui, root, create=create)
318 318 self._repo._subparent = r
319 319 self._repo._subsource = state[0]
320 320
321 321 if create:
322 322 fp = self._repo.opener("hgrc", "w", text=True)
323 323 fp.write('[paths]\n')
324 324
325 325 def addpathconfig(key, value):
326 326 if value:
327 327 fp.write('%s = %s\n' % (key, value))
328 328 self._repo.ui.setconfig('paths', key, value)
329 329
330 330 defpath = _abssource(self._repo, abort=False)
331 331 defpushpath = _abssource(self._repo, True, abort=False)
332 332 addpathconfig('default', defpath)
333 333 if defpath != defpushpath:
334 334 addpathconfig('default-push', defpushpath)
335 335 fp.close()
336 336
337 337 def add(self, ui, match, dryrun, prefix):
338 338 return cmdutil.add(ui, self._repo, match, dryrun, True,
339 339 os.path.join(prefix, self._path))
340 340
341 341 def status(self, rev2, **opts):
342 342 try:
343 343 rev1 = self._state[1]
344 344 ctx1 = self._repo[rev1]
345 345 ctx2 = self._repo[rev2]
346 346 return self._repo.status(ctx1, ctx2, **opts)
347 347 except error.RepoLookupError, inst:
348 348 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
349 349 % (inst, subrelpath(self)))
350 350 return [], [], [], [], [], [], []
351 351
352 352 def diff(self, diffopts, node2, match, prefix, **opts):
353 353 try:
354 354 node1 = node.bin(self._state[1])
355 355 # We currently expect node2 to come from substate and be
356 356 # in hex format
357 357 if node2 is not None:
358 358 node2 = node.bin(node2)
359 359 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
360 360 node1, node2, match,
361 361 prefix=os.path.join(prefix, self._path),
362 362 listsubrepos=True, **opts)
363 363 except error.RepoLookupError, inst:
364 364 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
365 365 % (inst, subrelpath(self)))
366 366
367 367 def archive(self, archiver, prefix):
368 368 abstractsubrepo.archive(self, archiver, prefix)
369 369
370 370 rev = self._state[1]
371 371 ctx = self._repo[rev]
372 372 for subpath in ctx.substate:
373 373 s = subrepo(ctx, subpath)
374 374 s.archive(archiver, os.path.join(prefix, self._path))
375 375
376 376 def dirty(self):
377 377 r = self._state[1]
378 378 if r == '':
379 379 return True
380 380 w = self._repo[None]
381 381 if w.p1() != self._repo[r]: # version checked out change
382 382 return True
383 383 return w.dirty() # working directory changed
384 384
385 385 def checknested(self, path):
386 386 return self._repo._checknested(self._repo.wjoin(path))
387 387
388 388 def commit(self, text, user, date):
389 389 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
390 390 n = self._repo.commit(text, user, date)
391 391 if not n:
392 392 return self._repo['.'].hex() # different version checked out
393 393 return node.hex(n)
394 394
395 395 def remove(self):
396 396 # we can't fully delete the repository as it may contain
397 397 # local-only history
398 398 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
399 399 hg.clean(self._repo, node.nullid, False)
400 400
401 401 def _get(self, state):
402 402 source, revision, kind = state
403 403 try:
404 404 self._repo.lookup(revision)
405 405 except error.RepoError:
406 406 self._repo._subsource = source
407 407 srcurl = _abssource(self._repo)
408 408 self._repo.ui.status(_('pulling subrepo %s from %s\n')
409 409 % (subrelpath(self), srcurl))
410 410 other = hg.repository(self._repo.ui, srcurl)
411 411 self._repo.pull(other)
412 412
413 413 def get(self, state):
414 414 self._get(state)
415 415 source, revision, kind = state
416 416 self._repo.ui.debug("getting subrepo %s\n" % self._path)
417 417 hg.clean(self._repo, revision, False)
418 418
419 419 def merge(self, state):
420 420 self._get(state)
421 421 cur = self._repo['.']
422 422 dst = self._repo[state[1]]
423 423 anc = dst.ancestor(cur)
424 424 if anc == cur:
425 425 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
426 426 hg.update(self._repo, state[1])
427 427 elif anc == dst:
428 428 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
429 429 else:
430 430 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
431 431 hg.merge(self._repo, state[1], remind=False)
432 432
433 433 def push(self, force):
434 434 # push subrepos depth-first for coherent ordering
435 435 c = self._repo['']
436 436 subs = c.substate # only repos that are committed
437 437 for s in sorted(subs):
438 438 if not c.sub(s).push(force):
439 439 return False
440 440
441 441 dsturl = _abssource(self._repo, True)
442 442 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
443 443 (subrelpath(self), dsturl))
444 444 other = hg.repository(self._repo.ui, dsturl)
445 445 return self._repo.push(other, force)
446 446
447 447 def outgoing(self, ui, dest, opts):
448 448 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
449 449
450 450 def incoming(self, ui, source, opts):
451 451 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
452 452
453 453 def files(self):
454 454 rev = self._state[1]
455 455 ctx = self._repo[rev]
456 456 return ctx.manifest()
457 457
458 458 def filedata(self, name):
459 459 rev = self._state[1]
460 460 return self._repo[rev][name].data()
461 461
462 462 def fileflags(self, name):
463 463 rev = self._state[1]
464 464 ctx = self._repo[rev]
465 465 return ctx.flags(name)
466 466
467 467
468 468 class svnsubrepo(abstractsubrepo):
469 469 def __init__(self, ctx, path, state):
470 470 self._path = path
471 471 self._state = state
472 472 self._ctx = ctx
473 473 self._ui = ctx._repo.ui
474 474
475 475 def _svncommand(self, commands, filename=''):
476 476 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
477 477 cmd = ['svn'] + commands + [path]
478 478 cmd = [util.shellquote(arg) for arg in cmd]
479 479 cmd = util.quotecommand(' '.join(cmd))
480 480 env = dict(os.environ)
481 481 # Avoid localized output, preserve current locale for everything else.
482 482 env['LC_MESSAGES'] = 'C'
483 483 write, read, err = util.popen3(cmd, env=env, newlines=True)
484 484 retdata = read.read()
485 485 err = err.read().strip()
486 486 if err:
487 487 raise util.Abort(err)
488 488 return retdata
489 489
490 490 def _wcrev(self):
491 491 output = self._svncommand(['info', '--xml'])
492 492 doc = xml.dom.minidom.parseString(output)
493 493 entries = doc.getElementsByTagName('entry')
494 494 if not entries:
495 495 return '0'
496 496 return str(entries[0].getAttribute('revision')) or '0'
497 497
498 498 def _wcchanged(self):
499 499 """Return (changes, extchanges) where changes is True
500 500 if the working directory was changed, and extchanges is
501 501 True if any of these changes concern an external entry.
502 502 """
503 503 output = self._svncommand(['status', '--xml'])
504 504 externals, changes = [], []
505 505 doc = xml.dom.minidom.parseString(output)
506 506 for e in doc.getElementsByTagName('entry'):
507 507 s = e.getElementsByTagName('wc-status')
508 508 if not s:
509 509 continue
510 510 item = s[0].getAttribute('item')
511 511 props = s[0].getAttribute('props')
512 512 path = e.getAttribute('path')
513 513 if item == 'external':
514 514 externals.append(path)
515 515 if (item not in ('', 'normal', 'unversioned', 'external')
516 516 or props not in ('', 'none')):
517 517 changes.append(path)
518 518 for path in changes:
519 519 for ext in externals:
520 520 if path == ext or path.startswith(ext + os.sep):
521 521 return True, True
522 522 return bool(changes), False
523 523
524 524 def dirty(self):
525 525 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
526 526 return False
527 527 return True
528 528
529 529 def commit(self, text, user, date):
530 530 # user and date are out of our hands since svn is centralized
531 531 changed, extchanged = self._wcchanged()
532 532 if not changed:
533 533 return self._wcrev()
534 534 if extchanged:
535 535 # Do not try to commit externals
536 536 raise util.Abort(_('cannot commit svn externals'))
537 537 commitinfo = self._svncommand(['commit', '-m', text])
538 538 self._ui.status(commitinfo)
539 539 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
540 540 if not newrev:
541 541 raise util.Abort(commitinfo.splitlines()[-1])
542 542 newrev = newrev.groups()[0]
543 543 self._ui.status(self._svncommand(['update', '-r', newrev]))
544 544 return newrev
545 545
546 546 def remove(self):
547 547 if self.dirty():
548 548 self._ui.warn(_('not removing repo %s because '
549 549 'it has changes.\n' % self._path))
550 550 return
551 551 self._ui.note(_('removing subrepo %s\n') % self._path)
552 552 shutil.rmtree(self._ctx._repo.wjoin(self._path))
553 553
554 554 def get(self, state):
555 555 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
556 556 if not re.search('Checked out revision [0-9]+.', status):
557 557 raise util.Abort(status.splitlines()[-1])
558 558 self._ui.status(status)
559 559
560 560 def merge(self, state):
561 561 old = int(self._state[1])
562 562 new = int(state[1])
563 563 if new > old:
564 564 self.get(state)
565 565
566 566 def push(self, force):
567 567 # push is a no-op for SVN
568 568 return True
569 569
570 570 def files(self):
571 571 output = self._svncommand(['list'])
572 572 # This works because svn forbids \n in filenames.
573 573 return output.splitlines()
574 574
575 575 def filedata(self, name):
576 576 return self._svncommand(['cat'], name)
577 577
578 578
579 579 class gitsubrepo(object):
580 580 def __init__(self, ctx, path, state):
581 581 # TODO add git version check.
582 582 self._state = state
583 583 self._ctx = ctx
584 584 self._relpath = path
585 585 self._path = ctx._repo.wjoin(path)
586 586 self._ui = ctx._repo.ui
587 587
588 588 def _gitcommand(self, commands):
589 589 return self._gitdir(commands)[0]
590 590
591 591 def _gitdir(self, commands):
592 592 commands = ['--no-pager', '--git-dir=%s/.git' % self._path,
593 593 '--work-tree=%s' % self._path] + commands
594 594 return self._gitnodir(commands)
595 595
596 596 def _gitnodir(self, commands):
597 597 """Calls the git command
598 598
599 599 The methods tries to call the git command. versions previor to 1.6.0
600 600 are not supported and very probably fail.
601 601 """
602 602 cmd = ['git'] + commands
603 603 cmd = [util.shellquote(arg) for arg in cmd]
604 604 cmd = util.quotecommand(' '.join(cmd))
605 605
606 606 # print git's stderr, which is mostly progress and useful info
607 607 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
608 608 close_fds=(os.name == 'posix'),
609 609 stdout=subprocess.PIPE)
610 610 retdata = p.stdout.read()
611 611 # wait for the child to exit to avoid race condition.
612 612 p.wait()
613 613
614 614 if p.returncode != 0:
615 615 # there are certain error codes that are ok
616 616 command = None
617 617 for arg in commands:
618 618 if not arg.startswith('-'):
619 619 command = arg
620 620 break
621 621 if command == 'cat-file':
622 622 return retdata, p.returncode
623 623 if command in ('commit', 'status') and p.returncode == 1:
624 624 return retdata, p.returncode
625 625 # for all others, abort
626 626 raise util.Abort('git %s error %d' % (command, p.returncode))
627 627
628 628 return retdata, p.returncode
629 629
630 630 def _gitstate(self):
631 631 return self._gitcommand(['rev-parse', 'HEAD']).strip()
632 632
633 633 def _githavelocally(self, revision):
634 634 out, code = self._gitdir(['cat-file', '-e', revision])
635 635 return code == 0
636 636
637 def _gitbranchmap(self):
638 'returns the current branch and a map from git revision to branch[es]'
639 bm = {}
640 redirects = {}
641 current = None
642 out = self._gitcommand(['branch', '-a', '--no-color',
643 '--verbose', '--abbrev=40'])
644 for line in out.split('\n'):
645 if not line:
646 continue
647 if line[2:].startswith('(no branch)'):
648 continue
649 branch, revision = line[2:].split()[:2]
650 if revision == '->':
651 continue # ignore remote/HEAD redirects
652 if line[0] == '*':
653 current = branch
654 bm.setdefault(revision, []).append(branch)
655 return current, bm
656
637 657 def _fetch(self, source, revision):
638 658 if not os.path.exists('%s/.git' % self._path):
639 659 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
640 660 self._gitnodir(['clone', source, self._path])
641 661 if self._githavelocally(revision):
642 662 return
643 663 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
644 664 self._gitcommand(['fetch', '--all', '-q'])
645 665 if not self._githavelocally(revision):
646 666 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
647 667 (revision, self._path))
648 668
649 669 def dirty(self):
650 670 if self._state[1] != self._gitstate(): # version checked out changed?
651 671 return True
652 672 # check for staged changes or modified files; ignore untracked files
653 673 # docs say --porcelain flag is future-proof format
654 674 changed = self._gitcommand(['status', '--porcelain',
655 675 '--untracked-files=no'])
656 676 return bool(changed.strip())
657 677
658 678 def get(self, state):
659 679 source, revision, kind = state
660 680 self._fetch(source, revision)
661 if self._gitstate() != revision:
681 if self._gitstate() == revision:
682 return
683 current, bm = self._gitbranchmap()
684 if revision not in bm:
685 # no branch to checkout, check it out with no branch
662 686 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
663 687 self._relpath)
664 688 self._ui.warn(_('check out a git branch if you intend '
665 689 'to make changes\n'))
666 690 self._gitcommand(['checkout', '-q', revision])
691 return
692 branches = bm[revision]
693 firstlocalbranch = None
694 for b in branches:
695 if b == 'master':
696 # master trumps all other branches
697 self._gitcommand(['checkout', 'master'])
698 return
699 if not firstlocalbranch and not b.startswith('remotes/'):
700 firstlocalbranch = b
701 if firstlocalbranch:
702 self._gitcommand(['checkout', firstlocalbranch])
703 else:
704 remote = branches[0]
705 local = remote.split('/')[-1]
706 self._gitcommand(['checkout', '-b', local, remote])
667 707
668 708 def commit(self, text, user, date):
669 709 cmd = ['commit', '-a', '-m', text]
670 710 if user:
671 711 cmd += ['--author', user]
672 712 if date:
673 713 # git's date parser silently ignores when seconds < 1e9
674 714 # convert to ISO8601
675 715 cmd += ['--date', util.datestr(date, '%Y-%m-%dT%H:%M:%S %1%2')]
676 716 self._gitcommand(cmd)
677 717 # make sure commit works otherwise HEAD might not exist under certain
678 718 # circumstances
679 719 return self._gitstate()
680 720
681 721 def merge(self, state):
682 722 source, revision, kind = state
683 723 self._fetch(source, revision)
684 724 base = self._gitcommand(['merge-base', revision,
685 725 self._state[1]]).strip()
686 726 if base == revision:
687 727 self.get(state) # fast forward merge
688 728 elif base != self._state[1]:
689 729 self._gitcommand(['merge', '--no-commit', revision])
690 730
691 731 def push(self, force):
692 732 cmd = ['push']
693 733 if force:
694 734 cmd.append('--force')
695 # as subrepos have no notion of "where to push to" we
696 # assume origin master. This is git's default
697 self._gitcommand(cmd + ['origin', 'master', '-q'])
735 # push the currently checked out branch
736 current, bm = self._gitbranchmap()
737 if current:
738 self._gitcommand(cmd + ['origin', current, '-q'])
698 739 return True
740 else:
741 self._ui.warn(_('no branch checked out in subrepo %s\n'
742 'nothing to push') % self._relpath)
743 return False
699 744
700 745 types = {
701 746 'hg': hgsubrepo,
702 747 'svn': svnsubrepo,
703 748 'git': gitsubrepo,
704 749 }
@@ -1,173 +1,177 b''
1 1 $ "$TESTDIR/hghave" git || exit 80
2 2
3 3 make git commits repeatable
4 4
5 5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
6 6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
7 7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
8 8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
9 9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
10 10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
11 11
12 12 root hg repo
13 13
14 14 $ hg init t
15 15 $ cd t
16 16 $ echo a > a
17 17 $ hg add a
18 18 $ hg commit -m a
19 19 $ cd ..
20 20
21 21 new external git repo
22 22
23 23 $ mkdir gitroot
24 24 $ cd gitroot
25 25 $ git init -q
26 26 $ echo g > g
27 27 $ git add g
28 28 $ git commit -q -m g
29 29
30 30 add subrepo clone
31 31
32 32 $ cd ../t
33 33 $ echo 's = [git]../gitroot' > .hgsub
34 34 $ git clone -q ../gitroot s
35 35 $ hg add .hgsub
36 36 $ hg commit -m 'new git subrepo'
37 37 committing subrepository $TESTTMP/t/s
38 38 $ hg debugsub
39 39 path s
40 40 source ../gitroot
41 41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
42 42
43 record a new commit from upstream
43 record a new commit from upstream from a different branch
44 44
45 45 $ cd ../gitroot
46 $ git checkout -b testing
47 Switched to a new branch 'testing'
46 48 $ echo gg >> g
47 49 $ git commit -q -a -m gg
48 50
49 51 $ cd ../t/s
50 52 $ git pull -q
53 $ git checkout -b testing origin/testing
54 Switched to a new branch 'testing'
55 Branch testing set up to track remote branch testing from origin.
51 56
52 57 $ cd ..
53 58 $ hg commit -m 'update git subrepo'
54 59 committing subrepository $TESTTMP/t/s
55 60 $ hg debugsub
56 61 path s
57 62 source ../gitroot
58 63 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
59 64
60 65 clone root
61 66
62 67 $ hg clone . ../tc
63 68 updating to branch default
64 69 cloning subrepo s
65 70 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
66 71 $ cd ../tc
67 72 $ hg debugsub
68 73 path s
69 74 source ../gitroot
70 75 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
71 76
72 77 update to previous substate
73 78
74 79 $ hg update 1
75 checking out detached HEAD in subrepo s
76 check out a git branch if you intend to make changes
80 Switched to a new branch 'master'
77 81 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 82 $ cat s/g
79 83 g
80 84 $ hg debugsub
81 85 path s
82 86 source ../gitroot
83 87 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
84 88
85 89 make $GITROOT pushable, by replacing it with a clone with nothing checked out
86 90
87 91 $ cd ..
88 92 $ git clone gitroot gitrootbare --bare -q
89 93 $ rm -rf gitroot
90 94 $ mv gitrootbare gitroot
91 95
92 96 clone root, make local change
93 97
94 98 $ cd t
95 99 $ hg clone . ../ta
96 100 updating to branch default
97 101 cloning subrepo s
98 102 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
99 103
100 104 $ cd ../ta
101 105 $ echo ggg >> s/g
102 106 $ hg commit -m ggg
103 107 committing subrepository $TESTTMP/ta/s
104 108 $ hg debugsub
105 109 path s
106 110 source ../gitroot
107 111 revision 79695940086840c99328513acbe35f90fcd55e57
108 112
109 113 clone root separately, make different local change
110 114
111 115 $ cd ../t
112 116 $ hg clone . ../tb
113 117 updating to branch default
114 118 cloning subrepo s
115 119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
116 120
117 121 $ cd ../tb/s
118 122 $ echo f > f
119 123 $ git add f
120 124 $ cd ..
121 125
122 126 $ hg commit -m f
123 127 committing subrepository $TESTTMP/tb/s
124 128 $ hg debugsub
125 129 path s
126 130 source ../gitroot
127 131 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
128 132
129 133 user b push changes
130 134
131 135 $ hg push
132 136 pushing to $TESTTMP/t
133 137 searching for changes
134 138 adding changesets
135 139 adding manifests
136 140 adding file changes
137 141 added 1 changesets with 1 changes to 1 files
138 142
139 143 user a pulls, merges, commits
140 144
141 145 $ cd ../ta
142 146 $ hg pull
143 147 pulling from $TESTTMP/t
144 148 searching for changes
145 149 adding changesets
146 150 adding manifests
147 151 adding file changes
148 152 added 1 changesets with 1 changes to 1 files (+1 heads)
149 153 (run 'hg heads' to see heads, 'hg merge' to merge)
150 154 $ hg merge
151 155 Automatic merge went well; stopped before committing as requested
152 156 pulling subrepo s
153 157 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
154 158 (branch merge, don't forget to commit)
155 159 $ cat s/f
156 160 f
157 161 $ cat s/g
158 162 g
159 163 gg
160 164 ggg
161 165 $ hg commit -m 'merge'
162 166 committing subrepository $TESTTMP/ta/s
163 167 $ hg debugsub
164 168 path s
165 169 source ../gitroot
166 170 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
167 171 $ hg push
168 172 pushing to $TESTTMP/t
169 173 searching for changes
170 174 adding changesets
171 175 adding manifests
172 176 adding file changes
173 177 added 2 changesets with 2 changes to 1 files
General Comments 0
You need to be logged in to leave comments. Login now