##// END OF EJS Templates
subrepo: handle diff with working copy...
Martin Geisler -
r12210:21eb85e9 default
parent child Browse files
Show More
@@ -1,495 +1,496 b''
1 # subrepo.py - sub-repository handling for Mercurial
1 # subrepo.py - sub-repository handling for Mercurial
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
9 from i18n import _
9 from i18n import _
10 import config, util, node, error, cmdutil
10 import config, util, node, error, cmdutil
11 hg = None
11 hg = None
12
12
13 nullstate = ('', '', 'empty')
13 nullstate = ('', '', 'empty')
14
14
15 def state(ctx, ui):
15 def state(ctx, ui):
16 """return a state dict, mapping subrepo paths configured in .hgsub
16 """return a state dict, mapping subrepo paths configured in .hgsub
17 to tuple: (source from .hgsub, revision from .hgsubstate, kind
17 to tuple: (source from .hgsub, revision from .hgsubstate, kind
18 (key in types dict))
18 (key in types dict))
19 """
19 """
20 p = config.config()
20 p = config.config()
21 def read(f, sections=None, remap=None):
21 def read(f, sections=None, remap=None):
22 if f in ctx:
22 if f in ctx:
23 p.parse(f, ctx[f].data(), sections, remap, read)
23 p.parse(f, ctx[f].data(), sections, remap, read)
24 else:
24 else:
25 raise util.Abort(_("subrepo spec file %s not found") % f)
25 raise util.Abort(_("subrepo spec file %s not found") % f)
26
26
27 if '.hgsub' in ctx:
27 if '.hgsub' in ctx:
28 read('.hgsub')
28 read('.hgsub')
29
29
30 for path, src in ui.configitems('subpaths'):
30 for path, src in ui.configitems('subpaths'):
31 p.set('subpaths', path, src, ui.configsource('subpaths', path))
31 p.set('subpaths', path, src, ui.configsource('subpaths', path))
32
32
33 rev = {}
33 rev = {}
34 if '.hgsubstate' in ctx:
34 if '.hgsubstate' in ctx:
35 try:
35 try:
36 for l in ctx['.hgsubstate'].data().splitlines():
36 for l in ctx['.hgsubstate'].data().splitlines():
37 revision, path = l.split(" ", 1)
37 revision, path = l.split(" ", 1)
38 rev[path] = revision
38 rev[path] = revision
39 except IOError, err:
39 except IOError, err:
40 if err.errno != errno.ENOENT:
40 if err.errno != errno.ENOENT:
41 raise
41 raise
42
42
43 state = {}
43 state = {}
44 for path, src in p[''].items():
44 for path, src in p[''].items():
45 kind = 'hg'
45 kind = 'hg'
46 if src.startswith('['):
46 if src.startswith('['):
47 if ']' not in src:
47 if ']' not in src:
48 raise util.Abort(_('missing ] in subrepo source'))
48 raise util.Abort(_('missing ] in subrepo source'))
49 kind, src = src.split(']', 1)
49 kind, src = src.split(']', 1)
50 kind = kind[1:]
50 kind = kind[1:]
51
51
52 for pattern, repl in p.items('subpaths'):
52 for pattern, repl in p.items('subpaths'):
53 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
53 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
54 # does a string decode.
54 # does a string decode.
55 repl = repl.encode('string-escape')
55 repl = repl.encode('string-escape')
56 # However, we still want to allow back references to go
56 # However, we still want to allow back references to go
57 # through unharmed, so we turn r'\\1' into r'\1'. Again,
57 # through unharmed, so we turn r'\\1' into r'\1'. Again,
58 # extra escapes are needed because re.sub string decodes.
58 # extra escapes are needed because re.sub string decodes.
59 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
59 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
60 try:
60 try:
61 src = re.sub(pattern, repl, src, 1)
61 src = re.sub(pattern, repl, src, 1)
62 except re.error, e:
62 except re.error, e:
63 raise util.Abort(_("bad subrepository pattern in %s: %s")
63 raise util.Abort(_("bad subrepository pattern in %s: %s")
64 % (p.source('subpaths', pattern), e))
64 % (p.source('subpaths', pattern), e))
65
65
66 state[path] = (src.strip(), rev.get(path, ''), kind)
66 state[path] = (src.strip(), rev.get(path, ''), kind)
67
67
68 return state
68 return state
69
69
70 def writestate(repo, state):
70 def writestate(repo, state):
71 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
71 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
72 repo.wwrite('.hgsubstate',
72 repo.wwrite('.hgsubstate',
73 ''.join(['%s %s\n' % (state[s][1], s)
73 ''.join(['%s %s\n' % (state[s][1], s)
74 for s in sorted(state)]), '')
74 for s in sorted(state)]), '')
75
75
76 def submerge(repo, wctx, mctx, actx):
76 def submerge(repo, wctx, mctx, actx):
77 """delegated from merge.applyupdates: merging of .hgsubstate file
77 """delegated from merge.applyupdates: merging of .hgsubstate file
78 in working context, merging context and ancestor context"""
78 in working context, merging context and ancestor context"""
79 if mctx == actx: # backwards?
79 if mctx == actx: # backwards?
80 actx = wctx.p1()
80 actx = wctx.p1()
81 s1 = wctx.substate
81 s1 = wctx.substate
82 s2 = mctx.substate
82 s2 = mctx.substate
83 sa = actx.substate
83 sa = actx.substate
84 sm = {}
84 sm = {}
85
85
86 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
86 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
87
87
88 def debug(s, msg, r=""):
88 def debug(s, msg, r=""):
89 if r:
89 if r:
90 r = "%s:%s:%s" % r
90 r = "%s:%s:%s" % r
91 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
91 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
92
92
93 for s, l in s1.items():
93 for s, l in s1.items():
94 a = sa.get(s, nullstate)
94 a = sa.get(s, nullstate)
95 ld = l # local state with possible dirty flag for compares
95 ld = l # local state with possible dirty flag for compares
96 if wctx.sub(s).dirty():
96 if wctx.sub(s).dirty():
97 ld = (l[0], l[1] + "+")
97 ld = (l[0], l[1] + "+")
98 if wctx == actx: # overwrite
98 if wctx == actx: # overwrite
99 a = ld
99 a = ld
100
100
101 if s in s2:
101 if s in s2:
102 r = s2[s]
102 r = s2[s]
103 if ld == r or r == a: # no change or local is newer
103 if ld == r or r == a: # no change or local is newer
104 sm[s] = l
104 sm[s] = l
105 continue
105 continue
106 elif ld == a: # other side changed
106 elif ld == a: # other side changed
107 debug(s, "other changed, get", r)
107 debug(s, "other changed, get", r)
108 wctx.sub(s).get(r)
108 wctx.sub(s).get(r)
109 sm[s] = r
109 sm[s] = r
110 elif ld[0] != r[0]: # sources differ
110 elif ld[0] != r[0]: # sources differ
111 if repo.ui.promptchoice(
111 if repo.ui.promptchoice(
112 _(' subrepository sources for %s differ\n'
112 _(' subrepository sources for %s differ\n'
113 'use (l)ocal source (%s) or (r)emote source (%s)?')
113 'use (l)ocal source (%s) or (r)emote source (%s)?')
114 % (s, l[0], r[0]),
114 % (s, l[0], r[0]),
115 (_('&Local'), _('&Remote')), 0):
115 (_('&Local'), _('&Remote')), 0):
116 debug(s, "prompt changed, get", r)
116 debug(s, "prompt changed, get", r)
117 wctx.sub(s).get(r)
117 wctx.sub(s).get(r)
118 sm[s] = r
118 sm[s] = r
119 elif ld[1] == a[1]: # local side is unchanged
119 elif ld[1] == a[1]: # local side is unchanged
120 debug(s, "other side changed, get", r)
120 debug(s, "other side changed, get", r)
121 wctx.sub(s).get(r)
121 wctx.sub(s).get(r)
122 sm[s] = r
122 sm[s] = r
123 else:
123 else:
124 debug(s, "both sides changed, merge with", r)
124 debug(s, "both sides changed, merge with", r)
125 wctx.sub(s).merge(r)
125 wctx.sub(s).merge(r)
126 sm[s] = l
126 sm[s] = l
127 elif ld == a: # remote removed, local unchanged
127 elif ld == a: # remote removed, local unchanged
128 debug(s, "remote removed, remove")
128 debug(s, "remote removed, remove")
129 wctx.sub(s).remove()
129 wctx.sub(s).remove()
130 else:
130 else:
131 if repo.ui.promptchoice(
131 if repo.ui.promptchoice(
132 _(' local changed subrepository %s which remote removed\n'
132 _(' local changed subrepository %s which remote removed\n'
133 'use (c)hanged version or (d)elete?') % s,
133 'use (c)hanged version or (d)elete?') % s,
134 (_('&Changed'), _('&Delete')), 0):
134 (_('&Changed'), _('&Delete')), 0):
135 debug(s, "prompt remove")
135 debug(s, "prompt remove")
136 wctx.sub(s).remove()
136 wctx.sub(s).remove()
137
137
138 for s, r in s2.items():
138 for s, r in s2.items():
139 if s in s1:
139 if s in s1:
140 continue
140 continue
141 elif s not in sa:
141 elif s not in sa:
142 debug(s, "remote added, get", r)
142 debug(s, "remote added, get", r)
143 mctx.sub(s).get(r)
143 mctx.sub(s).get(r)
144 sm[s] = r
144 sm[s] = r
145 elif r != sa[s]:
145 elif r != sa[s]:
146 if repo.ui.promptchoice(
146 if repo.ui.promptchoice(
147 _(' remote changed subrepository %s which local removed\n'
147 _(' remote changed subrepository %s which local removed\n'
148 'use (c)hanged version or (d)elete?') % s,
148 'use (c)hanged version or (d)elete?') % s,
149 (_('&Changed'), _('&Delete')), 0) == 0:
149 (_('&Changed'), _('&Delete')), 0) == 0:
150 debug(s, "prompt recreate", r)
150 debug(s, "prompt recreate", r)
151 wctx.sub(s).get(r)
151 wctx.sub(s).get(r)
152 sm[s] = r
152 sm[s] = r
153
153
154 # record merged .hgsubstate
154 # record merged .hgsubstate
155 writestate(repo, sm)
155 writestate(repo, sm)
156
156
157 def relpath(sub):
157 def relpath(sub):
158 """return path to this subrepo as seen from outermost repo"""
158 """return path to this subrepo as seen from outermost repo"""
159 if not hasattr(sub, '_repo'):
159 if not hasattr(sub, '_repo'):
160 return sub._path
160 return sub._path
161 parent = sub._repo
161 parent = sub._repo
162 while hasattr(parent, '_subparent'):
162 while hasattr(parent, '_subparent'):
163 parent = parent._subparent
163 parent = parent._subparent
164 return sub._repo.root[len(parent.root)+1:]
164 return sub._repo.root[len(parent.root)+1:]
165
165
166 def _abssource(repo, push=False):
166 def _abssource(repo, push=False):
167 """return pull/push path of repo - either based on parent repo
167 """return pull/push path of repo - either based on parent repo
168 .hgsub info or on the subrepos own config"""
168 .hgsub info or on the subrepos own config"""
169 if hasattr(repo, '_subparent'):
169 if hasattr(repo, '_subparent'):
170 source = repo._subsource
170 source = repo._subsource
171 if source.startswith('/') or '://' in source:
171 if source.startswith('/') or '://' in source:
172 return source
172 return source
173 parent = _abssource(repo._subparent, push)
173 parent = _abssource(repo._subparent, push)
174 if '://' in parent:
174 if '://' in parent:
175 if parent[-1] == '/':
175 if parent[-1] == '/':
176 parent = parent[:-1]
176 parent = parent[:-1]
177 r = urlparse.urlparse(parent + '/' + source)
177 r = urlparse.urlparse(parent + '/' + source)
178 r = urlparse.urlunparse((r[0], r[1],
178 r = urlparse.urlunparse((r[0], r[1],
179 posixpath.normpath(r[2]),
179 posixpath.normpath(r[2]),
180 r[3], r[4], r[5]))
180 r[3], r[4], r[5]))
181 return r
181 return r
182 return posixpath.normpath(os.path.join(parent, repo._subsource))
182 return posixpath.normpath(os.path.join(parent, repo._subsource))
183 if push and repo.ui.config('paths', 'default-push'):
183 if push and repo.ui.config('paths', 'default-push'):
184 return repo.ui.config('paths', 'default-push', repo.root)
184 return repo.ui.config('paths', 'default-push', repo.root)
185 return repo.ui.config('paths', 'default', repo.root)
185 return repo.ui.config('paths', 'default', repo.root)
186
186
187 def itersubrepos(ctx1, ctx2):
187 def itersubrepos(ctx1, ctx2):
188 """find subrepos in ctx1 or ctx2"""
188 """find subrepos in ctx1 or ctx2"""
189 # Create a (subpath, ctx) mapping where we prefer subpaths from
189 # Create a (subpath, ctx) mapping where we prefer subpaths from
190 # ctx1. The subpaths from ctx2 are important when the .hgsub file
190 # ctx1. The subpaths from ctx2 are important when the .hgsub file
191 # has been modified (in ctx2) but not yet committed (in ctx1).
191 # has been modified (in ctx2) but not yet committed (in ctx1).
192 subpaths = dict.fromkeys(ctx2.substate, ctx2)
192 subpaths = dict.fromkeys(ctx2.substate, ctx2)
193 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
193 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
194 for subpath, ctx in sorted(subpaths.iteritems()):
194 for subpath, ctx in sorted(subpaths.iteritems()):
195 yield subpath, ctx.sub(subpath)
195 yield subpath, ctx.sub(subpath)
196
196
197 def subrepo(ctx, path):
197 def subrepo(ctx, path):
198 """return instance of the right subrepo class for subrepo in path"""
198 """return instance of the right subrepo class for subrepo in path"""
199 # subrepo inherently violates our import layering rules
199 # subrepo inherently violates our import layering rules
200 # because it wants to make repo objects from deep inside the stack
200 # because it wants to make repo objects from deep inside the stack
201 # so we manually delay the circular imports to not break
201 # so we manually delay the circular imports to not break
202 # scripts that don't use our demand-loading
202 # scripts that don't use our demand-loading
203 global hg
203 global hg
204 import hg as h
204 import hg as h
205 hg = h
205 hg = h
206
206
207 util.path_auditor(ctx._repo.root)(path)
207 util.path_auditor(ctx._repo.root)(path)
208 state = ctx.substate.get(path, nullstate)
208 state = ctx.substate.get(path, nullstate)
209 if state[2] not in types:
209 if state[2] not in types:
210 raise util.Abort(_('unknown subrepo type %s') % state[2])
210 raise util.Abort(_('unknown subrepo type %s') % state[2])
211 return types[state[2]](ctx, path, state[:2])
211 return types[state[2]](ctx, path, state[:2])
212
212
213 # subrepo classes need to implement the following abstract class:
213 # subrepo classes need to implement the following abstract class:
214
214
215 class abstractsubrepo(object):
215 class abstractsubrepo(object):
216
216
217 def dirty(self):
217 def dirty(self):
218 """returns true if the dirstate of the subrepo does not match
218 """returns true if the dirstate of the subrepo does not match
219 current stored state
219 current stored state
220 """
220 """
221 raise NotImplementedError
221 raise NotImplementedError
222
222
223 def checknested(path):
223 def checknested(path):
224 """check if path is a subrepository within this repository"""
224 """check if path is a subrepository within this repository"""
225 return False
225 return False
226
226
227 def commit(self, text, user, date):
227 def commit(self, text, user, date):
228 """commit the current changes to the subrepo with the given
228 """commit the current changes to the subrepo with the given
229 log message. Use given user and date if possible. Return the
229 log message. Use given user and date if possible. Return the
230 new state of the subrepo.
230 new state of the subrepo.
231 """
231 """
232 raise NotImplementedError
232 raise NotImplementedError
233
233
234 def remove(self):
234 def remove(self):
235 """remove the subrepo
235 """remove the subrepo
236
236
237 (should verify the dirstate is not dirty first)
237 (should verify the dirstate is not dirty first)
238 """
238 """
239 raise NotImplementedError
239 raise NotImplementedError
240
240
241 def get(self, state):
241 def get(self, state):
242 """run whatever commands are needed to put the subrepo into
242 """run whatever commands are needed to put the subrepo into
243 this state
243 this state
244 """
244 """
245 raise NotImplementedError
245 raise NotImplementedError
246
246
247 def merge(self, state):
247 def merge(self, state):
248 """merge currently-saved state with the new state."""
248 """merge currently-saved state with the new state."""
249 raise NotImplementedError
249 raise NotImplementedError
250
250
251 def push(self, force):
251 def push(self, force):
252 """perform whatever action is analogous to 'hg push'
252 """perform whatever action is analogous to 'hg push'
253
253
254 This may be a no-op on some systems.
254 This may be a no-op on some systems.
255 """
255 """
256 raise NotImplementedError
256 raise NotImplementedError
257
257
258
258
259 def status(self, rev2, **opts):
259 def status(self, rev2, **opts):
260 return [], [], [], [], [], [], []
260 return [], [], [], [], [], [], []
261
261
262 def diff(self, diffopts, node2, match, prefix, **opts):
262 def diff(self, diffopts, node2, match, prefix, **opts):
263 pass
263 pass
264
264
265 class hgsubrepo(abstractsubrepo):
265 class hgsubrepo(abstractsubrepo):
266 def __init__(self, ctx, path, state):
266 def __init__(self, ctx, path, state):
267 self._path = path
267 self._path = path
268 self._state = state
268 self._state = state
269 r = ctx._repo
269 r = ctx._repo
270 root = r.wjoin(path)
270 root = r.wjoin(path)
271 create = False
271 create = False
272 if not os.path.exists(os.path.join(root, '.hg')):
272 if not os.path.exists(os.path.join(root, '.hg')):
273 create = True
273 create = True
274 util.makedirs(root)
274 util.makedirs(root)
275 self._repo = hg.repository(r.ui, root, create=create)
275 self._repo = hg.repository(r.ui, root, create=create)
276 self._repo._subparent = r
276 self._repo._subparent = r
277 self._repo._subsource = state[0]
277 self._repo._subsource = state[0]
278
278
279 if create:
279 if create:
280 fp = self._repo.opener("hgrc", "w", text=True)
280 fp = self._repo.opener("hgrc", "w", text=True)
281 fp.write('[paths]\n')
281 fp.write('[paths]\n')
282
282
283 def addpathconfig(key, value):
283 def addpathconfig(key, value):
284 fp.write('%s = %s\n' % (key, value))
284 fp.write('%s = %s\n' % (key, value))
285 self._repo.ui.setconfig('paths', key, value)
285 self._repo.ui.setconfig('paths', key, value)
286
286
287 defpath = _abssource(self._repo)
287 defpath = _abssource(self._repo)
288 defpushpath = _abssource(self._repo, True)
288 defpushpath = _abssource(self._repo, True)
289 addpathconfig('default', defpath)
289 addpathconfig('default', defpath)
290 if defpath != defpushpath:
290 if defpath != defpushpath:
291 addpathconfig('default-push', defpushpath)
291 addpathconfig('default-push', defpushpath)
292 fp.close()
292 fp.close()
293
293
294 def status(self, rev2, **opts):
294 def status(self, rev2, **opts):
295 try:
295 try:
296 rev1 = self._state[1]
296 rev1 = self._state[1]
297 ctx1 = self._repo[rev1]
297 ctx1 = self._repo[rev1]
298 ctx2 = self._repo[rev2]
298 ctx2 = self._repo[rev2]
299 return self._repo.status(ctx1, ctx2, **opts)
299 return self._repo.status(ctx1, ctx2, **opts)
300 except error.RepoLookupError, inst:
300 except error.RepoLookupError, inst:
301 self._repo.ui.warn(_("warning: %s in %s\n")
301 self._repo.ui.warn(_("warning: %s in %s\n")
302 % (inst, relpath(self)))
302 % (inst, relpath(self)))
303 return [], [], [], [], [], [], []
303 return [], [], [], [], [], [], []
304
304
305 def diff(self, diffopts, node2, match, prefix, **opts):
305 def diff(self, diffopts, node2, match, prefix, **opts):
306 try:
306 try:
307 node1 = node.bin(self._state[1])
307 node1 = node.bin(self._state[1])
308 # We currently expect node2 to come from substate and be
308 # We currently expect node2 to come from substate and be
309 # in hex format
309 # in hex format
310 node2 = node.bin(node2)
310 if node2 is not None:
311 node2 = node.bin(node2)
311 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
312 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
312 node1, node2, match,
313 node1, node2, match,
313 prefix=os.path.join(prefix, self._path),
314 prefix=os.path.join(prefix, self._path),
314 listsubrepos=True, **opts)
315 listsubrepos=True, **opts)
315 except error.RepoLookupError, inst:
316 except error.RepoLookupError, inst:
316 self._repo.ui.warn(_("warning: %s in %s\n")
317 self._repo.ui.warn(_("warning: %s in %s\n")
317 % (inst, relpath(self)))
318 % (inst, relpath(self)))
318
319
319 def dirty(self):
320 def dirty(self):
320 r = self._state[1]
321 r = self._state[1]
321 if r == '':
322 if r == '':
322 return True
323 return True
323 w = self._repo[None]
324 w = self._repo[None]
324 if w.p1() != self._repo[r]: # version checked out change
325 if w.p1() != self._repo[r]: # version checked out change
325 return True
326 return True
326 return w.dirty() # working directory changed
327 return w.dirty() # working directory changed
327
328
328 def checknested(self, path):
329 def checknested(self, path):
329 return self._repo._checknested(self._repo.wjoin(path))
330 return self._repo._checknested(self._repo.wjoin(path))
330
331
331 def commit(self, text, user, date):
332 def commit(self, text, user, date):
332 self._repo.ui.debug("committing subrepo %s\n" % relpath(self))
333 self._repo.ui.debug("committing subrepo %s\n" % relpath(self))
333 n = self._repo.commit(text, user, date)
334 n = self._repo.commit(text, user, date)
334 if not n:
335 if not n:
335 return self._repo['.'].hex() # different version checked out
336 return self._repo['.'].hex() # different version checked out
336 return node.hex(n)
337 return node.hex(n)
337
338
338 def remove(self):
339 def remove(self):
339 # we can't fully delete the repository as it may contain
340 # we can't fully delete the repository as it may contain
340 # local-only history
341 # local-only history
341 self._repo.ui.note(_('removing subrepo %s\n') % relpath(self))
342 self._repo.ui.note(_('removing subrepo %s\n') % relpath(self))
342 hg.clean(self._repo, node.nullid, False)
343 hg.clean(self._repo, node.nullid, False)
343
344
344 def _get(self, state):
345 def _get(self, state):
345 source, revision, kind = state
346 source, revision, kind = state
346 try:
347 try:
347 self._repo.lookup(revision)
348 self._repo.lookup(revision)
348 except error.RepoError:
349 except error.RepoError:
349 self._repo._subsource = source
350 self._repo._subsource = source
350 srcurl = _abssource(self._repo)
351 srcurl = _abssource(self._repo)
351 self._repo.ui.status(_('pulling subrepo %s from %s\n')
352 self._repo.ui.status(_('pulling subrepo %s from %s\n')
352 % (relpath(self), srcurl))
353 % (relpath(self), srcurl))
353 other = hg.repository(self._repo.ui, srcurl)
354 other = hg.repository(self._repo.ui, srcurl)
354 self._repo.pull(other)
355 self._repo.pull(other)
355
356
356 def get(self, state):
357 def get(self, state):
357 self._get(state)
358 self._get(state)
358 source, revision, kind = state
359 source, revision, kind = state
359 self._repo.ui.debug("getting subrepo %s\n" % self._path)
360 self._repo.ui.debug("getting subrepo %s\n" % self._path)
360 hg.clean(self._repo, revision, False)
361 hg.clean(self._repo, revision, False)
361
362
362 def merge(self, state):
363 def merge(self, state):
363 self._get(state)
364 self._get(state)
364 cur = self._repo['.']
365 cur = self._repo['.']
365 dst = self._repo[state[1]]
366 dst = self._repo[state[1]]
366 anc = dst.ancestor(cur)
367 anc = dst.ancestor(cur)
367 if anc == cur:
368 if anc == cur:
368 self._repo.ui.debug("updating subrepo %s\n" % relpath(self))
369 self._repo.ui.debug("updating subrepo %s\n" % relpath(self))
369 hg.update(self._repo, state[1])
370 hg.update(self._repo, state[1])
370 elif anc == dst:
371 elif anc == dst:
371 self._repo.ui.debug("skipping subrepo %s\n" % relpath(self))
372 self._repo.ui.debug("skipping subrepo %s\n" % relpath(self))
372 else:
373 else:
373 self._repo.ui.debug("merging subrepo %s\n" % relpath(self))
374 self._repo.ui.debug("merging subrepo %s\n" % relpath(self))
374 hg.merge(self._repo, state[1], remind=False)
375 hg.merge(self._repo, state[1], remind=False)
375
376
376 def push(self, force):
377 def push(self, force):
377 # push subrepos depth-first for coherent ordering
378 # push subrepos depth-first for coherent ordering
378 c = self._repo['']
379 c = self._repo['']
379 subs = c.substate # only repos that are committed
380 subs = c.substate # only repos that are committed
380 for s in sorted(subs):
381 for s in sorted(subs):
381 if not c.sub(s).push(force):
382 if not c.sub(s).push(force):
382 return False
383 return False
383
384
384 dsturl = _abssource(self._repo, True)
385 dsturl = _abssource(self._repo, True)
385 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
386 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
386 (relpath(self), dsturl))
387 (relpath(self), dsturl))
387 other = hg.repository(self._repo.ui, dsturl)
388 other = hg.repository(self._repo.ui, dsturl)
388 return self._repo.push(other, force)
389 return self._repo.push(other, force)
389
390
390 class svnsubrepo(abstractsubrepo):
391 class svnsubrepo(abstractsubrepo):
391 def __init__(self, ctx, path, state):
392 def __init__(self, ctx, path, state):
392 self._path = path
393 self._path = path
393 self._state = state
394 self._state = state
394 self._ctx = ctx
395 self._ctx = ctx
395 self._ui = ctx._repo.ui
396 self._ui = ctx._repo.ui
396
397
397 def _svncommand(self, commands, filename=''):
398 def _svncommand(self, commands, filename=''):
398 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
399 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
399 cmd = ['svn'] + commands + [path]
400 cmd = ['svn'] + commands + [path]
400 cmd = [util.shellquote(arg) for arg in cmd]
401 cmd = [util.shellquote(arg) for arg in cmd]
401 cmd = util.quotecommand(' '.join(cmd))
402 cmd = util.quotecommand(' '.join(cmd))
402 env = dict(os.environ)
403 env = dict(os.environ)
403 # Avoid localized output, preserve current locale for everything else.
404 # Avoid localized output, preserve current locale for everything else.
404 env['LC_MESSAGES'] = 'C'
405 env['LC_MESSAGES'] = 'C'
405 write, read, err = util.popen3(cmd, env=env, newlines=True)
406 write, read, err = util.popen3(cmd, env=env, newlines=True)
406 retdata = read.read()
407 retdata = read.read()
407 err = err.read().strip()
408 err = err.read().strip()
408 if err:
409 if err:
409 raise util.Abort(err)
410 raise util.Abort(err)
410 return retdata
411 return retdata
411
412
412 def _wcrev(self):
413 def _wcrev(self):
413 output = self._svncommand(['info', '--xml'])
414 output = self._svncommand(['info', '--xml'])
414 doc = xml.dom.minidom.parseString(output)
415 doc = xml.dom.minidom.parseString(output)
415 entries = doc.getElementsByTagName('entry')
416 entries = doc.getElementsByTagName('entry')
416 if not entries:
417 if not entries:
417 return 0
418 return 0
418 return int(entries[0].getAttribute('revision') or 0)
419 return int(entries[0].getAttribute('revision') or 0)
419
420
420 def _wcchanged(self):
421 def _wcchanged(self):
421 """Return (changes, extchanges) where changes is True
422 """Return (changes, extchanges) where changes is True
422 if the working directory was changed, and extchanges is
423 if the working directory was changed, and extchanges is
423 True if any of these changes concern an external entry.
424 True if any of these changes concern an external entry.
424 """
425 """
425 output = self._svncommand(['status', '--xml'])
426 output = self._svncommand(['status', '--xml'])
426 externals, changes = [], []
427 externals, changes = [], []
427 doc = xml.dom.minidom.parseString(output)
428 doc = xml.dom.minidom.parseString(output)
428 for e in doc.getElementsByTagName('entry'):
429 for e in doc.getElementsByTagName('entry'):
429 s = e.getElementsByTagName('wc-status')
430 s = e.getElementsByTagName('wc-status')
430 if not s:
431 if not s:
431 continue
432 continue
432 item = s[0].getAttribute('item')
433 item = s[0].getAttribute('item')
433 props = s[0].getAttribute('props')
434 props = s[0].getAttribute('props')
434 path = e.getAttribute('path')
435 path = e.getAttribute('path')
435 if item == 'external':
436 if item == 'external':
436 externals.append(path)
437 externals.append(path)
437 if (item not in ('', 'normal', 'unversioned', 'external')
438 if (item not in ('', 'normal', 'unversioned', 'external')
438 or props not in ('', 'none')):
439 or props not in ('', 'none')):
439 changes.append(path)
440 changes.append(path)
440 for path in changes:
441 for path in changes:
441 for ext in externals:
442 for ext in externals:
442 if path == ext or path.startswith(ext + os.sep):
443 if path == ext or path.startswith(ext + os.sep):
443 return True, True
444 return True, True
444 return bool(changes), False
445 return bool(changes), False
445
446
446 def dirty(self):
447 def dirty(self):
447 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
448 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
448 return False
449 return False
449 return True
450 return True
450
451
451 def commit(self, text, user, date):
452 def commit(self, text, user, date):
452 # user and date are out of our hands since svn is centralized
453 # user and date are out of our hands since svn is centralized
453 changed, extchanged = self._wcchanged()
454 changed, extchanged = self._wcchanged()
454 if not changed:
455 if not changed:
455 return self._wcrev()
456 return self._wcrev()
456 if extchanged:
457 if extchanged:
457 # Do not try to commit externals
458 # Do not try to commit externals
458 raise util.Abort(_('cannot commit svn externals'))
459 raise util.Abort(_('cannot commit svn externals'))
459 commitinfo = self._svncommand(['commit', '-m', text])
460 commitinfo = self._svncommand(['commit', '-m', text])
460 self._ui.status(commitinfo)
461 self._ui.status(commitinfo)
461 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
462 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
462 if not newrev:
463 if not newrev:
463 raise util.Abort(commitinfo.splitlines()[-1])
464 raise util.Abort(commitinfo.splitlines()[-1])
464 newrev = newrev.groups()[0]
465 newrev = newrev.groups()[0]
465 self._ui.status(self._svncommand(['update', '-r', newrev]))
466 self._ui.status(self._svncommand(['update', '-r', newrev]))
466 return newrev
467 return newrev
467
468
468 def remove(self):
469 def remove(self):
469 if self.dirty():
470 if self.dirty():
470 self._ui.warn(_('not removing repo %s because '
471 self._ui.warn(_('not removing repo %s because '
471 'it has changes.\n' % self._path))
472 'it has changes.\n' % self._path))
472 return
473 return
473 self._ui.note(_('removing subrepo %s\n') % self._path)
474 self._ui.note(_('removing subrepo %s\n') % self._path)
474 shutil.rmtree(self._ctx.repo.join(self._path))
475 shutil.rmtree(self._ctx.repo.join(self._path))
475
476
476 def get(self, state):
477 def get(self, state):
477 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
478 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
478 if not re.search('Checked out revision [0-9]+.', status):
479 if not re.search('Checked out revision [0-9]+.', status):
479 raise util.Abort(status.splitlines()[-1])
480 raise util.Abort(status.splitlines()[-1])
480 self._ui.status(status)
481 self._ui.status(status)
481
482
482 def merge(self, state):
483 def merge(self, state):
483 old = int(self._state[1])
484 old = int(self._state[1])
484 new = int(state[1])
485 new = int(state[1])
485 if new > old:
486 if new > old:
486 self.get(state)
487 self.get(state)
487
488
488 def push(self, force):
489 def push(self, force):
489 # push is a no-op for SVN
490 # push is a no-op for SVN
490 return True
491 return True
491
492
492 types = {
493 types = {
493 'hg': hgsubrepo,
494 'hg': hgsubrepo,
494 'svn': svnsubrepo,
495 'svn': svnsubrepo,
495 }
496 }
General Comments 0
You need to be logged in to leave comments. Login now