##// END OF EJS Templates
svn subrepo: attempt work around obstructed checkouts (issue2752)...
Augie Fackler -
r14041:bcc6ed0f default
parent child Browse files
Show More
@@ -1,1022 +1,1030
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'] + 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 # catch the case where the checkout operation is
625 # obstructed but the working copy is clean
626 if ('already a working copy for a different' in status and
627 not self.dirty()):
628 self.remove()
629 self.get(state, overwrite)
630 return
631 else:
632 raise util.Abort(status.splitlines()[-1])
625 self._ui.status(status)
633 self._ui.status(status)
626
634
627 def merge(self, state):
635 def merge(self, state):
628 old = self._state[1]
636 old = self._state[1]
629 new = state[1]
637 new = state[1]
630 if new != self._wcrev():
638 if new != self._wcrev():
631 dirty = old == self._wcrev() or self._wcchanged()[0]
639 dirty = old == self._wcrev() or self._wcchanged()[0]
632 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
640 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
633 self.get(state, False)
641 self.get(state, False)
634
642
635 def push(self, force):
643 def push(self, force):
636 # push is a no-op for SVN
644 # push is a no-op for SVN
637 return True
645 return True
638
646
639 def files(self):
647 def files(self):
640 output = self._svncommand(['list'])
648 output = self._svncommand(['list'])
641 # This works because svn forbids \n in filenames.
649 # This works because svn forbids \n in filenames.
642 return output.splitlines()
650 return output.splitlines()
643
651
644 def filedata(self, name):
652 def filedata(self, name):
645 return self._svncommand(['cat'], name)
653 return self._svncommand(['cat'], name)
646
654
647
655
648 class gitsubrepo(abstractsubrepo):
656 class gitsubrepo(abstractsubrepo):
649 def __init__(self, ctx, path, state):
657 def __init__(self, ctx, path, state):
650 # TODO add git version check.
658 # TODO add git version check.
651 self._state = state
659 self._state = state
652 self._ctx = ctx
660 self._ctx = ctx
653 self._path = path
661 self._path = path
654 self._relpath = os.path.join(reporelpath(ctx._repo), path)
662 self._relpath = os.path.join(reporelpath(ctx._repo), path)
655 self._abspath = ctx._repo.wjoin(path)
663 self._abspath = ctx._repo.wjoin(path)
656 self._subparent = ctx._repo
664 self._subparent = ctx._repo
657 self._ui = ctx._repo.ui
665 self._ui = ctx._repo.ui
658
666
659 def _gitcommand(self, commands, env=None, stream=False):
667 def _gitcommand(self, commands, env=None, stream=False):
660 return self._gitdir(commands, env=env, stream=stream)[0]
668 return self._gitdir(commands, env=env, stream=stream)[0]
661
669
662 def _gitdir(self, commands, env=None, stream=False):
670 def _gitdir(self, commands, env=None, stream=False):
663 return self._gitnodir(commands, env=env, stream=stream,
671 return self._gitnodir(commands, env=env, stream=stream,
664 cwd=self._abspath)
672 cwd=self._abspath)
665
673
666 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
674 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
667 """Calls the git command
675 """Calls the git command
668
676
669 The methods tries to call the git command. versions previor to 1.6.0
677 The methods tries to call the git command. versions previor to 1.6.0
670 are not supported and very probably fail.
678 are not supported and very probably fail.
671 """
679 """
672 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
680 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
673 # unless ui.quiet is set, print git's stderr,
681 # unless ui.quiet is set, print git's stderr,
674 # which is mostly progress and useful info
682 # which is mostly progress and useful info
675 errpipe = None
683 errpipe = None
676 if self._ui.quiet:
684 if self._ui.quiet:
677 errpipe = open(os.devnull, 'w')
685 errpipe = open(os.devnull, 'w')
678 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
686 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
679 close_fds=util.closefds,
687 close_fds=util.closefds,
680 stdout=subprocess.PIPE, stderr=errpipe)
688 stdout=subprocess.PIPE, stderr=errpipe)
681 if stream:
689 if stream:
682 return p.stdout, None
690 return p.stdout, None
683
691
684 retdata = p.stdout.read().strip()
692 retdata = p.stdout.read().strip()
685 # wait for the child to exit to avoid race condition.
693 # wait for the child to exit to avoid race condition.
686 p.wait()
694 p.wait()
687
695
688 if p.returncode != 0 and p.returncode != 1:
696 if p.returncode != 0 and p.returncode != 1:
689 # there are certain error codes that are ok
697 # there are certain error codes that are ok
690 command = commands[0]
698 command = commands[0]
691 if command in ('cat-file', 'symbolic-ref'):
699 if command in ('cat-file', 'symbolic-ref'):
692 return retdata, p.returncode
700 return retdata, p.returncode
693 # for all others, abort
701 # for all others, abort
694 raise util.Abort('git %s error %d in %s' %
702 raise util.Abort('git %s error %d in %s' %
695 (command, p.returncode, self._relpath))
703 (command, p.returncode, self._relpath))
696
704
697 return retdata, p.returncode
705 return retdata, p.returncode
698
706
699 def _gitmissing(self):
707 def _gitmissing(self):
700 return not os.path.exists(os.path.join(self._abspath, '.git'))
708 return not os.path.exists(os.path.join(self._abspath, '.git'))
701
709
702 def _gitstate(self):
710 def _gitstate(self):
703 return self._gitcommand(['rev-parse', 'HEAD'])
711 return self._gitcommand(['rev-parse', 'HEAD'])
704
712
705 def _gitcurrentbranch(self):
713 def _gitcurrentbranch(self):
706 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
714 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
707 if err:
715 if err:
708 current = None
716 current = None
709 return current
717 return current
710
718
711 def _gitremote(self, remote):
719 def _gitremote(self, remote):
712 out = self._gitcommand(['remote', 'show', '-n', remote])
720 out = self._gitcommand(['remote', 'show', '-n', remote])
713 line = out.split('\n')[1]
721 line = out.split('\n')[1]
714 i = line.index('URL: ') + len('URL: ')
722 i = line.index('URL: ') + len('URL: ')
715 return line[i:]
723 return line[i:]
716
724
717 def _githavelocally(self, revision):
725 def _githavelocally(self, revision):
718 out, code = self._gitdir(['cat-file', '-e', revision])
726 out, code = self._gitdir(['cat-file', '-e', revision])
719 return code == 0
727 return code == 0
720
728
721 def _gitisancestor(self, r1, r2):
729 def _gitisancestor(self, r1, r2):
722 base = self._gitcommand(['merge-base', r1, r2])
730 base = self._gitcommand(['merge-base', r1, r2])
723 return base == r1
731 return base == r1
724
732
725 def _gitbranchmap(self):
733 def _gitbranchmap(self):
726 '''returns 2 things:
734 '''returns 2 things:
727 a map from git branch to revision
735 a map from git branch to revision
728 a map from revision to branches'''
736 a map from revision to branches'''
729 branch2rev = {}
737 branch2rev = {}
730 rev2branch = {}
738 rev2branch = {}
731
739
732 out = self._gitcommand(['for-each-ref', '--format',
740 out = self._gitcommand(['for-each-ref', '--format',
733 '%(objectname) %(refname)'])
741 '%(objectname) %(refname)'])
734 for line in out.split('\n'):
742 for line in out.split('\n'):
735 revision, ref = line.split(' ')
743 revision, ref = line.split(' ')
736 if (not ref.startswith('refs/heads/') and
744 if (not ref.startswith('refs/heads/') and
737 not ref.startswith('refs/remotes/')):
745 not ref.startswith('refs/remotes/')):
738 continue
746 continue
739 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
747 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
740 continue # ignore remote/HEAD redirects
748 continue # ignore remote/HEAD redirects
741 branch2rev[ref] = revision
749 branch2rev[ref] = revision
742 rev2branch.setdefault(revision, []).append(ref)
750 rev2branch.setdefault(revision, []).append(ref)
743 return branch2rev, rev2branch
751 return branch2rev, rev2branch
744
752
745 def _gittracking(self, branches):
753 def _gittracking(self, branches):
746 'return map of remote branch to local tracking branch'
754 'return map of remote branch to local tracking branch'
747 # assumes no more than one local tracking branch for each remote
755 # assumes no more than one local tracking branch for each remote
748 tracking = {}
756 tracking = {}
749 for b in branches:
757 for b in branches:
750 if b.startswith('refs/remotes/'):
758 if b.startswith('refs/remotes/'):
751 continue
759 continue
752 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
760 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
753 if remote:
761 if remote:
754 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
762 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
755 tracking['refs/remotes/%s/%s' %
763 tracking['refs/remotes/%s/%s' %
756 (remote, ref.split('/', 2)[2])] = b
764 (remote, ref.split('/', 2)[2])] = b
757 return tracking
765 return tracking
758
766
759 def _abssource(self, source):
767 def _abssource(self, source):
760 if '://' not in source:
768 if '://' not in source:
761 # recognize the scp syntax as an absolute source
769 # recognize the scp syntax as an absolute source
762 colon = source.find(':')
770 colon = source.find(':')
763 if colon != -1 and '/' not in source[:colon]:
771 if colon != -1 and '/' not in source[:colon]:
764 return source
772 return source
765 self._subsource = source
773 self._subsource = source
766 return _abssource(self)
774 return _abssource(self)
767
775
768 def _fetch(self, source, revision):
776 def _fetch(self, source, revision):
769 if self._gitmissing():
777 if self._gitmissing():
770 source = self._abssource(source)
778 source = self._abssource(source)
771 self._ui.status(_('cloning subrepo %s from %s\n') %
779 self._ui.status(_('cloning subrepo %s from %s\n') %
772 (self._relpath, source))
780 (self._relpath, source))
773 self._gitnodir(['clone', source, self._abspath])
781 self._gitnodir(['clone', source, self._abspath])
774 if self._githavelocally(revision):
782 if self._githavelocally(revision):
775 return
783 return
776 self._ui.status(_('pulling subrepo %s from %s\n') %
784 self._ui.status(_('pulling subrepo %s from %s\n') %
777 (self._relpath, self._gitremote('origin')))
785 (self._relpath, self._gitremote('origin')))
778 # try only origin: the originally cloned repo
786 # try only origin: the originally cloned repo
779 self._gitcommand(['fetch'])
787 self._gitcommand(['fetch'])
780 if not self._githavelocally(revision):
788 if not self._githavelocally(revision):
781 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
789 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
782 (revision, self._relpath))
790 (revision, self._relpath))
783
791
784 def dirty(self, ignoreupdate=False):
792 def dirty(self, ignoreupdate=False):
785 if self._gitmissing():
793 if self._gitmissing():
786 return True
794 return True
787 if not ignoreupdate and self._state[1] != self._gitstate():
795 if not ignoreupdate and self._state[1] != self._gitstate():
788 # different version checked out
796 # different version checked out
789 return True
797 return True
790 # check for staged changes or modified files; ignore untracked files
798 # check for staged changes or modified files; ignore untracked files
791 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
799 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
792 return code == 1
800 return code == 1
793
801
794 def get(self, state, overwrite=False):
802 def get(self, state, overwrite=False):
795 source, revision, kind = state
803 source, revision, kind = state
796 self._fetch(source, revision)
804 self._fetch(source, revision)
797 # if the repo was set to be bare, unbare it
805 # if the repo was set to be bare, unbare it
798 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
806 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
799 self._gitcommand(['config', 'core.bare', 'false'])
807 self._gitcommand(['config', 'core.bare', 'false'])
800 if self._gitstate() == revision:
808 if self._gitstate() == revision:
801 self._gitcommand(['reset', '--hard', 'HEAD'])
809 self._gitcommand(['reset', '--hard', 'HEAD'])
802 return
810 return
803 elif self._gitstate() == revision:
811 elif self._gitstate() == revision:
804 if overwrite:
812 if overwrite:
805 # first reset the index to unmark new files for commit, because
813 # first reset the index to unmark new files for commit, because
806 # reset --hard will otherwise throw away files added for commit,
814 # reset --hard will otherwise throw away files added for commit,
807 # not just unmark them.
815 # not just unmark them.
808 self._gitcommand(['reset', 'HEAD'])
816 self._gitcommand(['reset', 'HEAD'])
809 self._gitcommand(['reset', '--hard', 'HEAD'])
817 self._gitcommand(['reset', '--hard', 'HEAD'])
810 return
818 return
811 branch2rev, rev2branch = self._gitbranchmap()
819 branch2rev, rev2branch = self._gitbranchmap()
812
820
813 def checkout(args):
821 def checkout(args):
814 cmd = ['checkout']
822 cmd = ['checkout']
815 if overwrite:
823 if overwrite:
816 # first reset the index to unmark new files for commit, because
824 # first reset the index to unmark new files for commit, because
817 # the -f option will otherwise throw away files added for
825 # the -f option will otherwise throw away files added for
818 # commit, not just unmark them.
826 # commit, not just unmark them.
819 self._gitcommand(['reset', 'HEAD'])
827 self._gitcommand(['reset', 'HEAD'])
820 cmd.append('-f')
828 cmd.append('-f')
821 self._gitcommand(cmd + args)
829 self._gitcommand(cmd + args)
822
830
823 def rawcheckout():
831 def rawcheckout():
824 # no branch to checkout, check it out with no branch
832 # no branch to checkout, check it out with no branch
825 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
833 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
826 self._relpath)
834 self._relpath)
827 self._ui.warn(_('check out a git branch if you intend '
835 self._ui.warn(_('check out a git branch if you intend '
828 'to make changes\n'))
836 'to make changes\n'))
829 checkout(['-q', revision])
837 checkout(['-q', revision])
830
838
831 if revision not in rev2branch:
839 if revision not in rev2branch:
832 rawcheckout()
840 rawcheckout()
833 return
841 return
834 branches = rev2branch[revision]
842 branches = rev2branch[revision]
835 firstlocalbranch = None
843 firstlocalbranch = None
836 for b in branches:
844 for b in branches:
837 if b == 'refs/heads/master':
845 if b == 'refs/heads/master':
838 # master trumps all other branches
846 # master trumps all other branches
839 checkout(['refs/heads/master'])
847 checkout(['refs/heads/master'])
840 return
848 return
841 if not firstlocalbranch and not b.startswith('refs/remotes/'):
849 if not firstlocalbranch and not b.startswith('refs/remotes/'):
842 firstlocalbranch = b
850 firstlocalbranch = b
843 if firstlocalbranch:
851 if firstlocalbranch:
844 checkout([firstlocalbranch])
852 checkout([firstlocalbranch])
845 return
853 return
846
854
847 tracking = self._gittracking(branch2rev.keys())
855 tracking = self._gittracking(branch2rev.keys())
848 # choose a remote branch already tracked if possible
856 # choose a remote branch already tracked if possible
849 remote = branches[0]
857 remote = branches[0]
850 if remote not in tracking:
858 if remote not in tracking:
851 for b in branches:
859 for b in branches:
852 if b in tracking:
860 if b in tracking:
853 remote = b
861 remote = b
854 break
862 break
855
863
856 if remote not in tracking:
864 if remote not in tracking:
857 # create a new local tracking branch
865 # create a new local tracking branch
858 local = remote.split('/', 2)[2]
866 local = remote.split('/', 2)[2]
859 checkout(['-b', local, remote])
867 checkout(['-b', local, remote])
860 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
868 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
861 # When updating to a tracked remote branch,
869 # When updating to a tracked remote branch,
862 # if the local tracking branch is downstream of it,
870 # if the local tracking branch is downstream of it,
863 # a normal `git pull` would have performed a "fast-forward merge"
871 # a normal `git pull` would have performed a "fast-forward merge"
864 # which is equivalent to updating the local branch to the remote.
872 # which is equivalent to updating the local branch to the remote.
865 # Since we are only looking at branching at update, we need to
873 # Since we are only looking at branching at update, we need to
866 # detect this situation and perform this action lazily.
874 # detect this situation and perform this action lazily.
867 if tracking[remote] != self._gitcurrentbranch():
875 if tracking[remote] != self._gitcurrentbranch():
868 checkout([tracking[remote]])
876 checkout([tracking[remote]])
869 self._gitcommand(['merge', '--ff', remote])
877 self._gitcommand(['merge', '--ff', remote])
870 else:
878 else:
871 # a real merge would be required, just checkout the revision
879 # a real merge would be required, just checkout the revision
872 rawcheckout()
880 rawcheckout()
873
881
874 def commit(self, text, user, date):
882 def commit(self, text, user, date):
875 if self._gitmissing():
883 if self._gitmissing():
876 raise util.Abort(_("subrepo %s is missing") % self._relpath)
884 raise util.Abort(_("subrepo %s is missing") % self._relpath)
877 cmd = ['commit', '-a', '-m', text]
885 cmd = ['commit', '-a', '-m', text]
878 env = os.environ.copy()
886 env = os.environ.copy()
879 if user:
887 if user:
880 cmd += ['--author', user]
888 cmd += ['--author', user]
881 if date:
889 if date:
882 # git's date parser silently ignores when seconds < 1e9
890 # git's date parser silently ignores when seconds < 1e9
883 # convert to ISO8601
891 # convert to ISO8601
884 env['GIT_AUTHOR_DATE'] = util.datestr(date,
892 env['GIT_AUTHOR_DATE'] = util.datestr(date,
885 '%Y-%m-%dT%H:%M:%S %1%2')
893 '%Y-%m-%dT%H:%M:%S %1%2')
886 self._gitcommand(cmd, env=env)
894 self._gitcommand(cmd, env=env)
887 # make sure commit works otherwise HEAD might not exist under certain
895 # make sure commit works otherwise HEAD might not exist under certain
888 # circumstances
896 # circumstances
889 return self._gitstate()
897 return self._gitstate()
890
898
891 def merge(self, state):
899 def merge(self, state):
892 source, revision, kind = state
900 source, revision, kind = state
893 self._fetch(source, revision)
901 self._fetch(source, revision)
894 base = self._gitcommand(['merge-base', revision, self._state[1]])
902 base = self._gitcommand(['merge-base', revision, self._state[1]])
895 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
903 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
896
904
897 def mergefunc():
905 def mergefunc():
898 if base == revision:
906 if base == revision:
899 self.get(state) # fast forward merge
907 self.get(state) # fast forward merge
900 elif base != self._state[1]:
908 elif base != self._state[1]:
901 self._gitcommand(['merge', '--no-commit', revision])
909 self._gitcommand(['merge', '--no-commit', revision])
902
910
903 if self.dirty():
911 if self.dirty():
904 if self._gitstate() != revision:
912 if self._gitstate() != revision:
905 dirty = self._gitstate() == self._state[1] or code != 0
913 dirty = self._gitstate() == self._state[1] or code != 0
906 if _updateprompt(self._ui, self, dirty,
914 if _updateprompt(self._ui, self, dirty,
907 self._state[1][:7], revision[:7]):
915 self._state[1][:7], revision[:7]):
908 mergefunc()
916 mergefunc()
909 else:
917 else:
910 mergefunc()
918 mergefunc()
911
919
912 def push(self, force):
920 def push(self, force):
913 if self._gitmissing():
921 if self._gitmissing():
914 raise util.Abort(_("subrepo %s is missing") % self._relpath)
922 raise util.Abort(_("subrepo %s is missing") % self._relpath)
915 # if a branch in origin contains the revision, nothing to do
923 # if a branch in origin contains the revision, nothing to do
916 branch2rev, rev2branch = self._gitbranchmap()
924 branch2rev, rev2branch = self._gitbranchmap()
917 if self._state[1] in rev2branch:
925 if self._state[1] in rev2branch:
918 for b in rev2branch[self._state[1]]:
926 for b in rev2branch[self._state[1]]:
919 if b.startswith('refs/remotes/origin/'):
927 if b.startswith('refs/remotes/origin/'):
920 return True
928 return True
921 for b, revision in branch2rev.iteritems():
929 for b, revision in branch2rev.iteritems():
922 if b.startswith('refs/remotes/origin/'):
930 if b.startswith('refs/remotes/origin/'):
923 if self._gitisancestor(self._state[1], revision):
931 if self._gitisancestor(self._state[1], revision):
924 return True
932 return True
925 # otherwise, try to push the currently checked out branch
933 # otherwise, try to push the currently checked out branch
926 cmd = ['push']
934 cmd = ['push']
927 if force:
935 if force:
928 cmd.append('--force')
936 cmd.append('--force')
929
937
930 current = self._gitcurrentbranch()
938 current = self._gitcurrentbranch()
931 if current:
939 if current:
932 # determine if the current branch is even useful
940 # determine if the current branch is even useful
933 if not self._gitisancestor(self._state[1], current):
941 if not self._gitisancestor(self._state[1], current):
934 self._ui.warn(_('unrelated git branch checked out '
942 self._ui.warn(_('unrelated git branch checked out '
935 'in subrepo %s\n') % self._relpath)
943 'in subrepo %s\n') % self._relpath)
936 return False
944 return False
937 self._ui.status(_('pushing branch %s of subrepo %s\n') %
945 self._ui.status(_('pushing branch %s of subrepo %s\n') %
938 (current.split('/', 2)[2], self._relpath))
946 (current.split('/', 2)[2], self._relpath))
939 self._gitcommand(cmd + ['origin', current])
947 self._gitcommand(cmd + ['origin', current])
940 return True
948 return True
941 else:
949 else:
942 self._ui.warn(_('no branch checked out in subrepo %s\n'
950 self._ui.warn(_('no branch checked out in subrepo %s\n'
943 'cannot push revision %s') %
951 'cannot push revision %s') %
944 (self._relpath, self._state[1]))
952 (self._relpath, self._state[1]))
945 return False
953 return False
946
954
947 def remove(self):
955 def remove(self):
948 if self._gitmissing():
956 if self._gitmissing():
949 return
957 return
950 if self.dirty():
958 if self.dirty():
951 self._ui.warn(_('not removing repo %s because '
959 self._ui.warn(_('not removing repo %s because '
952 'it has changes.\n') % self._relpath)
960 'it has changes.\n') % self._relpath)
953 return
961 return
954 # we can't fully delete the repository as it may contain
962 # we can't fully delete the repository as it may contain
955 # local-only history
963 # local-only history
956 self._ui.note(_('removing subrepo %s\n') % self._relpath)
964 self._ui.note(_('removing subrepo %s\n') % self._relpath)
957 self._gitcommand(['config', 'core.bare', 'true'])
965 self._gitcommand(['config', 'core.bare', 'true'])
958 for f in os.listdir(self._abspath):
966 for f in os.listdir(self._abspath):
959 if f == '.git':
967 if f == '.git':
960 continue
968 continue
961 path = os.path.join(self._abspath, f)
969 path = os.path.join(self._abspath, f)
962 if os.path.isdir(path) and not os.path.islink(path):
970 if os.path.isdir(path) and not os.path.islink(path):
963 shutil.rmtree(path)
971 shutil.rmtree(path)
964 else:
972 else:
965 os.remove(path)
973 os.remove(path)
966
974
967 def archive(self, ui, archiver, prefix):
975 def archive(self, ui, archiver, prefix):
968 source, revision = self._state
976 source, revision = self._state
969 self._fetch(source, revision)
977 self._fetch(source, revision)
970
978
971 # Parse git's native archive command.
979 # Parse git's native archive command.
972 # This should be much faster than manually traversing the trees
980 # This should be much faster than manually traversing the trees
973 # and objects with many subprocess calls.
981 # and objects with many subprocess calls.
974 tarstream = self._gitcommand(['archive', revision], stream=True)
982 tarstream = self._gitcommand(['archive', revision], stream=True)
975 tar = tarfile.open(fileobj=tarstream, mode='r|')
983 tar = tarfile.open(fileobj=tarstream, mode='r|')
976 relpath = subrelpath(self)
984 relpath = subrelpath(self)
977 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
985 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
978 for i, info in enumerate(tar):
986 for i, info in enumerate(tar):
979 if info.isdir():
987 if info.isdir():
980 continue
988 continue
981 if info.issym():
989 if info.issym():
982 data = info.linkname
990 data = info.linkname
983 else:
991 else:
984 data = tar.extractfile(info).read()
992 data = tar.extractfile(info).read()
985 archiver.addfile(os.path.join(prefix, self._path, info.name),
993 archiver.addfile(os.path.join(prefix, self._path, info.name),
986 info.mode, info.issym(), data)
994 info.mode, info.issym(), data)
987 ui.progress(_('archiving (%s)') % relpath, i + 1,
995 ui.progress(_('archiving (%s)') % relpath, i + 1,
988 unit=_('files'))
996 unit=_('files'))
989 ui.progress(_('archiving (%s)') % relpath, None)
997 ui.progress(_('archiving (%s)') % relpath, None)
990
998
991
999
992 def status(self, rev2, **opts):
1000 def status(self, rev2, **opts):
993 if self._gitmissing():
1001 if self._gitmissing():
994 # if the repo is missing, return no results
1002 # if the repo is missing, return no results
995 return [], [], [], [], [], [], []
1003 return [], [], [], [], [], [], []
996 rev1 = self._state[1]
1004 rev1 = self._state[1]
997 modified, added, removed = [], [], []
1005 modified, added, removed = [], [], []
998 if rev2:
1006 if rev2:
999 command = ['diff-tree', rev1, rev2]
1007 command = ['diff-tree', rev1, rev2]
1000 else:
1008 else:
1001 command = ['diff-index', rev1]
1009 command = ['diff-index', rev1]
1002 out = self._gitcommand(command)
1010 out = self._gitcommand(command)
1003 for line in out.split('\n'):
1011 for line in out.split('\n'):
1004 tab = line.find('\t')
1012 tab = line.find('\t')
1005 if tab == -1:
1013 if tab == -1:
1006 continue
1014 continue
1007 status, f = line[tab - 1], line[tab + 1:]
1015 status, f = line[tab - 1], line[tab + 1:]
1008 if status == 'M':
1016 if status == 'M':
1009 modified.append(f)
1017 modified.append(f)
1010 elif status == 'A':
1018 elif status == 'A':
1011 added.append(f)
1019 added.append(f)
1012 elif status == 'D':
1020 elif status == 'D':
1013 removed.append(f)
1021 removed.append(f)
1014
1022
1015 deleted = unknown = ignored = clean = []
1023 deleted = unknown = ignored = clean = []
1016 return modified, added, removed, deleted, unknown, ignored, clean
1024 return modified, added, removed, deleted, unknown, ignored, clean
1017
1025
1018 types = {
1026 types = {
1019 'hg': hgsubrepo,
1027 'hg': hgsubrepo,
1020 'svn': svnsubrepo,
1028 'svn': svnsubrepo,
1021 'git': gitsubrepo,
1029 'git': gitsubrepo,
1022 }
1030 }
General Comments 0
You need to be logged in to leave comments. Login now