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