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