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