##// END OF EJS Templates
merge with stable
Matt Mackall -
r14047:30ccb7d0 merge default
parent child Browse files
Show More
@@ -1,1030 +1,1039 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, scmutil, util, node, error, cmdutil, url, bookmarks
11 import config, scmutil, 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 sorted(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 scmutil.path_auditor(ctx._repo.root)(path)
237 scmutil.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']
516 # Starting in svn 1.5 --non-interactive is a global flag
517 # instead of being per-command, but we need to support 1.4 so
518 # we have to be intelligent about what commands take
519 # --non-interactive.
520 if (not self._ui.interactive() and
521 commands[0] in ('update', 'checkout', 'commit')):
522 cmd.append('--non-interactive')
523 cmd.extend(commands)
524 cmd.append(path)
516 env = dict(os.environ)
525 env = dict(os.environ)
517 # Avoid localized output, preserve current locale for everything else.
526 # Avoid localized output, preserve current locale for everything else.
518 env['LC_MESSAGES'] = 'C'
527 env['LC_MESSAGES'] = 'C'
519 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
528 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
520 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
529 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
521 universal_newlines=True, env=env)
530 universal_newlines=True, env=env)
522 stdout, stderr = p.communicate()
531 stdout, stderr = p.communicate()
523 stderr = stderr.strip()
532 stderr = stderr.strip()
524 if stderr:
533 if stderr:
525 raise util.Abort(stderr)
534 raise util.Abort(stderr)
526 return stdout
535 return stdout
527
536
528 def _wcrevs(self):
537 def _wcrevs(self):
529 # Get the working directory revision as well as the last
538 # Get the working directory revision as well as the last
530 # commit revision so we can compare the subrepo state with
539 # commit revision so we can compare the subrepo state with
531 # both. We used to store the working directory one.
540 # both. We used to store the working directory one.
532 output = self._svncommand(['info', '--xml'])
541 output = self._svncommand(['info', '--xml'])
533 doc = xml.dom.minidom.parseString(output)
542 doc = xml.dom.minidom.parseString(output)
534 entries = doc.getElementsByTagName('entry')
543 entries = doc.getElementsByTagName('entry')
535 lastrev, rev = '0', '0'
544 lastrev, rev = '0', '0'
536 if entries:
545 if entries:
537 rev = str(entries[0].getAttribute('revision')) or '0'
546 rev = str(entries[0].getAttribute('revision')) or '0'
538 commits = entries[0].getElementsByTagName('commit')
547 commits = entries[0].getElementsByTagName('commit')
539 if commits:
548 if commits:
540 lastrev = str(commits[0].getAttribute('revision')) or '0'
549 lastrev = str(commits[0].getAttribute('revision')) or '0'
541 return (lastrev, rev)
550 return (lastrev, rev)
542
551
543 def _wcrev(self):
552 def _wcrev(self):
544 return self._wcrevs()[0]
553 return self._wcrevs()[0]
545
554
546 def _wcchanged(self):
555 def _wcchanged(self):
547 """Return (changes, extchanges) where changes is True
556 """Return (changes, extchanges) where changes is True
548 if the working directory was changed, and extchanges is
557 if the working directory was changed, and extchanges is
549 True if any of these changes concern an external entry.
558 True if any of these changes concern an external entry.
550 """
559 """
551 output = self._svncommand(['status', '--xml'])
560 output = self._svncommand(['status', '--xml'])
552 externals, changes = [], []
561 externals, changes = [], []
553 doc = xml.dom.minidom.parseString(output)
562 doc = xml.dom.minidom.parseString(output)
554 for e in doc.getElementsByTagName('entry'):
563 for e in doc.getElementsByTagName('entry'):
555 s = e.getElementsByTagName('wc-status')
564 s = e.getElementsByTagName('wc-status')
556 if not s:
565 if not s:
557 continue
566 continue
558 item = s[0].getAttribute('item')
567 item = s[0].getAttribute('item')
559 props = s[0].getAttribute('props')
568 props = s[0].getAttribute('props')
560 path = e.getAttribute('path')
569 path = e.getAttribute('path')
561 if item == 'external':
570 if item == 'external':
562 externals.append(path)
571 externals.append(path)
563 if (item not in ('', 'normal', 'unversioned', 'external')
572 if (item not in ('', 'normal', 'unversioned', 'external')
564 or props not in ('', 'none')):
573 or props not in ('', 'none')):
565 changes.append(path)
574 changes.append(path)
566 for path in changes:
575 for path in changes:
567 for ext in externals:
576 for ext in externals:
568 if path == ext or path.startswith(ext + os.sep):
577 if path == ext or path.startswith(ext + os.sep):
569 return True, True
578 return True, True
570 return bool(changes), False
579 return bool(changes), False
571
580
572 def dirty(self, ignoreupdate=False):
581 def dirty(self, ignoreupdate=False):
573 if not self._wcchanged()[0]:
582 if not self._wcchanged()[0]:
574 if self._state[1] in self._wcrevs() or ignoreupdate:
583 if self._state[1] in self._wcrevs() or ignoreupdate:
575 return False
584 return False
576 return True
585 return True
577
586
578 def commit(self, text, user, date):
587 def commit(self, text, user, date):
579 # user and date are out of our hands since svn is centralized
588 # user and date are out of our hands since svn is centralized
580 changed, extchanged = self._wcchanged()
589 changed, extchanged = self._wcchanged()
581 if not changed:
590 if not changed:
582 return self._wcrev()
591 return self._wcrev()
583 if extchanged:
592 if extchanged:
584 # Do not try to commit externals
593 # Do not try to commit externals
585 raise util.Abort(_('cannot commit svn externals'))
594 raise util.Abort(_('cannot commit svn externals'))
586 commitinfo = self._svncommand(['commit', '-m', text])
595 commitinfo = self._svncommand(['commit', '-m', text])
587 self._ui.status(commitinfo)
596 self._ui.status(commitinfo)
588 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
597 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
589 if not newrev:
598 if not newrev:
590 raise util.Abort(commitinfo.splitlines()[-1])
599 raise util.Abort(commitinfo.splitlines()[-1])
591 newrev = newrev.groups()[0]
600 newrev = newrev.groups()[0]
592 self._ui.status(self._svncommand(['update', '-r', newrev]))
601 self._ui.status(self._svncommand(['update', '-r', newrev]))
593 return newrev
602 return newrev
594
603
595 def remove(self):
604 def remove(self):
596 if self.dirty():
605 if self.dirty():
597 self._ui.warn(_('not removing repo %s because '
606 self._ui.warn(_('not removing repo %s because '
598 'it has changes.\n' % self._path))
607 'it has changes.\n' % self._path))
599 return
608 return
600 self._ui.note(_('removing subrepo %s\n') % self._path)
609 self._ui.note(_('removing subrepo %s\n') % self._path)
601
610
602 def onerror(function, path, excinfo):
611 def onerror(function, path, excinfo):
603 if function is not os.remove:
612 if function is not os.remove:
604 raise
613 raise
605 # read-only files cannot be unlinked under Windows
614 # read-only files cannot be unlinked under Windows
606 s = os.stat(path)
615 s = os.stat(path)
607 if (s.st_mode & stat.S_IWRITE) != 0:
616 if (s.st_mode & stat.S_IWRITE) != 0:
608 raise
617 raise
609 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
618 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
610 os.remove(path)
619 os.remove(path)
611
620
612 path = self._ctx._repo.wjoin(self._path)
621 path = self._ctx._repo.wjoin(self._path)
613 shutil.rmtree(path, onerror=onerror)
622 shutil.rmtree(path, onerror=onerror)
614 try:
623 try:
615 os.removedirs(os.path.dirname(path))
624 os.removedirs(os.path.dirname(path))
616 except OSError:
625 except OSError:
617 pass
626 pass
618
627
619 def get(self, state, overwrite=False):
628 def get(self, state, overwrite=False):
620 if overwrite:
629 if overwrite:
621 self._svncommand(['revert', '--recursive'])
630 self._svncommand(['revert', '--recursive'])
622 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
631 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
623 if not re.search('Checked out revision [0-9]+.', status):
632 if not re.search('Checked out revision [0-9]+.', status):
624 # catch the case where the checkout operation is
633 # catch the case where the checkout operation is
625 # obstructed but the working copy is clean
634 # obstructed but the working copy is clean
626 if ('already a working copy for a different' in status and
635 if ('already a working copy for a different' in status and
627 not self.dirty()):
636 not self.dirty()):
628 self.remove()
637 self.remove()
629 self.get(state, overwrite)
638 self.get(state, overwrite)
630 return
639 return
631 else:
640 else:
632 raise util.Abort(status.splitlines()[-1])
641 raise util.Abort(status.splitlines()[-1])
633 self._ui.status(status)
642 self._ui.status(status)
634
643
635 def merge(self, state):
644 def merge(self, state):
636 old = self._state[1]
645 old = self._state[1]
637 new = state[1]
646 new = state[1]
638 if new != self._wcrev():
647 if new != self._wcrev():
639 dirty = old == self._wcrev() or self._wcchanged()[0]
648 dirty = old == self._wcrev() or self._wcchanged()[0]
640 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
649 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
641 self.get(state, False)
650 self.get(state, False)
642
651
643 def push(self, force):
652 def push(self, force):
644 # push is a no-op for SVN
653 # push is a no-op for SVN
645 return True
654 return True
646
655
647 def files(self):
656 def files(self):
648 output = self._svncommand(['list'])
657 output = self._svncommand(['list'])
649 # This works because svn forbids \n in filenames.
658 # This works because svn forbids \n in filenames.
650 return output.splitlines()
659 return output.splitlines()
651
660
652 def filedata(self, name):
661 def filedata(self, name):
653 return self._svncommand(['cat'], name)
662 return self._svncommand(['cat'], name)
654
663
655
664
656 class gitsubrepo(abstractsubrepo):
665 class gitsubrepo(abstractsubrepo):
657 def __init__(self, ctx, path, state):
666 def __init__(self, ctx, path, state):
658 # TODO add git version check.
667 # TODO add git version check.
659 self._state = state
668 self._state = state
660 self._ctx = ctx
669 self._ctx = ctx
661 self._path = path
670 self._path = path
662 self._relpath = os.path.join(reporelpath(ctx._repo), path)
671 self._relpath = os.path.join(reporelpath(ctx._repo), path)
663 self._abspath = ctx._repo.wjoin(path)
672 self._abspath = ctx._repo.wjoin(path)
664 self._subparent = ctx._repo
673 self._subparent = ctx._repo
665 self._ui = ctx._repo.ui
674 self._ui = ctx._repo.ui
666
675
667 def _gitcommand(self, commands, env=None, stream=False):
676 def _gitcommand(self, commands, env=None, stream=False):
668 return self._gitdir(commands, env=env, stream=stream)[0]
677 return self._gitdir(commands, env=env, stream=stream)[0]
669
678
670 def _gitdir(self, commands, env=None, stream=False):
679 def _gitdir(self, commands, env=None, stream=False):
671 return self._gitnodir(commands, env=env, stream=stream,
680 return self._gitnodir(commands, env=env, stream=stream,
672 cwd=self._abspath)
681 cwd=self._abspath)
673
682
674 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
683 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
675 """Calls the git command
684 """Calls the git command
676
685
677 The methods tries to call the git command. versions previor to 1.6.0
686 The methods tries to call the git command. versions previor to 1.6.0
678 are not supported and very probably fail.
687 are not supported and very probably fail.
679 """
688 """
680 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
689 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
681 # unless ui.quiet is set, print git's stderr,
690 # unless ui.quiet is set, print git's stderr,
682 # which is mostly progress and useful info
691 # which is mostly progress and useful info
683 errpipe = None
692 errpipe = None
684 if self._ui.quiet:
693 if self._ui.quiet:
685 errpipe = open(os.devnull, 'w')
694 errpipe = open(os.devnull, 'w')
686 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
695 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
687 close_fds=util.closefds,
696 close_fds=util.closefds,
688 stdout=subprocess.PIPE, stderr=errpipe)
697 stdout=subprocess.PIPE, stderr=errpipe)
689 if stream:
698 if stream:
690 return p.stdout, None
699 return p.stdout, None
691
700
692 retdata = p.stdout.read().strip()
701 retdata = p.stdout.read().strip()
693 # wait for the child to exit to avoid race condition.
702 # wait for the child to exit to avoid race condition.
694 p.wait()
703 p.wait()
695
704
696 if p.returncode != 0 and p.returncode != 1:
705 if p.returncode != 0 and p.returncode != 1:
697 # there are certain error codes that are ok
706 # there are certain error codes that are ok
698 command = commands[0]
707 command = commands[0]
699 if command in ('cat-file', 'symbolic-ref'):
708 if command in ('cat-file', 'symbolic-ref'):
700 return retdata, p.returncode
709 return retdata, p.returncode
701 # for all others, abort
710 # for all others, abort
702 raise util.Abort('git %s error %d in %s' %
711 raise util.Abort('git %s error %d in %s' %
703 (command, p.returncode, self._relpath))
712 (command, p.returncode, self._relpath))
704
713
705 return retdata, p.returncode
714 return retdata, p.returncode
706
715
707 def _gitmissing(self):
716 def _gitmissing(self):
708 return not os.path.exists(os.path.join(self._abspath, '.git'))
717 return not os.path.exists(os.path.join(self._abspath, '.git'))
709
718
710 def _gitstate(self):
719 def _gitstate(self):
711 return self._gitcommand(['rev-parse', 'HEAD'])
720 return self._gitcommand(['rev-parse', 'HEAD'])
712
721
713 def _gitcurrentbranch(self):
722 def _gitcurrentbranch(self):
714 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
723 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
715 if err:
724 if err:
716 current = None
725 current = None
717 return current
726 return current
718
727
719 def _gitremote(self, remote):
728 def _gitremote(self, remote):
720 out = self._gitcommand(['remote', 'show', '-n', remote])
729 out = self._gitcommand(['remote', 'show', '-n', remote])
721 line = out.split('\n')[1]
730 line = out.split('\n')[1]
722 i = line.index('URL: ') + len('URL: ')
731 i = line.index('URL: ') + len('URL: ')
723 return line[i:]
732 return line[i:]
724
733
725 def _githavelocally(self, revision):
734 def _githavelocally(self, revision):
726 out, code = self._gitdir(['cat-file', '-e', revision])
735 out, code = self._gitdir(['cat-file', '-e', revision])
727 return code == 0
736 return code == 0
728
737
729 def _gitisancestor(self, r1, r2):
738 def _gitisancestor(self, r1, r2):
730 base = self._gitcommand(['merge-base', r1, r2])
739 base = self._gitcommand(['merge-base', r1, r2])
731 return base == r1
740 return base == r1
732
741
733 def _gitbranchmap(self):
742 def _gitbranchmap(self):
734 '''returns 2 things:
743 '''returns 2 things:
735 a map from git branch to revision
744 a map from git branch to revision
736 a map from revision to branches'''
745 a map from revision to branches'''
737 branch2rev = {}
746 branch2rev = {}
738 rev2branch = {}
747 rev2branch = {}
739
748
740 out = self._gitcommand(['for-each-ref', '--format',
749 out = self._gitcommand(['for-each-ref', '--format',
741 '%(objectname) %(refname)'])
750 '%(objectname) %(refname)'])
742 for line in out.split('\n'):
751 for line in out.split('\n'):
743 revision, ref = line.split(' ')
752 revision, ref = line.split(' ')
744 if (not ref.startswith('refs/heads/') and
753 if (not ref.startswith('refs/heads/') and
745 not ref.startswith('refs/remotes/')):
754 not ref.startswith('refs/remotes/')):
746 continue
755 continue
747 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
756 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
748 continue # ignore remote/HEAD redirects
757 continue # ignore remote/HEAD redirects
749 branch2rev[ref] = revision
758 branch2rev[ref] = revision
750 rev2branch.setdefault(revision, []).append(ref)
759 rev2branch.setdefault(revision, []).append(ref)
751 return branch2rev, rev2branch
760 return branch2rev, rev2branch
752
761
753 def _gittracking(self, branches):
762 def _gittracking(self, branches):
754 'return map of remote branch to local tracking branch'
763 'return map of remote branch to local tracking branch'
755 # assumes no more than one local tracking branch for each remote
764 # assumes no more than one local tracking branch for each remote
756 tracking = {}
765 tracking = {}
757 for b in branches:
766 for b in branches:
758 if b.startswith('refs/remotes/'):
767 if b.startswith('refs/remotes/'):
759 continue
768 continue
760 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
769 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
761 if remote:
770 if remote:
762 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
771 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
763 tracking['refs/remotes/%s/%s' %
772 tracking['refs/remotes/%s/%s' %
764 (remote, ref.split('/', 2)[2])] = b
773 (remote, ref.split('/', 2)[2])] = b
765 return tracking
774 return tracking
766
775
767 def _abssource(self, source):
776 def _abssource(self, source):
768 if '://' not in source:
777 if '://' not in source:
769 # recognize the scp syntax as an absolute source
778 # recognize the scp syntax as an absolute source
770 colon = source.find(':')
779 colon = source.find(':')
771 if colon != -1 and '/' not in source[:colon]:
780 if colon != -1 and '/' not in source[:colon]:
772 return source
781 return source
773 self._subsource = source
782 self._subsource = source
774 return _abssource(self)
783 return _abssource(self)
775
784
776 def _fetch(self, source, revision):
785 def _fetch(self, source, revision):
777 if self._gitmissing():
786 if self._gitmissing():
778 source = self._abssource(source)
787 source = self._abssource(source)
779 self._ui.status(_('cloning subrepo %s from %s\n') %
788 self._ui.status(_('cloning subrepo %s from %s\n') %
780 (self._relpath, source))
789 (self._relpath, source))
781 self._gitnodir(['clone', source, self._abspath])
790 self._gitnodir(['clone', source, self._abspath])
782 if self._githavelocally(revision):
791 if self._githavelocally(revision):
783 return
792 return
784 self._ui.status(_('pulling subrepo %s from %s\n') %
793 self._ui.status(_('pulling subrepo %s from %s\n') %
785 (self._relpath, self._gitremote('origin')))
794 (self._relpath, self._gitremote('origin')))
786 # try only origin: the originally cloned repo
795 # try only origin: the originally cloned repo
787 self._gitcommand(['fetch'])
796 self._gitcommand(['fetch'])
788 if not self._githavelocally(revision):
797 if not self._githavelocally(revision):
789 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
798 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
790 (revision, self._relpath))
799 (revision, self._relpath))
791
800
792 def dirty(self, ignoreupdate=False):
801 def dirty(self, ignoreupdate=False):
793 if self._gitmissing():
802 if self._gitmissing():
794 return True
803 return True
795 if not ignoreupdate and self._state[1] != self._gitstate():
804 if not ignoreupdate and self._state[1] != self._gitstate():
796 # different version checked out
805 # different version checked out
797 return True
806 return True
798 # check for staged changes or modified files; ignore untracked files
807 # check for staged changes or modified files; ignore untracked files
799 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
808 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
800 return code == 1
809 return code == 1
801
810
802 def get(self, state, overwrite=False):
811 def get(self, state, overwrite=False):
803 source, revision, kind = state
812 source, revision, kind = state
804 self._fetch(source, revision)
813 self._fetch(source, revision)
805 # if the repo was set to be bare, unbare it
814 # if the repo was set to be bare, unbare it
806 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
815 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
807 self._gitcommand(['config', 'core.bare', 'false'])
816 self._gitcommand(['config', 'core.bare', 'false'])
808 if self._gitstate() == revision:
817 if self._gitstate() == revision:
809 self._gitcommand(['reset', '--hard', 'HEAD'])
818 self._gitcommand(['reset', '--hard', 'HEAD'])
810 return
819 return
811 elif self._gitstate() == revision:
820 elif self._gitstate() == revision:
812 if overwrite:
821 if overwrite:
813 # first reset the index to unmark new files for commit, because
822 # first reset the index to unmark new files for commit, because
814 # reset --hard will otherwise throw away files added for commit,
823 # reset --hard will otherwise throw away files added for commit,
815 # not just unmark them.
824 # not just unmark them.
816 self._gitcommand(['reset', 'HEAD'])
825 self._gitcommand(['reset', 'HEAD'])
817 self._gitcommand(['reset', '--hard', 'HEAD'])
826 self._gitcommand(['reset', '--hard', 'HEAD'])
818 return
827 return
819 branch2rev, rev2branch = self._gitbranchmap()
828 branch2rev, rev2branch = self._gitbranchmap()
820
829
821 def checkout(args):
830 def checkout(args):
822 cmd = ['checkout']
831 cmd = ['checkout']
823 if overwrite:
832 if overwrite:
824 # first reset the index to unmark new files for commit, because
833 # first reset the index to unmark new files for commit, because
825 # the -f option will otherwise throw away files added for
834 # the -f option will otherwise throw away files added for
826 # commit, not just unmark them.
835 # commit, not just unmark them.
827 self._gitcommand(['reset', 'HEAD'])
836 self._gitcommand(['reset', 'HEAD'])
828 cmd.append('-f')
837 cmd.append('-f')
829 self._gitcommand(cmd + args)
838 self._gitcommand(cmd + args)
830
839
831 def rawcheckout():
840 def rawcheckout():
832 # no branch to checkout, check it out with no branch
841 # no branch to checkout, check it out with no branch
833 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
842 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
834 self._relpath)
843 self._relpath)
835 self._ui.warn(_('check out a git branch if you intend '
844 self._ui.warn(_('check out a git branch if you intend '
836 'to make changes\n'))
845 'to make changes\n'))
837 checkout(['-q', revision])
846 checkout(['-q', revision])
838
847
839 if revision not in rev2branch:
848 if revision not in rev2branch:
840 rawcheckout()
849 rawcheckout()
841 return
850 return
842 branches = rev2branch[revision]
851 branches = rev2branch[revision]
843 firstlocalbranch = None
852 firstlocalbranch = None
844 for b in branches:
853 for b in branches:
845 if b == 'refs/heads/master':
854 if b == 'refs/heads/master':
846 # master trumps all other branches
855 # master trumps all other branches
847 checkout(['refs/heads/master'])
856 checkout(['refs/heads/master'])
848 return
857 return
849 if not firstlocalbranch and not b.startswith('refs/remotes/'):
858 if not firstlocalbranch and not b.startswith('refs/remotes/'):
850 firstlocalbranch = b
859 firstlocalbranch = b
851 if firstlocalbranch:
860 if firstlocalbranch:
852 checkout([firstlocalbranch])
861 checkout([firstlocalbranch])
853 return
862 return
854
863
855 tracking = self._gittracking(branch2rev.keys())
864 tracking = self._gittracking(branch2rev.keys())
856 # choose a remote branch already tracked if possible
865 # choose a remote branch already tracked if possible
857 remote = branches[0]
866 remote = branches[0]
858 if remote not in tracking:
867 if remote not in tracking:
859 for b in branches:
868 for b in branches:
860 if b in tracking:
869 if b in tracking:
861 remote = b
870 remote = b
862 break
871 break
863
872
864 if remote not in tracking:
873 if remote not in tracking:
865 # create a new local tracking branch
874 # create a new local tracking branch
866 local = remote.split('/', 2)[2]
875 local = remote.split('/', 2)[2]
867 checkout(['-b', local, remote])
876 checkout(['-b', local, remote])
868 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
877 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
869 # When updating to a tracked remote branch,
878 # When updating to a tracked remote branch,
870 # if the local tracking branch is downstream of it,
879 # if the local tracking branch is downstream of it,
871 # a normal `git pull` would have performed a "fast-forward merge"
880 # a normal `git pull` would have performed a "fast-forward merge"
872 # which is equivalent to updating the local branch to the remote.
881 # which is equivalent to updating the local branch to the remote.
873 # Since we are only looking at branching at update, we need to
882 # Since we are only looking at branching at update, we need to
874 # detect this situation and perform this action lazily.
883 # detect this situation and perform this action lazily.
875 if tracking[remote] != self._gitcurrentbranch():
884 if tracking[remote] != self._gitcurrentbranch():
876 checkout([tracking[remote]])
885 checkout([tracking[remote]])
877 self._gitcommand(['merge', '--ff', remote])
886 self._gitcommand(['merge', '--ff', remote])
878 else:
887 else:
879 # a real merge would be required, just checkout the revision
888 # a real merge would be required, just checkout the revision
880 rawcheckout()
889 rawcheckout()
881
890
882 def commit(self, text, user, date):
891 def commit(self, text, user, date):
883 if self._gitmissing():
892 if self._gitmissing():
884 raise util.Abort(_("subrepo %s is missing") % self._relpath)
893 raise util.Abort(_("subrepo %s is missing") % self._relpath)
885 cmd = ['commit', '-a', '-m', text]
894 cmd = ['commit', '-a', '-m', text]
886 env = os.environ.copy()
895 env = os.environ.copy()
887 if user:
896 if user:
888 cmd += ['--author', user]
897 cmd += ['--author', user]
889 if date:
898 if date:
890 # git's date parser silently ignores when seconds < 1e9
899 # git's date parser silently ignores when seconds < 1e9
891 # convert to ISO8601
900 # convert to ISO8601
892 env['GIT_AUTHOR_DATE'] = util.datestr(date,
901 env['GIT_AUTHOR_DATE'] = util.datestr(date,
893 '%Y-%m-%dT%H:%M:%S %1%2')
902 '%Y-%m-%dT%H:%M:%S %1%2')
894 self._gitcommand(cmd, env=env)
903 self._gitcommand(cmd, env=env)
895 # make sure commit works otherwise HEAD might not exist under certain
904 # make sure commit works otherwise HEAD might not exist under certain
896 # circumstances
905 # circumstances
897 return self._gitstate()
906 return self._gitstate()
898
907
899 def merge(self, state):
908 def merge(self, state):
900 source, revision, kind = state
909 source, revision, kind = state
901 self._fetch(source, revision)
910 self._fetch(source, revision)
902 base = self._gitcommand(['merge-base', revision, self._state[1]])
911 base = self._gitcommand(['merge-base', revision, self._state[1]])
903 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
912 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
904
913
905 def mergefunc():
914 def mergefunc():
906 if base == revision:
915 if base == revision:
907 self.get(state) # fast forward merge
916 self.get(state) # fast forward merge
908 elif base != self._state[1]:
917 elif base != self._state[1]:
909 self._gitcommand(['merge', '--no-commit', revision])
918 self._gitcommand(['merge', '--no-commit', revision])
910
919
911 if self.dirty():
920 if self.dirty():
912 if self._gitstate() != revision:
921 if self._gitstate() != revision:
913 dirty = self._gitstate() == self._state[1] or code != 0
922 dirty = self._gitstate() == self._state[1] or code != 0
914 if _updateprompt(self._ui, self, dirty,
923 if _updateprompt(self._ui, self, dirty,
915 self._state[1][:7], revision[:7]):
924 self._state[1][:7], revision[:7]):
916 mergefunc()
925 mergefunc()
917 else:
926 else:
918 mergefunc()
927 mergefunc()
919
928
920 def push(self, force):
929 def push(self, force):
921 if self._gitmissing():
930 if self._gitmissing():
922 raise util.Abort(_("subrepo %s is missing") % self._relpath)
931 raise util.Abort(_("subrepo %s is missing") % self._relpath)
923 # if a branch in origin contains the revision, nothing to do
932 # if a branch in origin contains the revision, nothing to do
924 branch2rev, rev2branch = self._gitbranchmap()
933 branch2rev, rev2branch = self._gitbranchmap()
925 if self._state[1] in rev2branch:
934 if self._state[1] in rev2branch:
926 for b in rev2branch[self._state[1]]:
935 for b in rev2branch[self._state[1]]:
927 if b.startswith('refs/remotes/origin/'):
936 if b.startswith('refs/remotes/origin/'):
928 return True
937 return True
929 for b, revision in branch2rev.iteritems():
938 for b, revision in branch2rev.iteritems():
930 if b.startswith('refs/remotes/origin/'):
939 if b.startswith('refs/remotes/origin/'):
931 if self._gitisancestor(self._state[1], revision):
940 if self._gitisancestor(self._state[1], revision):
932 return True
941 return True
933 # otherwise, try to push the currently checked out branch
942 # otherwise, try to push the currently checked out branch
934 cmd = ['push']
943 cmd = ['push']
935 if force:
944 if force:
936 cmd.append('--force')
945 cmd.append('--force')
937
946
938 current = self._gitcurrentbranch()
947 current = self._gitcurrentbranch()
939 if current:
948 if current:
940 # determine if the current branch is even useful
949 # determine if the current branch is even useful
941 if not self._gitisancestor(self._state[1], current):
950 if not self._gitisancestor(self._state[1], current):
942 self._ui.warn(_('unrelated git branch checked out '
951 self._ui.warn(_('unrelated git branch checked out '
943 'in subrepo %s\n') % self._relpath)
952 'in subrepo %s\n') % self._relpath)
944 return False
953 return False
945 self._ui.status(_('pushing branch %s of subrepo %s\n') %
954 self._ui.status(_('pushing branch %s of subrepo %s\n') %
946 (current.split('/', 2)[2], self._relpath))
955 (current.split('/', 2)[2], self._relpath))
947 self._gitcommand(cmd + ['origin', current])
956 self._gitcommand(cmd + ['origin', current])
948 return True
957 return True
949 else:
958 else:
950 self._ui.warn(_('no branch checked out in subrepo %s\n'
959 self._ui.warn(_('no branch checked out in subrepo %s\n'
951 'cannot push revision %s') %
960 'cannot push revision %s') %
952 (self._relpath, self._state[1]))
961 (self._relpath, self._state[1]))
953 return False
962 return False
954
963
955 def remove(self):
964 def remove(self):
956 if self._gitmissing():
965 if self._gitmissing():
957 return
966 return
958 if self.dirty():
967 if self.dirty():
959 self._ui.warn(_('not removing repo %s because '
968 self._ui.warn(_('not removing repo %s because '
960 'it has changes.\n') % self._relpath)
969 'it has changes.\n') % self._relpath)
961 return
970 return
962 # we can't fully delete the repository as it may contain
971 # we can't fully delete the repository as it may contain
963 # local-only history
972 # local-only history
964 self._ui.note(_('removing subrepo %s\n') % self._relpath)
973 self._ui.note(_('removing subrepo %s\n') % self._relpath)
965 self._gitcommand(['config', 'core.bare', 'true'])
974 self._gitcommand(['config', 'core.bare', 'true'])
966 for f in os.listdir(self._abspath):
975 for f in os.listdir(self._abspath):
967 if f == '.git':
976 if f == '.git':
968 continue
977 continue
969 path = os.path.join(self._abspath, f)
978 path = os.path.join(self._abspath, f)
970 if os.path.isdir(path) and not os.path.islink(path):
979 if os.path.isdir(path) and not os.path.islink(path):
971 shutil.rmtree(path)
980 shutil.rmtree(path)
972 else:
981 else:
973 os.remove(path)
982 os.remove(path)
974
983
975 def archive(self, ui, archiver, prefix):
984 def archive(self, ui, archiver, prefix):
976 source, revision = self._state
985 source, revision = self._state
977 self._fetch(source, revision)
986 self._fetch(source, revision)
978
987
979 # Parse git's native archive command.
988 # Parse git's native archive command.
980 # This should be much faster than manually traversing the trees
989 # This should be much faster than manually traversing the trees
981 # and objects with many subprocess calls.
990 # and objects with many subprocess calls.
982 tarstream = self._gitcommand(['archive', revision], stream=True)
991 tarstream = self._gitcommand(['archive', revision], stream=True)
983 tar = tarfile.open(fileobj=tarstream, mode='r|')
992 tar = tarfile.open(fileobj=tarstream, mode='r|')
984 relpath = subrelpath(self)
993 relpath = subrelpath(self)
985 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
994 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
986 for i, info in enumerate(tar):
995 for i, info in enumerate(tar):
987 if info.isdir():
996 if info.isdir():
988 continue
997 continue
989 if info.issym():
998 if info.issym():
990 data = info.linkname
999 data = info.linkname
991 else:
1000 else:
992 data = tar.extractfile(info).read()
1001 data = tar.extractfile(info).read()
993 archiver.addfile(os.path.join(prefix, self._path, info.name),
1002 archiver.addfile(os.path.join(prefix, self._path, info.name),
994 info.mode, info.issym(), data)
1003 info.mode, info.issym(), data)
995 ui.progress(_('archiving (%s)') % relpath, i + 1,
1004 ui.progress(_('archiving (%s)') % relpath, i + 1,
996 unit=_('files'))
1005 unit=_('files'))
997 ui.progress(_('archiving (%s)') % relpath, None)
1006 ui.progress(_('archiving (%s)') % relpath, None)
998
1007
999
1008
1000 def status(self, rev2, **opts):
1009 def status(self, rev2, **opts):
1001 if self._gitmissing():
1010 if self._gitmissing():
1002 # if the repo is missing, return no results
1011 # if the repo is missing, return no results
1003 return [], [], [], [], [], [], []
1012 return [], [], [], [], [], [], []
1004 rev1 = self._state[1]
1013 rev1 = self._state[1]
1005 modified, added, removed = [], [], []
1014 modified, added, removed = [], [], []
1006 if rev2:
1015 if rev2:
1007 command = ['diff-tree', rev1, rev2]
1016 command = ['diff-tree', rev1, rev2]
1008 else:
1017 else:
1009 command = ['diff-index', rev1]
1018 command = ['diff-index', rev1]
1010 out = self._gitcommand(command)
1019 out = self._gitcommand(command)
1011 for line in out.split('\n'):
1020 for line in out.split('\n'):
1012 tab = line.find('\t')
1021 tab = line.find('\t')
1013 if tab == -1:
1022 if tab == -1:
1014 continue
1023 continue
1015 status, f = line[tab - 1], line[tab + 1:]
1024 status, f = line[tab - 1], line[tab + 1:]
1016 if status == 'M':
1025 if status == 'M':
1017 modified.append(f)
1026 modified.append(f)
1018 elif status == 'A':
1027 elif status == 'A':
1019 added.append(f)
1028 added.append(f)
1020 elif status == 'D':
1029 elif status == 'D':
1021 removed.append(f)
1030 removed.append(f)
1022
1031
1023 deleted = unknown = ignored = clean = []
1032 deleted = unknown = ignored = clean = []
1024 return modified, added, removed, deleted, unknown, ignored, clean
1033 return modified, added, removed, deleted, unknown, ignored, clean
1025
1034
1026 types = {
1035 types = {
1027 'hg': hgsubrepo,
1036 'hg': hgsubrepo,
1028 'svn': svnsubrepo,
1037 'svn': svnsubrepo,
1029 'git': gitsubrepo,
1038 'git': gitsubrepo,
1030 }
1039 }
General Comments 0
You need to be logged in to leave comments. Login now