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