##// END OF EJS Templates
subrepo: backout 519ac79d680b...
Eric Eisner -
r13178:c4d857f5 default
parent child Browse files
Show More
@@ -1,889 +1,900 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, ignoreupdate=False):
239 def dirty(self, ignoreupdate=False):
240 """returns true if the dirstate of the subrepo is dirty or does not
240 """returns true if the dirstate of the subrepo is dirty or does not
241 match current stored state. If ignoreupdate is true, only check
241 match current stored state. If ignoreupdate is true, only check
242 whether the subrepo has uncommitted changes in its dirstate.
242 whether the subrepo has uncommitted changes in its dirstate.
243 """
243 """
244 raise NotImplementedError
244 raise NotImplementedError
245
245
246 def checknested(self, path):
246 def checknested(self, path):
247 """check if path is a subrepository within this repository"""
247 """check if path is a subrepository within this repository"""
248 return False
248 return False
249
249
250 def commit(self, text, user, date):
250 def commit(self, text, user, date):
251 """commit the current changes to the subrepo with the given
251 """commit the current changes to the subrepo with the given
252 log message. Use given user and date if possible. Return the
252 log message. Use given user and date if possible. Return the
253 new state of the subrepo.
253 new state of the subrepo.
254 """
254 """
255 raise NotImplementedError
255 raise NotImplementedError
256
256
257 def remove(self):
257 def remove(self):
258 """remove the subrepo
258 """remove the subrepo
259
259
260 (should verify the dirstate is not dirty first)
260 (should verify the dirstate is not dirty first)
261 """
261 """
262 raise NotImplementedError
262 raise NotImplementedError
263
263
264 def get(self, state):
264 def get(self, state):
265 """run whatever commands are needed to put the subrepo into
265 """run whatever commands are needed to put the subrepo into
266 this state
266 this state
267 """
267 """
268 raise NotImplementedError
268 raise NotImplementedError
269
269
270 def merge(self, state):
270 def merge(self, state):
271 """merge currently-saved state with the new state."""
271 """merge currently-saved state with the new state."""
272 raise NotImplementedError
272 raise NotImplementedError
273
273
274 def push(self, force):
274 def push(self, force):
275 """perform whatever action is analogous to 'hg push'
275 """perform whatever action is analogous to 'hg push'
276
276
277 This may be a no-op on some systems.
277 This may be a no-op on some systems.
278 """
278 """
279 raise NotImplementedError
279 raise NotImplementedError
280
280
281 def add(self, ui, match, dryrun, prefix):
281 def add(self, ui, match, dryrun, prefix):
282 return []
282 return []
283
283
284 def status(self, rev2, **opts):
284 def status(self, rev2, **opts):
285 return [], [], [], [], [], [], []
285 return [], [], [], [], [], [], []
286
286
287 def diff(self, diffopts, node2, match, prefix, **opts):
287 def diff(self, diffopts, node2, match, prefix, **opts):
288 pass
288 pass
289
289
290 def outgoing(self, ui, dest, opts):
290 def outgoing(self, ui, dest, opts):
291 return 1
291 return 1
292
292
293 def incoming(self, ui, source, opts):
293 def incoming(self, ui, source, opts):
294 return 1
294 return 1
295
295
296 def files(self):
296 def files(self):
297 """return filename iterator"""
297 """return filename iterator"""
298 raise NotImplementedError
298 raise NotImplementedError
299
299
300 def filedata(self, name):
300 def filedata(self, name):
301 """return file data"""
301 """return file data"""
302 raise NotImplementedError
302 raise NotImplementedError
303
303
304 def fileflags(self, name):
304 def fileflags(self, name):
305 """return file flags"""
305 """return file flags"""
306 return ''
306 return ''
307
307
308 def archive(self, ui, archiver, prefix):
308 def archive(self, ui, archiver, prefix):
309 files = self.files()
309 files = self.files()
310 total = len(files)
310 total = len(files)
311 relpath = subrelpath(self)
311 relpath = subrelpath(self)
312 ui.progress(_('archiving (%s)') % relpath, 0,
312 ui.progress(_('archiving (%s)') % relpath, 0,
313 unit=_('files'), total=total)
313 unit=_('files'), total=total)
314 for i, name in enumerate(files):
314 for i, name in enumerate(files):
315 flags = self.fileflags(name)
315 flags = self.fileflags(name)
316 mode = 'x' in flags and 0755 or 0644
316 mode = 'x' in flags and 0755 or 0644
317 symlink = 'l' in flags
317 symlink = 'l' in flags
318 archiver.addfile(os.path.join(prefix, self._path, name),
318 archiver.addfile(os.path.join(prefix, self._path, name),
319 mode, symlink, self.filedata(name))
319 mode, symlink, self.filedata(name))
320 ui.progress(_('archiving (%s)') % relpath, i + 1,
320 ui.progress(_('archiving (%s)') % relpath, i + 1,
321 unit=_('files'), total=total)
321 unit=_('files'), total=total)
322 ui.progress(_('archiving (%s)') % relpath, None)
322 ui.progress(_('archiving (%s)') % relpath, None)
323
323
324
324
325 class hgsubrepo(abstractsubrepo):
325 class hgsubrepo(abstractsubrepo):
326 def __init__(self, ctx, path, state):
326 def __init__(self, ctx, path, state):
327 self._path = path
327 self._path = path
328 self._state = state
328 self._state = state
329 r = ctx._repo
329 r = ctx._repo
330 root = r.wjoin(path)
330 root = r.wjoin(path)
331 create = False
331 create = False
332 if not os.path.exists(os.path.join(root, '.hg')):
332 if not os.path.exists(os.path.join(root, '.hg')):
333 create = True
333 create = True
334 util.makedirs(root)
334 util.makedirs(root)
335 self._repo = hg.repository(r.ui, root, create=create)
335 self._repo = hg.repository(r.ui, root, create=create)
336 self._repo._subparent = r
336 self._repo._subparent = r
337 self._repo._subsource = state[0]
337 self._repo._subsource = state[0]
338
338
339 if create:
339 if create:
340 fp = self._repo.opener("hgrc", "w", text=True)
340 fp = self._repo.opener("hgrc", "w", text=True)
341 fp.write('[paths]\n')
341 fp.write('[paths]\n')
342
342
343 def addpathconfig(key, value):
343 def addpathconfig(key, value):
344 if value:
344 if value:
345 fp.write('%s = %s\n' % (key, value))
345 fp.write('%s = %s\n' % (key, value))
346 self._repo.ui.setconfig('paths', key, value)
346 self._repo.ui.setconfig('paths', key, value)
347
347
348 defpath = _abssource(self._repo, abort=False)
348 defpath = _abssource(self._repo, abort=False)
349 defpushpath = _abssource(self._repo, True, abort=False)
349 defpushpath = _abssource(self._repo, True, abort=False)
350 addpathconfig('default', defpath)
350 addpathconfig('default', defpath)
351 if defpath != defpushpath:
351 if defpath != defpushpath:
352 addpathconfig('default-push', defpushpath)
352 addpathconfig('default-push', defpushpath)
353 fp.close()
353 fp.close()
354
354
355 def add(self, ui, match, dryrun, prefix):
355 def add(self, ui, match, dryrun, prefix):
356 return cmdutil.add(ui, self._repo, match, dryrun, True,
356 return cmdutil.add(ui, self._repo, match, dryrun, True,
357 os.path.join(prefix, self._path))
357 os.path.join(prefix, self._path))
358
358
359 def status(self, rev2, **opts):
359 def status(self, rev2, **opts):
360 try:
360 try:
361 rev1 = self._state[1]
361 rev1 = self._state[1]
362 ctx1 = self._repo[rev1]
362 ctx1 = self._repo[rev1]
363 ctx2 = self._repo[rev2]
363 ctx2 = self._repo[rev2]
364 return self._repo.status(ctx1, ctx2, **opts)
364 return self._repo.status(ctx1, ctx2, **opts)
365 except error.RepoLookupError, inst:
365 except error.RepoLookupError, inst:
366 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
366 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
367 % (inst, subrelpath(self)))
367 % (inst, subrelpath(self)))
368 return [], [], [], [], [], [], []
368 return [], [], [], [], [], [], []
369
369
370 def diff(self, diffopts, node2, match, prefix, **opts):
370 def diff(self, diffopts, node2, match, prefix, **opts):
371 try:
371 try:
372 node1 = node.bin(self._state[1])
372 node1 = node.bin(self._state[1])
373 # We currently expect node2 to come from substate and be
373 # We currently expect node2 to come from substate and be
374 # in hex format
374 # in hex format
375 if node2 is not None:
375 if node2 is not None:
376 node2 = node.bin(node2)
376 node2 = node.bin(node2)
377 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
377 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
378 node1, node2, match,
378 node1, node2, match,
379 prefix=os.path.join(prefix, self._path),
379 prefix=os.path.join(prefix, self._path),
380 listsubrepos=True, **opts)
380 listsubrepos=True, **opts)
381 except error.RepoLookupError, inst:
381 except error.RepoLookupError, inst:
382 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
382 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
383 % (inst, subrelpath(self)))
383 % (inst, subrelpath(self)))
384
384
385 def archive(self, ui, archiver, prefix):
385 def archive(self, ui, archiver, prefix):
386 abstractsubrepo.archive(self, ui, archiver, prefix)
386 abstractsubrepo.archive(self, ui, archiver, prefix)
387
387
388 rev = self._state[1]
388 rev = self._state[1]
389 ctx = self._repo[rev]
389 ctx = self._repo[rev]
390 for subpath in ctx.substate:
390 for subpath in ctx.substate:
391 s = subrepo(ctx, subpath)
391 s = subrepo(ctx, subpath)
392 s.archive(ui, archiver, os.path.join(prefix, self._path))
392 s.archive(ui, archiver, os.path.join(prefix, self._path))
393
393
394 def dirty(self, ignoreupdate=False):
394 def dirty(self, ignoreupdate=False):
395 r = self._state[1]
395 r = self._state[1]
396 if r == '' and not ignoreupdate: # no state recorded
396 if r == '' and not ignoreupdate: # no state recorded
397 return True
397 return True
398 w = self._repo[None]
398 w = self._repo[None]
399 # version checked out changed?
399 # version checked out changed?
400 if w.p1() != self._repo[r] and not ignoreupdate:
400 if w.p1() != self._repo[r] and not ignoreupdate:
401 return True
401 return True
402 return w.dirty() # working directory changed
402 return w.dirty() # working directory changed
403
403
404 def checknested(self, path):
404 def checknested(self, path):
405 return self._repo._checknested(self._repo.wjoin(path))
405 return self._repo._checknested(self._repo.wjoin(path))
406
406
407 def commit(self, text, user, date):
407 def commit(self, text, user, date):
408 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
408 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
409 n = self._repo.commit(text, user, date)
409 n = self._repo.commit(text, user, date)
410 if not n:
410 if not n:
411 return self._repo['.'].hex() # different version checked out
411 return self._repo['.'].hex() # different version checked out
412 return node.hex(n)
412 return node.hex(n)
413
413
414 def remove(self):
414 def remove(self):
415 # we can't fully delete the repository as it may contain
415 # we can't fully delete the repository as it may contain
416 # local-only history
416 # local-only history
417 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
417 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
418 hg.clean(self._repo, node.nullid, False)
418 hg.clean(self._repo, node.nullid, False)
419
419
420 def _get(self, state):
420 def _get(self, state):
421 source, revision, kind = state
421 source, revision, kind = state
422 try:
422 try:
423 self._repo.lookup(revision)
423 self._repo.lookup(revision)
424 except error.RepoError:
424 except error.RepoError:
425 self._repo._subsource = source
425 self._repo._subsource = source
426 srcurl = _abssource(self._repo)
426 srcurl = _abssource(self._repo)
427 self._repo.ui.status(_('pulling subrepo %s from %s\n')
427 self._repo.ui.status(_('pulling subrepo %s from %s\n')
428 % (subrelpath(self), srcurl))
428 % (subrelpath(self), srcurl))
429 other = hg.repository(self._repo.ui, srcurl)
429 other = hg.repository(self._repo.ui, srcurl)
430 self._repo.pull(other)
430 self._repo.pull(other)
431
431
432 def get(self, state):
432 def get(self, state):
433 self._get(state)
433 self._get(state)
434 source, revision, kind = state
434 source, revision, kind = state
435 self._repo.ui.debug("getting subrepo %s\n" % self._path)
435 self._repo.ui.debug("getting subrepo %s\n" % self._path)
436 hg.clean(self._repo, revision, False)
436 hg.clean(self._repo, revision, False)
437
437
438 def merge(self, state):
438 def merge(self, state):
439 self._get(state)
439 self._get(state)
440 cur = self._repo['.']
440 cur = self._repo['.']
441 dst = self._repo[state[1]]
441 dst = self._repo[state[1]]
442 anc = dst.ancestor(cur)
442 anc = dst.ancestor(cur)
443 if anc == cur:
443 if anc == cur:
444 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
444 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
445 hg.update(self._repo, state[1])
445 hg.update(self._repo, state[1])
446 elif anc == dst:
446 elif anc == dst:
447 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
447 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
448 else:
448 else:
449 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
449 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
450 hg.merge(self._repo, state[1], remind=False)
450 hg.merge(self._repo, state[1], remind=False)
451
451
452 def push(self, force):
452 def push(self, force):
453 # push subrepos depth-first for coherent ordering
453 # push subrepos depth-first for coherent ordering
454 c = self._repo['']
454 c = self._repo['']
455 subs = c.substate # only repos that are committed
455 subs = c.substate # only repos that are committed
456 for s in sorted(subs):
456 for s in sorted(subs):
457 if not c.sub(s).push(force):
457 if not c.sub(s).push(force):
458 return False
458 return False
459
459
460 dsturl = _abssource(self._repo, True)
460 dsturl = _abssource(self._repo, True)
461 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
461 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
462 (subrelpath(self), dsturl))
462 (subrelpath(self), dsturl))
463 other = hg.repository(self._repo.ui, dsturl)
463 other = hg.repository(self._repo.ui, dsturl)
464 return self._repo.push(other, force)
464 return self._repo.push(other, force)
465
465
466 def outgoing(self, ui, dest, opts):
466 def outgoing(self, ui, dest, opts):
467 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
467 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
468
468
469 def incoming(self, ui, source, opts):
469 def incoming(self, ui, source, opts):
470 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
470 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
471
471
472 def files(self):
472 def files(self):
473 rev = self._state[1]
473 rev = self._state[1]
474 ctx = self._repo[rev]
474 ctx = self._repo[rev]
475 return ctx.manifest()
475 return ctx.manifest()
476
476
477 def filedata(self, name):
477 def filedata(self, name):
478 rev = self._state[1]
478 rev = self._state[1]
479 return self._repo[rev][name].data()
479 return self._repo[rev][name].data()
480
480
481 def fileflags(self, name):
481 def fileflags(self, name):
482 rev = self._state[1]
482 rev = self._state[1]
483 ctx = self._repo[rev]
483 ctx = self._repo[rev]
484 return ctx.flags(name)
484 return ctx.flags(name)
485
485
486
486
487 class svnsubrepo(abstractsubrepo):
487 class svnsubrepo(abstractsubrepo):
488 def __init__(self, ctx, path, state):
488 def __init__(self, ctx, path, state):
489 self._path = path
489 self._path = path
490 self._state = state
490 self._state = state
491 self._ctx = ctx
491 self._ctx = ctx
492 self._ui = ctx._repo.ui
492 self._ui = ctx._repo.ui
493
493
494 def _svncommand(self, commands, filename=''):
494 def _svncommand(self, commands, filename=''):
495 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
495 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
496 cmd = ['svn'] + commands + [path]
496 cmd = ['svn'] + commands + [path]
497 env = dict(os.environ)
497 env = dict(os.environ)
498 # Avoid localized output, preserve current locale for everything else.
498 # Avoid localized output, preserve current locale for everything else.
499 env['LC_MESSAGES'] = 'C'
499 env['LC_MESSAGES'] = 'C'
500 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
500 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
501 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
501 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
502 universal_newlines=True, env=env)
502 universal_newlines=True, env=env)
503 stdout, stderr = p.communicate()
503 stdout, stderr = p.communicate()
504 stderr = stderr.strip()
504 stderr = stderr.strip()
505 if stderr:
505 if stderr:
506 raise util.Abort(stderr)
506 raise util.Abort(stderr)
507 return stdout
507 return stdout
508
508
509 def _wcrev(self):
509 def _wcrev(self):
510 output = self._svncommand(['info', '--xml'])
510 output = self._svncommand(['info', '--xml'])
511 doc = xml.dom.minidom.parseString(output)
511 doc = xml.dom.minidom.parseString(output)
512 entries = doc.getElementsByTagName('entry')
512 entries = doc.getElementsByTagName('entry')
513 if not entries:
513 if not entries:
514 return '0'
514 return '0'
515 return str(entries[0].getAttribute('revision')) or '0'
515 return str(entries[0].getAttribute('revision')) or '0'
516
516
517 def _wcchanged(self):
517 def _wcchanged(self):
518 """Return (changes, extchanges) where changes is True
518 """Return (changes, extchanges) where changes is True
519 if the working directory was changed, and extchanges is
519 if the working directory was changed, and extchanges is
520 True if any of these changes concern an external entry.
520 True if any of these changes concern an external entry.
521 """
521 """
522 output = self._svncommand(['status', '--xml'])
522 output = self._svncommand(['status', '--xml'])
523 externals, changes = [], []
523 externals, changes = [], []
524 doc = xml.dom.minidom.parseString(output)
524 doc = xml.dom.minidom.parseString(output)
525 for e in doc.getElementsByTagName('entry'):
525 for e in doc.getElementsByTagName('entry'):
526 s = e.getElementsByTagName('wc-status')
526 s = e.getElementsByTagName('wc-status')
527 if not s:
527 if not s:
528 continue
528 continue
529 item = s[0].getAttribute('item')
529 item = s[0].getAttribute('item')
530 props = s[0].getAttribute('props')
530 props = s[0].getAttribute('props')
531 path = e.getAttribute('path')
531 path = e.getAttribute('path')
532 if item == 'external':
532 if item == 'external':
533 externals.append(path)
533 externals.append(path)
534 if (item not in ('', 'normal', 'unversioned', 'external')
534 if (item not in ('', 'normal', 'unversioned', 'external')
535 or props not in ('', 'none')):
535 or props not in ('', 'none')):
536 changes.append(path)
536 changes.append(path)
537 for path in changes:
537 for path in changes:
538 for ext in externals:
538 for ext in externals:
539 if path == ext or path.startswith(ext + os.sep):
539 if path == ext or path.startswith(ext + os.sep):
540 return True, True
540 return True, True
541 return bool(changes), False
541 return bool(changes), False
542
542
543 def dirty(self, ignoreupdate=False):
543 def dirty(self, ignoreupdate=False):
544 if not self._wcchanged()[0]:
544 if not self._wcchanged()[0]:
545 if self._wcrev() == self._state[1] and not ignoreupdate:
545 if self._wcrev() == self._state[1] and not ignoreupdate:
546 return False
546 return False
547 return True
547 return True
548
548
549 def commit(self, text, user, date):
549 def commit(self, text, user, date):
550 # user and date are out of our hands since svn is centralized
550 # user and date are out of our hands since svn is centralized
551 changed, extchanged = self._wcchanged()
551 changed, extchanged = self._wcchanged()
552 if not changed:
552 if not changed:
553 return self._wcrev()
553 return self._wcrev()
554 if extchanged:
554 if extchanged:
555 # Do not try to commit externals
555 # Do not try to commit externals
556 raise util.Abort(_('cannot commit svn externals'))
556 raise util.Abort(_('cannot commit svn externals'))
557 commitinfo = self._svncommand(['commit', '-m', text])
557 commitinfo = self._svncommand(['commit', '-m', text])
558 self._ui.status(commitinfo)
558 self._ui.status(commitinfo)
559 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
559 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
560 if not newrev:
560 if not newrev:
561 raise util.Abort(commitinfo.splitlines()[-1])
561 raise util.Abort(commitinfo.splitlines()[-1])
562 newrev = newrev.groups()[0]
562 newrev = newrev.groups()[0]
563 self._ui.status(self._svncommand(['update', '-r', newrev]))
563 self._ui.status(self._svncommand(['update', '-r', newrev]))
564 return newrev
564 return newrev
565
565
566 def remove(self):
566 def remove(self):
567 if self.dirty():
567 if self.dirty():
568 self._ui.warn(_('not removing repo %s because '
568 self._ui.warn(_('not removing repo %s because '
569 'it has changes.\n' % self._path))
569 'it has changes.\n' % self._path))
570 return
570 return
571 self._ui.note(_('removing subrepo %s\n') % self._path)
571 self._ui.note(_('removing subrepo %s\n') % self._path)
572
572
573 def onerror(function, path, excinfo):
573 def onerror(function, path, excinfo):
574 if function is not os.remove:
574 if function is not os.remove:
575 raise
575 raise
576 # read-only files cannot be unlinked under Windows
576 # read-only files cannot be unlinked under Windows
577 s = os.stat(path)
577 s = os.stat(path)
578 if (s.st_mode & stat.S_IWRITE) != 0:
578 if (s.st_mode & stat.S_IWRITE) != 0:
579 raise
579 raise
580 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
580 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
581 os.remove(path)
581 os.remove(path)
582
582
583 path = self._ctx._repo.wjoin(self._path)
583 path = self._ctx._repo.wjoin(self._path)
584 shutil.rmtree(path, onerror=onerror)
584 shutil.rmtree(path, onerror=onerror)
585 try:
585 try:
586 os.removedirs(os.path.dirname(path))
586 os.removedirs(os.path.dirname(path))
587 except OSError:
587 except OSError:
588 pass
588 pass
589
589
590 def get(self, state):
590 def get(self, state):
591 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
591 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
592 if not re.search('Checked out revision [0-9]+.', status):
592 if not re.search('Checked out revision [0-9]+.', status):
593 raise util.Abort(status.splitlines()[-1])
593 raise util.Abort(status.splitlines()[-1])
594 self._ui.status(status)
594 self._ui.status(status)
595
595
596 def merge(self, state):
596 def merge(self, state):
597 old = int(self._state[1])
597 old = int(self._state[1])
598 new = int(state[1])
598 new = int(state[1])
599 if new > old:
599 if new > old:
600 self.get(state)
600 self.get(state)
601
601
602 def push(self, force):
602 def push(self, force):
603 # push is a no-op for SVN
603 # push is a no-op for SVN
604 return True
604 return True
605
605
606 def files(self):
606 def files(self):
607 output = self._svncommand(['list'])
607 output = self._svncommand(['list'])
608 # This works because svn forbids \n in filenames.
608 # This works because svn forbids \n in filenames.
609 return output.splitlines()
609 return output.splitlines()
610
610
611 def filedata(self, name):
611 def filedata(self, name):
612 return self._svncommand(['cat'], name)
612 return self._svncommand(['cat'], name)
613
613
614
614
615 class gitsubrepo(abstractsubrepo):
615 class gitsubrepo(abstractsubrepo):
616 def __init__(self, ctx, path, state):
616 def __init__(self, ctx, path, state):
617 # TODO add git version check.
617 # TODO add git version check.
618 self._state = state
618 self._state = state
619 self._ctx = ctx
619 self._ctx = ctx
620 self._relpath = path
620 self._relpath = path
621 self._path = ctx._repo.wjoin(path)
621 self._path = ctx._repo.wjoin(path)
622 self._ui = ctx._repo.ui
622 self._ui = ctx._repo.ui
623
623
624 def _gitcommand(self, commands, env=None, stream=False):
624 def _gitcommand(self, commands, env=None, stream=False):
625 return self._gitdir(commands, env=env, stream=stream)[0]
625 return self._gitdir(commands, env=env, stream=stream)[0]
626
626
627 def _gitdir(self, commands, env=None, stream=False):
627 def _gitdir(self, commands, env=None, stream=False):
628 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
628 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
629
629
630 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
630 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
631 """Calls the git command
631 """Calls the git command
632
632
633 The methods tries to call the git command. versions previor to 1.6.0
633 The methods tries to call the git command. versions previor to 1.6.0
634 are not supported and very probably fail.
634 are not supported and very probably fail.
635 """
635 """
636 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
636 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
637 # unless ui.quiet is set, print git's stderr,
637 # unless ui.quiet is set, print git's stderr,
638 # which is mostly progress and useful info
638 # which is mostly progress and useful info
639 errpipe = None
639 errpipe = None
640 if self._ui.quiet:
640 if self._ui.quiet:
641 errpipe = open(os.devnull, 'w')
641 errpipe = open(os.devnull, 'w')
642 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
642 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
643 close_fds=util.closefds,
643 close_fds=util.closefds,
644 stdout=subprocess.PIPE, stderr=errpipe)
644 stdout=subprocess.PIPE, stderr=errpipe)
645 if stream:
645 if stream:
646 return p.stdout, None
646 return p.stdout, None
647
647
648 retdata = p.stdout.read().strip()
648 retdata = p.stdout.read().strip()
649 # wait for the child to exit to avoid race condition.
649 # wait for the child to exit to avoid race condition.
650 p.wait()
650 p.wait()
651
651
652 if p.returncode != 0 and p.returncode != 1:
652 if p.returncode != 0 and p.returncode != 1:
653 # there are certain error codes that are ok
653 # there are certain error codes that are ok
654 command = commands[0]
654 command = commands[0]
655 if command in ('cat-file', 'symbolic-ref'):
655 if command in ('cat-file', 'symbolic-ref'):
656 return retdata, p.returncode
656 return retdata, p.returncode
657 # for all others, abort
657 # for all others, abort
658 raise util.Abort('git %s error %d in %s' %
658 raise util.Abort('git %s error %d in %s' %
659 (command, p.returncode, self._relpath))
659 (command, p.returncode, self._relpath))
660
660
661 return retdata, p.returncode
661 return retdata, p.returncode
662
662
663 def _gitstate(self):
663 def _gitstate(self):
664 return self._gitcommand(['rev-parse', 'HEAD'])
664 return self._gitcommand(['rev-parse', 'HEAD'])
665
665
666 def _gitcurrentbranch(self):
666 def _gitcurrentbranch(self):
667 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
667 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
668 if err:
668 if err:
669 current = None
669 current = None
670 return current
670 return current
671
671
672 def _githavelocally(self, revision):
672 def _githavelocally(self, revision):
673 out, code = self._gitdir(['cat-file', '-e', revision])
673 out, code = self._gitdir(['cat-file', '-e', revision])
674 return code == 0
674 return code == 0
675
675
676 def _gitisancestor(self, r1, r2):
676 def _gitisancestor(self, r1, r2):
677 base = self._gitcommand(['merge-base', r1, r2])
677 base = self._gitcommand(['merge-base', r1, r2])
678 return base == r1
678 return base == r1
679
679
680 def _gitbranchmap(self):
680 def _gitbranchmap(self):
681 '''returns 3 things:
681 '''returns 2 things:
682 a map from git branch to revision
682 a map from git branch to revision
683 a map from revision to branches
683 a map from revision to branches'''
684 a map from remote branch to local tracking branch'''
685 branch2rev = {}
684 branch2rev = {}
686 rev2branch = {}
685 rev2branch = {}
687 tracking = {}
686
688 out = self._gitcommand(['for-each-ref', '--format',
687 out = self._gitcommand(['for-each-ref', '--format',
689 '%(objectname) %(refname) %(upstream) end'])
688 '%(objectname) %(refname)'])
690 for line in out.split('\n'):
689 for line in out.split('\n'):
691 revision, ref, upstream = line.split(' ')[:3]
690 revision, ref = line.split(' ')
692 if ref.startswith('refs/tags/'):
691 if ref.startswith('refs/tags/'):
693 continue
692 continue
694 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
693 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
695 continue # ignore remote/HEAD redirects
694 continue # ignore remote/HEAD redirects
696 branch2rev[ref] = revision
695 branch2rev[ref] = revision
697 rev2branch.setdefault(revision, []).append(ref)
696 rev2branch.setdefault(revision, []).append(ref)
698 if upstream:
697 return branch2rev, rev2branch
699 # assumes no more than one local tracking branch for a remote
698
700 tracking[upstream] = ref
699 def _gittracking(self, branches):
701 return branch2rev, rev2branch, tracking
700 'return map of remote branch to local tracking branch'
701 # assumes no more than one local tracking branch for each remote
702 tracking = {}
703 for b in branches:
704 if b.startswith('refs/remotes/'):
705 continue
706 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
707 if remote:
708 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
709 tracking['refs/remotes/%s/%s' %
710 (remote, ref.split('/', 2)[2])] = b
711 return tracking
702
712
703 def _fetch(self, source, revision):
713 def _fetch(self, source, revision):
704 if not os.path.exists('%s/.git' % self._path):
714 if not os.path.exists('%s/.git' % self._path):
705 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
715 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
706 self._gitnodir(['clone', source, self._path])
716 self._gitnodir(['clone', source, self._path])
707 if self._githavelocally(revision):
717 if self._githavelocally(revision):
708 return
718 return
709 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
719 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
710 # first try from origin
720 # first try from origin
711 self._gitcommand(['fetch'])
721 self._gitcommand(['fetch'])
712 if self._githavelocally(revision):
722 if self._githavelocally(revision):
713 return
723 return
714 # then try from known subrepo source
724 # then try from known subrepo source
715 self._gitcommand(['fetch', source])
725 self._gitcommand(['fetch', source])
716 if not self._githavelocally(revision):
726 if not self._githavelocally(revision):
717 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
727 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
718 (revision, self._path))
728 (revision, self._path))
719
729
720 def dirty(self):
730 def dirty(self):
721 if self._state[1] != self._gitstate(): # version checked out changed?
731 if self._state[1] != self._gitstate(): # version checked out changed?
722 return True
732 return True
723 # check for staged changes or modified files; ignore untracked files
733 # check for staged changes or modified files; ignore untracked files
724 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
734 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
725 return code == 1
735 return code == 1
726
736
727 def get(self, state):
737 def get(self, state):
728 source, revision, kind = state
738 source, revision, kind = state
729 self._fetch(source, revision)
739 self._fetch(source, revision)
730 # if the repo was set to be bare, unbare it
740 # if the repo was set to be bare, unbare it
731 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
741 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
732 self._gitcommand(['config', 'core.bare', 'false'])
742 self._gitcommand(['config', 'core.bare', 'false'])
733 if self._gitstate() == revision:
743 if self._gitstate() == revision:
734 self._gitcommand(['reset', '--hard', 'HEAD'])
744 self._gitcommand(['reset', '--hard', 'HEAD'])
735 return
745 return
736 elif self._gitstate() == revision:
746 elif self._gitstate() == revision:
737 return
747 return
738 branch2rev, rev2branch, tracking = self._gitbranchmap()
748 branch2rev, rev2branch = self._gitbranchmap()
739
749
740 def rawcheckout():
750 def rawcheckout():
741 # no branch to checkout, check it out with no branch
751 # no branch to checkout, check it out with no branch
742 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
752 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
743 self._relpath)
753 self._relpath)
744 self._ui.warn(_('check out a git branch if you intend '
754 self._ui.warn(_('check out a git branch if you intend '
745 'to make changes\n'))
755 'to make changes\n'))
746 self._gitcommand(['checkout', '-q', revision])
756 self._gitcommand(['checkout', '-q', revision])
747
757
748 if revision not in rev2branch:
758 if revision not in rev2branch:
749 rawcheckout()
759 rawcheckout()
750 return
760 return
751 branches = rev2branch[revision]
761 branches = rev2branch[revision]
752 firstlocalbranch = None
762 firstlocalbranch = None
753 for b in branches:
763 for b in branches:
754 if b == 'refs/heads/master':
764 if b == 'refs/heads/master':
755 # master trumps all other branches
765 # master trumps all other branches
756 self._gitcommand(['checkout', 'refs/heads/master'])
766 self._gitcommand(['checkout', 'refs/heads/master'])
757 return
767 return
758 if not firstlocalbranch and not b.startswith('refs/remotes/'):
768 if not firstlocalbranch and not b.startswith('refs/remotes/'):
759 firstlocalbranch = b
769 firstlocalbranch = b
760 if firstlocalbranch:
770 if firstlocalbranch:
761 self._gitcommand(['checkout', firstlocalbranch])
771 self._gitcommand(['checkout', firstlocalbranch])
762 return
772 return
763
773
774 tracking = self._gittracking(branch2rev.keys())
764 # choose a remote branch already tracked if possible
775 # choose a remote branch already tracked if possible
765 remote = branches[0]
776 remote = branches[0]
766 if remote not in tracking:
777 if remote not in tracking:
767 for b in branches:
778 for b in branches:
768 if b in tracking:
779 if b in tracking:
769 remote = b
780 remote = b
770 break
781 break
771
782
772 if remote not in tracking:
783 if remote not in tracking:
773 # create a new local tracking branch
784 # create a new local tracking branch
774 local = remote.split('/', 2)[2]
785 local = remote.split('/', 2)[2]
775 self._gitcommand(['checkout', '-b', local, remote])
786 self._gitcommand(['checkout', '-b', local, remote])
776 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
787 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
777 # When updating to a tracked remote branch,
788 # When updating to a tracked remote branch,
778 # if the local tracking branch is downstream of it,
789 # if the local tracking branch is downstream of it,
779 # a normal `git pull` would have performed a "fast-forward merge"
790 # a normal `git pull` would have performed a "fast-forward merge"
780 # which is equivalent to updating the local branch to the remote.
791 # which is equivalent to updating the local branch to the remote.
781 # Since we are only looking at branching at update, we need to
792 # Since we are only looking at branching at update, we need to
782 # detect this situation and perform this action lazily.
793 # detect this situation and perform this action lazily.
783 if tracking[remote] != self._gitcurrentbranch():
794 if tracking[remote] != self._gitcurrentbranch():
784 self._gitcommand(['checkout', tracking[remote]])
795 self._gitcommand(['checkout', tracking[remote]])
785 self._gitcommand(['merge', '--ff', remote])
796 self._gitcommand(['merge', '--ff', remote])
786 else:
797 else:
787 # a real merge would be required, just checkout the revision
798 # a real merge would be required, just checkout the revision
788 rawcheckout()
799 rawcheckout()
789
800
790 def commit(self, text, user, date):
801 def commit(self, text, user, date):
791 cmd = ['commit', '-a', '-m', text]
802 cmd = ['commit', '-a', '-m', text]
792 env = os.environ.copy()
803 env = os.environ.copy()
793 if user:
804 if user:
794 cmd += ['--author', user]
805 cmd += ['--author', user]
795 if date:
806 if date:
796 # git's date parser silently ignores when seconds < 1e9
807 # git's date parser silently ignores when seconds < 1e9
797 # convert to ISO8601
808 # convert to ISO8601
798 env['GIT_AUTHOR_DATE'] = util.datestr(date,
809 env['GIT_AUTHOR_DATE'] = util.datestr(date,
799 '%Y-%m-%dT%H:%M:%S %1%2')
810 '%Y-%m-%dT%H:%M:%S %1%2')
800 self._gitcommand(cmd, env=env)
811 self._gitcommand(cmd, env=env)
801 # make sure commit works otherwise HEAD might not exist under certain
812 # make sure commit works otherwise HEAD might not exist under certain
802 # circumstances
813 # circumstances
803 return self._gitstate()
814 return self._gitstate()
804
815
805 def merge(self, state):
816 def merge(self, state):
806 source, revision, kind = state
817 source, revision, kind = state
807 self._fetch(source, revision)
818 self._fetch(source, revision)
808 base = self._gitcommand(['merge-base', revision, self._state[1]])
819 base = self._gitcommand(['merge-base', revision, self._state[1]])
809 if base == revision:
820 if base == revision:
810 self.get(state) # fast forward merge
821 self.get(state) # fast forward merge
811 elif base != self._state[1]:
822 elif base != self._state[1]:
812 self._gitcommand(['merge', '--no-commit', revision])
823 self._gitcommand(['merge', '--no-commit', revision])
813
824
814 def push(self, force):
825 def push(self, force):
815 # if a branch in origin contains the revision, nothing to do
826 # if a branch in origin contains the revision, nothing to do
816 branch2rev, rev2branch, tracking = self._gitbranchmap()
827 branch2rev, rev2branch = self._gitbranchmap()
817 if self._state[1] in rev2branch:
828 if self._state[1] in rev2branch:
818 for b in rev2branch[self._state[1]]:
829 for b in rev2branch[self._state[1]]:
819 if b.startswith('refs/remotes/origin/'):
830 if b.startswith('refs/remotes/origin/'):
820 return True
831 return True
821 for b, revision in branch2rev.iteritems():
832 for b, revision in branch2rev.iteritems():
822 if b.startswith('refs/remotes/origin/'):
833 if b.startswith('refs/remotes/origin/'):
823 if self._gitisancestor(self._state[1], revision):
834 if self._gitisancestor(self._state[1], revision):
824 return True
835 return True
825 # otherwise, try to push the currently checked out branch
836 # otherwise, try to push the currently checked out branch
826 cmd = ['push']
837 cmd = ['push']
827 if force:
838 if force:
828 cmd.append('--force')
839 cmd.append('--force')
829
840
830 current = self._gitcurrentbranch()
841 current = self._gitcurrentbranch()
831 if current:
842 if current:
832 # determine if the current branch is even useful
843 # determine if the current branch is even useful
833 if not self._gitisancestor(self._state[1], current):
844 if not self._gitisancestor(self._state[1], current):
834 self._ui.warn(_('unrelated git branch checked out '
845 self._ui.warn(_('unrelated git branch checked out '
835 'in subrepo %s\n') % self._relpath)
846 'in subrepo %s\n') % self._relpath)
836 return False
847 return False
837 self._ui.status(_('pushing branch %s of subrepo %s\n') %
848 self._ui.status(_('pushing branch %s of subrepo %s\n') %
838 (current.split('/', 2)[2], self._relpath))
849 (current.split('/', 2)[2], self._relpath))
839 self._gitcommand(cmd + ['origin', current])
850 self._gitcommand(cmd + ['origin', current])
840 return True
851 return True
841 else:
852 else:
842 self._ui.warn(_('no branch checked out in subrepo %s\n'
853 self._ui.warn(_('no branch checked out in subrepo %s\n'
843 'cannot push revision %s') %
854 'cannot push revision %s') %
844 (self._relpath, self._state[1]))
855 (self._relpath, self._state[1]))
845 return False
856 return False
846
857
847 def remove(self):
858 def remove(self):
848 if self.dirty():
859 if self.dirty():
849 self._ui.warn(_('not removing repo %s because '
860 self._ui.warn(_('not removing repo %s because '
850 'it has changes.\n') % self._path)
861 'it has changes.\n') % self._path)
851 return
862 return
852 # we can't fully delete the repository as it may contain
863 # we can't fully delete the repository as it may contain
853 # local-only history
864 # local-only history
854 self._ui.note(_('removing subrepo %s\n') % self._path)
865 self._ui.note(_('removing subrepo %s\n') % self._path)
855 self._gitcommand(['config', 'core.bare', 'true'])
866 self._gitcommand(['config', 'core.bare', 'true'])
856 for f in os.listdir(self._path):
867 for f in os.listdir(self._path):
857 if f == '.git':
868 if f == '.git':
858 continue
869 continue
859 path = os.path.join(self._path, f)
870 path = os.path.join(self._path, f)
860 if os.path.isdir(path) and not os.path.islink(path):
871 if os.path.isdir(path) and not os.path.islink(path):
861 shutil.rmtree(path)
872 shutil.rmtree(path)
862 else:
873 else:
863 os.remove(path)
874 os.remove(path)
864
875
865 def archive(self, ui, archiver, prefix):
876 def archive(self, ui, archiver, prefix):
866 source, revision = self._state
877 source, revision = self._state
867 self._fetch(source, revision)
878 self._fetch(source, revision)
868
879
869 # Parse git's native archive command.
880 # Parse git's native archive command.
870 # This should be much faster than manually traversing the trees
881 # This should be much faster than manually traversing the trees
871 # and objects with many subprocess calls.
882 # and objects with many subprocess calls.
872 tarstream = self._gitcommand(['archive', revision], stream=True)
883 tarstream = self._gitcommand(['archive', revision], stream=True)
873 tar = tarfile.open(fileobj=tarstream, mode='r|')
884 tar = tarfile.open(fileobj=tarstream, mode='r|')
874 relpath = subrelpath(self)
885 relpath = subrelpath(self)
875 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
886 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
876 for i, info in enumerate(tar):
887 for i, info in enumerate(tar):
877 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
888 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
878 info.mode, info.issym(),
889 info.mode, info.issym(),
879 tar.extractfile(info).read())
890 tar.extractfile(info).read())
880 ui.progress(_('archiving (%s)') % relpath, i + 1,
891 ui.progress(_('archiving (%s)') % relpath, i + 1,
881 unit=_('files'))
892 unit=_('files'))
882 ui.progress(_('archiving (%s)') % relpath, None)
893 ui.progress(_('archiving (%s)') % relpath, None)
883
894
884
895
885 types = {
896 types = {
886 'hg': hgsubrepo,
897 'hg': hgsubrepo,
887 'svn': svnsubrepo,
898 'svn': svnsubrepo,
888 'git': gitsubrepo,
899 'git': gitsubrepo,
889 }
900 }
General Comments 0
You need to be logged in to leave comments. Login now