##// END OF EJS Templates
subrepo: use [0-9] instead of [\d] in svn subrepo regex...
Martin Geisler -
r12060:63eab3b7 default
parent child Browse files
Show More
@@ -1,447 +1,447 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
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 subrepo(ctx, path):
188 188 """return instance of the right subrepo class for subrepo in path"""
189 189 # subrepo inherently violates our import layering rules
190 190 # because it wants to make repo objects from deep inside the stack
191 191 # so we manually delay the circular imports to not break
192 192 # scripts that don't use our demand-loading
193 193 global hg
194 194 import hg as h
195 195 hg = h
196 196
197 197 util.path_auditor(ctx._repo.root)(path)
198 198 state = ctx.substate.get(path, nullstate)
199 199 if state[2] not in types:
200 200 raise util.Abort(_('unknown subrepo type %s') % state[2])
201 201 return types[state[2]](ctx, path, state[:2])
202 202
203 203 # subrepo classes need to implement the following abstract class:
204 204
205 205 class abstractsubrepo(object):
206 206
207 207 def dirty(self):
208 208 """returns true if the dirstate of the subrepo does not match
209 209 current stored state
210 210 """
211 211 raise NotImplementedError
212 212
213 213 def commit(self, text, user, date):
214 214 """commit the current changes to the subrepo with the given
215 215 log message. Use given user and date if possible. Return the
216 216 new state of the subrepo.
217 217 """
218 218 raise NotImplementedError
219 219
220 220 def remove(self):
221 221 """remove the subrepo
222 222
223 223 (should verify the dirstate is not dirty first)
224 224 """
225 225 raise NotImplementedError
226 226
227 227 def get(self, state):
228 228 """run whatever commands are needed to put the subrepo into
229 229 this state
230 230 """
231 231 raise NotImplementedError
232 232
233 233 def merge(self, state):
234 234 """merge currently-saved state with the new state."""
235 235 raise NotImplementedError
236 236
237 237 def push(self, force):
238 238 """perform whatever action is analogous to 'hg push'
239 239
240 240 This may be a no-op on some systems.
241 241 """
242 242 raise NotImplementedError
243 243
244 244
245 245 class hgsubrepo(abstractsubrepo):
246 246 def __init__(self, ctx, path, state):
247 247 self._path = path
248 248 self._state = state
249 249 r = ctx._repo
250 250 root = r.wjoin(path)
251 251 create = False
252 252 if not os.path.exists(os.path.join(root, '.hg')):
253 253 create = True
254 254 util.makedirs(root)
255 255 self._repo = hg.repository(r.ui, root, create=create)
256 256 self._repo._subparent = r
257 257 self._repo._subsource = state[0]
258 258
259 259 if create:
260 260 fp = self._repo.opener("hgrc", "w", text=True)
261 261 fp.write('[paths]\n')
262 262
263 263 def addpathconfig(key, value):
264 264 fp.write('%s = %s\n' % (key, value))
265 265 self._repo.ui.setconfig('paths', key, value)
266 266
267 267 defpath = _abssource(self._repo)
268 268 defpushpath = _abssource(self._repo, True)
269 269 addpathconfig('default', defpath)
270 270 if defpath != defpushpath:
271 271 addpathconfig('default-push', defpushpath)
272 272 fp.close()
273 273
274 274 def dirty(self):
275 275 r = self._state[1]
276 276 if r == '':
277 277 return True
278 278 w = self._repo[None]
279 279 if w.p1() != self._repo[r]: # version checked out change
280 280 return True
281 281 return w.dirty() # working directory changed
282 282
283 283 def commit(self, text, user, date):
284 284 self._repo.ui.debug("committing subrepo %s\n" % relpath(self))
285 285 n = self._repo.commit(text, user, date)
286 286 if not n:
287 287 return self._repo['.'].hex() # different version checked out
288 288 return node.hex(n)
289 289
290 290 def remove(self):
291 291 # we can't fully delete the repository as it may contain
292 292 # local-only history
293 293 self._repo.ui.note(_('removing subrepo %s\n') % relpath(self))
294 294 hg.clean(self._repo, node.nullid, False)
295 295
296 296 def _get(self, state):
297 297 source, revision, kind = state
298 298 try:
299 299 self._repo.lookup(revision)
300 300 except error.RepoError:
301 301 self._repo._subsource = source
302 302 srcurl = _abssource(self._repo)
303 303 self._repo.ui.status(_('pulling subrepo %s from %s\n')
304 304 % (relpath(self), srcurl))
305 305 other = hg.repository(self._repo.ui, srcurl)
306 306 self._repo.pull(other)
307 307
308 308 def get(self, state):
309 309 self._get(state)
310 310 source, revision, kind = state
311 311 self._repo.ui.debug("getting subrepo %s\n" % self._path)
312 312 hg.clean(self._repo, revision, False)
313 313
314 314 def merge(self, state):
315 315 self._get(state)
316 316 cur = self._repo['.']
317 317 dst = self._repo[state[1]]
318 318 anc = dst.ancestor(cur)
319 319 if anc == cur:
320 320 self._repo.ui.debug("updating subrepo %s\n" % relpath(self))
321 321 hg.update(self._repo, state[1])
322 322 elif anc == dst:
323 323 self._repo.ui.debug("skipping subrepo %s\n" % relpath(self))
324 324 else:
325 325 self._repo.ui.debug("merging subrepo %s\n" % relpath(self))
326 326 hg.merge(self._repo, state[1], remind=False)
327 327
328 328 def push(self, force):
329 329 # push subrepos depth-first for coherent ordering
330 330 c = self._repo['']
331 331 subs = c.substate # only repos that are committed
332 332 for s in sorted(subs):
333 333 if not c.sub(s).push(force):
334 334 return False
335 335
336 336 dsturl = _abssource(self._repo, True)
337 337 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
338 338 (relpath(self), dsturl))
339 339 other = hg.repository(self._repo.ui, dsturl)
340 340 return self._repo.push(other, force)
341 341
342 342 class svnsubrepo(abstractsubrepo):
343 343 def __init__(self, ctx, path, state):
344 344 self._path = path
345 345 self._state = state
346 346 self._ctx = ctx
347 347 self._ui = ctx._repo.ui
348 348
349 349 def _svncommand(self, commands, filename=''):
350 350 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
351 351 cmd = ['svn'] + commands + [path]
352 352 cmd = [util.shellquote(arg) for arg in cmd]
353 353 cmd = util.quotecommand(' '.join(cmd))
354 354 env = dict(os.environ)
355 355 # Avoid localized output, preserve current locale for everything else.
356 356 env['LC_MESSAGES'] = 'C'
357 357 write, read, err = util.popen3(cmd, env=env, newlines=True)
358 358 retdata = read.read()
359 359 err = err.read().strip()
360 360 if err:
361 361 raise util.Abort(err)
362 362 return retdata
363 363
364 364 def _wcrev(self):
365 365 output = self._svncommand(['info', '--xml'])
366 366 doc = xml.dom.minidom.parseString(output)
367 367 entries = doc.getElementsByTagName('entry')
368 368 if not entries:
369 369 return 0
370 370 return int(entries[0].getAttribute('revision') or 0)
371 371
372 372 def _wcchanged(self):
373 373 """Return (changes, extchanges) where changes is True
374 374 if the working directory was changed, and extchanges is
375 375 True if any of these changes concern an external entry.
376 376 """
377 377 output = self._svncommand(['status', '--xml'])
378 378 externals, changes = [], []
379 379 doc = xml.dom.minidom.parseString(output)
380 380 for e in doc.getElementsByTagName('entry'):
381 381 s = e.getElementsByTagName('wc-status')
382 382 if not s:
383 383 continue
384 384 item = s[0].getAttribute('item')
385 385 props = s[0].getAttribute('props')
386 386 path = e.getAttribute('path')
387 387 if item == 'external':
388 388 externals.append(path)
389 389 if (item not in ('', 'normal', 'unversioned', 'external')
390 390 or props not in ('', 'none')):
391 391 changes.append(path)
392 392 for path in changes:
393 393 for ext in externals:
394 394 if path == ext or path.startswith(ext + os.sep):
395 395 return True, True
396 396 return bool(changes), False
397 397
398 398 def dirty(self):
399 399 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
400 400 return False
401 401 return True
402 402
403 403 def commit(self, text, user, date):
404 404 # user and date are out of our hands since svn is centralized
405 405 changed, extchanged = self._wcchanged()
406 406 if not changed:
407 407 return self._wcrev()
408 408 if extchanged:
409 409 # Do not try to commit externals
410 410 raise util.Abort(_('cannot commit svn externals'))
411 411 commitinfo = self._svncommand(['commit', '-m', text])
412 412 self._ui.status(commitinfo)
413 newrev = re.search('Committed revision ([\d]+).', commitinfo)
413 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
414 414 if not newrev:
415 415 raise util.Abort(commitinfo.splitlines()[-1])
416 416 newrev = newrev.groups()[0]
417 417 self._ui.status(self._svncommand(['update', '-r', newrev]))
418 418 return newrev
419 419
420 420 def remove(self):
421 421 if self.dirty():
422 422 self._ui.warn(_('not removing repo %s because '
423 423 'it has changes.\n' % self._path))
424 424 return
425 425 self._ui.note(_('removing subrepo %s\n') % self._path)
426 426 shutil.rmtree(self._ctx.repo.join(self._path))
427 427
428 428 def get(self, state):
429 429 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
430 if not re.search('Checked out revision [\d]+.', status):
430 if not re.search('Checked out revision [0-9]+.', status):
431 431 raise util.Abort(status.splitlines()[-1])
432 432 self._ui.status(status)
433 433
434 434 def merge(self, state):
435 435 old = int(self._state[1])
436 436 new = int(state[1])
437 437 if new > old:
438 438 self.get(state)
439 439
440 440 def push(self, force):
441 441 # push is a no-op for SVN
442 442 return True
443 443
444 444 types = {
445 445 'hg': hgsubrepo,
446 446 'svn': svnsubrepo,
447 447 }
General Comments 0
You need to be logged in to leave comments. Login now