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