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