##// END OF EJS Templates
subrepo: use subprocess directly to avoid python 2.6 bug...
Patrick Mezard -
r13014:d1c52354 stable
parent child Browse files
Show More
@@ -1,594 +1,597 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 import stat
9 import stat, subprocess
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 p.parse(f, ctx[f].data(), sections, remap, read)
25 25 else:
26 26 raise util.Abort(_("subrepo spec file %s not found") % f)
27 27
28 28 if '.hgsub' in ctx:
29 29 read('.hgsub')
30 30
31 31 for path, src in ui.configitems('subpaths'):
32 32 p.set('subpaths', path, src, ui.configsource('subpaths', path))
33 33
34 34 rev = {}
35 35 if '.hgsubstate' in ctx:
36 36 try:
37 37 for l in ctx['.hgsubstate'].data().splitlines():
38 38 revision, path = l.split(" ", 1)
39 39 rev[path] = revision
40 40 except IOError, err:
41 41 if err.errno != errno.ENOENT:
42 42 raise
43 43
44 44 state = {}
45 45 for path, src in p[''].items():
46 46 kind = 'hg'
47 47 if src.startswith('['):
48 48 if ']' not in src:
49 49 raise util.Abort(_('missing ] in subrepo source'))
50 50 kind, src = src.split(']', 1)
51 51 kind = kind[1:]
52 52
53 53 for pattern, repl in p.items('subpaths'):
54 54 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
55 55 # does a string decode.
56 56 repl = repl.encode('string-escape')
57 57 # However, we still want to allow back references to go
58 58 # through unharmed, so we turn r'\\1' into r'\1'. Again,
59 59 # extra escapes are needed because re.sub string decodes.
60 60 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
61 61 try:
62 62 src = re.sub(pattern, repl, src, 1)
63 63 except re.error, e:
64 64 raise util.Abort(_("bad subrepository pattern in %s: %s")
65 65 % (p.source('subpaths', pattern), e))
66 66
67 67 state[path] = (src.strip(), rev.get(path, ''), kind)
68 68
69 69 return state
70 70
71 71 def writestate(repo, state):
72 72 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
73 73 repo.wwrite('.hgsubstate',
74 74 ''.join(['%s %s\n' % (state[s][1], s)
75 75 for s in sorted(state)]), '')
76 76
77 77 def submerge(repo, wctx, mctx, actx):
78 78 """delegated from merge.applyupdates: merging of .hgsubstate file
79 79 in working context, merging context and ancestor context"""
80 80 if mctx == actx: # backwards?
81 81 actx = wctx.p1()
82 82 s1 = wctx.substate
83 83 s2 = mctx.substate
84 84 sa = actx.substate
85 85 sm = {}
86 86
87 87 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
88 88
89 89 def debug(s, msg, r=""):
90 90 if r:
91 91 r = "%s:%s:%s" % r
92 92 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
93 93
94 94 for s, l in s1.items():
95 95 a = sa.get(s, nullstate)
96 96 ld = l # local state with possible dirty flag for compares
97 97 if wctx.sub(s).dirty():
98 98 ld = (l[0], l[1] + "+")
99 99 if wctx == actx: # overwrite
100 100 a = ld
101 101
102 102 if s in s2:
103 103 r = s2[s]
104 104 if ld == r or r == a: # no change or local is newer
105 105 sm[s] = l
106 106 continue
107 107 elif ld == a: # other side changed
108 108 debug(s, "other changed, get", r)
109 109 wctx.sub(s).get(r)
110 110 sm[s] = r
111 111 elif ld[0] != r[0]: # sources differ
112 112 if repo.ui.promptchoice(
113 113 _(' subrepository sources for %s differ\n'
114 114 'use (l)ocal source (%s) or (r)emote source (%s)?')
115 115 % (s, l[0], r[0]),
116 116 (_('&Local'), _('&Remote')), 0):
117 117 debug(s, "prompt changed, get", r)
118 118 wctx.sub(s).get(r)
119 119 sm[s] = r
120 120 elif ld[1] == a[1]: # local side is unchanged
121 121 debug(s, "other side changed, get", r)
122 122 wctx.sub(s).get(r)
123 123 sm[s] = r
124 124 else:
125 125 debug(s, "both sides changed, merge with", r)
126 126 wctx.sub(s).merge(r)
127 127 sm[s] = l
128 128 elif ld == a: # remote removed, local unchanged
129 129 debug(s, "remote removed, remove")
130 130 wctx.sub(s).remove()
131 131 else:
132 132 if repo.ui.promptchoice(
133 133 _(' local changed subrepository %s which remote removed\n'
134 134 'use (c)hanged version or (d)elete?') % s,
135 135 (_('&Changed'), _('&Delete')), 0):
136 136 debug(s, "prompt remove")
137 137 wctx.sub(s).remove()
138 138
139 139 for s, r in s2.items():
140 140 if s in s1:
141 141 continue
142 142 elif s not in sa:
143 143 debug(s, "remote added, get", r)
144 144 mctx.sub(s).get(r)
145 145 sm[s] = r
146 146 elif r != sa[s]:
147 147 if repo.ui.promptchoice(
148 148 _(' remote changed subrepository %s which local removed\n'
149 149 'use (c)hanged version or (d)elete?') % s,
150 150 (_('&Changed'), _('&Delete')), 0) == 0:
151 151 debug(s, "prompt recreate", r)
152 152 wctx.sub(s).get(r)
153 153 sm[s] = r
154 154
155 155 # record merged .hgsubstate
156 156 writestate(repo, sm)
157 157
158 158 def reporelpath(repo):
159 159 """return path to this (sub)repo as seen from outermost repo"""
160 160 parent = repo
161 161 while hasattr(parent, '_subparent'):
162 162 parent = parent._subparent
163 163 return repo.root[len(parent.root)+1:]
164 164
165 165 def subrelpath(sub):
166 166 """return path to this subrepo as seen from outermost repo"""
167 167 if not hasattr(sub, '_repo'):
168 168 return sub._path
169 169 return reporelpath(sub._repo)
170 170
171 171 def _abssource(repo, push=False, abort=True):
172 172 """return pull/push path of repo - either based on parent repo .hgsub info
173 173 or on the top repo config. Abort or return None if no source found."""
174 174 if hasattr(repo, '_subparent'):
175 175 source = repo._subsource
176 176 if source.startswith('/') or '://' in source:
177 177 return source
178 178 parent = _abssource(repo._subparent, push, abort=False)
179 179 if parent:
180 180 if '://' in parent:
181 181 if parent[-1] == '/':
182 182 parent = parent[:-1]
183 183 r = urlparse.urlparse(parent + '/' + source)
184 184 r = urlparse.urlunparse((r[0], r[1],
185 185 posixpath.normpath(r[2]),
186 186 r[3], r[4], r[5]))
187 187 return r
188 188 else: # plain file system path
189 189 return posixpath.normpath(os.path.join(parent, repo._subsource))
190 190 else: # recursion reached top repo
191 191 if hasattr(repo, '_subtoppath'):
192 192 return repo._subtoppath
193 193 if push and repo.ui.config('paths', 'default-push'):
194 194 return repo.ui.config('paths', 'default-push')
195 195 if repo.ui.config('paths', 'default'):
196 196 return repo.ui.config('paths', 'default')
197 197 if abort:
198 198 raise util.Abort(_("default path for subrepository %s not found") %
199 199 reporelpath(repo))
200 200
201 201 def itersubrepos(ctx1, ctx2):
202 202 """find subrepos in ctx1 or ctx2"""
203 203 # Create a (subpath, ctx) mapping where we prefer subpaths from
204 204 # ctx1. The subpaths from ctx2 are important when the .hgsub file
205 205 # has been modified (in ctx2) but not yet committed (in ctx1).
206 206 subpaths = dict.fromkeys(ctx2.substate, ctx2)
207 207 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
208 208 for subpath, ctx in sorted(subpaths.iteritems()):
209 209 yield subpath, ctx.sub(subpath)
210 210
211 211 def subrepo(ctx, path):
212 212 """return instance of the right subrepo class for subrepo in path"""
213 213 # subrepo inherently violates our import layering rules
214 214 # because it wants to make repo objects from deep inside the stack
215 215 # so we manually delay the circular imports to not break
216 216 # scripts that don't use our demand-loading
217 217 global hg
218 218 import hg as h
219 219 hg = h
220 220
221 221 util.path_auditor(ctx._repo.root)(path)
222 222 state = ctx.substate.get(path, nullstate)
223 223 if state[2] not in types:
224 224 raise util.Abort(_('unknown subrepo type %s') % state[2])
225 225 return types[state[2]](ctx, path, state[:2])
226 226
227 227 # subrepo classes need to implement the following abstract class:
228 228
229 229 class abstractsubrepo(object):
230 230
231 231 def dirty(self):
232 232 """returns true if the dirstate of the subrepo does not match
233 233 current stored state
234 234 """
235 235 raise NotImplementedError
236 236
237 237 def checknested(self, path):
238 238 """check if path is a subrepository within this repository"""
239 239 return False
240 240
241 241 def commit(self, text, user, date):
242 242 """commit the current changes to the subrepo with the given
243 243 log message. Use given user and date if possible. Return the
244 244 new state of the subrepo.
245 245 """
246 246 raise NotImplementedError
247 247
248 248 def remove(self):
249 249 """remove the subrepo
250 250
251 251 (should verify the dirstate is not dirty first)
252 252 """
253 253 raise NotImplementedError
254 254
255 255 def get(self, state):
256 256 """run whatever commands are needed to put the subrepo into
257 257 this state
258 258 """
259 259 raise NotImplementedError
260 260
261 261 def merge(self, state):
262 262 """merge currently-saved state with the new state."""
263 263 raise NotImplementedError
264 264
265 265 def push(self, force):
266 266 """perform whatever action is analogous to 'hg push'
267 267
268 268 This may be a no-op on some systems.
269 269 """
270 270 raise NotImplementedError
271 271
272 272 def add(self, ui, match, dryrun, prefix):
273 273 return []
274 274
275 275 def status(self, rev2, **opts):
276 276 return [], [], [], [], [], [], []
277 277
278 278 def diff(self, diffopts, node2, match, prefix, **opts):
279 279 pass
280 280
281 281 def outgoing(self, ui, dest, opts):
282 282 return 1
283 283
284 284 def incoming(self, ui, source, opts):
285 285 return 1
286 286
287 287 def files(self):
288 288 """return filename iterator"""
289 289 raise NotImplementedError
290 290
291 291 def filedata(self, name):
292 292 """return file data"""
293 293 raise NotImplementedError
294 294
295 295 def fileflags(self, name):
296 296 """return file flags"""
297 297 return ''
298 298
299 299 def archive(self, archiver, prefix):
300 300 for name in self.files():
301 301 flags = self.fileflags(name)
302 302 mode = 'x' in flags and 0755 or 0644
303 303 symlink = 'l' in flags
304 304 archiver.addfile(os.path.join(prefix, self._path, name),
305 305 mode, symlink, self.filedata(name))
306 306
307 307
308 308 class hgsubrepo(abstractsubrepo):
309 309 def __init__(self, ctx, path, state):
310 310 self._path = path
311 311 self._state = state
312 312 r = ctx._repo
313 313 root = r.wjoin(path)
314 314 create = False
315 315 if not os.path.exists(os.path.join(root, '.hg')):
316 316 create = True
317 317 util.makedirs(root)
318 318 self._repo = hg.repository(r.ui, root, create=create)
319 319 self._repo._subparent = r
320 320 self._repo._subsource = state[0]
321 321
322 322 if create:
323 323 fp = self._repo.opener("hgrc", "w", text=True)
324 324 fp.write('[paths]\n')
325 325
326 326 def addpathconfig(key, value):
327 327 if value:
328 328 fp.write('%s = %s\n' % (key, value))
329 329 self._repo.ui.setconfig('paths', key, value)
330 330
331 331 defpath = _abssource(self._repo, abort=False)
332 332 defpushpath = _abssource(self._repo, True, abort=False)
333 333 addpathconfig('default', defpath)
334 334 if defpath != defpushpath:
335 335 addpathconfig('default-push', defpushpath)
336 336 fp.close()
337 337
338 338 def add(self, ui, match, dryrun, prefix):
339 339 return cmdutil.add(ui, self._repo, match, dryrun, True,
340 340 os.path.join(prefix, self._path))
341 341
342 342 def status(self, rev2, **opts):
343 343 try:
344 344 rev1 = self._state[1]
345 345 ctx1 = self._repo[rev1]
346 346 ctx2 = self._repo[rev2]
347 347 return self._repo.status(ctx1, ctx2, **opts)
348 348 except error.RepoLookupError, inst:
349 349 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
350 350 % (inst, subrelpath(self)))
351 351 return [], [], [], [], [], [], []
352 352
353 353 def diff(self, diffopts, node2, match, prefix, **opts):
354 354 try:
355 355 node1 = node.bin(self._state[1])
356 356 # We currently expect node2 to come from substate and be
357 357 # in hex format
358 358 if node2 is not None:
359 359 node2 = node.bin(node2)
360 360 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
361 361 node1, node2, match,
362 362 prefix=os.path.join(prefix, self._path),
363 363 listsubrepos=True, **opts)
364 364 except error.RepoLookupError, inst:
365 365 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
366 366 % (inst, subrelpath(self)))
367 367
368 368 def archive(self, archiver, prefix):
369 369 abstractsubrepo.archive(self, archiver, prefix)
370 370
371 371 rev = self._state[1]
372 372 ctx = self._repo[rev]
373 373 for subpath in ctx.substate:
374 374 s = subrepo(ctx, subpath)
375 375 s.archive(archiver, os.path.join(prefix, self._path))
376 376
377 377 def dirty(self):
378 378 r = self._state[1]
379 379 if r == '':
380 380 return True
381 381 w = self._repo[None]
382 382 if w.p1() != self._repo[r]: # version checked out change
383 383 return True
384 384 return w.dirty() # working directory changed
385 385
386 386 def checknested(self, path):
387 387 return self._repo._checknested(self._repo.wjoin(path))
388 388
389 389 def commit(self, text, user, date):
390 390 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
391 391 n = self._repo.commit(text, user, date)
392 392 if not n:
393 393 return self._repo['.'].hex() # different version checked out
394 394 return node.hex(n)
395 395
396 396 def remove(self):
397 397 # we can't fully delete the repository as it may contain
398 398 # local-only history
399 399 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
400 400 hg.clean(self._repo, node.nullid, False)
401 401
402 402 def _get(self, state):
403 403 source, revision, kind = state
404 404 try:
405 405 self._repo.lookup(revision)
406 406 except error.RepoError:
407 407 self._repo._subsource = source
408 408 srcurl = _abssource(self._repo)
409 409 self._repo.ui.status(_('pulling subrepo %s from %s\n')
410 410 % (subrelpath(self), srcurl))
411 411 other = hg.repository(self._repo.ui, srcurl)
412 412 self._repo.pull(other)
413 413
414 414 def get(self, state):
415 415 self._get(state)
416 416 source, revision, kind = state
417 417 self._repo.ui.debug("getting subrepo %s\n" % self._path)
418 418 hg.clean(self._repo, revision, False)
419 419
420 420 def merge(self, state):
421 421 self._get(state)
422 422 cur = self._repo['.']
423 423 dst = self._repo[state[1]]
424 424 anc = dst.ancestor(cur)
425 425 if anc == cur:
426 426 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
427 427 hg.update(self._repo, state[1])
428 428 elif anc == dst:
429 429 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
430 430 else:
431 431 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
432 432 hg.merge(self._repo, state[1], remind=False)
433 433
434 434 def push(self, force):
435 435 # push subrepos depth-first for coherent ordering
436 436 c = self._repo['']
437 437 subs = c.substate # only repos that are committed
438 438 for s in sorted(subs):
439 439 if not c.sub(s).push(force):
440 440 return False
441 441
442 442 dsturl = _abssource(self._repo, True)
443 443 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
444 444 (subrelpath(self), dsturl))
445 445 other = hg.repository(self._repo.ui, dsturl)
446 446 return self._repo.push(other, force)
447 447
448 448 def outgoing(self, ui, dest, opts):
449 449 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
450 450
451 451 def incoming(self, ui, source, opts):
452 452 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
453 453
454 454 def files(self):
455 455 rev = self._state[1]
456 456 ctx = self._repo[rev]
457 457 return ctx.manifest()
458 458
459 459 def filedata(self, name):
460 460 rev = self._state[1]
461 461 return self._repo[rev][name].data()
462 462
463 463 def fileflags(self, name):
464 464 rev = self._state[1]
465 465 ctx = self._repo[rev]
466 466 return ctx.flags(name)
467 467
468 468
469 469 class svnsubrepo(abstractsubrepo):
470 470 def __init__(self, ctx, path, state):
471 471 self._path = path
472 472 self._state = state
473 473 self._ctx = ctx
474 474 self._ui = ctx._repo.ui
475 475
476 476 def _svncommand(self, commands, filename=''):
477 477 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
478 478 cmd = ['svn'] + commands + [path]
479 479 cmd = [util.shellquote(arg) for arg in cmd]
480 480 cmd = util.quotecommand(' '.join(cmd))
481 481 env = dict(os.environ)
482 482 # Avoid localized output, preserve current locale for everything else.
483 483 env['LC_MESSAGES'] = 'C'
484 write, read, err = util.popen3(cmd, env=env, newlines=True)
485 retdata = read.read()
486 err = err.read().strip()
487 if err:
488 raise util.Abort(err)
489 return retdata
484 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
485 close_fds=util.closefds,
486 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
487 universal_newlines=True, env=env)
488 stdout, stderr = p.communicate()
489 stderr = stderr.strip()
490 if stderr:
491 raise util.Abort(stderr)
492 return stdout
490 493
491 494 def _wcrev(self):
492 495 output = self._svncommand(['info', '--xml'])
493 496 doc = xml.dom.minidom.parseString(output)
494 497 entries = doc.getElementsByTagName('entry')
495 498 if not entries:
496 499 return '0'
497 500 return str(entries[0].getAttribute('revision')) or '0'
498 501
499 502 def _wcchanged(self):
500 503 """Return (changes, extchanges) where changes is True
501 504 if the working directory was changed, and extchanges is
502 505 True if any of these changes concern an external entry.
503 506 """
504 507 output = self._svncommand(['status', '--xml'])
505 508 externals, changes = [], []
506 509 doc = xml.dom.minidom.parseString(output)
507 510 for e in doc.getElementsByTagName('entry'):
508 511 s = e.getElementsByTagName('wc-status')
509 512 if not s:
510 513 continue
511 514 item = s[0].getAttribute('item')
512 515 props = s[0].getAttribute('props')
513 516 path = e.getAttribute('path')
514 517 if item == 'external':
515 518 externals.append(path)
516 519 if (item not in ('', 'normal', 'unversioned', 'external')
517 520 or props not in ('', 'none')):
518 521 changes.append(path)
519 522 for path in changes:
520 523 for ext in externals:
521 524 if path == ext or path.startswith(ext + os.sep):
522 525 return True, True
523 526 return bool(changes), False
524 527
525 528 def dirty(self):
526 529 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
527 530 return False
528 531 return True
529 532
530 533 def commit(self, text, user, date):
531 534 # user and date are out of our hands since svn is centralized
532 535 changed, extchanged = self._wcchanged()
533 536 if not changed:
534 537 return self._wcrev()
535 538 if extchanged:
536 539 # Do not try to commit externals
537 540 raise util.Abort(_('cannot commit svn externals'))
538 541 commitinfo = self._svncommand(['commit', '-m', text])
539 542 self._ui.status(commitinfo)
540 543 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
541 544 if not newrev:
542 545 raise util.Abort(commitinfo.splitlines()[-1])
543 546 newrev = newrev.groups()[0]
544 547 self._ui.status(self._svncommand(['update', '-r', newrev]))
545 548 return newrev
546 549
547 550 def remove(self):
548 551 if self.dirty():
549 552 self._ui.warn(_('not removing repo %s because '
550 553 'it has changes.\n' % self._path))
551 554 return
552 555 self._ui.note(_('removing subrepo %s\n') % self._path)
553 556
554 557 def onerror(function, path, excinfo):
555 558 if function is not os.remove:
556 559 raise
557 560 # read-only files cannot be unlinked under Windows
558 561 s = os.stat(path)
559 562 if (s.st_mode & stat.S_IWRITE) != 0:
560 563 raise
561 564 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
562 565 os.remove(path)
563 566
564 567 shutil.rmtree(self._ctx._repo.wjoin(self._path), onerror=onerror)
565 568
566 569 def get(self, state):
567 570 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
568 571 if not re.search('Checked out revision [0-9]+.', status):
569 572 raise util.Abort(status.splitlines()[-1])
570 573 self._ui.status(status)
571 574
572 575 def merge(self, state):
573 576 old = int(self._state[1])
574 577 new = int(state[1])
575 578 if new > old:
576 579 self.get(state)
577 580
578 581 def push(self, force):
579 582 # push is a no-op for SVN
580 583 return True
581 584
582 585 def files(self):
583 586 output = self._svncommand(['list'])
584 587 # This works because svn forbids \n in filenames.
585 588 return output.splitlines()
586 589
587 590 def filedata(self, name):
588 591 return self._svncommand(['cat'], name)
589 592
590 593
591 594 types = {
592 595 'hg': hgsubrepo,
593 596 'svn': svnsubrepo,
594 597 }
General Comments 0
You need to be logged in to leave comments. Login now