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