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