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