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