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