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