##// END OF EJS Templates
subrepo: simplify hgsubrepo._get a little
Martin Geisler -
r13753:78a0a815 default
parent child Browse files
Show More
@@ -1,1029 +1,1027 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, bookmarks
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, overwrite):
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, overwrite)
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, overwrite)
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, overwrite)
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 _updateprompt(ui, sub, dirty, local, remote):
167 167 if dirty:
168 168 msg = (_(' subrepository sources for %s differ\n'
169 169 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
170 170 % (subrelpath(sub), local, remote))
171 171 else:
172 172 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
173 173 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
174 174 % (subrelpath(sub), local, remote))
175 175 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
176 176
177 177 def reporelpath(repo):
178 178 """return path to this (sub)repo as seen from outermost repo"""
179 179 parent = repo
180 180 while hasattr(parent, '_subparent'):
181 181 parent = parent._subparent
182 182 return repo.root[len(parent.root)+1:]
183 183
184 184 def subrelpath(sub):
185 185 """return path to this subrepo as seen from outermost repo"""
186 186 if hasattr(sub, '_relpath'):
187 187 return sub._relpath
188 188 if not hasattr(sub, '_repo'):
189 189 return sub._path
190 190 return reporelpath(sub._repo)
191 191
192 192 def _abssource(repo, push=False, abort=True):
193 193 """return pull/push path of repo - either based on parent repo .hgsub info
194 194 or on the top repo config. Abort or return None if no source found."""
195 195 if hasattr(repo, '_subparent'):
196 196 source = repo._subsource
197 197 if source.startswith('/') or '://' in source:
198 198 return source
199 199 parent = _abssource(repo._subparent, push, abort=False)
200 200 if parent:
201 201 if '://' in parent:
202 202 if parent[-1] == '/':
203 203 parent = parent[:-1]
204 204 r = urlparse.urlparse(parent + '/' + source)
205 205 r = urlparse.urlunparse((r[0], r[1],
206 206 posixpath.normpath(r[2]),
207 207 r[3], r[4], r[5]))
208 208 return r
209 209 else: # plain file system path
210 210 return posixpath.normpath(os.path.join(parent, repo._subsource))
211 211 else: # recursion reached top repo
212 212 if hasattr(repo, '_subtoppath'):
213 213 return repo._subtoppath
214 214 if push and repo.ui.config('paths', 'default-push'):
215 215 return repo.ui.config('paths', 'default-push')
216 216 if repo.ui.config('paths', 'default'):
217 217 return repo.ui.config('paths', 'default')
218 218 if abort:
219 219 raise util.Abort(_("default path for subrepository %s not found") %
220 220 reporelpath(repo))
221 221
222 222 def itersubrepos(ctx1, ctx2):
223 223 """find subrepos in ctx1 or ctx2"""
224 224 # Create a (subpath, ctx) mapping where we prefer subpaths from
225 225 # ctx1. The subpaths from ctx2 are important when the .hgsub file
226 226 # has been modified (in ctx2) but not yet committed (in ctx1).
227 227 subpaths = dict.fromkeys(ctx2.substate, ctx2)
228 228 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
229 229 for subpath, ctx in sorted(subpaths.iteritems()):
230 230 yield subpath, ctx.sub(subpath)
231 231
232 232 def subrepo(ctx, path):
233 233 """return instance of the right subrepo class for subrepo in path"""
234 234 # subrepo inherently violates our import layering rules
235 235 # because it wants to make repo objects from deep inside the stack
236 236 # so we manually delay the circular imports to not break
237 237 # scripts that don't use our demand-loading
238 238 global hg
239 239 import hg as h
240 240 hg = h
241 241
242 242 util.path_auditor(ctx._repo.root)(path)
243 243 state = ctx.substate.get(path, nullstate)
244 244 if state[2] not in types:
245 245 raise util.Abort(_('unknown subrepo type %s') % state[2])
246 246 return types[state[2]](ctx, path, state[:2])
247 247
248 248 # subrepo classes need to implement the following abstract class:
249 249
250 250 class abstractsubrepo(object):
251 251
252 252 def dirty(self, ignoreupdate=False):
253 253 """returns true if the dirstate of the subrepo is dirty or does not
254 254 match current stored state. If ignoreupdate is true, only check
255 255 whether the subrepo has uncommitted changes in its dirstate.
256 256 """
257 257 raise NotImplementedError
258 258
259 259 def checknested(self, path):
260 260 """check if path is a subrepository within this repository"""
261 261 return False
262 262
263 263 def commit(self, text, user, date):
264 264 """commit the current changes to the subrepo with the given
265 265 log message. Use given user and date if possible. Return the
266 266 new state of the subrepo.
267 267 """
268 268 raise NotImplementedError
269 269
270 270 def remove(self):
271 271 """remove the subrepo
272 272
273 273 (should verify the dirstate is not dirty first)
274 274 """
275 275 raise NotImplementedError
276 276
277 277 def get(self, state, overwrite=False):
278 278 """run whatever commands are needed to put the subrepo into
279 279 this state
280 280 """
281 281 raise NotImplementedError
282 282
283 283 def merge(self, state):
284 284 """merge currently-saved state with the new state."""
285 285 raise NotImplementedError
286 286
287 287 def push(self, force):
288 288 """perform whatever action is analogous to 'hg push'
289 289
290 290 This may be a no-op on some systems.
291 291 """
292 292 raise NotImplementedError
293 293
294 294 def add(self, ui, match, dryrun, prefix):
295 295 return []
296 296
297 297 def status(self, rev2, **opts):
298 298 return [], [], [], [], [], [], []
299 299
300 300 def diff(self, diffopts, node2, match, prefix, **opts):
301 301 pass
302 302
303 303 def outgoing(self, ui, dest, opts):
304 304 return 1
305 305
306 306 def incoming(self, ui, source, opts):
307 307 return 1
308 308
309 309 def files(self):
310 310 """return filename iterator"""
311 311 raise NotImplementedError
312 312
313 313 def filedata(self, name):
314 314 """return file data"""
315 315 raise NotImplementedError
316 316
317 317 def fileflags(self, name):
318 318 """return file flags"""
319 319 return ''
320 320
321 321 def archive(self, ui, archiver, prefix):
322 322 files = self.files()
323 323 total = len(files)
324 324 relpath = subrelpath(self)
325 325 ui.progress(_('archiving (%s)') % relpath, 0,
326 326 unit=_('files'), total=total)
327 327 for i, name in enumerate(files):
328 328 flags = self.fileflags(name)
329 329 mode = 'x' in flags and 0755 or 0644
330 330 symlink = 'l' in flags
331 331 archiver.addfile(os.path.join(prefix, self._path, name),
332 332 mode, symlink, self.filedata(name))
333 333 ui.progress(_('archiving (%s)') % relpath, i + 1,
334 334 unit=_('files'), total=total)
335 335 ui.progress(_('archiving (%s)') % relpath, None)
336 336
337 337
338 338 class hgsubrepo(abstractsubrepo):
339 339 def __init__(self, ctx, path, state):
340 340 self._path = path
341 341 self._state = state
342 342 r = ctx._repo
343 343 root = r.wjoin(path)
344 344 create = False
345 345 if not os.path.exists(os.path.join(root, '.hg')):
346 346 create = True
347 347 util.makedirs(root)
348 348 self._repo = hg.repository(r.ui, root, create=create)
349 349 self._repo._subparent = r
350 350 self._repo._subsource = state[0]
351 351
352 352 if create:
353 353 fp = self._repo.opener("hgrc", "w", text=True)
354 354 fp.write('[paths]\n')
355 355
356 356 def addpathconfig(key, value):
357 357 if value:
358 358 fp.write('%s = %s\n' % (key, value))
359 359 self._repo.ui.setconfig('paths', key, value)
360 360
361 361 defpath = _abssource(self._repo, abort=False)
362 362 defpushpath = _abssource(self._repo, True, abort=False)
363 363 addpathconfig('default', defpath)
364 364 if defpath != defpushpath:
365 365 addpathconfig('default-push', defpushpath)
366 366 fp.close()
367 367
368 368 def add(self, ui, match, dryrun, prefix):
369 369 return cmdutil.add(ui, self._repo, match, dryrun, True,
370 370 os.path.join(prefix, self._path))
371 371
372 372 def status(self, rev2, **opts):
373 373 try:
374 374 rev1 = self._state[1]
375 375 ctx1 = self._repo[rev1]
376 376 ctx2 = self._repo[rev2]
377 377 return self._repo.status(ctx1, ctx2, **opts)
378 378 except error.RepoLookupError, inst:
379 379 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
380 380 % (inst, subrelpath(self)))
381 381 return [], [], [], [], [], [], []
382 382
383 383 def diff(self, diffopts, node2, match, prefix, **opts):
384 384 try:
385 385 node1 = node.bin(self._state[1])
386 386 # We currently expect node2 to come from substate and be
387 387 # in hex format
388 388 if node2 is not None:
389 389 node2 = node.bin(node2)
390 390 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
391 391 node1, node2, match,
392 392 prefix=os.path.join(prefix, self._path),
393 393 listsubrepos=True, **opts)
394 394 except error.RepoLookupError, inst:
395 395 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
396 396 % (inst, subrelpath(self)))
397 397
398 398 def archive(self, ui, archiver, prefix):
399 399 abstractsubrepo.archive(self, ui, archiver, prefix)
400 400
401 401 rev = self._state[1]
402 402 ctx = self._repo[rev]
403 403 for subpath in ctx.substate:
404 404 s = subrepo(ctx, subpath)
405 405 s.archive(ui, archiver, os.path.join(prefix, self._path))
406 406
407 407 def dirty(self, ignoreupdate=False):
408 408 r = self._state[1]
409 409 if r == '' and not ignoreupdate: # no state recorded
410 410 return True
411 411 w = self._repo[None]
412 412 if w.p1() != self._repo[r] and not ignoreupdate:
413 413 # different version checked out
414 414 return True
415 415 return w.dirty() # working directory changed
416 416
417 417 def checknested(self, path):
418 418 return self._repo._checknested(self._repo.wjoin(path))
419 419
420 420 def commit(self, text, user, date):
421 421 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
422 422 n = self._repo.commit(text, user, date)
423 423 if not n:
424 424 return self._repo['.'].hex() # different version checked out
425 425 return node.hex(n)
426 426
427 427 def remove(self):
428 428 # we can't fully delete the repository as it may contain
429 429 # local-only history
430 430 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
431 431 hg.clean(self._repo, node.nullid, False)
432 432
433 433 def _get(self, state):
434 434 source, revision, kind = state
435 try:
436 self._repo.lookup(revision)
437 except error.RepoError:
435 if revision not in self._repo:
438 436 self._repo._subsource = source
439 437 srcurl = _abssource(self._repo)
440 438 self._repo.ui.status(_('pulling subrepo %s from %s\n')
441 439 % (subrelpath(self), srcurl))
442 440 other = hg.repository(self._repo.ui, srcurl)
443 441 self._repo.pull(other)
444 442 bookmarks.updatefromremote(self._repo.ui, self._repo, other)
445 443
446 444 def get(self, state, overwrite=False):
447 445 self._get(state)
448 446 source, revision, kind = state
449 447 self._repo.ui.debug("getting subrepo %s\n" % self._path)
450 448 hg.clean(self._repo, revision, False)
451 449
452 450 def merge(self, state):
453 451 self._get(state)
454 452 cur = self._repo['.']
455 453 dst = self._repo[state[1]]
456 454 anc = dst.ancestor(cur)
457 455
458 456 def mergefunc():
459 457 if anc == cur:
460 458 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
461 459 hg.update(self._repo, state[1])
462 460 elif anc == dst:
463 461 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
464 462 else:
465 463 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
466 464 hg.merge(self._repo, state[1], remind=False)
467 465
468 466 wctx = self._repo[None]
469 467 if self.dirty():
470 468 if anc != dst:
471 469 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
472 470 mergefunc()
473 471 else:
474 472 mergefunc()
475 473 else:
476 474 mergefunc()
477 475
478 476 def push(self, force):
479 477 # push subrepos depth-first for coherent ordering
480 478 c = self._repo['']
481 479 subs = c.substate # only repos that are committed
482 480 for s in sorted(subs):
483 481 if not c.sub(s).push(force):
484 482 return False
485 483
486 484 dsturl = _abssource(self._repo, True)
487 485 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
488 486 (subrelpath(self), dsturl))
489 487 other = hg.repository(self._repo.ui, dsturl)
490 488 return self._repo.push(other, force)
491 489
492 490 def outgoing(self, ui, dest, opts):
493 491 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
494 492
495 493 def incoming(self, ui, source, opts):
496 494 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
497 495
498 496 def files(self):
499 497 rev = self._state[1]
500 498 ctx = self._repo[rev]
501 499 return ctx.manifest()
502 500
503 501 def filedata(self, name):
504 502 rev = self._state[1]
505 503 return self._repo[rev][name].data()
506 504
507 505 def fileflags(self, name):
508 506 rev = self._state[1]
509 507 ctx = self._repo[rev]
510 508 return ctx.flags(name)
511 509
512 510
513 511 class svnsubrepo(abstractsubrepo):
514 512 def __init__(self, ctx, path, state):
515 513 self._path = path
516 514 self._state = state
517 515 self._ctx = ctx
518 516 self._ui = ctx._repo.ui
519 517
520 518 def _svncommand(self, commands, filename=''):
521 519 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
522 520 cmd = ['svn'] + commands + [path]
523 521 env = dict(os.environ)
524 522 # Avoid localized output, preserve current locale for everything else.
525 523 env['LC_MESSAGES'] = 'C'
526 524 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
527 525 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
528 526 universal_newlines=True, env=env)
529 527 stdout, stderr = p.communicate()
530 528 stderr = stderr.strip()
531 529 if stderr:
532 530 raise util.Abort(stderr)
533 531 return stdout
534 532
535 533 def _wcrevs(self):
536 534 # Get the working directory revision as well as the last
537 535 # commit revision so we can compare the subrepo state with
538 536 # both. We used to store the working directory one.
539 537 output = self._svncommand(['info', '--xml'])
540 538 doc = xml.dom.minidom.parseString(output)
541 539 entries = doc.getElementsByTagName('entry')
542 540 lastrev, rev = '0', '0'
543 541 if entries:
544 542 rev = str(entries[0].getAttribute('revision')) or '0'
545 543 commits = entries[0].getElementsByTagName('commit')
546 544 if commits:
547 545 lastrev = str(commits[0].getAttribute('revision')) or '0'
548 546 return (lastrev, rev)
549 547
550 548 def _wcrev(self):
551 549 return self._wcrevs()[0]
552 550
553 551 def _wcchanged(self):
554 552 """Return (changes, extchanges) where changes is True
555 553 if the working directory was changed, and extchanges is
556 554 True if any of these changes concern an external entry.
557 555 """
558 556 output = self._svncommand(['status', '--xml'])
559 557 externals, changes = [], []
560 558 doc = xml.dom.minidom.parseString(output)
561 559 for e in doc.getElementsByTagName('entry'):
562 560 s = e.getElementsByTagName('wc-status')
563 561 if not s:
564 562 continue
565 563 item = s[0].getAttribute('item')
566 564 props = s[0].getAttribute('props')
567 565 path = e.getAttribute('path')
568 566 if item == 'external':
569 567 externals.append(path)
570 568 if (item not in ('', 'normal', 'unversioned', 'external')
571 569 or props not in ('', 'none')):
572 570 changes.append(path)
573 571 for path in changes:
574 572 for ext in externals:
575 573 if path == ext or path.startswith(ext + os.sep):
576 574 return True, True
577 575 return bool(changes), False
578 576
579 577 def dirty(self, ignoreupdate=False):
580 578 if not self._wcchanged()[0]:
581 579 if self._state[1] in self._wcrevs() or ignoreupdate:
582 580 return False
583 581 return True
584 582
585 583 def commit(self, text, user, date):
586 584 # user and date are out of our hands since svn is centralized
587 585 changed, extchanged = self._wcchanged()
588 586 if not changed:
589 587 return self._wcrev()
590 588 if extchanged:
591 589 # Do not try to commit externals
592 590 raise util.Abort(_('cannot commit svn externals'))
593 591 commitinfo = self._svncommand(['commit', '-m', text])
594 592 self._ui.status(commitinfo)
595 593 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
596 594 if not newrev:
597 595 raise util.Abort(commitinfo.splitlines()[-1])
598 596 newrev = newrev.groups()[0]
599 597 self._ui.status(self._svncommand(['update', '-r', newrev]))
600 598 return newrev
601 599
602 600 def remove(self):
603 601 if self.dirty():
604 602 self._ui.warn(_('not removing repo %s because '
605 603 'it has changes.\n' % self._path))
606 604 return
607 605 self._ui.note(_('removing subrepo %s\n') % self._path)
608 606
609 607 def onerror(function, path, excinfo):
610 608 if function is not os.remove:
611 609 raise
612 610 # read-only files cannot be unlinked under Windows
613 611 s = os.stat(path)
614 612 if (s.st_mode & stat.S_IWRITE) != 0:
615 613 raise
616 614 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
617 615 os.remove(path)
618 616
619 617 path = self._ctx._repo.wjoin(self._path)
620 618 shutil.rmtree(path, onerror=onerror)
621 619 try:
622 620 os.removedirs(os.path.dirname(path))
623 621 except OSError:
624 622 pass
625 623
626 624 def get(self, state, overwrite=False):
627 625 if overwrite:
628 626 self._svncommand(['revert', '--recursive'])
629 627 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
630 628 if not re.search('Checked out revision [0-9]+.', status):
631 629 raise util.Abort(status.splitlines()[-1])
632 630 self._ui.status(status)
633 631
634 632 def merge(self, state):
635 633 old = self._state[1]
636 634 new = state[1]
637 635 if new != self._wcrev():
638 636 dirty = old == self._wcrev() or self._wcchanged()[0]
639 637 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
640 638 self.get(state, False)
641 639
642 640 def push(self, force):
643 641 # push is a no-op for SVN
644 642 return True
645 643
646 644 def files(self):
647 645 output = self._svncommand(['list'])
648 646 # This works because svn forbids \n in filenames.
649 647 return output.splitlines()
650 648
651 649 def filedata(self, name):
652 650 return self._svncommand(['cat'], name)
653 651
654 652
655 653 class gitsubrepo(abstractsubrepo):
656 654 def __init__(self, ctx, path, state):
657 655 # TODO add git version check.
658 656 self._state = state
659 657 self._ctx = ctx
660 658 self._path = path
661 659 self._relpath = os.path.join(reporelpath(ctx._repo), path)
662 660 self._abspath = ctx._repo.wjoin(path)
663 661 self._subparent = ctx._repo
664 662 self._ui = ctx._repo.ui
665 663
666 664 def _gitcommand(self, commands, env=None, stream=False):
667 665 return self._gitdir(commands, env=env, stream=stream)[0]
668 666
669 667 def _gitdir(self, commands, env=None, stream=False):
670 668 return self._gitnodir(commands, env=env, stream=stream,
671 669 cwd=self._abspath)
672 670
673 671 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
674 672 """Calls the git command
675 673
676 674 The methods tries to call the git command. versions previor to 1.6.0
677 675 are not supported and very probably fail.
678 676 """
679 677 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
680 678 # unless ui.quiet is set, print git's stderr,
681 679 # which is mostly progress and useful info
682 680 errpipe = None
683 681 if self._ui.quiet:
684 682 errpipe = open(os.devnull, 'w')
685 683 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
686 684 close_fds=util.closefds,
687 685 stdout=subprocess.PIPE, stderr=errpipe)
688 686 if stream:
689 687 return p.stdout, None
690 688
691 689 retdata = p.stdout.read().strip()
692 690 # wait for the child to exit to avoid race condition.
693 691 p.wait()
694 692
695 693 if p.returncode != 0 and p.returncode != 1:
696 694 # there are certain error codes that are ok
697 695 command = commands[0]
698 696 if command in ('cat-file', 'symbolic-ref'):
699 697 return retdata, p.returncode
700 698 # for all others, abort
701 699 raise util.Abort('git %s error %d in %s' %
702 700 (command, p.returncode, self._relpath))
703 701
704 702 return retdata, p.returncode
705 703
706 704 def _gitmissing(self):
707 705 return not os.path.exists(os.path.join(self._abspath, '.git'))
708 706
709 707 def _gitstate(self):
710 708 return self._gitcommand(['rev-parse', 'HEAD'])
711 709
712 710 def _gitcurrentbranch(self):
713 711 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
714 712 if err:
715 713 current = None
716 714 return current
717 715
718 716 def _gitremote(self, remote):
719 717 out = self._gitcommand(['remote', 'show', '-n', remote])
720 718 line = out.split('\n')[1]
721 719 i = line.index('URL: ') + len('URL: ')
722 720 return line[i:]
723 721
724 722 def _githavelocally(self, revision):
725 723 out, code = self._gitdir(['cat-file', '-e', revision])
726 724 return code == 0
727 725
728 726 def _gitisancestor(self, r1, r2):
729 727 base = self._gitcommand(['merge-base', r1, r2])
730 728 return base == r1
731 729
732 730 def _gitbranchmap(self):
733 731 '''returns 2 things:
734 732 a map from git branch to revision
735 733 a map from revision to branches'''
736 734 branch2rev = {}
737 735 rev2branch = {}
738 736
739 737 out = self._gitcommand(['for-each-ref', '--format',
740 738 '%(objectname) %(refname)'])
741 739 for line in out.split('\n'):
742 740 revision, ref = line.split(' ')
743 741 if (not ref.startswith('refs/heads/') and
744 742 not ref.startswith('refs/remotes/')):
745 743 continue
746 744 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
747 745 continue # ignore remote/HEAD redirects
748 746 branch2rev[ref] = revision
749 747 rev2branch.setdefault(revision, []).append(ref)
750 748 return branch2rev, rev2branch
751 749
752 750 def _gittracking(self, branches):
753 751 'return map of remote branch to local tracking branch'
754 752 # assumes no more than one local tracking branch for each remote
755 753 tracking = {}
756 754 for b in branches:
757 755 if b.startswith('refs/remotes/'):
758 756 continue
759 757 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
760 758 if remote:
761 759 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
762 760 tracking['refs/remotes/%s/%s' %
763 761 (remote, ref.split('/', 2)[2])] = b
764 762 return tracking
765 763
766 764 def _abssource(self, source):
767 765 if '://' not in source:
768 766 # recognize the scp syntax as an absolute source
769 767 colon = source.find(':')
770 768 if colon != -1 and '/' not in source[:colon]:
771 769 return source
772 770 self._subsource = source
773 771 return _abssource(self)
774 772
775 773 def _fetch(self, source, revision):
776 774 if self._gitmissing():
777 775 source = self._abssource(source)
778 776 self._ui.status(_('cloning subrepo %s from %s\n') %
779 777 (self._relpath, source))
780 778 self._gitnodir(['clone', source, self._abspath])
781 779 if self._githavelocally(revision):
782 780 return
783 781 self._ui.status(_('pulling subrepo %s from %s\n') %
784 782 (self._relpath, self._gitremote('origin')))
785 783 # try only origin: the originally cloned repo
786 784 self._gitcommand(['fetch'])
787 785 if not self._githavelocally(revision):
788 786 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
789 787 (revision, self._relpath))
790 788
791 789 def dirty(self, ignoreupdate=False):
792 790 if self._gitmissing():
793 791 return True
794 792 if not ignoreupdate and self._state[1] != self._gitstate():
795 793 # different version checked out
796 794 return True
797 795 # check for staged changes or modified files; ignore untracked files
798 796 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
799 797 return code == 1
800 798
801 799 def get(self, state, overwrite=False):
802 800 source, revision, kind = state
803 801 self._fetch(source, revision)
804 802 # if the repo was set to be bare, unbare it
805 803 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
806 804 self._gitcommand(['config', 'core.bare', 'false'])
807 805 if self._gitstate() == revision:
808 806 self._gitcommand(['reset', '--hard', 'HEAD'])
809 807 return
810 808 elif self._gitstate() == revision:
811 809 if overwrite:
812 810 # first reset the index to unmark new files for commit, because
813 811 # reset --hard will otherwise throw away files added for commit,
814 812 # not just unmark them.
815 813 self._gitcommand(['reset', 'HEAD'])
816 814 self._gitcommand(['reset', '--hard', 'HEAD'])
817 815 return
818 816 branch2rev, rev2branch = self._gitbranchmap()
819 817
820 818 def checkout(args):
821 819 cmd = ['checkout']
822 820 if overwrite:
823 821 # first reset the index to unmark new files for commit, because
824 822 # the -f option will otherwise throw away files added for
825 823 # commit, not just unmark them.
826 824 self._gitcommand(['reset', 'HEAD'])
827 825 cmd.append('-f')
828 826 self._gitcommand(cmd + args)
829 827
830 828 def rawcheckout():
831 829 # no branch to checkout, check it out with no branch
832 830 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
833 831 self._relpath)
834 832 self._ui.warn(_('check out a git branch if you intend '
835 833 'to make changes\n'))
836 834 checkout(['-q', revision])
837 835
838 836 if revision not in rev2branch:
839 837 rawcheckout()
840 838 return
841 839 branches = rev2branch[revision]
842 840 firstlocalbranch = None
843 841 for b in branches:
844 842 if b == 'refs/heads/master':
845 843 # master trumps all other branches
846 844 checkout(['refs/heads/master'])
847 845 return
848 846 if not firstlocalbranch and not b.startswith('refs/remotes/'):
849 847 firstlocalbranch = b
850 848 if firstlocalbranch:
851 849 checkout([firstlocalbranch])
852 850 return
853 851
854 852 tracking = self._gittracking(branch2rev.keys())
855 853 # choose a remote branch already tracked if possible
856 854 remote = branches[0]
857 855 if remote not in tracking:
858 856 for b in branches:
859 857 if b in tracking:
860 858 remote = b
861 859 break
862 860
863 861 if remote not in tracking:
864 862 # create a new local tracking branch
865 863 local = remote.split('/', 2)[2]
866 864 checkout(['-b', local, remote])
867 865 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
868 866 # When updating to a tracked remote branch,
869 867 # if the local tracking branch is downstream of it,
870 868 # a normal `git pull` would have performed a "fast-forward merge"
871 869 # which is equivalent to updating the local branch to the remote.
872 870 # Since we are only looking at branching at update, we need to
873 871 # detect this situation and perform this action lazily.
874 872 if tracking[remote] != self._gitcurrentbranch():
875 873 checkout([tracking[remote]])
876 874 self._gitcommand(['merge', '--ff', remote])
877 875 else:
878 876 # a real merge would be required, just checkout the revision
879 877 rawcheckout()
880 878
881 879 def commit(self, text, user, date):
882 880 if self._gitmissing():
883 881 raise util.Abort(_("subrepo %s is missing") % self._relpath)
884 882 cmd = ['commit', '-a', '-m', text]
885 883 env = os.environ.copy()
886 884 if user:
887 885 cmd += ['--author', user]
888 886 if date:
889 887 # git's date parser silently ignores when seconds < 1e9
890 888 # convert to ISO8601
891 889 env['GIT_AUTHOR_DATE'] = util.datestr(date,
892 890 '%Y-%m-%dT%H:%M:%S %1%2')
893 891 self._gitcommand(cmd, env=env)
894 892 # make sure commit works otherwise HEAD might not exist under certain
895 893 # circumstances
896 894 return self._gitstate()
897 895
898 896 def merge(self, state):
899 897 source, revision, kind = state
900 898 self._fetch(source, revision)
901 899 base = self._gitcommand(['merge-base', revision, self._state[1]])
902 900 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
903 901
904 902 def mergefunc():
905 903 if base == revision:
906 904 self.get(state) # fast forward merge
907 905 elif base != self._state[1]:
908 906 self._gitcommand(['merge', '--no-commit', revision])
909 907
910 908 if self.dirty():
911 909 if self._gitstate() != revision:
912 910 dirty = self._gitstate() == self._state[1] or code != 0
913 911 if _updateprompt(self._ui, self, dirty,
914 912 self._state[1][:7], revision[:7]):
915 913 mergefunc()
916 914 else:
917 915 mergefunc()
918 916
919 917 def push(self, force):
920 918 if self._gitmissing():
921 919 raise util.Abort(_("subrepo %s is missing") % self._relpath)
922 920 # if a branch in origin contains the revision, nothing to do
923 921 branch2rev, rev2branch = self._gitbranchmap()
924 922 if self._state[1] in rev2branch:
925 923 for b in rev2branch[self._state[1]]:
926 924 if b.startswith('refs/remotes/origin/'):
927 925 return True
928 926 for b, revision in branch2rev.iteritems():
929 927 if b.startswith('refs/remotes/origin/'):
930 928 if self._gitisancestor(self._state[1], revision):
931 929 return True
932 930 # otherwise, try to push the currently checked out branch
933 931 cmd = ['push']
934 932 if force:
935 933 cmd.append('--force')
936 934
937 935 current = self._gitcurrentbranch()
938 936 if current:
939 937 # determine if the current branch is even useful
940 938 if not self._gitisancestor(self._state[1], current):
941 939 self._ui.warn(_('unrelated git branch checked out '
942 940 'in subrepo %s\n') % self._relpath)
943 941 return False
944 942 self._ui.status(_('pushing branch %s of subrepo %s\n') %
945 943 (current.split('/', 2)[2], self._relpath))
946 944 self._gitcommand(cmd + ['origin', current])
947 945 return True
948 946 else:
949 947 self._ui.warn(_('no branch checked out in subrepo %s\n'
950 948 'cannot push revision %s') %
951 949 (self._relpath, self._state[1]))
952 950 return False
953 951
954 952 def remove(self):
955 953 if self._gitmissing():
956 954 return
957 955 if self.dirty():
958 956 self._ui.warn(_('not removing repo %s because '
959 957 'it has changes.\n') % self._relpath)
960 958 return
961 959 # we can't fully delete the repository as it may contain
962 960 # local-only history
963 961 self._ui.note(_('removing subrepo %s\n') % self._relpath)
964 962 self._gitcommand(['config', 'core.bare', 'true'])
965 963 for f in os.listdir(self._abspath):
966 964 if f == '.git':
967 965 continue
968 966 path = os.path.join(self._abspath, f)
969 967 if os.path.isdir(path) and not os.path.islink(path):
970 968 shutil.rmtree(path)
971 969 else:
972 970 os.remove(path)
973 971
974 972 def archive(self, ui, archiver, prefix):
975 973 source, revision = self._state
976 974 self._fetch(source, revision)
977 975
978 976 # Parse git's native archive command.
979 977 # This should be much faster than manually traversing the trees
980 978 # and objects with many subprocess calls.
981 979 tarstream = self._gitcommand(['archive', revision], stream=True)
982 980 tar = tarfile.open(fileobj=tarstream, mode='r|')
983 981 relpath = subrelpath(self)
984 982 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
985 983 for i, info in enumerate(tar):
986 984 if info.isdir():
987 985 continue
988 986 if info.issym():
989 987 data = info.linkname
990 988 else:
991 989 data = tar.extractfile(info).read()
992 990 archiver.addfile(os.path.join(prefix, self._path, info.name),
993 991 info.mode, info.issym(), data)
994 992 ui.progress(_('archiving (%s)') % relpath, i + 1,
995 993 unit=_('files'))
996 994 ui.progress(_('archiving (%s)') % relpath, None)
997 995
998 996
999 997 def status(self, rev2, **opts):
1000 998 if self._gitmissing():
1001 999 # if the repo is missing, return no results
1002 1000 return [], [], [], [], [], [], []
1003 1001 rev1 = self._state[1]
1004 1002 modified, added, removed = [], [], []
1005 1003 if rev2:
1006 1004 command = ['diff-tree', rev1, rev2]
1007 1005 else:
1008 1006 command = ['diff-index', rev1]
1009 1007 out = self._gitcommand(command)
1010 1008 for line in out.split('\n'):
1011 1009 tab = line.find('\t')
1012 1010 if tab == -1:
1013 1011 continue
1014 1012 status, f = line[tab - 1], line[tab + 1:]
1015 1013 if status == 'M':
1016 1014 modified.append(f)
1017 1015 elif status == 'A':
1018 1016 added.append(f)
1019 1017 elif status == 'D':
1020 1018 removed.append(f)
1021 1019
1022 1020 deleted = unknown = ignored = clean = []
1023 1021 return modified, added, removed, deleted, unknown, ignored, clean
1024 1022
1025 1023 types = {
1026 1024 'hg': hgsubrepo,
1027 1025 'svn': svnsubrepo,
1028 1026 'git': gitsubrepo,
1029 1027 }
General Comments 0
You need to be logged in to leave comments. Login now