##// END OF EJS Templates
subrepo: simplify hgsubrepo._get a little
Martin Geisler -
r13753:78a0a815 default
parent child Browse files
Show More
@@ -1,1029 +1,1027 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, bookmarks
11 import config, util, node, error, cmdutil, bookmarks
12 hg = None
12 hg = None
13
13
14 nullstate = ('', '', 'empty')
14 nullstate = ('', '', 'empty')
15
15
16 def state(ctx, ui):
16 def state(ctx, ui):
17 """return a state dict, mapping subrepo paths configured in .hgsub
17 """return a state dict, mapping subrepo paths configured in .hgsub
18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
18 to tuple: (source from .hgsub, revision from .hgsubstate, kind
19 (key in types dict))
19 (key in types dict))
20 """
20 """
21 p = config.config()
21 p = config.config()
22 def read(f, sections=None, remap=None):
22 def read(f, sections=None, remap=None):
23 if f in ctx:
23 if f in ctx:
24 try:
24 try:
25 data = ctx[f].data()
25 data = ctx[f].data()
26 except IOError, err:
26 except IOError, err:
27 if err.errno != errno.ENOENT:
27 if err.errno != errno.ENOENT:
28 raise
28 raise
29 # handle missing subrepo spec files as removed
29 # handle missing subrepo spec files as removed
30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
30 ui.warn(_("warning: subrepo spec file %s not found\n") % f)
31 return
31 return
32 p.parse(f, data, sections, remap, read)
32 p.parse(f, data, sections, remap, read)
33 else:
33 else:
34 raise util.Abort(_("subrepo spec file %s not found") % f)
34 raise util.Abort(_("subrepo spec file %s not found") % f)
35
35
36 if '.hgsub' in ctx:
36 if '.hgsub' in ctx:
37 read('.hgsub')
37 read('.hgsub')
38
38
39 for path, src in ui.configitems('subpaths'):
39 for path, src in ui.configitems('subpaths'):
40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
40 p.set('subpaths', path, src, ui.configsource('subpaths', path))
41
41
42 rev = {}
42 rev = {}
43 if '.hgsubstate' in ctx:
43 if '.hgsubstate' in ctx:
44 try:
44 try:
45 for l in ctx['.hgsubstate'].data().splitlines():
45 for l in ctx['.hgsubstate'].data().splitlines():
46 revision, path = l.split(" ", 1)
46 revision, path = l.split(" ", 1)
47 rev[path] = revision
47 rev[path] = revision
48 except IOError, err:
48 except IOError, err:
49 if err.errno != errno.ENOENT:
49 if err.errno != errno.ENOENT:
50 raise
50 raise
51
51
52 state = {}
52 state = {}
53 for path, src in p[''].items():
53 for path, src in p[''].items():
54 kind = 'hg'
54 kind = 'hg'
55 if src.startswith('['):
55 if src.startswith('['):
56 if ']' not in src:
56 if ']' not in src:
57 raise util.Abort(_('missing ] in subrepo source'))
57 raise util.Abort(_('missing ] in subrepo source'))
58 kind, src = src.split(']', 1)
58 kind, src = src.split(']', 1)
59 kind = kind[1:]
59 kind = kind[1:]
60
60
61 for pattern, repl in p.items('subpaths'):
61 for pattern, repl in p.items('subpaths'):
62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
62 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
63 # does a string decode.
63 # does a string decode.
64 repl = repl.encode('string-escape')
64 repl = repl.encode('string-escape')
65 # However, we still want to allow back references to go
65 # However, we still want to allow back references to go
66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
66 # through unharmed, so we turn r'\\1' into r'\1'. Again,
67 # extra escapes are needed because re.sub string decodes.
67 # extra escapes are needed because re.sub string decodes.
68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
68 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
69 try:
69 try:
70 src = re.sub(pattern, repl, src, 1)
70 src = re.sub(pattern, repl, src, 1)
71 except re.error, e:
71 except re.error, e:
72 raise util.Abort(_("bad subrepository pattern in %s: %s")
72 raise util.Abort(_("bad subrepository pattern in %s: %s")
73 % (p.source('subpaths', pattern), e))
73 % (p.source('subpaths', pattern), e))
74
74
75 state[path] = (src.strip(), rev.get(path, ''), kind)
75 state[path] = (src.strip(), rev.get(path, ''), kind)
76
76
77 return state
77 return state
78
78
79 def writestate(repo, state):
79 def writestate(repo, state):
80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
80 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
81 repo.wwrite('.hgsubstate',
81 repo.wwrite('.hgsubstate',
82 ''.join(['%s %s\n' % (state[s][1], s)
82 ''.join(['%s %s\n' % (state[s][1], s)
83 for s in sorted(state)]), '')
83 for s in sorted(state)]), '')
84
84
85 def submerge(repo, wctx, mctx, actx, overwrite):
85 def submerge(repo, wctx, mctx, actx, overwrite):
86 """delegated from merge.applyupdates: merging of .hgsubstate file
86 """delegated from merge.applyupdates: merging of .hgsubstate file
87 in working context, merging context and ancestor context"""
87 in working context, merging context and ancestor context"""
88 if mctx == actx: # backwards?
88 if mctx == actx: # backwards?
89 actx = wctx.p1()
89 actx = wctx.p1()
90 s1 = wctx.substate
90 s1 = wctx.substate
91 s2 = mctx.substate
91 s2 = mctx.substate
92 sa = actx.substate
92 sa = actx.substate
93 sm = {}
93 sm = {}
94
94
95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
95 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
96
96
97 def debug(s, msg, r=""):
97 def debug(s, msg, r=""):
98 if r:
98 if r:
99 r = "%s:%s:%s" % r
99 r = "%s:%s:%s" % r
100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
100 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
101
101
102 for s, l in s1.items():
102 for s, l in s1.items():
103 a = sa.get(s, nullstate)
103 a = sa.get(s, nullstate)
104 ld = l # local state with possible dirty flag for compares
104 ld = l # local state with possible dirty flag for compares
105 if wctx.sub(s).dirty():
105 if wctx.sub(s).dirty():
106 ld = (l[0], l[1] + "+")
106 ld = (l[0], l[1] + "+")
107 if wctx == actx: # overwrite
107 if wctx == actx: # overwrite
108 a = ld
108 a = ld
109
109
110 if s in s2:
110 if s in s2:
111 r = s2[s]
111 r = s2[s]
112 if ld == r or r == a: # no change or local is newer
112 if ld == r or r == a: # no change or local is newer
113 sm[s] = l
113 sm[s] = l
114 continue
114 continue
115 elif ld == a: # other side changed
115 elif ld == a: # other side changed
116 debug(s, "other changed, get", r)
116 debug(s, "other changed, get", r)
117 wctx.sub(s).get(r, overwrite)
117 wctx.sub(s).get(r, overwrite)
118 sm[s] = r
118 sm[s] = r
119 elif ld[0] != r[0]: # sources differ
119 elif ld[0] != r[0]: # sources differ
120 if repo.ui.promptchoice(
120 if repo.ui.promptchoice(
121 _(' subrepository sources for %s differ\n'
121 _(' subrepository sources for %s differ\n'
122 'use (l)ocal source (%s) or (r)emote source (%s)?')
122 'use (l)ocal source (%s) or (r)emote source (%s)?')
123 % (s, l[0], r[0]),
123 % (s, l[0], r[0]),
124 (_('&Local'), _('&Remote')), 0):
124 (_('&Local'), _('&Remote')), 0):
125 debug(s, "prompt changed, get", r)
125 debug(s, "prompt changed, get", r)
126 wctx.sub(s).get(r, overwrite)
126 wctx.sub(s).get(r, overwrite)
127 sm[s] = r
127 sm[s] = r
128 elif ld[1] == a[1]: # local side is unchanged
128 elif ld[1] == a[1]: # local side is unchanged
129 debug(s, "other side changed, get", r)
129 debug(s, "other side changed, get", r)
130 wctx.sub(s).get(r, overwrite)
130 wctx.sub(s).get(r, overwrite)
131 sm[s] = r
131 sm[s] = r
132 else:
132 else:
133 debug(s, "both sides changed, merge with", r)
133 debug(s, "both sides changed, merge with", r)
134 wctx.sub(s).merge(r)
134 wctx.sub(s).merge(r)
135 sm[s] = l
135 sm[s] = l
136 elif ld == a: # remote removed, local unchanged
136 elif ld == a: # remote removed, local unchanged
137 debug(s, "remote removed, remove")
137 debug(s, "remote removed, remove")
138 wctx.sub(s).remove()
138 wctx.sub(s).remove()
139 else:
139 else:
140 if repo.ui.promptchoice(
140 if repo.ui.promptchoice(
141 _(' local changed subrepository %s which remote removed\n'
141 _(' local changed subrepository %s which remote removed\n'
142 'use (c)hanged version or (d)elete?') % s,
142 'use (c)hanged version or (d)elete?') % s,
143 (_('&Changed'), _('&Delete')), 0):
143 (_('&Changed'), _('&Delete')), 0):
144 debug(s, "prompt remove")
144 debug(s, "prompt remove")
145 wctx.sub(s).remove()
145 wctx.sub(s).remove()
146
146
147 for s, r in s2.items():
147 for s, r in 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 if revision not in self._repo:
436 self._repo.lookup(revision)
437 except error.RepoError:
438 self._repo._subsource = source
436 self._repo._subsource = source
439 srcurl = _abssource(self._repo)
437 srcurl = _abssource(self._repo)
440 self._repo.ui.status(_('pulling subrepo %s from %s\n')
438 self._repo.ui.status(_('pulling subrepo %s from %s\n')
441 % (subrelpath(self), srcurl))
439 % (subrelpath(self), srcurl))
442 other = hg.repository(self._repo.ui, srcurl)
440 other = hg.repository(self._repo.ui, srcurl)
443 self._repo.pull(other)
441 self._repo.pull(other)
444 bookmarks.updatefromremote(self._repo.ui, self._repo, other)
442 bookmarks.updatefromremote(self._repo.ui, self._repo, other)
445
443
446 def get(self, state, overwrite=False):
444 def get(self, state, overwrite=False):
447 self._get(state)
445 self._get(state)
448 source, revision, kind = state
446 source, revision, kind = state
449 self._repo.ui.debug("getting subrepo %s\n" % self._path)
447 self._repo.ui.debug("getting subrepo %s\n" % self._path)
450 hg.clean(self._repo, revision, False)
448 hg.clean(self._repo, revision, False)
451
449
452 def merge(self, state):
450 def merge(self, state):
453 self._get(state)
451 self._get(state)
454 cur = self._repo['.']
452 cur = self._repo['.']
455 dst = self._repo[state[1]]
453 dst = self._repo[state[1]]
456 anc = dst.ancestor(cur)
454 anc = dst.ancestor(cur)
457
455
458 def mergefunc():
456 def mergefunc():
459 if anc == cur:
457 if anc == cur:
460 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
458 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
461 hg.update(self._repo, state[1])
459 hg.update(self._repo, state[1])
462 elif anc == dst:
460 elif anc == dst:
463 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
461 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
464 else:
462 else:
465 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
463 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
466 hg.merge(self._repo, state[1], remind=False)
464 hg.merge(self._repo, state[1], remind=False)
467
465
468 wctx = self._repo[None]
466 wctx = self._repo[None]
469 if self.dirty():
467 if self.dirty():
470 if anc != dst:
468 if anc != dst:
471 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
469 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
472 mergefunc()
470 mergefunc()
473 else:
471 else:
474 mergefunc()
472 mergefunc()
475 else:
473 else:
476 mergefunc()
474 mergefunc()
477
475
478 def push(self, force):
476 def push(self, force):
479 # push subrepos depth-first for coherent ordering
477 # push subrepos depth-first for coherent ordering
480 c = self._repo['']
478 c = self._repo['']
481 subs = c.substate # only repos that are committed
479 subs = c.substate # only repos that are committed
482 for s in sorted(subs):
480 for s in sorted(subs):
483 if not c.sub(s).push(force):
481 if not c.sub(s).push(force):
484 return False
482 return False
485
483
486 dsturl = _abssource(self._repo, True)
484 dsturl = _abssource(self._repo, True)
487 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
485 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
488 (subrelpath(self), dsturl))
486 (subrelpath(self), dsturl))
489 other = hg.repository(self._repo.ui, dsturl)
487 other = hg.repository(self._repo.ui, dsturl)
490 return self._repo.push(other, force)
488 return self._repo.push(other, force)
491
489
492 def outgoing(self, ui, dest, opts):
490 def outgoing(self, ui, dest, opts):
493 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
491 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
494
492
495 def incoming(self, ui, source, opts):
493 def incoming(self, ui, source, opts):
496 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
494 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
497
495
498 def files(self):
496 def files(self):
499 rev = self._state[1]
497 rev = self._state[1]
500 ctx = self._repo[rev]
498 ctx = self._repo[rev]
501 return ctx.manifest()
499 return ctx.manifest()
502
500
503 def filedata(self, name):
501 def filedata(self, name):
504 rev = self._state[1]
502 rev = self._state[1]
505 return self._repo[rev][name].data()
503 return self._repo[rev][name].data()
506
504
507 def fileflags(self, name):
505 def fileflags(self, name):
508 rev = self._state[1]
506 rev = self._state[1]
509 ctx = self._repo[rev]
507 ctx = self._repo[rev]
510 return ctx.flags(name)
508 return ctx.flags(name)
511
509
512
510
513 class svnsubrepo(abstractsubrepo):
511 class svnsubrepo(abstractsubrepo):
514 def __init__(self, ctx, path, state):
512 def __init__(self, ctx, path, state):
515 self._path = path
513 self._path = path
516 self._state = state
514 self._state = state
517 self._ctx = ctx
515 self._ctx = ctx
518 self._ui = ctx._repo.ui
516 self._ui = ctx._repo.ui
519
517
520 def _svncommand(self, commands, filename=''):
518 def _svncommand(self, commands, filename=''):
521 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
519 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
522 cmd = ['svn'] + commands + [path]
520 cmd = ['svn'] + commands + [path]
523 env = dict(os.environ)
521 env = dict(os.environ)
524 # Avoid localized output, preserve current locale for everything else.
522 # Avoid localized output, preserve current locale for everything else.
525 env['LC_MESSAGES'] = 'C'
523 env['LC_MESSAGES'] = 'C'
526 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
524 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
527 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
525 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
528 universal_newlines=True, env=env)
526 universal_newlines=True, env=env)
529 stdout, stderr = p.communicate()
527 stdout, stderr = p.communicate()
530 stderr = stderr.strip()
528 stderr = stderr.strip()
531 if stderr:
529 if stderr:
532 raise util.Abort(stderr)
530 raise util.Abort(stderr)
533 return stdout
531 return stdout
534
532
535 def _wcrevs(self):
533 def _wcrevs(self):
536 # Get the working directory revision as well as the last
534 # Get the working directory revision as well as the last
537 # commit revision so we can compare the subrepo state with
535 # commit revision so we can compare the subrepo state with
538 # both. We used to store the working directory one.
536 # both. We used to store the working directory one.
539 output = self._svncommand(['info', '--xml'])
537 output = self._svncommand(['info', '--xml'])
540 doc = xml.dom.minidom.parseString(output)
538 doc = xml.dom.minidom.parseString(output)
541 entries = doc.getElementsByTagName('entry')
539 entries = doc.getElementsByTagName('entry')
542 lastrev, rev = '0', '0'
540 lastrev, rev = '0', '0'
543 if entries:
541 if entries:
544 rev = str(entries[0].getAttribute('revision')) or '0'
542 rev = str(entries[0].getAttribute('revision')) or '0'
545 commits = entries[0].getElementsByTagName('commit')
543 commits = entries[0].getElementsByTagName('commit')
546 if commits:
544 if commits:
547 lastrev = str(commits[0].getAttribute('revision')) or '0'
545 lastrev = str(commits[0].getAttribute('revision')) or '0'
548 return (lastrev, rev)
546 return (lastrev, rev)
549
547
550 def _wcrev(self):
548 def _wcrev(self):
551 return self._wcrevs()[0]
549 return self._wcrevs()[0]
552
550
553 def _wcchanged(self):
551 def _wcchanged(self):
554 """Return (changes, extchanges) where changes is True
552 """Return (changes, extchanges) where changes is True
555 if the working directory was changed, and extchanges is
553 if the working directory was changed, and extchanges is
556 True if any of these changes concern an external entry.
554 True if any of these changes concern an external entry.
557 """
555 """
558 output = self._svncommand(['status', '--xml'])
556 output = self._svncommand(['status', '--xml'])
559 externals, changes = [], []
557 externals, changes = [], []
560 doc = xml.dom.minidom.parseString(output)
558 doc = xml.dom.minidom.parseString(output)
561 for e in doc.getElementsByTagName('entry'):
559 for e in doc.getElementsByTagName('entry'):
562 s = e.getElementsByTagName('wc-status')
560 s = e.getElementsByTagName('wc-status')
563 if not s:
561 if not s:
564 continue
562 continue
565 item = s[0].getAttribute('item')
563 item = s[0].getAttribute('item')
566 props = s[0].getAttribute('props')
564 props = s[0].getAttribute('props')
567 path = e.getAttribute('path')
565 path = e.getAttribute('path')
568 if item == 'external':
566 if item == 'external':
569 externals.append(path)
567 externals.append(path)
570 if (item not in ('', 'normal', 'unversioned', 'external')
568 if (item not in ('', 'normal', 'unversioned', 'external')
571 or props not in ('', 'none')):
569 or props not in ('', 'none')):
572 changes.append(path)
570 changes.append(path)
573 for path in changes:
571 for path in changes:
574 for ext in externals:
572 for ext in externals:
575 if path == ext or path.startswith(ext + os.sep):
573 if path == ext or path.startswith(ext + os.sep):
576 return True, True
574 return True, True
577 return bool(changes), False
575 return bool(changes), False
578
576
579 def dirty(self, ignoreupdate=False):
577 def dirty(self, ignoreupdate=False):
580 if not self._wcchanged()[0]:
578 if not self._wcchanged()[0]:
581 if self._state[1] in self._wcrevs() or ignoreupdate:
579 if self._state[1] in self._wcrevs() or ignoreupdate:
582 return False
580 return False
583 return True
581 return True
584
582
585 def commit(self, text, user, date):
583 def commit(self, text, user, date):
586 # user and date are out of our hands since svn is centralized
584 # user and date are out of our hands since svn is centralized
587 changed, extchanged = self._wcchanged()
585 changed, extchanged = self._wcchanged()
588 if not changed:
586 if not changed:
589 return self._wcrev()
587 return self._wcrev()
590 if extchanged:
588 if extchanged:
591 # Do not try to commit externals
589 # Do not try to commit externals
592 raise util.Abort(_('cannot commit svn externals'))
590 raise util.Abort(_('cannot commit svn externals'))
593 commitinfo = self._svncommand(['commit', '-m', text])
591 commitinfo = self._svncommand(['commit', '-m', text])
594 self._ui.status(commitinfo)
592 self._ui.status(commitinfo)
595 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
593 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
596 if not newrev:
594 if not newrev:
597 raise util.Abort(commitinfo.splitlines()[-1])
595 raise util.Abort(commitinfo.splitlines()[-1])
598 newrev = newrev.groups()[0]
596 newrev = newrev.groups()[0]
599 self._ui.status(self._svncommand(['update', '-r', newrev]))
597 self._ui.status(self._svncommand(['update', '-r', newrev]))
600 return newrev
598 return newrev
601
599
602 def remove(self):
600 def remove(self):
603 if self.dirty():
601 if self.dirty():
604 self._ui.warn(_('not removing repo %s because '
602 self._ui.warn(_('not removing repo %s because '
605 'it has changes.\n' % self._path))
603 'it has changes.\n' % self._path))
606 return
604 return
607 self._ui.note(_('removing subrepo %s\n') % self._path)
605 self._ui.note(_('removing subrepo %s\n') % self._path)
608
606
609 def onerror(function, path, excinfo):
607 def onerror(function, path, excinfo):
610 if function is not os.remove:
608 if function is not os.remove:
611 raise
609 raise
612 # read-only files cannot be unlinked under Windows
610 # read-only files cannot be unlinked under Windows
613 s = os.stat(path)
611 s = os.stat(path)
614 if (s.st_mode & stat.S_IWRITE) != 0:
612 if (s.st_mode & stat.S_IWRITE) != 0:
615 raise
613 raise
616 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
614 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
617 os.remove(path)
615 os.remove(path)
618
616
619 path = self._ctx._repo.wjoin(self._path)
617 path = self._ctx._repo.wjoin(self._path)
620 shutil.rmtree(path, onerror=onerror)
618 shutil.rmtree(path, onerror=onerror)
621 try:
619 try:
622 os.removedirs(os.path.dirname(path))
620 os.removedirs(os.path.dirname(path))
623 except OSError:
621 except OSError:
624 pass
622 pass
625
623
626 def get(self, state, overwrite=False):
624 def get(self, state, overwrite=False):
627 if overwrite:
625 if overwrite:
628 self._svncommand(['revert', '--recursive'])
626 self._svncommand(['revert', '--recursive'])
629 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
627 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
630 if not re.search('Checked out revision [0-9]+.', status):
628 if not re.search('Checked out revision [0-9]+.', status):
631 raise util.Abort(status.splitlines()[-1])
629 raise util.Abort(status.splitlines()[-1])
632 self._ui.status(status)
630 self._ui.status(status)
633
631
634 def merge(self, state):
632 def merge(self, state):
635 old = self._state[1]
633 old = self._state[1]
636 new = state[1]
634 new = state[1]
637 if new != self._wcrev():
635 if new != self._wcrev():
638 dirty = old == self._wcrev() or self._wcchanged()[0]
636 dirty = old == self._wcrev() or self._wcchanged()[0]
639 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
637 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
640 self.get(state, False)
638 self.get(state, False)
641
639
642 def push(self, force):
640 def push(self, force):
643 # push is a no-op for SVN
641 # push is a no-op for SVN
644 return True
642 return True
645
643
646 def files(self):
644 def files(self):
647 output = self._svncommand(['list'])
645 output = self._svncommand(['list'])
648 # This works because svn forbids \n in filenames.
646 # This works because svn forbids \n in filenames.
649 return output.splitlines()
647 return output.splitlines()
650
648
651 def filedata(self, name):
649 def filedata(self, name):
652 return self._svncommand(['cat'], name)
650 return self._svncommand(['cat'], name)
653
651
654
652
655 class gitsubrepo(abstractsubrepo):
653 class gitsubrepo(abstractsubrepo):
656 def __init__(self, ctx, path, state):
654 def __init__(self, ctx, path, state):
657 # TODO add git version check.
655 # TODO add git version check.
658 self._state = state
656 self._state = state
659 self._ctx = ctx
657 self._ctx = ctx
660 self._path = path
658 self._path = path
661 self._relpath = os.path.join(reporelpath(ctx._repo), path)
659 self._relpath = os.path.join(reporelpath(ctx._repo), path)
662 self._abspath = ctx._repo.wjoin(path)
660 self._abspath = ctx._repo.wjoin(path)
663 self._subparent = ctx._repo
661 self._subparent = ctx._repo
664 self._ui = ctx._repo.ui
662 self._ui = ctx._repo.ui
665
663
666 def _gitcommand(self, commands, env=None, stream=False):
664 def _gitcommand(self, commands, env=None, stream=False):
667 return self._gitdir(commands, env=env, stream=stream)[0]
665 return self._gitdir(commands, env=env, stream=stream)[0]
668
666
669 def _gitdir(self, commands, env=None, stream=False):
667 def _gitdir(self, commands, env=None, stream=False):
670 return self._gitnodir(commands, env=env, stream=stream,
668 return self._gitnodir(commands, env=env, stream=stream,
671 cwd=self._abspath)
669 cwd=self._abspath)
672
670
673 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
671 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
674 """Calls the git command
672 """Calls the git command
675
673
676 The methods tries to call the git command. versions previor to 1.6.0
674 The methods tries to call the git command. versions previor to 1.6.0
677 are not supported and very probably fail.
675 are not supported and very probably fail.
678 """
676 """
679 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
677 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
680 # unless ui.quiet is set, print git's stderr,
678 # unless ui.quiet is set, print git's stderr,
681 # which is mostly progress and useful info
679 # which is mostly progress and useful info
682 errpipe = None
680 errpipe = None
683 if self._ui.quiet:
681 if self._ui.quiet:
684 errpipe = open(os.devnull, 'w')
682 errpipe = open(os.devnull, 'w')
685 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
683 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
686 close_fds=util.closefds,
684 close_fds=util.closefds,
687 stdout=subprocess.PIPE, stderr=errpipe)
685 stdout=subprocess.PIPE, stderr=errpipe)
688 if stream:
686 if stream:
689 return p.stdout, None
687 return p.stdout, None
690
688
691 retdata = p.stdout.read().strip()
689 retdata = p.stdout.read().strip()
692 # wait for the child to exit to avoid race condition.
690 # wait for the child to exit to avoid race condition.
693 p.wait()
691 p.wait()
694
692
695 if p.returncode != 0 and p.returncode != 1:
693 if p.returncode != 0 and p.returncode != 1:
696 # there are certain error codes that are ok
694 # there are certain error codes that are ok
697 command = commands[0]
695 command = commands[0]
698 if command in ('cat-file', 'symbolic-ref'):
696 if command in ('cat-file', 'symbolic-ref'):
699 return retdata, p.returncode
697 return retdata, p.returncode
700 # for all others, abort
698 # for all others, abort
701 raise util.Abort('git %s error %d in %s' %
699 raise util.Abort('git %s error %d in %s' %
702 (command, p.returncode, self._relpath))
700 (command, p.returncode, self._relpath))
703
701
704 return retdata, p.returncode
702 return retdata, p.returncode
705
703
706 def _gitmissing(self):
704 def _gitmissing(self):
707 return not os.path.exists(os.path.join(self._abspath, '.git'))
705 return not os.path.exists(os.path.join(self._abspath, '.git'))
708
706
709 def _gitstate(self):
707 def _gitstate(self):
710 return self._gitcommand(['rev-parse', 'HEAD'])
708 return self._gitcommand(['rev-parse', 'HEAD'])
711
709
712 def _gitcurrentbranch(self):
710 def _gitcurrentbranch(self):
713 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
711 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
714 if err:
712 if err:
715 current = None
713 current = None
716 return current
714 return current
717
715
718 def _gitremote(self, remote):
716 def _gitremote(self, remote):
719 out = self._gitcommand(['remote', 'show', '-n', remote])
717 out = self._gitcommand(['remote', 'show', '-n', remote])
720 line = out.split('\n')[1]
718 line = out.split('\n')[1]
721 i = line.index('URL: ') + len('URL: ')
719 i = line.index('URL: ') + len('URL: ')
722 return line[i:]
720 return line[i:]
723
721
724 def _githavelocally(self, revision):
722 def _githavelocally(self, revision):
725 out, code = self._gitdir(['cat-file', '-e', revision])
723 out, code = self._gitdir(['cat-file', '-e', revision])
726 return code == 0
724 return code == 0
727
725
728 def _gitisancestor(self, r1, r2):
726 def _gitisancestor(self, r1, r2):
729 base = self._gitcommand(['merge-base', r1, r2])
727 base = self._gitcommand(['merge-base', r1, r2])
730 return base == r1
728 return base == r1
731
729
732 def _gitbranchmap(self):
730 def _gitbranchmap(self):
733 '''returns 2 things:
731 '''returns 2 things:
734 a map from git branch to revision
732 a map from git branch to revision
735 a map from revision to branches'''
733 a map from revision to branches'''
736 branch2rev = {}
734 branch2rev = {}
737 rev2branch = {}
735 rev2branch = {}
738
736
739 out = self._gitcommand(['for-each-ref', '--format',
737 out = self._gitcommand(['for-each-ref', '--format',
740 '%(objectname) %(refname)'])
738 '%(objectname) %(refname)'])
741 for line in out.split('\n'):
739 for line in out.split('\n'):
742 revision, ref = line.split(' ')
740 revision, ref = line.split(' ')
743 if (not ref.startswith('refs/heads/') and
741 if (not ref.startswith('refs/heads/') and
744 not ref.startswith('refs/remotes/')):
742 not ref.startswith('refs/remotes/')):
745 continue
743 continue
746 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
744 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
747 continue # ignore remote/HEAD redirects
745 continue # ignore remote/HEAD redirects
748 branch2rev[ref] = revision
746 branch2rev[ref] = revision
749 rev2branch.setdefault(revision, []).append(ref)
747 rev2branch.setdefault(revision, []).append(ref)
750 return branch2rev, rev2branch
748 return branch2rev, rev2branch
751
749
752 def _gittracking(self, branches):
750 def _gittracking(self, branches):
753 'return map of remote branch to local tracking branch'
751 'return map of remote branch to local tracking branch'
754 # assumes no more than one local tracking branch for each remote
752 # assumes no more than one local tracking branch for each remote
755 tracking = {}
753 tracking = {}
756 for b in branches:
754 for b in branches:
757 if b.startswith('refs/remotes/'):
755 if b.startswith('refs/remotes/'):
758 continue
756 continue
759 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
757 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
760 if remote:
758 if remote:
761 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
759 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
762 tracking['refs/remotes/%s/%s' %
760 tracking['refs/remotes/%s/%s' %
763 (remote, ref.split('/', 2)[2])] = b
761 (remote, ref.split('/', 2)[2])] = b
764 return tracking
762 return tracking
765
763
766 def _abssource(self, source):
764 def _abssource(self, source):
767 if '://' not in source:
765 if '://' not in source:
768 # recognize the scp syntax as an absolute source
766 # recognize the scp syntax as an absolute source
769 colon = source.find(':')
767 colon = source.find(':')
770 if colon != -1 and '/' not in source[:colon]:
768 if colon != -1 and '/' not in source[:colon]:
771 return source
769 return source
772 self._subsource = source
770 self._subsource = source
773 return _abssource(self)
771 return _abssource(self)
774
772
775 def _fetch(self, source, revision):
773 def _fetch(self, source, revision):
776 if self._gitmissing():
774 if self._gitmissing():
777 source = self._abssource(source)
775 source = self._abssource(source)
778 self._ui.status(_('cloning subrepo %s from %s\n') %
776 self._ui.status(_('cloning subrepo %s from %s\n') %
779 (self._relpath, source))
777 (self._relpath, source))
780 self._gitnodir(['clone', source, self._abspath])
778 self._gitnodir(['clone', source, self._abspath])
781 if self._githavelocally(revision):
779 if self._githavelocally(revision):
782 return
780 return
783 self._ui.status(_('pulling subrepo %s from %s\n') %
781 self._ui.status(_('pulling subrepo %s from %s\n') %
784 (self._relpath, self._gitremote('origin')))
782 (self._relpath, self._gitremote('origin')))
785 # try only origin: the originally cloned repo
783 # try only origin: the originally cloned repo
786 self._gitcommand(['fetch'])
784 self._gitcommand(['fetch'])
787 if not self._githavelocally(revision):
785 if not self._githavelocally(revision):
788 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
786 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
789 (revision, self._relpath))
787 (revision, self._relpath))
790
788
791 def dirty(self, ignoreupdate=False):
789 def dirty(self, ignoreupdate=False):
792 if self._gitmissing():
790 if self._gitmissing():
793 return True
791 return True
794 if not ignoreupdate and self._state[1] != self._gitstate():
792 if not ignoreupdate and self._state[1] != self._gitstate():
795 # different version checked out
793 # different version checked out
796 return True
794 return True
797 # check for staged changes or modified files; ignore untracked files
795 # check for staged changes or modified files; ignore untracked files
798 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
796 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
799 return code == 1
797 return code == 1
800
798
801 def get(self, state, overwrite=False):
799 def get(self, state, overwrite=False):
802 source, revision, kind = state
800 source, revision, kind = state
803 self._fetch(source, revision)
801 self._fetch(source, revision)
804 # if the repo was set to be bare, unbare it
802 # if the repo was set to be bare, unbare it
805 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
803 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
806 self._gitcommand(['config', 'core.bare', 'false'])
804 self._gitcommand(['config', 'core.bare', 'false'])
807 if self._gitstate() == revision:
805 if self._gitstate() == revision:
808 self._gitcommand(['reset', '--hard', 'HEAD'])
806 self._gitcommand(['reset', '--hard', 'HEAD'])
809 return
807 return
810 elif self._gitstate() == revision:
808 elif self._gitstate() == revision:
811 if overwrite:
809 if overwrite:
812 # first reset the index to unmark new files for commit, because
810 # first reset the index to unmark new files for commit, because
813 # reset --hard will otherwise throw away files added for commit,
811 # reset --hard will otherwise throw away files added for commit,
814 # not just unmark them.
812 # not just unmark them.
815 self._gitcommand(['reset', 'HEAD'])
813 self._gitcommand(['reset', 'HEAD'])
816 self._gitcommand(['reset', '--hard', 'HEAD'])
814 self._gitcommand(['reset', '--hard', 'HEAD'])
817 return
815 return
818 branch2rev, rev2branch = self._gitbranchmap()
816 branch2rev, rev2branch = self._gitbranchmap()
819
817
820 def checkout(args):
818 def checkout(args):
821 cmd = ['checkout']
819 cmd = ['checkout']
822 if overwrite:
820 if overwrite:
823 # first reset the index to unmark new files for commit, because
821 # first reset the index to unmark new files for commit, because
824 # the -f option will otherwise throw away files added for
822 # the -f option will otherwise throw away files added for
825 # commit, not just unmark them.
823 # commit, not just unmark them.
826 self._gitcommand(['reset', 'HEAD'])
824 self._gitcommand(['reset', 'HEAD'])
827 cmd.append('-f')
825 cmd.append('-f')
828 self._gitcommand(cmd + args)
826 self._gitcommand(cmd + args)
829
827
830 def rawcheckout():
828 def rawcheckout():
831 # no branch to checkout, check it out with no branch
829 # no branch to checkout, check it out with no branch
832 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
830 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
833 self._relpath)
831 self._relpath)
834 self._ui.warn(_('check out a git branch if you intend '
832 self._ui.warn(_('check out a git branch if you intend '
835 'to make changes\n'))
833 'to make changes\n'))
836 checkout(['-q', revision])
834 checkout(['-q', revision])
837
835
838 if revision not in rev2branch:
836 if revision not in rev2branch:
839 rawcheckout()
837 rawcheckout()
840 return
838 return
841 branches = rev2branch[revision]
839 branches = rev2branch[revision]
842 firstlocalbranch = None
840 firstlocalbranch = None
843 for b in branches:
841 for b in branches:
844 if b == 'refs/heads/master':
842 if b == 'refs/heads/master':
845 # master trumps all other branches
843 # master trumps all other branches
846 checkout(['refs/heads/master'])
844 checkout(['refs/heads/master'])
847 return
845 return
848 if not firstlocalbranch and not b.startswith('refs/remotes/'):
846 if not firstlocalbranch and not b.startswith('refs/remotes/'):
849 firstlocalbranch = b
847 firstlocalbranch = b
850 if firstlocalbranch:
848 if firstlocalbranch:
851 checkout([firstlocalbranch])
849 checkout([firstlocalbranch])
852 return
850 return
853
851
854 tracking = self._gittracking(branch2rev.keys())
852 tracking = self._gittracking(branch2rev.keys())
855 # choose a remote branch already tracked if possible
853 # choose a remote branch already tracked if possible
856 remote = branches[0]
854 remote = branches[0]
857 if remote not in tracking:
855 if remote not in tracking:
858 for b in branches:
856 for b in branches:
859 if b in tracking:
857 if b in tracking:
860 remote = b
858 remote = b
861 break
859 break
862
860
863 if remote not in tracking:
861 if remote not in tracking:
864 # create a new local tracking branch
862 # create a new local tracking branch
865 local = remote.split('/', 2)[2]
863 local = remote.split('/', 2)[2]
866 checkout(['-b', local, remote])
864 checkout(['-b', local, remote])
867 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
865 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
868 # When updating to a tracked remote branch,
866 # When updating to a tracked remote branch,
869 # if the local tracking branch is downstream of it,
867 # if the local tracking branch is downstream of it,
870 # a normal `git pull` would have performed a "fast-forward merge"
868 # a normal `git pull` would have performed a "fast-forward merge"
871 # which is equivalent to updating the local branch to the remote.
869 # which is equivalent to updating the local branch to the remote.
872 # Since we are only looking at branching at update, we need to
870 # Since we are only looking at branching at update, we need to
873 # detect this situation and perform this action lazily.
871 # detect this situation and perform this action lazily.
874 if tracking[remote] != self._gitcurrentbranch():
872 if tracking[remote] != self._gitcurrentbranch():
875 checkout([tracking[remote]])
873 checkout([tracking[remote]])
876 self._gitcommand(['merge', '--ff', remote])
874 self._gitcommand(['merge', '--ff', remote])
877 else:
875 else:
878 # a real merge would be required, just checkout the revision
876 # a real merge would be required, just checkout the revision
879 rawcheckout()
877 rawcheckout()
880
878
881 def commit(self, text, user, date):
879 def commit(self, text, user, date):
882 if self._gitmissing():
880 if self._gitmissing():
883 raise util.Abort(_("subrepo %s is missing") % self._relpath)
881 raise util.Abort(_("subrepo %s is missing") % self._relpath)
884 cmd = ['commit', '-a', '-m', text]
882 cmd = ['commit', '-a', '-m', text]
885 env = os.environ.copy()
883 env = os.environ.copy()
886 if user:
884 if user:
887 cmd += ['--author', user]
885 cmd += ['--author', user]
888 if date:
886 if date:
889 # git's date parser silently ignores when seconds < 1e9
887 # git's date parser silently ignores when seconds < 1e9
890 # convert to ISO8601
888 # convert to ISO8601
891 env['GIT_AUTHOR_DATE'] = util.datestr(date,
889 env['GIT_AUTHOR_DATE'] = util.datestr(date,
892 '%Y-%m-%dT%H:%M:%S %1%2')
890 '%Y-%m-%dT%H:%M:%S %1%2')
893 self._gitcommand(cmd, env=env)
891 self._gitcommand(cmd, env=env)
894 # make sure commit works otherwise HEAD might not exist under certain
892 # make sure commit works otherwise HEAD might not exist under certain
895 # circumstances
893 # circumstances
896 return self._gitstate()
894 return self._gitstate()
897
895
898 def merge(self, state):
896 def merge(self, state):
899 source, revision, kind = state
897 source, revision, kind = state
900 self._fetch(source, revision)
898 self._fetch(source, revision)
901 base = self._gitcommand(['merge-base', revision, self._state[1]])
899 base = self._gitcommand(['merge-base', revision, self._state[1]])
902 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
900 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
903
901
904 def mergefunc():
902 def mergefunc():
905 if base == revision:
903 if base == revision:
906 self.get(state) # fast forward merge
904 self.get(state) # fast forward merge
907 elif base != self._state[1]:
905 elif base != self._state[1]:
908 self._gitcommand(['merge', '--no-commit', revision])
906 self._gitcommand(['merge', '--no-commit', revision])
909
907
910 if self.dirty():
908 if self.dirty():
911 if self._gitstate() != revision:
909 if self._gitstate() != revision:
912 dirty = self._gitstate() == self._state[1] or code != 0
910 dirty = self._gitstate() == self._state[1] or code != 0
913 if _updateprompt(self._ui, self, dirty,
911 if _updateprompt(self._ui, self, dirty,
914 self._state[1][:7], revision[:7]):
912 self._state[1][:7], revision[:7]):
915 mergefunc()
913 mergefunc()
916 else:
914 else:
917 mergefunc()
915 mergefunc()
918
916
919 def push(self, force):
917 def push(self, force):
920 if self._gitmissing():
918 if self._gitmissing():
921 raise util.Abort(_("subrepo %s is missing") % self._relpath)
919 raise util.Abort(_("subrepo %s is missing") % self._relpath)
922 # if a branch in origin contains the revision, nothing to do
920 # if a branch in origin contains the revision, nothing to do
923 branch2rev, rev2branch = self._gitbranchmap()
921 branch2rev, rev2branch = self._gitbranchmap()
924 if self._state[1] in rev2branch:
922 if self._state[1] in rev2branch:
925 for b in rev2branch[self._state[1]]:
923 for b in rev2branch[self._state[1]]:
926 if b.startswith('refs/remotes/origin/'):
924 if b.startswith('refs/remotes/origin/'):
927 return True
925 return True
928 for b, revision in branch2rev.iteritems():
926 for b, revision in branch2rev.iteritems():
929 if b.startswith('refs/remotes/origin/'):
927 if b.startswith('refs/remotes/origin/'):
930 if self._gitisancestor(self._state[1], revision):
928 if self._gitisancestor(self._state[1], revision):
931 return True
929 return True
932 # otherwise, try to push the currently checked out branch
930 # otherwise, try to push the currently checked out branch
933 cmd = ['push']
931 cmd = ['push']
934 if force:
932 if force:
935 cmd.append('--force')
933 cmd.append('--force')
936
934
937 current = self._gitcurrentbranch()
935 current = self._gitcurrentbranch()
938 if current:
936 if current:
939 # determine if the current branch is even useful
937 # determine if the current branch is even useful
940 if not self._gitisancestor(self._state[1], current):
938 if not self._gitisancestor(self._state[1], current):
941 self._ui.warn(_('unrelated git branch checked out '
939 self._ui.warn(_('unrelated git branch checked out '
942 'in subrepo %s\n') % self._relpath)
940 'in subrepo %s\n') % self._relpath)
943 return False
941 return False
944 self._ui.status(_('pushing branch %s of subrepo %s\n') %
942 self._ui.status(_('pushing branch %s of subrepo %s\n') %
945 (current.split('/', 2)[2], self._relpath))
943 (current.split('/', 2)[2], self._relpath))
946 self._gitcommand(cmd + ['origin', current])
944 self._gitcommand(cmd + ['origin', current])
947 return True
945 return True
948 else:
946 else:
949 self._ui.warn(_('no branch checked out in subrepo %s\n'
947 self._ui.warn(_('no branch checked out in subrepo %s\n'
950 'cannot push revision %s') %
948 'cannot push revision %s') %
951 (self._relpath, self._state[1]))
949 (self._relpath, self._state[1]))
952 return False
950 return False
953
951
954 def remove(self):
952 def remove(self):
955 if self._gitmissing():
953 if self._gitmissing():
956 return
954 return
957 if self.dirty():
955 if self.dirty():
958 self._ui.warn(_('not removing repo %s because '
956 self._ui.warn(_('not removing repo %s because '
959 'it has changes.\n') % self._relpath)
957 'it has changes.\n') % self._relpath)
960 return
958 return
961 # we can't fully delete the repository as it may contain
959 # we can't fully delete the repository as it may contain
962 # local-only history
960 # local-only history
963 self._ui.note(_('removing subrepo %s\n') % self._relpath)
961 self._ui.note(_('removing subrepo %s\n') % self._relpath)
964 self._gitcommand(['config', 'core.bare', 'true'])
962 self._gitcommand(['config', 'core.bare', 'true'])
965 for f in os.listdir(self._abspath):
963 for f in os.listdir(self._abspath):
966 if f == '.git':
964 if f == '.git':
967 continue
965 continue
968 path = os.path.join(self._abspath, f)
966 path = os.path.join(self._abspath, f)
969 if os.path.isdir(path) and not os.path.islink(path):
967 if os.path.isdir(path) and not os.path.islink(path):
970 shutil.rmtree(path)
968 shutil.rmtree(path)
971 else:
969 else:
972 os.remove(path)
970 os.remove(path)
973
971
974 def archive(self, ui, archiver, prefix):
972 def archive(self, ui, archiver, prefix):
975 source, revision = self._state
973 source, revision = self._state
976 self._fetch(source, revision)
974 self._fetch(source, revision)
977
975
978 # Parse git's native archive command.
976 # Parse git's native archive command.
979 # This should be much faster than manually traversing the trees
977 # This should be much faster than manually traversing the trees
980 # and objects with many subprocess calls.
978 # and objects with many subprocess calls.
981 tarstream = self._gitcommand(['archive', revision], stream=True)
979 tarstream = self._gitcommand(['archive', revision], stream=True)
982 tar = tarfile.open(fileobj=tarstream, mode='r|')
980 tar = tarfile.open(fileobj=tarstream, mode='r|')
983 relpath = subrelpath(self)
981 relpath = subrelpath(self)
984 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
982 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
985 for i, info in enumerate(tar):
983 for i, info in enumerate(tar):
986 if info.isdir():
984 if info.isdir():
987 continue
985 continue
988 if info.issym():
986 if info.issym():
989 data = info.linkname
987 data = info.linkname
990 else:
988 else:
991 data = tar.extractfile(info).read()
989 data = tar.extractfile(info).read()
992 archiver.addfile(os.path.join(prefix, self._path, info.name),
990 archiver.addfile(os.path.join(prefix, self._path, info.name),
993 info.mode, info.issym(), data)
991 info.mode, info.issym(), data)
994 ui.progress(_('archiving (%s)') % relpath, i + 1,
992 ui.progress(_('archiving (%s)') % relpath, i + 1,
995 unit=_('files'))
993 unit=_('files'))
996 ui.progress(_('archiving (%s)') % relpath, None)
994 ui.progress(_('archiving (%s)') % relpath, None)
997
995
998
996
999 def status(self, rev2, **opts):
997 def status(self, rev2, **opts):
1000 if self._gitmissing():
998 if self._gitmissing():
1001 # if the repo is missing, return no results
999 # if the repo is missing, return no results
1002 return [], [], [], [], [], [], []
1000 return [], [], [], [], [], [], []
1003 rev1 = self._state[1]
1001 rev1 = self._state[1]
1004 modified, added, removed = [], [], []
1002 modified, added, removed = [], [], []
1005 if rev2:
1003 if rev2:
1006 command = ['diff-tree', rev1, rev2]
1004 command = ['diff-tree', rev1, rev2]
1007 else:
1005 else:
1008 command = ['diff-index', rev1]
1006 command = ['diff-index', rev1]
1009 out = self._gitcommand(command)
1007 out = self._gitcommand(command)
1010 for line in out.split('\n'):
1008 for line in out.split('\n'):
1011 tab = line.find('\t')
1009 tab = line.find('\t')
1012 if tab == -1:
1010 if tab == -1:
1013 continue
1011 continue
1014 status, f = line[tab - 1], line[tab + 1:]
1012 status, f = line[tab - 1], line[tab + 1:]
1015 if status == 'M':
1013 if status == 'M':
1016 modified.append(f)
1014 modified.append(f)
1017 elif status == 'A':
1015 elif status == 'A':
1018 added.append(f)
1016 added.append(f)
1019 elif status == 'D':
1017 elif status == 'D':
1020 removed.append(f)
1018 removed.append(f)
1021
1019
1022 deleted = unknown = ignored = clean = []
1020 deleted = unknown = ignored = clean = []
1023 return modified, added, removed, deleted, unknown, ignored, clean
1021 return modified, added, removed, deleted, unknown, ignored, clean
1024
1022
1025 types = {
1023 types = {
1026 'hg': hgsubrepo,
1024 'hg': hgsubrepo,
1027 'svn': svnsubrepo,
1025 'svn': svnsubrepo,
1028 'git': gitsubrepo,
1026 'git': gitsubrepo,
1029 }
1027 }
General Comments 0
You need to be logged in to leave comments. Login now