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