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