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