##// END OF EJS Templates
subrepos: add missing self argument to abstractsubrepo.checknested
Brodie Rao -
r12506:e7d45e41 default
parent child Browse files
Show More
@@ -1,568 +1,568
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 def checknested(path):
223 def checknested(self, 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 273 def files(self):
274 274 """return filename iterator"""
275 275 raise NotImplementedError
276 276
277 277 def filedata(self, name):
278 278 """return file data"""
279 279 raise NotImplementedError
280 280
281 281 def fileflags(self, name):
282 282 """return file flags"""
283 283 return ''
284 284
285 285 def archive(self, archiver, prefix):
286 286 for name in self.files():
287 287 flags = self.fileflags(name)
288 288 mode = 'x' in flags and 0755 or 0644
289 289 symlink = 'l' in flags
290 290 archiver.addfile(os.path.join(prefix, self._path, name),
291 291 mode, symlink, self.filedata(name))
292 292
293 293
294 294 class hgsubrepo(abstractsubrepo):
295 295 def __init__(self, ctx, path, state):
296 296 self._path = path
297 297 self._state = state
298 298 r = ctx._repo
299 299 root = r.wjoin(path)
300 300 create = False
301 301 if not os.path.exists(os.path.join(root, '.hg')):
302 302 create = True
303 303 util.makedirs(root)
304 304 self._repo = hg.repository(r.ui, root, create=create)
305 305 self._repo._subparent = r
306 306 self._repo._subsource = state[0]
307 307
308 308 if create:
309 309 fp = self._repo.opener("hgrc", "w", text=True)
310 310 fp.write('[paths]\n')
311 311
312 312 def addpathconfig(key, value):
313 313 fp.write('%s = %s\n' % (key, value))
314 314 self._repo.ui.setconfig('paths', key, value)
315 315
316 316 defpath = _abssource(self._repo)
317 317 defpushpath = _abssource(self._repo, True)
318 318 addpathconfig('default', defpath)
319 319 if defpath != defpushpath:
320 320 addpathconfig('default-push', defpushpath)
321 321 fp.close()
322 322
323 323 def add(self, ui, match, dryrun, prefix):
324 324 return cmdutil.add(ui, self._repo, match, dryrun, True,
325 325 os.path.join(prefix, self._path))
326 326
327 327 def status(self, rev2, **opts):
328 328 try:
329 329 rev1 = self._state[1]
330 330 ctx1 = self._repo[rev1]
331 331 ctx2 = self._repo[rev2]
332 332 return self._repo.status(ctx1, ctx2, **opts)
333 333 except error.RepoLookupError, inst:
334 334 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
335 335 % (inst, relpath(self)))
336 336 return [], [], [], [], [], [], []
337 337
338 338 def diff(self, diffopts, node2, match, prefix, **opts):
339 339 try:
340 340 node1 = node.bin(self._state[1])
341 341 # We currently expect node2 to come from substate and be
342 342 # in hex format
343 343 if node2 is not None:
344 344 node2 = node.bin(node2)
345 345 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
346 346 node1, node2, match,
347 347 prefix=os.path.join(prefix, self._path),
348 348 listsubrepos=True, **opts)
349 349 except error.RepoLookupError, inst:
350 350 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
351 351 % (inst, relpath(self)))
352 352
353 353 def archive(self, archiver, prefix):
354 354 abstractsubrepo.archive(self, archiver, prefix)
355 355
356 356 rev = self._state[1]
357 357 ctx = self._repo[rev]
358 358 for subpath in ctx.substate:
359 359 s = subrepo(ctx, subpath)
360 360 s.archive(archiver, os.path.join(prefix, self._path))
361 361
362 362 def dirty(self):
363 363 r = self._state[1]
364 364 if r == '':
365 365 return True
366 366 w = self._repo[None]
367 367 if w.p1() != self._repo[r]: # version checked out change
368 368 return True
369 369 return w.dirty() # working directory changed
370 370
371 371 def checknested(self, path):
372 372 return self._repo._checknested(self._repo.wjoin(path))
373 373
374 374 def commit(self, text, user, date):
375 375 self._repo.ui.debug("committing subrepo %s\n" % relpath(self))
376 376 n = self._repo.commit(text, user, date)
377 377 if not n:
378 378 return self._repo['.'].hex() # different version checked out
379 379 return node.hex(n)
380 380
381 381 def remove(self):
382 382 # we can't fully delete the repository as it may contain
383 383 # local-only history
384 384 self._repo.ui.note(_('removing subrepo %s\n') % relpath(self))
385 385 hg.clean(self._repo, node.nullid, False)
386 386
387 387 def _get(self, state):
388 388 source, revision, kind = state
389 389 try:
390 390 self._repo.lookup(revision)
391 391 except error.RepoError:
392 392 self._repo._subsource = source
393 393 srcurl = _abssource(self._repo)
394 394 self._repo.ui.status(_('pulling subrepo %s from %s\n')
395 395 % (relpath(self), srcurl))
396 396 other = hg.repository(self._repo.ui, srcurl)
397 397 self._repo.pull(other)
398 398
399 399 def get(self, state):
400 400 self._get(state)
401 401 source, revision, kind = state
402 402 self._repo.ui.debug("getting subrepo %s\n" % self._path)
403 403 hg.clean(self._repo, revision, False)
404 404
405 405 def merge(self, state):
406 406 self._get(state)
407 407 cur = self._repo['.']
408 408 dst = self._repo[state[1]]
409 409 anc = dst.ancestor(cur)
410 410 if anc == cur:
411 411 self._repo.ui.debug("updating subrepo %s\n" % relpath(self))
412 412 hg.update(self._repo, state[1])
413 413 elif anc == dst:
414 414 self._repo.ui.debug("skipping subrepo %s\n" % relpath(self))
415 415 else:
416 416 self._repo.ui.debug("merging subrepo %s\n" % relpath(self))
417 417 hg.merge(self._repo, state[1], remind=False)
418 418
419 419 def push(self, force):
420 420 # push subrepos depth-first for coherent ordering
421 421 c = self._repo['']
422 422 subs = c.substate # only repos that are committed
423 423 for s in sorted(subs):
424 424 if not c.sub(s).push(force):
425 425 return False
426 426
427 427 dsturl = _abssource(self._repo, True)
428 428 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
429 429 (relpath(self), dsturl))
430 430 other = hg.repository(self._repo.ui, dsturl)
431 431 return self._repo.push(other, force)
432 432
433 433 def outgoing(self, ui, dest, opts):
434 434 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
435 435
436 436 def incoming(self, ui, source, opts):
437 437 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
438 438
439 439 def files(self):
440 440 rev = self._state[1]
441 441 ctx = self._repo[rev]
442 442 return ctx.manifest()
443 443
444 444 def filedata(self, name):
445 445 rev = self._state[1]
446 446 return self._repo[rev][name].data()
447 447
448 448 def fileflags(self, name):
449 449 rev = self._state[1]
450 450 ctx = self._repo[rev]
451 451 return ctx.flags(name)
452 452
453 453
454 454 class svnsubrepo(abstractsubrepo):
455 455 def __init__(self, ctx, path, state):
456 456 self._path = path
457 457 self._state = state
458 458 self._ctx = ctx
459 459 self._ui = ctx._repo.ui
460 460
461 461 def _svncommand(self, commands, filename=''):
462 462 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
463 463 cmd = ['svn'] + commands + [path]
464 464 cmd = [util.shellquote(arg) for arg in cmd]
465 465 cmd = util.quotecommand(' '.join(cmd))
466 466 env = dict(os.environ)
467 467 # Avoid localized output, preserve current locale for everything else.
468 468 env['LC_MESSAGES'] = 'C'
469 469 write, read, err = util.popen3(cmd, env=env, newlines=True)
470 470 retdata = read.read()
471 471 err = err.read().strip()
472 472 if err:
473 473 raise util.Abort(err)
474 474 return retdata
475 475
476 476 def _wcrev(self):
477 477 output = self._svncommand(['info', '--xml'])
478 478 doc = xml.dom.minidom.parseString(output)
479 479 entries = doc.getElementsByTagName('entry')
480 480 if not entries:
481 481 return 0
482 482 return int(entries[0].getAttribute('revision') or 0)
483 483
484 484 def _wcchanged(self):
485 485 """Return (changes, extchanges) where changes is True
486 486 if the working directory was changed, and extchanges is
487 487 True if any of these changes concern an external entry.
488 488 """
489 489 output = self._svncommand(['status', '--xml'])
490 490 externals, changes = [], []
491 491 doc = xml.dom.minidom.parseString(output)
492 492 for e in doc.getElementsByTagName('entry'):
493 493 s = e.getElementsByTagName('wc-status')
494 494 if not s:
495 495 continue
496 496 item = s[0].getAttribute('item')
497 497 props = s[0].getAttribute('props')
498 498 path = e.getAttribute('path')
499 499 if item == 'external':
500 500 externals.append(path)
501 501 if (item not in ('', 'normal', 'unversioned', 'external')
502 502 or props not in ('', 'none')):
503 503 changes.append(path)
504 504 for path in changes:
505 505 for ext in externals:
506 506 if path == ext or path.startswith(ext + os.sep):
507 507 return True, True
508 508 return bool(changes), False
509 509
510 510 def dirty(self):
511 511 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
512 512 return False
513 513 return True
514 514
515 515 def commit(self, text, user, date):
516 516 # user and date are out of our hands since svn is centralized
517 517 changed, extchanged = self._wcchanged()
518 518 if not changed:
519 519 return self._wcrev()
520 520 if extchanged:
521 521 # Do not try to commit externals
522 522 raise util.Abort(_('cannot commit svn externals'))
523 523 commitinfo = self._svncommand(['commit', '-m', text])
524 524 self._ui.status(commitinfo)
525 525 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
526 526 if not newrev:
527 527 raise util.Abort(commitinfo.splitlines()[-1])
528 528 newrev = newrev.groups()[0]
529 529 self._ui.status(self._svncommand(['update', '-r', newrev]))
530 530 return newrev
531 531
532 532 def remove(self):
533 533 if self.dirty():
534 534 self._ui.warn(_('not removing repo %s because '
535 535 'it has changes.\n' % self._path))
536 536 return
537 537 self._ui.note(_('removing subrepo %s\n') % self._path)
538 538 shutil.rmtree(self._ctx.repo.join(self._path))
539 539
540 540 def get(self, state):
541 541 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
542 542 if not re.search('Checked out revision [0-9]+.', status):
543 543 raise util.Abort(status.splitlines()[-1])
544 544 self._ui.status(status)
545 545
546 546 def merge(self, state):
547 547 old = int(self._state[1])
548 548 new = int(state[1])
549 549 if new > old:
550 550 self.get(state)
551 551
552 552 def push(self, force):
553 553 # push is a no-op for SVN
554 554 return True
555 555
556 556 def files(self):
557 557 output = self._svncommand(['list'])
558 558 # This works because svn forbids \n in filenames.
559 559 return output.splitlines()
560 560
561 561 def filedata(self, name):
562 562 return self._svncommand(['cat'], name)
563 563
564 564
565 565 types = {
566 566 'hg': hgsubrepo,
567 567 'svn': svnsubrepo,
568 568 }
General Comments 0
You need to be logged in to leave comments. Login now