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