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