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