##// END OF EJS Templates
subrepo: re-backout 2245fcd0e160...
Matt Mackall -
r13137:7397a532 default
parent child Browse files
Show More
@@ -1,883 +1,881 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, urlparse, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, urlparse, posixpath
9 import stat, subprocess, tarfile
9 import stat, subprocess, tarfile
10 from i18n import _
10 from i18n import _
11 import config, util, node, error, cmdutil
11 import config, util, node, error, cmdutil
12 hg = None
12 hg = None
13
13
14 nullstate = ('', '', 'empty')
14 nullstate = ('', '', 'empty')
15
15
16 def state(ctx, ui):
16 def state(ctx, ui):
17 """return a state dict, mapping subrepo paths configured in .hgsub
17 """return a state dict, mapping subrepo paths configured in .hgsub
18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 (key in types dict))
19 (key in types dict))
20 """
20 """
21 p = config.config()
21 p = config.config()
22 def read(f, sections=None, remap=None):
22 def read(f, sections=None, remap=None):
23 if f in ctx:
23 if f in ctx:
24 try:
24 try:
25 data = ctx[f].data()
25 data = ctx[f].data()
26 except IOError, err:
26 except IOError, err:
27 if err.errno != errno.ENOENT:
27 if err.errno != errno.ENOENT:
28 raise
28 raise
29 # handle missing subrepo spec files as removed
29 # handle missing subrepo spec files as removed
30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 return
31 return
32 p.parse(f, data, sections, remap, read)
32 p.parse(f, data, sections, remap, read)
33 else:
33 else:
34 raise util.Abort(_("subrepo spec file %s not found") % f)
34 raise util.Abort(_("subrepo spec file %s not found") % f)
35
35
36 if '.hgsub' in ctx:
36 if '.hgsub' in ctx:
37 read('.hgsub')
37 read('.hgsub')
38
38
39 for path, src in ui.configitems('subpaths'):
39 for path, src in ui.configitems('subpaths'):
40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41
41
42 rev = {}
42 rev = {}
43 if '.hgsubstate' in ctx:
43 if '.hgsubstate' in ctx:
44 try:
44 try:
45 for l in ctx['.hgsubstate'].data().splitlines():
45 for l in ctx['.hgsubstate'].data().splitlines():
46 revision, path = l.split(" ", 1)
46 revision, path = l.split(" ", 1)
47 rev[path] = revision
47 rev[path] = revision
48 except IOError, err:
48 except IOError, err:
49 if err.errno != errno.ENOENT:
49 if err.errno != errno.ENOENT:
50 raise
50 raise
51
51
52 state = {}
52 state = {}
53 for path, src in p[''].items():
53 for path, src in p[''].items():
54 kind = 'hg'
54 kind = 'hg'
55 if src.startswith('['):
55 if src.startswith('['):
56 if ']' not in src:
56 if ']' not in src:
57 raise util.Abort(_('missing ] in subrepo source'))
57 raise util.Abort(_('missing ] in subrepo source'))
58 kind, src = src.split(']', 1)
58 kind, src = src.split(']', 1)
59 kind = kind[1:]
59 kind = kind[1:]
60
60
61 for pattern, repl in p.items('subpaths'):
61 for pattern, repl in p.items('subpaths'):
62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 # does a string decode.
63 # does a string decode.
64 repl = repl.encode('string-escape')
64 repl = repl.encode('string-escape')
65 # However, we still want to allow back references to go
65 # However, we still want to allow back references to go
66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 # extra escapes are needed because re.sub string decodes.
67 # extra escapes are needed because re.sub string decodes.
68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 try:
69 try:
70 src = re.sub(pattern, repl, src, 1)
70 src = re.sub(pattern, repl, src, 1)
71 except re.error, e:
71 except re.error, e:
72 raise util.Abort(_("bad subrepository pattern in %s: %s")
72 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 % (p.source('subpaths', pattern), e))
73 % (p.source('subpaths', pattern), e))
74
74
75 state[path] = (src.strip(), rev.get(path, ''), kind)
75 state[path] = (src.strip(), rev.get(path, ''), kind)
76
76
77 return state
77 return state
78
78
79 def writestate(repo, state):
79 def writestate(repo, state):
80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 repo.wwrite('.hgsubstate',
81 repo.wwrite('.hgsubstate',
82 ''.join(['%s %s\n' % (state[s][1], s)
82 ''.join(['%s %s\n' % (state[s][1], s)
83 for s in sorted(state)]), '')
83 for s in sorted(state)]), '')
84
84
85 def submerge(repo, wctx, mctx, actx):
85 def submerge(repo, wctx, mctx, actx):
86 """delegated from merge.applyupdates: merging of .hgsubstate file
86 """delegated from merge.applyupdates: merging of .hgsubstate file
87 in working context, merging context and ancestor context"""
87 in working context, merging context and ancestor context"""
88 if mctx == actx: # backwards?
88 if mctx == actx: # backwards?
89 actx = wctx.p1()
89 actx = wctx.p1()
90 s1 = wctx.substate
90 s1 = wctx.substate
91 s2 = mctx.substate
91 s2 = mctx.substate
92 sa = actx.substate
92 sa = actx.substate
93 sm = {}
93 sm = {}
94
94
95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96
96
97 def debug(s, msg, r=""):
97 def debug(s, msg, r=""):
98 if r:
98 if r:
99 r = "%s:%s:%s" % r
99 r = "%s:%s:%s" % r
100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101
101
102 for s, l in s1.items():
102 for s, l in s1.items():
103 a = sa.get(s, nullstate)
103 a = sa.get(s, nullstate)
104 ld = l # local state with possible dirty flag for compares
104 ld = l # local state with possible dirty flag for compares
105 if wctx.sub(s).dirty():
105 if wctx.sub(s).dirty():
106 ld = (l[0], l[1] + "+")
106 ld = (l[0], l[1] + "+")
107 if wctx == actx: # overwrite
107 if wctx == actx: # overwrite
108 a = ld
108 a = ld
109
109
110 if s in s2:
110 if s in s2:
111 r = s2[s]
111 r = s2[s]
112 if ld == r or r == a: # no change or local is newer
112 if ld == r or r == a: # no change or local is newer
113 sm[s] = l
113 sm[s] = l
114 continue
114 continue
115 elif ld == a: # other side changed
115 elif ld == a: # other side changed
116 debug(s, "other changed, get", r)
116 debug(s, "other changed, get", r)
117 wctx.sub(s).get(r)
117 wctx.sub(s).get(r)
118 sm[s] = r
118 sm[s] = r
119 elif ld[0] != r[0]: # sources differ
119 elif ld[0] != r[0]: # sources differ
120 if repo.ui.promptchoice(
120 if repo.ui.promptchoice(
121 _(' subrepository sources for %s differ\n'
121 _(' subrepository sources for %s differ\n'
122 'use (l)ocal source (%s) or (r)emote source (%s)?')
122 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 % (s, l[0], r[0]),
123 % (s, l[0], r[0]),
124 (_('&Local'), _('&Remote')), 0):
124 (_('&Local'), _('&Remote')), 0):
125 debug(s, "prompt changed, get", r)
125 debug(s, "prompt changed, get", r)
126 wctx.sub(s).get(r)
126 wctx.sub(s).get(r)
127 sm[s] = r
127 sm[s] = r
128 elif ld[1] == a[1]: # local side is unchanged
128 elif ld[1] == a[1]: # local side is unchanged
129 debug(s, "other side changed, get", r)
129 debug(s, "other side changed, get", r)
130 wctx.sub(s).get(r)
130 wctx.sub(s).get(r)
131 sm[s] = r
131 sm[s] = r
132 else:
132 else:
133 debug(s, "both sides changed, merge with", r)
133 debug(s, "both sides changed, merge with", r)
134 wctx.sub(s).merge(r)
134 wctx.sub(s).merge(r)
135 sm[s] = l
135 sm[s] = l
136 elif ld == a: # remote removed, local unchanged
136 elif ld == a: # remote removed, local unchanged
137 debug(s, "remote removed, remove")
137 debug(s, "remote removed, remove")
138 wctx.sub(s).remove()
138 wctx.sub(s).remove()
139 else:
139 else:
140 if repo.ui.promptchoice(
140 if repo.ui.promptchoice(
141 _(' local changed subrepository %s which remote removed\n'
141 _(' local changed subrepository %s which remote removed\n'
142 'use (c)hanged version or (d)elete?') % s,
142 'use (c)hanged version or (d)elete?') % s,
143 (_('&Changed'), _('&Delete')), 0):
143 (_('&Changed'), _('&Delete')), 0):
144 debug(s, "prompt remove")
144 debug(s, "prompt remove")
145 wctx.sub(s).remove()
145 wctx.sub(s).remove()
146
146
147 for s, r in s2.items():
147 for s, r in s2.items():
148 if s in s1:
148 if s in s1:
149 continue
149 continue
150 elif s not in sa:
150 elif s not in sa:
151 debug(s, "remote added, get", r)
151 debug(s, "remote added, get", r)
152 mctx.sub(s).get(r)
152 mctx.sub(s).get(r)
153 sm[s] = r
153 sm[s] = r
154 elif r != sa[s]:
154 elif r != sa[s]:
155 if repo.ui.promptchoice(
155 if repo.ui.promptchoice(
156 _(' remote changed subrepository %s which local removed\n'
156 _(' remote changed subrepository %s which local removed\n'
157 'use (c)hanged version or (d)elete?') % s,
157 'use (c)hanged version or (d)elete?') % s,
158 (_('&Changed'), _('&Delete')), 0) == 0:
158 (_('&Changed'), _('&Delete')), 0) == 0:
159 debug(s, "prompt recreate", r)
159 debug(s, "prompt recreate", r)
160 wctx.sub(s).get(r)
160 wctx.sub(s).get(r)
161 sm[s] = r
161 sm[s] = r
162
162
163 # record merged .hgsubstate
163 # record merged .hgsubstate
164 writestate(repo, sm)
164 writestate(repo, sm)
165
165
166 def reporelpath(repo):
166 def reporelpath(repo):
167 """return path to this (sub)repo as seen from outermost repo"""
167 """return path to this (sub)repo as seen from outermost repo"""
168 parent = repo
168 parent = repo
169 while hasattr(parent, '_subparent'):
169 while hasattr(parent, '_subparent'):
170 parent = parent._subparent
170 parent = parent._subparent
171 return repo.root[len(parent.root)+1:]
171 return repo.root[len(parent.root)+1:]
172
172
173 def subrelpath(sub):
173 def subrelpath(sub):
174 """return path to this subrepo as seen from outermost repo"""
174 """return path to this subrepo as seen from outermost repo"""
175 if not hasattr(sub, '_repo'):
175 if not hasattr(sub, '_repo'):
176 return sub._path
176 return sub._path
177 return reporelpath(sub._repo)
177 return reporelpath(sub._repo)
178
178
179 def _abssource(repo, push=False, abort=True):
179 def _abssource(repo, push=False, abort=True):
180 """return pull/push path of repo - either based on parent repo .hgsub info
180 """return pull/push path of repo - either based on parent repo .hgsub info
181 or on the top repo config. Abort or return None if no source found."""
181 or on the top repo config. Abort or return None if no source found."""
182 if hasattr(repo, '_subparent'):
182 if hasattr(repo, '_subparent'):
183 source = repo._subsource
183 source = repo._subsource
184 if source.startswith('/') or '://' in source:
184 if source.startswith('/') or '://' in source:
185 return source
185 return source
186 parent = _abssource(repo._subparent, push, abort=False)
186 parent = _abssource(repo._subparent, push, abort=False)
187 if parent:
187 if parent:
188 if '://' in parent:
188 if '://' in parent:
189 if parent[-1] == '/':
189 if parent[-1] == '/':
190 parent = parent[:-1]
190 parent = parent[:-1]
191 r = urlparse.urlparse(parent + '/' + source)
191 r = urlparse.urlparse(parent + '/' + source)
192 r = urlparse.urlunparse((r[0], r[1],
192 r = urlparse.urlunparse((r[0], r[1],
193 posixpath.normpath(r[2]),
193 posixpath.normpath(r[2]),
194 r[3], r[4], r[5]))
194 r[3], r[4], r[5]))
195 return r
195 return r
196 else: # plain file system path
196 else: # plain file system path
197 return posixpath.normpath(os.path.join(parent, repo._subsource))
197 return posixpath.normpath(os.path.join(parent, repo._subsource))
198 else: # recursion reached top repo
198 else: # recursion reached top repo
199 if hasattr(repo, '_subtoppath'):
199 if hasattr(repo, '_subtoppath'):
200 return repo._subtoppath
200 return repo._subtoppath
201 if push and repo.ui.config('paths', 'default-push'):
201 if push and repo.ui.config('paths', 'default-push'):
202 return repo.ui.config('paths', 'default-push')
202 return repo.ui.config('paths', 'default-push')
203 if repo.ui.config('paths', 'default'):
203 if repo.ui.config('paths', 'default'):
204 return repo.ui.config('paths', 'default')
204 return repo.ui.config('paths', 'default')
205 if abort:
205 if abort:
206 raise util.Abort(_("default path for subrepository %s not found") %
206 raise util.Abort(_("default path for subrepository %s not found") %
207 reporelpath(repo))
207 reporelpath(repo))
208
208
209 def itersubrepos(ctx1, ctx2):
209 def itersubrepos(ctx1, ctx2):
210 """find subrepos in ctx1 or ctx2"""
210 """find subrepos in ctx1 or ctx2"""
211 # Create a (subpath, ctx) mapping where we prefer subpaths from
211 # Create a (subpath, ctx) mapping where we prefer subpaths from
212 # ctx1. The subpaths from ctx2 are important when the .hgsub file
212 # ctx1. The subpaths from ctx2 are important when the .hgsub file
213 # has been modified (in ctx2) but not yet committed (in ctx1).
213 # has been modified (in ctx2) but not yet committed (in ctx1).
214 subpaths = dict.fromkeys(ctx2.substate, ctx2)
214 subpaths = dict.fromkeys(ctx2.substate, ctx2)
215 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
215 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
216 for subpath, ctx in sorted(subpaths.iteritems()):
216 for subpath, ctx in sorted(subpaths.iteritems()):
217 yield subpath, ctx.sub(subpath)
217 yield subpath, ctx.sub(subpath)
218
218
219 def subrepo(ctx, path):
219 def subrepo(ctx, path):
220 """return instance of the right subrepo class for subrepo in path"""
220 """return instance of the right subrepo class for subrepo in path"""
221 # subrepo inherently violates our import layering rules
221 # subrepo inherently violates our import layering rules
222 # because it wants to make repo objects from deep inside the stack
222 # because it wants to make repo objects from deep inside the stack
223 # so we manually delay the circular imports to not break
223 # so we manually delay the circular imports to not break
224 # scripts that don't use our demand-loading
224 # scripts that don't use our demand-loading
225 global hg
225 global hg
226 import hg as h
226 import hg as h
227 hg = h
227 hg = h
228
228
229 util.path_auditor(ctx._repo.root)(path)
229 util.path_auditor(ctx._repo.root)(path)
230 state = ctx.substate.get(path, nullstate)
230 state = ctx.substate.get(path, nullstate)
231 if state[2] not in types:
231 if state[2] not in types:
232 raise util.Abort(_('unknown subrepo type %s') % state[2])
232 raise util.Abort(_('unknown subrepo type %s') % state[2])
233 return types[state[2]](ctx, path, state[:2])
233 return types[state[2]](ctx, path, state[:2])
234
234
235 # subrepo classes need to implement the following abstract class:
235 # subrepo classes need to implement the following abstract class:
236
236
237 class abstractsubrepo(object):
237 class abstractsubrepo(object):
238
238
239 def dirty(self):
239 def dirty(self):
240 """returns true if the dirstate of the subrepo does not match
240 """returns true if the dirstate of the subrepo does not match
241 current stored state
241 current stored state
242 """
242 """
243 raise NotImplementedError
243 raise NotImplementedError
244
244
245 def checknested(self, path):
245 def checknested(self, path):
246 """check if path is a subrepository within this repository"""
246 """check if path is a subrepository within this repository"""
247 return False
247 return False
248
248
249 def commit(self, text, user, date):
249 def commit(self, text, user, date):
250 """commit the current changes to the subrepo with the given
250 """commit the current changes to the subrepo with the given
251 log message. Use given user and date if possible. Return the
251 log message. Use given user and date if possible. Return the
252 new state of the subrepo.
252 new state of the subrepo.
253 """
253 """
254 raise NotImplementedError
254 raise NotImplementedError
255
255
256 def remove(self):
256 def remove(self):
257 """remove the subrepo
257 """remove the subrepo
258
258
259 (should verify the dirstate is not dirty first)
259 (should verify the dirstate is not dirty first)
260 """
260 """
261 raise NotImplementedError
261 raise NotImplementedError
262
262
263 def get(self, state):
263 def get(self, state):
264 """run whatever commands are needed to put the subrepo into
264 """run whatever commands are needed to put the subrepo into
265 this state
265 this state
266 """
266 """
267 raise NotImplementedError
267 raise NotImplementedError
268
268
269 def merge(self, state):
269 def merge(self, state):
270 """merge currently-saved state with the new state."""
270 """merge currently-saved state with the new state."""
271 raise NotImplementedError
271 raise NotImplementedError
272
272
273 def push(self, force):
273 def push(self, force):
274 """perform whatever action is analogous to 'hg push'
274 """perform whatever action is analogous to 'hg push'
275
275
276 This may be a no-op on some systems.
276 This may be a no-op on some systems.
277 """
277 """
278 raise NotImplementedError
278 raise NotImplementedError
279
279
280 def add(self, ui, match, dryrun, prefix):
280 def add(self, ui, match, dryrun, prefix):
281 return []
281 return []
282
282
283 def status(self, rev2, **opts):
283 def status(self, rev2, **opts):
284 return [], [], [], [], [], [], []
284 return [], [], [], [], [], [], []
285
285
286 def diff(self, diffopts, node2, match, prefix, **opts):
286 def diff(self, diffopts, node2, match, prefix, **opts):
287 pass
287 pass
288
288
289 def outgoing(self, ui, dest, opts):
289 def outgoing(self, ui, dest, opts):
290 return 1
290 return 1
291
291
292 def incoming(self, ui, source, opts):
292 def incoming(self, ui, source, opts):
293 return 1
293 return 1
294
294
295 def files(self):
295 def files(self):
296 """return filename iterator"""
296 """return filename iterator"""
297 raise NotImplementedError
297 raise NotImplementedError
298
298
299 def filedata(self, name):
299 def filedata(self, name):
300 """return file data"""
300 """return file data"""
301 raise NotImplementedError
301 raise NotImplementedError
302
302
303 def fileflags(self, name):
303 def fileflags(self, name):
304 """return file flags"""
304 """return file flags"""
305 return ''
305 return ''
306
306
307 def archive(self, archiver, prefix):
307 def archive(self, archiver, prefix):
308 for name in self.files():
308 for name in self.files():
309 flags = self.fileflags(name)
309 flags = self.fileflags(name)
310 mode = 'x' in flags and 0755 or 0644
310 mode = 'x' in flags and 0755 or 0644
311 symlink = 'l' in flags
311 symlink = 'l' in flags
312 archiver.addfile(os.path.join(prefix, self._path, name),
312 archiver.addfile(os.path.join(prefix, self._path, name),
313 mode, symlink, self.filedata(name))
313 mode, symlink, self.filedata(name))
314
314
315
315
316 class hgsubrepo(abstractsubrepo):
316 class hgsubrepo(abstractsubrepo):
317 def __init__(self, ctx, path, state):
317 def __init__(self, ctx, path, state):
318 self._path = path
318 self._path = path
319 self._state = state
319 self._state = state
320 r = ctx._repo
320 r = ctx._repo
321 root = r.wjoin(path)
321 root = r.wjoin(path)
322 create = False
322 create = False
323 if not os.path.exists(os.path.join(root, '.hg')):
323 if not os.path.exists(os.path.join(root, '.hg')):
324 create = True
324 create = True
325 util.makedirs(root)
325 util.makedirs(root)
326 self._repo = hg.repository(r.ui, root, create=create)
326 self._repo = hg.repository(r.ui, root, create=create)
327 self._repo._subparent = r
327 self._repo._subparent = r
328 self._repo._subsource = state[0]
328 self._repo._subsource = state[0]
329
329
330 if create:
330 if create:
331 fp = self._repo.opener("hgrc", "w", text=True)
331 fp = self._repo.opener("hgrc", "w", text=True)
332 fp.write('[paths]\n')
332 fp.write('[paths]\n')
333
333
334 def addpathconfig(key, value):
334 def addpathconfig(key, value):
335 if value:
335 if value:
336 if not os.path.isabs(value):
337 value = os.path.relpath(os.path.abspath(value), root)
338 fp.write('%s = %s\n' % (key, value))
336 fp.write('%s = %s\n' % (key, value))
339 self._repo.ui.setconfig('paths', key, value)
337 self._repo.ui.setconfig('paths', key, value)
340
338
341 defpath = _abssource(self._repo, abort=False)
339 defpath = _abssource(self._repo, abort=False)
342 defpushpath = _abssource(self._repo, True, abort=False)
340 defpushpath = _abssource(self._repo, True, abort=False)
343 addpathconfig('default', defpath)
341 addpathconfig('default', defpath)
344 if defpath != defpushpath:
342 if defpath != defpushpath:
345 addpathconfig('default-push', defpushpath)
343 addpathconfig('default-push', defpushpath)
346 fp.close()
344 fp.close()
347
345
348 def add(self, ui, match, dryrun, prefix):
346 def add(self, ui, match, dryrun, prefix):
349 return cmdutil.add(ui, self._repo, match, dryrun, True,
347 return cmdutil.add(ui, self._repo, match, dryrun, True,
350 os.path.join(prefix, self._path))
348 os.path.join(prefix, self._path))
351
349
352 def status(self, rev2, **opts):
350 def status(self, rev2, **opts):
353 try:
351 try:
354 rev1 = self._state[1]
352 rev1 = self._state[1]
355 ctx1 = self._repo[rev1]
353 ctx1 = self._repo[rev1]
356 ctx2 = self._repo[rev2]
354 ctx2 = self._repo[rev2]
357 return self._repo.status(ctx1, ctx2, **opts)
355 return self._repo.status(ctx1, ctx2, **opts)
358 except error.RepoLookupError, inst:
356 except error.RepoLookupError, inst:
359 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
357 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
360 % (inst, subrelpath(self)))
358 % (inst, subrelpath(self)))
361 return [], [], [], [], [], [], []
359 return [], [], [], [], [], [], []
362
360
363 def diff(self, diffopts, node2, match, prefix, **opts):
361 def diff(self, diffopts, node2, match, prefix, **opts):
364 try:
362 try:
365 node1 = node.bin(self._state[1])
363 node1 = node.bin(self._state[1])
366 # We currently expect node2 to come from substate and be
364 # We currently expect node2 to come from substate and be
367 # in hex format
365 # in hex format
368 if node2 is not None:
366 if node2 is not None:
369 node2 = node.bin(node2)
367 node2 = node.bin(node2)
370 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
368 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
371 node1, node2, match,
369 node1, node2, match,
372 prefix=os.path.join(prefix, self._path),
370 prefix=os.path.join(prefix, self._path),
373 listsubrepos=True, **opts)
371 listsubrepos=True, **opts)
374 except error.RepoLookupError, inst:
372 except error.RepoLookupError, inst:
375 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
373 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
376 % (inst, subrelpath(self)))
374 % (inst, subrelpath(self)))
377
375
378 def archive(self, archiver, prefix):
376 def archive(self, archiver, prefix):
379 abstractsubrepo.archive(self, archiver, prefix)
377 abstractsubrepo.archive(self, archiver, prefix)
380
378
381 rev = self._state[1]
379 rev = self._state[1]
382 ctx = self._repo[rev]
380 ctx = self._repo[rev]
383 for subpath in ctx.substate:
381 for subpath in ctx.substate:
384 s = subrepo(ctx, subpath)
382 s = subrepo(ctx, subpath)
385 s.archive(archiver, os.path.join(prefix, self._path))
383 s.archive(archiver, os.path.join(prefix, self._path))
386
384
387 def dirty(self):
385 def dirty(self):
388 r = self._state[1]
386 r = self._state[1]
389 if r == '':
387 if r == '':
390 return True
388 return True
391 w = self._repo[None]
389 w = self._repo[None]
392 if w.p1() != self._repo[r]: # version checked out change
390 if w.p1() != self._repo[r]: # version checked out change
393 return True
391 return True
394 return w.dirty() # working directory changed
392 return w.dirty() # working directory changed
395
393
396 def checknested(self, path):
394 def checknested(self, path):
397 return self._repo._checknested(self._repo.wjoin(path))
395 return self._repo._checknested(self._repo.wjoin(path))
398
396
399 def commit(self, text, user, date):
397 def commit(self, text, user, date):
400 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
398 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
401 n = self._repo.commit(text, user, date)
399 n = self._repo.commit(text, user, date)
402 if not n:
400 if not n:
403 return self._repo['.'].hex() # different version checked out
401 return self._repo['.'].hex() # different version checked out
404 return node.hex(n)
402 return node.hex(n)
405
403
406 def remove(self):
404 def remove(self):
407 # we can't fully delete the repository as it may contain
405 # we can't fully delete the repository as it may contain
408 # local-only history
406 # local-only history
409 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
407 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
410 hg.clean(self._repo, node.nullid, False)
408 hg.clean(self._repo, node.nullid, False)
411
409
412 def _get(self, state):
410 def _get(self, state):
413 source, revision, kind = state
411 source, revision, kind = state
414 try:
412 try:
415 self._repo.lookup(revision)
413 self._repo.lookup(revision)
416 except error.RepoError:
414 except error.RepoError:
417 self._repo._subsource = source
415 self._repo._subsource = source
418 srcurl = _abssource(self._repo)
416 srcurl = _abssource(self._repo)
419 self._repo.ui.status(_('pulling subrepo %s from %s\n')
417 self._repo.ui.status(_('pulling subrepo %s from %s\n')
420 % (subrelpath(self), srcurl))
418 % (subrelpath(self), srcurl))
421 other = hg.repository(self._repo.ui, srcurl)
419 other = hg.repository(self._repo.ui, srcurl)
422 self._repo.pull(other)
420 self._repo.pull(other)
423
421
424 def get(self, state):
422 def get(self, state):
425 self._get(state)
423 self._get(state)
426 source, revision, kind = state
424 source, revision, kind = state
427 self._repo.ui.debug("getting subrepo %s\n" % self._path)
425 self._repo.ui.debug("getting subrepo %s\n" % self._path)
428 hg.clean(self._repo, revision, False)
426 hg.clean(self._repo, revision, False)
429
427
430 def merge(self, state):
428 def merge(self, state):
431 self._get(state)
429 self._get(state)
432 cur = self._repo['.']
430 cur = self._repo['.']
433 dst = self._repo[state[1]]
431 dst = self._repo[state[1]]
434 anc = dst.ancestor(cur)
432 anc = dst.ancestor(cur)
435 if anc == cur:
433 if anc == cur:
436 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
434 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
437 hg.update(self._repo, state[1])
435 hg.update(self._repo, state[1])
438 elif anc == dst:
436 elif anc == dst:
439 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
437 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
440 else:
438 else:
441 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
439 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
442 hg.merge(self._repo, state[1], remind=False)
440 hg.merge(self._repo, state[1], remind=False)
443
441
444 def push(self, force):
442 def push(self, force):
445 # push subrepos depth-first for coherent ordering
443 # push subrepos depth-first for coherent ordering
446 c = self._repo['']
444 c = self._repo['']
447 subs = c.substate # only repos that are committed
445 subs = c.substate # only repos that are committed
448 for s in sorted(subs):
446 for s in sorted(subs):
449 if not c.sub(s).push(force):
447 if not c.sub(s).push(force):
450 return False
448 return False
451
449
452 dsturl = _abssource(self._repo, True)
450 dsturl = _abssource(self._repo, True)
453 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
451 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
454 (subrelpath(self), dsturl))
452 (subrelpath(self), dsturl))
455 other = hg.repository(self._repo.ui, dsturl)
453 other = hg.repository(self._repo.ui, dsturl)
456 return self._repo.push(other, force)
454 return self._repo.push(other, force)
457
455
458 def outgoing(self, ui, dest, opts):
456 def outgoing(self, ui, dest, opts):
459 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
457 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
460
458
461 def incoming(self, ui, source, opts):
459 def incoming(self, ui, source, opts):
462 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
460 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
463
461
464 def files(self):
462 def files(self):
465 rev = self._state[1]
463 rev = self._state[1]
466 ctx = self._repo[rev]
464 ctx = self._repo[rev]
467 return ctx.manifest()
465 return ctx.manifest()
468
466
469 def filedata(self, name):
467 def filedata(self, name):
470 rev = self._state[1]
468 rev = self._state[1]
471 return self._repo[rev][name].data()
469 return self._repo[rev][name].data()
472
470
473 def fileflags(self, name):
471 def fileflags(self, name):
474 rev = self._state[1]
472 rev = self._state[1]
475 ctx = self._repo[rev]
473 ctx = self._repo[rev]
476 return ctx.flags(name)
474 return ctx.flags(name)
477
475
478
476
479 class svnsubrepo(abstractsubrepo):
477 class svnsubrepo(abstractsubrepo):
480 def __init__(self, ctx, path, state):
478 def __init__(self, ctx, path, state):
481 self._path = path
479 self._path = path
482 self._state = state
480 self._state = state
483 self._ctx = ctx
481 self._ctx = ctx
484 self._ui = ctx._repo.ui
482 self._ui = ctx._repo.ui
485
483
486 def _svncommand(self, commands, filename=''):
484 def _svncommand(self, commands, filename=''):
487 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
485 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
488 cmd = ['svn'] + commands + [path]
486 cmd = ['svn'] + commands + [path]
489 env = dict(os.environ)
487 env = dict(os.environ)
490 # Avoid localized output, preserve current locale for everything else.
488 # Avoid localized output, preserve current locale for everything else.
491 env['LC_MESSAGES'] = 'C'
489 env['LC_MESSAGES'] = 'C'
492 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
490 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
493 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
491 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
494 universal_newlines=True, env=env)
492 universal_newlines=True, env=env)
495 stdout, stderr = p.communicate()
493 stdout, stderr = p.communicate()
496 stderr = stderr.strip()
494 stderr = stderr.strip()
497 if stderr:
495 if stderr:
498 raise util.Abort(stderr)
496 raise util.Abort(stderr)
499 return stdout
497 return stdout
500
498
501 def _wcrev(self):
499 def _wcrev(self):
502 output = self._svncommand(['info', '--xml'])
500 output = self._svncommand(['info', '--xml'])
503 doc = xml.dom.minidom.parseString(output)
501 doc = xml.dom.minidom.parseString(output)
504 entries = doc.getElementsByTagName('entry')
502 entries = doc.getElementsByTagName('entry')
505 if not entries:
503 if not entries:
506 return '0'
504 return '0'
507 return str(entries[0].getAttribute('revision')) or '0'
505 return str(entries[0].getAttribute('revision')) or '0'
508
506
509 def _wcchanged(self):
507 def _wcchanged(self):
510 """Return (changes, extchanges) where changes is True
508 """Return (changes, extchanges) where changes is True
511 if the working directory was changed, and extchanges is
509 if the working directory was changed, and extchanges is
512 True if any of these changes concern an external entry.
510 True if any of these changes concern an external entry.
513 """
511 """
514 output = self._svncommand(['status', '--xml'])
512 output = self._svncommand(['status', '--xml'])
515 externals, changes = [], []
513 externals, changes = [], []
516 doc = xml.dom.minidom.parseString(output)
514 doc = xml.dom.minidom.parseString(output)
517 for e in doc.getElementsByTagName('entry'):
515 for e in doc.getElementsByTagName('entry'):
518 s = e.getElementsByTagName('wc-status')
516 s = e.getElementsByTagName('wc-status')
519 if not s:
517 if not s:
520 continue
518 continue
521 item = s[0].getAttribute('item')
519 item = s[0].getAttribute('item')
522 props = s[0].getAttribute('props')
520 props = s[0].getAttribute('props')
523 path = e.getAttribute('path')
521 path = e.getAttribute('path')
524 if item == 'external':
522 if item == 'external':
525 externals.append(path)
523 externals.append(path)
526 if (item not in ('', 'normal', 'unversioned', 'external')
524 if (item not in ('', 'normal', 'unversioned', 'external')
527 or props not in ('', 'none')):
525 or props not in ('', 'none')):
528 changes.append(path)
526 changes.append(path)
529 for path in changes:
527 for path in changes:
530 for ext in externals:
528 for ext in externals:
531 if path == ext or path.startswith(ext + os.sep):
529 if path == ext or path.startswith(ext + os.sep):
532 return True, True
530 return True, True
533 return bool(changes), False
531 return bool(changes), False
534
532
535 def dirty(self):
533 def dirty(self):
536 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
534 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
537 return False
535 return False
538 return True
536 return True
539
537
540 def commit(self, text, user, date):
538 def commit(self, text, user, date):
541 # user and date are out of our hands since svn is centralized
539 # user and date are out of our hands since svn is centralized
542 changed, extchanged = self._wcchanged()
540 changed, extchanged = self._wcchanged()
543 if not changed:
541 if not changed:
544 return self._wcrev()
542 return self._wcrev()
545 if extchanged:
543 if extchanged:
546 # Do not try to commit externals
544 # Do not try to commit externals
547 raise util.Abort(_('cannot commit svn externals'))
545 raise util.Abort(_('cannot commit svn externals'))
548 commitinfo = self._svncommand(['commit', '-m', text])
546 commitinfo = self._svncommand(['commit', '-m', text])
549 self._ui.status(commitinfo)
547 self._ui.status(commitinfo)
550 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
548 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
551 if not newrev:
549 if not newrev:
552 raise util.Abort(commitinfo.splitlines()[-1])
550 raise util.Abort(commitinfo.splitlines()[-1])
553 newrev = newrev.groups()[0]
551 newrev = newrev.groups()[0]
554 self._ui.status(self._svncommand(['update', '-r', newrev]))
552 self._ui.status(self._svncommand(['update', '-r', newrev]))
555 return newrev
553 return newrev
556
554
557 def remove(self):
555 def remove(self):
558 if self.dirty():
556 if self.dirty():
559 self._ui.warn(_('not removing repo %s because '
557 self._ui.warn(_('not removing repo %s because '
560 'it has changes.\n' % self._path))
558 'it has changes.\n' % self._path))
561 return
559 return
562 self._ui.note(_('removing subrepo %s\n') % self._path)
560 self._ui.note(_('removing subrepo %s\n') % self._path)
563
561
564 def onerror(function, path, excinfo):
562 def onerror(function, path, excinfo):
565 if function is not os.remove:
563 if function is not os.remove:
566 raise
564 raise
567 # read-only files cannot be unlinked under Windows
565 # read-only files cannot be unlinked under Windows
568 s = os.stat(path)
566 s = os.stat(path)
569 if (s.st_mode & stat.S_IWRITE) != 0:
567 if (s.st_mode & stat.S_IWRITE) != 0:
570 raise
568 raise
571 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
569 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
572 os.remove(path)
570 os.remove(path)
573
571
574 path = self._ctx._repo.wjoin(self._path)
572 path = self._ctx._repo.wjoin(self._path)
575 shutil.rmtree(path, onerror=onerror)
573 shutil.rmtree(path, onerror=onerror)
576 try:
574 try:
577 os.removedirs(os.path.dirname(path))
575 os.removedirs(os.path.dirname(path))
578 except OSError:
576 except OSError:
579 pass
577 pass
580
578
581 def get(self, state):
579 def get(self, state):
582 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
580 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
583 if not re.search('Checked out revision [0-9]+.', status):
581 if not re.search('Checked out revision [0-9]+.', status):
584 raise util.Abort(status.splitlines()[-1])
582 raise util.Abort(status.splitlines()[-1])
585 self._ui.status(status)
583 self._ui.status(status)
586
584
587 def merge(self, state):
585 def merge(self, state):
588 old = int(self._state[1])
586 old = int(self._state[1])
589 new = int(state[1])
587 new = int(state[1])
590 if new > old:
588 if new > old:
591 self.get(state)
589 self.get(state)
592
590
593 def push(self, force):
591 def push(self, force):
594 # push is a no-op for SVN
592 # push is a no-op for SVN
595 return True
593 return True
596
594
597 def files(self):
595 def files(self):
598 output = self._svncommand(['list'])
596 output = self._svncommand(['list'])
599 # This works because svn forbids \n in filenames.
597 # This works because svn forbids \n in filenames.
600 return output.splitlines()
598 return output.splitlines()
601
599
602 def filedata(self, name):
600 def filedata(self, name):
603 return self._svncommand(['cat'], name)
601 return self._svncommand(['cat'], name)
604
602
605
603
606 class gitsubrepo(abstractsubrepo):
604 class gitsubrepo(abstractsubrepo):
607 def __init__(self, ctx, path, state):
605 def __init__(self, ctx, path, state):
608 # TODO add git version check.
606 # TODO add git version check.
609 self._state = state
607 self._state = state
610 self._ctx = ctx
608 self._ctx = ctx
611 self._relpath = path
609 self._relpath = path
612 self._path = ctx._repo.wjoin(path)
610 self._path = ctx._repo.wjoin(path)
613 self._ui = ctx._repo.ui
611 self._ui = ctx._repo.ui
614
612
615 def _gitcommand(self, commands, env=None, stream=False):
613 def _gitcommand(self, commands, env=None, stream=False):
616 return self._gitdir(commands, env=env, stream=stream)[0]
614 return self._gitdir(commands, env=env, stream=stream)[0]
617
615
618 def _gitdir(self, commands, env=None, stream=False):
616 def _gitdir(self, commands, env=None, stream=False):
619 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
617 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
620
618
621 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
619 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
622 """Calls the git command
620 """Calls the git command
623
621
624 The methods tries to call the git command. versions previor to 1.6.0
622 The methods tries to call the git command. versions previor to 1.6.0
625 are not supported and very probably fail.
623 are not supported and very probably fail.
626 """
624 """
627 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
625 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
628 # unless ui.quiet is set, print git's stderr,
626 # unless ui.quiet is set, print git's stderr,
629 # which is mostly progress and useful info
627 # which is mostly progress and useful info
630 errpipe = None
628 errpipe = None
631 if self._ui.quiet:
629 if self._ui.quiet:
632 errpipe = open(os.devnull, 'w')
630 errpipe = open(os.devnull, 'w')
633 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
631 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
634 close_fds=util.closefds,
632 close_fds=util.closefds,
635 stdout=subprocess.PIPE, stderr=errpipe)
633 stdout=subprocess.PIPE, stderr=errpipe)
636 if stream:
634 if stream:
637 return p.stdout, None
635 return p.stdout, None
638
636
639 retdata = p.stdout.read().strip()
637 retdata = p.stdout.read().strip()
640 # wait for the child to exit to avoid race condition.
638 # wait for the child to exit to avoid race condition.
641 p.wait()
639 p.wait()
642
640
643 if p.returncode != 0 and p.returncode != 1:
641 if p.returncode != 0 and p.returncode != 1:
644 # there are certain error codes that are ok
642 # there are certain error codes that are ok
645 command = commands[0]
643 command = commands[0]
646 if command == 'cat-file':
644 if command == 'cat-file':
647 return retdata, p.returncode
645 return retdata, p.returncode
648 # for all others, abort
646 # for all others, abort
649 raise util.Abort('git %s error %d in %s' %
647 raise util.Abort('git %s error %d in %s' %
650 (command, p.returncode, self._relpath))
648 (command, p.returncode, self._relpath))
651
649
652 return retdata, p.returncode
650 return retdata, p.returncode
653
651
654 def _gitstate(self):
652 def _gitstate(self):
655 return self._gitcommand(['rev-parse', 'HEAD'])
653 return self._gitcommand(['rev-parse', 'HEAD'])
656
654
657 def _githavelocally(self, revision):
655 def _githavelocally(self, revision):
658 out, code = self._gitdir(['cat-file', '-e', revision])
656 out, code = self._gitdir(['cat-file', '-e', revision])
659 return code == 0
657 return code == 0
660
658
661 def _gitisancestor(self, r1, r2):
659 def _gitisancestor(self, r1, r2):
662 base = self._gitcommand(['merge-base', r1, r2])
660 base = self._gitcommand(['merge-base', r1, r2])
663 return base == r1
661 return base == r1
664
662
665 def _gitbranchmap(self):
663 def _gitbranchmap(self):
666 '''returns 3 things:
664 '''returns 3 things:
667 the current branch,
665 the current branch,
668 a map from git branch to revision
666 a map from git branch to revision
669 a map from revision to branches'''
667 a map from revision to branches'''
670 branch2rev = {}
668 branch2rev = {}
671 rev2branch = {}
669 rev2branch = {}
672 current = None
670 current = None
673 out = self._gitcommand(['branch', '-a', '--no-color',
671 out = self._gitcommand(['branch', '-a', '--no-color',
674 '--verbose', '--no-abbrev'])
672 '--verbose', '--no-abbrev'])
675 for line in out.split('\n'):
673 for line in out.split('\n'):
676 if line[2:].startswith('(no branch)'):
674 if line[2:].startswith('(no branch)'):
677 continue
675 continue
678 branch, revision = line[2:].split()[:2]
676 branch, revision = line[2:].split()[:2]
679 if revision == '->' or branch.endswith('/HEAD'):
677 if revision == '->' or branch.endswith('/HEAD'):
680 continue # ignore remote/HEAD redirects
678 continue # ignore remote/HEAD redirects
681 if '/' in branch and not branch.startswith('remotes/'):
679 if '/' in branch and not branch.startswith('remotes/'):
682 # old git compatability
680 # old git compatability
683 branch = 'remotes/' + branch
681 branch = 'remotes/' + branch
684 if line[0] == '*':
682 if line[0] == '*':
685 current = branch
683 current = branch
686 branch2rev[branch] = revision
684 branch2rev[branch] = revision
687 rev2branch.setdefault(revision, []).append(branch)
685 rev2branch.setdefault(revision, []).append(branch)
688 return current, branch2rev, rev2branch
686 return current, branch2rev, rev2branch
689
687
690 def _gittracking(self, branches):
688 def _gittracking(self, branches):
691 'return map of remote branch to local tracking branch'
689 'return map of remote branch to local tracking branch'
692 # assumes no more than one local tracking branch for each remote
690 # assumes no more than one local tracking branch for each remote
693 tracking = {}
691 tracking = {}
694 for b in branches:
692 for b in branches:
695 if b.startswith('remotes/'):
693 if b.startswith('remotes/'):
696 continue
694 continue
697 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
695 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
698 if remote:
696 if remote:
699 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
697 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
700 tracking['remotes/%s/%s' % (remote, ref.split('/')[-1])] = b
698 tracking['remotes/%s/%s' % (remote, ref.split('/')[-1])] = b
701 return tracking
699 return tracking
702
700
703 def _fetch(self, source, revision):
701 def _fetch(self, source, revision):
704 if not os.path.exists('%s/.git' % self._path):
702 if not os.path.exists('%s/.git' % self._path):
705 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
703 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
706 self._gitnodir(['clone', source, self._path])
704 self._gitnodir(['clone', source, self._path])
707 if self._githavelocally(revision):
705 if self._githavelocally(revision):
708 return
706 return
709 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
707 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
710 # first try from origin
708 # first try from origin
711 self._gitcommand(['fetch'])
709 self._gitcommand(['fetch'])
712 if self._githavelocally(revision):
710 if self._githavelocally(revision):
713 return
711 return
714 # then try from known subrepo source
712 # then try from known subrepo source
715 self._gitcommand(['fetch', source])
713 self._gitcommand(['fetch', source])
716 if not self._githavelocally(revision):
714 if not self._githavelocally(revision):
717 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
715 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
718 (revision, self._path))
716 (revision, self._path))
719
717
720 def dirty(self):
718 def dirty(self):
721 if self._state[1] != self._gitstate(): # version checked out changed?
719 if self._state[1] != self._gitstate(): # version checked out changed?
722 return True
720 return True
723 # check for staged changes or modified files; ignore untracked files
721 # check for staged changes or modified files; ignore untracked files
724 status = self._gitcommand(['status'])
722 status = self._gitcommand(['status'])
725 return ('\n# Changed but not updated:' in status or
723 return ('\n# Changed but not updated:' in status or
726 '\n# Changes to be committed:' in status)
724 '\n# Changes to be committed:' in status)
727
725
728 def get(self, state):
726 def get(self, state):
729 source, revision, kind = state
727 source, revision, kind = state
730 self._fetch(source, revision)
728 self._fetch(source, revision)
731 # if the repo was set to be bare, unbare it
729 # if the repo was set to be bare, unbare it
732 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
730 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
733 self._gitcommand(['config', 'core.bare', 'false'])
731 self._gitcommand(['config', 'core.bare', 'false'])
734 if self._gitstate() == revision:
732 if self._gitstate() == revision:
735 self._gitcommand(['reset', '--hard', 'HEAD'])
733 self._gitcommand(['reset', '--hard', 'HEAD'])
736 return
734 return
737 elif self._gitstate() == revision:
735 elif self._gitstate() == revision:
738 return
736 return
739 current, branch2rev, rev2branch = self._gitbranchmap()
737 current, branch2rev, rev2branch = self._gitbranchmap()
740
738
741 def rawcheckout():
739 def rawcheckout():
742 # no branch to checkout, check it out with no branch
740 # no branch to checkout, check it out with no branch
743 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
741 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
744 self._relpath)
742 self._relpath)
745 self._ui.warn(_('check out a git branch if you intend '
743 self._ui.warn(_('check out a git branch if you intend '
746 'to make changes\n'))
744 'to make changes\n'))
747 self._gitcommand(['checkout', '-q', revision])
745 self._gitcommand(['checkout', '-q', revision])
748
746
749 if revision not in rev2branch:
747 if revision not in rev2branch:
750 rawcheckout()
748 rawcheckout()
751 return
749 return
752 branches = rev2branch[revision]
750 branches = rev2branch[revision]
753 firstlocalbranch = None
751 firstlocalbranch = None
754 for b in branches:
752 for b in branches:
755 if b == 'master':
753 if b == 'master':
756 # master trumps all other branches
754 # master trumps all other branches
757 self._gitcommand(['checkout', 'master'])
755 self._gitcommand(['checkout', 'master'])
758 return
756 return
759 if not firstlocalbranch and not b.startswith('remotes/'):
757 if not firstlocalbranch and not b.startswith('remotes/'):
760 firstlocalbranch = b
758 firstlocalbranch = b
761 if firstlocalbranch:
759 if firstlocalbranch:
762 self._gitcommand(['checkout', firstlocalbranch])
760 self._gitcommand(['checkout', firstlocalbranch])
763 return
761 return
764
762
765 tracking = self._gittracking(branch2rev.keys())
763 tracking = self._gittracking(branch2rev.keys())
766 # choose a remote branch already tracked if possible
764 # choose a remote branch already tracked if possible
767 remote = branches[0]
765 remote = branches[0]
768 if remote not in tracking:
766 if remote not in tracking:
769 for b in branches:
767 for b in branches:
770 if b in tracking:
768 if b in tracking:
771 remote = b
769 remote = b
772 break
770 break
773
771
774 if remote not in tracking:
772 if remote not in tracking:
775 # create a new local tracking branch
773 # create a new local tracking branch
776 local = remote.split('/')[-1]
774 local = remote.split('/')[-1]
777 self._gitcommand(['checkout', '-b', local, remote])
775 self._gitcommand(['checkout', '-b', local, remote])
778 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
776 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
779 # When updating to a tracked remote branch,
777 # When updating to a tracked remote branch,
780 # if the local tracking branch is downstream of it,
778 # if the local tracking branch is downstream of it,
781 # a normal `git pull` would have performed a "fast-forward merge"
779 # a normal `git pull` would have performed a "fast-forward merge"
782 # which is equivalent to updating the local branch to the remote.
780 # which is equivalent to updating the local branch to the remote.
783 # Since we are only looking at branching at update, we need to
781 # Since we are only looking at branching at update, we need to
784 # detect this situation and perform this action lazily.
782 # detect this situation and perform this action lazily.
785 if tracking[remote] != current:
783 if tracking[remote] != current:
786 self._gitcommand(['checkout', tracking[remote]])
784 self._gitcommand(['checkout', tracking[remote]])
787 self._gitcommand(['merge', '--ff', remote])
785 self._gitcommand(['merge', '--ff', remote])
788 else:
786 else:
789 # a real merge would be required, just checkout the revision
787 # a real merge would be required, just checkout the revision
790 rawcheckout()
788 rawcheckout()
791
789
792 def commit(self, text, user, date):
790 def commit(self, text, user, date):
793 cmd = ['commit', '-a', '-m', text]
791 cmd = ['commit', '-a', '-m', text]
794 env = os.environ.copy()
792 env = os.environ.copy()
795 if user:
793 if user:
796 cmd += ['--author', user]
794 cmd += ['--author', user]
797 if date:
795 if date:
798 # git's date parser silently ignores when seconds < 1e9
796 # git's date parser silently ignores when seconds < 1e9
799 # convert to ISO8601
797 # convert to ISO8601
800 env['GIT_AUTHOR_DATE'] = util.datestr(date,
798 env['GIT_AUTHOR_DATE'] = util.datestr(date,
801 '%Y-%m-%dT%H:%M:%S %1%2')
799 '%Y-%m-%dT%H:%M:%S %1%2')
802 self._gitcommand(cmd, env=env)
800 self._gitcommand(cmd, env=env)
803 # make sure commit works otherwise HEAD might not exist under certain
801 # make sure commit works otherwise HEAD might not exist under certain
804 # circumstances
802 # circumstances
805 return self._gitstate()
803 return self._gitstate()
806
804
807 def merge(self, state):
805 def merge(self, state):
808 source, revision, kind = state
806 source, revision, kind = state
809 self._fetch(source, revision)
807 self._fetch(source, revision)
810 base = self._gitcommand(['merge-base', revision, self._state[1]])
808 base = self._gitcommand(['merge-base', revision, self._state[1]])
811 if base == revision:
809 if base == revision:
812 self.get(state) # fast forward merge
810 self.get(state) # fast forward merge
813 elif base != self._state[1]:
811 elif base != self._state[1]:
814 self._gitcommand(['merge', '--no-commit', revision])
812 self._gitcommand(['merge', '--no-commit', revision])
815
813
816 def push(self, force):
814 def push(self, force):
817 # if a branch in origin contains the revision, nothing to do
815 # if a branch in origin contains the revision, nothing to do
818 current, branch2rev, rev2branch = self._gitbranchmap()
816 current, branch2rev, rev2branch = self._gitbranchmap()
819 if self._state[1] in rev2branch:
817 if self._state[1] in rev2branch:
820 for b in rev2branch[self._state[1]]:
818 for b in rev2branch[self._state[1]]:
821 if b.startswith('remotes/origin/'):
819 if b.startswith('remotes/origin/'):
822 return True
820 return True
823 for b, revision in branch2rev.iteritems():
821 for b, revision in branch2rev.iteritems():
824 if b.startswith('remotes/origin/'):
822 if b.startswith('remotes/origin/'):
825 if self._gitisancestor(self._state[1], revision):
823 if self._gitisancestor(self._state[1], revision):
826 return True
824 return True
827 # otherwise, try to push the currently checked out branch
825 # otherwise, try to push the currently checked out branch
828 cmd = ['push']
826 cmd = ['push']
829 if force:
827 if force:
830 cmd.append('--force')
828 cmd.append('--force')
831 if current:
829 if current:
832 # determine if the current branch is even useful
830 # determine if the current branch is even useful
833 if not self._gitisancestor(self._state[1], current):
831 if not self._gitisancestor(self._state[1], current):
834 self._ui.warn(_('unrelated git branch checked out '
832 self._ui.warn(_('unrelated git branch checked out '
835 'in subrepo %s\n') % self._relpath)
833 'in subrepo %s\n') % self._relpath)
836 return False
834 return False
837 self._ui.status(_('pushing branch %s of subrepo %s\n') %
835 self._ui.status(_('pushing branch %s of subrepo %s\n') %
838 (current, self._relpath))
836 (current, self._relpath))
839 self._gitcommand(cmd + ['origin', current])
837 self._gitcommand(cmd + ['origin', current])
840 return True
838 return True
841 else:
839 else:
842 self._ui.warn(_('no branch checked out in subrepo %s\n'
840 self._ui.warn(_('no branch checked out in subrepo %s\n'
843 'cannot push revision %s') %
841 'cannot push revision %s') %
844 (self._relpath, self._state[1]))
842 (self._relpath, self._state[1]))
845 return False
843 return False
846
844
847 def remove(self):
845 def remove(self):
848 if self.dirty():
846 if self.dirty():
849 self._ui.warn(_('not removing repo %s because '
847 self._ui.warn(_('not removing repo %s because '
850 'it has changes.\n') % self._path)
848 'it has changes.\n') % self._path)
851 return
849 return
852 # we can't fully delete the repository as it may contain
850 # we can't fully delete the repository as it may contain
853 # local-only history
851 # local-only history
854 self._ui.note(_('removing subrepo %s\n') % self._path)
852 self._ui.note(_('removing subrepo %s\n') % self._path)
855 self._gitcommand(['config', 'core.bare', 'true'])
853 self._gitcommand(['config', 'core.bare', 'true'])
856 for f in os.listdir(self._path):
854 for f in os.listdir(self._path):
857 if f == '.git':
855 if f == '.git':
858 continue
856 continue
859 path = os.path.join(self._path, f)
857 path = os.path.join(self._path, f)
860 if os.path.isdir(path) and not os.path.islink(path):
858 if os.path.isdir(path) and not os.path.islink(path):
861 shutil.rmtree(path)
859 shutil.rmtree(path)
862 else:
860 else:
863 os.remove(path)
861 os.remove(path)
864
862
865 def archive(self, archiver, prefix):
863 def archive(self, archiver, prefix):
866 source, revision = self._state
864 source, revision = self._state
867 self._fetch(source, revision)
865 self._fetch(source, revision)
868
866
869 # Parse git's native archive command.
867 # Parse git's native archive command.
870 # This should be much faster than manually traversing the trees
868 # This should be much faster than manually traversing the trees
871 # and objects with many subprocess calls.
869 # and objects with many subprocess calls.
872 tarstream = self._gitcommand(['archive', revision], stream=True)
870 tarstream = self._gitcommand(['archive', revision], stream=True)
873 tar = tarfile.open(fileobj=tarstream, mode='r|')
871 tar = tarfile.open(fileobj=tarstream, mode='r|')
874 for info in tar:
872 for info in tar:
875 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
873 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
876 info.mode, info.issym(),
874 info.mode, info.issym(),
877 tar.extractfile(info).read())
875 tar.extractfile(info).read())
878
876
879 types = {
877 types = {
880 'hg': hgsubrepo,
878 'hg': hgsubrepo,
881 'svn': svnsubrepo,
879 'svn': svnsubrepo,
882 'git': gitsubrepo,
880 'git': gitsubrepo,
883 }
881 }
General Comments 0
You need to be logged in to leave comments. Login now