##// END OF EJS Templates
subrepo: improve error message when svn isn't found...
Matt Mackall -
r15190:6dc67dce default
parent child Browse files
Show More
@@ -1,1110 +1,1114 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, posixpath
9 9 import stat, subprocess, tarfile
10 10 from i18n import _
11 11 import config, scmutil, util, node, error, cmdutil, bookmarks
12 12 hg = None
13 13 propertycache = util.propertycache
14 14
15 15 nullstate = ('', '', 'empty')
16 16
17 17 def state(ctx, ui):
18 18 """return a state dict, mapping subrepo paths configured in .hgsub
19 19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 20 (key in types dict))
21 21 """
22 22 p = config.config()
23 23 def read(f, sections=None, remap=None):
24 24 if f in ctx:
25 25 try:
26 26 data = ctx[f].data()
27 27 except IOError, err:
28 28 if err.errno != errno.ENOENT:
29 29 raise
30 30 # handle missing subrepo spec files as removed
31 31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 32 return
33 33 p.parse(f, data, sections, remap, read)
34 34 else:
35 35 raise util.Abort(_("subrepo spec file %s not found") % f)
36 36
37 37 if '.hgsub' in ctx:
38 38 read('.hgsub')
39 39
40 40 for path, src in ui.configitems('subpaths'):
41 41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42 42
43 43 rev = {}
44 44 if '.hgsubstate' in ctx:
45 45 try:
46 46 for l in ctx['.hgsubstate'].data().splitlines():
47 47 revision, path = l.split(" ", 1)
48 48 rev[path] = revision
49 49 except IOError, err:
50 50 if err.errno != errno.ENOENT:
51 51 raise
52 52
53 53 def remap(src):
54 54 for pattern, repl in p.items('subpaths'):
55 55 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
56 56 # does a string decode.
57 57 repl = repl.encode('string-escape')
58 58 # However, we still want to allow back references to go
59 59 # through unharmed, so we turn r'\\1' into r'\1'. Again,
60 60 # extra escapes are needed because re.sub string decodes.
61 61 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
62 62 try:
63 63 src = re.sub(pattern, repl, src, 1)
64 64 except re.error, e:
65 65 raise util.Abort(_("bad subrepository pattern in %s: %s")
66 66 % (p.source('subpaths', pattern), e))
67 67 return src
68 68
69 69 state = {}
70 70 for path, src in p[''].items():
71 71 kind = 'hg'
72 72 if src.startswith('['):
73 73 if ']' not in src:
74 74 raise util.Abort(_('missing ] in subrepo source'))
75 75 kind, src = src.split(']', 1)
76 76 kind = kind[1:]
77 77 src = src.lstrip() # strip any extra whitespace after ']'
78 78
79 79 if not util.url(src).isabs():
80 80 parent = _abssource(ctx._repo, abort=False)
81 81 if parent:
82 82 parent = util.url(parent)
83 83 parent.path = posixpath.join(parent.path or '', src)
84 84 parent.path = posixpath.normpath(parent.path)
85 85 joined = str(parent)
86 86 # Remap the full joined path and use it if it changes,
87 87 # else remap the original source.
88 88 remapped = remap(joined)
89 89 if remapped == joined:
90 90 src = remap(src)
91 91 else:
92 92 src = remapped
93 93
94 94 src = remap(src)
95 95 state[path] = (src.strip(), rev.get(path, ''), kind)
96 96
97 97 return state
98 98
99 99 def writestate(repo, state):
100 100 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
101 101 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
102 102 repo.wwrite('.hgsubstate', ''.join(lines), '')
103 103
104 104 def submerge(repo, wctx, mctx, actx, overwrite):
105 105 """delegated from merge.applyupdates: merging of .hgsubstate file
106 106 in working context, merging context and ancestor context"""
107 107 if mctx == actx: # backwards?
108 108 actx = wctx.p1()
109 109 s1 = wctx.substate
110 110 s2 = mctx.substate
111 111 sa = actx.substate
112 112 sm = {}
113 113
114 114 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
115 115
116 116 def debug(s, msg, r=""):
117 117 if r:
118 118 r = "%s:%s:%s" % r
119 119 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
120 120
121 121 for s, l in s1.items():
122 122 a = sa.get(s, nullstate)
123 123 ld = l # local state with possible dirty flag for compares
124 124 if wctx.sub(s).dirty():
125 125 ld = (l[0], l[1] + "+")
126 126 if wctx == actx: # overwrite
127 127 a = ld
128 128
129 129 if s in s2:
130 130 r = s2[s]
131 131 if ld == r or r == a: # no change or local is newer
132 132 sm[s] = l
133 133 continue
134 134 elif ld == a: # other side changed
135 135 debug(s, "other changed, get", r)
136 136 wctx.sub(s).get(r, overwrite)
137 137 sm[s] = r
138 138 elif ld[0] != r[0]: # sources differ
139 139 if repo.ui.promptchoice(
140 140 _(' subrepository sources for %s differ\n'
141 141 'use (l)ocal source (%s) or (r)emote source (%s)?')
142 142 % (s, l[0], r[0]),
143 143 (_('&Local'), _('&Remote')), 0):
144 144 debug(s, "prompt changed, get", r)
145 145 wctx.sub(s).get(r, overwrite)
146 146 sm[s] = r
147 147 elif ld[1] == a[1]: # local side is unchanged
148 148 debug(s, "other side changed, get", r)
149 149 wctx.sub(s).get(r, overwrite)
150 150 sm[s] = r
151 151 else:
152 152 debug(s, "both sides changed, merge with", r)
153 153 wctx.sub(s).merge(r)
154 154 sm[s] = l
155 155 elif ld == a: # remote removed, local unchanged
156 156 debug(s, "remote removed, remove")
157 157 wctx.sub(s).remove()
158 158 elif a == nullstate: # not present in remote or ancestor
159 159 debug(s, "local added, keep")
160 160 sm[s] = l
161 161 continue
162 162 else:
163 163 if repo.ui.promptchoice(
164 164 _(' local changed subrepository %s which remote removed\n'
165 165 'use (c)hanged version or (d)elete?') % s,
166 166 (_('&Changed'), _('&Delete')), 0):
167 167 debug(s, "prompt remove")
168 168 wctx.sub(s).remove()
169 169
170 170 for s, r in sorted(s2.items()):
171 171 if s in s1:
172 172 continue
173 173 elif s not in sa:
174 174 debug(s, "remote added, get", r)
175 175 mctx.sub(s).get(r)
176 176 sm[s] = r
177 177 elif r != sa[s]:
178 178 if repo.ui.promptchoice(
179 179 _(' remote changed subrepository %s which local removed\n'
180 180 'use (c)hanged version or (d)elete?') % s,
181 181 (_('&Changed'), _('&Delete')), 0) == 0:
182 182 debug(s, "prompt recreate", r)
183 183 wctx.sub(s).get(r)
184 184 sm[s] = r
185 185
186 186 # record merged .hgsubstate
187 187 writestate(repo, sm)
188 188
189 189 def _updateprompt(ui, sub, dirty, local, remote):
190 190 if dirty:
191 191 msg = (_(' subrepository sources for %s differ\n'
192 192 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
193 193 % (subrelpath(sub), local, remote))
194 194 else:
195 195 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
196 196 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
197 197 % (subrelpath(sub), local, remote))
198 198 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
199 199
200 200 def reporelpath(repo):
201 201 """return path to this (sub)repo as seen from outermost repo"""
202 202 parent = repo
203 203 while util.safehasattr(parent, '_subparent'):
204 204 parent = parent._subparent
205 205 return repo.root[len(parent.root)+1:]
206 206
207 207 def subrelpath(sub):
208 208 """return path to this subrepo as seen from outermost repo"""
209 209 if util.safehasattr(sub, '_relpath'):
210 210 return sub._relpath
211 211 if not util.safehasattr(sub, '_repo'):
212 212 return sub._path
213 213 return reporelpath(sub._repo)
214 214
215 215 def _abssource(repo, push=False, abort=True):
216 216 """return pull/push path of repo - either based on parent repo .hgsub info
217 217 or on the top repo config. Abort or return None if no source found."""
218 218 if util.safehasattr(repo, '_subparent'):
219 219 source = util.url(repo._subsource)
220 220 if source.isabs():
221 221 return str(source)
222 222 source.path = posixpath.normpath(source.path)
223 223 parent = _abssource(repo._subparent, push, abort=False)
224 224 if parent:
225 225 parent = util.url(parent)
226 226 parent.path = posixpath.join(parent.path or '', source.path)
227 227 parent.path = posixpath.normpath(parent.path)
228 228 return str(parent)
229 229 else: # recursion reached top repo
230 230 if util.safehasattr(repo, '_subtoppath'):
231 231 return repo._subtoppath
232 232 if push and repo.ui.config('paths', 'default-push'):
233 233 return repo.ui.config('paths', 'default-push')
234 234 if repo.ui.config('paths', 'default'):
235 235 return repo.ui.config('paths', 'default')
236 236 if abort:
237 237 raise util.Abort(_("default path for subrepository %s not found") %
238 238 reporelpath(repo))
239 239
240 240 def itersubrepos(ctx1, ctx2):
241 241 """find subrepos in ctx1 or ctx2"""
242 242 # Create a (subpath, ctx) mapping where we prefer subpaths from
243 243 # ctx1. The subpaths from ctx2 are important when the .hgsub file
244 244 # has been modified (in ctx2) but not yet committed (in ctx1).
245 245 subpaths = dict.fromkeys(ctx2.substate, ctx2)
246 246 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
247 247 for subpath, ctx in sorted(subpaths.iteritems()):
248 248 yield subpath, ctx.sub(subpath)
249 249
250 250 def subrepo(ctx, path):
251 251 """return instance of the right subrepo class for subrepo in path"""
252 252 # subrepo inherently violates our import layering rules
253 253 # because it wants to make repo objects from deep inside the stack
254 254 # so we manually delay the circular imports to not break
255 255 # scripts that don't use our demand-loading
256 256 global hg
257 257 import hg as h
258 258 hg = h
259 259
260 260 scmutil.pathauditor(ctx._repo.root)(path)
261 261 state = ctx.substate.get(path, nullstate)
262 262 if state[2] not in types:
263 263 raise util.Abort(_('unknown subrepo type %s') % state[2])
264 264 return types[state[2]](ctx, path, state[:2])
265 265
266 266 # subrepo classes need to implement the following abstract class:
267 267
268 268 class abstractsubrepo(object):
269 269
270 270 def dirty(self, ignoreupdate=False):
271 271 """returns true if the dirstate of the subrepo is dirty or does not
272 272 match current stored state. If ignoreupdate is true, only check
273 273 whether the subrepo has uncommitted changes in its dirstate.
274 274 """
275 275 raise NotImplementedError
276 276
277 277 def checknested(self, path):
278 278 """check if path is a subrepository within this repository"""
279 279 return False
280 280
281 281 def commit(self, text, user, date):
282 282 """commit the current changes to the subrepo with the given
283 283 log message. Use given user and date if possible. Return the
284 284 new state of the subrepo.
285 285 """
286 286 raise NotImplementedError
287 287
288 288 def remove(self):
289 289 """remove the subrepo
290 290
291 291 (should verify the dirstate is not dirty first)
292 292 """
293 293 raise NotImplementedError
294 294
295 295 def get(self, state, overwrite=False):
296 296 """run whatever commands are needed to put the subrepo into
297 297 this state
298 298 """
299 299 raise NotImplementedError
300 300
301 301 def merge(self, state):
302 302 """merge currently-saved state with the new state."""
303 303 raise NotImplementedError
304 304
305 305 def push(self, force):
306 306 """perform whatever action is analogous to 'hg push'
307 307
308 308 This may be a no-op on some systems.
309 309 """
310 310 raise NotImplementedError
311 311
312 312 def add(self, ui, match, dryrun, prefix):
313 313 return []
314 314
315 315 def status(self, rev2, **opts):
316 316 return [], [], [], [], [], [], []
317 317
318 318 def diff(self, diffopts, node2, match, prefix, **opts):
319 319 pass
320 320
321 321 def outgoing(self, ui, dest, opts):
322 322 return 1
323 323
324 324 def incoming(self, ui, source, opts):
325 325 return 1
326 326
327 327 def files(self):
328 328 """return filename iterator"""
329 329 raise NotImplementedError
330 330
331 331 def filedata(self, name):
332 332 """return file data"""
333 333 raise NotImplementedError
334 334
335 335 def fileflags(self, name):
336 336 """return file flags"""
337 337 return ''
338 338
339 339 def archive(self, ui, archiver, prefix):
340 340 files = self.files()
341 341 total = len(files)
342 342 relpath = subrelpath(self)
343 343 ui.progress(_('archiving (%s)') % relpath, 0,
344 344 unit=_('files'), total=total)
345 345 for i, name in enumerate(files):
346 346 flags = self.fileflags(name)
347 347 mode = 'x' in flags and 0755 or 0644
348 348 symlink = 'l' in flags
349 349 archiver.addfile(os.path.join(prefix, self._path, name),
350 350 mode, symlink, self.filedata(name))
351 351 ui.progress(_('archiving (%s)') % relpath, i + 1,
352 352 unit=_('files'), total=total)
353 353 ui.progress(_('archiving (%s)') % relpath, None)
354 354
355 355
356 356 class hgsubrepo(abstractsubrepo):
357 357 def __init__(self, ctx, path, state):
358 358 self._path = path
359 359 self._state = state
360 360 r = ctx._repo
361 361 root = r.wjoin(path)
362 362 create = False
363 363 if not os.path.exists(os.path.join(root, '.hg')):
364 364 create = True
365 365 util.makedirs(root)
366 366 self._repo = hg.repository(r.ui, root, create=create)
367 367 self._initrepo(r, state[0], create)
368 368
369 369 def _initrepo(self, parentrepo, source, create):
370 370 self._repo._subparent = parentrepo
371 371 self._repo._subsource = source
372 372
373 373 if create:
374 374 fp = self._repo.opener("hgrc", "w", text=True)
375 375 fp.write('[paths]\n')
376 376
377 377 def addpathconfig(key, value):
378 378 if value:
379 379 fp.write('%s = %s\n' % (key, value))
380 380 self._repo.ui.setconfig('paths', key, value)
381 381
382 382 defpath = _abssource(self._repo, abort=False)
383 383 defpushpath = _abssource(self._repo, True, abort=False)
384 384 addpathconfig('default', defpath)
385 385 if defpath != defpushpath:
386 386 addpathconfig('default-push', defpushpath)
387 387 fp.close()
388 388
389 389 def add(self, ui, match, dryrun, prefix):
390 390 return cmdutil.add(ui, self._repo, match, dryrun, True,
391 391 os.path.join(prefix, self._path))
392 392
393 393 def status(self, rev2, **opts):
394 394 try:
395 395 rev1 = self._state[1]
396 396 ctx1 = self._repo[rev1]
397 397 ctx2 = self._repo[rev2]
398 398 return self._repo.status(ctx1, ctx2, **opts)
399 399 except error.RepoLookupError, inst:
400 400 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
401 401 % (inst, subrelpath(self)))
402 402 return [], [], [], [], [], [], []
403 403
404 404 def diff(self, diffopts, node2, match, prefix, **opts):
405 405 try:
406 406 node1 = node.bin(self._state[1])
407 407 # We currently expect node2 to come from substate and be
408 408 # in hex format
409 409 if node2 is not None:
410 410 node2 = node.bin(node2)
411 411 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
412 412 node1, node2, match,
413 413 prefix=os.path.join(prefix, self._path),
414 414 listsubrepos=True, **opts)
415 415 except error.RepoLookupError, inst:
416 416 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
417 417 % (inst, subrelpath(self)))
418 418
419 419 def archive(self, ui, archiver, prefix):
420 420 abstractsubrepo.archive(self, ui, archiver, prefix)
421 421
422 422 rev = self._state[1]
423 423 ctx = self._repo[rev]
424 424 for subpath in ctx.substate:
425 425 s = subrepo(ctx, subpath)
426 426 s.archive(ui, archiver, os.path.join(prefix, self._path))
427 427
428 428 def dirty(self, ignoreupdate=False):
429 429 r = self._state[1]
430 430 if r == '' and not ignoreupdate: # no state recorded
431 431 return True
432 432 w = self._repo[None]
433 433 if r != w.p1().hex() and not ignoreupdate:
434 434 # different version checked out
435 435 return True
436 436 return w.dirty() # working directory changed
437 437
438 438 def checknested(self, path):
439 439 return self._repo._checknested(self._repo.wjoin(path))
440 440
441 441 def commit(self, text, user, date):
442 442 # don't bother committing in the subrepo if it's only been
443 443 # updated
444 444 if not self.dirty(True):
445 445 return self._repo['.'].hex()
446 446 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
447 447 n = self._repo.commit(text, user, date)
448 448 if not n:
449 449 return self._repo['.'].hex() # different version checked out
450 450 return node.hex(n)
451 451
452 452 def remove(self):
453 453 # we can't fully delete the repository as it may contain
454 454 # local-only history
455 455 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
456 456 hg.clean(self._repo, node.nullid, False)
457 457
458 458 def _get(self, state):
459 459 source, revision, kind = state
460 460 if revision not in self._repo:
461 461 self._repo._subsource = source
462 462 srcurl = _abssource(self._repo)
463 463 other = hg.peer(self._repo.ui, {}, srcurl)
464 464 if len(self._repo) == 0:
465 465 self._repo.ui.status(_('cloning subrepo %s from %s\n')
466 466 % (subrelpath(self), srcurl))
467 467 parentrepo = self._repo._subparent
468 468 shutil.rmtree(self._repo.root)
469 469 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
470 470 self._repo.root, update=False)
471 471 self._initrepo(parentrepo, source, create=True)
472 472 else:
473 473 self._repo.ui.status(_('pulling subrepo %s from %s\n')
474 474 % (subrelpath(self), srcurl))
475 475 self._repo.pull(other)
476 476 bookmarks.updatefromremote(self._repo.ui, self._repo, other)
477 477
478 478 def get(self, state, overwrite=False):
479 479 self._get(state)
480 480 source, revision, kind = state
481 481 self._repo.ui.debug("getting subrepo %s\n" % self._path)
482 482 hg.clean(self._repo, revision, False)
483 483
484 484 def merge(self, state):
485 485 self._get(state)
486 486 cur = self._repo['.']
487 487 dst = self._repo[state[1]]
488 488 anc = dst.ancestor(cur)
489 489
490 490 def mergefunc():
491 491 if anc == cur:
492 492 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
493 493 hg.update(self._repo, state[1])
494 494 elif anc == dst:
495 495 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
496 496 else:
497 497 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
498 498 hg.merge(self._repo, state[1], remind=False)
499 499
500 500 wctx = self._repo[None]
501 501 if self.dirty():
502 502 if anc != dst:
503 503 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
504 504 mergefunc()
505 505 else:
506 506 mergefunc()
507 507 else:
508 508 mergefunc()
509 509
510 510 def push(self, force):
511 511 # push subrepos depth-first for coherent ordering
512 512 c = self._repo['']
513 513 subs = c.substate # only repos that are committed
514 514 for s in sorted(subs):
515 515 if not c.sub(s).push(force):
516 516 return False
517 517
518 518 dsturl = _abssource(self._repo, True)
519 519 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
520 520 (subrelpath(self), dsturl))
521 521 other = hg.peer(self._repo.ui, {}, dsturl)
522 522 return self._repo.push(other, force)
523 523
524 524 def outgoing(self, ui, dest, opts):
525 525 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
526 526
527 527 def incoming(self, ui, source, opts):
528 528 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
529 529
530 530 def files(self):
531 531 rev = self._state[1]
532 532 ctx = self._repo[rev]
533 533 return ctx.manifest()
534 534
535 535 def filedata(self, name):
536 536 rev = self._state[1]
537 537 return self._repo[rev][name].data()
538 538
539 539 def fileflags(self, name):
540 540 rev = self._state[1]
541 541 ctx = self._repo[rev]
542 542 return ctx.flags(name)
543 543
544 544
545 545 class svnsubrepo(abstractsubrepo):
546 546 def __init__(self, ctx, path, state):
547 547 self._path = path
548 548 self._state = state
549 549 self._ctx = ctx
550 550 self._ui = ctx._repo.ui
551 self._exe = util.findexe('svn')
552 if not self._exe:
553 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
554 % self._path)
551 555
552 556 def _svncommand(self, commands, filename='', failok=False):
553 cmd = ['svn']
557 cmd = [self._exe]
554 558 extrakw = {}
555 559 if not self._ui.interactive():
556 560 # Making stdin be a pipe should prevent svn from behaving
557 561 # interactively even if we can't pass --non-interactive.
558 562 extrakw['stdin'] = subprocess.PIPE
559 563 # Starting in svn 1.5 --non-interactive is a global flag
560 564 # instead of being per-command, but we need to support 1.4 so
561 565 # we have to be intelligent about what commands take
562 566 # --non-interactive.
563 567 if commands[0] in ('update', 'checkout', 'commit'):
564 568 cmd.append('--non-interactive')
565 569 cmd.extend(commands)
566 570 if filename is not None:
567 571 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
568 572 cmd.append(path)
569 573 env = dict(os.environ)
570 574 # Avoid localized output, preserve current locale for everything else.
571 575 env['LC_MESSAGES'] = 'C'
572 576 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
573 577 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
574 578 universal_newlines=True, env=env, **extrakw)
575 579 stdout, stderr = p.communicate()
576 580 stderr = stderr.strip()
577 581 if not failok:
578 582 if p.returncode:
579 583 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
580 584 if stderr:
581 585 self._ui.warn(stderr + '\n')
582 586 return stdout, stderr
583 587
584 588 @propertycache
585 589 def _svnversion(self):
586 590 output, err = self._svncommand(['--version'], filename=None)
587 591 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
588 592 if not m:
589 593 raise util.Abort(_('cannot retrieve svn tool version'))
590 594 return (int(m.group(1)), int(m.group(2)))
591 595
592 596 def _wcrevs(self):
593 597 # Get the working directory revision as well as the last
594 598 # commit revision so we can compare the subrepo state with
595 599 # both. We used to store the working directory one.
596 600 output, err = self._svncommand(['info', '--xml'])
597 601 doc = xml.dom.minidom.parseString(output)
598 602 entries = doc.getElementsByTagName('entry')
599 603 lastrev, rev = '0', '0'
600 604 if entries:
601 605 rev = str(entries[0].getAttribute('revision')) or '0'
602 606 commits = entries[0].getElementsByTagName('commit')
603 607 if commits:
604 608 lastrev = str(commits[0].getAttribute('revision')) or '0'
605 609 return (lastrev, rev)
606 610
607 611 def _wcrev(self):
608 612 return self._wcrevs()[0]
609 613
610 614 def _wcchanged(self):
611 615 """Return (changes, extchanges) where changes is True
612 616 if the working directory was changed, and extchanges is
613 617 True if any of these changes concern an external entry.
614 618 """
615 619 output, err = self._svncommand(['status', '--xml'])
616 620 externals, changes = [], []
617 621 doc = xml.dom.minidom.parseString(output)
618 622 for e in doc.getElementsByTagName('entry'):
619 623 s = e.getElementsByTagName('wc-status')
620 624 if not s:
621 625 continue
622 626 item = s[0].getAttribute('item')
623 627 props = s[0].getAttribute('props')
624 628 path = e.getAttribute('path')
625 629 if item == 'external':
626 630 externals.append(path)
627 631 if (item not in ('', 'normal', 'unversioned', 'external')
628 632 or props not in ('', 'none', 'normal')):
629 633 changes.append(path)
630 634 for path in changes:
631 635 for ext in externals:
632 636 if path == ext or path.startswith(ext + os.sep):
633 637 return True, True
634 638 return bool(changes), False
635 639
636 640 def dirty(self, ignoreupdate=False):
637 641 if not self._wcchanged()[0]:
638 642 if self._state[1] in self._wcrevs() or ignoreupdate:
639 643 return False
640 644 return True
641 645
642 646 def commit(self, text, user, date):
643 647 # user and date are out of our hands since svn is centralized
644 648 changed, extchanged = self._wcchanged()
645 649 if not changed:
646 650 return self._wcrev()
647 651 if extchanged:
648 652 # Do not try to commit externals
649 653 raise util.Abort(_('cannot commit svn externals'))
650 654 commitinfo, err = self._svncommand(['commit', '-m', text])
651 655 self._ui.status(commitinfo)
652 656 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
653 657 if not newrev:
654 658 raise util.Abort(commitinfo.splitlines()[-1])
655 659 newrev = newrev.groups()[0]
656 660 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
657 661 return newrev
658 662
659 663 def remove(self):
660 664 if self.dirty():
661 665 self._ui.warn(_('not removing repo %s because '
662 666 'it has changes.\n' % self._path))
663 667 return
664 668 self._ui.note(_('removing subrepo %s\n') % self._path)
665 669
666 670 def onerror(function, path, excinfo):
667 671 if function is not os.remove:
668 672 raise
669 673 # read-only files cannot be unlinked under Windows
670 674 s = os.stat(path)
671 675 if (s.st_mode & stat.S_IWRITE) != 0:
672 676 raise
673 677 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
674 678 os.remove(path)
675 679
676 680 path = self._ctx._repo.wjoin(self._path)
677 681 shutil.rmtree(path, onerror=onerror)
678 682 try:
679 683 os.removedirs(os.path.dirname(path))
680 684 except OSError:
681 685 pass
682 686
683 687 def get(self, state, overwrite=False):
684 688 if overwrite:
685 689 self._svncommand(['revert', '--recursive'])
686 690 args = ['checkout']
687 691 if self._svnversion >= (1, 5):
688 692 args.append('--force')
689 693 # The revision must be specified at the end of the URL to properly
690 694 # update to a directory which has since been deleted and recreated.
691 695 args.append('%s@%s' % (state[0], state[1]))
692 696 status, err = self._svncommand(args, failok=True)
693 697 if not re.search('Checked out revision [0-9]+.', status):
694 698 if ('is already a working copy for a different URL' in err
695 699 and (self._wcchanged() == (False, False))):
696 700 # obstructed but clean working copy, so just blow it away.
697 701 self.remove()
698 702 self.get(state, overwrite=False)
699 703 return
700 704 raise util.Abort((status or err).splitlines()[-1])
701 705 self._ui.status(status)
702 706
703 707 def merge(self, state):
704 708 old = self._state[1]
705 709 new = state[1]
706 710 if new != self._wcrev():
707 711 dirty = old == self._wcrev() or self._wcchanged()[0]
708 712 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
709 713 self.get(state, False)
710 714
711 715 def push(self, force):
712 716 # push is a no-op for SVN
713 717 return True
714 718
715 719 def files(self):
716 720 output = self._svncommand(['list'])
717 721 # This works because svn forbids \n in filenames.
718 722 return output.splitlines()
719 723
720 724 def filedata(self, name):
721 725 return self._svncommand(['cat'], name)
722 726
723 727
724 728 class gitsubrepo(abstractsubrepo):
725 729 def __init__(self, ctx, path, state):
726 730 # TODO add git version check.
727 731 self._state = state
728 732 self._ctx = ctx
729 733 self._path = path
730 734 self._relpath = os.path.join(reporelpath(ctx._repo), path)
731 735 self._abspath = ctx._repo.wjoin(path)
732 736 self._subparent = ctx._repo
733 737 self._ui = ctx._repo.ui
734 738
735 739 def _gitcommand(self, commands, env=None, stream=False):
736 740 return self._gitdir(commands, env=env, stream=stream)[0]
737 741
738 742 def _gitdir(self, commands, env=None, stream=False):
739 743 return self._gitnodir(commands, env=env, stream=stream,
740 744 cwd=self._abspath)
741 745
742 746 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
743 747 """Calls the git command
744 748
745 749 The methods tries to call the git command. versions previor to 1.6.0
746 750 are not supported and very probably fail.
747 751 """
748 752 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
749 753 # unless ui.quiet is set, print git's stderr,
750 754 # which is mostly progress and useful info
751 755 errpipe = None
752 756 if self._ui.quiet:
753 757 errpipe = open(os.devnull, 'w')
754 758 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
755 759 close_fds=util.closefds,
756 760 stdout=subprocess.PIPE, stderr=errpipe)
757 761 if stream:
758 762 return p.stdout, None
759 763
760 764 retdata = p.stdout.read().strip()
761 765 # wait for the child to exit to avoid race condition.
762 766 p.wait()
763 767
764 768 if p.returncode != 0 and p.returncode != 1:
765 769 # there are certain error codes that are ok
766 770 command = commands[0]
767 771 if command in ('cat-file', 'symbolic-ref'):
768 772 return retdata, p.returncode
769 773 # for all others, abort
770 774 raise util.Abort('git %s error %d in %s' %
771 775 (command, p.returncode, self._relpath))
772 776
773 777 return retdata, p.returncode
774 778
775 779 def _gitmissing(self):
776 780 return not os.path.exists(os.path.join(self._abspath, '.git'))
777 781
778 782 def _gitstate(self):
779 783 return self._gitcommand(['rev-parse', 'HEAD'])
780 784
781 785 def _gitcurrentbranch(self):
782 786 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
783 787 if err:
784 788 current = None
785 789 return current
786 790
787 791 def _gitremote(self, remote):
788 792 out = self._gitcommand(['remote', 'show', '-n', remote])
789 793 line = out.split('\n')[1]
790 794 i = line.index('URL: ') + len('URL: ')
791 795 return line[i:]
792 796
793 797 def _githavelocally(self, revision):
794 798 out, code = self._gitdir(['cat-file', '-e', revision])
795 799 return code == 0
796 800
797 801 def _gitisancestor(self, r1, r2):
798 802 base = self._gitcommand(['merge-base', r1, r2])
799 803 return base == r1
800 804
801 805 def _gitisbare(self):
802 806 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
803 807
804 808 def _gitbranchmap(self):
805 809 '''returns 2 things:
806 810 a map from git branch to revision
807 811 a map from revision to branches'''
808 812 branch2rev = {}
809 813 rev2branch = {}
810 814
811 815 out = self._gitcommand(['for-each-ref', '--format',
812 816 '%(objectname) %(refname)'])
813 817 for line in out.split('\n'):
814 818 revision, ref = line.split(' ')
815 819 if (not ref.startswith('refs/heads/') and
816 820 not ref.startswith('refs/remotes/')):
817 821 continue
818 822 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
819 823 continue # ignore remote/HEAD redirects
820 824 branch2rev[ref] = revision
821 825 rev2branch.setdefault(revision, []).append(ref)
822 826 return branch2rev, rev2branch
823 827
824 828 def _gittracking(self, branches):
825 829 'return map of remote branch to local tracking branch'
826 830 # assumes no more than one local tracking branch for each remote
827 831 tracking = {}
828 832 for b in branches:
829 833 if b.startswith('refs/remotes/'):
830 834 continue
831 835 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
832 836 if remote:
833 837 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
834 838 tracking['refs/remotes/%s/%s' %
835 839 (remote, ref.split('/', 2)[2])] = b
836 840 return tracking
837 841
838 842 def _abssource(self, source):
839 843 if '://' not in source:
840 844 # recognize the scp syntax as an absolute source
841 845 colon = source.find(':')
842 846 if colon != -1 and '/' not in source[:colon]:
843 847 return source
844 848 self._subsource = source
845 849 return _abssource(self)
846 850
847 851 def _fetch(self, source, revision):
848 852 if self._gitmissing():
849 853 source = self._abssource(source)
850 854 self._ui.status(_('cloning subrepo %s from %s\n') %
851 855 (self._relpath, source))
852 856 self._gitnodir(['clone', source, self._abspath])
853 857 if self._githavelocally(revision):
854 858 return
855 859 self._ui.status(_('pulling subrepo %s from %s\n') %
856 860 (self._relpath, self._gitremote('origin')))
857 861 # try only origin: the originally cloned repo
858 862 self._gitcommand(['fetch'])
859 863 if not self._githavelocally(revision):
860 864 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
861 865 (revision, self._relpath))
862 866
863 867 def dirty(self, ignoreupdate=False):
864 868 if self._gitmissing():
865 869 return self._state[1] != ''
866 870 if self._gitisbare():
867 871 return True
868 872 if not ignoreupdate and self._state[1] != self._gitstate():
869 873 # different version checked out
870 874 return True
871 875 # check for staged changes or modified files; ignore untracked files
872 876 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
873 877 return code == 1
874 878
875 879 def get(self, state, overwrite=False):
876 880 source, revision, kind = state
877 881 if not revision:
878 882 self.remove()
879 883 return
880 884 self._fetch(source, revision)
881 885 # if the repo was set to be bare, unbare it
882 886 if self._gitisbare():
883 887 self._gitcommand(['config', 'core.bare', 'false'])
884 888 if self._gitstate() == revision:
885 889 self._gitcommand(['reset', '--hard', 'HEAD'])
886 890 return
887 891 elif self._gitstate() == revision:
888 892 if overwrite:
889 893 # first reset the index to unmark new files for commit, because
890 894 # reset --hard will otherwise throw away files added for commit,
891 895 # not just unmark them.
892 896 self._gitcommand(['reset', 'HEAD'])
893 897 self._gitcommand(['reset', '--hard', 'HEAD'])
894 898 return
895 899 branch2rev, rev2branch = self._gitbranchmap()
896 900
897 901 def checkout(args):
898 902 cmd = ['checkout']
899 903 if overwrite:
900 904 # first reset the index to unmark new files for commit, because
901 905 # the -f option will otherwise throw away files added for
902 906 # commit, not just unmark them.
903 907 self._gitcommand(['reset', 'HEAD'])
904 908 cmd.append('-f')
905 909 self._gitcommand(cmd + args)
906 910
907 911 def rawcheckout():
908 912 # no branch to checkout, check it out with no branch
909 913 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
910 914 self._relpath)
911 915 self._ui.warn(_('check out a git branch if you intend '
912 916 'to make changes\n'))
913 917 checkout(['-q', revision])
914 918
915 919 if revision not in rev2branch:
916 920 rawcheckout()
917 921 return
918 922 branches = rev2branch[revision]
919 923 firstlocalbranch = None
920 924 for b in branches:
921 925 if b == 'refs/heads/master':
922 926 # master trumps all other branches
923 927 checkout(['refs/heads/master'])
924 928 return
925 929 if not firstlocalbranch and not b.startswith('refs/remotes/'):
926 930 firstlocalbranch = b
927 931 if firstlocalbranch:
928 932 checkout([firstlocalbranch])
929 933 return
930 934
931 935 tracking = self._gittracking(branch2rev.keys())
932 936 # choose a remote branch already tracked if possible
933 937 remote = branches[0]
934 938 if remote not in tracking:
935 939 for b in branches:
936 940 if b in tracking:
937 941 remote = b
938 942 break
939 943
940 944 if remote not in tracking:
941 945 # create a new local tracking branch
942 946 local = remote.split('/', 2)[2]
943 947 checkout(['-b', local, remote])
944 948 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
945 949 # When updating to a tracked remote branch,
946 950 # if the local tracking branch is downstream of it,
947 951 # a normal `git pull` would have performed a "fast-forward merge"
948 952 # which is equivalent to updating the local branch to the remote.
949 953 # Since we are only looking at branching at update, we need to
950 954 # detect this situation and perform this action lazily.
951 955 if tracking[remote] != self._gitcurrentbranch():
952 956 checkout([tracking[remote]])
953 957 self._gitcommand(['merge', '--ff', remote])
954 958 else:
955 959 # a real merge would be required, just checkout the revision
956 960 rawcheckout()
957 961
958 962 def commit(self, text, user, date):
959 963 if self._gitmissing():
960 964 raise util.Abort(_("subrepo %s is missing") % self._relpath)
961 965 cmd = ['commit', '-a', '-m', text]
962 966 env = os.environ.copy()
963 967 if user:
964 968 cmd += ['--author', user]
965 969 if date:
966 970 # git's date parser silently ignores when seconds < 1e9
967 971 # convert to ISO8601
968 972 env['GIT_AUTHOR_DATE'] = util.datestr(date,
969 973 '%Y-%m-%dT%H:%M:%S %1%2')
970 974 self._gitcommand(cmd, env=env)
971 975 # make sure commit works otherwise HEAD might not exist under certain
972 976 # circumstances
973 977 return self._gitstate()
974 978
975 979 def merge(self, state):
976 980 source, revision, kind = state
977 981 self._fetch(source, revision)
978 982 base = self._gitcommand(['merge-base', revision, self._state[1]])
979 983 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
980 984
981 985 def mergefunc():
982 986 if base == revision:
983 987 self.get(state) # fast forward merge
984 988 elif base != self._state[1]:
985 989 self._gitcommand(['merge', '--no-commit', revision])
986 990
987 991 if self.dirty():
988 992 if self._gitstate() != revision:
989 993 dirty = self._gitstate() == self._state[1] or code != 0
990 994 if _updateprompt(self._ui, self, dirty,
991 995 self._state[1][:7], revision[:7]):
992 996 mergefunc()
993 997 else:
994 998 mergefunc()
995 999
996 1000 def push(self, force):
997 1001 if not self._state[1]:
998 1002 return True
999 1003 if self._gitmissing():
1000 1004 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1001 1005 # if a branch in origin contains the revision, nothing to do
1002 1006 branch2rev, rev2branch = self._gitbranchmap()
1003 1007 if self._state[1] in rev2branch:
1004 1008 for b in rev2branch[self._state[1]]:
1005 1009 if b.startswith('refs/remotes/origin/'):
1006 1010 return True
1007 1011 for b, revision in branch2rev.iteritems():
1008 1012 if b.startswith('refs/remotes/origin/'):
1009 1013 if self._gitisancestor(self._state[1], revision):
1010 1014 return True
1011 1015 # otherwise, try to push the currently checked out branch
1012 1016 cmd = ['push']
1013 1017 if force:
1014 1018 cmd.append('--force')
1015 1019
1016 1020 current = self._gitcurrentbranch()
1017 1021 if current:
1018 1022 # determine if the current branch is even useful
1019 1023 if not self._gitisancestor(self._state[1], current):
1020 1024 self._ui.warn(_('unrelated git branch checked out '
1021 1025 'in subrepo %s\n') % self._relpath)
1022 1026 return False
1023 1027 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1024 1028 (current.split('/', 2)[2], self._relpath))
1025 1029 self._gitcommand(cmd + ['origin', current])
1026 1030 return True
1027 1031 else:
1028 1032 self._ui.warn(_('no branch checked out in subrepo %s\n'
1029 1033 'cannot push revision %s') %
1030 1034 (self._relpath, self._state[1]))
1031 1035 return False
1032 1036
1033 1037 def remove(self):
1034 1038 if self._gitmissing():
1035 1039 return
1036 1040 if self.dirty():
1037 1041 self._ui.warn(_('not removing repo %s because '
1038 1042 'it has changes.\n') % self._relpath)
1039 1043 return
1040 1044 # we can't fully delete the repository as it may contain
1041 1045 # local-only history
1042 1046 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1043 1047 self._gitcommand(['config', 'core.bare', 'true'])
1044 1048 for f in os.listdir(self._abspath):
1045 1049 if f == '.git':
1046 1050 continue
1047 1051 path = os.path.join(self._abspath, f)
1048 1052 if os.path.isdir(path) and not os.path.islink(path):
1049 1053 shutil.rmtree(path)
1050 1054 else:
1051 1055 os.remove(path)
1052 1056
1053 1057 def archive(self, ui, archiver, prefix):
1054 1058 source, revision = self._state
1055 1059 if not revision:
1056 1060 return
1057 1061 self._fetch(source, revision)
1058 1062
1059 1063 # Parse git's native archive command.
1060 1064 # This should be much faster than manually traversing the trees
1061 1065 # and objects with many subprocess calls.
1062 1066 tarstream = self._gitcommand(['archive', revision], stream=True)
1063 1067 tar = tarfile.open(fileobj=tarstream, mode='r|')
1064 1068 relpath = subrelpath(self)
1065 1069 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1066 1070 for i, info in enumerate(tar):
1067 1071 if info.isdir():
1068 1072 continue
1069 1073 if info.issym():
1070 1074 data = info.linkname
1071 1075 else:
1072 1076 data = tar.extractfile(info).read()
1073 1077 archiver.addfile(os.path.join(prefix, self._path, info.name),
1074 1078 info.mode, info.issym(), data)
1075 1079 ui.progress(_('archiving (%s)') % relpath, i + 1,
1076 1080 unit=_('files'))
1077 1081 ui.progress(_('archiving (%s)') % relpath, None)
1078 1082
1079 1083
1080 1084 def status(self, rev2, **opts):
1081 1085 rev1 = self._state[1]
1082 1086 if self._gitmissing() or not rev1:
1083 1087 # if the repo is missing, return no results
1084 1088 return [], [], [], [], [], [], []
1085 1089 modified, added, removed = [], [], []
1086 1090 if rev2:
1087 1091 command = ['diff-tree', rev1, rev2]
1088 1092 else:
1089 1093 command = ['diff-index', rev1]
1090 1094 out = self._gitcommand(command)
1091 1095 for line in out.split('\n'):
1092 1096 tab = line.find('\t')
1093 1097 if tab == -1:
1094 1098 continue
1095 1099 status, f = line[tab - 1], line[tab + 1:]
1096 1100 if status == 'M':
1097 1101 modified.append(f)
1098 1102 elif status == 'A':
1099 1103 added.append(f)
1100 1104 elif status == 'D':
1101 1105 removed.append(f)
1102 1106
1103 1107 deleted = unknown = ignored = clean = []
1104 1108 return modified, added, removed, deleted, unknown, ignored, clean
1105 1109
1106 1110 types = {
1107 1111 'hg': hgsubrepo,
1108 1112 'svn': svnsubrepo,
1109 1113 'git': gitsubrepo,
1110 1114 }
General Comments 0
You need to be logged in to leave comments. Login now