##// END OF EJS Templates
subrepo: lazier git push logic...
Eric Eisner -
r13029:f930032a default
parent child Browse files
Show More
@@ -1,819 +1,837 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 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 cmd = [util.shellquote(arg) for arg in cmd]
488 488 cmd = util.quotecommand(' '.join(cmd))
489 489 env = dict(os.environ)
490 490 # Avoid localized output, preserve current locale for everything else.
491 491 env['LC_MESSAGES'] = 'C'
492 492 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
493 493 close_fds=util.closefds,
494 494 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
495 495 universal_newlines=True, env=env)
496 496 stdout, stderr = p.communicate()
497 497 stderr = stderr.strip()
498 498 if stderr:
499 499 raise util.Abort(stderr)
500 500 return stdout
501 501
502 502 def _wcrev(self):
503 503 output = self._svncommand(['info', '--xml'])
504 504 doc = xml.dom.minidom.parseString(output)
505 505 entries = doc.getElementsByTagName('entry')
506 506 if not entries:
507 507 return '0'
508 508 return str(entries[0].getAttribute('revision')) or '0'
509 509
510 510 def _wcchanged(self):
511 511 """Return (changes, extchanges) where changes is True
512 512 if the working directory was changed, and extchanges is
513 513 True if any of these changes concern an external entry.
514 514 """
515 515 output = self._svncommand(['status', '--xml'])
516 516 externals, changes = [], []
517 517 doc = xml.dom.minidom.parseString(output)
518 518 for e in doc.getElementsByTagName('entry'):
519 519 s = e.getElementsByTagName('wc-status')
520 520 if not s:
521 521 continue
522 522 item = s[0].getAttribute('item')
523 523 props = s[0].getAttribute('props')
524 524 path = e.getAttribute('path')
525 525 if item == 'external':
526 526 externals.append(path)
527 527 if (item not in ('', 'normal', 'unversioned', 'external')
528 528 or props not in ('', 'none')):
529 529 changes.append(path)
530 530 for path in changes:
531 531 for ext in externals:
532 532 if path == ext or path.startswith(ext + os.sep):
533 533 return True, True
534 534 return bool(changes), False
535 535
536 536 def dirty(self):
537 537 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
538 538 return False
539 539 return True
540 540
541 541 def commit(self, text, user, date):
542 542 # user and date are out of our hands since svn is centralized
543 543 changed, extchanged = self._wcchanged()
544 544 if not changed:
545 545 return self._wcrev()
546 546 if extchanged:
547 547 # Do not try to commit externals
548 548 raise util.Abort(_('cannot commit svn externals'))
549 549 commitinfo = self._svncommand(['commit', '-m', text])
550 550 self._ui.status(commitinfo)
551 551 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
552 552 if not newrev:
553 553 raise util.Abort(commitinfo.splitlines()[-1])
554 554 newrev = newrev.groups()[0]
555 555 self._ui.status(self._svncommand(['update', '-r', newrev]))
556 556 return newrev
557 557
558 558 def remove(self):
559 559 if self.dirty():
560 560 self._ui.warn(_('not removing repo %s because '
561 561 'it has changes.\n' % self._path))
562 562 return
563 563 self._ui.note(_('removing subrepo %s\n') % self._path)
564 564
565 565 def onerror(function, path, excinfo):
566 566 if function is not os.remove:
567 567 raise
568 568 # read-only files cannot be unlinked under Windows
569 569 s = os.stat(path)
570 570 if (s.st_mode & stat.S_IWRITE) != 0:
571 571 raise
572 572 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
573 573 os.remove(path)
574 574
575 575 path = self._ctx._repo.wjoin(self._path)
576 576 shutil.rmtree(path, onerror=onerror)
577 577 try:
578 578 os.removedirs(os.path.dirname(path))
579 579 except OSError:
580 580 pass
581 581
582 582 def get(self, state):
583 583 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
584 584 if not re.search('Checked out revision [0-9]+.', status):
585 585 raise util.Abort(status.splitlines()[-1])
586 586 self._ui.status(status)
587 587
588 588 def merge(self, state):
589 589 old = int(self._state[1])
590 590 new = int(state[1])
591 591 if new > old:
592 592 self.get(state)
593 593
594 594 def push(self, force):
595 595 # push is a no-op for SVN
596 596 return True
597 597
598 598 def files(self):
599 599 output = self._svncommand(['list'])
600 600 # This works because svn forbids \n in filenames.
601 601 return output.splitlines()
602 602
603 603 def filedata(self, name):
604 604 return self._svncommand(['cat'], name)
605 605
606 606
607 607 class gitsubrepo(object):
608 608 def __init__(self, ctx, path, state):
609 609 # TODO add git version check.
610 610 self._state = state
611 611 self._ctx = ctx
612 612 self._relpath = path
613 613 self._path = ctx._repo.wjoin(path)
614 614 self._ui = ctx._repo.ui
615 615
616 616 def _gitcommand(self, commands, stream=False):
617 617 return self._gitdir(commands, stream=stream)[0]
618 618
619 619 def _gitdir(self, commands, stream=False):
620 620 commands = ['--no-pager', '--git-dir=%s/.git' % self._path,
621 621 '--work-tree=%s' % self._path] + commands
622 622 return self._gitnodir(commands, stream=stream)
623 623
624 624 def _gitnodir(self, commands, stream=False):
625 625 """Calls the git command
626 626
627 627 The methods tries to call the git command. versions previor to 1.6.0
628 628 are not supported and very probably fail.
629 629 """
630 630 cmd = ['git'] + commands
631 631 cmd = [util.shellquote(arg) for arg in cmd]
632 632 cmd = util.quotecommand(' '.join(cmd))
633 633
634 634 # print git's stderr, which is mostly progress and useful info
635 635 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
636 636 close_fds=util.closefds,
637 637 stdout=subprocess.PIPE)
638 638 if stream:
639 639 return p.stdout, None
640 640
641 641 retdata = p.stdout.read()
642 642 # wait for the child to exit to avoid race condition.
643 643 p.wait()
644 644
645 645 if p.returncode != 0:
646 646 # there are certain error codes that are ok
647 647 command = None
648 648 for arg in commands:
649 649 if not arg.startswith('-'):
650 650 command = arg
651 651 break
652 652 if command == 'cat-file':
653 653 return retdata, p.returncode
654 654 if command in ('commit', 'status') and p.returncode == 1:
655 655 return retdata, p.returncode
656 656 # for all others, abort
657 657 raise util.Abort('git %s error %d' % (command, p.returncode))
658 658
659 659 return retdata, p.returncode
660 660
661 661 def _gitstate(self):
662 662 return self._gitcommand(['rev-parse', 'HEAD']).strip()
663 663
664 664 def _githavelocally(self, revision):
665 665 out, code = self._gitdir(['cat-file', '-e', revision])
666 666 return code == 0
667 667
668 def _gitisancestor(self, r1, r2):
669 base = self._gitcommand(['merge-base', r1, r2]).strip()
670 return base == r1
671
668 672 def _gitbranchmap(self):
669 673 'returns the current branch and a map from git revision to branch[es]'
670 674 bm = {}
671 675 redirects = {}
672 676 current = None
673 677 out = self._gitcommand(['branch', '-a', '--no-color',
674 678 '--verbose', '--abbrev=40'])
675 679 for line in out.split('\n'):
676 680 if not line:
677 681 continue
678 682 if line[2:].startswith('(no branch)'):
679 683 continue
680 684 branch, revision = line[2:].split()[:2]
681 685 if revision == '->':
682 686 continue # ignore remote/HEAD redirects
683 687 if line[0] == '*':
684 688 current = branch
685 689 bm.setdefault(revision, []).append(branch)
686 690 return current, bm
687 691
688 692 def _fetch(self, source, revision):
689 693 if not os.path.exists('%s/.git' % self._path):
690 694 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
691 695 self._gitnodir(['clone', source, self._path])
692 696 if self._githavelocally(revision):
693 697 return
694 698 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
695 699 self._gitcommand(['fetch', '--all', '-q'])
696 700 if not self._githavelocally(revision):
697 701 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
698 702 (revision, self._path))
699 703
700 704 def dirty(self):
701 705 if self._state[1] != self._gitstate(): # version checked out changed?
702 706 return True
703 707 # check for staged changes or modified files; ignore untracked files
704 708 # docs say --porcelain flag is future-proof format
705 709 changed = self._gitcommand(['status', '--porcelain',
706 710 '--untracked-files=no'])
707 711 return bool(changed.strip())
708 712
709 713 def get(self, state):
710 714 source, revision, kind = state
711 715 self._fetch(source, revision)
712 716 # if the repo was set to be bare, unbare it
713 717 if self._gitcommand(['config', '--get', 'core.bare']
714 718 ).strip() == 'true':
715 719 self._gitcommand(['config', 'core.bare', 'false'])
716 720 if self._gitstate() == revision:
717 721 self._gitcommand(['reset', '--hard', 'HEAD'])
718 722 return
719 723 elif self._gitstate() == revision:
720 724 return
721 725 current, bm = self._gitbranchmap()
722 726 if revision not in bm:
723 727 # no branch to checkout, check it out with no branch
724 728 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
725 729 self._relpath)
726 730 self._ui.warn(_('check out a git branch if you intend '
727 731 'to make changes\n'))
728 732 self._gitcommand(['checkout', '-q', revision])
729 733 return
730 734 branches = bm[revision]
731 735 firstlocalbranch = None
732 736 for b in branches:
733 737 if b == 'master':
734 738 # master trumps all other branches
735 739 self._gitcommand(['checkout', 'master'])
736 740 return
737 741 if not firstlocalbranch and not b.startswith('remotes/'):
738 742 firstlocalbranch = b
739 743 if firstlocalbranch:
740 744 self._gitcommand(['checkout', firstlocalbranch])
741 745 else:
742 746 remote = branches[0]
743 747 local = remote.split('/')[-1]
744 748 self._gitcommand(['checkout', '-b', local, remote])
745 749
746 750 def commit(self, text, user, date):
747 751 cmd = ['commit', '-a', '-m', text]
748 752 if user:
749 753 cmd += ['--author', user]
750 754 if date:
751 755 # git's date parser silently ignores when seconds < 1e9
752 756 # convert to ISO8601
753 757 cmd += ['--date', util.datestr(date, '%Y-%m-%dT%H:%M:%S %1%2')]
754 758 self._gitcommand(cmd)
755 759 # make sure commit works otherwise HEAD might not exist under certain
756 760 # circumstances
757 761 return self._gitstate()
758 762
759 763 def merge(self, state):
760 764 source, revision, kind = state
761 765 self._fetch(source, revision)
762 766 base = self._gitcommand(['merge-base', revision,
763 767 self._state[1]]).strip()
764 768 if base == revision:
765 769 self.get(state) # fast forward merge
766 770 elif base != self._state[1]:
767 771 self._gitcommand(['merge', '--no-commit', revision])
768 772
769 773 def push(self, force):
774 # if a branch in origin contains the revision, nothing to do
775 current, bm = self._gitbranchmap()
776 for revision, branches in bm.iteritems():
777 for b in branches:
778 if b.startswith('remotes/origin'):
779 if self._gitisancestor(self._state[1], revision):
780 return True
781 # otherwise, try to push the currently checked out branch
770 782 cmd = ['push']
771 783 if force:
772 784 cmd.append('--force')
773 # push the currently checked out branch
774 current, bm = self._gitbranchmap()
775 785 if current:
786 # determine if the current branch is even useful
787 if not self._gitisancestor(self._state[1], current):
788 self._ui.warn(_('unrelated git branch checked out '
789 'in subrepo %s\n') % self._relpath)
790 return False
791 self._ui.status(_('pushing branch %s of subrepo %s\n') %
792 (current, self._relpath))
776 793 self._gitcommand(cmd + ['origin', current, '-q'])
777 794 return True
778 795 else:
779 796 self._ui.warn(_('no branch checked out in subrepo %s\n'
780 'nothing to push') % self._relpath)
797 'cannot push revision %s') %
798 (self._relpath, self._state[1]))
781 799 return False
782 800
783 801 def remove(self):
784 802 if self.dirty():
785 803 self._ui.warn(_('not removing repo %s because '
786 804 'it has changes.\n') % self._path)
787 805 return
788 806 # we can't fully delete the repository as it may contain
789 807 # local-only history
790 808 self._ui.note(_('removing subrepo %s\n') % self._path)
791 809 self._gitcommand(['config', 'core.bare', 'true'])
792 810 for f in os.listdir(self._path):
793 811 if f == '.git':
794 812 continue
795 813 path = os.path.join(self._path, f)
796 814 if os.path.isdir(path) and not os.path.islink(path):
797 815 shutil.rmtree(path)
798 816 else:
799 817 os.remove(path)
800 818
801 819 def archive(self, archiver, prefix):
802 820 source, revision = self._state
803 821 self._fetch(source, revision)
804 822
805 823 # Parse git's native archive command.
806 824 # This should be much faster than manually traversing the trees
807 825 # and objects with many subprocess calls.
808 826 tarstream = self._gitcommand(['archive', revision], stream=True)
809 827 tar = tarfile.open(fileobj=tarstream, mode='r|')
810 828 for info in tar:
811 829 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
812 830 info.mode, info.issym(),
813 831 tar.extractfile(info).read())
814 832
815 833 types = {
816 834 'hg': hgsubrepo,
817 835 'svn': svnsubrepo,
818 836 'git': gitsubrepo,
819 837 }
@@ -1,209 +1,242 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 43 record a new commit from upstream from a different branch
44 44
45 45 $ cd ../gitroot
46 46 $ git checkout -b testing
47 47 Switched to a new branch 'testing'
48 48 $ echo gg >> g
49 49 $ git commit -q -a -m gg
50 50
51 51 $ cd ../t/s
52 52 $ git pull -q
53 53 $ git checkout -b testing origin/testing
54 54 Switched to a new branch 'testing'
55 55 Branch testing set up to track remote branch testing from origin.
56 56
57 57 $ cd ..
58 58 $ hg commit -m 'update git subrepo'
59 59 committing subrepository $TESTTMP/t/s
60 60 $ hg debugsub
61 61 path s
62 62 source ../gitroot
63 63 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
64 64
65 65 clone root
66 66
67 67 $ hg clone . ../tc
68 68 updating to branch default
69 69 cloning subrepo s
70 70 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
71 71 $ cd ../tc
72 72 $ hg debugsub
73 73 path s
74 74 source ../gitroot
75 75 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
76 76
77 77 update to previous substate
78 78
79 79 $ hg update 1
80 80 Switched to a new branch 'master'
81 81 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
82 82 $ cat s/g
83 83 g
84 84 $ hg debugsub
85 85 path s
86 86 source ../gitroot
87 87 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
88 88
89 89 make $GITROOT pushable, by replacing it with a clone with nothing checked out
90 90
91 91 $ cd ..
92 92 $ git clone gitroot gitrootbare --bare -q
93 93 $ rm -rf gitroot
94 94 $ mv gitrootbare gitroot
95 95
96 96 clone root, make local change
97 97
98 98 $ cd t
99 99 $ hg clone . ../ta
100 100 updating to branch default
101 101 cloning subrepo s
102 102 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
103 103
104 104 $ cd ../ta
105 105 $ echo ggg >> s/g
106 106 $ hg commit -m ggg
107 107 committing subrepository $TESTTMP/ta/s
108 108 $ hg debugsub
109 109 path s
110 110 source ../gitroot
111 111 revision 79695940086840c99328513acbe35f90fcd55e57
112 112
113 113 clone root separately, make different local change
114 114
115 115 $ cd ../t
116 116 $ hg clone . ../tb
117 117 updating to branch default
118 118 cloning subrepo s
119 119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
120 120
121 121 $ cd ../tb/s
122 122 $ echo f > f
123 123 $ git add f
124 124 $ cd ..
125 125
126 126 $ hg commit -m f
127 127 committing subrepository $TESTTMP/tb/s
128 128 $ hg debugsub
129 129 path s
130 130 source ../gitroot
131 131 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
132 132
133 133 user b push changes
134 134
135 135 $ hg push
136 136 pushing to $TESTTMP/t
137 pushing branch testing of subrepo s
137 138 searching for changes
138 139 adding changesets
139 140 adding manifests
140 141 adding file changes
141 142 added 1 changesets with 1 changes to 1 files
142 143
143 144 user a pulls, merges, commits
144 145
145 146 $ cd ../ta
146 147 $ hg pull
147 148 pulling from $TESTTMP/t
148 149 searching for changes
149 150 adding changesets
150 151 adding manifests
151 152 adding file changes
152 153 added 1 changesets with 1 changes to 1 files (+1 heads)
153 154 (run 'hg heads' to see heads, 'hg merge' to merge)
154 155 $ hg merge
155 156 Automatic merge went well; stopped before committing as requested
156 157 pulling subrepo s
157 158 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
158 159 (branch merge, don't forget to commit)
159 160 $ cat s/f
160 161 f
161 162 $ cat s/g
162 163 g
163 164 gg
164 165 ggg
165 166 $ hg commit -m 'merge'
166 167 committing subrepository $TESTTMP/ta/s
167 168 $ hg debugsub
168 169 path s
169 170 source ../gitroot
170 171 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
171 172 $ hg push
172 173 pushing to $TESTTMP/t
174 pushing branch testing of subrepo s
173 175 searching for changes
174 176 adding changesets
175 177 adding manifests
176 178 adding file changes
177 179 added 2 changesets with 2 changes to 1 files
178 180
181 make upstream git changes
182
183 $ cd ..
184 $ git clone -q gitroot gitclone
185 $ cd gitclone
186 $ echo ff >> f
187 $ git commit -q -a -m ff
188 $ echo fff >> f
189 $ git commit -q -a -m fff
190 $ git push -q origin testing
191
192 make and push changes to hg without updating the subrepo
193
194 $ cd ../t
195 $ hg clone . ../td
196 updating to branch default
197 cloning subrepo s
198 checking out detached HEAD in subrepo s
199 check out a git branch if you intend to make changes
200 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
201 $ cd ../td
202 $ echo aa >> a
203 $ hg commit -m aa
204 $ hg push
205 pushing to $TESTTMP/t
206 searching for changes
207 adding changesets
208 adding manifests
209 adding file changes
210 added 1 changesets with 1 changes to 1 files
211
179 212 update to a revision without the subrepo, keeping the local git repository
180 213
181 214 $ cd ../t
182 215 $ hg up 0
183 216 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
184 217 $ ls -a s
185 218 .
186 219 ..
187 220 .git
188 221
189 222 $ hg up 2
190 223 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
191 224 $ ls -a s
192 225 .
193 226 ..
194 227 .git
195 228 g
196 229
197 230 archive subrepos
198 231
199 232 $ cd ../t
200 233 $ hg archive --subrepos -r tip ../archive
201 234 pulling subrepo s
202 235 $ cd ../archive
203 236 $ cat s/f
204 237 f
205 238 $ cat s/g
206 239 g
207 240 gg
208 241 ggg
209 242
General Comments 0
You need to be logged in to leave comments. Login now