##// END OF EJS Templates
subrepos: handle backslashes in subpaths
Martin Geisler -
r11961:f3075ffa default
parent child Browse files
Show More
@@ -1,440 +1,447
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
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 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
54 # does a string decode.
55 repl = repl.encode('string-escape')
56 # However, we still want to allow back references to go
57 # through unharmed, so we turn r'\\1' into r'\1'. Again,
58 # extra escapes are needed because re.sub string decodes.
59 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
53 60 try:
54 61 src = re.sub(pattern, repl, src, 1)
55 62 except re.error, e:
56 63 raise util.Abort(_("bad subrepository pattern in %s: %s")
57 64 % (p.source('subpaths', pattern), e))
58 65
59 66 state[path] = (src.strip(), rev.get(path, ''), kind)
60 67
61 68 return state
62 69
63 70 def writestate(repo, state):
64 71 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
65 72 repo.wwrite('.hgsubstate',
66 73 ''.join(['%s %s\n' % (state[s][1], s)
67 74 for s in sorted(state)]), '')
68 75
69 76 def submerge(repo, wctx, mctx, actx):
70 77 """delegated from merge.applyupdates: merging of .hgsubstate file
71 78 in working context, merging context and ancestor context"""
72 79 if mctx == actx: # backwards?
73 80 actx = wctx.p1()
74 81 s1 = wctx.substate
75 82 s2 = mctx.substate
76 83 sa = actx.substate
77 84 sm = {}
78 85
79 86 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
80 87
81 88 def debug(s, msg, r=""):
82 89 if r:
83 90 r = "%s:%s:%s" % r
84 91 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
85 92
86 93 for s, l in s1.items():
87 94 a = sa.get(s, nullstate)
88 95 ld = l # local state with possible dirty flag for compares
89 96 if wctx.sub(s).dirty():
90 97 ld = (l[0], l[1] + "+")
91 98 if wctx == actx: # overwrite
92 99 a = ld
93 100
94 101 if s in s2:
95 102 r = s2[s]
96 103 if ld == r or r == a: # no change or local is newer
97 104 sm[s] = l
98 105 continue
99 106 elif ld == a: # other side changed
100 107 debug(s, "other changed, get", r)
101 108 wctx.sub(s).get(r)
102 109 sm[s] = r
103 110 elif ld[0] != r[0]: # sources differ
104 111 if repo.ui.promptchoice(
105 112 _(' subrepository sources for %s differ\n'
106 113 'use (l)ocal source (%s) or (r)emote source (%s)?')
107 114 % (s, l[0], r[0]),
108 115 (_('&Local'), _('&Remote')), 0):
109 116 debug(s, "prompt changed, get", r)
110 117 wctx.sub(s).get(r)
111 118 sm[s] = r
112 119 elif ld[1] == a[1]: # local side is unchanged
113 120 debug(s, "other side changed, get", r)
114 121 wctx.sub(s).get(r)
115 122 sm[s] = r
116 123 else:
117 124 debug(s, "both sides changed, merge with", r)
118 125 wctx.sub(s).merge(r)
119 126 sm[s] = l
120 127 elif ld == a: # remote removed, local unchanged
121 128 debug(s, "remote removed, remove")
122 129 wctx.sub(s).remove()
123 130 else:
124 131 if repo.ui.promptchoice(
125 132 _(' local changed subrepository %s which remote removed\n'
126 133 'use (c)hanged version or (d)elete?') % s,
127 134 (_('&Changed'), _('&Delete')), 0):
128 135 debug(s, "prompt remove")
129 136 wctx.sub(s).remove()
130 137
131 138 for s, r in s2.items():
132 139 if s in s1:
133 140 continue
134 141 elif s not in sa:
135 142 debug(s, "remote added, get", r)
136 143 mctx.sub(s).get(r)
137 144 sm[s] = r
138 145 elif r != sa[s]:
139 146 if repo.ui.promptchoice(
140 147 _(' remote changed subrepository %s which local removed\n'
141 148 'use (c)hanged version or (d)elete?') % s,
142 149 (_('&Changed'), _('&Delete')), 0) == 0:
143 150 debug(s, "prompt recreate", r)
144 151 wctx.sub(s).get(r)
145 152 sm[s] = r
146 153
147 154 # record merged .hgsubstate
148 155 writestate(repo, sm)
149 156
150 157 def relpath(sub):
151 158 """return path to this subrepo as seen from outermost repo"""
152 159 if not hasattr(sub, '_repo'):
153 160 return sub._path
154 161 parent = sub._repo
155 162 while hasattr(parent, '_subparent'):
156 163 parent = parent._subparent
157 164 return sub._repo.root[len(parent.root)+1:]
158 165
159 166 def _abssource(repo, push=False):
160 167 """return pull/push path of repo - either based on parent repo
161 168 .hgsub info or on the subrepos own config"""
162 169 if hasattr(repo, '_subparent'):
163 170 source = repo._subsource
164 171 if source.startswith('/') or '://' in source:
165 172 return source
166 173 parent = _abssource(repo._subparent, push)
167 174 if '://' in parent:
168 175 if parent[-1] == '/':
169 176 parent = parent[:-1]
170 177 r = urlparse.urlparse(parent + '/' + source)
171 178 r = urlparse.urlunparse((r[0], r[1],
172 179 posixpath.normpath(r[2]),
173 180 r[3], r[4], r[5]))
174 181 return r
175 182 return posixpath.normpath(os.path.join(parent, repo._subsource))
176 183 if push and repo.ui.config('paths', 'default-push'):
177 184 return repo.ui.config('paths', 'default-push', repo.root)
178 185 return repo.ui.config('paths', 'default', repo.root)
179 186
180 187 def subrepo(ctx, path):
181 188 """return instance of the right subrepo class for subrepo in path"""
182 189 # subrepo inherently violates our import layering rules
183 190 # because it wants to make repo objects from deep inside the stack
184 191 # so we manually delay the circular imports to not break
185 192 # scripts that don't use our demand-loading
186 193 global hg
187 194 import hg as h
188 195 hg = h
189 196
190 197 util.path_auditor(ctx._repo.root)(path)
191 198 state = ctx.substate.get(path, nullstate)
192 199 if state[2] not in types:
193 200 raise util.Abort(_('unknown subrepo type %s') % state[2])
194 201 return types[state[2]](ctx, path, state[:2])
195 202
196 203 # subrepo classes need to implement the following abstract class:
197 204
198 205 class abstractsubrepo(object):
199 206
200 207 def dirty(self):
201 208 """returns true if the dirstate of the subrepo does not match
202 209 current stored state
203 210 """
204 211 raise NotImplementedError
205 212
206 213 def commit(self, text, user, date):
207 214 """commit the current changes to the subrepo with the given
208 215 log message. Use given user and date if possible. Return the
209 216 new state of the subrepo.
210 217 """
211 218 raise NotImplementedError
212 219
213 220 def remove(self):
214 221 """remove the subrepo
215 222
216 223 (should verify the dirstate is not dirty first)
217 224 """
218 225 raise NotImplementedError
219 226
220 227 def get(self, state):
221 228 """run whatever commands are needed to put the subrepo into
222 229 this state
223 230 """
224 231 raise NotImplementedError
225 232
226 233 def merge(self, state):
227 234 """merge currently-saved state with the new state."""
228 235 raise NotImplementedError
229 236
230 237 def push(self, force):
231 238 """perform whatever action is analogous to 'hg push'
232 239
233 240 This may be a no-op on some systems.
234 241 """
235 242 raise NotImplementedError
236 243
237 244
238 245 class hgsubrepo(abstractsubrepo):
239 246 def __init__(self, ctx, path, state):
240 247 self._path = path
241 248 self._state = state
242 249 r = ctx._repo
243 250 root = r.wjoin(path)
244 251 create = False
245 252 if not os.path.exists(os.path.join(root, '.hg')):
246 253 create = True
247 254 util.makedirs(root)
248 255 self._repo = hg.repository(r.ui, root, create=create)
249 256 self._repo._subparent = r
250 257 self._repo._subsource = state[0]
251 258
252 259 if create:
253 260 fp = self._repo.opener("hgrc", "w", text=True)
254 261 fp.write('[paths]\n')
255 262
256 263 def addpathconfig(key, value):
257 264 fp.write('%s = %s\n' % (key, value))
258 265 self._repo.ui.setconfig('paths', key, value)
259 266
260 267 defpath = _abssource(self._repo)
261 268 defpushpath = _abssource(self._repo, True)
262 269 addpathconfig('default', defpath)
263 270 if defpath != defpushpath:
264 271 addpathconfig('default-push', defpushpath)
265 272 fp.close()
266 273
267 274 def dirty(self):
268 275 r = self._state[1]
269 276 if r == '':
270 277 return True
271 278 w = self._repo[None]
272 279 if w.p1() != self._repo[r]: # version checked out change
273 280 return True
274 281 return w.dirty() # working directory changed
275 282
276 283 def commit(self, text, user, date):
277 284 self._repo.ui.debug("committing subrepo %s\n" % relpath(self))
278 285 n = self._repo.commit(text, user, date)
279 286 if not n:
280 287 return self._repo['.'].hex() # different version checked out
281 288 return node.hex(n)
282 289
283 290 def remove(self):
284 291 # we can't fully delete the repository as it may contain
285 292 # local-only history
286 293 self._repo.ui.note(_('removing subrepo %s\n') % relpath(self))
287 294 hg.clean(self._repo, node.nullid, False)
288 295
289 296 def _get(self, state):
290 297 source, revision, kind = state
291 298 try:
292 299 self._repo.lookup(revision)
293 300 except error.RepoError:
294 301 self._repo._subsource = source
295 302 srcurl = _abssource(self._repo)
296 303 self._repo.ui.status(_('pulling subrepo %s from %s\n')
297 304 % (relpath(self), srcurl))
298 305 other = hg.repository(self._repo.ui, srcurl)
299 306 self._repo.pull(other)
300 307
301 308 def get(self, state):
302 309 self._get(state)
303 310 source, revision, kind = state
304 311 self._repo.ui.debug("getting subrepo %s\n" % self._path)
305 312 hg.clean(self._repo, revision, False)
306 313
307 314 def merge(self, state):
308 315 self._get(state)
309 316 cur = self._repo['.']
310 317 dst = self._repo[state[1]]
311 318 anc = dst.ancestor(cur)
312 319 if anc == cur:
313 320 self._repo.ui.debug("updating subrepo %s\n" % relpath(self))
314 321 hg.update(self._repo, state[1])
315 322 elif anc == dst:
316 323 self._repo.ui.debug("skipping subrepo %s\n" % relpath(self))
317 324 else:
318 325 self._repo.ui.debug("merging subrepo %s\n" % relpath(self))
319 326 hg.merge(self._repo, state[1], remind=False)
320 327
321 328 def push(self, force):
322 329 # push subrepos depth-first for coherent ordering
323 330 c = self._repo['']
324 331 subs = c.substate # only repos that are committed
325 332 for s in sorted(subs):
326 333 if not c.sub(s).push(force):
327 334 return False
328 335
329 336 dsturl = _abssource(self._repo, True)
330 337 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
331 338 (relpath(self), dsturl))
332 339 other = hg.repository(self._repo.ui, dsturl)
333 340 return self._repo.push(other, force)
334 341
335 342 class svnsubrepo(abstractsubrepo):
336 343 def __init__(self, ctx, path, state):
337 344 self._path = path
338 345 self._state = state
339 346 self._ctx = ctx
340 347 self._ui = ctx._repo.ui
341 348
342 349 def _svncommand(self, commands, filename=''):
343 350 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
344 351 cmd = ['svn'] + commands + [path]
345 352 cmd = [util.shellquote(arg) for arg in cmd]
346 353 cmd = util.quotecommand(' '.join(cmd))
347 354 env = dict(os.environ)
348 355 # Avoid localized output, preserve current locale for everything else.
349 356 env['LC_MESSAGES'] = 'C'
350 357 write, read, err = util.popen3(cmd, env=env, newlines=True)
351 358 retdata = read.read()
352 359 err = err.read().strip()
353 360 if err:
354 361 raise util.Abort(err)
355 362 return retdata
356 363
357 364 def _wcrev(self):
358 365 output = self._svncommand(['info', '--xml'])
359 366 doc = xml.dom.minidom.parseString(output)
360 367 entries = doc.getElementsByTagName('entry')
361 368 if not entries:
362 369 return 0
363 370 return int(entries[0].getAttribute('revision') or 0)
364 371
365 372 def _wcchanged(self):
366 373 """Return (changes, extchanges) where changes is True
367 374 if the working directory was changed, and extchanges is
368 375 True if any of these changes concern an external entry.
369 376 """
370 377 output = self._svncommand(['status', '--xml'])
371 378 externals, changes = [], []
372 379 doc = xml.dom.minidom.parseString(output)
373 380 for e in doc.getElementsByTagName('entry'):
374 381 s = e.getElementsByTagName('wc-status')
375 382 if not s:
376 383 continue
377 384 item = s[0].getAttribute('item')
378 385 props = s[0].getAttribute('props')
379 386 path = e.getAttribute('path')
380 387 if item == 'external':
381 388 externals.append(path)
382 389 if (item not in ('', 'normal', 'unversioned', 'external')
383 390 or props not in ('', 'none')):
384 391 changes.append(path)
385 392 for path in changes:
386 393 for ext in externals:
387 394 if path == ext or path.startswith(ext + os.sep):
388 395 return True, True
389 396 return bool(changes), False
390 397
391 398 def dirty(self):
392 399 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
393 400 return False
394 401 return True
395 402
396 403 def commit(self, text, user, date):
397 404 # user and date are out of our hands since svn is centralized
398 405 changed, extchanged = self._wcchanged()
399 406 if not changed:
400 407 return self._wcrev()
401 408 if extchanged:
402 409 # Do not try to commit externals
403 410 raise util.Abort(_('cannot commit svn externals'))
404 411 commitinfo = self._svncommand(['commit', '-m', text])
405 412 self._ui.status(commitinfo)
406 413 newrev = re.search('Committed revision ([\d]+).', commitinfo)
407 414 if not newrev:
408 415 raise util.Abort(commitinfo.splitlines()[-1])
409 416 newrev = newrev.groups()[0]
410 417 self._ui.status(self._svncommand(['update', '-r', newrev]))
411 418 return newrev
412 419
413 420 def remove(self):
414 421 if self.dirty():
415 422 self._ui.warn(_('not removing repo %s because '
416 423 'it has changes.\n' % self._path))
417 424 return
418 425 self._ui.note(_('removing subrepo %s\n') % self._path)
419 426 shutil.rmtree(self._ctx.repo.join(self._path))
420 427
421 428 def get(self, state):
422 429 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
423 430 if not re.search('Checked out revision [\d]+.', status):
424 431 raise util.Abort(status.splitlines()[-1])
425 432 self._ui.status(status)
426 433
427 434 def merge(self, state):
428 435 old = int(self._state[1])
429 436 new = int(state[1])
430 437 if new > old:
431 438 self.get(state)
432 439
433 440 def push(self, force):
434 441 # push is a no-op for SVN
435 442 return True
436 443
437 444 types = {
438 445 'hg': hgsubrepo,
439 446 'svn': svnsubrepo,
440 447 }
@@ -1,35 +1,33
1 1 $ hg init outer
2 2 $ cd outer
3 3
4 4 hg debugsub with no remapping
5 5
6 6 $ echo 'sub = http://example.net/libfoo' > .hgsub
7 7 $ hg add .hgsub
8 8
9 9 $ hg debugsub
10 10 path sub
11 11 source http://example.net/libfoo
12 12 revision
13 13
14 14 hg debugsub with remapping
15 15
16 $ cat > .hg/hgrc <<EOF
17 > [subpaths]
18 > http://example.net = ssh://localhost
19 > EOF
16 $ echo '[subpaths]' > .hg/hgrc
17 $ echo 'http://example.net/lib(.*) = C:\libs\\1-lib\' >> .hg/hgrc
20 18
21 19 $ hg debugsub
22 20 path sub
23 source ssh://localhost/libfoo
21 source C:\libs\foo-lib\
24 22 revision
25 23
26 24 test bad subpaths pattern
27 25
28 26 $ cat > .hg/hgrc <<EOF
29 27 > [subpaths]
30 28 > .* = \1
31 29 > EOF
32 30 $ hg debugsub
33 31 abort: bad subrepository pattern in .*/test-subrepo-paths.t/outer/.hg/hgrc:2: invalid group reference
34 32
35 33 $ exit 0
General Comments 0
You need to be logged in to leave comments. Login now