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