##// END OF EJS Templates
subrepo: do not traceback on .hgsubstate parsing errors...
Patrick Mezard -
r16596:95ca6c8b stable
parent child Browse files
Show More
@@ -1,1237 +1,1242
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, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, scmutil, util, node, error, cmdutil, bookmarks
11 import config, scmutil, util, node, error, cmdutil, bookmarks
12 hg = None
12 hg = None
13 propertycache = util.propertycache
13 propertycache = util.propertycache
14
14
15 nullstate = ('', '', 'empty')
15 nullstate = ('', '', 'empty')
16
16
17 def state(ctx, ui):
17 def state(ctx, ui):
18 """return a state dict, mapping subrepo paths configured in .hgsub
18 """return a state dict, mapping subrepo paths configured in .hgsub
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 to tuple: (source from .hgsub, revision from .hgsubstate, kind
20 (key in types dict))
20 (key in types dict))
21 """
21 """
22 p = config.config()
22 p = config.config()
23 def read(f, sections=None, remap=None):
23 def read(f, sections=None, remap=None):
24 if f in ctx:
24 if f in ctx:
25 try:
25 try:
26 data = ctx[f].data()
26 data = ctx[f].data()
27 except IOError, err:
27 except IOError, err:
28 if err.errno != errno.ENOENT:
28 if err.errno != errno.ENOENT:
29 raise
29 raise
30 # handle missing subrepo spec files as removed
30 # handle missing subrepo spec files as removed
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
32 return
32 return
33 p.parse(f, data, sections, remap, read)
33 p.parse(f, data, sections, remap, read)
34 else:
34 else:
35 raise util.Abort(_("subrepo spec file %s not found") % f)
35 raise util.Abort(_("subrepo spec file %s not found") % f)
36
36
37 if '.hgsub' in ctx:
37 if '.hgsub' in ctx:
38 read('.hgsub')
38 read('.hgsub')
39
39
40 for path, src in ui.configitems('subpaths'):
40 for path, src in ui.configitems('subpaths'):
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41 p.set('subpaths', path, src, ui.configsource('subpaths', path))
42
42
43 rev = {}
43 rev = {}
44 if '.hgsubstate' in ctx:
44 if '.hgsubstate' in ctx:
45 try:
45 try:
46 for l in ctx['.hgsubstate'].data().splitlines():
46 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
47 l = l.lstrip()
47 l = l.lstrip()
48 if not l:
48 if not l:
49 continue
49 continue
50 try:
50 revision, path = l.split(" ", 1)
51 revision, path = l.split(" ", 1)
52 except ValueError:
53 raise util.Abort(_("invalid subrepository revision "
54 "specifier in .hgsubstate line %d")
55 % (i + 1))
51 rev[path] = revision
56 rev[path] = revision
52 except IOError, err:
57 except IOError, err:
53 if err.errno != errno.ENOENT:
58 if err.errno != errno.ENOENT:
54 raise
59 raise
55
60
56 def remap(src):
61 def remap(src):
57 for pattern, repl in p.items('subpaths'):
62 for pattern, repl in p.items('subpaths'):
58 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
59 # does a string decode.
64 # does a string decode.
60 repl = repl.encode('string-escape')
65 repl = repl.encode('string-escape')
61 # However, we still want to allow back references to go
66 # However, we still want to allow back references to go
62 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 # through unharmed, so we turn r'\\1' into r'\1'. Again,
63 # extra escapes are needed because re.sub string decodes.
68 # extra escapes are needed because re.sub string decodes.
64 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
65 try:
70 try:
66 src = re.sub(pattern, repl, src, 1)
71 src = re.sub(pattern, repl, src, 1)
67 except re.error, e:
72 except re.error, e:
68 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 raise util.Abort(_("bad subrepository pattern in %s: %s")
69 % (p.source('subpaths', pattern), e))
74 % (p.source('subpaths', pattern), e))
70 return src
75 return src
71
76
72 state = {}
77 state = {}
73 for path, src in p[''].items():
78 for path, src in p[''].items():
74 kind = 'hg'
79 kind = 'hg'
75 if src.startswith('['):
80 if src.startswith('['):
76 if ']' not in src:
81 if ']' not in src:
77 raise util.Abort(_('missing ] in subrepo source'))
82 raise util.Abort(_('missing ] in subrepo source'))
78 kind, src = src.split(']', 1)
83 kind, src = src.split(']', 1)
79 kind = kind[1:]
84 kind = kind[1:]
80 src = src.lstrip() # strip any extra whitespace after ']'
85 src = src.lstrip() # strip any extra whitespace after ']'
81
86
82 if not util.url(src).isabs():
87 if not util.url(src).isabs():
83 parent = _abssource(ctx._repo, abort=False)
88 parent = _abssource(ctx._repo, abort=False)
84 if parent:
89 if parent:
85 parent = util.url(parent)
90 parent = util.url(parent)
86 parent.path = posixpath.join(parent.path or '', src)
91 parent.path = posixpath.join(parent.path or '', src)
87 parent.path = posixpath.normpath(parent.path)
92 parent.path = posixpath.normpath(parent.path)
88 joined = str(parent)
93 joined = str(parent)
89 # Remap the full joined path and use it if it changes,
94 # Remap the full joined path and use it if it changes,
90 # else remap the original source.
95 # else remap the original source.
91 remapped = remap(joined)
96 remapped = remap(joined)
92 if remapped == joined:
97 if remapped == joined:
93 src = remap(src)
98 src = remap(src)
94 else:
99 else:
95 src = remapped
100 src = remapped
96
101
97 src = remap(src)
102 src = remap(src)
98 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
103 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
99
104
100 return state
105 return state
101
106
102 def writestate(repo, state):
107 def writestate(repo, state):
103 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
108 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
104 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
109 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
105 repo.wwrite('.hgsubstate', ''.join(lines), '')
110 repo.wwrite('.hgsubstate', ''.join(lines), '')
106
111
107 def submerge(repo, wctx, mctx, actx, overwrite):
112 def submerge(repo, wctx, mctx, actx, overwrite):
108 """delegated from merge.applyupdates: merging of .hgsubstate file
113 """delegated from merge.applyupdates: merging of .hgsubstate file
109 in working context, merging context and ancestor context"""
114 in working context, merging context and ancestor context"""
110 if mctx == actx: # backwards?
115 if mctx == actx: # backwards?
111 actx = wctx.p1()
116 actx = wctx.p1()
112 s1 = wctx.substate
117 s1 = wctx.substate
113 s2 = mctx.substate
118 s2 = mctx.substate
114 sa = actx.substate
119 sa = actx.substate
115 sm = {}
120 sm = {}
116
121
117 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
122 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
118
123
119 def debug(s, msg, r=""):
124 def debug(s, msg, r=""):
120 if r:
125 if r:
121 r = "%s:%s:%s" % r
126 r = "%s:%s:%s" % r
122 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
127 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
123
128
124 for s, l in s1.items():
129 for s, l in s1.items():
125 a = sa.get(s, nullstate)
130 a = sa.get(s, nullstate)
126 ld = l # local state with possible dirty flag for compares
131 ld = l # local state with possible dirty flag for compares
127 if wctx.sub(s).dirty():
132 if wctx.sub(s).dirty():
128 ld = (l[0], l[1] + "+")
133 ld = (l[0], l[1] + "+")
129 if wctx == actx: # overwrite
134 if wctx == actx: # overwrite
130 a = ld
135 a = ld
131
136
132 if s in s2:
137 if s in s2:
133 r = s2[s]
138 r = s2[s]
134 if ld == r or r == a: # no change or local is newer
139 if ld == r or r == a: # no change or local is newer
135 sm[s] = l
140 sm[s] = l
136 continue
141 continue
137 elif ld == a: # other side changed
142 elif ld == a: # other side changed
138 debug(s, "other changed, get", r)
143 debug(s, "other changed, get", r)
139 wctx.sub(s).get(r, overwrite)
144 wctx.sub(s).get(r, overwrite)
140 sm[s] = r
145 sm[s] = r
141 elif ld[0] != r[0]: # sources differ
146 elif ld[0] != r[0]: # sources differ
142 if repo.ui.promptchoice(
147 if repo.ui.promptchoice(
143 _(' subrepository sources for %s differ\n'
148 _(' subrepository sources for %s differ\n'
144 'use (l)ocal source (%s) or (r)emote source (%s)?')
149 'use (l)ocal source (%s) or (r)emote source (%s)?')
145 % (s, l[0], r[0]),
150 % (s, l[0], r[0]),
146 (_('&Local'), _('&Remote')), 0):
151 (_('&Local'), _('&Remote')), 0):
147 debug(s, "prompt changed, get", r)
152 debug(s, "prompt changed, get", r)
148 wctx.sub(s).get(r, overwrite)
153 wctx.sub(s).get(r, overwrite)
149 sm[s] = r
154 sm[s] = r
150 elif ld[1] == a[1]: # local side is unchanged
155 elif ld[1] == a[1]: # local side is unchanged
151 debug(s, "other side changed, get", r)
156 debug(s, "other side changed, get", r)
152 wctx.sub(s).get(r, overwrite)
157 wctx.sub(s).get(r, overwrite)
153 sm[s] = r
158 sm[s] = r
154 else:
159 else:
155 debug(s, "both sides changed, merge with", r)
160 debug(s, "both sides changed, merge with", r)
156 wctx.sub(s).merge(r)
161 wctx.sub(s).merge(r)
157 sm[s] = l
162 sm[s] = l
158 elif ld == a: # remote removed, local unchanged
163 elif ld == a: # remote removed, local unchanged
159 debug(s, "remote removed, remove")
164 debug(s, "remote removed, remove")
160 wctx.sub(s).remove()
165 wctx.sub(s).remove()
161 elif a == nullstate: # not present in remote or ancestor
166 elif a == nullstate: # not present in remote or ancestor
162 debug(s, "local added, keep")
167 debug(s, "local added, keep")
163 sm[s] = l
168 sm[s] = l
164 continue
169 continue
165 else:
170 else:
166 if repo.ui.promptchoice(
171 if repo.ui.promptchoice(
167 _(' local changed subrepository %s which remote removed\n'
172 _(' local changed subrepository %s which remote removed\n'
168 'use (c)hanged version or (d)elete?') % s,
173 'use (c)hanged version or (d)elete?') % s,
169 (_('&Changed'), _('&Delete')), 0):
174 (_('&Changed'), _('&Delete')), 0):
170 debug(s, "prompt remove")
175 debug(s, "prompt remove")
171 wctx.sub(s).remove()
176 wctx.sub(s).remove()
172
177
173 for s, r in sorted(s2.items()):
178 for s, r in sorted(s2.items()):
174 if s in s1:
179 if s in s1:
175 continue
180 continue
176 elif s not in sa:
181 elif s not in sa:
177 debug(s, "remote added, get", r)
182 debug(s, "remote added, get", r)
178 mctx.sub(s).get(r)
183 mctx.sub(s).get(r)
179 sm[s] = r
184 sm[s] = r
180 elif r != sa[s]:
185 elif r != sa[s]:
181 if repo.ui.promptchoice(
186 if repo.ui.promptchoice(
182 _(' remote changed subrepository %s which local removed\n'
187 _(' remote changed subrepository %s which local removed\n'
183 'use (c)hanged version or (d)elete?') % s,
188 'use (c)hanged version or (d)elete?') % s,
184 (_('&Changed'), _('&Delete')), 0) == 0:
189 (_('&Changed'), _('&Delete')), 0) == 0:
185 debug(s, "prompt recreate", r)
190 debug(s, "prompt recreate", r)
186 wctx.sub(s).get(r)
191 wctx.sub(s).get(r)
187 sm[s] = r
192 sm[s] = r
188
193
189 # record merged .hgsubstate
194 # record merged .hgsubstate
190 writestate(repo, sm)
195 writestate(repo, sm)
191
196
192 def _updateprompt(ui, sub, dirty, local, remote):
197 def _updateprompt(ui, sub, dirty, local, remote):
193 if dirty:
198 if dirty:
194 msg = (_(' subrepository sources for %s differ\n'
199 msg = (_(' subrepository sources for %s differ\n'
195 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
200 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
196 % (subrelpath(sub), local, remote))
201 % (subrelpath(sub), local, remote))
197 else:
202 else:
198 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
203 msg = (_(' subrepository sources for %s differ (in checked out version)\n'
199 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
204 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
200 % (subrelpath(sub), local, remote))
205 % (subrelpath(sub), local, remote))
201 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
206 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
202
207
203 def reporelpath(repo):
208 def reporelpath(repo):
204 """return path to this (sub)repo as seen from outermost repo"""
209 """return path to this (sub)repo as seen from outermost repo"""
205 parent = repo
210 parent = repo
206 while util.safehasattr(parent, '_subparent'):
211 while util.safehasattr(parent, '_subparent'):
207 parent = parent._subparent
212 parent = parent._subparent
208 p = parent.root.rstrip(os.sep)
213 p = parent.root.rstrip(os.sep)
209 return repo.root[len(p) + 1:]
214 return repo.root[len(p) + 1:]
210
215
211 def subrelpath(sub):
216 def subrelpath(sub):
212 """return path to this subrepo as seen from outermost repo"""
217 """return path to this subrepo as seen from outermost repo"""
213 if util.safehasattr(sub, '_relpath'):
218 if util.safehasattr(sub, '_relpath'):
214 return sub._relpath
219 return sub._relpath
215 if not util.safehasattr(sub, '_repo'):
220 if not util.safehasattr(sub, '_repo'):
216 return sub._path
221 return sub._path
217 return reporelpath(sub._repo)
222 return reporelpath(sub._repo)
218
223
219 def _abssource(repo, push=False, abort=True):
224 def _abssource(repo, push=False, abort=True):
220 """return pull/push path of repo - either based on parent repo .hgsub info
225 """return pull/push path of repo - either based on parent repo .hgsub info
221 or on the top repo config. Abort or return None if no source found."""
226 or on the top repo config. Abort or return None if no source found."""
222 if util.safehasattr(repo, '_subparent'):
227 if util.safehasattr(repo, '_subparent'):
223 source = util.url(repo._subsource)
228 source = util.url(repo._subsource)
224 if source.isabs():
229 if source.isabs():
225 return str(source)
230 return str(source)
226 source.path = posixpath.normpath(source.path)
231 source.path = posixpath.normpath(source.path)
227 parent = _abssource(repo._subparent, push, abort=False)
232 parent = _abssource(repo._subparent, push, abort=False)
228 if parent:
233 if parent:
229 parent = util.url(util.pconvert(parent))
234 parent = util.url(util.pconvert(parent))
230 parent.path = posixpath.join(parent.path or '', source.path)
235 parent.path = posixpath.join(parent.path or '', source.path)
231 parent.path = posixpath.normpath(parent.path)
236 parent.path = posixpath.normpath(parent.path)
232 return str(parent)
237 return str(parent)
233 else: # recursion reached top repo
238 else: # recursion reached top repo
234 if util.safehasattr(repo, '_subtoppath'):
239 if util.safehasattr(repo, '_subtoppath'):
235 return repo._subtoppath
240 return repo._subtoppath
236 if push and repo.ui.config('paths', 'default-push'):
241 if push and repo.ui.config('paths', 'default-push'):
237 return repo.ui.config('paths', 'default-push')
242 return repo.ui.config('paths', 'default-push')
238 if repo.ui.config('paths', 'default'):
243 if repo.ui.config('paths', 'default'):
239 return repo.ui.config('paths', 'default')
244 return repo.ui.config('paths', 'default')
240 if abort:
245 if abort:
241 raise util.Abort(_("default path for subrepository %s not found") %
246 raise util.Abort(_("default path for subrepository %s not found") %
242 reporelpath(repo))
247 reporelpath(repo))
243
248
244 def itersubrepos(ctx1, ctx2):
249 def itersubrepos(ctx1, ctx2):
245 """find subrepos in ctx1 or ctx2"""
250 """find subrepos in ctx1 or ctx2"""
246 # Create a (subpath, ctx) mapping where we prefer subpaths from
251 # Create a (subpath, ctx) mapping where we prefer subpaths from
247 # ctx1. The subpaths from ctx2 are important when the .hgsub file
252 # ctx1. The subpaths from ctx2 are important when the .hgsub file
248 # has been modified (in ctx2) but not yet committed (in ctx1).
253 # has been modified (in ctx2) but not yet committed (in ctx1).
249 subpaths = dict.fromkeys(ctx2.substate, ctx2)
254 subpaths = dict.fromkeys(ctx2.substate, ctx2)
250 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
255 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
251 for subpath, ctx in sorted(subpaths.iteritems()):
256 for subpath, ctx in sorted(subpaths.iteritems()):
252 yield subpath, ctx.sub(subpath)
257 yield subpath, ctx.sub(subpath)
253
258
254 def subrepo(ctx, path):
259 def subrepo(ctx, path):
255 """return instance of the right subrepo class for subrepo in path"""
260 """return instance of the right subrepo class for subrepo in path"""
256 # subrepo inherently violates our import layering rules
261 # subrepo inherently violates our import layering rules
257 # because it wants to make repo objects from deep inside the stack
262 # because it wants to make repo objects from deep inside the stack
258 # so we manually delay the circular imports to not break
263 # so we manually delay the circular imports to not break
259 # scripts that don't use our demand-loading
264 # scripts that don't use our demand-loading
260 global hg
265 global hg
261 import hg as h
266 import hg as h
262 hg = h
267 hg = h
263
268
264 scmutil.pathauditor(ctx._repo.root)(path)
269 scmutil.pathauditor(ctx._repo.root)(path)
265 state = ctx.substate.get(path, nullstate)
270 state = ctx.substate.get(path, nullstate)
266 if state[2] not in types:
271 if state[2] not in types:
267 raise util.Abort(_('unknown subrepo type %s') % state[2])
272 raise util.Abort(_('unknown subrepo type %s') % state[2])
268 return types[state[2]](ctx, path, state[:2])
273 return types[state[2]](ctx, path, state[:2])
269
274
270 # subrepo classes need to implement the following abstract class:
275 # subrepo classes need to implement the following abstract class:
271
276
272 class abstractsubrepo(object):
277 class abstractsubrepo(object):
273
278
274 def dirty(self, ignoreupdate=False):
279 def dirty(self, ignoreupdate=False):
275 """returns true if the dirstate of the subrepo is dirty or does not
280 """returns true if the dirstate of the subrepo is dirty or does not
276 match current stored state. If ignoreupdate is true, only check
281 match current stored state. If ignoreupdate is true, only check
277 whether the subrepo has uncommitted changes in its dirstate.
282 whether the subrepo has uncommitted changes in its dirstate.
278 """
283 """
279 raise NotImplementedError
284 raise NotImplementedError
280
285
281 def basestate(self):
286 def basestate(self):
282 """current working directory base state, disregarding .hgsubstate
287 """current working directory base state, disregarding .hgsubstate
283 state and working directory modifications"""
288 state and working directory modifications"""
284 raise NotImplementedError
289 raise NotImplementedError
285
290
286 def checknested(self, path):
291 def checknested(self, path):
287 """check if path is a subrepository within this repository"""
292 """check if path is a subrepository within this repository"""
288 return False
293 return False
289
294
290 def commit(self, text, user, date):
295 def commit(self, text, user, date):
291 """commit the current changes to the subrepo with the given
296 """commit the current changes to the subrepo with the given
292 log message. Use given user and date if possible. Return the
297 log message. Use given user and date if possible. Return the
293 new state of the subrepo.
298 new state of the subrepo.
294 """
299 """
295 raise NotImplementedError
300 raise NotImplementedError
296
301
297 def remove(self):
302 def remove(self):
298 """remove the subrepo
303 """remove the subrepo
299
304
300 (should verify the dirstate is not dirty first)
305 (should verify the dirstate is not dirty first)
301 """
306 """
302 raise NotImplementedError
307 raise NotImplementedError
303
308
304 def get(self, state, overwrite=False):
309 def get(self, state, overwrite=False):
305 """run whatever commands are needed to put the subrepo into
310 """run whatever commands are needed to put the subrepo into
306 this state
311 this state
307 """
312 """
308 raise NotImplementedError
313 raise NotImplementedError
309
314
310 def merge(self, state):
315 def merge(self, state):
311 """merge currently-saved state with the new state."""
316 """merge currently-saved state with the new state."""
312 raise NotImplementedError
317 raise NotImplementedError
313
318
314 def push(self, opts):
319 def push(self, opts):
315 """perform whatever action is analogous to 'hg push'
320 """perform whatever action is analogous to 'hg push'
316
321
317 This may be a no-op on some systems.
322 This may be a no-op on some systems.
318 """
323 """
319 raise NotImplementedError
324 raise NotImplementedError
320
325
321 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
326 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
322 return []
327 return []
323
328
324 def status(self, rev2, **opts):
329 def status(self, rev2, **opts):
325 return [], [], [], [], [], [], []
330 return [], [], [], [], [], [], []
326
331
327 def diff(self, diffopts, node2, match, prefix, **opts):
332 def diff(self, diffopts, node2, match, prefix, **opts):
328 pass
333 pass
329
334
330 def outgoing(self, ui, dest, opts):
335 def outgoing(self, ui, dest, opts):
331 return 1
336 return 1
332
337
333 def incoming(self, ui, source, opts):
338 def incoming(self, ui, source, opts):
334 return 1
339 return 1
335
340
336 def files(self):
341 def files(self):
337 """return filename iterator"""
342 """return filename iterator"""
338 raise NotImplementedError
343 raise NotImplementedError
339
344
340 def filedata(self, name):
345 def filedata(self, name):
341 """return file data"""
346 """return file data"""
342 raise NotImplementedError
347 raise NotImplementedError
343
348
344 def fileflags(self, name):
349 def fileflags(self, name):
345 """return file flags"""
350 """return file flags"""
346 return ''
351 return ''
347
352
348 def archive(self, ui, archiver, prefix):
353 def archive(self, ui, archiver, prefix):
349 files = self.files()
354 files = self.files()
350 total = len(files)
355 total = len(files)
351 relpath = subrelpath(self)
356 relpath = subrelpath(self)
352 ui.progress(_('archiving (%s)') % relpath, 0,
357 ui.progress(_('archiving (%s)') % relpath, 0,
353 unit=_('files'), total=total)
358 unit=_('files'), total=total)
354 for i, name in enumerate(files):
359 for i, name in enumerate(files):
355 flags = self.fileflags(name)
360 flags = self.fileflags(name)
356 mode = 'x' in flags and 0755 or 0644
361 mode = 'x' in flags and 0755 or 0644
357 symlink = 'l' in flags
362 symlink = 'l' in flags
358 archiver.addfile(os.path.join(prefix, self._path, name),
363 archiver.addfile(os.path.join(prefix, self._path, name),
359 mode, symlink, self.filedata(name))
364 mode, symlink, self.filedata(name))
360 ui.progress(_('archiving (%s)') % relpath, i + 1,
365 ui.progress(_('archiving (%s)') % relpath, i + 1,
361 unit=_('files'), total=total)
366 unit=_('files'), total=total)
362 ui.progress(_('archiving (%s)') % relpath, None)
367 ui.progress(_('archiving (%s)') % relpath, None)
363
368
364 def walk(self, match):
369 def walk(self, match):
365 '''
370 '''
366 walk recursively through the directory tree, finding all files
371 walk recursively through the directory tree, finding all files
367 matched by the match function
372 matched by the match function
368 '''
373 '''
369 pass
374 pass
370
375
371 def forget(self, ui, match, prefix):
376 def forget(self, ui, match, prefix):
372 return ([], [])
377 return ([], [])
373
378
374 def revert(self, ui, substate, *pats, **opts):
379 def revert(self, ui, substate, *pats, **opts):
375 ui.warn('%s: reverting %s subrepos is unsupported\n' \
380 ui.warn('%s: reverting %s subrepos is unsupported\n' \
376 % (substate[0], substate[2]))
381 % (substate[0], substate[2]))
377 return []
382 return []
378
383
379 class hgsubrepo(abstractsubrepo):
384 class hgsubrepo(abstractsubrepo):
380 def __init__(self, ctx, path, state):
385 def __init__(self, ctx, path, state):
381 self._path = path
386 self._path = path
382 self._state = state
387 self._state = state
383 r = ctx._repo
388 r = ctx._repo
384 root = r.wjoin(path)
389 root = r.wjoin(path)
385 create = False
390 create = False
386 if not os.path.exists(os.path.join(root, '.hg')):
391 if not os.path.exists(os.path.join(root, '.hg')):
387 create = True
392 create = True
388 util.makedirs(root)
393 util.makedirs(root)
389 self._repo = hg.repository(r.ui, root, create=create)
394 self._repo = hg.repository(r.ui, root, create=create)
390 self._initrepo(r, state[0], create)
395 self._initrepo(r, state[0], create)
391
396
392 def _initrepo(self, parentrepo, source, create):
397 def _initrepo(self, parentrepo, source, create):
393 self._repo._subparent = parentrepo
398 self._repo._subparent = parentrepo
394 self._repo._subsource = source
399 self._repo._subsource = source
395
400
396 if create:
401 if create:
397 fp = self._repo.opener("hgrc", "w", text=True)
402 fp = self._repo.opener("hgrc", "w", text=True)
398 fp.write('[paths]\n')
403 fp.write('[paths]\n')
399
404
400 def addpathconfig(key, value):
405 def addpathconfig(key, value):
401 if value:
406 if value:
402 fp.write('%s = %s\n' % (key, value))
407 fp.write('%s = %s\n' % (key, value))
403 self._repo.ui.setconfig('paths', key, value)
408 self._repo.ui.setconfig('paths', key, value)
404
409
405 defpath = _abssource(self._repo, abort=False)
410 defpath = _abssource(self._repo, abort=False)
406 defpushpath = _abssource(self._repo, True, abort=False)
411 defpushpath = _abssource(self._repo, True, abort=False)
407 addpathconfig('default', defpath)
412 addpathconfig('default', defpath)
408 if defpath != defpushpath:
413 if defpath != defpushpath:
409 addpathconfig('default-push', defpushpath)
414 addpathconfig('default-push', defpushpath)
410 fp.close()
415 fp.close()
411
416
412 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
417 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
413 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
418 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
414 os.path.join(prefix, self._path), explicitonly)
419 os.path.join(prefix, self._path), explicitonly)
415
420
416 def status(self, rev2, **opts):
421 def status(self, rev2, **opts):
417 try:
422 try:
418 rev1 = self._state[1]
423 rev1 = self._state[1]
419 ctx1 = self._repo[rev1]
424 ctx1 = self._repo[rev1]
420 ctx2 = self._repo[rev2]
425 ctx2 = self._repo[rev2]
421 return self._repo.status(ctx1, ctx2, **opts)
426 return self._repo.status(ctx1, ctx2, **opts)
422 except error.RepoLookupError, inst:
427 except error.RepoLookupError, inst:
423 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
428 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
424 % (inst, subrelpath(self)))
429 % (inst, subrelpath(self)))
425 return [], [], [], [], [], [], []
430 return [], [], [], [], [], [], []
426
431
427 def diff(self, diffopts, node2, match, prefix, **opts):
432 def diff(self, diffopts, node2, match, prefix, **opts):
428 try:
433 try:
429 node1 = node.bin(self._state[1])
434 node1 = node.bin(self._state[1])
430 # We currently expect node2 to come from substate and be
435 # We currently expect node2 to come from substate and be
431 # in hex format
436 # in hex format
432 if node2 is not None:
437 if node2 is not None:
433 node2 = node.bin(node2)
438 node2 = node.bin(node2)
434 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
439 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
435 node1, node2, match,
440 node1, node2, match,
436 prefix=os.path.join(prefix, self._path),
441 prefix=os.path.join(prefix, self._path),
437 listsubrepos=True, **opts)
442 listsubrepos=True, **opts)
438 except error.RepoLookupError, inst:
443 except error.RepoLookupError, inst:
439 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
444 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
440 % (inst, subrelpath(self)))
445 % (inst, subrelpath(self)))
441
446
442 def archive(self, ui, archiver, prefix):
447 def archive(self, ui, archiver, prefix):
443 self._get(self._state + ('hg',))
448 self._get(self._state + ('hg',))
444 abstractsubrepo.archive(self, ui, archiver, prefix)
449 abstractsubrepo.archive(self, ui, archiver, prefix)
445
450
446 rev = self._state[1]
451 rev = self._state[1]
447 ctx = self._repo[rev]
452 ctx = self._repo[rev]
448 for subpath in ctx.substate:
453 for subpath in ctx.substate:
449 s = subrepo(ctx, subpath)
454 s = subrepo(ctx, subpath)
450 s.archive(ui, archiver, os.path.join(prefix, self._path))
455 s.archive(ui, archiver, os.path.join(prefix, self._path))
451
456
452 def dirty(self, ignoreupdate=False):
457 def dirty(self, ignoreupdate=False):
453 r = self._state[1]
458 r = self._state[1]
454 if r == '' and not ignoreupdate: # no state recorded
459 if r == '' and not ignoreupdate: # no state recorded
455 return True
460 return True
456 w = self._repo[None]
461 w = self._repo[None]
457 if r != w.p1().hex() and not ignoreupdate:
462 if r != w.p1().hex() and not ignoreupdate:
458 # different version checked out
463 # different version checked out
459 return True
464 return True
460 return w.dirty() # working directory changed
465 return w.dirty() # working directory changed
461
466
462 def basestate(self):
467 def basestate(self):
463 return self._repo['.'].hex()
468 return self._repo['.'].hex()
464
469
465 def checknested(self, path):
470 def checknested(self, path):
466 return self._repo._checknested(self._repo.wjoin(path))
471 return self._repo._checknested(self._repo.wjoin(path))
467
472
468 def commit(self, text, user, date):
473 def commit(self, text, user, date):
469 # don't bother committing in the subrepo if it's only been
474 # don't bother committing in the subrepo if it's only been
470 # updated
475 # updated
471 if not self.dirty(True):
476 if not self.dirty(True):
472 return self._repo['.'].hex()
477 return self._repo['.'].hex()
473 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
478 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
474 n = self._repo.commit(text, user, date)
479 n = self._repo.commit(text, user, date)
475 if not n:
480 if not n:
476 return self._repo['.'].hex() # different version checked out
481 return self._repo['.'].hex() # different version checked out
477 return node.hex(n)
482 return node.hex(n)
478
483
479 def remove(self):
484 def remove(self):
480 # we can't fully delete the repository as it may contain
485 # we can't fully delete the repository as it may contain
481 # local-only history
486 # local-only history
482 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
487 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
483 hg.clean(self._repo, node.nullid, False)
488 hg.clean(self._repo, node.nullid, False)
484
489
485 def _get(self, state):
490 def _get(self, state):
486 source, revision, kind = state
491 source, revision, kind = state
487 if revision not in self._repo:
492 if revision not in self._repo:
488 self._repo._subsource = source
493 self._repo._subsource = source
489 srcurl = _abssource(self._repo)
494 srcurl = _abssource(self._repo)
490 other = hg.peer(self._repo.ui, {}, srcurl)
495 other = hg.peer(self._repo.ui, {}, srcurl)
491 if len(self._repo) == 0:
496 if len(self._repo) == 0:
492 self._repo.ui.status(_('cloning subrepo %s from %s\n')
497 self._repo.ui.status(_('cloning subrepo %s from %s\n')
493 % (subrelpath(self), srcurl))
498 % (subrelpath(self), srcurl))
494 parentrepo = self._repo._subparent
499 parentrepo = self._repo._subparent
495 shutil.rmtree(self._repo.path)
500 shutil.rmtree(self._repo.path)
496 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
501 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
497 self._repo.root, update=False)
502 self._repo.root, update=False)
498 self._initrepo(parentrepo, source, create=True)
503 self._initrepo(parentrepo, source, create=True)
499 else:
504 else:
500 self._repo.ui.status(_('pulling subrepo %s from %s\n')
505 self._repo.ui.status(_('pulling subrepo %s from %s\n')
501 % (subrelpath(self), srcurl))
506 % (subrelpath(self), srcurl))
502 self._repo.pull(other)
507 self._repo.pull(other)
503 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
508 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
504 srcurl)
509 srcurl)
505
510
506 def get(self, state, overwrite=False):
511 def get(self, state, overwrite=False):
507 self._get(state)
512 self._get(state)
508 source, revision, kind = state
513 source, revision, kind = state
509 self._repo.ui.debug("getting subrepo %s\n" % self._path)
514 self._repo.ui.debug("getting subrepo %s\n" % self._path)
510 hg.clean(self._repo, revision, False)
515 hg.clean(self._repo, revision, False)
511
516
512 def merge(self, state):
517 def merge(self, state):
513 self._get(state)
518 self._get(state)
514 cur = self._repo['.']
519 cur = self._repo['.']
515 dst = self._repo[state[1]]
520 dst = self._repo[state[1]]
516 anc = dst.ancestor(cur)
521 anc = dst.ancestor(cur)
517
522
518 def mergefunc():
523 def mergefunc():
519 if anc == cur and dst.branch() == cur.branch():
524 if anc == cur and dst.branch() == cur.branch():
520 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
525 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
521 hg.update(self._repo, state[1])
526 hg.update(self._repo, state[1])
522 elif anc == dst:
527 elif anc == dst:
523 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
528 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
524 else:
529 else:
525 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
530 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
526 hg.merge(self._repo, state[1], remind=False)
531 hg.merge(self._repo, state[1], remind=False)
527
532
528 wctx = self._repo[None]
533 wctx = self._repo[None]
529 if self.dirty():
534 if self.dirty():
530 if anc != dst:
535 if anc != dst:
531 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
536 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
532 mergefunc()
537 mergefunc()
533 else:
538 else:
534 mergefunc()
539 mergefunc()
535 else:
540 else:
536 mergefunc()
541 mergefunc()
537
542
538 def push(self, opts):
543 def push(self, opts):
539 force = opts.get('force')
544 force = opts.get('force')
540 newbranch = opts.get('new_branch')
545 newbranch = opts.get('new_branch')
541 ssh = opts.get('ssh')
546 ssh = opts.get('ssh')
542
547
543 # push subrepos depth-first for coherent ordering
548 # push subrepos depth-first for coherent ordering
544 c = self._repo['']
549 c = self._repo['']
545 subs = c.substate # only repos that are committed
550 subs = c.substate # only repos that are committed
546 for s in sorted(subs):
551 for s in sorted(subs):
547 if c.sub(s).push(opts) == 0:
552 if c.sub(s).push(opts) == 0:
548 return False
553 return False
549
554
550 dsturl = _abssource(self._repo, True)
555 dsturl = _abssource(self._repo, True)
551 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
556 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
552 (subrelpath(self), dsturl))
557 (subrelpath(self), dsturl))
553 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
558 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
554 return self._repo.push(other, force, newbranch=newbranch)
559 return self._repo.push(other, force, newbranch=newbranch)
555
560
556 def outgoing(self, ui, dest, opts):
561 def outgoing(self, ui, dest, opts):
557 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
562 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
558
563
559 def incoming(self, ui, source, opts):
564 def incoming(self, ui, source, opts):
560 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
565 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
561
566
562 def files(self):
567 def files(self):
563 rev = self._state[1]
568 rev = self._state[1]
564 ctx = self._repo[rev]
569 ctx = self._repo[rev]
565 return ctx.manifest()
570 return ctx.manifest()
566
571
567 def filedata(self, name):
572 def filedata(self, name):
568 rev = self._state[1]
573 rev = self._state[1]
569 return self._repo[rev][name].data()
574 return self._repo[rev][name].data()
570
575
571 def fileflags(self, name):
576 def fileflags(self, name):
572 rev = self._state[1]
577 rev = self._state[1]
573 ctx = self._repo[rev]
578 ctx = self._repo[rev]
574 return ctx.flags(name)
579 return ctx.flags(name)
575
580
576 def walk(self, match):
581 def walk(self, match):
577 ctx = self._repo[None]
582 ctx = self._repo[None]
578 return ctx.walk(match)
583 return ctx.walk(match)
579
584
580 def forget(self, ui, match, prefix):
585 def forget(self, ui, match, prefix):
581 return cmdutil.forget(ui, self._repo, match,
586 return cmdutil.forget(ui, self._repo, match,
582 os.path.join(prefix, self._path), True)
587 os.path.join(prefix, self._path), True)
583
588
584 def revert(self, ui, substate, *pats, **opts):
589 def revert(self, ui, substate, *pats, **opts):
585 # reverting a subrepo is a 2 step process:
590 # reverting a subrepo is a 2 step process:
586 # 1. if the no_backup is not set, revert all modified
591 # 1. if the no_backup is not set, revert all modified
587 # files inside the subrepo
592 # files inside the subrepo
588 # 2. update the subrepo to the revision specified in
593 # 2. update the subrepo to the revision specified in
589 # the corresponding substate dictionary
594 # the corresponding substate dictionary
590 ui.status(_('reverting subrepo %s\n') % substate[0])
595 ui.status(_('reverting subrepo %s\n') % substate[0])
591 if not opts.get('no_backup'):
596 if not opts.get('no_backup'):
592 # Revert all files on the subrepo, creating backups
597 # Revert all files on the subrepo, creating backups
593 # Note that this will not recursively revert subrepos
598 # Note that this will not recursively revert subrepos
594 # We could do it if there was a set:subrepos() predicate
599 # We could do it if there was a set:subrepos() predicate
595 opts = opts.copy()
600 opts = opts.copy()
596 opts['date'] = None
601 opts['date'] = None
597 opts['rev'] = substate[1]
602 opts['rev'] = substate[1]
598
603
599 pats = []
604 pats = []
600 if not opts['all']:
605 if not opts['all']:
601 pats = ['set:modified()']
606 pats = ['set:modified()']
602 self.filerevert(ui, *pats, **opts)
607 self.filerevert(ui, *pats, **opts)
603
608
604 # Update the repo to the revision specified in the given substate
609 # Update the repo to the revision specified in the given substate
605 self.get(substate, overwrite=True)
610 self.get(substate, overwrite=True)
606
611
607 def filerevert(self, ui, *pats, **opts):
612 def filerevert(self, ui, *pats, **opts):
608 ctx = self._repo[opts['rev']]
613 ctx = self._repo[opts['rev']]
609 parents = self._repo.dirstate.parents()
614 parents = self._repo.dirstate.parents()
610 if opts['all']:
615 if opts['all']:
611 pats = ['set:modified()']
616 pats = ['set:modified()']
612 else:
617 else:
613 pats = []
618 pats = []
614 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
619 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
615
620
616 class svnsubrepo(abstractsubrepo):
621 class svnsubrepo(abstractsubrepo):
617 def __init__(self, ctx, path, state):
622 def __init__(self, ctx, path, state):
618 self._path = path
623 self._path = path
619 self._state = state
624 self._state = state
620 self._ctx = ctx
625 self._ctx = ctx
621 self._ui = ctx._repo.ui
626 self._ui = ctx._repo.ui
622 self._exe = util.findexe('svn')
627 self._exe = util.findexe('svn')
623 if not self._exe:
628 if not self._exe:
624 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
629 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
625 % self._path)
630 % self._path)
626
631
627 def _svncommand(self, commands, filename='', failok=False):
632 def _svncommand(self, commands, filename='', failok=False):
628 cmd = [self._exe]
633 cmd = [self._exe]
629 extrakw = {}
634 extrakw = {}
630 if not self._ui.interactive():
635 if not self._ui.interactive():
631 # Making stdin be a pipe should prevent svn from behaving
636 # Making stdin be a pipe should prevent svn from behaving
632 # interactively even if we can't pass --non-interactive.
637 # interactively even if we can't pass --non-interactive.
633 extrakw['stdin'] = subprocess.PIPE
638 extrakw['stdin'] = subprocess.PIPE
634 # Starting in svn 1.5 --non-interactive is a global flag
639 # Starting in svn 1.5 --non-interactive is a global flag
635 # instead of being per-command, but we need to support 1.4 so
640 # instead of being per-command, but we need to support 1.4 so
636 # we have to be intelligent about what commands take
641 # we have to be intelligent about what commands take
637 # --non-interactive.
642 # --non-interactive.
638 if commands[0] in ('update', 'checkout', 'commit'):
643 if commands[0] in ('update', 'checkout', 'commit'):
639 cmd.append('--non-interactive')
644 cmd.append('--non-interactive')
640 cmd.extend(commands)
645 cmd.extend(commands)
641 if filename is not None:
646 if filename is not None:
642 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
647 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
643 cmd.append(path)
648 cmd.append(path)
644 env = dict(os.environ)
649 env = dict(os.environ)
645 # Avoid localized output, preserve current locale for everything else.
650 # Avoid localized output, preserve current locale for everything else.
646 env['LC_MESSAGES'] = 'C'
651 env['LC_MESSAGES'] = 'C'
647 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
652 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
648 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
653 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
649 universal_newlines=True, env=env, **extrakw)
654 universal_newlines=True, env=env, **extrakw)
650 stdout, stderr = p.communicate()
655 stdout, stderr = p.communicate()
651 stderr = stderr.strip()
656 stderr = stderr.strip()
652 if not failok:
657 if not failok:
653 if p.returncode:
658 if p.returncode:
654 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
659 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
655 if stderr:
660 if stderr:
656 self._ui.warn(stderr + '\n')
661 self._ui.warn(stderr + '\n')
657 return stdout, stderr
662 return stdout, stderr
658
663
659 @propertycache
664 @propertycache
660 def _svnversion(self):
665 def _svnversion(self):
661 output, err = self._svncommand(['--version'], filename=None)
666 output, err = self._svncommand(['--version'], filename=None)
662 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
667 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
663 if not m:
668 if not m:
664 raise util.Abort(_('cannot retrieve svn tool version'))
669 raise util.Abort(_('cannot retrieve svn tool version'))
665 return (int(m.group(1)), int(m.group(2)))
670 return (int(m.group(1)), int(m.group(2)))
666
671
667 def _wcrevs(self):
672 def _wcrevs(self):
668 # Get the working directory revision as well as the last
673 # Get the working directory revision as well as the last
669 # commit revision so we can compare the subrepo state with
674 # commit revision so we can compare the subrepo state with
670 # both. We used to store the working directory one.
675 # both. We used to store the working directory one.
671 output, err = self._svncommand(['info', '--xml'])
676 output, err = self._svncommand(['info', '--xml'])
672 doc = xml.dom.minidom.parseString(output)
677 doc = xml.dom.minidom.parseString(output)
673 entries = doc.getElementsByTagName('entry')
678 entries = doc.getElementsByTagName('entry')
674 lastrev, rev = '0', '0'
679 lastrev, rev = '0', '0'
675 if entries:
680 if entries:
676 rev = str(entries[0].getAttribute('revision')) or '0'
681 rev = str(entries[0].getAttribute('revision')) or '0'
677 commits = entries[0].getElementsByTagName('commit')
682 commits = entries[0].getElementsByTagName('commit')
678 if commits:
683 if commits:
679 lastrev = str(commits[0].getAttribute('revision')) or '0'
684 lastrev = str(commits[0].getAttribute('revision')) or '0'
680 return (lastrev, rev)
685 return (lastrev, rev)
681
686
682 def _wcrev(self):
687 def _wcrev(self):
683 return self._wcrevs()[0]
688 return self._wcrevs()[0]
684
689
685 def _wcchanged(self):
690 def _wcchanged(self):
686 """Return (changes, extchanges, missing) where changes is True
691 """Return (changes, extchanges, missing) where changes is True
687 if the working directory was changed, extchanges is
692 if the working directory was changed, extchanges is
688 True if any of these changes concern an external entry and missing
693 True if any of these changes concern an external entry and missing
689 is True if any change is a missing entry.
694 is True if any change is a missing entry.
690 """
695 """
691 output, err = self._svncommand(['status', '--xml'])
696 output, err = self._svncommand(['status', '--xml'])
692 externals, changes, missing = [], [], []
697 externals, changes, missing = [], [], []
693 doc = xml.dom.minidom.parseString(output)
698 doc = xml.dom.minidom.parseString(output)
694 for e in doc.getElementsByTagName('entry'):
699 for e in doc.getElementsByTagName('entry'):
695 s = e.getElementsByTagName('wc-status')
700 s = e.getElementsByTagName('wc-status')
696 if not s:
701 if not s:
697 continue
702 continue
698 item = s[0].getAttribute('item')
703 item = s[0].getAttribute('item')
699 props = s[0].getAttribute('props')
704 props = s[0].getAttribute('props')
700 path = e.getAttribute('path')
705 path = e.getAttribute('path')
701 if item == 'external':
706 if item == 'external':
702 externals.append(path)
707 externals.append(path)
703 elif item == 'missing':
708 elif item == 'missing':
704 missing.append(path)
709 missing.append(path)
705 if (item not in ('', 'normal', 'unversioned', 'external')
710 if (item not in ('', 'normal', 'unversioned', 'external')
706 or props not in ('', 'none', 'normal')):
711 or props not in ('', 'none', 'normal')):
707 changes.append(path)
712 changes.append(path)
708 for path in changes:
713 for path in changes:
709 for ext in externals:
714 for ext in externals:
710 if path == ext or path.startswith(ext + os.sep):
715 if path == ext or path.startswith(ext + os.sep):
711 return True, True, bool(missing)
716 return True, True, bool(missing)
712 return bool(changes), False, bool(missing)
717 return bool(changes), False, bool(missing)
713
718
714 def dirty(self, ignoreupdate=False):
719 def dirty(self, ignoreupdate=False):
715 if not self._wcchanged()[0]:
720 if not self._wcchanged()[0]:
716 if self._state[1] in self._wcrevs() or ignoreupdate:
721 if self._state[1] in self._wcrevs() or ignoreupdate:
717 return False
722 return False
718 return True
723 return True
719
724
720 def basestate(self):
725 def basestate(self):
721 lastrev, rev = self._wcrevs()
726 lastrev, rev = self._wcrevs()
722 if lastrev != rev:
727 if lastrev != rev:
723 # Last committed rev is not the same than rev. We would
728 # Last committed rev is not the same than rev. We would
724 # like to take lastrev but we do not know if the subrepo
729 # like to take lastrev but we do not know if the subrepo
725 # URL exists at lastrev. Test it and fallback to rev it
730 # URL exists at lastrev. Test it and fallback to rev it
726 # is not there.
731 # is not there.
727 try:
732 try:
728 self._svncommand(['info', '%s@%s' % (self._state[0], lastrev)])
733 self._svncommand(['info', '%s@%s' % (self._state[0], lastrev)])
729 return lastrev
734 return lastrev
730 except error.Abort:
735 except error.Abort:
731 pass
736 pass
732 return rev
737 return rev
733
738
734 def commit(self, text, user, date):
739 def commit(self, text, user, date):
735 # user and date are out of our hands since svn is centralized
740 # user and date are out of our hands since svn is centralized
736 changed, extchanged, missing = self._wcchanged()
741 changed, extchanged, missing = self._wcchanged()
737 if not changed:
742 if not changed:
738 return self.basestate()
743 return self.basestate()
739 if extchanged:
744 if extchanged:
740 # Do not try to commit externals
745 # Do not try to commit externals
741 raise util.Abort(_('cannot commit svn externals'))
746 raise util.Abort(_('cannot commit svn externals'))
742 if missing:
747 if missing:
743 # svn can commit with missing entries but aborting like hg
748 # svn can commit with missing entries but aborting like hg
744 # seems a better approach.
749 # seems a better approach.
745 raise util.Abort(_('cannot commit missing svn entries'))
750 raise util.Abort(_('cannot commit missing svn entries'))
746 commitinfo, err = self._svncommand(['commit', '-m', text])
751 commitinfo, err = self._svncommand(['commit', '-m', text])
747 self._ui.status(commitinfo)
752 self._ui.status(commitinfo)
748 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
753 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
749 if not newrev:
754 if not newrev:
750 if not commitinfo.strip():
755 if not commitinfo.strip():
751 # Sometimes, our definition of "changed" differs from
756 # Sometimes, our definition of "changed" differs from
752 # svn one. For instance, svn ignores missing files
757 # svn one. For instance, svn ignores missing files
753 # when committing. If there are only missing files, no
758 # when committing. If there are only missing files, no
754 # commit is made, no output and no error code.
759 # commit is made, no output and no error code.
755 raise util.Abort(_('failed to commit svn changes'))
760 raise util.Abort(_('failed to commit svn changes'))
756 raise util.Abort(commitinfo.splitlines()[-1])
761 raise util.Abort(commitinfo.splitlines()[-1])
757 newrev = newrev.groups()[0]
762 newrev = newrev.groups()[0]
758 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
763 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
759 return newrev
764 return newrev
760
765
761 def remove(self):
766 def remove(self):
762 if self.dirty():
767 if self.dirty():
763 self._ui.warn(_('not removing repo %s because '
768 self._ui.warn(_('not removing repo %s because '
764 'it has changes.\n' % self._path))
769 'it has changes.\n' % self._path))
765 return
770 return
766 self._ui.note(_('removing subrepo %s\n') % self._path)
771 self._ui.note(_('removing subrepo %s\n') % self._path)
767
772
768 def onerror(function, path, excinfo):
773 def onerror(function, path, excinfo):
769 if function is not os.remove:
774 if function is not os.remove:
770 raise
775 raise
771 # read-only files cannot be unlinked under Windows
776 # read-only files cannot be unlinked under Windows
772 s = os.stat(path)
777 s = os.stat(path)
773 if (s.st_mode & stat.S_IWRITE) != 0:
778 if (s.st_mode & stat.S_IWRITE) != 0:
774 raise
779 raise
775 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
780 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
776 os.remove(path)
781 os.remove(path)
777
782
778 path = self._ctx._repo.wjoin(self._path)
783 path = self._ctx._repo.wjoin(self._path)
779 shutil.rmtree(path, onerror=onerror)
784 shutil.rmtree(path, onerror=onerror)
780 try:
785 try:
781 os.removedirs(os.path.dirname(path))
786 os.removedirs(os.path.dirname(path))
782 except OSError:
787 except OSError:
783 pass
788 pass
784
789
785 def get(self, state, overwrite=False):
790 def get(self, state, overwrite=False):
786 if overwrite:
791 if overwrite:
787 self._svncommand(['revert', '--recursive'])
792 self._svncommand(['revert', '--recursive'])
788 args = ['checkout']
793 args = ['checkout']
789 if self._svnversion >= (1, 5):
794 if self._svnversion >= (1, 5):
790 args.append('--force')
795 args.append('--force')
791 # The revision must be specified at the end of the URL to properly
796 # The revision must be specified at the end of the URL to properly
792 # update to a directory which has since been deleted and recreated.
797 # update to a directory which has since been deleted and recreated.
793 args.append('%s@%s' % (state[0], state[1]))
798 args.append('%s@%s' % (state[0], state[1]))
794 status, err = self._svncommand(args, failok=True)
799 status, err = self._svncommand(args, failok=True)
795 if not re.search('Checked out revision [0-9]+.', status):
800 if not re.search('Checked out revision [0-9]+.', status):
796 if ('is already a working copy for a different URL' in err
801 if ('is already a working copy for a different URL' in err
797 and (self._wcchanged()[:2] == (False, False))):
802 and (self._wcchanged()[:2] == (False, False))):
798 # obstructed but clean working copy, so just blow it away.
803 # obstructed but clean working copy, so just blow it away.
799 self.remove()
804 self.remove()
800 self.get(state, overwrite=False)
805 self.get(state, overwrite=False)
801 return
806 return
802 raise util.Abort((status or err).splitlines()[-1])
807 raise util.Abort((status or err).splitlines()[-1])
803 self._ui.status(status)
808 self._ui.status(status)
804
809
805 def merge(self, state):
810 def merge(self, state):
806 old = self._state[1]
811 old = self._state[1]
807 new = state[1]
812 new = state[1]
808 wcrev = self._wcrev()
813 wcrev = self._wcrev()
809 if new != wcrev:
814 if new != wcrev:
810 dirty = old == wcrev or self._wcchanged()[0]
815 dirty = old == wcrev or self._wcchanged()[0]
811 if _updateprompt(self._ui, self, dirty, wcrev, new):
816 if _updateprompt(self._ui, self, dirty, wcrev, new):
812 self.get(state, False)
817 self.get(state, False)
813
818
814 def push(self, opts):
819 def push(self, opts):
815 # push is a no-op for SVN
820 # push is a no-op for SVN
816 return True
821 return True
817
822
818 def files(self):
823 def files(self):
819 output = self._svncommand(['list', '--recursive', '--xml'])[0]
824 output = self._svncommand(['list', '--recursive', '--xml'])[0]
820 doc = xml.dom.minidom.parseString(output)
825 doc = xml.dom.minidom.parseString(output)
821 paths = []
826 paths = []
822 for e in doc.getElementsByTagName('entry'):
827 for e in doc.getElementsByTagName('entry'):
823 kind = str(e.getAttribute('kind'))
828 kind = str(e.getAttribute('kind'))
824 if kind != 'file':
829 if kind != 'file':
825 continue
830 continue
826 name = ''.join(c.data for c
831 name = ''.join(c.data for c
827 in e.getElementsByTagName('name')[0].childNodes
832 in e.getElementsByTagName('name')[0].childNodes
828 if c.nodeType == c.TEXT_NODE)
833 if c.nodeType == c.TEXT_NODE)
829 paths.append(name)
834 paths.append(name)
830 return paths
835 return paths
831
836
832 def filedata(self, name):
837 def filedata(self, name):
833 return self._svncommand(['cat'], name)[0]
838 return self._svncommand(['cat'], name)[0]
834
839
835
840
836 class gitsubrepo(abstractsubrepo):
841 class gitsubrepo(abstractsubrepo):
837 def __init__(self, ctx, path, state):
842 def __init__(self, ctx, path, state):
838 # TODO add git version check.
843 # TODO add git version check.
839 self._state = state
844 self._state = state
840 self._ctx = ctx
845 self._ctx = ctx
841 self._path = path
846 self._path = path
842 self._relpath = os.path.join(reporelpath(ctx._repo), path)
847 self._relpath = os.path.join(reporelpath(ctx._repo), path)
843 self._abspath = ctx._repo.wjoin(path)
848 self._abspath = ctx._repo.wjoin(path)
844 self._subparent = ctx._repo
849 self._subparent = ctx._repo
845 self._ui = ctx._repo.ui
850 self._ui = ctx._repo.ui
846
851
847 def _gitcommand(self, commands, env=None, stream=False):
852 def _gitcommand(self, commands, env=None, stream=False):
848 return self._gitdir(commands, env=env, stream=stream)[0]
853 return self._gitdir(commands, env=env, stream=stream)[0]
849
854
850 def _gitdir(self, commands, env=None, stream=False):
855 def _gitdir(self, commands, env=None, stream=False):
851 return self._gitnodir(commands, env=env, stream=stream,
856 return self._gitnodir(commands, env=env, stream=stream,
852 cwd=self._abspath)
857 cwd=self._abspath)
853
858
854 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
859 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
855 """Calls the git command
860 """Calls the git command
856
861
857 The methods tries to call the git command. versions previor to 1.6.0
862 The methods tries to call the git command. versions previor to 1.6.0
858 are not supported and very probably fail.
863 are not supported and very probably fail.
859 """
864 """
860 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
865 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
861 # unless ui.quiet is set, print git's stderr,
866 # unless ui.quiet is set, print git's stderr,
862 # which is mostly progress and useful info
867 # which is mostly progress and useful info
863 errpipe = None
868 errpipe = None
864 if self._ui.quiet:
869 if self._ui.quiet:
865 errpipe = open(os.devnull, 'w')
870 errpipe = open(os.devnull, 'w')
866 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
871 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
867 close_fds=util.closefds,
872 close_fds=util.closefds,
868 stdout=subprocess.PIPE, stderr=errpipe)
873 stdout=subprocess.PIPE, stderr=errpipe)
869 if stream:
874 if stream:
870 return p.stdout, None
875 return p.stdout, None
871
876
872 retdata = p.stdout.read().strip()
877 retdata = p.stdout.read().strip()
873 # wait for the child to exit to avoid race condition.
878 # wait for the child to exit to avoid race condition.
874 p.wait()
879 p.wait()
875
880
876 if p.returncode != 0 and p.returncode != 1:
881 if p.returncode != 0 and p.returncode != 1:
877 # there are certain error codes that are ok
882 # there are certain error codes that are ok
878 command = commands[0]
883 command = commands[0]
879 if command in ('cat-file', 'symbolic-ref'):
884 if command in ('cat-file', 'symbolic-ref'):
880 return retdata, p.returncode
885 return retdata, p.returncode
881 # for all others, abort
886 # for all others, abort
882 raise util.Abort('git %s error %d in %s' %
887 raise util.Abort('git %s error %d in %s' %
883 (command, p.returncode, self._relpath))
888 (command, p.returncode, self._relpath))
884
889
885 return retdata, p.returncode
890 return retdata, p.returncode
886
891
887 def _gitmissing(self):
892 def _gitmissing(self):
888 return not os.path.exists(os.path.join(self._abspath, '.git'))
893 return not os.path.exists(os.path.join(self._abspath, '.git'))
889
894
890 def _gitstate(self):
895 def _gitstate(self):
891 return self._gitcommand(['rev-parse', 'HEAD'])
896 return self._gitcommand(['rev-parse', 'HEAD'])
892
897
893 def _gitcurrentbranch(self):
898 def _gitcurrentbranch(self):
894 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
899 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
895 if err:
900 if err:
896 current = None
901 current = None
897 return current
902 return current
898
903
899 def _gitremote(self, remote):
904 def _gitremote(self, remote):
900 out = self._gitcommand(['remote', 'show', '-n', remote])
905 out = self._gitcommand(['remote', 'show', '-n', remote])
901 line = out.split('\n')[1]
906 line = out.split('\n')[1]
902 i = line.index('URL: ') + len('URL: ')
907 i = line.index('URL: ') + len('URL: ')
903 return line[i:]
908 return line[i:]
904
909
905 def _githavelocally(self, revision):
910 def _githavelocally(self, revision):
906 out, code = self._gitdir(['cat-file', '-e', revision])
911 out, code = self._gitdir(['cat-file', '-e', revision])
907 return code == 0
912 return code == 0
908
913
909 def _gitisancestor(self, r1, r2):
914 def _gitisancestor(self, r1, r2):
910 base = self._gitcommand(['merge-base', r1, r2])
915 base = self._gitcommand(['merge-base', r1, r2])
911 return base == r1
916 return base == r1
912
917
913 def _gitisbare(self):
918 def _gitisbare(self):
914 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
919 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
915
920
916 def _gitupdatestat(self):
921 def _gitupdatestat(self):
917 """This must be run before git diff-index.
922 """This must be run before git diff-index.
918 diff-index only looks at changes to file stat;
923 diff-index only looks at changes to file stat;
919 this command looks at file contents and updates the stat."""
924 this command looks at file contents and updates the stat."""
920 self._gitcommand(['update-index', '-q', '--refresh'])
925 self._gitcommand(['update-index', '-q', '--refresh'])
921
926
922 def _gitbranchmap(self):
927 def _gitbranchmap(self):
923 '''returns 2 things:
928 '''returns 2 things:
924 a map from git branch to revision
929 a map from git branch to revision
925 a map from revision to branches'''
930 a map from revision to branches'''
926 branch2rev = {}
931 branch2rev = {}
927 rev2branch = {}
932 rev2branch = {}
928
933
929 out = self._gitcommand(['for-each-ref', '--format',
934 out = self._gitcommand(['for-each-ref', '--format',
930 '%(objectname) %(refname)'])
935 '%(objectname) %(refname)'])
931 for line in out.split('\n'):
936 for line in out.split('\n'):
932 revision, ref = line.split(' ')
937 revision, ref = line.split(' ')
933 if (not ref.startswith('refs/heads/') and
938 if (not ref.startswith('refs/heads/') and
934 not ref.startswith('refs/remotes/')):
939 not ref.startswith('refs/remotes/')):
935 continue
940 continue
936 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
941 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
937 continue # ignore remote/HEAD redirects
942 continue # ignore remote/HEAD redirects
938 branch2rev[ref] = revision
943 branch2rev[ref] = revision
939 rev2branch.setdefault(revision, []).append(ref)
944 rev2branch.setdefault(revision, []).append(ref)
940 return branch2rev, rev2branch
945 return branch2rev, rev2branch
941
946
942 def _gittracking(self, branches):
947 def _gittracking(self, branches):
943 'return map of remote branch to local tracking branch'
948 'return map of remote branch to local tracking branch'
944 # assumes no more than one local tracking branch for each remote
949 # assumes no more than one local tracking branch for each remote
945 tracking = {}
950 tracking = {}
946 for b in branches:
951 for b in branches:
947 if b.startswith('refs/remotes/'):
952 if b.startswith('refs/remotes/'):
948 continue
953 continue
949 bname = b.split('/', 2)[2]
954 bname = b.split('/', 2)[2]
950 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
955 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
951 if remote:
956 if remote:
952 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
957 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
953 tracking['refs/remotes/%s/%s' %
958 tracking['refs/remotes/%s/%s' %
954 (remote, ref.split('/', 2)[2])] = b
959 (remote, ref.split('/', 2)[2])] = b
955 return tracking
960 return tracking
956
961
957 def _abssource(self, source):
962 def _abssource(self, source):
958 if '://' not in source:
963 if '://' not in source:
959 # recognize the scp syntax as an absolute source
964 # recognize the scp syntax as an absolute source
960 colon = source.find(':')
965 colon = source.find(':')
961 if colon != -1 and '/' not in source[:colon]:
966 if colon != -1 and '/' not in source[:colon]:
962 return source
967 return source
963 self._subsource = source
968 self._subsource = source
964 return _abssource(self)
969 return _abssource(self)
965
970
966 def _fetch(self, source, revision):
971 def _fetch(self, source, revision):
967 if self._gitmissing():
972 if self._gitmissing():
968 source = self._abssource(source)
973 source = self._abssource(source)
969 self._ui.status(_('cloning subrepo %s from %s\n') %
974 self._ui.status(_('cloning subrepo %s from %s\n') %
970 (self._relpath, source))
975 (self._relpath, source))
971 self._gitnodir(['clone', source, self._abspath])
976 self._gitnodir(['clone', source, self._abspath])
972 if self._githavelocally(revision):
977 if self._githavelocally(revision):
973 return
978 return
974 self._ui.status(_('pulling subrepo %s from %s\n') %
979 self._ui.status(_('pulling subrepo %s from %s\n') %
975 (self._relpath, self._gitremote('origin')))
980 (self._relpath, self._gitremote('origin')))
976 # try only origin: the originally cloned repo
981 # try only origin: the originally cloned repo
977 self._gitcommand(['fetch'])
982 self._gitcommand(['fetch'])
978 if not self._githavelocally(revision):
983 if not self._githavelocally(revision):
979 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
984 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
980 (revision, self._relpath))
985 (revision, self._relpath))
981
986
982 def dirty(self, ignoreupdate=False):
987 def dirty(self, ignoreupdate=False):
983 if self._gitmissing():
988 if self._gitmissing():
984 return self._state[1] != ''
989 return self._state[1] != ''
985 if self._gitisbare():
990 if self._gitisbare():
986 return True
991 return True
987 if not ignoreupdate and self._state[1] != self._gitstate():
992 if not ignoreupdate and self._state[1] != self._gitstate():
988 # different version checked out
993 # different version checked out
989 return True
994 return True
990 # check for staged changes or modified files; ignore untracked files
995 # check for staged changes or modified files; ignore untracked files
991 self._gitupdatestat()
996 self._gitupdatestat()
992 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
997 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
993 return code == 1
998 return code == 1
994
999
995 def basestate(self):
1000 def basestate(self):
996 return self._gitstate()
1001 return self._gitstate()
997
1002
998 def get(self, state, overwrite=False):
1003 def get(self, state, overwrite=False):
999 source, revision, kind = state
1004 source, revision, kind = state
1000 if not revision:
1005 if not revision:
1001 self.remove()
1006 self.remove()
1002 return
1007 return
1003 self._fetch(source, revision)
1008 self._fetch(source, revision)
1004 # if the repo was set to be bare, unbare it
1009 # if the repo was set to be bare, unbare it
1005 if self._gitisbare():
1010 if self._gitisbare():
1006 self._gitcommand(['config', 'core.bare', 'false'])
1011 self._gitcommand(['config', 'core.bare', 'false'])
1007 if self._gitstate() == revision:
1012 if self._gitstate() == revision:
1008 self._gitcommand(['reset', '--hard', 'HEAD'])
1013 self._gitcommand(['reset', '--hard', 'HEAD'])
1009 return
1014 return
1010 elif self._gitstate() == revision:
1015 elif self._gitstate() == revision:
1011 if overwrite:
1016 if overwrite:
1012 # first reset the index to unmark new files for commit, because
1017 # first reset the index to unmark new files for commit, because
1013 # reset --hard will otherwise throw away files added for commit,
1018 # reset --hard will otherwise throw away files added for commit,
1014 # not just unmark them.
1019 # not just unmark them.
1015 self._gitcommand(['reset', 'HEAD'])
1020 self._gitcommand(['reset', 'HEAD'])
1016 self._gitcommand(['reset', '--hard', 'HEAD'])
1021 self._gitcommand(['reset', '--hard', 'HEAD'])
1017 return
1022 return
1018 branch2rev, rev2branch = self._gitbranchmap()
1023 branch2rev, rev2branch = self._gitbranchmap()
1019
1024
1020 def checkout(args):
1025 def checkout(args):
1021 cmd = ['checkout']
1026 cmd = ['checkout']
1022 if overwrite:
1027 if overwrite:
1023 # first reset the index to unmark new files for commit, because
1028 # first reset the index to unmark new files for commit, because
1024 # the -f option will otherwise throw away files added for
1029 # the -f option will otherwise throw away files added for
1025 # commit, not just unmark them.
1030 # commit, not just unmark them.
1026 self._gitcommand(['reset', 'HEAD'])
1031 self._gitcommand(['reset', 'HEAD'])
1027 cmd.append('-f')
1032 cmd.append('-f')
1028 self._gitcommand(cmd + args)
1033 self._gitcommand(cmd + args)
1029
1034
1030 def rawcheckout():
1035 def rawcheckout():
1031 # no branch to checkout, check it out with no branch
1036 # no branch to checkout, check it out with no branch
1032 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1037 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1033 self._relpath)
1038 self._relpath)
1034 self._ui.warn(_('check out a git branch if you intend '
1039 self._ui.warn(_('check out a git branch if you intend '
1035 'to make changes\n'))
1040 'to make changes\n'))
1036 checkout(['-q', revision])
1041 checkout(['-q', revision])
1037
1042
1038 if revision not in rev2branch:
1043 if revision not in rev2branch:
1039 rawcheckout()
1044 rawcheckout()
1040 return
1045 return
1041 branches = rev2branch[revision]
1046 branches = rev2branch[revision]
1042 firstlocalbranch = None
1047 firstlocalbranch = None
1043 for b in branches:
1048 for b in branches:
1044 if b == 'refs/heads/master':
1049 if b == 'refs/heads/master':
1045 # master trumps all other branches
1050 # master trumps all other branches
1046 checkout(['refs/heads/master'])
1051 checkout(['refs/heads/master'])
1047 return
1052 return
1048 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1053 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1049 firstlocalbranch = b
1054 firstlocalbranch = b
1050 if firstlocalbranch:
1055 if firstlocalbranch:
1051 checkout([firstlocalbranch])
1056 checkout([firstlocalbranch])
1052 return
1057 return
1053
1058
1054 tracking = self._gittracking(branch2rev.keys())
1059 tracking = self._gittracking(branch2rev.keys())
1055 # choose a remote branch already tracked if possible
1060 # choose a remote branch already tracked if possible
1056 remote = branches[0]
1061 remote = branches[0]
1057 if remote not in tracking:
1062 if remote not in tracking:
1058 for b in branches:
1063 for b in branches:
1059 if b in tracking:
1064 if b in tracking:
1060 remote = b
1065 remote = b
1061 break
1066 break
1062
1067
1063 if remote not in tracking:
1068 if remote not in tracking:
1064 # create a new local tracking branch
1069 # create a new local tracking branch
1065 local = remote.split('/', 2)[2]
1070 local = remote.split('/', 2)[2]
1066 checkout(['-b', local, remote])
1071 checkout(['-b', local, remote])
1067 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1072 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1068 # When updating to a tracked remote branch,
1073 # When updating to a tracked remote branch,
1069 # if the local tracking branch is downstream of it,
1074 # if the local tracking branch is downstream of it,
1070 # a normal `git pull` would have performed a "fast-forward merge"
1075 # a normal `git pull` would have performed a "fast-forward merge"
1071 # which is equivalent to updating the local branch to the remote.
1076 # which is equivalent to updating the local branch to the remote.
1072 # Since we are only looking at branching at update, we need to
1077 # Since we are only looking at branching at update, we need to
1073 # detect this situation and perform this action lazily.
1078 # detect this situation and perform this action lazily.
1074 if tracking[remote] != self._gitcurrentbranch():
1079 if tracking[remote] != self._gitcurrentbranch():
1075 checkout([tracking[remote]])
1080 checkout([tracking[remote]])
1076 self._gitcommand(['merge', '--ff', remote])
1081 self._gitcommand(['merge', '--ff', remote])
1077 else:
1082 else:
1078 # a real merge would be required, just checkout the revision
1083 # a real merge would be required, just checkout the revision
1079 rawcheckout()
1084 rawcheckout()
1080
1085
1081 def commit(self, text, user, date):
1086 def commit(self, text, user, date):
1082 if self._gitmissing():
1087 if self._gitmissing():
1083 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1088 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1084 cmd = ['commit', '-a', '-m', text]
1089 cmd = ['commit', '-a', '-m', text]
1085 env = os.environ.copy()
1090 env = os.environ.copy()
1086 if user:
1091 if user:
1087 cmd += ['--author', user]
1092 cmd += ['--author', user]
1088 if date:
1093 if date:
1089 # git's date parser silently ignores when seconds < 1e9
1094 # git's date parser silently ignores when seconds < 1e9
1090 # convert to ISO8601
1095 # convert to ISO8601
1091 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1096 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1092 '%Y-%m-%dT%H:%M:%S %1%2')
1097 '%Y-%m-%dT%H:%M:%S %1%2')
1093 self._gitcommand(cmd, env=env)
1098 self._gitcommand(cmd, env=env)
1094 # make sure commit works otherwise HEAD might not exist under certain
1099 # make sure commit works otherwise HEAD might not exist under certain
1095 # circumstances
1100 # circumstances
1096 return self._gitstate()
1101 return self._gitstate()
1097
1102
1098 def merge(self, state):
1103 def merge(self, state):
1099 source, revision, kind = state
1104 source, revision, kind = state
1100 self._fetch(source, revision)
1105 self._fetch(source, revision)
1101 base = self._gitcommand(['merge-base', revision, self._state[1]])
1106 base = self._gitcommand(['merge-base', revision, self._state[1]])
1102 self._gitupdatestat()
1107 self._gitupdatestat()
1103 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1108 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1104
1109
1105 def mergefunc():
1110 def mergefunc():
1106 if base == revision:
1111 if base == revision:
1107 self.get(state) # fast forward merge
1112 self.get(state) # fast forward merge
1108 elif base != self._state[1]:
1113 elif base != self._state[1]:
1109 self._gitcommand(['merge', '--no-commit', revision])
1114 self._gitcommand(['merge', '--no-commit', revision])
1110
1115
1111 if self.dirty():
1116 if self.dirty():
1112 if self._gitstate() != revision:
1117 if self._gitstate() != revision:
1113 dirty = self._gitstate() == self._state[1] or code != 0
1118 dirty = self._gitstate() == self._state[1] or code != 0
1114 if _updateprompt(self._ui, self, dirty,
1119 if _updateprompt(self._ui, self, dirty,
1115 self._state[1][:7], revision[:7]):
1120 self._state[1][:7], revision[:7]):
1116 mergefunc()
1121 mergefunc()
1117 else:
1122 else:
1118 mergefunc()
1123 mergefunc()
1119
1124
1120 def push(self, opts):
1125 def push(self, opts):
1121 force = opts.get('force')
1126 force = opts.get('force')
1122
1127
1123 if not self._state[1]:
1128 if not self._state[1]:
1124 return True
1129 return True
1125 if self._gitmissing():
1130 if self._gitmissing():
1126 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1131 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1127 # if a branch in origin contains the revision, nothing to do
1132 # if a branch in origin contains the revision, nothing to do
1128 branch2rev, rev2branch = self._gitbranchmap()
1133 branch2rev, rev2branch = self._gitbranchmap()
1129 if self._state[1] in rev2branch:
1134 if self._state[1] in rev2branch:
1130 for b in rev2branch[self._state[1]]:
1135 for b in rev2branch[self._state[1]]:
1131 if b.startswith('refs/remotes/origin/'):
1136 if b.startswith('refs/remotes/origin/'):
1132 return True
1137 return True
1133 for b, revision in branch2rev.iteritems():
1138 for b, revision in branch2rev.iteritems():
1134 if b.startswith('refs/remotes/origin/'):
1139 if b.startswith('refs/remotes/origin/'):
1135 if self._gitisancestor(self._state[1], revision):
1140 if self._gitisancestor(self._state[1], revision):
1136 return True
1141 return True
1137 # otherwise, try to push the currently checked out branch
1142 # otherwise, try to push the currently checked out branch
1138 cmd = ['push']
1143 cmd = ['push']
1139 if force:
1144 if force:
1140 cmd.append('--force')
1145 cmd.append('--force')
1141
1146
1142 current = self._gitcurrentbranch()
1147 current = self._gitcurrentbranch()
1143 if current:
1148 if current:
1144 # determine if the current branch is even useful
1149 # determine if the current branch is even useful
1145 if not self._gitisancestor(self._state[1], current):
1150 if not self._gitisancestor(self._state[1], current):
1146 self._ui.warn(_('unrelated git branch checked out '
1151 self._ui.warn(_('unrelated git branch checked out '
1147 'in subrepo %s\n') % self._relpath)
1152 'in subrepo %s\n') % self._relpath)
1148 return False
1153 return False
1149 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1154 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1150 (current.split('/', 2)[2], self._relpath))
1155 (current.split('/', 2)[2], self._relpath))
1151 self._gitcommand(cmd + ['origin', current])
1156 self._gitcommand(cmd + ['origin', current])
1152 return True
1157 return True
1153 else:
1158 else:
1154 self._ui.warn(_('no branch checked out in subrepo %s\n'
1159 self._ui.warn(_('no branch checked out in subrepo %s\n'
1155 'cannot push revision %s') %
1160 'cannot push revision %s') %
1156 (self._relpath, self._state[1]))
1161 (self._relpath, self._state[1]))
1157 return False
1162 return False
1158
1163
1159 def remove(self):
1164 def remove(self):
1160 if self._gitmissing():
1165 if self._gitmissing():
1161 return
1166 return
1162 if self.dirty():
1167 if self.dirty():
1163 self._ui.warn(_('not removing repo %s because '
1168 self._ui.warn(_('not removing repo %s because '
1164 'it has changes.\n') % self._relpath)
1169 'it has changes.\n') % self._relpath)
1165 return
1170 return
1166 # we can't fully delete the repository as it may contain
1171 # we can't fully delete the repository as it may contain
1167 # local-only history
1172 # local-only history
1168 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1173 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1169 self._gitcommand(['config', 'core.bare', 'true'])
1174 self._gitcommand(['config', 'core.bare', 'true'])
1170 for f in os.listdir(self._abspath):
1175 for f in os.listdir(self._abspath):
1171 if f == '.git':
1176 if f == '.git':
1172 continue
1177 continue
1173 path = os.path.join(self._abspath, f)
1178 path = os.path.join(self._abspath, f)
1174 if os.path.isdir(path) and not os.path.islink(path):
1179 if os.path.isdir(path) and not os.path.islink(path):
1175 shutil.rmtree(path)
1180 shutil.rmtree(path)
1176 else:
1181 else:
1177 os.remove(path)
1182 os.remove(path)
1178
1183
1179 def archive(self, ui, archiver, prefix):
1184 def archive(self, ui, archiver, prefix):
1180 source, revision = self._state
1185 source, revision = self._state
1181 if not revision:
1186 if not revision:
1182 return
1187 return
1183 self._fetch(source, revision)
1188 self._fetch(source, revision)
1184
1189
1185 # Parse git's native archive command.
1190 # Parse git's native archive command.
1186 # This should be much faster than manually traversing the trees
1191 # This should be much faster than manually traversing the trees
1187 # and objects with many subprocess calls.
1192 # and objects with many subprocess calls.
1188 tarstream = self._gitcommand(['archive', revision], stream=True)
1193 tarstream = self._gitcommand(['archive', revision], stream=True)
1189 tar = tarfile.open(fileobj=tarstream, mode='r|')
1194 tar = tarfile.open(fileobj=tarstream, mode='r|')
1190 relpath = subrelpath(self)
1195 relpath = subrelpath(self)
1191 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1196 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1192 for i, info in enumerate(tar):
1197 for i, info in enumerate(tar):
1193 if info.isdir():
1198 if info.isdir():
1194 continue
1199 continue
1195 if info.issym():
1200 if info.issym():
1196 data = info.linkname
1201 data = info.linkname
1197 else:
1202 else:
1198 data = tar.extractfile(info).read()
1203 data = tar.extractfile(info).read()
1199 archiver.addfile(os.path.join(prefix, self._path, info.name),
1204 archiver.addfile(os.path.join(prefix, self._path, info.name),
1200 info.mode, info.issym(), data)
1205 info.mode, info.issym(), data)
1201 ui.progress(_('archiving (%s)') % relpath, i + 1,
1206 ui.progress(_('archiving (%s)') % relpath, i + 1,
1202 unit=_('files'))
1207 unit=_('files'))
1203 ui.progress(_('archiving (%s)') % relpath, None)
1208 ui.progress(_('archiving (%s)') % relpath, None)
1204
1209
1205
1210
1206 def status(self, rev2, **opts):
1211 def status(self, rev2, **opts):
1207 rev1 = self._state[1]
1212 rev1 = self._state[1]
1208 if self._gitmissing() or not rev1:
1213 if self._gitmissing() or not rev1:
1209 # if the repo is missing, return no results
1214 # if the repo is missing, return no results
1210 return [], [], [], [], [], [], []
1215 return [], [], [], [], [], [], []
1211 modified, added, removed = [], [], []
1216 modified, added, removed = [], [], []
1212 self._gitupdatestat()
1217 self._gitupdatestat()
1213 if rev2:
1218 if rev2:
1214 command = ['diff-tree', rev1, rev2]
1219 command = ['diff-tree', rev1, rev2]
1215 else:
1220 else:
1216 command = ['diff-index', rev1]
1221 command = ['diff-index', rev1]
1217 out = self._gitcommand(command)
1222 out = self._gitcommand(command)
1218 for line in out.split('\n'):
1223 for line in out.split('\n'):
1219 tab = line.find('\t')
1224 tab = line.find('\t')
1220 if tab == -1:
1225 if tab == -1:
1221 continue
1226 continue
1222 status, f = line[tab - 1], line[tab + 1:]
1227 status, f = line[tab - 1], line[tab + 1:]
1223 if status == 'M':
1228 if status == 'M':
1224 modified.append(f)
1229 modified.append(f)
1225 elif status == 'A':
1230 elif status == 'A':
1226 added.append(f)
1231 added.append(f)
1227 elif status == 'D':
1232 elif status == 'D':
1228 removed.append(f)
1233 removed.append(f)
1229
1234
1230 deleted = unknown = ignored = clean = []
1235 deleted = unknown = ignored = clean = []
1231 return modified, added, removed, deleted, unknown, ignored, clean
1236 return modified, added, removed, deleted, unknown, ignored, clean
1232
1237
1233 types = {
1238 types = {
1234 'hg': hgsubrepo,
1239 'hg': hgsubrepo,
1235 'svn': svnsubrepo,
1240 'svn': svnsubrepo,
1236 'git': gitsubrepo,
1241 'git': gitsubrepo,
1237 }
1242 }
@@ -1,60 +1,69
1 $ hg init repo
1 $ hg init repo
2 $ cd repo
2 $ cd repo
3 $ hg init subrepo
3 $ hg init subrepo
4 $ echo a > subrepo/a
4 $ echo a > subrepo/a
5 $ hg -R subrepo ci -Am adda
5 $ hg -R subrepo ci -Am adda
6 adding a
6 adding a
7 $ echo 'subrepo = subrepo' > .hgsub
7 $ echo 'subrepo = subrepo' > .hgsub
8 $ hg ci -Am addsubrepo
8 $ hg ci -Am addsubrepo
9 adding .hgsub
9 adding .hgsub
10 $ echo b > subrepo/b
10 $ echo b > subrepo/b
11 $ hg -R subrepo ci -Am addb
11 $ hg -R subrepo ci -Am addb
12 adding b
12 adding b
13 $ hg ci -m updatedsub
13 $ hg ci -m updatedsub
14
14
15 ignore blanklines in .hgsubstate
15 ignore blanklines in .hgsubstate
16
16
17 >>> file('.hgsubstate', 'wb').write('\n\n \t \n \n')
17 >>> file('.hgsubstate', 'wb').write('\n\n \t \n \n')
18 $ hg st --subrepos
18 $ hg st --subrepos
19 M .hgsubstate
19 M .hgsubstate
20 $ hg revert -qC .hgsubstate
20 $ hg revert -qC .hgsubstate
21
21
22 abort more gracefully on .hgsubstate parsing error
23
24 $ cp .hgsubstate .hgsubstate.old
25 >>> file('.hgsubstate', 'wb').write('\ninvalid')
26 $ hg st --subrepos
27 abort: invalid subrepository revision specifier in .hgsubstate line 2
28 [255]
29 $ mv .hgsubstate.old .hgsubstate
30
22 delete .hgsub and revert it
31 delete .hgsub and revert it
23
32
24 $ rm .hgsub
33 $ rm .hgsub
25 $ hg revert .hgsub
34 $ hg revert .hgsub
26 warning: subrepo spec file .hgsub not found
35 warning: subrepo spec file .hgsub not found
27 warning: subrepo spec file .hgsub not found
36 warning: subrepo spec file .hgsub not found
28
37
29 delete .hgsubstate and revert it
38 delete .hgsubstate and revert it
30
39
31 $ rm .hgsubstate
40 $ rm .hgsubstate
32 $ hg revert .hgsubstate
41 $ hg revert .hgsubstate
33
42
34 delete .hgsub and update
43 delete .hgsub and update
35
44
36 $ rm .hgsub
45 $ rm .hgsub
37 $ hg up 0
46 $ hg up 0
38 warning: subrepo spec file .hgsub not found
47 warning: subrepo spec file .hgsub not found
39 warning: subrepo spec file .hgsub not found
48 warning: subrepo spec file .hgsub not found
40 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
49 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
41 $ hg st
50 $ hg st
42 warning: subrepo spec file .hgsub not found
51 warning: subrepo spec file .hgsub not found
43 ! .hgsub
52 ! .hgsub
44 $ ls subrepo
53 $ ls subrepo
45 a
54 a
46
55
47 delete .hgsubstate and update
56 delete .hgsubstate and update
48
57
49 $ hg up -C
58 $ hg up -C
50 warning: subrepo spec file .hgsub not found
59 warning: subrepo spec file .hgsub not found
51 warning: subrepo spec file .hgsub not found
60 warning: subrepo spec file .hgsub not found
52 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
61 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 $ rm .hgsubstate
62 $ rm .hgsubstate
54 $ hg up 0
63 $ hg up 0
55 remote changed .hgsubstate which local deleted
64 remote changed .hgsubstate which local deleted
56 use (c)hanged version or leave (d)eleted? c
65 use (c)hanged version or leave (d)eleted? c
57 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
66 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
58 $ hg st
67 $ hg st
59 $ ls subrepo
68 $ ls subrepo
60 a
69 a
General Comments 0
You need to be logged in to leave comments. Login now