##// END OF EJS Templates
subrepo: fix git archive parsing of directories and symfiles
Eric Eisner -
r13180:a79e0688 default
parent child Browse files
Show More
@@ -1,901 +1,906 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 2 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 branch2rev = {}
684 branch2rev = {}
685 rev2branch = {}
685 rev2branch = {}
686
686
687 out = self._gitcommand(['for-each-ref', '--format',
687 out = self._gitcommand(['for-each-ref', '--format',
688 '%(objectname) %(refname)'])
688 '%(objectname) %(refname)'])
689 for line in out.split('\n'):
689 for line in out.split('\n'):
690 revision, ref = line.split(' ')
690 revision, ref = line.split(' ')
691 if ref.startswith('refs/tags/'):
691 if ref.startswith('refs/tags/'):
692 continue
692 continue
693 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
693 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
694 continue # ignore remote/HEAD redirects
694 continue # ignore remote/HEAD redirects
695 branch2rev[ref] = revision
695 branch2rev[ref] = revision
696 rev2branch.setdefault(revision, []).append(ref)
696 rev2branch.setdefault(revision, []).append(ref)
697 return branch2rev, rev2branch
697 return branch2rev, rev2branch
698
698
699 def _gittracking(self, branches):
699 def _gittracking(self, branches):
700 'return map of remote branch to local tracking branch'
700 'return map of remote branch to local tracking branch'
701 # assumes no more than one local tracking branch for each remote
701 # assumes no more than one local tracking branch for each remote
702 tracking = {}
702 tracking = {}
703 for b in branches:
703 for b in branches:
704 if b.startswith('refs/remotes/'):
704 if b.startswith('refs/remotes/'):
705 continue
705 continue
706 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
706 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
707 if remote:
707 if remote:
708 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
708 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
709 tracking['refs/remotes/%s/%s' %
709 tracking['refs/remotes/%s/%s' %
710 (remote, ref.split('/', 2)[2])] = b
710 (remote, ref.split('/', 2)[2])] = b
711 return tracking
711 return tracking
712
712
713 def _fetch(self, source, revision):
713 def _fetch(self, source, revision):
714 if not os.path.exists('%s/.git' % self._path):
714 if not os.path.exists('%s/.git' % self._path):
715 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
715 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
716 self._gitnodir(['clone', source, self._path])
716 self._gitnodir(['clone', source, self._path])
717 if self._githavelocally(revision):
717 if self._githavelocally(revision):
718 return
718 return
719 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
719 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
720 # first try from origin
720 # first try from origin
721 self._gitcommand(['fetch'])
721 self._gitcommand(['fetch'])
722 if self._githavelocally(revision):
722 if self._githavelocally(revision):
723 return
723 return
724 # then try from known subrepo source
724 # then try from known subrepo source
725 self._gitcommand(['fetch', source])
725 self._gitcommand(['fetch', source])
726 if not self._githavelocally(revision):
726 if not self._githavelocally(revision):
727 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") %
728 (revision, self._path))
728 (revision, self._path))
729
729
730 def dirty(self, ignoreupdate=False):
730 def dirty(self, ignoreupdate=False):
731 # version checked out changed?
731 # version checked out changed?
732 if not ignoreupdate and self._state[1] != self._gitstate():
732 if not ignoreupdate and self._state[1] != self._gitstate():
733 return True
733 return True
734 # check for staged changes or modified files; ignore untracked files
734 # check for staged changes or modified files; ignore untracked files
735 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
735 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
736 return code == 1
736 return code == 1
737
737
738 def get(self, state):
738 def get(self, state):
739 source, revision, kind = state
739 source, revision, kind = state
740 self._fetch(source, revision)
740 self._fetch(source, revision)
741 # if the repo was set to be bare, unbare it
741 # if the repo was set to be bare, unbare it
742 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
742 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
743 self._gitcommand(['config', 'core.bare', 'false'])
743 self._gitcommand(['config', 'core.bare', 'false'])
744 if self._gitstate() == revision:
744 if self._gitstate() == revision:
745 self._gitcommand(['reset', '--hard', 'HEAD'])
745 self._gitcommand(['reset', '--hard', 'HEAD'])
746 return
746 return
747 elif self._gitstate() == revision:
747 elif self._gitstate() == revision:
748 return
748 return
749 branch2rev, rev2branch = self._gitbranchmap()
749 branch2rev, rev2branch = self._gitbranchmap()
750
750
751 def rawcheckout():
751 def rawcheckout():
752 # no branch to checkout, check it out with no branch
752 # no branch to checkout, check it out with no branch
753 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
753 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
754 self._relpath)
754 self._relpath)
755 self._ui.warn(_('check out a git branch if you intend '
755 self._ui.warn(_('check out a git branch if you intend '
756 'to make changes\n'))
756 'to make changes\n'))
757 self._gitcommand(['checkout', '-q', revision])
757 self._gitcommand(['checkout', '-q', revision])
758
758
759 if revision not in rev2branch:
759 if revision not in rev2branch:
760 rawcheckout()
760 rawcheckout()
761 return
761 return
762 branches = rev2branch[revision]
762 branches = rev2branch[revision]
763 firstlocalbranch = None
763 firstlocalbranch = None
764 for b in branches:
764 for b in branches:
765 if b == 'refs/heads/master':
765 if b == 'refs/heads/master':
766 # master trumps all other branches
766 # master trumps all other branches
767 self._gitcommand(['checkout', 'refs/heads/master'])
767 self._gitcommand(['checkout', 'refs/heads/master'])
768 return
768 return
769 if not firstlocalbranch and not b.startswith('refs/remotes/'):
769 if not firstlocalbranch and not b.startswith('refs/remotes/'):
770 firstlocalbranch = b
770 firstlocalbranch = b
771 if firstlocalbranch:
771 if firstlocalbranch:
772 self._gitcommand(['checkout', firstlocalbranch])
772 self._gitcommand(['checkout', firstlocalbranch])
773 return
773 return
774
774
775 tracking = self._gittracking(branch2rev.keys())
775 tracking = self._gittracking(branch2rev.keys())
776 # choose a remote branch already tracked if possible
776 # choose a remote branch already tracked if possible
777 remote = branches[0]
777 remote = branches[0]
778 if remote not in tracking:
778 if remote not in tracking:
779 for b in branches:
779 for b in branches:
780 if b in tracking:
780 if b in tracking:
781 remote = b
781 remote = b
782 break
782 break
783
783
784 if remote not in tracking:
784 if remote not in tracking:
785 # create a new local tracking branch
785 # create a new local tracking branch
786 local = remote.split('/', 2)[2]
786 local = remote.split('/', 2)[2]
787 self._gitcommand(['checkout', '-b', local, remote])
787 self._gitcommand(['checkout', '-b', local, remote])
788 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
788 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
789 # When updating to a tracked remote branch,
789 # When updating to a tracked remote branch,
790 # if the local tracking branch is downstream of it,
790 # if the local tracking branch is downstream of it,
791 # a normal `git pull` would have performed a "fast-forward merge"
791 # a normal `git pull` would have performed a "fast-forward merge"
792 # which is equivalent to updating the local branch to the remote.
792 # which is equivalent to updating the local branch to the remote.
793 # Since we are only looking at branching at update, we need to
793 # Since we are only looking at branching at update, we need to
794 # detect this situation and perform this action lazily.
794 # detect this situation and perform this action lazily.
795 if tracking[remote] != self._gitcurrentbranch():
795 if tracking[remote] != self._gitcurrentbranch():
796 self._gitcommand(['checkout', tracking[remote]])
796 self._gitcommand(['checkout', tracking[remote]])
797 self._gitcommand(['merge', '--ff', remote])
797 self._gitcommand(['merge', '--ff', remote])
798 else:
798 else:
799 # a real merge would be required, just checkout the revision
799 # a real merge would be required, just checkout the revision
800 rawcheckout()
800 rawcheckout()
801
801
802 def commit(self, text, user, date):
802 def commit(self, text, user, date):
803 cmd = ['commit', '-a', '-m', text]
803 cmd = ['commit', '-a', '-m', text]
804 env = os.environ.copy()
804 env = os.environ.copy()
805 if user:
805 if user:
806 cmd += ['--author', user]
806 cmd += ['--author', user]
807 if date:
807 if date:
808 # git's date parser silently ignores when seconds < 1e9
808 # git's date parser silently ignores when seconds < 1e9
809 # convert to ISO8601
809 # convert to ISO8601
810 env['GIT_AUTHOR_DATE'] = util.datestr(date,
810 env['GIT_AUTHOR_DATE'] = util.datestr(date,
811 '%Y-%m-%dT%H:%M:%S %1%2')
811 '%Y-%m-%dT%H:%M:%S %1%2')
812 self._gitcommand(cmd, env=env)
812 self._gitcommand(cmd, env=env)
813 # make sure commit works otherwise HEAD might not exist under certain
813 # make sure commit works otherwise HEAD might not exist under certain
814 # circumstances
814 # circumstances
815 return self._gitstate()
815 return self._gitstate()
816
816
817 def merge(self, state):
817 def merge(self, state):
818 source, revision, kind = state
818 source, revision, kind = state
819 self._fetch(source, revision)
819 self._fetch(source, revision)
820 base = self._gitcommand(['merge-base', revision, self._state[1]])
820 base = self._gitcommand(['merge-base', revision, self._state[1]])
821 if base == revision:
821 if base == revision:
822 self.get(state) # fast forward merge
822 self.get(state) # fast forward merge
823 elif base != self._state[1]:
823 elif base != self._state[1]:
824 self._gitcommand(['merge', '--no-commit', revision])
824 self._gitcommand(['merge', '--no-commit', revision])
825
825
826 def push(self, force):
826 def push(self, force):
827 # if a branch in origin contains the revision, nothing to do
827 # if a branch in origin contains the revision, nothing to do
828 branch2rev, rev2branch = self._gitbranchmap()
828 branch2rev, rev2branch = self._gitbranchmap()
829 if self._state[1] in rev2branch:
829 if self._state[1] in rev2branch:
830 for b in rev2branch[self._state[1]]:
830 for b in rev2branch[self._state[1]]:
831 if b.startswith('refs/remotes/origin/'):
831 if b.startswith('refs/remotes/origin/'):
832 return True
832 return True
833 for b, revision in branch2rev.iteritems():
833 for b, revision in branch2rev.iteritems():
834 if b.startswith('refs/remotes/origin/'):
834 if b.startswith('refs/remotes/origin/'):
835 if self._gitisancestor(self._state[1], revision):
835 if self._gitisancestor(self._state[1], revision):
836 return True
836 return True
837 # otherwise, try to push the currently checked out branch
837 # otherwise, try to push the currently checked out branch
838 cmd = ['push']
838 cmd = ['push']
839 if force:
839 if force:
840 cmd.append('--force')
840 cmd.append('--force')
841
841
842 current = self._gitcurrentbranch()
842 current = self._gitcurrentbranch()
843 if current:
843 if current:
844 # determine if the current branch is even useful
844 # determine if the current branch is even useful
845 if not self._gitisancestor(self._state[1], current):
845 if not self._gitisancestor(self._state[1], current):
846 self._ui.warn(_('unrelated git branch checked out '
846 self._ui.warn(_('unrelated git branch checked out '
847 'in subrepo %s\n') % self._relpath)
847 'in subrepo %s\n') % self._relpath)
848 return False
848 return False
849 self._ui.status(_('pushing branch %s of subrepo %s\n') %
849 self._ui.status(_('pushing branch %s of subrepo %s\n') %
850 (current.split('/', 2)[2], self._relpath))
850 (current.split('/', 2)[2], self._relpath))
851 self._gitcommand(cmd + ['origin', current])
851 self._gitcommand(cmd + ['origin', current])
852 return True
852 return True
853 else:
853 else:
854 self._ui.warn(_('no branch checked out in subrepo %s\n'
854 self._ui.warn(_('no branch checked out in subrepo %s\n'
855 'cannot push revision %s') %
855 'cannot push revision %s') %
856 (self._relpath, self._state[1]))
856 (self._relpath, self._state[1]))
857 return False
857 return False
858
858
859 def remove(self):
859 def remove(self):
860 if self.dirty():
860 if self.dirty():
861 self._ui.warn(_('not removing repo %s because '
861 self._ui.warn(_('not removing repo %s because '
862 'it has changes.\n') % self._path)
862 'it has changes.\n') % self._path)
863 return
863 return
864 # we can't fully delete the repository as it may contain
864 # we can't fully delete the repository as it may contain
865 # local-only history
865 # local-only history
866 self._ui.note(_('removing subrepo %s\n') % self._path)
866 self._ui.note(_('removing subrepo %s\n') % self._path)
867 self._gitcommand(['config', 'core.bare', 'true'])
867 self._gitcommand(['config', 'core.bare', 'true'])
868 for f in os.listdir(self._path):
868 for f in os.listdir(self._path):
869 if f == '.git':
869 if f == '.git':
870 continue
870 continue
871 path = os.path.join(self._path, f)
871 path = os.path.join(self._path, f)
872 if os.path.isdir(path) and not os.path.islink(path):
872 if os.path.isdir(path) and not os.path.islink(path):
873 shutil.rmtree(path)
873 shutil.rmtree(path)
874 else:
874 else:
875 os.remove(path)
875 os.remove(path)
876
876
877 def archive(self, ui, archiver, prefix):
877 def archive(self, ui, archiver, prefix):
878 source, revision = self._state
878 source, revision = self._state
879 self._fetch(source, revision)
879 self._fetch(source, revision)
880
880
881 # Parse git's native archive command.
881 # Parse git's native archive command.
882 # This should be much faster than manually traversing the trees
882 # This should be much faster than manually traversing the trees
883 # and objects with many subprocess calls.
883 # and objects with many subprocess calls.
884 tarstream = self._gitcommand(['archive', revision], stream=True)
884 tarstream = self._gitcommand(['archive', revision], stream=True)
885 tar = tarfile.open(fileobj=tarstream, mode='r|')
885 tar = tarfile.open(fileobj=tarstream, mode='r|')
886 relpath = subrelpath(self)
886 relpath = subrelpath(self)
887 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
887 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
888 for i, info in enumerate(tar):
888 for i, info in enumerate(tar):
889 if info.isdir():
890 continue
891 if info.issym():
892 data = info.linkname
893 else:
894 data = tar.extractfile(info).read()
889 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
895 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
890 info.mode, info.issym(),
896 info.mode, info.issym(), data)
891 tar.extractfile(info).read())
892 ui.progress(_('archiving (%s)') % relpath, i + 1,
897 ui.progress(_('archiving (%s)') % relpath, i + 1,
893 unit=_('files'))
898 unit=_('files'))
894 ui.progress(_('archiving (%s)') % relpath, None)
899 ui.progress(_('archiving (%s)') % relpath, None)
895
900
896
901
897 types = {
902 types = {
898 'hg': hgsubrepo,
903 'hg': hgsubrepo,
899 'svn': svnsubrepo,
904 'svn': svnsubrepo,
900 'git': gitsubrepo,
905 'git': gitsubrepo,
901 }
906 }
General Comments 0
You need to be logged in to leave comments. Login now