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