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