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