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