##// END OF EJS Templates
subrepo: handle svn externals and meta changes (issue1982)...
Patrick Mezard -
r10273:e898bc78 default
parent child Browse files
Show More
@@ -1,342 +1,360 b''
1 1 # subrepo.py - sub-repository handling for Mercurial
2 2 #
3 3 # Copyright 2006, 2007 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
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, 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)
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') % t)
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 180 if os.path.exists(os.path.join(root, '.hg')):
181 181 self._repo = hg.repository(r.ui, root)
182 182 else:
183 183 util.makedirs(root)
184 184 self._repo = hg.repository(r.ui, root, create=True)
185 185 f = file(os.path.join(root, '.hg', 'hgrc'), 'w')
186 186 f.write('[paths]\ndefault = %s\n' % state[0])
187 187 f.close()
188 188 self._repo._subparent = r
189 189 self._repo._subsource = state[0]
190 190
191 191 def dirty(self):
192 192 r = self._state[1]
193 193 if r == '':
194 194 return True
195 195 w = self._repo[None]
196 196 if w.p1() != self._repo[r]: # version checked out changed
197 197 return True
198 198 return w.dirty() # working directory changed
199 199
200 200 def commit(self, text, user, date):
201 201 self._repo.ui.debug("committing subrepo %s\n" % self._path)
202 202 n = self._repo.commit(text, user, date)
203 203 if not n:
204 204 return self._repo['.'].hex() # different version checked out
205 205 return node.hex(n)
206 206
207 207 def remove(self):
208 208 # we can't fully delete the repository as it may contain
209 209 # local-only history
210 210 self._repo.ui.note(_('removing subrepo %s\n') % self._path)
211 211 hg.clean(self._repo, node.nullid, False)
212 212
213 213 def _get(self, state):
214 214 source, revision, kind = state
215 215 try:
216 216 self._repo.lookup(revision)
217 217 except error.RepoError:
218 218 self._repo._subsource = source
219 219 self._repo.ui.status(_('pulling subrepo %s\n') % self._path)
220 220 srcurl = _abssource(self._repo)
221 221 other = hg.repository(self._repo.ui, srcurl)
222 222 self._repo.pull(other)
223 223
224 224 def get(self, state):
225 225 self._get(state)
226 226 source, revision, kind = state
227 227 self._repo.ui.debug("getting subrepo %s\n" % self._path)
228 228 hg.clean(self._repo, revision, False)
229 229
230 230 def merge(self, state):
231 231 self._get(state)
232 232 cur = self._repo['.']
233 233 dst = self._repo[state[1]]
234 234 anc = dst.ancestor(cur)
235 235 if anc == cur:
236 236 self._repo.ui.debug("updating subrepo %s\n" % self._path)
237 237 hg.update(self._repo, state[1])
238 238 elif anc == dst:
239 239 self._repo.ui.debug("skipping subrepo %s\n" % self._path)
240 240 else:
241 241 self._repo.ui.debug("merging subrepo %s\n" % self._path)
242 242 hg.merge(self._repo, state[1], remind=False)
243 243
244 244 def push(self, force):
245 245 # push subrepos depth-first for coherent ordering
246 246 c = self._repo['']
247 247 subs = c.substate # only repos that are committed
248 248 for s in sorted(subs):
249 249 c.sub(s).push(force)
250 250
251 251 self._repo.ui.status(_('pushing subrepo %s\n') % self._path)
252 252 dsturl = _abssource(self._repo, True)
253 253 other = hg.repository(self._repo.ui, dsturl)
254 254 self._repo.push(other, force)
255 255
256 256 class svnsubrepo(object):
257 257 def __init__(self, ctx, path, state):
258 258 self._path = path
259 259 self._state = state
260 260 self._ctx = ctx
261 261 self._ui = ctx._repo.ui
262 262
263 263 def _svncommand(self, commands):
264 264 cmd = ['svn'] + commands + [self._path]
265 265 cmd = [util.shellquote(arg) for arg in cmd]
266 266 cmd = util.quotecommand(' '.join(cmd))
267 267 env = dict(os.environ)
268 268 # Avoid localized output, preserve current locale for everything else.
269 269 env['LC_MESSAGES'] = 'C'
270 270 write, read, err = util.popen3(cmd, env=env, newlines=True)
271 271 retdata = read.read()
272 272 err = err.read().strip()
273 273 if err:
274 274 raise util.Abort(err)
275 275 return retdata
276 276
277 277 def _wcrev(self):
278 278 output = self._svncommand(['info', '--xml'])
279 279 doc = xml.dom.minidom.parseString(output)
280 280 entries = doc.getElementsByTagName('entry')
281 281 if not entries:
282 282 return 0
283 283 return int(entries[0].getAttribute('revision') or 0)
284 284
285 def _wcclean(self):
285 def _wcchanged(self):
286 """Return (changes, extchanges) where changes is True
287 if the working directory was changed, and extchanges is
288 True if any of these changes concern an external entry.
289 """
286 290 output = self._svncommand(['status', '--xml'])
291 externals, changes = [], []
287 292 doc = xml.dom.minidom.parseString(output)
288 for s in doc.getElementsByTagName('wc-status'):
289 st = s.getAttribute('item')
290 if st and st != 'unversioned':
291 return False
292 props = s.getAttribute('props')
293 if props and props != 'none':
294 return False
295 return True
293 for e in doc.getElementsByTagName('entry'):
294 s = e.getElementsByTagName('wc-status')
295 if not s:
296 continue
297 item = s[0].getAttribute('item')
298 props = s[0].getAttribute('props')
299 path = e.getAttribute('path')
300 if item == 'external':
301 externals.append(path)
302 if (item not in ('', 'normal', 'unversioned', 'external')
303 or props not in ('', 'none')):
304 changes.append(path)
305 for path in changes:
306 for ext in externals:
307 if path == ext or path.startswith(ext + os.sep):
308 return True, True
309 return bool(changes), False
296 310
297 311 def dirty(self):
298 if self._wcrev() == self._state[1] and self._wcclean():
312 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
299 313 return False
300 314 return True
301 315
302 316 def commit(self, text, user, date):
303 317 # user and date are out of our hands since svn is centralized
304 if self._wcclean():
318 changed, extchanged = self._wcchanged()
319 if not changed:
305 320 return self._wcrev()
321 if extchanged:
322 # Do not try to commit externals
323 raise util.Abort(_('cannot commit svn externals'))
306 324 commitinfo = self._svncommand(['commit', '-m', text])
307 325 self._ui.status(commitinfo)
308 326 newrev = re.search('Committed revision ([\d]+).', commitinfo)
309 327 if not newrev:
310 328 raise util.Abort(commitinfo.splitlines()[-1])
311 329 newrev = newrev.groups()[0]
312 330 self._ui.status(self._svncommand(['update', '-r', newrev]))
313 331 return newrev
314 332
315 333 def remove(self):
316 334 if self.dirty():
317 335 self._repo.ui.warn(_('not removing repo %s because '
318 336 'it has changes.\n' % self._path))
319 337 return
320 338 self._repo.ui.note('removing subrepo %s\n' % self._path)
321 339 shutil.rmtree(self._ctx.repo.join(self._path))
322 340
323 341 def get(self, state):
324 342 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
325 343 if not re.search('Checked out revision [\d]+.', status):
326 344 raise util.Abort(status.splitlines()[-1])
327 345 self._ui.status(status)
328 346
329 347 def merge(self, state):
330 348 old = int(self._state[1])
331 349 new = int(state[1])
332 350 if new > old:
333 351 self.get(state)
334 352
335 353 def push(self, force):
336 354 # nothing for svn
337 355 pass
338 356
339 357 types = {
340 358 'hg': hgsubrepo,
341 359 'svn': svnsubrepo,
342 360 }
@@ -1,78 +1,106 b''
1 1 #!/bin/sh
2 2
3 3 "$TESTDIR/hghave" svn || exit 80
4 4
5 5 fix_path()
6 6 {
7 7 tr '\\' /
8 8 }
9 9
10 10 escapedwd=`pwd | fix_path`
11 11 # SVN wants all paths to start with a slash. Unfortunately,
12 12 # Windows ones don't. Handle that.
13 13 expr $escapedwd : "\/" > /dev/null
14 14 if [ $? -ne 0 ]; then
15 15 escapedwd='/'$escapedwd
16 16 fi
17 17 filterpath="sed s|$escapedwd|/root|"
18 18
19 19 echo % create subversion repo
20 20
21 21 SVNREPO="file://$escapedwd/svn-repo"
22 22 WCROOT="`pwd`/svn-wc"
23 23 svnadmin create svn-repo
24 24 svn co $SVNREPO svn-wc
25 25 cd svn-wc
26 echo alpha > alpha
27 svn add alpha
26 mkdir src
27 echo alpha > src/alpha
28 svn add src
29 mkdir externals
30 echo other > externals/other
31 svn add externals
28 32 svn ci -m 'Add alpha'
33 svn up
34 cat > extdef <<EOF
35 externals -r1 $SVNREPO/externals
36 EOF
37 svn propset -F extdef svn:externals src
38 svn ci -m 'Setting externals'
29 39 cd ..
30 40
31 41 echo % create hg repo
32 42 mkdir sub
33 43 cd sub
34 44 hg init t
35 45 cd t
36 46
37 47 echo % first revision, no sub
38 48 echo a > a
39 49 hg ci -Am0
40 50
41 51 echo % add first svn sub
42 echo "s = [svn]$SVNREPO" >> .hgsub
43 svn co --quiet $SVNREPO s
52 echo "s = [svn]$SVNREPO/src" >> .hgsub
53 svn co --quiet $SVNREPO/src s
44 54 hg add .hgsub
45 55 hg ci -m1
46 56 echo % debugsub
47 57 hg debugsub | $filterpath
48 58
49 59 echo
50 60 echo % change file in svn and hg, commit
51 61 echo a >> a
52 62 echo alpha >> s/alpha
53 63 hg commit -m 'Message!'
54 64 hg debugsub | $filterpath
55 65
56 66 echo
57 67 echo a > s/a
58 68 echo % should be empty despite change to s/a
59 69 hg st
60 70
61 71 echo
62 72 echo % add a commit from svn
63 cd "$WCROOT"
73 cd "$WCROOT"/src
64 74 svn up
65 75 echo xyz >> alpha
76 svn propset svn:mime-type 'text/xml' alpha
66 77 svn ci -m 'amend a from svn'
67 cd ../sub/t
78 cd ../../sub/t
79
68 80 echo % this commit from hg will fail
69 81 echo zzz >> s/alpha
70 82 hg ci -m 'amend alpha from hg'
83 svn revert -q s/alpha
84
85 echo % this commit fails because of meta changes
86 svn propset svn:mime-type 'text/html' s/alpha
87 hg ci -m 'amend alpha from hg'
88 svn revert -q s/alpha
89
90 echo % this commit fails because of externals changes
91 echo zzz > s/externals/other
92 hg ci -m 'amend externals from hg'
93 svn revert -q s/externals/other
94
95 echo % this commit fails because of externals meta changes
96 svn propset svn:mime-type 'text/html' s/externals/other
97 hg ci -m 'amend externals from hg'
98 svn revert -q s/externals/other
71 99
72 100 echo
73 101 echo % clone
74 102 cd ..
75 103 hg clone t tc | fix_path
76 104 cd tc
77 105 echo % debugsub in clone
78 106 hg debugsub | $filterpath
@@ -1,48 +1,87 b''
1 1 % create subversion repo
2 2 Checked out revision 0.
3 A alpha
4 Adding alpha
5 Transmitting file data .
3 A src
4 A src/alpha
5 A externals
6 A externals/other
7 Adding externals
8 Adding externals/other
9 Adding src
10 Adding src/alpha
11 Transmitting file data ..
6 12 Committed revision 1.
13 At revision 1.
14 property 'svn:externals' set on 'src'
15 Sending src
16
17 Committed revision 2.
7 18 % create hg repo
8 19 % first revision, no sub
9 20 adding a
10 21 % add first svn sub
11 22 committing subrepository s
12 23 % debugsub
13 24 path s
14 source file:///root/svn-repo
15 revision 1
25 source file:///root/svn-repo/src
26 revision 2
16 27
17 28 % change file in svn and hg, commit
18 29 committing subrepository s
19 30 Sending s/alpha
20 31 Transmitting file data .
21 Committed revision 2.
22 At revision 2.
32 Committed revision 3.
33
34 Fetching external item into 's/externals'
35 External at revision 1.
36
37 At revision 3.
23 38 path s
24 source file:///root/svn-repo
25 revision 2
39 source file:///root/svn-repo/src
40 revision 3
26 41
27 42 % should be empty despite change to s/a
28 43
29 44 % add a commit from svn
30 45 U alpha
31 Updated to revision 2.
32 Sending alpha
46
47 Fetching external item into 'externals'
48 A externals/other
49 Updated external to revision 1.
50
51 Updated to revision 3.
52 property 'svn:mime-type' set on 'alpha'
53 Sending src/alpha
33 54 Transmitting file data .
34 Committed revision 3.
55 Committed revision 4.
35 56 % this commit from hg will fail
36 57 committing subrepository s
37 58 abort: svn: Commit failed (details follow):
38 svn: File '/alpha' is out of date
59 svn: File '/src/alpha' is out of date
60 % this commit fails because of meta changes
61 property 'svn:mime-type' set on 's/alpha'
62 committing subrepository s
63 abort: svn: Commit failed (details follow):
64 svn: File '/src/alpha' is out of date
65 % this commit fails because of externals changes
66 committing subrepository s
67 abort: cannot commit svn externals
68 % this commit fails because of externals meta changes
69 property 'svn:mime-type' set on 's/externals/other'
70 committing subrepository s
71 abort: cannot commit svn externals
39 72
40 73 % clone
41 74 updating to branch default
42 75 A s/alpha
43 Checked out revision 2.
76 U s
77
78 Fetching external item into 's/externals'
79 A s/externals/other
80 Checked out external at revision 1.
81
82 Checked out revision 3.
44 83 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
45 84 % debugsub in clone
46 85 path s
47 source file:///root/svn-repo
48 revision 2
86 source file:///root/svn-repo/src
87 revision 3
General Comments 0
You need to be logged in to leave comments. Login now