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