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