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