##// END OF EJS Templates
subrepo: make 'in subrepo' string easier to find by external tools...
Angel Ezquerra -
r18297:7196f11c default
parent child Browse files
Show More
@@ -1,1329 +1,1329
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, match as matchmod
11 import config, scmutil, util, node, error, cmdutil, bookmarks, match as matchmod
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 class SubrepoAbort(error.Abort):
17 class SubrepoAbort(error.Abort):
18 """Exception class used to avoid handling a subrepo error more than once"""
18 """Exception class used to avoid handling a subrepo error more than once"""
19 def __init__(self, *args, **kw):
19 def __init__(self, *args, **kw):
20 error.Abort.__init__(self, *args, **kw)
20 error.Abort.__init__(self, *args, **kw)
21 self.subrepo = kw.get('subrepo')
21 self.subrepo = kw.get('subrepo')
22
22
23 def annotatesubrepoerror(func):
23 def annotatesubrepoerror(func):
24 def decoratedmethod(self, *args, **kargs):
24 def decoratedmethod(self, *args, **kargs):
25 try:
25 try:
26 res = func(self, *args, **kargs)
26 res = func(self, *args, **kargs)
27 except SubrepoAbort, ex:
27 except SubrepoAbort, ex:
28 # This exception has already been handled
28 # This exception has already been handled
29 raise ex
29 raise ex
30 except error.Abort, ex:
30 except error.Abort, ex:
31 subrepo = subrelpath(self)
31 subrepo = subrelpath(self)
32 errormsg = _('%s (in subrepo %s)') % (str(ex), subrepo)
32 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
33 # avoid handling this exception by raising a SubrepoAbort exception
33 # avoid handling this exception by raising a SubrepoAbort exception
34 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo)
34 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo)
35 return res
35 return res
36 return decoratedmethod
36 return decoratedmethod
37
37
38 def state(ctx, ui):
38 def state(ctx, ui):
39 """return a state dict, mapping subrepo paths configured in .hgsub
39 """return a state dict, mapping subrepo paths configured in .hgsub
40 to tuple: (source from .hgsub, revision from .hgsubstate, kind
40 to tuple: (source from .hgsub, revision from .hgsubstate, kind
41 (key in types dict))
41 (key in types dict))
42 """
42 """
43 p = config.config()
43 p = config.config()
44 def read(f, sections=None, remap=None):
44 def read(f, sections=None, remap=None):
45 if f in ctx:
45 if f in ctx:
46 try:
46 try:
47 data = ctx[f].data()
47 data = ctx[f].data()
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 # handle missing subrepo spec files as removed
51 # handle missing subrepo spec files as removed
52 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
52 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
53 return
53 return
54 p.parse(f, data, sections, remap, read)
54 p.parse(f, data, sections, remap, read)
55 else:
55 else:
56 raise util.Abort(_("subrepo spec file %s not found") % f)
56 raise util.Abort(_("subrepo spec file %s not found") % f)
57
57
58 if '.hgsub' in ctx:
58 if '.hgsub' in ctx:
59 read('.hgsub')
59 read('.hgsub')
60
60
61 for path, src in ui.configitems('subpaths'):
61 for path, src in ui.configitems('subpaths'):
62 p.set('subpaths', path, src, ui.configsource('subpaths', path))
62 p.set('subpaths', path, src, ui.configsource('subpaths', path))
63
63
64 rev = {}
64 rev = {}
65 if '.hgsubstate' in ctx:
65 if '.hgsubstate' in ctx:
66 try:
66 try:
67 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
67 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
68 l = l.lstrip()
68 l = l.lstrip()
69 if not l:
69 if not l:
70 continue
70 continue
71 try:
71 try:
72 revision, path = l.split(" ", 1)
72 revision, path = l.split(" ", 1)
73 except ValueError:
73 except ValueError:
74 raise util.Abort(_("invalid subrepository revision "
74 raise util.Abort(_("invalid subrepository revision "
75 "specifier in .hgsubstate line %d")
75 "specifier in .hgsubstate line %d")
76 % (i + 1))
76 % (i + 1))
77 rev[path] = revision
77 rev[path] = revision
78 except IOError, err:
78 except IOError, err:
79 if err.errno != errno.ENOENT:
79 if err.errno != errno.ENOENT:
80 raise
80 raise
81
81
82 def remap(src):
82 def remap(src):
83 for pattern, repl in p.items('subpaths'):
83 for pattern, repl in p.items('subpaths'):
84 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
84 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
85 # does a string decode.
85 # does a string decode.
86 repl = repl.encode('string-escape')
86 repl = repl.encode('string-escape')
87 # However, we still want to allow back references to go
87 # However, we still want to allow back references to go
88 # through unharmed, so we turn r'\\1' into r'\1'. Again,
88 # through unharmed, so we turn r'\\1' into r'\1'. Again,
89 # extra escapes are needed because re.sub string decodes.
89 # extra escapes are needed because re.sub string decodes.
90 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
90 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
91 try:
91 try:
92 src = re.sub(pattern, repl, src, 1)
92 src = re.sub(pattern, repl, src, 1)
93 except re.error, e:
93 except re.error, e:
94 raise util.Abort(_("bad subrepository pattern in %s: %s")
94 raise util.Abort(_("bad subrepository pattern in %s: %s")
95 % (p.source('subpaths', pattern), e))
95 % (p.source('subpaths', pattern), e))
96 return src
96 return src
97
97
98 state = {}
98 state = {}
99 for path, src in p[''].items():
99 for path, src in p[''].items():
100 kind = 'hg'
100 kind = 'hg'
101 if src.startswith('['):
101 if src.startswith('['):
102 if ']' not in src:
102 if ']' not in src:
103 raise util.Abort(_('missing ] in subrepo source'))
103 raise util.Abort(_('missing ] in subrepo source'))
104 kind, src = src.split(']', 1)
104 kind, src = src.split(']', 1)
105 kind = kind[1:]
105 kind = kind[1:]
106 src = src.lstrip() # strip any extra whitespace after ']'
106 src = src.lstrip() # strip any extra whitespace after ']'
107
107
108 if not util.url(src).isabs():
108 if not util.url(src).isabs():
109 parent = _abssource(ctx._repo, abort=False)
109 parent = _abssource(ctx._repo, abort=False)
110 if parent:
110 if parent:
111 parent = util.url(parent)
111 parent = util.url(parent)
112 parent.path = posixpath.join(parent.path or '', src)
112 parent.path = posixpath.join(parent.path or '', src)
113 parent.path = posixpath.normpath(parent.path)
113 parent.path = posixpath.normpath(parent.path)
114 joined = str(parent)
114 joined = str(parent)
115 # Remap the full joined path and use it if it changes,
115 # Remap the full joined path and use it if it changes,
116 # else remap the original source.
116 # else remap the original source.
117 remapped = remap(joined)
117 remapped = remap(joined)
118 if remapped == joined:
118 if remapped == joined:
119 src = remap(src)
119 src = remap(src)
120 else:
120 else:
121 src = remapped
121 src = remapped
122
122
123 src = remap(src)
123 src = remap(src)
124 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
124 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
125
125
126 return state
126 return state
127
127
128 def writestate(repo, state):
128 def writestate(repo, state):
129 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
129 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
130 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
130 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)]
131 repo.wwrite('.hgsubstate', ''.join(lines), '')
131 repo.wwrite('.hgsubstate', ''.join(lines), '')
132
132
133 def submerge(repo, wctx, mctx, actx, overwrite):
133 def submerge(repo, wctx, mctx, actx, overwrite):
134 """delegated from merge.applyupdates: merging of .hgsubstate file
134 """delegated from merge.applyupdates: merging of .hgsubstate file
135 in working context, merging context and ancestor context"""
135 in working context, merging context and ancestor context"""
136 if mctx == actx: # backwards?
136 if mctx == actx: # backwards?
137 actx = wctx.p1()
137 actx = wctx.p1()
138 s1 = wctx.substate
138 s1 = wctx.substate
139 s2 = mctx.substate
139 s2 = mctx.substate
140 sa = actx.substate
140 sa = actx.substate
141 sm = {}
141 sm = {}
142
142
143 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
143 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
144
144
145 def debug(s, msg, r=""):
145 def debug(s, msg, r=""):
146 if r:
146 if r:
147 r = "%s:%s:%s" % r
147 r = "%s:%s:%s" % r
148 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
148 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
149
149
150 for s, l in s1.items():
150 for s, l in s1.items():
151 a = sa.get(s, nullstate)
151 a = sa.get(s, nullstate)
152 ld = l # local state with possible dirty flag for compares
152 ld = l # local state with possible dirty flag for compares
153 if wctx.sub(s).dirty():
153 if wctx.sub(s).dirty():
154 ld = (l[0], l[1] + "+")
154 ld = (l[0], l[1] + "+")
155 if wctx == actx: # overwrite
155 if wctx == actx: # overwrite
156 a = ld
156 a = ld
157
157
158 if s in s2:
158 if s in s2:
159 r = s2[s]
159 r = s2[s]
160 if ld == r or r == a: # no change or local is newer
160 if ld == r or r == a: # no change or local is newer
161 sm[s] = l
161 sm[s] = l
162 continue
162 continue
163 elif ld == a: # other side changed
163 elif ld == a: # other side changed
164 debug(s, "other changed, get", r)
164 debug(s, "other changed, get", r)
165 wctx.sub(s).get(r, overwrite)
165 wctx.sub(s).get(r, overwrite)
166 sm[s] = r
166 sm[s] = r
167 elif ld[0] != r[0]: # sources differ
167 elif ld[0] != r[0]: # sources differ
168 if repo.ui.promptchoice(
168 if repo.ui.promptchoice(
169 _(' subrepository sources for %s differ\n'
169 _(' subrepository sources for %s differ\n'
170 'use (l)ocal source (%s) or (r)emote source (%s)?')
170 'use (l)ocal source (%s) or (r)emote source (%s)?')
171 % (s, l[0], r[0]),
171 % (s, l[0], r[0]),
172 (_('&Local'), _('&Remote')), 0):
172 (_('&Local'), _('&Remote')), 0):
173 debug(s, "prompt changed, get", r)
173 debug(s, "prompt changed, get", r)
174 wctx.sub(s).get(r, overwrite)
174 wctx.sub(s).get(r, overwrite)
175 sm[s] = r
175 sm[s] = r
176 elif ld[1] == a[1]: # local side is unchanged
176 elif ld[1] == a[1]: # local side is unchanged
177 debug(s, "other side changed, get", r)
177 debug(s, "other side changed, get", r)
178 wctx.sub(s).get(r, overwrite)
178 wctx.sub(s).get(r, overwrite)
179 sm[s] = r
179 sm[s] = r
180 else:
180 else:
181 debug(s, "both sides changed, merge with", r)
181 debug(s, "both sides changed, merge with", r)
182 wctx.sub(s).merge(r)
182 wctx.sub(s).merge(r)
183 sm[s] = l
183 sm[s] = l
184 elif ld == a: # remote removed, local unchanged
184 elif ld == a: # remote removed, local unchanged
185 debug(s, "remote removed, remove")
185 debug(s, "remote removed, remove")
186 wctx.sub(s).remove()
186 wctx.sub(s).remove()
187 elif a == nullstate: # not present in remote or ancestor
187 elif a == nullstate: # not present in remote or ancestor
188 debug(s, "local added, keep")
188 debug(s, "local added, keep")
189 sm[s] = l
189 sm[s] = l
190 continue
190 continue
191 else:
191 else:
192 if repo.ui.promptchoice(
192 if repo.ui.promptchoice(
193 _(' local changed subrepository %s which remote removed\n'
193 _(' local changed subrepository %s which remote removed\n'
194 'use (c)hanged version or (d)elete?') % s,
194 'use (c)hanged version or (d)elete?') % s,
195 (_('&Changed'), _('&Delete')), 0):
195 (_('&Changed'), _('&Delete')), 0):
196 debug(s, "prompt remove")
196 debug(s, "prompt remove")
197 wctx.sub(s).remove()
197 wctx.sub(s).remove()
198
198
199 for s, r in sorted(s2.items()):
199 for s, r in sorted(s2.items()):
200 if s in s1:
200 if s in s1:
201 continue
201 continue
202 elif s not in sa:
202 elif s not in sa:
203 debug(s, "remote added, get", r)
203 debug(s, "remote added, get", r)
204 mctx.sub(s).get(r)
204 mctx.sub(s).get(r)
205 sm[s] = r
205 sm[s] = r
206 elif r != sa[s]:
206 elif r != sa[s]:
207 if repo.ui.promptchoice(
207 if repo.ui.promptchoice(
208 _(' remote changed subrepository %s which local removed\n'
208 _(' remote changed subrepository %s which local removed\n'
209 'use (c)hanged version or (d)elete?') % s,
209 'use (c)hanged version or (d)elete?') % s,
210 (_('&Changed'), _('&Delete')), 0) == 0:
210 (_('&Changed'), _('&Delete')), 0) == 0:
211 debug(s, "prompt recreate", r)
211 debug(s, "prompt recreate", r)
212 wctx.sub(s).get(r)
212 wctx.sub(s).get(r)
213 sm[s] = r
213 sm[s] = r
214
214
215 # record merged .hgsubstate
215 # record merged .hgsubstate
216 writestate(repo, sm)
216 writestate(repo, sm)
217
217
218 def _updateprompt(ui, sub, dirty, local, remote):
218 def _updateprompt(ui, sub, dirty, local, remote):
219 if dirty:
219 if dirty:
220 msg = (_(' subrepository sources for %s differ\n'
220 msg = (_(' subrepository sources for %s differ\n'
221 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
221 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
222 % (subrelpath(sub), local, remote))
222 % (subrelpath(sub), local, remote))
223 else:
223 else:
224 msg = (_(' subrepository sources for %s differ (in checked out '
224 msg = (_(' subrepository sources for %s differ (in checked out '
225 'version)\n'
225 'version)\n'
226 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
226 'use (l)ocal source (%s) or (r)emote source (%s)?\n')
227 % (subrelpath(sub), local, remote))
227 % (subrelpath(sub), local, remote))
228 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
228 return ui.promptchoice(msg, (_('&Local'), _('&Remote')), 0)
229
229
230 def reporelpath(repo):
230 def reporelpath(repo):
231 """return path to this (sub)repo as seen from outermost repo"""
231 """return path to this (sub)repo as seen from outermost repo"""
232 parent = repo
232 parent = repo
233 while util.safehasattr(parent, '_subparent'):
233 while util.safehasattr(parent, '_subparent'):
234 parent = parent._subparent
234 parent = parent._subparent
235 p = parent.root.rstrip(os.sep)
235 p = parent.root.rstrip(os.sep)
236 return repo.root[len(p) + 1:]
236 return repo.root[len(p) + 1:]
237
237
238 def subrelpath(sub):
238 def subrelpath(sub):
239 """return path to this subrepo as seen from outermost repo"""
239 """return path to this subrepo as seen from outermost repo"""
240 if util.safehasattr(sub, '_relpath'):
240 if util.safehasattr(sub, '_relpath'):
241 return sub._relpath
241 return sub._relpath
242 if not util.safehasattr(sub, '_repo'):
242 if not util.safehasattr(sub, '_repo'):
243 return sub._path
243 return sub._path
244 return reporelpath(sub._repo)
244 return reporelpath(sub._repo)
245
245
246 def _abssource(repo, push=False, abort=True):
246 def _abssource(repo, push=False, abort=True):
247 """return pull/push path of repo - either based on parent repo .hgsub info
247 """return pull/push path of repo - either based on parent repo .hgsub info
248 or on the top repo config. Abort or return None if no source found."""
248 or on the top repo config. Abort or return None if no source found."""
249 if util.safehasattr(repo, '_subparent'):
249 if util.safehasattr(repo, '_subparent'):
250 source = util.url(repo._subsource)
250 source = util.url(repo._subsource)
251 if source.isabs():
251 if source.isabs():
252 return str(source)
252 return str(source)
253 source.path = posixpath.normpath(source.path)
253 source.path = posixpath.normpath(source.path)
254 parent = _abssource(repo._subparent, push, abort=False)
254 parent = _abssource(repo._subparent, push, abort=False)
255 if parent:
255 if parent:
256 parent = util.url(util.pconvert(parent))
256 parent = util.url(util.pconvert(parent))
257 parent.path = posixpath.join(parent.path or '', source.path)
257 parent.path = posixpath.join(parent.path or '', source.path)
258 parent.path = posixpath.normpath(parent.path)
258 parent.path = posixpath.normpath(parent.path)
259 return str(parent)
259 return str(parent)
260 else: # recursion reached top repo
260 else: # recursion reached top repo
261 if util.safehasattr(repo, '_subtoppath'):
261 if util.safehasattr(repo, '_subtoppath'):
262 return repo._subtoppath
262 return repo._subtoppath
263 if push and repo.ui.config('paths', 'default-push'):
263 if push and repo.ui.config('paths', 'default-push'):
264 return repo.ui.config('paths', 'default-push')
264 return repo.ui.config('paths', 'default-push')
265 if repo.ui.config('paths', 'default'):
265 if repo.ui.config('paths', 'default'):
266 return repo.ui.config('paths', 'default')
266 return repo.ui.config('paths', 'default')
267 if abort:
267 if abort:
268 raise util.Abort(_("default path for subrepository not found"))
268 raise util.Abort(_("default path for subrepository not found"))
269
269
270 def itersubrepos(ctx1, ctx2):
270 def itersubrepos(ctx1, ctx2):
271 """find subrepos in ctx1 or ctx2"""
271 """find subrepos in ctx1 or ctx2"""
272 # Create a (subpath, ctx) mapping where we prefer subpaths from
272 # Create a (subpath, ctx) mapping where we prefer subpaths from
273 # ctx1. The subpaths from ctx2 are important when the .hgsub file
273 # ctx1. The subpaths from ctx2 are important when the .hgsub file
274 # has been modified (in ctx2) but not yet committed (in ctx1).
274 # has been modified (in ctx2) but not yet committed (in ctx1).
275 subpaths = dict.fromkeys(ctx2.substate, ctx2)
275 subpaths = dict.fromkeys(ctx2.substate, ctx2)
276 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
276 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
277 for subpath, ctx in sorted(subpaths.iteritems()):
277 for subpath, ctx in sorted(subpaths.iteritems()):
278 yield subpath, ctx.sub(subpath)
278 yield subpath, ctx.sub(subpath)
279
279
280 def subrepo(ctx, path):
280 def subrepo(ctx, path):
281 """return instance of the right subrepo class for subrepo in path"""
281 """return instance of the right subrepo class for subrepo in path"""
282 # subrepo inherently violates our import layering rules
282 # subrepo inherently violates our import layering rules
283 # because it wants to make repo objects from deep inside the stack
283 # because it wants to make repo objects from deep inside the stack
284 # so we manually delay the circular imports to not break
284 # so we manually delay the circular imports to not break
285 # scripts that don't use our demand-loading
285 # scripts that don't use our demand-loading
286 global hg
286 global hg
287 import hg as h
287 import hg as h
288 hg = h
288 hg = h
289
289
290 scmutil.pathauditor(ctx._repo.root)(path)
290 scmutil.pathauditor(ctx._repo.root)(path)
291 state = ctx.substate[path]
291 state = ctx.substate[path]
292 if state[2] not in types:
292 if state[2] not in types:
293 raise util.Abort(_('unknown subrepo type %s') % state[2])
293 raise util.Abort(_('unknown subrepo type %s') % state[2])
294 return types[state[2]](ctx, path, state[:2])
294 return types[state[2]](ctx, path, state[:2])
295
295
296 # subrepo classes need to implement the following abstract class:
296 # subrepo classes need to implement the following abstract class:
297
297
298 class abstractsubrepo(object):
298 class abstractsubrepo(object):
299
299
300 def dirty(self, ignoreupdate=False):
300 def dirty(self, ignoreupdate=False):
301 """returns true if the dirstate of the subrepo is dirty or does not
301 """returns true if the dirstate of the subrepo is dirty or does not
302 match current stored state. If ignoreupdate is true, only check
302 match current stored state. If ignoreupdate is true, only check
303 whether the subrepo has uncommitted changes in its dirstate.
303 whether the subrepo has uncommitted changes in its dirstate.
304 """
304 """
305 raise NotImplementedError
305 raise NotImplementedError
306
306
307 def basestate(self):
307 def basestate(self):
308 """current working directory base state, disregarding .hgsubstate
308 """current working directory base state, disregarding .hgsubstate
309 state and working directory modifications"""
309 state and working directory modifications"""
310 raise NotImplementedError
310 raise NotImplementedError
311
311
312 def checknested(self, path):
312 def checknested(self, path):
313 """check if path is a subrepository within this repository"""
313 """check if path is a subrepository within this repository"""
314 return False
314 return False
315
315
316 def commit(self, text, user, date):
316 def commit(self, text, user, date):
317 """commit the current changes to the subrepo with the given
317 """commit the current changes to the subrepo with the given
318 log message. Use given user and date if possible. Return the
318 log message. Use given user and date if possible. Return the
319 new state of the subrepo.
319 new state of the subrepo.
320 """
320 """
321 raise NotImplementedError
321 raise NotImplementedError
322
322
323 def remove(self):
323 def remove(self):
324 """remove the subrepo
324 """remove the subrepo
325
325
326 (should verify the dirstate is not dirty first)
326 (should verify the dirstate is not dirty first)
327 """
327 """
328 raise NotImplementedError
328 raise NotImplementedError
329
329
330 def get(self, state, overwrite=False):
330 def get(self, state, overwrite=False):
331 """run whatever commands are needed to put the subrepo into
331 """run whatever commands are needed to put the subrepo into
332 this state
332 this state
333 """
333 """
334 raise NotImplementedError
334 raise NotImplementedError
335
335
336 def merge(self, state):
336 def merge(self, state):
337 """merge currently-saved state with the new state."""
337 """merge currently-saved state with the new state."""
338 raise NotImplementedError
338 raise NotImplementedError
339
339
340 def push(self, opts):
340 def push(self, opts):
341 """perform whatever action is analogous to 'hg push'
341 """perform whatever action is analogous to 'hg push'
342
342
343 This may be a no-op on some systems.
343 This may be a no-op on some systems.
344 """
344 """
345 raise NotImplementedError
345 raise NotImplementedError
346
346
347 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
347 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
348 return []
348 return []
349
349
350 def status(self, rev2, **opts):
350 def status(self, rev2, **opts):
351 return [], [], [], [], [], [], []
351 return [], [], [], [], [], [], []
352
352
353 def diff(self, ui, diffopts, node2, match, prefix, **opts):
353 def diff(self, ui, diffopts, node2, match, prefix, **opts):
354 pass
354 pass
355
355
356 def outgoing(self, ui, dest, opts):
356 def outgoing(self, ui, dest, opts):
357 return 1
357 return 1
358
358
359 def incoming(self, ui, source, opts):
359 def incoming(self, ui, source, opts):
360 return 1
360 return 1
361
361
362 def files(self):
362 def files(self):
363 """return filename iterator"""
363 """return filename iterator"""
364 raise NotImplementedError
364 raise NotImplementedError
365
365
366 def filedata(self, name):
366 def filedata(self, name):
367 """return file data"""
367 """return file data"""
368 raise NotImplementedError
368 raise NotImplementedError
369
369
370 def fileflags(self, name):
370 def fileflags(self, name):
371 """return file flags"""
371 """return file flags"""
372 return ''
372 return ''
373
373
374 def archive(self, ui, archiver, prefix, match=None):
374 def archive(self, ui, archiver, prefix, match=None):
375 if match is not None:
375 if match is not None:
376 files = [f for f in self.files() if match(f)]
376 files = [f for f in self.files() if match(f)]
377 else:
377 else:
378 files = self.files()
378 files = self.files()
379 total = len(files)
379 total = len(files)
380 relpath = subrelpath(self)
380 relpath = subrelpath(self)
381 ui.progress(_('archiving (%s)') % relpath, 0,
381 ui.progress(_('archiving (%s)') % relpath, 0,
382 unit=_('files'), total=total)
382 unit=_('files'), total=total)
383 for i, name in enumerate(files):
383 for i, name in enumerate(files):
384 flags = self.fileflags(name)
384 flags = self.fileflags(name)
385 mode = 'x' in flags and 0755 or 0644
385 mode = 'x' in flags and 0755 or 0644
386 symlink = 'l' in flags
386 symlink = 'l' in flags
387 archiver.addfile(os.path.join(prefix, self._path, name),
387 archiver.addfile(os.path.join(prefix, self._path, name),
388 mode, symlink, self.filedata(name))
388 mode, symlink, self.filedata(name))
389 ui.progress(_('archiving (%s)') % relpath, i + 1,
389 ui.progress(_('archiving (%s)') % relpath, i + 1,
390 unit=_('files'), total=total)
390 unit=_('files'), total=total)
391 ui.progress(_('archiving (%s)') % relpath, None)
391 ui.progress(_('archiving (%s)') % relpath, None)
392
392
393 def walk(self, match):
393 def walk(self, match):
394 '''
394 '''
395 walk recursively through the directory tree, finding all files
395 walk recursively through the directory tree, finding all files
396 matched by the match function
396 matched by the match function
397 '''
397 '''
398 pass
398 pass
399
399
400 def forget(self, ui, match, prefix):
400 def forget(self, ui, match, prefix):
401 return ([], [])
401 return ([], [])
402
402
403 def revert(self, ui, substate, *pats, **opts):
403 def revert(self, ui, substate, *pats, **opts):
404 ui.warn('%s: reverting %s subrepos is unsupported\n' \
404 ui.warn('%s: reverting %s subrepos is unsupported\n' \
405 % (substate[0], substate[2]))
405 % (substate[0], substate[2]))
406 return []
406 return []
407
407
408 class hgsubrepo(abstractsubrepo):
408 class hgsubrepo(abstractsubrepo):
409 def __init__(self, ctx, path, state):
409 def __init__(self, ctx, path, state):
410 self._path = path
410 self._path = path
411 self._state = state
411 self._state = state
412 r = ctx._repo
412 r = ctx._repo
413 root = r.wjoin(path)
413 root = r.wjoin(path)
414 create = False
414 create = False
415 if not os.path.exists(os.path.join(root, '.hg')):
415 if not os.path.exists(os.path.join(root, '.hg')):
416 create = True
416 create = True
417 util.makedirs(root)
417 util.makedirs(root)
418 self._repo = hg.repository(r.baseui, root, create=create)
418 self._repo = hg.repository(r.baseui, root, create=create)
419 for s, k in [('ui', 'commitsubrepos')]:
419 for s, k in [('ui', 'commitsubrepos')]:
420 v = r.ui.config(s, k)
420 v = r.ui.config(s, k)
421 if v:
421 if v:
422 self._repo.ui.setconfig(s, k, v)
422 self._repo.ui.setconfig(s, k, v)
423 self._initrepo(r, state[0], create)
423 self._initrepo(r, state[0], create)
424
424
425 @annotatesubrepoerror
425 @annotatesubrepoerror
426 def _initrepo(self, parentrepo, source, create):
426 def _initrepo(self, parentrepo, source, create):
427 self._repo._subparent = parentrepo
427 self._repo._subparent = parentrepo
428 self._repo._subsource = source
428 self._repo._subsource = source
429
429
430 if create:
430 if create:
431 fp = self._repo.opener("hgrc", "w", text=True)
431 fp = self._repo.opener("hgrc", "w", text=True)
432 fp.write('[paths]\n')
432 fp.write('[paths]\n')
433
433
434 def addpathconfig(key, value):
434 def addpathconfig(key, value):
435 if value:
435 if value:
436 fp.write('%s = %s\n' % (key, value))
436 fp.write('%s = %s\n' % (key, value))
437 self._repo.ui.setconfig('paths', key, value)
437 self._repo.ui.setconfig('paths', key, value)
438
438
439 defpath = _abssource(self._repo, abort=False)
439 defpath = _abssource(self._repo, abort=False)
440 defpushpath = _abssource(self._repo, True, abort=False)
440 defpushpath = _abssource(self._repo, True, abort=False)
441 addpathconfig('default', defpath)
441 addpathconfig('default', defpath)
442 if defpath != defpushpath:
442 if defpath != defpushpath:
443 addpathconfig('default-push', defpushpath)
443 addpathconfig('default-push', defpushpath)
444 fp.close()
444 fp.close()
445
445
446 @annotatesubrepoerror
446 @annotatesubrepoerror
447 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
447 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
448 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
448 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
449 os.path.join(prefix, self._path), explicitonly)
449 os.path.join(prefix, self._path), explicitonly)
450
450
451 @annotatesubrepoerror
451 @annotatesubrepoerror
452 def status(self, rev2, **opts):
452 def status(self, rev2, **opts):
453 try:
453 try:
454 rev1 = self._state[1]
454 rev1 = self._state[1]
455 ctx1 = self._repo[rev1]
455 ctx1 = self._repo[rev1]
456 ctx2 = self._repo[rev2]
456 ctx2 = self._repo[rev2]
457 return self._repo.status(ctx1, ctx2, **opts)
457 return self._repo.status(ctx1, ctx2, **opts)
458 except error.RepoLookupError, inst:
458 except error.RepoLookupError, inst:
459 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
459 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
460 % (inst, subrelpath(self)))
460 % (inst, subrelpath(self)))
461 return [], [], [], [], [], [], []
461 return [], [], [], [], [], [], []
462
462
463 @annotatesubrepoerror
463 @annotatesubrepoerror
464 def diff(self, ui, diffopts, node2, match, prefix, **opts):
464 def diff(self, ui, diffopts, node2, match, prefix, **opts):
465 try:
465 try:
466 node1 = node.bin(self._state[1])
466 node1 = node.bin(self._state[1])
467 # We currently expect node2 to come from substate and be
467 # We currently expect node2 to come from substate and be
468 # in hex format
468 # in hex format
469 if node2 is not None:
469 if node2 is not None:
470 node2 = node.bin(node2)
470 node2 = node.bin(node2)
471 cmdutil.diffordiffstat(ui, self._repo, diffopts,
471 cmdutil.diffordiffstat(ui, self._repo, diffopts,
472 node1, node2, match,
472 node1, node2, match,
473 prefix=posixpath.join(prefix, self._path),
473 prefix=posixpath.join(prefix, self._path),
474 listsubrepos=True, **opts)
474 listsubrepos=True, **opts)
475 except error.RepoLookupError, inst:
475 except error.RepoLookupError, inst:
476 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
476 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
477 % (inst, subrelpath(self)))
477 % (inst, subrelpath(self)))
478
478
479 @annotatesubrepoerror
479 @annotatesubrepoerror
480 def archive(self, ui, archiver, prefix, match=None):
480 def archive(self, ui, archiver, prefix, match=None):
481 self._get(self._state + ('hg',))
481 self._get(self._state + ('hg',))
482 abstractsubrepo.archive(self, ui, archiver, prefix, match)
482 abstractsubrepo.archive(self, ui, archiver, prefix, match)
483
483
484 rev = self._state[1]
484 rev = self._state[1]
485 ctx = self._repo[rev]
485 ctx = self._repo[rev]
486 for subpath in ctx.substate:
486 for subpath in ctx.substate:
487 s = subrepo(ctx, subpath)
487 s = subrepo(ctx, subpath)
488 submatch = matchmod.narrowmatcher(subpath, match)
488 submatch = matchmod.narrowmatcher(subpath, match)
489 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
489 s.archive(ui, archiver, os.path.join(prefix, self._path), submatch)
490
490
491 @annotatesubrepoerror
491 @annotatesubrepoerror
492 def dirty(self, ignoreupdate=False):
492 def dirty(self, ignoreupdate=False):
493 r = self._state[1]
493 r = self._state[1]
494 if r == '' and not ignoreupdate: # no state recorded
494 if r == '' and not ignoreupdate: # no state recorded
495 return True
495 return True
496 w = self._repo[None]
496 w = self._repo[None]
497 if r != w.p1().hex() and not ignoreupdate:
497 if r != w.p1().hex() and not ignoreupdate:
498 # different version checked out
498 # different version checked out
499 return True
499 return True
500 return w.dirty() # working directory changed
500 return w.dirty() # working directory changed
501
501
502 def basestate(self):
502 def basestate(self):
503 return self._repo['.'].hex()
503 return self._repo['.'].hex()
504
504
505 def checknested(self, path):
505 def checknested(self, path):
506 return self._repo._checknested(self._repo.wjoin(path))
506 return self._repo._checknested(self._repo.wjoin(path))
507
507
508 @annotatesubrepoerror
508 @annotatesubrepoerror
509 def commit(self, text, user, date):
509 def commit(self, text, user, date):
510 # don't bother committing in the subrepo if it's only been
510 # don't bother committing in the subrepo if it's only been
511 # updated
511 # updated
512 if not self.dirty(True):
512 if not self.dirty(True):
513 return self._repo['.'].hex()
513 return self._repo['.'].hex()
514 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
514 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
515 n = self._repo.commit(text, user, date)
515 n = self._repo.commit(text, user, date)
516 if not n:
516 if not n:
517 return self._repo['.'].hex() # different version checked out
517 return self._repo['.'].hex() # different version checked out
518 return node.hex(n)
518 return node.hex(n)
519
519
520 @annotatesubrepoerror
520 @annotatesubrepoerror
521 def remove(self):
521 def remove(self):
522 # we can't fully delete the repository as it may contain
522 # we can't fully delete the repository as it may contain
523 # local-only history
523 # local-only history
524 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
524 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
525 hg.clean(self._repo, node.nullid, False)
525 hg.clean(self._repo, node.nullid, False)
526
526
527 def _get(self, state):
527 def _get(self, state):
528 source, revision, kind = state
528 source, revision, kind = state
529 if revision not in self._repo:
529 if revision not in self._repo:
530 self._repo._subsource = source
530 self._repo._subsource = source
531 srcurl = _abssource(self._repo)
531 srcurl = _abssource(self._repo)
532 other = hg.peer(self._repo, {}, srcurl)
532 other = hg.peer(self._repo, {}, srcurl)
533 if len(self._repo) == 0:
533 if len(self._repo) == 0:
534 self._repo.ui.status(_('cloning subrepo %s from %s\n')
534 self._repo.ui.status(_('cloning subrepo %s from %s\n')
535 % (subrelpath(self), srcurl))
535 % (subrelpath(self), srcurl))
536 parentrepo = self._repo._subparent
536 parentrepo = self._repo._subparent
537 shutil.rmtree(self._repo.path)
537 shutil.rmtree(self._repo.path)
538 other, cloned = hg.clone(self._repo._subparent.baseui, {},
538 other, cloned = hg.clone(self._repo._subparent.baseui, {},
539 other, self._repo.root,
539 other, self._repo.root,
540 update=False)
540 update=False)
541 self._repo = cloned.local()
541 self._repo = cloned.local()
542 self._initrepo(parentrepo, source, create=True)
542 self._initrepo(parentrepo, source, create=True)
543 else:
543 else:
544 self._repo.ui.status(_('pulling subrepo %s from %s\n')
544 self._repo.ui.status(_('pulling subrepo %s from %s\n')
545 % (subrelpath(self), srcurl))
545 % (subrelpath(self), srcurl))
546 self._repo.pull(other)
546 self._repo.pull(other)
547 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
547 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
548 srcurl)
548 srcurl)
549
549
550 @annotatesubrepoerror
550 @annotatesubrepoerror
551 def get(self, state, overwrite=False):
551 def get(self, state, overwrite=False):
552 self._get(state)
552 self._get(state)
553 source, revision, kind = state
553 source, revision, kind = state
554 self._repo.ui.debug("getting subrepo %s\n" % self._path)
554 self._repo.ui.debug("getting subrepo %s\n" % self._path)
555 hg.updaterepo(self._repo, revision, overwrite)
555 hg.updaterepo(self._repo, revision, overwrite)
556
556
557 @annotatesubrepoerror
557 @annotatesubrepoerror
558 def merge(self, state):
558 def merge(self, state):
559 self._get(state)
559 self._get(state)
560 cur = self._repo['.']
560 cur = self._repo['.']
561 dst = self._repo[state[1]]
561 dst = self._repo[state[1]]
562 anc = dst.ancestor(cur)
562 anc = dst.ancestor(cur)
563
563
564 def mergefunc():
564 def mergefunc():
565 if anc == cur and dst.branch() == cur.branch():
565 if anc == cur and dst.branch() == cur.branch():
566 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
566 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
567 hg.update(self._repo, state[1])
567 hg.update(self._repo, state[1])
568 elif anc == dst:
568 elif anc == dst:
569 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
569 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
570 else:
570 else:
571 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
571 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
572 hg.merge(self._repo, state[1], remind=False)
572 hg.merge(self._repo, state[1], remind=False)
573
573
574 wctx = self._repo[None]
574 wctx = self._repo[None]
575 if self.dirty():
575 if self.dirty():
576 if anc != dst:
576 if anc != dst:
577 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
577 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
578 mergefunc()
578 mergefunc()
579 else:
579 else:
580 mergefunc()
580 mergefunc()
581 else:
581 else:
582 mergefunc()
582 mergefunc()
583
583
584 @annotatesubrepoerror
584 @annotatesubrepoerror
585 def push(self, opts):
585 def push(self, opts):
586 force = opts.get('force')
586 force = opts.get('force')
587 newbranch = opts.get('new_branch')
587 newbranch = opts.get('new_branch')
588 ssh = opts.get('ssh')
588 ssh = opts.get('ssh')
589
589
590 # push subrepos depth-first for coherent ordering
590 # push subrepos depth-first for coherent ordering
591 c = self._repo['']
591 c = self._repo['']
592 subs = c.substate # only repos that are committed
592 subs = c.substate # only repos that are committed
593 for s in sorted(subs):
593 for s in sorted(subs):
594 if c.sub(s).push(opts) == 0:
594 if c.sub(s).push(opts) == 0:
595 return False
595 return False
596
596
597 dsturl = _abssource(self._repo, True)
597 dsturl = _abssource(self._repo, True)
598 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
598 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
599 (subrelpath(self), dsturl))
599 (subrelpath(self), dsturl))
600 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
600 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
601 return self._repo.push(other, force, newbranch=newbranch)
601 return self._repo.push(other, force, newbranch=newbranch)
602
602
603 @annotatesubrepoerror
603 @annotatesubrepoerror
604 def outgoing(self, ui, dest, opts):
604 def outgoing(self, ui, dest, opts):
605 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
605 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
606
606
607 @annotatesubrepoerror
607 @annotatesubrepoerror
608 def incoming(self, ui, source, opts):
608 def incoming(self, ui, source, opts):
609 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
609 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
610
610
611 @annotatesubrepoerror
611 @annotatesubrepoerror
612 def files(self):
612 def files(self):
613 rev = self._state[1]
613 rev = self._state[1]
614 ctx = self._repo[rev]
614 ctx = self._repo[rev]
615 return ctx.manifest()
615 return ctx.manifest()
616
616
617 def filedata(self, name):
617 def filedata(self, name):
618 rev = self._state[1]
618 rev = self._state[1]
619 return self._repo[rev][name].data()
619 return self._repo[rev][name].data()
620
620
621 def fileflags(self, name):
621 def fileflags(self, name):
622 rev = self._state[1]
622 rev = self._state[1]
623 ctx = self._repo[rev]
623 ctx = self._repo[rev]
624 return ctx.flags(name)
624 return ctx.flags(name)
625
625
626 def walk(self, match):
626 def walk(self, match):
627 ctx = self._repo[None]
627 ctx = self._repo[None]
628 return ctx.walk(match)
628 return ctx.walk(match)
629
629
630 @annotatesubrepoerror
630 @annotatesubrepoerror
631 def forget(self, ui, match, prefix):
631 def forget(self, ui, match, prefix):
632 return cmdutil.forget(ui, self._repo, match,
632 return cmdutil.forget(ui, self._repo, match,
633 os.path.join(prefix, self._path), True)
633 os.path.join(prefix, self._path), True)
634
634
635 @annotatesubrepoerror
635 @annotatesubrepoerror
636 def revert(self, ui, substate, *pats, **opts):
636 def revert(self, ui, substate, *pats, **opts):
637 # reverting a subrepo is a 2 step process:
637 # reverting a subrepo is a 2 step process:
638 # 1. if the no_backup is not set, revert all modified
638 # 1. if the no_backup is not set, revert all modified
639 # files inside the subrepo
639 # files inside the subrepo
640 # 2. update the subrepo to the revision specified in
640 # 2. update the subrepo to the revision specified in
641 # the corresponding substate dictionary
641 # the corresponding substate dictionary
642 ui.status(_('reverting subrepo %s\n') % substate[0])
642 ui.status(_('reverting subrepo %s\n') % substate[0])
643 if not opts.get('no_backup'):
643 if not opts.get('no_backup'):
644 # Revert all files on the subrepo, creating backups
644 # Revert all files on the subrepo, creating backups
645 # Note that this will not recursively revert subrepos
645 # Note that this will not recursively revert subrepos
646 # We could do it if there was a set:subrepos() predicate
646 # We could do it if there was a set:subrepos() predicate
647 opts = opts.copy()
647 opts = opts.copy()
648 opts['date'] = None
648 opts['date'] = None
649 opts['rev'] = substate[1]
649 opts['rev'] = substate[1]
650
650
651 pats = []
651 pats = []
652 if not opts['all']:
652 if not opts['all']:
653 pats = ['set:modified()']
653 pats = ['set:modified()']
654 self.filerevert(ui, *pats, **opts)
654 self.filerevert(ui, *pats, **opts)
655
655
656 # Update the repo to the revision specified in the given substate
656 # Update the repo to the revision specified in the given substate
657 self.get(substate, overwrite=True)
657 self.get(substate, overwrite=True)
658
658
659 def filerevert(self, ui, *pats, **opts):
659 def filerevert(self, ui, *pats, **opts):
660 ctx = self._repo[opts['rev']]
660 ctx = self._repo[opts['rev']]
661 parents = self._repo.dirstate.parents()
661 parents = self._repo.dirstate.parents()
662 if opts['all']:
662 if opts['all']:
663 pats = ['set:modified()']
663 pats = ['set:modified()']
664 else:
664 else:
665 pats = []
665 pats = []
666 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
666 cmdutil.revert(ui, self._repo, ctx, parents, *pats, **opts)
667
667
668 class svnsubrepo(abstractsubrepo):
668 class svnsubrepo(abstractsubrepo):
669 def __init__(self, ctx, path, state):
669 def __init__(self, ctx, path, state):
670 self._path = path
670 self._path = path
671 self._state = state
671 self._state = state
672 self._ctx = ctx
672 self._ctx = ctx
673 self._ui = ctx._repo.ui
673 self._ui = ctx._repo.ui
674 self._exe = util.findexe('svn')
674 self._exe = util.findexe('svn')
675 if not self._exe:
675 if not self._exe:
676 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
676 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
677 % self._path)
677 % self._path)
678
678
679 def _svncommand(self, commands, filename='', failok=False):
679 def _svncommand(self, commands, filename='', failok=False):
680 cmd = [self._exe]
680 cmd = [self._exe]
681 extrakw = {}
681 extrakw = {}
682 if not self._ui.interactive():
682 if not self._ui.interactive():
683 # Making stdin be a pipe should prevent svn from behaving
683 # Making stdin be a pipe should prevent svn from behaving
684 # interactively even if we can't pass --non-interactive.
684 # interactively even if we can't pass --non-interactive.
685 extrakw['stdin'] = subprocess.PIPE
685 extrakw['stdin'] = subprocess.PIPE
686 # Starting in svn 1.5 --non-interactive is a global flag
686 # Starting in svn 1.5 --non-interactive is a global flag
687 # instead of being per-command, but we need to support 1.4 so
687 # instead of being per-command, but we need to support 1.4 so
688 # we have to be intelligent about what commands take
688 # we have to be intelligent about what commands take
689 # --non-interactive.
689 # --non-interactive.
690 if commands[0] in ('update', 'checkout', 'commit'):
690 if commands[0] in ('update', 'checkout', 'commit'):
691 cmd.append('--non-interactive')
691 cmd.append('--non-interactive')
692 cmd.extend(commands)
692 cmd.extend(commands)
693 if filename is not None:
693 if filename is not None:
694 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
694 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
695 cmd.append(path)
695 cmd.append(path)
696 env = dict(os.environ)
696 env = dict(os.environ)
697 # Avoid localized output, preserve current locale for everything else.
697 # Avoid localized output, preserve current locale for everything else.
698 lc_all = env.get('LC_ALL')
698 lc_all = env.get('LC_ALL')
699 if lc_all:
699 if lc_all:
700 env['LANG'] = lc_all
700 env['LANG'] = lc_all
701 del env['LC_ALL']
701 del env['LC_ALL']
702 env['LC_MESSAGES'] = 'C'
702 env['LC_MESSAGES'] = 'C'
703 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
703 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
704 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
704 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
705 universal_newlines=True, env=env, **extrakw)
705 universal_newlines=True, env=env, **extrakw)
706 stdout, stderr = p.communicate()
706 stdout, stderr = p.communicate()
707 stderr = stderr.strip()
707 stderr = stderr.strip()
708 if not failok:
708 if not failok:
709 if p.returncode:
709 if p.returncode:
710 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
710 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
711 if stderr:
711 if stderr:
712 self._ui.warn(stderr + '\n')
712 self._ui.warn(stderr + '\n')
713 return stdout, stderr
713 return stdout, stderr
714
714
715 @propertycache
715 @propertycache
716 def _svnversion(self):
716 def _svnversion(self):
717 output, err = self._svncommand(['--version', '--quiet'], filename=None)
717 output, err = self._svncommand(['--version', '--quiet'], filename=None)
718 m = re.search(r'^(\d+)\.(\d+)', output)
718 m = re.search(r'^(\d+)\.(\d+)', output)
719 if not m:
719 if not m:
720 raise util.Abort(_('cannot retrieve svn tool version'))
720 raise util.Abort(_('cannot retrieve svn tool version'))
721 return (int(m.group(1)), int(m.group(2)))
721 return (int(m.group(1)), int(m.group(2)))
722
722
723 def _wcrevs(self):
723 def _wcrevs(self):
724 # Get the working directory revision as well as the last
724 # Get the working directory revision as well as the last
725 # commit revision so we can compare the subrepo state with
725 # commit revision so we can compare the subrepo state with
726 # both. We used to store the working directory one.
726 # both. We used to store the working directory one.
727 output, err = self._svncommand(['info', '--xml'])
727 output, err = self._svncommand(['info', '--xml'])
728 doc = xml.dom.minidom.parseString(output)
728 doc = xml.dom.minidom.parseString(output)
729 entries = doc.getElementsByTagName('entry')
729 entries = doc.getElementsByTagName('entry')
730 lastrev, rev = '0', '0'
730 lastrev, rev = '0', '0'
731 if entries:
731 if entries:
732 rev = str(entries[0].getAttribute('revision')) or '0'
732 rev = str(entries[0].getAttribute('revision')) or '0'
733 commits = entries[0].getElementsByTagName('commit')
733 commits = entries[0].getElementsByTagName('commit')
734 if commits:
734 if commits:
735 lastrev = str(commits[0].getAttribute('revision')) or '0'
735 lastrev = str(commits[0].getAttribute('revision')) or '0'
736 return (lastrev, rev)
736 return (lastrev, rev)
737
737
738 def _wcrev(self):
738 def _wcrev(self):
739 return self._wcrevs()[0]
739 return self._wcrevs()[0]
740
740
741 def _wcchanged(self):
741 def _wcchanged(self):
742 """Return (changes, extchanges, missing) where changes is True
742 """Return (changes, extchanges, missing) where changes is True
743 if the working directory was changed, extchanges is
743 if the working directory was changed, extchanges is
744 True if any of these changes concern an external entry and missing
744 True if any of these changes concern an external entry and missing
745 is True if any change is a missing entry.
745 is True if any change is a missing entry.
746 """
746 """
747 output, err = self._svncommand(['status', '--xml'])
747 output, err = self._svncommand(['status', '--xml'])
748 externals, changes, missing = [], [], []
748 externals, changes, missing = [], [], []
749 doc = xml.dom.minidom.parseString(output)
749 doc = xml.dom.minidom.parseString(output)
750 for e in doc.getElementsByTagName('entry'):
750 for e in doc.getElementsByTagName('entry'):
751 s = e.getElementsByTagName('wc-status')
751 s = e.getElementsByTagName('wc-status')
752 if not s:
752 if not s:
753 continue
753 continue
754 item = s[0].getAttribute('item')
754 item = s[0].getAttribute('item')
755 props = s[0].getAttribute('props')
755 props = s[0].getAttribute('props')
756 path = e.getAttribute('path')
756 path = e.getAttribute('path')
757 if item == 'external':
757 if item == 'external':
758 externals.append(path)
758 externals.append(path)
759 elif item == 'missing':
759 elif item == 'missing':
760 missing.append(path)
760 missing.append(path)
761 if (item not in ('', 'normal', 'unversioned', 'external')
761 if (item not in ('', 'normal', 'unversioned', 'external')
762 or props not in ('', 'none', 'normal')):
762 or props not in ('', 'none', 'normal')):
763 changes.append(path)
763 changes.append(path)
764 for path in changes:
764 for path in changes:
765 for ext in externals:
765 for ext in externals:
766 if path == ext or path.startswith(ext + os.sep):
766 if path == ext or path.startswith(ext + os.sep):
767 return True, True, bool(missing)
767 return True, True, bool(missing)
768 return bool(changes), False, bool(missing)
768 return bool(changes), False, bool(missing)
769
769
770 def dirty(self, ignoreupdate=False):
770 def dirty(self, ignoreupdate=False):
771 if not self._wcchanged()[0]:
771 if not self._wcchanged()[0]:
772 if self._state[1] in self._wcrevs() or ignoreupdate:
772 if self._state[1] in self._wcrevs() or ignoreupdate:
773 return False
773 return False
774 return True
774 return True
775
775
776 def basestate(self):
776 def basestate(self):
777 lastrev, rev = self._wcrevs()
777 lastrev, rev = self._wcrevs()
778 if lastrev != rev:
778 if lastrev != rev:
779 # Last committed rev is not the same than rev. We would
779 # Last committed rev is not the same than rev. We would
780 # like to take lastrev but we do not know if the subrepo
780 # like to take lastrev but we do not know if the subrepo
781 # URL exists at lastrev. Test it and fallback to rev it
781 # URL exists at lastrev. Test it and fallback to rev it
782 # is not there.
782 # is not there.
783 try:
783 try:
784 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
784 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
785 return lastrev
785 return lastrev
786 except error.Abort:
786 except error.Abort:
787 pass
787 pass
788 return rev
788 return rev
789
789
790 @annotatesubrepoerror
790 @annotatesubrepoerror
791 def commit(self, text, user, date):
791 def commit(self, text, user, date):
792 # user and date are out of our hands since svn is centralized
792 # user and date are out of our hands since svn is centralized
793 changed, extchanged, missing = self._wcchanged()
793 changed, extchanged, missing = self._wcchanged()
794 if not changed:
794 if not changed:
795 return self.basestate()
795 return self.basestate()
796 if extchanged:
796 if extchanged:
797 # Do not try to commit externals
797 # Do not try to commit externals
798 raise util.Abort(_('cannot commit svn externals'))
798 raise util.Abort(_('cannot commit svn externals'))
799 if missing:
799 if missing:
800 # svn can commit with missing entries but aborting like hg
800 # svn can commit with missing entries but aborting like hg
801 # seems a better approach.
801 # seems a better approach.
802 raise util.Abort(_('cannot commit missing svn entries'))
802 raise util.Abort(_('cannot commit missing svn entries'))
803 commitinfo, err = self._svncommand(['commit', '-m', text])
803 commitinfo, err = self._svncommand(['commit', '-m', text])
804 self._ui.status(commitinfo)
804 self._ui.status(commitinfo)
805 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
805 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
806 if not newrev:
806 if not newrev:
807 if not commitinfo.strip():
807 if not commitinfo.strip():
808 # Sometimes, our definition of "changed" differs from
808 # Sometimes, our definition of "changed" differs from
809 # svn one. For instance, svn ignores missing files
809 # svn one. For instance, svn ignores missing files
810 # when committing. If there are only missing files, no
810 # when committing. If there are only missing files, no
811 # commit is made, no output and no error code.
811 # commit is made, no output and no error code.
812 raise util.Abort(_('failed to commit svn changes'))
812 raise util.Abort(_('failed to commit svn changes'))
813 raise util.Abort(commitinfo.splitlines()[-1])
813 raise util.Abort(commitinfo.splitlines()[-1])
814 newrev = newrev.groups()[0]
814 newrev = newrev.groups()[0]
815 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
815 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
816 return newrev
816 return newrev
817
817
818 @annotatesubrepoerror
818 @annotatesubrepoerror
819 def remove(self):
819 def remove(self):
820 if self.dirty():
820 if self.dirty():
821 self._ui.warn(_('not removing repo %s because '
821 self._ui.warn(_('not removing repo %s because '
822 'it has changes.\n' % self._path))
822 'it has changes.\n' % self._path))
823 return
823 return
824 self._ui.note(_('removing subrepo %s\n') % self._path)
824 self._ui.note(_('removing subrepo %s\n') % self._path)
825
825
826 def onerror(function, path, excinfo):
826 def onerror(function, path, excinfo):
827 if function is not os.remove:
827 if function is not os.remove:
828 raise
828 raise
829 # read-only files cannot be unlinked under Windows
829 # read-only files cannot be unlinked under Windows
830 s = os.stat(path)
830 s = os.stat(path)
831 if (s.st_mode & stat.S_IWRITE) != 0:
831 if (s.st_mode & stat.S_IWRITE) != 0:
832 raise
832 raise
833 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
833 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
834 os.remove(path)
834 os.remove(path)
835
835
836 path = self._ctx._repo.wjoin(self._path)
836 path = self._ctx._repo.wjoin(self._path)
837 shutil.rmtree(path, onerror=onerror)
837 shutil.rmtree(path, onerror=onerror)
838 try:
838 try:
839 os.removedirs(os.path.dirname(path))
839 os.removedirs(os.path.dirname(path))
840 except OSError:
840 except OSError:
841 pass
841 pass
842
842
843 @annotatesubrepoerror
843 @annotatesubrepoerror
844 def get(self, state, overwrite=False):
844 def get(self, state, overwrite=False):
845 if overwrite:
845 if overwrite:
846 self._svncommand(['revert', '--recursive'])
846 self._svncommand(['revert', '--recursive'])
847 args = ['checkout']
847 args = ['checkout']
848 if self._svnversion >= (1, 5):
848 if self._svnversion >= (1, 5):
849 args.append('--force')
849 args.append('--force')
850 # The revision must be specified at the end of the URL to properly
850 # The revision must be specified at the end of the URL to properly
851 # update to a directory which has since been deleted and recreated.
851 # update to a directory which has since been deleted and recreated.
852 args.append('%s@%s' % (state[0], state[1]))
852 args.append('%s@%s' % (state[0], state[1]))
853 status, err = self._svncommand(args, failok=True)
853 status, err = self._svncommand(args, failok=True)
854 if not re.search('Checked out revision [0-9]+.', status):
854 if not re.search('Checked out revision [0-9]+.', status):
855 if ('is already a working copy for a different URL' in err
855 if ('is already a working copy for a different URL' in err
856 and (self._wcchanged()[:2] == (False, False))):
856 and (self._wcchanged()[:2] == (False, False))):
857 # obstructed but clean working copy, so just blow it away.
857 # obstructed but clean working copy, so just blow it away.
858 self.remove()
858 self.remove()
859 self.get(state, overwrite=False)
859 self.get(state, overwrite=False)
860 return
860 return
861 raise util.Abort((status or err).splitlines()[-1])
861 raise util.Abort((status or err).splitlines()[-1])
862 self._ui.status(status)
862 self._ui.status(status)
863
863
864 @annotatesubrepoerror
864 @annotatesubrepoerror
865 def merge(self, state):
865 def merge(self, state):
866 old = self._state[1]
866 old = self._state[1]
867 new = state[1]
867 new = state[1]
868 wcrev = self._wcrev()
868 wcrev = self._wcrev()
869 if new != wcrev:
869 if new != wcrev:
870 dirty = old == wcrev or self._wcchanged()[0]
870 dirty = old == wcrev or self._wcchanged()[0]
871 if _updateprompt(self._ui, self, dirty, wcrev, new):
871 if _updateprompt(self._ui, self, dirty, wcrev, new):
872 self.get(state, False)
872 self.get(state, False)
873
873
874 def push(self, opts):
874 def push(self, opts):
875 # push is a no-op for SVN
875 # push is a no-op for SVN
876 return True
876 return True
877
877
878 @annotatesubrepoerror
878 @annotatesubrepoerror
879 def files(self):
879 def files(self):
880 output = self._svncommand(['list', '--recursive', '--xml'])[0]
880 output = self._svncommand(['list', '--recursive', '--xml'])[0]
881 doc = xml.dom.minidom.parseString(output)
881 doc = xml.dom.minidom.parseString(output)
882 paths = []
882 paths = []
883 for e in doc.getElementsByTagName('entry'):
883 for e in doc.getElementsByTagName('entry'):
884 kind = str(e.getAttribute('kind'))
884 kind = str(e.getAttribute('kind'))
885 if kind != 'file':
885 if kind != 'file':
886 continue
886 continue
887 name = ''.join(c.data for c
887 name = ''.join(c.data for c
888 in e.getElementsByTagName('name')[0].childNodes
888 in e.getElementsByTagName('name')[0].childNodes
889 if c.nodeType == c.TEXT_NODE)
889 if c.nodeType == c.TEXT_NODE)
890 paths.append(name.encode('utf-8'))
890 paths.append(name.encode('utf-8'))
891 return paths
891 return paths
892
892
893 def filedata(self, name):
893 def filedata(self, name):
894 return self._svncommand(['cat'], name)[0]
894 return self._svncommand(['cat'], name)[0]
895
895
896
896
897 class gitsubrepo(abstractsubrepo):
897 class gitsubrepo(abstractsubrepo):
898 def __init__(self, ctx, path, state):
898 def __init__(self, ctx, path, state):
899 self._state = state
899 self._state = state
900 self._ctx = ctx
900 self._ctx = ctx
901 self._path = path
901 self._path = path
902 self._relpath = os.path.join(reporelpath(ctx._repo), path)
902 self._relpath = os.path.join(reporelpath(ctx._repo), path)
903 self._abspath = ctx._repo.wjoin(path)
903 self._abspath = ctx._repo.wjoin(path)
904 self._subparent = ctx._repo
904 self._subparent = ctx._repo
905 self._ui = ctx._repo.ui
905 self._ui = ctx._repo.ui
906 self._ensuregit()
906 self._ensuregit()
907
907
908 def _ensuregit(self):
908 def _ensuregit(self):
909 try:
909 try:
910 self._gitexecutable = 'git'
910 self._gitexecutable = 'git'
911 out, err = self._gitnodir(['--version'])
911 out, err = self._gitnodir(['--version'])
912 except OSError, e:
912 except OSError, e:
913 if e.errno != 2 or os.name != 'nt':
913 if e.errno != 2 or os.name != 'nt':
914 raise
914 raise
915 self._gitexecutable = 'git.cmd'
915 self._gitexecutable = 'git.cmd'
916 out, err = self._gitnodir(['--version'])
916 out, err = self._gitnodir(['--version'])
917 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
917 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
918 if not m:
918 if not m:
919 self._ui.warn(_('cannot retrieve git version'))
919 self._ui.warn(_('cannot retrieve git version'))
920 return
920 return
921 version = (int(m.group(1)), m.group(2), m.group(3))
921 version = (int(m.group(1)), m.group(2), m.group(3))
922 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
922 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
923 # despite the docstring comment. For now, error on 1.4.0, warn on
923 # despite the docstring comment. For now, error on 1.4.0, warn on
924 # 1.5.0 but attempt to continue.
924 # 1.5.0 but attempt to continue.
925 if version < (1, 5, 0):
925 if version < (1, 5, 0):
926 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
926 raise util.Abort(_('git subrepo requires at least 1.6.0 or later'))
927 elif version < (1, 6, 0):
927 elif version < (1, 6, 0):
928 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
928 self._ui.warn(_('git subrepo requires at least 1.6.0 or later'))
929
929
930 def _gitcommand(self, commands, env=None, stream=False):
930 def _gitcommand(self, commands, env=None, stream=False):
931 return self._gitdir(commands, env=env, stream=stream)[0]
931 return self._gitdir(commands, env=env, stream=stream)[0]
932
932
933 def _gitdir(self, commands, env=None, stream=False):
933 def _gitdir(self, commands, env=None, stream=False):
934 return self._gitnodir(commands, env=env, stream=stream,
934 return self._gitnodir(commands, env=env, stream=stream,
935 cwd=self._abspath)
935 cwd=self._abspath)
936
936
937 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
937 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
938 """Calls the git command
938 """Calls the git command
939
939
940 The methods tries to call the git command. versions prior to 1.6.0
940 The methods tries to call the git command. versions prior to 1.6.0
941 are not supported and very probably fail.
941 are not supported and very probably fail.
942 """
942 """
943 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
943 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
944 # unless ui.quiet is set, print git's stderr,
944 # unless ui.quiet is set, print git's stderr,
945 # which is mostly progress and useful info
945 # which is mostly progress and useful info
946 errpipe = None
946 errpipe = None
947 if self._ui.quiet:
947 if self._ui.quiet:
948 errpipe = open(os.devnull, 'w')
948 errpipe = open(os.devnull, 'w')
949 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
949 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
950 cwd=cwd, env=env, close_fds=util.closefds,
950 cwd=cwd, env=env, close_fds=util.closefds,
951 stdout=subprocess.PIPE, stderr=errpipe)
951 stdout=subprocess.PIPE, stderr=errpipe)
952 if stream:
952 if stream:
953 return p.stdout, None
953 return p.stdout, None
954
954
955 retdata = p.stdout.read().strip()
955 retdata = p.stdout.read().strip()
956 # wait for the child to exit to avoid race condition.
956 # wait for the child to exit to avoid race condition.
957 p.wait()
957 p.wait()
958
958
959 if p.returncode != 0 and p.returncode != 1:
959 if p.returncode != 0 and p.returncode != 1:
960 # there are certain error codes that are ok
960 # there are certain error codes that are ok
961 command = commands[0]
961 command = commands[0]
962 if command in ('cat-file', 'symbolic-ref'):
962 if command in ('cat-file', 'symbolic-ref'):
963 return retdata, p.returncode
963 return retdata, p.returncode
964 # for all others, abort
964 # for all others, abort
965 raise util.Abort('git %s error %d in %s' %
965 raise util.Abort('git %s error %d in %s' %
966 (command, p.returncode, self._relpath))
966 (command, p.returncode, self._relpath))
967
967
968 return retdata, p.returncode
968 return retdata, p.returncode
969
969
970 def _gitmissing(self):
970 def _gitmissing(self):
971 return not os.path.exists(os.path.join(self._abspath, '.git'))
971 return not os.path.exists(os.path.join(self._abspath, '.git'))
972
972
973 def _gitstate(self):
973 def _gitstate(self):
974 return self._gitcommand(['rev-parse', 'HEAD'])
974 return self._gitcommand(['rev-parse', 'HEAD'])
975
975
976 def _gitcurrentbranch(self):
976 def _gitcurrentbranch(self):
977 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
977 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
978 if err:
978 if err:
979 current = None
979 current = None
980 return current
980 return current
981
981
982 def _gitremote(self, remote):
982 def _gitremote(self, remote):
983 out = self._gitcommand(['remote', 'show', '-n', remote])
983 out = self._gitcommand(['remote', 'show', '-n', remote])
984 line = out.split('\n')[1]
984 line = out.split('\n')[1]
985 i = line.index('URL: ') + len('URL: ')
985 i = line.index('URL: ') + len('URL: ')
986 return line[i:]
986 return line[i:]
987
987
988 def _githavelocally(self, revision):
988 def _githavelocally(self, revision):
989 out, code = self._gitdir(['cat-file', '-e', revision])
989 out, code = self._gitdir(['cat-file', '-e', revision])
990 return code == 0
990 return code == 0
991
991
992 def _gitisancestor(self, r1, r2):
992 def _gitisancestor(self, r1, r2):
993 base = self._gitcommand(['merge-base', r1, r2])
993 base = self._gitcommand(['merge-base', r1, r2])
994 return base == r1
994 return base == r1
995
995
996 def _gitisbare(self):
996 def _gitisbare(self):
997 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
997 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
998
998
999 def _gitupdatestat(self):
999 def _gitupdatestat(self):
1000 """This must be run before git diff-index.
1000 """This must be run before git diff-index.
1001 diff-index only looks at changes to file stat;
1001 diff-index only looks at changes to file stat;
1002 this command looks at file contents and updates the stat."""
1002 this command looks at file contents and updates the stat."""
1003 self._gitcommand(['update-index', '-q', '--refresh'])
1003 self._gitcommand(['update-index', '-q', '--refresh'])
1004
1004
1005 def _gitbranchmap(self):
1005 def _gitbranchmap(self):
1006 '''returns 2 things:
1006 '''returns 2 things:
1007 a map from git branch to revision
1007 a map from git branch to revision
1008 a map from revision to branches'''
1008 a map from revision to branches'''
1009 branch2rev = {}
1009 branch2rev = {}
1010 rev2branch = {}
1010 rev2branch = {}
1011
1011
1012 out = self._gitcommand(['for-each-ref', '--format',
1012 out = self._gitcommand(['for-each-ref', '--format',
1013 '%(objectname) %(refname)'])
1013 '%(objectname) %(refname)'])
1014 for line in out.split('\n'):
1014 for line in out.split('\n'):
1015 revision, ref = line.split(' ')
1015 revision, ref = line.split(' ')
1016 if (not ref.startswith('refs/heads/') and
1016 if (not ref.startswith('refs/heads/') and
1017 not ref.startswith('refs/remotes/')):
1017 not ref.startswith('refs/remotes/')):
1018 continue
1018 continue
1019 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1019 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1020 continue # ignore remote/HEAD redirects
1020 continue # ignore remote/HEAD redirects
1021 branch2rev[ref] = revision
1021 branch2rev[ref] = revision
1022 rev2branch.setdefault(revision, []).append(ref)
1022 rev2branch.setdefault(revision, []).append(ref)
1023 return branch2rev, rev2branch
1023 return branch2rev, rev2branch
1024
1024
1025 def _gittracking(self, branches):
1025 def _gittracking(self, branches):
1026 'return map of remote branch to local tracking branch'
1026 'return map of remote branch to local tracking branch'
1027 # assumes no more than one local tracking branch for each remote
1027 # assumes no more than one local tracking branch for each remote
1028 tracking = {}
1028 tracking = {}
1029 for b in branches:
1029 for b in branches:
1030 if b.startswith('refs/remotes/'):
1030 if b.startswith('refs/remotes/'):
1031 continue
1031 continue
1032 bname = b.split('/', 2)[2]
1032 bname = b.split('/', 2)[2]
1033 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1033 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1034 if remote:
1034 if remote:
1035 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1035 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1036 tracking['refs/remotes/%s/%s' %
1036 tracking['refs/remotes/%s/%s' %
1037 (remote, ref.split('/', 2)[2])] = b
1037 (remote, ref.split('/', 2)[2])] = b
1038 return tracking
1038 return tracking
1039
1039
1040 def _abssource(self, source):
1040 def _abssource(self, source):
1041 if '://' not in source:
1041 if '://' not in source:
1042 # recognize the scp syntax as an absolute source
1042 # recognize the scp syntax as an absolute source
1043 colon = source.find(':')
1043 colon = source.find(':')
1044 if colon != -1 and '/' not in source[:colon]:
1044 if colon != -1 and '/' not in source[:colon]:
1045 return source
1045 return source
1046 self._subsource = source
1046 self._subsource = source
1047 return _abssource(self)
1047 return _abssource(self)
1048
1048
1049 def _fetch(self, source, revision):
1049 def _fetch(self, source, revision):
1050 if self._gitmissing():
1050 if self._gitmissing():
1051 source = self._abssource(source)
1051 source = self._abssource(source)
1052 self._ui.status(_('cloning subrepo %s from %s\n') %
1052 self._ui.status(_('cloning subrepo %s from %s\n') %
1053 (self._relpath, source))
1053 (self._relpath, source))
1054 self._gitnodir(['clone', source, self._abspath])
1054 self._gitnodir(['clone', source, self._abspath])
1055 if self._githavelocally(revision):
1055 if self._githavelocally(revision):
1056 return
1056 return
1057 self._ui.status(_('pulling subrepo %s from %s\n') %
1057 self._ui.status(_('pulling subrepo %s from %s\n') %
1058 (self._relpath, self._gitremote('origin')))
1058 (self._relpath, self._gitremote('origin')))
1059 # try only origin: the originally cloned repo
1059 # try only origin: the originally cloned repo
1060 self._gitcommand(['fetch'])
1060 self._gitcommand(['fetch'])
1061 if not self._githavelocally(revision):
1061 if not self._githavelocally(revision):
1062 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1062 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
1063 (revision, self._relpath))
1063 (revision, self._relpath))
1064
1064
1065 @annotatesubrepoerror
1065 @annotatesubrepoerror
1066 def dirty(self, ignoreupdate=False):
1066 def dirty(self, ignoreupdate=False):
1067 if self._gitmissing():
1067 if self._gitmissing():
1068 return self._state[1] != ''
1068 return self._state[1] != ''
1069 if self._gitisbare():
1069 if self._gitisbare():
1070 return True
1070 return True
1071 if not ignoreupdate and self._state[1] != self._gitstate():
1071 if not ignoreupdate and self._state[1] != self._gitstate():
1072 # different version checked out
1072 # different version checked out
1073 return True
1073 return True
1074 # check for staged changes or modified files; ignore untracked files
1074 # check for staged changes or modified files; ignore untracked files
1075 self._gitupdatestat()
1075 self._gitupdatestat()
1076 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1076 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1077 return code == 1
1077 return code == 1
1078
1078
1079 def basestate(self):
1079 def basestate(self):
1080 return self._gitstate()
1080 return self._gitstate()
1081
1081
1082 @annotatesubrepoerror
1082 @annotatesubrepoerror
1083 def get(self, state, overwrite=False):
1083 def get(self, state, overwrite=False):
1084 source, revision, kind = state
1084 source, revision, kind = state
1085 if not revision:
1085 if not revision:
1086 self.remove()
1086 self.remove()
1087 return
1087 return
1088 self._fetch(source, revision)
1088 self._fetch(source, revision)
1089 # if the repo was set to be bare, unbare it
1089 # if the repo was set to be bare, unbare it
1090 if self._gitisbare():
1090 if self._gitisbare():
1091 self._gitcommand(['config', 'core.bare', 'false'])
1091 self._gitcommand(['config', 'core.bare', 'false'])
1092 if self._gitstate() == revision:
1092 if self._gitstate() == revision:
1093 self._gitcommand(['reset', '--hard', 'HEAD'])
1093 self._gitcommand(['reset', '--hard', 'HEAD'])
1094 return
1094 return
1095 elif self._gitstate() == revision:
1095 elif self._gitstate() == revision:
1096 if overwrite:
1096 if overwrite:
1097 # first reset the index to unmark new files for commit, because
1097 # first reset the index to unmark new files for commit, because
1098 # reset --hard will otherwise throw away files added for commit,
1098 # reset --hard will otherwise throw away files added for commit,
1099 # not just unmark them.
1099 # not just unmark them.
1100 self._gitcommand(['reset', 'HEAD'])
1100 self._gitcommand(['reset', 'HEAD'])
1101 self._gitcommand(['reset', '--hard', 'HEAD'])
1101 self._gitcommand(['reset', '--hard', 'HEAD'])
1102 return
1102 return
1103 branch2rev, rev2branch = self._gitbranchmap()
1103 branch2rev, rev2branch = self._gitbranchmap()
1104
1104
1105 def checkout(args):
1105 def checkout(args):
1106 cmd = ['checkout']
1106 cmd = ['checkout']
1107 if overwrite:
1107 if overwrite:
1108 # first reset the index to unmark new files for commit, because
1108 # first reset the index to unmark new files for commit, because
1109 # the -f option will otherwise throw away files added for
1109 # the -f option will otherwise throw away files added for
1110 # commit, not just unmark them.
1110 # commit, not just unmark them.
1111 self._gitcommand(['reset', 'HEAD'])
1111 self._gitcommand(['reset', 'HEAD'])
1112 cmd.append('-f')
1112 cmd.append('-f')
1113 self._gitcommand(cmd + args)
1113 self._gitcommand(cmd + args)
1114
1114
1115 def rawcheckout():
1115 def rawcheckout():
1116 # no branch to checkout, check it out with no branch
1116 # no branch to checkout, check it out with no branch
1117 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1117 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1118 self._relpath)
1118 self._relpath)
1119 self._ui.warn(_('check out a git branch if you intend '
1119 self._ui.warn(_('check out a git branch if you intend '
1120 'to make changes\n'))
1120 'to make changes\n'))
1121 checkout(['-q', revision])
1121 checkout(['-q', revision])
1122
1122
1123 if revision not in rev2branch:
1123 if revision not in rev2branch:
1124 rawcheckout()
1124 rawcheckout()
1125 return
1125 return
1126 branches = rev2branch[revision]
1126 branches = rev2branch[revision]
1127 firstlocalbranch = None
1127 firstlocalbranch = None
1128 for b in branches:
1128 for b in branches:
1129 if b == 'refs/heads/master':
1129 if b == 'refs/heads/master':
1130 # master trumps all other branches
1130 # master trumps all other branches
1131 checkout(['refs/heads/master'])
1131 checkout(['refs/heads/master'])
1132 return
1132 return
1133 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1133 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1134 firstlocalbranch = b
1134 firstlocalbranch = b
1135 if firstlocalbranch:
1135 if firstlocalbranch:
1136 checkout([firstlocalbranch])
1136 checkout([firstlocalbranch])
1137 return
1137 return
1138
1138
1139 tracking = self._gittracking(branch2rev.keys())
1139 tracking = self._gittracking(branch2rev.keys())
1140 # choose a remote branch already tracked if possible
1140 # choose a remote branch already tracked if possible
1141 remote = branches[0]
1141 remote = branches[0]
1142 if remote not in tracking:
1142 if remote not in tracking:
1143 for b in branches:
1143 for b in branches:
1144 if b in tracking:
1144 if b in tracking:
1145 remote = b
1145 remote = b
1146 break
1146 break
1147
1147
1148 if remote not in tracking:
1148 if remote not in tracking:
1149 # create a new local tracking branch
1149 # create a new local tracking branch
1150 local = remote.split('/', 2)[2]
1150 local = remote.split('/', 2)[2]
1151 checkout(['-b', local, remote])
1151 checkout(['-b', local, remote])
1152 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1152 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1153 # When updating to a tracked remote branch,
1153 # When updating to a tracked remote branch,
1154 # if the local tracking branch is downstream of it,
1154 # if the local tracking branch is downstream of it,
1155 # a normal `git pull` would have performed a "fast-forward merge"
1155 # a normal `git pull` would have performed a "fast-forward merge"
1156 # which is equivalent to updating the local branch to the remote.
1156 # which is equivalent to updating the local branch to the remote.
1157 # Since we are only looking at branching at update, we need to
1157 # Since we are only looking at branching at update, we need to
1158 # detect this situation and perform this action lazily.
1158 # detect this situation and perform this action lazily.
1159 if tracking[remote] != self._gitcurrentbranch():
1159 if tracking[remote] != self._gitcurrentbranch():
1160 checkout([tracking[remote]])
1160 checkout([tracking[remote]])
1161 self._gitcommand(['merge', '--ff', remote])
1161 self._gitcommand(['merge', '--ff', remote])
1162 else:
1162 else:
1163 # a real merge would be required, just checkout the revision
1163 # a real merge would be required, just checkout the revision
1164 rawcheckout()
1164 rawcheckout()
1165
1165
1166 @annotatesubrepoerror
1166 @annotatesubrepoerror
1167 def commit(self, text, user, date):
1167 def commit(self, text, user, date):
1168 if self._gitmissing():
1168 if self._gitmissing():
1169 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1169 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1170 cmd = ['commit', '-a', '-m', text]
1170 cmd = ['commit', '-a', '-m', text]
1171 env = os.environ.copy()
1171 env = os.environ.copy()
1172 if user:
1172 if user:
1173 cmd += ['--author', user]
1173 cmd += ['--author', user]
1174 if date:
1174 if date:
1175 # git's date parser silently ignores when seconds < 1e9
1175 # git's date parser silently ignores when seconds < 1e9
1176 # convert to ISO8601
1176 # convert to ISO8601
1177 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1177 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1178 '%Y-%m-%dT%H:%M:%S %1%2')
1178 '%Y-%m-%dT%H:%M:%S %1%2')
1179 self._gitcommand(cmd, env=env)
1179 self._gitcommand(cmd, env=env)
1180 # make sure commit works otherwise HEAD might not exist under certain
1180 # make sure commit works otherwise HEAD might not exist under certain
1181 # circumstances
1181 # circumstances
1182 return self._gitstate()
1182 return self._gitstate()
1183
1183
1184 @annotatesubrepoerror
1184 @annotatesubrepoerror
1185 def merge(self, state):
1185 def merge(self, state):
1186 source, revision, kind = state
1186 source, revision, kind = state
1187 self._fetch(source, revision)
1187 self._fetch(source, revision)
1188 base = self._gitcommand(['merge-base', revision, self._state[1]])
1188 base = self._gitcommand(['merge-base', revision, self._state[1]])
1189 self._gitupdatestat()
1189 self._gitupdatestat()
1190 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1190 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1191
1191
1192 def mergefunc():
1192 def mergefunc():
1193 if base == revision:
1193 if base == revision:
1194 self.get(state) # fast forward merge
1194 self.get(state) # fast forward merge
1195 elif base != self._state[1]:
1195 elif base != self._state[1]:
1196 self._gitcommand(['merge', '--no-commit', revision])
1196 self._gitcommand(['merge', '--no-commit', revision])
1197
1197
1198 if self.dirty():
1198 if self.dirty():
1199 if self._gitstate() != revision:
1199 if self._gitstate() != revision:
1200 dirty = self._gitstate() == self._state[1] or code != 0
1200 dirty = self._gitstate() == self._state[1] or code != 0
1201 if _updateprompt(self._ui, self, dirty,
1201 if _updateprompt(self._ui, self, dirty,
1202 self._state[1][:7], revision[:7]):
1202 self._state[1][:7], revision[:7]):
1203 mergefunc()
1203 mergefunc()
1204 else:
1204 else:
1205 mergefunc()
1205 mergefunc()
1206
1206
1207 @annotatesubrepoerror
1207 @annotatesubrepoerror
1208 def push(self, opts):
1208 def push(self, opts):
1209 force = opts.get('force')
1209 force = opts.get('force')
1210
1210
1211 if not self._state[1]:
1211 if not self._state[1]:
1212 return True
1212 return True
1213 if self._gitmissing():
1213 if self._gitmissing():
1214 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1214 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1215 # if a branch in origin contains the revision, nothing to do
1215 # if a branch in origin contains the revision, nothing to do
1216 branch2rev, rev2branch = self._gitbranchmap()
1216 branch2rev, rev2branch = self._gitbranchmap()
1217 if self._state[1] in rev2branch:
1217 if self._state[1] in rev2branch:
1218 for b in rev2branch[self._state[1]]:
1218 for b in rev2branch[self._state[1]]:
1219 if b.startswith('refs/remotes/origin/'):
1219 if b.startswith('refs/remotes/origin/'):
1220 return True
1220 return True
1221 for b, revision in branch2rev.iteritems():
1221 for b, revision in branch2rev.iteritems():
1222 if b.startswith('refs/remotes/origin/'):
1222 if b.startswith('refs/remotes/origin/'):
1223 if self._gitisancestor(self._state[1], revision):
1223 if self._gitisancestor(self._state[1], revision):
1224 return True
1224 return True
1225 # otherwise, try to push the currently checked out branch
1225 # otherwise, try to push the currently checked out branch
1226 cmd = ['push']
1226 cmd = ['push']
1227 if force:
1227 if force:
1228 cmd.append('--force')
1228 cmd.append('--force')
1229
1229
1230 current = self._gitcurrentbranch()
1230 current = self._gitcurrentbranch()
1231 if current:
1231 if current:
1232 # determine if the current branch is even useful
1232 # determine if the current branch is even useful
1233 if not self._gitisancestor(self._state[1], current):
1233 if not self._gitisancestor(self._state[1], current):
1234 self._ui.warn(_('unrelated git branch checked out '
1234 self._ui.warn(_('unrelated git branch checked out '
1235 'in subrepo %s\n') % self._relpath)
1235 'in subrepo %s\n') % self._relpath)
1236 return False
1236 return False
1237 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1237 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1238 (current.split('/', 2)[2], self._relpath))
1238 (current.split('/', 2)[2], self._relpath))
1239 self._gitcommand(cmd + ['origin', current])
1239 self._gitcommand(cmd + ['origin', current])
1240 return True
1240 return True
1241 else:
1241 else:
1242 self._ui.warn(_('no branch checked out in subrepo %s\n'
1242 self._ui.warn(_('no branch checked out in subrepo %s\n'
1243 'cannot push revision %s\n') %
1243 'cannot push revision %s\n') %
1244 (self._relpath, self._state[1]))
1244 (self._relpath, self._state[1]))
1245 return False
1245 return False
1246
1246
1247 @annotatesubrepoerror
1247 @annotatesubrepoerror
1248 def remove(self):
1248 def remove(self):
1249 if self._gitmissing():
1249 if self._gitmissing():
1250 return
1250 return
1251 if self.dirty():
1251 if self.dirty():
1252 self._ui.warn(_('not removing repo %s because '
1252 self._ui.warn(_('not removing repo %s because '
1253 'it has changes.\n') % self._relpath)
1253 'it has changes.\n') % self._relpath)
1254 return
1254 return
1255 # we can't fully delete the repository as it may contain
1255 # we can't fully delete the repository as it may contain
1256 # local-only history
1256 # local-only history
1257 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1257 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1258 self._gitcommand(['config', 'core.bare', 'true'])
1258 self._gitcommand(['config', 'core.bare', 'true'])
1259 for f in os.listdir(self._abspath):
1259 for f in os.listdir(self._abspath):
1260 if f == '.git':
1260 if f == '.git':
1261 continue
1261 continue
1262 path = os.path.join(self._abspath, f)
1262 path = os.path.join(self._abspath, f)
1263 if os.path.isdir(path) and not os.path.islink(path):
1263 if os.path.isdir(path) and not os.path.islink(path):
1264 shutil.rmtree(path)
1264 shutil.rmtree(path)
1265 else:
1265 else:
1266 os.remove(path)
1266 os.remove(path)
1267
1267
1268 def archive(self, ui, archiver, prefix, match=None):
1268 def archive(self, ui, archiver, prefix, match=None):
1269 source, revision = self._state
1269 source, revision = self._state
1270 if not revision:
1270 if not revision:
1271 return
1271 return
1272 self._fetch(source, revision)
1272 self._fetch(source, revision)
1273
1273
1274 # Parse git's native archive command.
1274 # Parse git's native archive command.
1275 # This should be much faster than manually traversing the trees
1275 # This should be much faster than manually traversing the trees
1276 # and objects with many subprocess calls.
1276 # and objects with many subprocess calls.
1277 tarstream = self._gitcommand(['archive', revision], stream=True)
1277 tarstream = self._gitcommand(['archive', revision], stream=True)
1278 tar = tarfile.open(fileobj=tarstream, mode='r|')
1278 tar = tarfile.open(fileobj=tarstream, mode='r|')
1279 relpath = subrelpath(self)
1279 relpath = subrelpath(self)
1280 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1280 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1281 for i, info in enumerate(tar):
1281 for i, info in enumerate(tar):
1282 if info.isdir():
1282 if info.isdir():
1283 continue
1283 continue
1284 if match and not match(info.name):
1284 if match and not match(info.name):
1285 continue
1285 continue
1286 if info.issym():
1286 if info.issym():
1287 data = info.linkname
1287 data = info.linkname
1288 else:
1288 else:
1289 data = tar.extractfile(info).read()
1289 data = tar.extractfile(info).read()
1290 archiver.addfile(os.path.join(prefix, self._path, info.name),
1290 archiver.addfile(os.path.join(prefix, self._path, info.name),
1291 info.mode, info.issym(), data)
1291 info.mode, info.issym(), data)
1292 ui.progress(_('archiving (%s)') % relpath, i + 1,
1292 ui.progress(_('archiving (%s)') % relpath, i + 1,
1293 unit=_('files'))
1293 unit=_('files'))
1294 ui.progress(_('archiving (%s)') % relpath, None)
1294 ui.progress(_('archiving (%s)') % relpath, None)
1295
1295
1296
1296
1297 @annotatesubrepoerror
1297 @annotatesubrepoerror
1298 def status(self, rev2, **opts):
1298 def status(self, rev2, **opts):
1299 rev1 = self._state[1]
1299 rev1 = self._state[1]
1300 if self._gitmissing() or not rev1:
1300 if self._gitmissing() or not rev1:
1301 # if the repo is missing, return no results
1301 # if the repo is missing, return no results
1302 return [], [], [], [], [], [], []
1302 return [], [], [], [], [], [], []
1303 modified, added, removed = [], [], []
1303 modified, added, removed = [], [], []
1304 self._gitupdatestat()
1304 self._gitupdatestat()
1305 if rev2:
1305 if rev2:
1306 command = ['diff-tree', rev1, rev2]
1306 command = ['diff-tree', rev1, rev2]
1307 else:
1307 else:
1308 command = ['diff-index', rev1]
1308 command = ['diff-index', rev1]
1309 out = self._gitcommand(command)
1309 out = self._gitcommand(command)
1310 for line in out.split('\n'):
1310 for line in out.split('\n'):
1311 tab = line.find('\t')
1311 tab = line.find('\t')
1312 if tab == -1:
1312 if tab == -1:
1313 continue
1313 continue
1314 status, f = line[tab - 1], line[tab + 1:]
1314 status, f = line[tab - 1], line[tab + 1:]
1315 if status == 'M':
1315 if status == 'M':
1316 modified.append(f)
1316 modified.append(f)
1317 elif status == 'A':
1317 elif status == 'A':
1318 added.append(f)
1318 added.append(f)
1319 elif status == 'D':
1319 elif status == 'D':
1320 removed.append(f)
1320 removed.append(f)
1321
1321
1322 deleted = unknown = ignored = clean = []
1322 deleted = unknown = ignored = clean = []
1323 return modified, added, removed, deleted, unknown, ignored, clean
1323 return modified, added, removed, deleted, unknown, ignored, clean
1324
1324
1325 types = {
1325 types = {
1326 'hg': hgsubrepo,
1326 'hg': hgsubrepo,
1327 'svn': svnsubrepo,
1327 'svn': svnsubrepo,
1328 'git': gitsubrepo,
1328 'git': gitsubrepo,
1329 }
1329 }
General Comments 0
You need to be logged in to leave comments. Login now