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