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