##// END OF EJS Templates
merge default heads in crew and main
Martin Geisler -
r13126:e76701bf merge default
parent child Browse files
Show More
@@ -1,881 +1,883 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):
85 def submerge(repo, wctx, mctx, actx):
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)
117 wctx.sub(s).get(r)
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)
126 wctx.sub(s).get(r)
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)
130 wctx.sub(s).get(r)
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 reporelpath(repo):
166 def reporelpath(repo):
167 """return path to this (sub)repo as seen from outermost repo"""
167 """return path to this (sub)repo as seen from outermost repo"""
168 parent = repo
168 parent = repo
169 while hasattr(parent, '_subparent'):
169 while hasattr(parent, '_subparent'):
170 parent = parent._subparent
170 parent = parent._subparent
171 return repo.root[len(parent.root)+1:]
171 return repo.root[len(parent.root)+1:]
172
172
173 def subrelpath(sub):
173 def subrelpath(sub):
174 """return path to this subrepo as seen from outermost repo"""
174 """return path to this subrepo as seen from outermost repo"""
175 if not hasattr(sub, '_repo'):
175 if not hasattr(sub, '_repo'):
176 return sub._path
176 return sub._path
177 return reporelpath(sub._repo)
177 return reporelpath(sub._repo)
178
178
179 def _abssource(repo, push=False, abort=True):
179 def _abssource(repo, push=False, abort=True):
180 """return pull/push path of repo - either based on parent repo .hgsub info
180 """return pull/push path of repo - either based on parent repo .hgsub info
181 or on the top repo config. Abort or return None if no source found."""
181 or on the top repo config. Abort or return None if no source found."""
182 if hasattr(repo, '_subparent'):
182 if hasattr(repo, '_subparent'):
183 source = repo._subsource
183 source = repo._subsource
184 if source.startswith('/') or '://' in source:
184 if source.startswith('/') or '://' in source:
185 return source
185 return source
186 parent = _abssource(repo._subparent, push, abort=False)
186 parent = _abssource(repo._subparent, push, abort=False)
187 if parent:
187 if parent:
188 if '://' in parent:
188 if '://' in parent:
189 if parent[-1] == '/':
189 if parent[-1] == '/':
190 parent = parent[:-1]
190 parent = parent[:-1]
191 r = urlparse.urlparse(parent + '/' + source)
191 r = urlparse.urlparse(parent + '/' + source)
192 r = urlparse.urlunparse((r[0], r[1],
192 r = urlparse.urlunparse((r[0], r[1],
193 posixpath.normpath(r[2]),
193 posixpath.normpath(r[2]),
194 r[3], r[4], r[5]))
194 r[3], r[4], r[5]))
195 return r
195 return r
196 else: # plain file system path
196 else: # plain file system path
197 return posixpath.normpath(os.path.join(parent, repo._subsource))
197 return posixpath.normpath(os.path.join(parent, repo._subsource))
198 else: # recursion reached top repo
198 else: # recursion reached top repo
199 if hasattr(repo, '_subtoppath'):
199 if hasattr(repo, '_subtoppath'):
200 return repo._subtoppath
200 return repo._subtoppath
201 if push and repo.ui.config('paths', 'default-push'):
201 if push and repo.ui.config('paths', 'default-push'):
202 return repo.ui.config('paths', 'default-push')
202 return repo.ui.config('paths', 'default-push')
203 if repo.ui.config('paths', 'default'):
203 if repo.ui.config('paths', 'default'):
204 return repo.ui.config('paths', 'default')
204 return repo.ui.config('paths', 'default')
205 if abort:
205 if abort:
206 raise util.Abort(_("default path for subrepository %s not found") %
206 raise util.Abort(_("default path for subrepository %s not found") %
207 reporelpath(repo))
207 reporelpath(repo))
208
208
209 def itersubrepos(ctx1, ctx2):
209 def itersubrepos(ctx1, ctx2):
210 """find subrepos in ctx1 or ctx2"""
210 """find subrepos in ctx1 or ctx2"""
211 # Create a (subpath, ctx) mapping where we prefer subpaths from
211 # Create a (subpath, ctx) mapping where we prefer subpaths from
212 # ctx1. The subpaths from ctx2 are important when the .hgsub file
212 # ctx1. The subpaths from ctx2 are important when the .hgsub file
213 # has been modified (in ctx2) but not yet committed (in ctx1).
213 # has been modified (in ctx2) but not yet committed (in ctx1).
214 subpaths = dict.fromkeys(ctx2.substate, ctx2)
214 subpaths = dict.fromkeys(ctx2.substate, ctx2)
215 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
215 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
216 for subpath, ctx in sorted(subpaths.iteritems()):
216 for subpath, ctx in sorted(subpaths.iteritems()):
217 yield subpath, ctx.sub(subpath)
217 yield subpath, ctx.sub(subpath)
218
218
219 def subrepo(ctx, path):
219 def subrepo(ctx, path):
220 """return instance of the right subrepo class for subrepo in path"""
220 """return instance of the right subrepo class for subrepo in path"""
221 # subrepo inherently violates our import layering rules
221 # subrepo inherently violates our import layering rules
222 # because it wants to make repo objects from deep inside the stack
222 # because it wants to make repo objects from deep inside the stack
223 # so we manually delay the circular imports to not break
223 # so we manually delay the circular imports to not break
224 # scripts that don't use our demand-loading
224 # scripts that don't use our demand-loading
225 global hg
225 global hg
226 import hg as h
226 import hg as h
227 hg = h
227 hg = h
228
228
229 util.path_auditor(ctx._repo.root)(path)
229 util.path_auditor(ctx._repo.root)(path)
230 state = ctx.substate.get(path, nullstate)
230 state = ctx.substate.get(path, nullstate)
231 if state[2] not in types:
231 if state[2] not in types:
232 raise util.Abort(_('unknown subrepo type %s') % state[2])
232 raise util.Abort(_('unknown subrepo type %s') % state[2])
233 return types[state[2]](ctx, path, state[:2])
233 return types[state[2]](ctx, path, state[:2])
234
234
235 # subrepo classes need to implement the following abstract class:
235 # subrepo classes need to implement the following abstract class:
236
236
237 class abstractsubrepo(object):
237 class abstractsubrepo(object):
238
238
239 def dirty(self):
239 def dirty(self):
240 """returns true if the dirstate of the subrepo does not match
240 """returns true if the dirstate of the subrepo does not match
241 current stored state
241 current stored state
242 """
242 """
243 raise NotImplementedError
243 raise NotImplementedError
244
244
245 def checknested(self, path):
245 def checknested(self, path):
246 """check if path is a subrepository within this repository"""
246 """check if path is a subrepository within this repository"""
247 return False
247 return False
248
248
249 def commit(self, text, user, date):
249 def commit(self, text, user, date):
250 """commit the current changes to the subrepo with the given
250 """commit the current changes to the subrepo with the given
251 log message. Use given user and date if possible. Return the
251 log message. Use given user and date if possible. Return the
252 new state of the subrepo.
252 new state of the subrepo.
253 """
253 """
254 raise NotImplementedError
254 raise NotImplementedError
255
255
256 def remove(self):
256 def remove(self):
257 """remove the subrepo
257 """remove the subrepo
258
258
259 (should verify the dirstate is not dirty first)
259 (should verify the dirstate is not dirty first)
260 """
260 """
261 raise NotImplementedError
261 raise NotImplementedError
262
262
263 def get(self, state):
263 def get(self, state):
264 """run whatever commands are needed to put the subrepo into
264 """run whatever commands are needed to put the subrepo into
265 this state
265 this state
266 """
266 """
267 raise NotImplementedError
267 raise NotImplementedError
268
268
269 def merge(self, state):
269 def merge(self, state):
270 """merge currently-saved state with the new state."""
270 """merge currently-saved state with the new state."""
271 raise NotImplementedError
271 raise NotImplementedError
272
272
273 def push(self, force):
273 def push(self, force):
274 """perform whatever action is analogous to 'hg push'
274 """perform whatever action is analogous to 'hg push'
275
275
276 This may be a no-op on some systems.
276 This may be a no-op on some systems.
277 """
277 """
278 raise NotImplementedError
278 raise NotImplementedError
279
279
280 def add(self, ui, match, dryrun, prefix):
280 def add(self, ui, match, dryrun, prefix):
281 return []
281 return []
282
282
283 def status(self, rev2, **opts):
283 def status(self, rev2, **opts):
284 return [], [], [], [], [], [], []
284 return [], [], [], [], [], [], []
285
285
286 def diff(self, diffopts, node2, match, prefix, **opts):
286 def diff(self, diffopts, node2, match, prefix, **opts):
287 pass
287 pass
288
288
289 def outgoing(self, ui, dest, opts):
289 def outgoing(self, ui, dest, opts):
290 return 1
290 return 1
291
291
292 def incoming(self, ui, source, opts):
292 def incoming(self, ui, source, opts):
293 return 1
293 return 1
294
294
295 def files(self):
295 def files(self):
296 """return filename iterator"""
296 """return filename iterator"""
297 raise NotImplementedError
297 raise NotImplementedError
298
298
299 def filedata(self, name):
299 def filedata(self, name):
300 """return file data"""
300 """return file data"""
301 raise NotImplementedError
301 raise NotImplementedError
302
302
303 def fileflags(self, name):
303 def fileflags(self, name):
304 """return file flags"""
304 """return file flags"""
305 return ''
305 return ''
306
306
307 def archive(self, archiver, prefix):
307 def archive(self, archiver, prefix):
308 for name in self.files():
308 for name in self.files():
309 flags = self.fileflags(name)
309 flags = self.fileflags(name)
310 mode = 'x' in flags and 0755 or 0644
310 mode = 'x' in flags and 0755 or 0644
311 symlink = 'l' in flags
311 symlink = 'l' in flags
312 archiver.addfile(os.path.join(prefix, self._path, name),
312 archiver.addfile(os.path.join(prefix, self._path, name),
313 mode, symlink, self.filedata(name))
313 mode, symlink, self.filedata(name))
314
314
315
315
316 class hgsubrepo(abstractsubrepo):
316 class hgsubrepo(abstractsubrepo):
317 def __init__(self, ctx, path, state):
317 def __init__(self, ctx, path, state):
318 self._path = path
318 self._path = path
319 self._state = state
319 self._state = state
320 r = ctx._repo
320 r = ctx._repo
321 root = r.wjoin(path)
321 root = r.wjoin(path)
322 create = False
322 create = False
323 if not os.path.exists(os.path.join(root, '.hg')):
323 if not os.path.exists(os.path.join(root, '.hg')):
324 create = True
324 create = True
325 util.makedirs(root)
325 util.makedirs(root)
326 self._repo = hg.repository(r.ui, root, create=create)
326 self._repo = hg.repository(r.ui, root, create=create)
327 self._repo._subparent = r
327 self._repo._subparent = r
328 self._repo._subsource = state[0]
328 self._repo._subsource = state[0]
329
329
330 if create:
330 if create:
331 fp = self._repo.opener("hgrc", "w", text=True)
331 fp = self._repo.opener("hgrc", "w", text=True)
332 fp.write('[paths]\n')
332 fp.write('[paths]\n')
333
333
334 def addpathconfig(key, value):
334 def addpathconfig(key, value):
335 if value:
335 if value:
336 if not os.path.isabs(value):
337 value = os.path.relpath(os.path.abspath(value), root)
336 fp.write('%s = %s\n' % (key, value))
338 fp.write('%s = %s\n' % (key, value))
337 self._repo.ui.setconfig('paths', key, value)
339 self._repo.ui.setconfig('paths', key, value)
338
340
339 defpath = _abssource(self._repo, abort=False)
341 defpath = _abssource(self._repo, abort=False)
340 defpushpath = _abssource(self._repo, True, abort=False)
342 defpushpath = _abssource(self._repo, True, abort=False)
341 addpathconfig('default', defpath)
343 addpathconfig('default', defpath)
342 if defpath != defpushpath:
344 if defpath != defpushpath:
343 addpathconfig('default-push', defpushpath)
345 addpathconfig('default-push', defpushpath)
344 fp.close()
346 fp.close()
345
347
346 def add(self, ui, match, dryrun, prefix):
348 def add(self, ui, match, dryrun, prefix):
347 return cmdutil.add(ui, self._repo, match, dryrun, True,
349 return cmdutil.add(ui, self._repo, match, dryrun, True,
348 os.path.join(prefix, self._path))
350 os.path.join(prefix, self._path))
349
351
350 def status(self, rev2, **opts):
352 def status(self, rev2, **opts):
351 try:
353 try:
352 rev1 = self._state[1]
354 rev1 = self._state[1]
353 ctx1 = self._repo[rev1]
355 ctx1 = self._repo[rev1]
354 ctx2 = self._repo[rev2]
356 ctx2 = self._repo[rev2]
355 return self._repo.status(ctx1, ctx2, **opts)
357 return self._repo.status(ctx1, ctx2, **opts)
356 except error.RepoLookupError, inst:
358 except error.RepoLookupError, inst:
357 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
359 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
358 % (inst, subrelpath(self)))
360 % (inst, subrelpath(self)))
359 return [], [], [], [], [], [], []
361 return [], [], [], [], [], [], []
360
362
361 def diff(self, diffopts, node2, match, prefix, **opts):
363 def diff(self, diffopts, node2, match, prefix, **opts):
362 try:
364 try:
363 node1 = node.bin(self._state[1])
365 node1 = node.bin(self._state[1])
364 # We currently expect node2 to come from substate and be
366 # We currently expect node2 to come from substate and be
365 # in hex format
367 # in hex format
366 if node2 is not None:
368 if node2 is not None:
367 node2 = node.bin(node2)
369 node2 = node.bin(node2)
368 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
370 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
369 node1, node2, match,
371 node1, node2, match,
370 prefix=os.path.join(prefix, self._path),
372 prefix=os.path.join(prefix, self._path),
371 listsubrepos=True, **opts)
373 listsubrepos=True, **opts)
372 except error.RepoLookupError, inst:
374 except error.RepoLookupError, inst:
373 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
375 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
374 % (inst, subrelpath(self)))
376 % (inst, subrelpath(self)))
375
377
376 def archive(self, archiver, prefix):
378 def archive(self, archiver, prefix):
377 abstractsubrepo.archive(self, archiver, prefix)
379 abstractsubrepo.archive(self, archiver, prefix)
378
380
379 rev = self._state[1]
381 rev = self._state[1]
380 ctx = self._repo[rev]
382 ctx = self._repo[rev]
381 for subpath in ctx.substate:
383 for subpath in ctx.substate:
382 s = subrepo(ctx, subpath)
384 s = subrepo(ctx, subpath)
383 s.archive(archiver, os.path.join(prefix, self._path))
385 s.archive(archiver, os.path.join(prefix, self._path))
384
386
385 def dirty(self):
387 def dirty(self):
386 r = self._state[1]
388 r = self._state[1]
387 if r == '':
389 if r == '':
388 return True
390 return True
389 w = self._repo[None]
391 w = self._repo[None]
390 if w.p1() != self._repo[r]: # version checked out change
392 if w.p1() != self._repo[r]: # version checked out change
391 return True
393 return True
392 return w.dirty() # working directory changed
394 return w.dirty() # working directory changed
393
395
394 def checknested(self, path):
396 def checknested(self, path):
395 return self._repo._checknested(self._repo.wjoin(path))
397 return self._repo._checknested(self._repo.wjoin(path))
396
398
397 def commit(self, text, user, date):
399 def commit(self, text, user, date):
398 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
400 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
399 n = self._repo.commit(text, user, date)
401 n = self._repo.commit(text, user, date)
400 if not n:
402 if not n:
401 return self._repo['.'].hex() # different version checked out
403 return self._repo['.'].hex() # different version checked out
402 return node.hex(n)
404 return node.hex(n)
403
405
404 def remove(self):
406 def remove(self):
405 # we can't fully delete the repository as it may contain
407 # we can't fully delete the repository as it may contain
406 # local-only history
408 # local-only history
407 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
409 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
408 hg.clean(self._repo, node.nullid, False)
410 hg.clean(self._repo, node.nullid, False)
409
411
410 def _get(self, state):
412 def _get(self, state):
411 source, revision, kind = state
413 source, revision, kind = state
412 try:
414 try:
413 self._repo.lookup(revision)
415 self._repo.lookup(revision)
414 except error.RepoError:
416 except error.RepoError:
415 self._repo._subsource = source
417 self._repo._subsource = source
416 srcurl = _abssource(self._repo)
418 srcurl = _abssource(self._repo)
417 self._repo.ui.status(_('pulling subrepo %s from %s\n')
419 self._repo.ui.status(_('pulling subrepo %s from %s\n')
418 % (subrelpath(self), srcurl))
420 % (subrelpath(self), srcurl))
419 other = hg.repository(self._repo.ui, srcurl)
421 other = hg.repository(self._repo.ui, srcurl)
420 self._repo.pull(other)
422 self._repo.pull(other)
421
423
422 def get(self, state):
424 def get(self, state):
423 self._get(state)
425 self._get(state)
424 source, revision, kind = state
426 source, revision, kind = state
425 self._repo.ui.debug("getting subrepo %s\n" % self._path)
427 self._repo.ui.debug("getting subrepo %s\n" % self._path)
426 hg.clean(self._repo, revision, False)
428 hg.clean(self._repo, revision, False)
427
429
428 def merge(self, state):
430 def merge(self, state):
429 self._get(state)
431 self._get(state)
430 cur = self._repo['.']
432 cur = self._repo['.']
431 dst = self._repo[state[1]]
433 dst = self._repo[state[1]]
432 anc = dst.ancestor(cur)
434 anc = dst.ancestor(cur)
433 if anc == cur:
435 if anc == cur:
434 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
436 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
435 hg.update(self._repo, state[1])
437 hg.update(self._repo, state[1])
436 elif anc == dst:
438 elif anc == dst:
437 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
439 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
438 else:
440 else:
439 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
441 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
440 hg.merge(self._repo, state[1], remind=False)
442 hg.merge(self._repo, state[1], remind=False)
441
443
442 def push(self, force):
444 def push(self, force):
443 # push subrepos depth-first for coherent ordering
445 # push subrepos depth-first for coherent ordering
444 c = self._repo['']
446 c = self._repo['']
445 subs = c.substate # only repos that are committed
447 subs = c.substate # only repos that are committed
446 for s in sorted(subs):
448 for s in sorted(subs):
447 if not c.sub(s).push(force):
449 if not c.sub(s).push(force):
448 return False
450 return False
449
451
450 dsturl = _abssource(self._repo, True)
452 dsturl = _abssource(self._repo, True)
451 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
453 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
452 (subrelpath(self), dsturl))
454 (subrelpath(self), dsturl))
453 other = hg.repository(self._repo.ui, dsturl)
455 other = hg.repository(self._repo.ui, dsturl)
454 return self._repo.push(other, force)
456 return self._repo.push(other, force)
455
457
456 def outgoing(self, ui, dest, opts):
458 def outgoing(self, ui, dest, opts):
457 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
459 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
458
460
459 def incoming(self, ui, source, opts):
461 def incoming(self, ui, source, opts):
460 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
462 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
461
463
462 def files(self):
464 def files(self):
463 rev = self._state[1]
465 rev = self._state[1]
464 ctx = self._repo[rev]
466 ctx = self._repo[rev]
465 return ctx.manifest()
467 return ctx.manifest()
466
468
467 def filedata(self, name):
469 def filedata(self, name):
468 rev = self._state[1]
470 rev = self._state[1]
469 return self._repo[rev][name].data()
471 return self._repo[rev][name].data()
470
472
471 def fileflags(self, name):
473 def fileflags(self, name):
472 rev = self._state[1]
474 rev = self._state[1]
473 ctx = self._repo[rev]
475 ctx = self._repo[rev]
474 return ctx.flags(name)
476 return ctx.flags(name)
475
477
476
478
477 class svnsubrepo(abstractsubrepo):
479 class svnsubrepo(abstractsubrepo):
478 def __init__(self, ctx, path, state):
480 def __init__(self, ctx, path, state):
479 self._path = path
481 self._path = path
480 self._state = state
482 self._state = state
481 self._ctx = ctx
483 self._ctx = ctx
482 self._ui = ctx._repo.ui
484 self._ui = ctx._repo.ui
483
485
484 def _svncommand(self, commands, filename=''):
486 def _svncommand(self, commands, filename=''):
485 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
487 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
486 cmd = ['svn'] + commands + [path]
488 cmd = ['svn'] + commands + [path]
487 env = dict(os.environ)
489 env = dict(os.environ)
488 # Avoid localized output, preserve current locale for everything else.
490 # Avoid localized output, preserve current locale for everything else.
489 env['LC_MESSAGES'] = 'C'
491 env['LC_MESSAGES'] = 'C'
490 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
492 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
491 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
493 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
492 universal_newlines=True, env=env)
494 universal_newlines=True, env=env)
493 stdout, stderr = p.communicate()
495 stdout, stderr = p.communicate()
494 stderr = stderr.strip()
496 stderr = stderr.strip()
495 if stderr:
497 if stderr:
496 raise util.Abort(stderr)
498 raise util.Abort(stderr)
497 return stdout
499 return stdout
498
500
499 def _wcrev(self):
501 def _wcrev(self):
500 output = self._svncommand(['info', '--xml'])
502 output = self._svncommand(['info', '--xml'])
501 doc = xml.dom.minidom.parseString(output)
503 doc = xml.dom.minidom.parseString(output)
502 entries = doc.getElementsByTagName('entry')
504 entries = doc.getElementsByTagName('entry')
503 if not entries:
505 if not entries:
504 return '0'
506 return '0'
505 return str(entries[0].getAttribute('revision')) or '0'
507 return str(entries[0].getAttribute('revision')) or '0'
506
508
507 def _wcchanged(self):
509 def _wcchanged(self):
508 """Return (changes, extchanges) where changes is True
510 """Return (changes, extchanges) where changes is True
509 if the working directory was changed, and extchanges is
511 if the working directory was changed, and extchanges is
510 True if any of these changes concern an external entry.
512 True if any of these changes concern an external entry.
511 """
513 """
512 output = self._svncommand(['status', '--xml'])
514 output = self._svncommand(['status', '--xml'])
513 externals, changes = [], []
515 externals, changes = [], []
514 doc = xml.dom.minidom.parseString(output)
516 doc = xml.dom.minidom.parseString(output)
515 for e in doc.getElementsByTagName('entry'):
517 for e in doc.getElementsByTagName('entry'):
516 s = e.getElementsByTagName('wc-status')
518 s = e.getElementsByTagName('wc-status')
517 if not s:
519 if not s:
518 continue
520 continue
519 item = s[0].getAttribute('item')
521 item = s[0].getAttribute('item')
520 props = s[0].getAttribute('props')
522 props = s[0].getAttribute('props')
521 path = e.getAttribute('path')
523 path = e.getAttribute('path')
522 if item == 'external':
524 if item == 'external':
523 externals.append(path)
525 externals.append(path)
524 if (item not in ('', 'normal', 'unversioned', 'external')
526 if (item not in ('', 'normal', 'unversioned', 'external')
525 or props not in ('', 'none')):
527 or props not in ('', 'none')):
526 changes.append(path)
528 changes.append(path)
527 for path in changes:
529 for path in changes:
528 for ext in externals:
530 for ext in externals:
529 if path == ext or path.startswith(ext + os.sep):
531 if path == ext or path.startswith(ext + os.sep):
530 return True, True
532 return True, True
531 return bool(changes), False
533 return bool(changes), False
532
534
533 def dirty(self):
535 def dirty(self):
534 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
536 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
535 return False
537 return False
536 return True
538 return True
537
539
538 def commit(self, text, user, date):
540 def commit(self, text, user, date):
539 # user and date are out of our hands since svn is centralized
541 # user and date are out of our hands since svn is centralized
540 changed, extchanged = self._wcchanged()
542 changed, extchanged = self._wcchanged()
541 if not changed:
543 if not changed:
542 return self._wcrev()
544 return self._wcrev()
543 if extchanged:
545 if extchanged:
544 # Do not try to commit externals
546 # Do not try to commit externals
545 raise util.Abort(_('cannot commit svn externals'))
547 raise util.Abort(_('cannot commit svn externals'))
546 commitinfo = self._svncommand(['commit', '-m', text])
548 commitinfo = self._svncommand(['commit', '-m', text])
547 self._ui.status(commitinfo)
549 self._ui.status(commitinfo)
548 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
550 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
549 if not newrev:
551 if not newrev:
550 raise util.Abort(commitinfo.splitlines()[-1])
552 raise util.Abort(commitinfo.splitlines()[-1])
551 newrev = newrev.groups()[0]
553 newrev = newrev.groups()[0]
552 self._ui.status(self._svncommand(['update', '-r', newrev]))
554 self._ui.status(self._svncommand(['update', '-r', newrev]))
553 return newrev
555 return newrev
554
556
555 def remove(self):
557 def remove(self):
556 if self.dirty():
558 if self.dirty():
557 self._ui.warn(_('not removing repo %s because '
559 self._ui.warn(_('not removing repo %s because '
558 'it has changes.\n' % self._path))
560 'it has changes.\n' % self._path))
559 return
561 return
560 self._ui.note(_('removing subrepo %s\n') % self._path)
562 self._ui.note(_('removing subrepo %s\n') % self._path)
561
563
562 def onerror(function, path, excinfo):
564 def onerror(function, path, excinfo):
563 if function is not os.remove:
565 if function is not os.remove:
564 raise
566 raise
565 # read-only files cannot be unlinked under Windows
567 # read-only files cannot be unlinked under Windows
566 s = os.stat(path)
568 s = os.stat(path)
567 if (s.st_mode & stat.S_IWRITE) != 0:
569 if (s.st_mode & stat.S_IWRITE) != 0:
568 raise
570 raise
569 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
571 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
570 os.remove(path)
572 os.remove(path)
571
573
572 path = self._ctx._repo.wjoin(self._path)
574 path = self._ctx._repo.wjoin(self._path)
573 shutil.rmtree(path, onerror=onerror)
575 shutil.rmtree(path, onerror=onerror)
574 try:
576 try:
575 os.removedirs(os.path.dirname(path))
577 os.removedirs(os.path.dirname(path))
576 except OSError:
578 except OSError:
577 pass
579 pass
578
580
579 def get(self, state):
581 def get(self, state):
580 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
582 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
581 if not re.search('Checked out revision [0-9]+.', status):
583 if not re.search('Checked out revision [0-9]+.', status):
582 raise util.Abort(status.splitlines()[-1])
584 raise util.Abort(status.splitlines()[-1])
583 self._ui.status(status)
585 self._ui.status(status)
584
586
585 def merge(self, state):
587 def merge(self, state):
586 old = int(self._state[1])
588 old = int(self._state[1])
587 new = int(state[1])
589 new = int(state[1])
588 if new > old:
590 if new > old:
589 self.get(state)
591 self.get(state)
590
592
591 def push(self, force):
593 def push(self, force):
592 # push is a no-op for SVN
594 # push is a no-op for SVN
593 return True
595 return True
594
596
595 def files(self):
597 def files(self):
596 output = self._svncommand(['list'])
598 output = self._svncommand(['list'])
597 # This works because svn forbids \n in filenames.
599 # This works because svn forbids \n in filenames.
598 return output.splitlines()
600 return output.splitlines()
599
601
600 def filedata(self, name):
602 def filedata(self, name):
601 return self._svncommand(['cat'], name)
603 return self._svncommand(['cat'], name)
602
604
603
605
604 class gitsubrepo(abstractsubrepo):
606 class gitsubrepo(abstractsubrepo):
605 def __init__(self, ctx, path, state):
607 def __init__(self, ctx, path, state):
606 # TODO add git version check.
608 # TODO add git version check.
607 self._state = state
609 self._state = state
608 self._ctx = ctx
610 self._ctx = ctx
609 self._relpath = path
611 self._relpath = path
610 self._path = ctx._repo.wjoin(path)
612 self._path = ctx._repo.wjoin(path)
611 self._ui = ctx._repo.ui
613 self._ui = ctx._repo.ui
612
614
613 def _gitcommand(self, commands, env=None, stream=False):
615 def _gitcommand(self, commands, env=None, stream=False):
614 return self._gitdir(commands, env=env, stream=stream)[0]
616 return self._gitdir(commands, env=env, stream=stream)[0]
615
617
616 def _gitdir(self, commands, env=None, stream=False):
618 def _gitdir(self, commands, env=None, stream=False):
617 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
619 return self._gitnodir(commands, env=env, stream=stream, cwd=self._path)
618
620
619 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
621 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
620 """Calls the git command
622 """Calls the git command
621
623
622 The methods tries to call the git command. versions previor to 1.6.0
624 The methods tries to call the git command. versions previor to 1.6.0
623 are not supported and very probably fail.
625 are not supported and very probably fail.
624 """
626 """
625 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
627 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
626 # unless ui.quiet is set, print git's stderr,
628 # unless ui.quiet is set, print git's stderr,
627 # which is mostly progress and useful info
629 # which is mostly progress and useful info
628 errpipe = None
630 errpipe = None
629 if self._ui.quiet:
631 if self._ui.quiet:
630 errpipe = open(os.devnull, 'w')
632 errpipe = open(os.devnull, 'w')
631 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
633 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
632 close_fds=util.closefds,
634 close_fds=util.closefds,
633 stdout=subprocess.PIPE, stderr=errpipe)
635 stdout=subprocess.PIPE, stderr=errpipe)
634 if stream:
636 if stream:
635 return p.stdout, None
637 return p.stdout, None
636
638
637 retdata = p.stdout.read().strip()
639 retdata = p.stdout.read().strip()
638 # wait for the child to exit to avoid race condition.
640 # wait for the child to exit to avoid race condition.
639 p.wait()
641 p.wait()
640
642
641 if p.returncode != 0 and p.returncode != 1:
643 if p.returncode != 0 and p.returncode != 1:
642 # there are certain error codes that are ok
644 # there are certain error codes that are ok
643 command = commands[0]
645 command = commands[0]
644 if command == 'cat-file':
646 if command == 'cat-file':
645 return retdata, p.returncode
647 return retdata, p.returncode
646 # for all others, abort
648 # for all others, abort
647 raise util.Abort('git %s error %d in %s' %
649 raise util.Abort('git %s error %d in %s' %
648 (command, p.returncode, self._relpath))
650 (command, p.returncode, self._relpath))
649
651
650 return retdata, p.returncode
652 return retdata, p.returncode
651
653
652 def _gitstate(self):
654 def _gitstate(self):
653 return self._gitcommand(['rev-parse', 'HEAD'])
655 return self._gitcommand(['rev-parse', 'HEAD'])
654
656
655 def _githavelocally(self, revision):
657 def _githavelocally(self, revision):
656 out, code = self._gitdir(['cat-file', '-e', revision])
658 out, code = self._gitdir(['cat-file', '-e', revision])
657 return code == 0
659 return code == 0
658
660
659 def _gitisancestor(self, r1, r2):
661 def _gitisancestor(self, r1, r2):
660 base = self._gitcommand(['merge-base', r1, r2])
662 base = self._gitcommand(['merge-base', r1, r2])
661 return base == r1
663 return base == r1
662
664
663 def _gitbranchmap(self):
665 def _gitbranchmap(self):
664 '''returns 3 things:
666 '''returns 3 things:
665 the current branch,
667 the current branch,
666 a map from git branch to revision
668 a map from git branch to revision
667 a map from revision to branches'''
669 a map from revision to branches'''
668 branch2rev = {}
670 branch2rev = {}
669 rev2branch = {}
671 rev2branch = {}
670 current = None
672 current = None
671 out = self._gitcommand(['branch', '-a', '--no-color',
673 out = self._gitcommand(['branch', '-a', '--no-color',
672 '--verbose', '--no-abbrev'])
674 '--verbose', '--no-abbrev'])
673 for line in out.split('\n'):
675 for line in out.split('\n'):
674 if line[2:].startswith('(no branch)'):
676 if line[2:].startswith('(no branch)'):
675 continue
677 continue
676 branch, revision = line[2:].split()[:2]
678 branch, revision = line[2:].split()[:2]
677 if revision == '->' or branch.endswith('/HEAD'):
679 if revision == '->' or branch.endswith('/HEAD'):
678 continue # ignore remote/HEAD redirects
680 continue # ignore remote/HEAD redirects
679 if '/' in branch and not branch.startswith('remotes/'):
681 if '/' in branch and not branch.startswith('remotes/'):
680 # old git compatability
682 # old git compatability
681 branch = 'remotes/' + branch
683 branch = 'remotes/' + branch
682 if line[0] == '*':
684 if line[0] == '*':
683 current = branch
685 current = branch
684 branch2rev[branch] = revision
686 branch2rev[branch] = revision
685 rev2branch.setdefault(revision, []).append(branch)
687 rev2branch.setdefault(revision, []).append(branch)
686 return current, branch2rev, rev2branch
688 return current, branch2rev, rev2branch
687
689
688 def _gittracking(self, branches):
690 def _gittracking(self, branches):
689 'return map of remote branch to local tracking branch'
691 'return map of remote branch to local tracking branch'
690 # assumes no more than one local tracking branch for each remote
692 # assumes no more than one local tracking branch for each remote
691 tracking = {}
693 tracking = {}
692 for b in branches:
694 for b in branches:
693 if b.startswith('remotes/'):
695 if b.startswith('remotes/'):
694 continue
696 continue
695 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
697 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
696 if remote:
698 if remote:
697 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
699 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
698 tracking['remotes/%s/%s' % (remote, ref.split('/')[-1])] = b
700 tracking['remotes/%s/%s' % (remote, ref.split('/')[-1])] = b
699 return tracking
701 return tracking
700
702
701 def _fetch(self, source, revision):
703 def _fetch(self, source, revision):
702 if not os.path.exists('%s/.git' % self._path):
704 if not os.path.exists('%s/.git' % self._path):
703 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
705 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
704 self._gitnodir(['clone', source, self._path])
706 self._gitnodir(['clone', source, self._path])
705 if self._githavelocally(revision):
707 if self._githavelocally(revision):
706 return
708 return
707 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
709 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
708 # first try from origin
710 # first try from origin
709 self._gitcommand(['fetch'])
711 self._gitcommand(['fetch'])
710 if self._githavelocally(revision):
712 if self._githavelocally(revision):
711 return
713 return
712 # then try from known subrepo source
714 # then try from known subrepo source
713 self._gitcommand(['fetch', source])
715 self._gitcommand(['fetch', source])
714 if not self._githavelocally(revision):
716 if not self._githavelocally(revision):
715 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
717 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
716 (revision, self._path))
718 (revision, self._path))
717
719
718 def dirty(self):
720 def dirty(self):
719 if self._state[1] != self._gitstate(): # version checked out changed?
721 if self._state[1] != self._gitstate(): # version checked out changed?
720 return True
722 return True
721 # check for staged changes or modified files; ignore untracked files
723 # check for staged changes or modified files; ignore untracked files
722 status = self._gitcommand(['status'])
724 status = self._gitcommand(['status'])
723 return ('\n# Changed but not updated:' in status or
725 return ('\n# Changed but not updated:' in status or
724 '\n# Changes to be committed:' in status)
726 '\n# Changes to be committed:' in status)
725
727
726 def get(self, state):
728 def get(self, state):
727 source, revision, kind = state
729 source, revision, kind = state
728 self._fetch(source, revision)
730 self._fetch(source, revision)
729 # if the repo was set to be bare, unbare it
731 # if the repo was set to be bare, unbare it
730 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
732 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
731 self._gitcommand(['config', 'core.bare', 'false'])
733 self._gitcommand(['config', 'core.bare', 'false'])
732 if self._gitstate() == revision:
734 if self._gitstate() == revision:
733 self._gitcommand(['reset', '--hard', 'HEAD'])
735 self._gitcommand(['reset', '--hard', 'HEAD'])
734 return
736 return
735 elif self._gitstate() == revision:
737 elif self._gitstate() == revision:
736 return
738 return
737 current, branch2rev, rev2branch = self._gitbranchmap()
739 current, branch2rev, rev2branch = self._gitbranchmap()
738
740
739 def rawcheckout():
741 def rawcheckout():
740 # no branch to checkout, check it out with no branch
742 # no branch to checkout, check it out with no branch
741 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
743 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
742 self._relpath)
744 self._relpath)
743 self._ui.warn(_('check out a git branch if you intend '
745 self._ui.warn(_('check out a git branch if you intend '
744 'to make changes\n'))
746 'to make changes\n'))
745 self._gitcommand(['checkout', '-q', revision])
747 self._gitcommand(['checkout', '-q', revision])
746
748
747 if revision not in rev2branch:
749 if revision not in rev2branch:
748 rawcheckout()
750 rawcheckout()
749 return
751 return
750 branches = rev2branch[revision]
752 branches = rev2branch[revision]
751 firstlocalbranch = None
753 firstlocalbranch = None
752 for b in branches:
754 for b in branches:
753 if b == 'master':
755 if b == 'master':
754 # master trumps all other branches
756 # master trumps all other branches
755 self._gitcommand(['checkout', 'master'])
757 self._gitcommand(['checkout', 'master'])
756 return
758 return
757 if not firstlocalbranch and not b.startswith('remotes/'):
759 if not firstlocalbranch and not b.startswith('remotes/'):
758 firstlocalbranch = b
760 firstlocalbranch = b
759 if firstlocalbranch:
761 if firstlocalbranch:
760 self._gitcommand(['checkout', firstlocalbranch])
762 self._gitcommand(['checkout', firstlocalbranch])
761 return
763 return
762
764
763 tracking = self._gittracking(branch2rev.keys())
765 tracking = self._gittracking(branch2rev.keys())
764 # choose a remote branch already tracked if possible
766 # choose a remote branch already tracked if possible
765 remote = branches[0]
767 remote = branches[0]
766 if remote not in tracking:
768 if remote not in tracking:
767 for b in branches:
769 for b in branches:
768 if b in tracking:
770 if b in tracking:
769 remote = b
771 remote = b
770 break
772 break
771
773
772 if remote not in tracking:
774 if remote not in tracking:
773 # create a new local tracking branch
775 # create a new local tracking branch
774 local = remote.split('/')[-1]
776 local = remote.split('/')[-1]
775 self._gitcommand(['checkout', '-b', local, remote])
777 self._gitcommand(['checkout', '-b', local, remote])
776 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
778 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
777 # When updating to a tracked remote branch,
779 # When updating to a tracked remote branch,
778 # if the local tracking branch is downstream of it,
780 # if the local tracking branch is downstream of it,
779 # a normal `git pull` would have performed a "fast-forward merge"
781 # a normal `git pull` would have performed a "fast-forward merge"
780 # which is equivalent to updating the local branch to the remote.
782 # which is equivalent to updating the local branch to the remote.
781 # Since we are only looking at branching at update, we need to
783 # Since we are only looking at branching at update, we need to
782 # detect this situation and perform this action lazily.
784 # detect this situation and perform this action lazily.
783 if tracking[remote] != current:
785 if tracking[remote] != current:
784 self._gitcommand(['checkout', tracking[remote]])
786 self._gitcommand(['checkout', tracking[remote]])
785 self._gitcommand(['merge', '--ff', remote])
787 self._gitcommand(['merge', '--ff', remote])
786 else:
788 else:
787 # a real merge would be required, just checkout the revision
789 # a real merge would be required, just checkout the revision
788 rawcheckout()
790 rawcheckout()
789
791
790 def commit(self, text, user, date):
792 def commit(self, text, user, date):
791 cmd = ['commit', '-a', '-m', text]
793 cmd = ['commit', '-a', '-m', text]
792 env = os.environ.copy()
794 env = os.environ.copy()
793 if user:
795 if user:
794 cmd += ['--author', user]
796 cmd += ['--author', user]
795 if date:
797 if date:
796 # git's date parser silently ignores when seconds < 1e9
798 # git's date parser silently ignores when seconds < 1e9
797 # convert to ISO8601
799 # convert to ISO8601
798 env['GIT_AUTHOR_DATE'] = util.datestr(date,
800 env['GIT_AUTHOR_DATE'] = util.datestr(date,
799 '%Y-%m-%dT%H:%M:%S %1%2')
801 '%Y-%m-%dT%H:%M:%S %1%2')
800 self._gitcommand(cmd, env=env)
802 self._gitcommand(cmd, env=env)
801 # make sure commit works otherwise HEAD might not exist under certain
803 # make sure commit works otherwise HEAD might not exist under certain
802 # circumstances
804 # circumstances
803 return self._gitstate()
805 return self._gitstate()
804
806
805 def merge(self, state):
807 def merge(self, state):
806 source, revision, kind = state
808 source, revision, kind = state
807 self._fetch(source, revision)
809 self._fetch(source, revision)
808 base = self._gitcommand(['merge-base', revision, self._state[1]])
810 base = self._gitcommand(['merge-base', revision, self._state[1]])
809 if base == revision:
811 if base == revision:
810 self.get(state) # fast forward merge
812 self.get(state) # fast forward merge
811 elif base != self._state[1]:
813 elif base != self._state[1]:
812 self._gitcommand(['merge', '--no-commit', revision])
814 self._gitcommand(['merge', '--no-commit', revision])
813
815
814 def push(self, force):
816 def push(self, force):
815 # if a branch in origin contains the revision, nothing to do
817 # if a branch in origin contains the revision, nothing to do
816 current, branch2rev, rev2branch = self._gitbranchmap()
818 current, branch2rev, rev2branch = self._gitbranchmap()
817 if self._state[1] in rev2branch:
819 if self._state[1] in rev2branch:
818 for b in rev2branch[self._state[1]]:
820 for b in rev2branch[self._state[1]]:
819 if b.startswith('remotes/origin/'):
821 if b.startswith('remotes/origin/'):
820 return True
822 return True
821 for b, revision in branch2rev.iteritems():
823 for b, revision in branch2rev.iteritems():
822 if b.startswith('remotes/origin/'):
824 if b.startswith('remotes/origin/'):
823 if self._gitisancestor(self._state[1], revision):
825 if self._gitisancestor(self._state[1], revision):
824 return True
826 return True
825 # otherwise, try to push the currently checked out branch
827 # otherwise, try to push the currently checked out branch
826 cmd = ['push']
828 cmd = ['push']
827 if force:
829 if force:
828 cmd.append('--force')
830 cmd.append('--force')
829 if current:
831 if current:
830 # determine if the current branch is even useful
832 # determine if the current branch is even useful
831 if not self._gitisancestor(self._state[1], current):
833 if not self._gitisancestor(self._state[1], current):
832 self._ui.warn(_('unrelated git branch checked out '
834 self._ui.warn(_('unrelated git branch checked out '
833 'in subrepo %s\n') % self._relpath)
835 'in subrepo %s\n') % self._relpath)
834 return False
836 return False
835 self._ui.status(_('pushing branch %s of subrepo %s\n') %
837 self._ui.status(_('pushing branch %s of subrepo %s\n') %
836 (current, self._relpath))
838 (current, self._relpath))
837 self._gitcommand(cmd + ['origin', current])
839 self._gitcommand(cmd + ['origin', current])
838 return True
840 return True
839 else:
841 else:
840 self._ui.warn(_('no branch checked out in subrepo %s\n'
842 self._ui.warn(_('no branch checked out in subrepo %s\n'
841 'cannot push revision %s') %
843 'cannot push revision %s') %
842 (self._relpath, self._state[1]))
844 (self._relpath, self._state[1]))
843 return False
845 return False
844
846
845 def remove(self):
847 def remove(self):
846 if self.dirty():
848 if self.dirty():
847 self._ui.warn(_('not removing repo %s because '
849 self._ui.warn(_('not removing repo %s because '
848 'it has changes.\n') % self._path)
850 'it has changes.\n') % self._path)
849 return
851 return
850 # we can't fully delete the repository as it may contain
852 # we can't fully delete the repository as it may contain
851 # local-only history
853 # local-only history
852 self._ui.note(_('removing subrepo %s\n') % self._path)
854 self._ui.note(_('removing subrepo %s\n') % self._path)
853 self._gitcommand(['config', 'core.bare', 'true'])
855 self._gitcommand(['config', 'core.bare', 'true'])
854 for f in os.listdir(self._path):
856 for f in os.listdir(self._path):
855 if f == '.git':
857 if f == '.git':
856 continue
858 continue
857 path = os.path.join(self._path, f)
859 path = os.path.join(self._path, f)
858 if os.path.isdir(path) and not os.path.islink(path):
860 if os.path.isdir(path) and not os.path.islink(path):
859 shutil.rmtree(path)
861 shutil.rmtree(path)
860 else:
862 else:
861 os.remove(path)
863 os.remove(path)
862
864
863 def archive(self, archiver, prefix):
865 def archive(self, archiver, prefix):
864 source, revision = self._state
866 source, revision = self._state
865 self._fetch(source, revision)
867 self._fetch(source, revision)
866
868
867 # Parse git's native archive command.
869 # Parse git's native archive command.
868 # This should be much faster than manually traversing the trees
870 # This should be much faster than manually traversing the trees
869 # and objects with many subprocess calls.
871 # and objects with many subprocess calls.
870 tarstream = self._gitcommand(['archive', revision], stream=True)
872 tarstream = self._gitcommand(['archive', revision], stream=True)
871 tar = tarfile.open(fileobj=tarstream, mode='r|')
873 tar = tarfile.open(fileobj=tarstream, mode='r|')
872 for info in tar:
874 for info in tar:
873 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
875 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
874 info.mode, info.issym(),
876 info.mode, info.issym(),
875 tar.extractfile(info).read())
877 tar.extractfile(info).read())
876
878
877 types = {
879 types = {
878 'hg': hgsubrepo,
880 'hg': hgsubrepo,
879 'svn': svnsubrepo,
881 'svn': svnsubrepo,
880 'git': gitsubrepo,
882 'git': gitsubrepo,
881 }
883 }
@@ -1,69 +1,124 b''
1 Preparing the subrepository 'sub'
1 Preparing the subrepository 'sub'
2
2
3 $ hg init sub
3 $ hg init sub
4 $ echo sub > sub/sub
4 $ echo sub > sub/sub
5 $ hg add -R sub
5 $ hg add -R sub
6 adding sub/sub
6 adding sub/sub
7 $ hg commit -R sub -m "sub import"
7 $ hg commit -R sub -m "sub import"
8
8
9 Preparing the 'main' repo which depends on the subrepo 'sub'
9 Preparing the 'main' repo which depends on the subrepo 'sub'
10
10
11 $ hg init main
11 $ hg init main
12 $ echo main > main/main
12 $ echo main > main/main
13 $ echo "sub = ../sub" > main/.hgsub
13 $ echo "sub = ../sub" > main/.hgsub
14 $ hg clone sub main/sub
14 $ hg clone sub main/sub
15 updating to branch default
15 updating to branch default
16 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
16 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
17 $ hg add -R main
17 $ hg add -R main
18 adding main/.hgsub
18 adding main/.hgsub
19 adding main/main
19 adding main/main
20 $ hg commit -R main -m "main import"
20 $ hg commit -R main -m "main import"
21 committing subrepository sub
21 committing subrepository sub
22
22
23 Cleaning both repositories, just as a clone -U
23 Cleaning both repositories, just as a clone -U
24
24
25 $ hg up -C -R sub null
25 $ hg up -C -R sub null
26 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
26 0 files updated, 0 files merged, 1 files removed, 0 files unresolved
27 $ hg up -C -R main null
27 $ hg up -C -R main null
28 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
28 0 files updated, 0 files merged, 3 files removed, 0 files unresolved
29 $ rm -rf main/sub
29 $ rm -rf main/sub
30
30
31 Serving them both using hgweb
31 Serving them both using hgweb
32
32
33 $ printf '[paths]\n/main = main\nsub = sub\n' > webdir.conf
33 $ printf '[paths]\n/main = main\nsub = sub\n' > webdir.conf
34 $ hg serve --webdir-conf webdir.conf -a localhost -p $HGPORT \
34 $ hg serve --webdir-conf webdir.conf -a localhost -p $HGPORT \
35 > -A /dev/null -E /dev/null --pid-file hg.pid -d
35 > -A /dev/null -E /dev/null --pid-file hg.pid -d
36 $ cat hg.pid >> $DAEMON_PIDS
36 $ cat hg.pid >> $DAEMON_PIDS
37
37
38 Clone main from hgweb
38 Clone main from hgweb
39
39
40 $ hg clone "http://localhost:$HGPORT/main" cloned
40 $ hg clone "http://localhost:$HGPORT/main" cloned
41 requesting all changes
41 requesting all changes
42 adding changesets
42 adding changesets
43 adding manifests
43 adding manifests
44 adding file changes
44 adding file changes
45 added 1 changesets with 3 changes to 3 files
45 added 1 changesets with 3 changes to 3 files
46 updating to branch default
46 updating to branch default
47 pulling subrepo sub from http://localhost:$HGPORT/sub
47 pulling subrepo sub from http://localhost:$HGPORT/sub
48 requesting all changes
48 requesting all changes
49 adding changesets
49 adding changesets
50 adding manifests
50 adding manifests
51 adding file changes
51 adding file changes
52 added 1 changesets with 1 changes to 1 files
52 added 1 changesets with 1 changes to 1 files
53 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
53 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
54
54
55 Checking cloned repo ids
55 Checking cloned repo ids
56
56
57 $ hg id -R cloned
57 $ hg id -R cloned
58 fdfeeb3e979e tip
58 fdfeeb3e979e tip
59 $ hg id -R cloned/sub
59 $ hg id -R cloned/sub
60 863c1745b441 tip
60 863c1745b441 tip
61
61
62 subrepo debug for 'main' clone
62 subrepo debug for 'main' clone
63
63
64 $ hg debugsub -R cloned
64 $ hg debugsub -R cloned
65 path sub
65 path sub
66 source ../sub
66 source ../sub
67 revision 863c1745b441bd97a8c4a096e87793073f4fb215
67 revision 863c1745b441bd97a8c4a096e87793073f4fb215
68
68
69 $ "$TESTDIR/killdaemons.py"
69 $ "$TESTDIR/killdaemons.py"
70
71
72 Create repo with nested relative subrepos
73
74 $ hg init r1
75 $ hg init r1/sub
76 $ echo sub = sub > r1/.hgsub
77 $ hg add --cwd r1 .hgsub
78 $ hg init r1/sub/subsub
79 $ echo subsub = subsub > r1/sub/.hgsub
80 $ hg add --cwd r1/sub .hgsub
81 $ echo c1 > r1/sub/subsub/f
82 $ hg add --cwd r1/sub/subsub f
83 $ hg ci --cwd r1 -m0
84 committing subrepository sub
85 committing subrepository sub/subsub
86
87 Ensure correct relative paths are used when pulling
88
89 $ hg init r2
90 $ cd r2/
91 $ hg pull -u ../r1
92 pulling from ../r1
93 requesting all changes
94 adding changesets
95 adding manifests
96 adding file changes
97 added 1 changesets with 2 changes to 2 files
98 pulling subrepo sub from ../r1/sub
99 requesting all changes
100 adding changesets
101 adding manifests
102 adding file changes
103 added 1 changesets with 2 changes to 2 files
104 pulling subrepo sub/subsub from ../r1/sub/subsub
105 requesting all changes
106 adding changesets
107 adding manifests
108 adding file changes
109 added 1 changesets with 1 changes to 1 files
110 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
111 $ cd ..
112
113 Verify subrepo default paths were set correctly
114
115 $ hg -R r2/sub paths
116 default = $TESTTMP/r1/sub
117 $ cat r2/sub/.hg/hgrc
118 [paths]
119 default = ../../r1/sub
120 $ hg -R r2/sub/subsub paths
121 default = $TESTTMP/r1/sub/subsub
122 $ cat r2/sub/subsub/.hg/hgrc
123 [paths]
124 default = ../../../r1/sub/subsub
General Comments 0
You need to be logged in to leave comments. Login now