##// END OF EJS Templates
subrepo: allow git subrepos to push and merge...
Eric Eisner -
r12994:845c602b default
parent child Browse files
Show More
@@ -1,685 +1,704
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, subprocess, urlparse, posixpath
8 import errno, os, re, xml.dom.minidom, shutil, subprocess, urlparse, posixpath
9 from i18n import _
9 from i18n import _
10 import config, util, node, error, cmdutil
10 import config, util, node, error, cmdutil
11 hg = None
11 hg = None
12
12
13 nullstate = ('', '', 'empty')
13 nullstate = ('', '', 'empty')
14
14
15 def state(ctx, ui):
15 def state(ctx, ui):
16 """return a state dict, mapping subrepo paths configured in .hgsub
16 """return a state dict, mapping subrepo paths configured in .hgsub
17 to tuple: (source from .hgsub, revision from .hgsubstate, kind
17 to tuple: (source from .hgsub, revision from .hgsubstate, kind
18 (key in types dict))
18 (key in types dict))
19 """
19 """
20 p = config.config()
20 p = config.config()
21 def read(f, sections=None, remap=None):
21 def read(f, sections=None, remap=None):
22 if f in ctx:
22 if f in ctx:
23 p.parse(f, ctx[f].data(), sections, remap, read)
23 p.parse(f, ctx[f].data(), sections, remap, read)
24 else:
24 else:
25 raise util.Abort(_("subrepo spec file %s not found") % f)
25 raise util.Abort(_("subrepo spec file %s not found") % f)
26
26
27 if '.hgsub' in ctx:
27 if '.hgsub' in ctx:
28 read('.hgsub')
28 read('.hgsub')
29
29
30 for path, src in ui.configitems('subpaths'):
30 for path, src in ui.configitems('subpaths'):
31 p.set('subpaths', path, src, ui.configsource('subpaths', path))
31 p.set('subpaths', path, src, ui.configsource('subpaths', path))
32
32
33 rev = {}
33 rev = {}
34 if '.hgsubstate' in ctx:
34 if '.hgsubstate' in ctx:
35 try:
35 try:
36 for l in ctx['.hgsubstate'].data().splitlines():
36 for l in ctx['.hgsubstate'].data().splitlines():
37 revision, path = l.split(" ", 1)
37 revision, path = l.split(" ", 1)
38 rev[path] = revision
38 rev[path] = revision
39 except IOError, err:
39 except IOError, err:
40 if err.errno != errno.ENOENT:
40 if err.errno != errno.ENOENT:
41 raise
41 raise
42
42
43 state = {}
43 state = {}
44 for path, src in p[''].items():
44 for path, src in p[''].items():
45 kind = 'hg'
45 kind = 'hg'
46 if src.startswith('['):
46 if src.startswith('['):
47 if ']' not in src:
47 if ']' not in src:
48 raise util.Abort(_('missing ] in subrepo source'))
48 raise util.Abort(_('missing ] in subrepo source'))
49 kind, src = src.split(']', 1)
49 kind, src = src.split(']', 1)
50 kind = kind[1:]
50 kind = kind[1:]
51
51
52 for pattern, repl in p.items('subpaths'):
52 for pattern, repl in p.items('subpaths'):
53 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
53 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
54 # does a string decode.
54 # does a string decode.
55 repl = repl.encode('string-escape')
55 repl = repl.encode('string-escape')
56 # However, we still want to allow back references to go
56 # However, we still want to allow back references to go
57 # through unharmed, so we turn r'\\1' into r'\1'. Again,
57 # through unharmed, so we turn r'\\1' into r'\1'. Again,
58 # extra escapes are needed because re.sub string decodes.
58 # extra escapes are needed because re.sub string decodes.
59 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
59 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
60 try:
60 try:
61 src = re.sub(pattern, repl, src, 1)
61 src = re.sub(pattern, repl, src, 1)
62 except re.error, e:
62 except re.error, e:
63 raise util.Abort(_("bad subrepository pattern in %s: %s")
63 raise util.Abort(_("bad subrepository pattern in %s: %s")
64 % (p.source('subpaths', pattern), e))
64 % (p.source('subpaths', pattern), e))
65
65
66 state[path] = (src.strip(), rev.get(path, ''), kind)
66 state[path] = (src.strip(), rev.get(path, ''), kind)
67
67
68 return state
68 return state
69
69
70 def writestate(repo, state):
70 def writestate(repo, state):
71 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
71 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
72 repo.wwrite('.hgsubstate',
72 repo.wwrite('.hgsubstate',
73 ''.join(['%s %s\n' % (state[s][1], s)
73 ''.join(['%s %s\n' % (state[s][1], s)
74 for s in sorted(state)]), '')
74 for s in sorted(state)]), '')
75
75
76 def submerge(repo, wctx, mctx, actx):
76 def submerge(repo, wctx, mctx, actx):
77 """delegated from merge.applyupdates: merging of .hgsubstate file
77 """delegated from merge.applyupdates: merging of .hgsubstate file
78 in working context, merging context and ancestor context"""
78 in working context, merging context and ancestor context"""
79 if mctx == actx: # backwards?
79 if mctx == actx: # backwards?
80 actx = wctx.p1()
80 actx = wctx.p1()
81 s1 = wctx.substate
81 s1 = wctx.substate
82 s2 = mctx.substate
82 s2 = mctx.substate
83 sa = actx.substate
83 sa = actx.substate
84 sm = {}
84 sm = {}
85
85
86 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
86 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
87
87
88 def debug(s, msg, r=""):
88 def debug(s, msg, r=""):
89 if r:
89 if r:
90 r = "%s:%s:%s" % r
90 r = "%s:%s:%s" % r
91 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
91 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
92
92
93 for s, l in s1.items():
93 for s, l in s1.items():
94 a = sa.get(s, nullstate)
94 a = sa.get(s, nullstate)
95 ld = l # local state with possible dirty flag for compares
95 ld = l # local state with possible dirty flag for compares
96 if wctx.sub(s).dirty():
96 if wctx.sub(s).dirty():
97 ld = (l[0], l[1] + "+")
97 ld = (l[0], l[1] + "+")
98 if wctx == actx: # overwrite
98 if wctx == actx: # overwrite
99 a = ld
99 a = ld
100
100
101 if s in s2:
101 if s in s2:
102 r = s2[s]
102 r = s2[s]
103 if ld == r or r == a: # no change or local is newer
103 if ld == r or r == a: # no change or local is newer
104 sm[s] = l
104 sm[s] = l
105 continue
105 continue
106 elif ld == a: # other side changed
106 elif ld == a: # other side changed
107 debug(s, "other changed, get", r)
107 debug(s, "other changed, get", r)
108 wctx.sub(s).get(r)
108 wctx.sub(s).get(r)
109 sm[s] = r
109 sm[s] = r
110 elif ld[0] != r[0]: # sources differ
110 elif ld[0] != r[0]: # sources differ
111 if repo.ui.promptchoice(
111 if repo.ui.promptchoice(
112 _(' subrepository sources for %s differ\n'
112 _(' subrepository sources for %s differ\n'
113 'use (l)ocal source (%s) or (r)emote source (%s)?')
113 'use (l)ocal source (%s) or (r)emote source (%s)?')
114 % (s, l[0], r[0]),
114 % (s, l[0], r[0]),
115 (_('&Local'), _('&Remote')), 0):
115 (_('&Local'), _('&Remote')), 0):
116 debug(s, "prompt changed, get", r)
116 debug(s, "prompt 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[1] == a[1]: # local side is unchanged
119 elif ld[1] == a[1]: # local side is unchanged
120 debug(s, "other side changed, get", r)
120 debug(s, "other side changed, get", r)
121 wctx.sub(s).get(r)
121 wctx.sub(s).get(r)
122 sm[s] = r
122 sm[s] = r
123 else:
123 else:
124 debug(s, "both sides changed, merge with", r)
124 debug(s, "both sides changed, merge with", r)
125 wctx.sub(s).merge(r)
125 wctx.sub(s).merge(r)
126 sm[s] = l
126 sm[s] = l
127 elif ld == a: # remote removed, local unchanged
127 elif ld == a: # remote removed, local unchanged
128 debug(s, "remote removed, remove")
128 debug(s, "remote removed, remove")
129 wctx.sub(s).remove()
129 wctx.sub(s).remove()
130 else:
130 else:
131 if repo.ui.promptchoice(
131 if repo.ui.promptchoice(
132 _(' local changed subrepository %s which remote removed\n'
132 _(' local changed subrepository %s which remote removed\n'
133 'use (c)hanged version or (d)elete?') % s,
133 'use (c)hanged version or (d)elete?') % s,
134 (_('&Changed'), _('&Delete')), 0):
134 (_('&Changed'), _('&Delete')), 0):
135 debug(s, "prompt remove")
135 debug(s, "prompt remove")
136 wctx.sub(s).remove()
136 wctx.sub(s).remove()
137
137
138 for s, r in s2.items():
138 for s, r in s2.items():
139 if s in s1:
139 if s in s1:
140 continue
140 continue
141 elif s not in sa:
141 elif s not in sa:
142 debug(s, "remote added, get", r)
142 debug(s, "remote added, get", r)
143 mctx.sub(s).get(r)
143 mctx.sub(s).get(r)
144 sm[s] = r
144 sm[s] = r
145 elif r != sa[s]:
145 elif r != sa[s]:
146 if repo.ui.promptchoice(
146 if repo.ui.promptchoice(
147 _(' remote changed subrepository %s which local removed\n'
147 _(' remote changed subrepository %s which local removed\n'
148 'use (c)hanged version or (d)elete?') % s,
148 'use (c)hanged version or (d)elete?') % s,
149 (_('&Changed'), _('&Delete')), 0) == 0:
149 (_('&Changed'), _('&Delete')), 0) == 0:
150 debug(s, "prompt recreate", r)
150 debug(s, "prompt recreate", r)
151 wctx.sub(s).get(r)
151 wctx.sub(s).get(r)
152 sm[s] = r
152 sm[s] = r
153
153
154 # record merged .hgsubstate
154 # record merged .hgsubstate
155 writestate(repo, sm)
155 writestate(repo, sm)
156
156
157 def reporelpath(repo):
157 def reporelpath(repo):
158 """return path to this (sub)repo as seen from outermost repo"""
158 """return path to this (sub)repo as seen from outermost repo"""
159 parent = repo
159 parent = repo
160 while hasattr(parent, '_subparent'):
160 while hasattr(parent, '_subparent'):
161 parent = parent._subparent
161 parent = parent._subparent
162 return repo.root[len(parent.root)+1:]
162 return repo.root[len(parent.root)+1:]
163
163
164 def subrelpath(sub):
164 def subrelpath(sub):
165 """return path to this subrepo as seen from outermost repo"""
165 """return path to this subrepo as seen from outermost repo"""
166 if not hasattr(sub, '_repo'):
166 if not hasattr(sub, '_repo'):
167 return sub._path
167 return sub._path
168 return reporelpath(sub._repo)
168 return reporelpath(sub._repo)
169
169
170 def _abssource(repo, push=False, abort=True):
170 def _abssource(repo, push=False, abort=True):
171 """return pull/push path of repo - either based on parent repo .hgsub info
171 """return pull/push path of repo - either based on parent repo .hgsub info
172 or on the top repo config. Abort or return None if no source found."""
172 or on the top repo config. Abort or return None if no source found."""
173 if hasattr(repo, '_subparent'):
173 if hasattr(repo, '_subparent'):
174 source = repo._subsource
174 source = repo._subsource
175 if source.startswith('/') or '://' in source:
175 if source.startswith('/') or '://' in source:
176 return source
176 return source
177 parent = _abssource(repo._subparent, push, abort=False)
177 parent = _abssource(repo._subparent, push, abort=False)
178 if parent:
178 if parent:
179 if '://' in parent:
179 if '://' in parent:
180 if parent[-1] == '/':
180 if parent[-1] == '/':
181 parent = parent[:-1]
181 parent = parent[:-1]
182 r = urlparse.urlparse(parent + '/' + source)
182 r = urlparse.urlparse(parent + '/' + source)
183 r = urlparse.urlunparse((r[0], r[1],
183 r = urlparse.urlunparse((r[0], r[1],
184 posixpath.normpath(r[2]),
184 posixpath.normpath(r[2]),
185 r[3], r[4], r[5]))
185 r[3], r[4], r[5]))
186 return r
186 return r
187 else: # plain file system path
187 else: # plain file system path
188 return posixpath.normpath(os.path.join(parent, repo._subsource))
188 return posixpath.normpath(os.path.join(parent, repo._subsource))
189 else: # recursion reached top repo
189 else: # recursion reached top repo
190 if hasattr(repo, '_subtoppath'):
190 if hasattr(repo, '_subtoppath'):
191 return repo._subtoppath
191 return repo._subtoppath
192 if push and repo.ui.config('paths', 'default-push'):
192 if push and repo.ui.config('paths', 'default-push'):
193 return repo.ui.config('paths', 'default-push')
193 return repo.ui.config('paths', 'default-push')
194 if repo.ui.config('paths', 'default'):
194 if repo.ui.config('paths', 'default'):
195 return repo.ui.config('paths', 'default')
195 return repo.ui.config('paths', 'default')
196 if abort:
196 if abort:
197 raise util.Abort(_("default path for subrepository %s not found") %
197 raise util.Abort(_("default path for subrepository %s not found") %
198 reporelpath(repo))
198 reporelpath(repo))
199
199
200 def itersubrepos(ctx1, ctx2):
200 def itersubrepos(ctx1, ctx2):
201 """find subrepos in ctx1 or ctx2"""
201 """find subrepos in ctx1 or ctx2"""
202 # Create a (subpath, ctx) mapping where we prefer subpaths from
202 # Create a (subpath, ctx) mapping where we prefer subpaths from
203 # ctx1. The subpaths from ctx2 are important when the .hgsub file
203 # ctx1. The subpaths from ctx2 are important when the .hgsub file
204 # has been modified (in ctx2) but not yet committed (in ctx1).
204 # has been modified (in ctx2) but not yet committed (in ctx1).
205 subpaths = dict.fromkeys(ctx2.substate, ctx2)
205 subpaths = dict.fromkeys(ctx2.substate, ctx2)
206 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
206 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
207 for subpath, ctx in sorted(subpaths.iteritems()):
207 for subpath, ctx in sorted(subpaths.iteritems()):
208 yield subpath, ctx.sub(subpath)
208 yield subpath, ctx.sub(subpath)
209
209
210 def subrepo(ctx, path):
210 def subrepo(ctx, path):
211 """return instance of the right subrepo class for subrepo in path"""
211 """return instance of the right subrepo class for subrepo in path"""
212 # subrepo inherently violates our import layering rules
212 # subrepo inherently violates our import layering rules
213 # because it wants to make repo objects from deep inside the stack
213 # because it wants to make repo objects from deep inside the stack
214 # so we manually delay the circular imports to not break
214 # so we manually delay the circular imports to not break
215 # scripts that don't use our demand-loading
215 # scripts that don't use our demand-loading
216 global hg
216 global hg
217 import hg as h
217 import hg as h
218 hg = h
218 hg = h
219
219
220 util.path_auditor(ctx._repo.root)(path)
220 util.path_auditor(ctx._repo.root)(path)
221 state = ctx.substate.get(path, nullstate)
221 state = ctx.substate.get(path, nullstate)
222 if state[2] not in types:
222 if state[2] not in types:
223 raise util.Abort(_('unknown subrepo type %s') % state[2])
223 raise util.Abort(_('unknown subrepo type %s') % state[2])
224 return types[state[2]](ctx, path, state[:2])
224 return types[state[2]](ctx, path, state[:2])
225
225
226 # subrepo classes need to implement the following abstract class:
226 # subrepo classes need to implement the following abstract class:
227
227
228 class abstractsubrepo(object):
228 class abstractsubrepo(object):
229
229
230 def dirty(self):
230 def dirty(self):
231 """returns true if the dirstate of the subrepo does not match
231 """returns true if the dirstate of the subrepo does not match
232 current stored state
232 current stored state
233 """
233 """
234 raise NotImplementedError
234 raise NotImplementedError
235
235
236 def checknested(self, path):
236 def checknested(self, path):
237 """check if path is a subrepository within this repository"""
237 """check if path is a subrepository within this repository"""
238 return False
238 return False
239
239
240 def commit(self, text, user, date):
240 def commit(self, text, user, date):
241 """commit the current changes to the subrepo with the given
241 """commit the current changes to the subrepo with the given
242 log message. Use given user and date if possible. Return the
242 log message. Use given user and date if possible. Return the
243 new state of the subrepo.
243 new state of the subrepo.
244 """
244 """
245 raise NotImplementedError
245 raise NotImplementedError
246
246
247 def remove(self):
247 def remove(self):
248 """remove the subrepo
248 """remove the subrepo
249
249
250 (should verify the dirstate is not dirty first)
250 (should verify the dirstate is not dirty first)
251 """
251 """
252 raise NotImplementedError
252 raise NotImplementedError
253
253
254 def get(self, state):
254 def get(self, state):
255 """run whatever commands are needed to put the subrepo into
255 """run whatever commands are needed to put the subrepo into
256 this state
256 this state
257 """
257 """
258 raise NotImplementedError
258 raise NotImplementedError
259
259
260 def merge(self, state):
260 def merge(self, state):
261 """merge currently-saved state with the new state."""
261 """merge currently-saved state with the new state."""
262 raise NotImplementedError
262 raise NotImplementedError
263
263
264 def push(self, force):
264 def push(self, force):
265 """perform whatever action is analogous to 'hg push'
265 """perform whatever action is analogous to 'hg push'
266
266
267 This may be a no-op on some systems.
267 This may be a no-op on some systems.
268 """
268 """
269 raise NotImplementedError
269 raise NotImplementedError
270
270
271 def add(self, ui, match, dryrun, prefix):
271 def add(self, ui, match, dryrun, prefix):
272 return []
272 return []
273
273
274 def status(self, rev2, **opts):
274 def status(self, rev2, **opts):
275 return [], [], [], [], [], [], []
275 return [], [], [], [], [], [], []
276
276
277 def diff(self, diffopts, node2, match, prefix, **opts):
277 def diff(self, diffopts, node2, match, prefix, **opts):
278 pass
278 pass
279
279
280 def outgoing(self, ui, dest, opts):
280 def outgoing(self, ui, dest, opts):
281 return 1
281 return 1
282
282
283 def incoming(self, ui, source, opts):
283 def incoming(self, ui, source, opts):
284 return 1
284 return 1
285
285
286 def files(self):
286 def files(self):
287 """return filename iterator"""
287 """return filename iterator"""
288 raise NotImplementedError
288 raise NotImplementedError
289
289
290 def filedata(self, name):
290 def filedata(self, name):
291 """return file data"""
291 """return file data"""
292 raise NotImplementedError
292 raise NotImplementedError
293
293
294 def fileflags(self, name):
294 def fileflags(self, name):
295 """return file flags"""
295 """return file flags"""
296 return ''
296 return ''
297
297
298 def archive(self, archiver, prefix):
298 def archive(self, archiver, prefix):
299 for name in self.files():
299 for name in self.files():
300 flags = self.fileflags(name)
300 flags = self.fileflags(name)
301 mode = 'x' in flags and 0755 or 0644
301 mode = 'x' in flags and 0755 or 0644
302 symlink = 'l' in flags
302 symlink = 'l' in flags
303 archiver.addfile(os.path.join(prefix, self._path, name),
303 archiver.addfile(os.path.join(prefix, self._path, name),
304 mode, symlink, self.filedata(name))
304 mode, symlink, self.filedata(name))
305
305
306
306
307 class hgsubrepo(abstractsubrepo):
307 class hgsubrepo(abstractsubrepo):
308 def __init__(self, ctx, path, state):
308 def __init__(self, ctx, path, state):
309 self._path = path
309 self._path = path
310 self._state = state
310 self._state = state
311 r = ctx._repo
311 r = ctx._repo
312 root = r.wjoin(path)
312 root = r.wjoin(path)
313 create = False
313 create = False
314 if not os.path.exists(os.path.join(root, '.hg')):
314 if not os.path.exists(os.path.join(root, '.hg')):
315 create = True
315 create = True
316 util.makedirs(root)
316 util.makedirs(root)
317 self._repo = hg.repository(r.ui, root, create=create)
317 self._repo = hg.repository(r.ui, root, create=create)
318 self._repo._subparent = r
318 self._repo._subparent = r
319 self._repo._subsource = state[0]
319 self._repo._subsource = state[0]
320
320
321 if create:
321 if create:
322 fp = self._repo.opener("hgrc", "w", text=True)
322 fp = self._repo.opener("hgrc", "w", text=True)
323 fp.write('[paths]\n')
323 fp.write('[paths]\n')
324
324
325 def addpathconfig(key, value):
325 def addpathconfig(key, value):
326 if value:
326 if value:
327 fp.write('%s = %s\n' % (key, value))
327 fp.write('%s = %s\n' % (key, value))
328 self._repo.ui.setconfig('paths', key, value)
328 self._repo.ui.setconfig('paths', key, value)
329
329
330 defpath = _abssource(self._repo, abort=False)
330 defpath = _abssource(self._repo, abort=False)
331 defpushpath = _abssource(self._repo, True, abort=False)
331 defpushpath = _abssource(self._repo, True, abort=False)
332 addpathconfig('default', defpath)
332 addpathconfig('default', defpath)
333 if defpath != defpushpath:
333 if defpath != defpushpath:
334 addpathconfig('default-push', defpushpath)
334 addpathconfig('default-push', defpushpath)
335 fp.close()
335 fp.close()
336
336
337 def add(self, ui, match, dryrun, prefix):
337 def add(self, ui, match, dryrun, prefix):
338 return cmdutil.add(ui, self._repo, match, dryrun, True,
338 return cmdutil.add(ui, self._repo, match, dryrun, True,
339 os.path.join(prefix, self._path))
339 os.path.join(prefix, self._path))
340
340
341 def status(self, rev2, **opts):
341 def status(self, rev2, **opts):
342 try:
342 try:
343 rev1 = self._state[1]
343 rev1 = self._state[1]
344 ctx1 = self._repo[rev1]
344 ctx1 = self._repo[rev1]
345 ctx2 = self._repo[rev2]
345 ctx2 = self._repo[rev2]
346 return self._repo.status(ctx1, ctx2, **opts)
346 return self._repo.status(ctx1, ctx2, **opts)
347 except error.RepoLookupError, inst:
347 except error.RepoLookupError, inst:
348 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
348 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
349 % (inst, subrelpath(self)))
349 % (inst, subrelpath(self)))
350 return [], [], [], [], [], [], []
350 return [], [], [], [], [], [], []
351
351
352 def diff(self, diffopts, node2, match, prefix, **opts):
352 def diff(self, diffopts, node2, match, prefix, **opts):
353 try:
353 try:
354 node1 = node.bin(self._state[1])
354 node1 = node.bin(self._state[1])
355 # We currently expect node2 to come from substate and be
355 # We currently expect node2 to come from substate and be
356 # in hex format
356 # in hex format
357 if node2 is not None:
357 if node2 is not None:
358 node2 = node.bin(node2)
358 node2 = node.bin(node2)
359 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
359 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
360 node1, node2, match,
360 node1, node2, match,
361 prefix=os.path.join(prefix, self._path),
361 prefix=os.path.join(prefix, self._path),
362 listsubrepos=True, **opts)
362 listsubrepos=True, **opts)
363 except error.RepoLookupError, inst:
363 except error.RepoLookupError, inst:
364 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
364 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
365 % (inst, subrelpath(self)))
365 % (inst, subrelpath(self)))
366
366
367 def archive(self, archiver, prefix):
367 def archive(self, archiver, prefix):
368 abstractsubrepo.archive(self, archiver, prefix)
368 abstractsubrepo.archive(self, archiver, prefix)
369
369
370 rev = self._state[1]
370 rev = self._state[1]
371 ctx = self._repo[rev]
371 ctx = self._repo[rev]
372 for subpath in ctx.substate:
372 for subpath in ctx.substate:
373 s = subrepo(ctx, subpath)
373 s = subrepo(ctx, subpath)
374 s.archive(archiver, os.path.join(prefix, self._path))
374 s.archive(archiver, os.path.join(prefix, self._path))
375
375
376 def dirty(self):
376 def dirty(self):
377 r = self._state[1]
377 r = self._state[1]
378 if r == '':
378 if r == '':
379 return True
379 return True
380 w = self._repo[None]
380 w = self._repo[None]
381 if w.p1() != self._repo[r]: # version checked out change
381 if w.p1() != self._repo[r]: # version checked out change
382 return True
382 return True
383 return w.dirty() # working directory changed
383 return w.dirty() # working directory changed
384
384
385 def checknested(self, path):
385 def checknested(self, path):
386 return self._repo._checknested(self._repo.wjoin(path))
386 return self._repo._checknested(self._repo.wjoin(path))
387
387
388 def commit(self, text, user, date):
388 def commit(self, text, user, date):
389 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
389 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
390 n = self._repo.commit(text, user, date)
390 n = self._repo.commit(text, user, date)
391 if not n:
391 if not n:
392 return self._repo['.'].hex() # different version checked out
392 return self._repo['.'].hex() # different version checked out
393 return node.hex(n)
393 return node.hex(n)
394
394
395 def remove(self):
395 def remove(self):
396 # we can't fully delete the repository as it may contain
396 # we can't fully delete the repository as it may contain
397 # local-only history
397 # local-only history
398 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
398 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
399 hg.clean(self._repo, node.nullid, False)
399 hg.clean(self._repo, node.nullid, False)
400
400
401 def _get(self, state):
401 def _get(self, state):
402 source, revision, kind = state
402 source, revision, kind = state
403 try:
403 try:
404 self._repo.lookup(revision)
404 self._repo.lookup(revision)
405 except error.RepoError:
405 except error.RepoError:
406 self._repo._subsource = source
406 self._repo._subsource = source
407 srcurl = _abssource(self._repo)
407 srcurl = _abssource(self._repo)
408 self._repo.ui.status(_('pulling subrepo %s from %s\n')
408 self._repo.ui.status(_('pulling subrepo %s from %s\n')
409 % (subrelpath(self), srcurl))
409 % (subrelpath(self), srcurl))
410 other = hg.repository(self._repo.ui, srcurl)
410 other = hg.repository(self._repo.ui, srcurl)
411 self._repo.pull(other)
411 self._repo.pull(other)
412
412
413 def get(self, state):
413 def get(self, state):
414 self._get(state)
414 self._get(state)
415 source, revision, kind = state
415 source, revision, kind = state
416 self._repo.ui.debug("getting subrepo %s\n" % self._path)
416 self._repo.ui.debug("getting subrepo %s\n" % self._path)
417 hg.clean(self._repo, revision, False)
417 hg.clean(self._repo, revision, False)
418
418
419 def merge(self, state):
419 def merge(self, state):
420 self._get(state)
420 self._get(state)
421 cur = self._repo['.']
421 cur = self._repo['.']
422 dst = self._repo[state[1]]
422 dst = self._repo[state[1]]
423 anc = dst.ancestor(cur)
423 anc = dst.ancestor(cur)
424 if anc == cur:
424 if anc == cur:
425 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
425 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
426 hg.update(self._repo, state[1])
426 hg.update(self._repo, state[1])
427 elif anc == dst:
427 elif anc == dst:
428 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
428 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
429 else:
429 else:
430 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
430 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
431 hg.merge(self._repo, state[1], remind=False)
431 hg.merge(self._repo, state[1], remind=False)
432
432
433 def push(self, force):
433 def push(self, force):
434 # push subrepos depth-first for coherent ordering
434 # push subrepos depth-first for coherent ordering
435 c = self._repo['']
435 c = self._repo['']
436 subs = c.substate # only repos that are committed
436 subs = c.substate # only repos that are committed
437 for s in sorted(subs):
437 for s in sorted(subs):
438 if not c.sub(s).push(force):
438 if not c.sub(s).push(force):
439 return False
439 return False
440
440
441 dsturl = _abssource(self._repo, True)
441 dsturl = _abssource(self._repo, True)
442 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
442 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
443 (subrelpath(self), dsturl))
443 (subrelpath(self), dsturl))
444 other = hg.repository(self._repo.ui, dsturl)
444 other = hg.repository(self._repo.ui, dsturl)
445 return self._repo.push(other, force)
445 return self._repo.push(other, force)
446
446
447 def outgoing(self, ui, dest, opts):
447 def outgoing(self, ui, dest, opts):
448 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
448 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
449
449
450 def incoming(self, ui, source, opts):
450 def incoming(self, ui, source, opts):
451 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
451 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
452
452
453 def files(self):
453 def files(self):
454 rev = self._state[1]
454 rev = self._state[1]
455 ctx = self._repo[rev]
455 ctx = self._repo[rev]
456 return ctx.manifest()
456 return ctx.manifest()
457
457
458 def filedata(self, name):
458 def filedata(self, name):
459 rev = self._state[1]
459 rev = self._state[1]
460 return self._repo[rev][name].data()
460 return self._repo[rev][name].data()
461
461
462 def fileflags(self, name):
462 def fileflags(self, name):
463 rev = self._state[1]
463 rev = self._state[1]
464 ctx = self._repo[rev]
464 ctx = self._repo[rev]
465 return ctx.flags(name)
465 return ctx.flags(name)
466
466
467
467
468 class svnsubrepo(abstractsubrepo):
468 class svnsubrepo(abstractsubrepo):
469 def __init__(self, ctx, path, state):
469 def __init__(self, ctx, path, state):
470 self._path = path
470 self._path = path
471 self._state = state
471 self._state = state
472 self._ctx = ctx
472 self._ctx = ctx
473 self._ui = ctx._repo.ui
473 self._ui = ctx._repo.ui
474
474
475 def _svncommand(self, commands, filename=''):
475 def _svncommand(self, commands, filename=''):
476 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
476 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
477 cmd = ['svn'] + commands + [path]
477 cmd = ['svn'] + commands + [path]
478 cmd = [util.shellquote(arg) for arg in cmd]
478 cmd = [util.shellquote(arg) for arg in cmd]
479 cmd = util.quotecommand(' '.join(cmd))
479 cmd = util.quotecommand(' '.join(cmd))
480 env = dict(os.environ)
480 env = dict(os.environ)
481 # Avoid localized output, preserve current locale for everything else.
481 # Avoid localized output, preserve current locale for everything else.
482 env['LC_MESSAGES'] = 'C'
482 env['LC_MESSAGES'] = 'C'
483 write, read, err = util.popen3(cmd, env=env, newlines=True)
483 write, read, err = util.popen3(cmd, env=env, newlines=True)
484 retdata = read.read()
484 retdata = read.read()
485 err = err.read().strip()
485 err = err.read().strip()
486 if err:
486 if err:
487 raise util.Abort(err)
487 raise util.Abort(err)
488 return retdata
488 return retdata
489
489
490 def _wcrev(self):
490 def _wcrev(self):
491 output = self._svncommand(['info', '--xml'])
491 output = self._svncommand(['info', '--xml'])
492 doc = xml.dom.minidom.parseString(output)
492 doc = xml.dom.minidom.parseString(output)
493 entries = doc.getElementsByTagName('entry')
493 entries = doc.getElementsByTagName('entry')
494 if not entries:
494 if not entries:
495 return '0'
495 return '0'
496 return str(entries[0].getAttribute('revision')) or '0'
496 return str(entries[0].getAttribute('revision')) or '0'
497
497
498 def _wcchanged(self):
498 def _wcchanged(self):
499 """Return (changes, extchanges) where changes is True
499 """Return (changes, extchanges) where changes is True
500 if the working directory was changed, and extchanges is
500 if the working directory was changed, and extchanges is
501 True if any of these changes concern an external entry.
501 True if any of these changes concern an external entry.
502 """
502 """
503 output = self._svncommand(['status', '--xml'])
503 output = self._svncommand(['status', '--xml'])
504 externals, changes = [], []
504 externals, changes = [], []
505 doc = xml.dom.minidom.parseString(output)
505 doc = xml.dom.minidom.parseString(output)
506 for e in doc.getElementsByTagName('entry'):
506 for e in doc.getElementsByTagName('entry'):
507 s = e.getElementsByTagName('wc-status')
507 s = e.getElementsByTagName('wc-status')
508 if not s:
508 if not s:
509 continue
509 continue
510 item = s[0].getAttribute('item')
510 item = s[0].getAttribute('item')
511 props = s[0].getAttribute('props')
511 props = s[0].getAttribute('props')
512 path = e.getAttribute('path')
512 path = e.getAttribute('path')
513 if item == 'external':
513 if item == 'external':
514 externals.append(path)
514 externals.append(path)
515 if (item not in ('', 'normal', 'unversioned', 'external')
515 if (item not in ('', 'normal', 'unversioned', 'external')
516 or props not in ('', 'none')):
516 or props not in ('', 'none')):
517 changes.append(path)
517 changes.append(path)
518 for path in changes:
518 for path in changes:
519 for ext in externals:
519 for ext in externals:
520 if path == ext or path.startswith(ext + os.sep):
520 if path == ext or path.startswith(ext + os.sep):
521 return True, True
521 return True, True
522 return bool(changes), False
522 return bool(changes), False
523
523
524 def dirty(self):
524 def dirty(self):
525 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
525 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
526 return False
526 return False
527 return True
527 return True
528
528
529 def commit(self, text, user, date):
529 def commit(self, text, user, date):
530 # user and date are out of our hands since svn is centralized
530 # user and date are out of our hands since svn is centralized
531 changed, extchanged = self._wcchanged()
531 changed, extchanged = self._wcchanged()
532 if not changed:
532 if not changed:
533 return self._wcrev()
533 return self._wcrev()
534 if extchanged:
534 if extchanged:
535 # Do not try to commit externals
535 # Do not try to commit externals
536 raise util.Abort(_('cannot commit svn externals'))
536 raise util.Abort(_('cannot commit svn externals'))
537 commitinfo = self._svncommand(['commit', '-m', text])
537 commitinfo = self._svncommand(['commit', '-m', text])
538 self._ui.status(commitinfo)
538 self._ui.status(commitinfo)
539 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
539 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
540 if not newrev:
540 if not newrev:
541 raise util.Abort(commitinfo.splitlines()[-1])
541 raise util.Abort(commitinfo.splitlines()[-1])
542 newrev = newrev.groups()[0]
542 newrev = newrev.groups()[0]
543 self._ui.status(self._svncommand(['update', '-r', newrev]))
543 self._ui.status(self._svncommand(['update', '-r', newrev]))
544 return newrev
544 return newrev
545
545
546 def remove(self):
546 def remove(self):
547 if self.dirty():
547 if self.dirty():
548 self._ui.warn(_('not removing repo %s because '
548 self._ui.warn(_('not removing repo %s because '
549 'it has changes.\n' % self._path))
549 'it has changes.\n' % self._path))
550 return
550 return
551 self._ui.note(_('removing subrepo %s\n') % self._path)
551 self._ui.note(_('removing subrepo %s\n') % self._path)
552 shutil.rmtree(self._ctx._repo.wjoin(self._path))
552 shutil.rmtree(self._ctx._repo.wjoin(self._path))
553
553
554 def get(self, state):
554 def get(self, state):
555 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
555 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
556 if not re.search('Checked out revision [0-9]+.', status):
556 if not re.search('Checked out revision [0-9]+.', status):
557 raise util.Abort(status.splitlines()[-1])
557 raise util.Abort(status.splitlines()[-1])
558 self._ui.status(status)
558 self._ui.status(status)
559
559
560 def merge(self, state):
560 def merge(self, state):
561 old = int(self._state[1])
561 old = int(self._state[1])
562 new = int(state[1])
562 new = int(state[1])
563 if new > old:
563 if new > old:
564 self.get(state)
564 self.get(state)
565
565
566 def push(self, force):
566 def push(self, force):
567 # push is a no-op for SVN
567 # push is a no-op for SVN
568 return True
568 return True
569
569
570 def files(self):
570 def files(self):
571 output = self._svncommand(['list'])
571 output = self._svncommand(['list'])
572 # This works because svn forbids \n in filenames.
572 # This works because svn forbids \n in filenames.
573 return output.splitlines()
573 return output.splitlines()
574
574
575 def filedata(self, name):
575 def filedata(self, name):
576 return self._svncommand(['cat'], name)
576 return self._svncommand(['cat'], name)
577
577
578
578
579 class gitsubrepo(object):
579 class gitsubrepo(object):
580 def __init__(self, ctx, path, state):
580 def __init__(self, ctx, path, state):
581 # TODO add git version check.
581 # TODO add git version check.
582 self._state = state
582 self._state = state
583 self._ctx = ctx
583 self._ctx = ctx
584 self._relpath = path
584 self._relpath = path
585 self._path = ctx._repo.wjoin(path)
585 self._path = ctx._repo.wjoin(path)
586 self._ui = ctx._repo.ui
586 self._ui = ctx._repo.ui
587
587
588 def _gitcommand(self, commands):
588 def _gitcommand(self, commands):
589 return self._gitdir(commands)[0]
589 return self._gitdir(commands)[0]
590
590
591 def _gitdir(self, commands):
591 def _gitdir(self, commands):
592 commands = ['--no-pager', '--git-dir=%s/.git' % self._path,
592 commands = ['--no-pager', '--git-dir=%s/.git' % self._path,
593 '--work-tree=%s' % self._path] + commands
593 '--work-tree=%s' % self._path] + commands
594 return self._gitnodir(commands)
594 return self._gitnodir(commands)
595
595
596 def _gitnodir(self, commands):
596 def _gitnodir(self, commands):
597 """Calls the git command
597 """Calls the git command
598
598
599 The methods tries to call the git command. versions previor to 1.6.0
599 The methods tries to call the git command. versions previor to 1.6.0
600 are not supported and very probably fail.
600 are not supported and very probably fail.
601 """
601 """
602 cmd = ['git'] + commands
602 cmd = ['git'] + commands
603 cmd = [util.shellquote(arg) for arg in cmd]
603 cmd = [util.shellquote(arg) for arg in cmd]
604 cmd = util.quotecommand(' '.join(cmd))
604 cmd = util.quotecommand(' '.join(cmd))
605
605
606 # print git's stderr, which is mostly progress and useful info
606 # print git's stderr, which is mostly progress and useful info
607 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
607 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
608 close_fds=(os.name == 'posix'),
608 close_fds=(os.name == 'posix'),
609 stdout=subprocess.PIPE)
609 stdout=subprocess.PIPE)
610 retdata = p.stdout.read()
610 retdata = p.stdout.read()
611 # wait for the child to exit to avoid race condition.
611 # wait for the child to exit to avoid race condition.
612 p.wait()
612 p.wait()
613
613
614 if p.returncode != 0:
614 if p.returncode != 0:
615 # there are certain error codes that are ok
615 # there are certain error codes that are ok
616 command = None
616 command = None
617 for arg in commands:
617 for arg in commands:
618 if not arg.startswith('-'):
618 if not arg.startswith('-'):
619 command = arg
619 command = arg
620 break
620 break
621 if command == 'cat-file':
621 if command == 'cat-file':
622 return retdata, p.returncode
622 return retdata, p.returncode
623 if command in ('commit', 'status') and p.returncode == 1:
623 if command in ('commit', 'status') and p.returncode == 1:
624 return retdata, p.returncode
624 return retdata, p.returncode
625 # for all others, abort
625 # for all others, abort
626 raise util.Abort('git %s error %d' % (command, p.returncode))
626 raise util.Abort('git %s error %d' % (command, p.returncode))
627
627
628 return retdata, p.returncode
628 return retdata, p.returncode
629
629
630 def _gitstate(self):
630 def _gitstate(self):
631 return self._gitcommand(['rev-parse', 'HEAD']).strip()
631 return self._gitcommand(['rev-parse', 'HEAD']).strip()
632
632
633 def _githavelocally(self, revision):
633 def _githavelocally(self, revision):
634 out, code = self._gitdir(['cat-file', '-e', revision])
634 out, code = self._gitdir(['cat-file', '-e', revision])
635 return code == 0
635 return code == 0
636
636
637 def _fetch(self, source, revision):
637 def _fetch(self, source, revision):
638 if not os.path.exists('%s/.git' % self._path):
638 if not os.path.exists('%s/.git' % self._path):
639 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
639 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
640 self._gitnodir(['clone', source, self._path])
640 self._gitnodir(['clone', source, self._path])
641 if self._githavelocally(revision):
641 if self._githavelocally(revision):
642 return
642 return
643 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
643 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
644 self._gitcommand(['fetch', '--all', '-q'])
644 self._gitcommand(['fetch', '--all', '-q'])
645 if not self._githavelocally(revision):
645 if not self._githavelocally(revision):
646 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
646 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
647 (revision, self._path))
647 (revision, self._path))
648
648
649 def dirty(self):
649 def dirty(self):
650 if self._state[1] != self._gitstate(): # version checked out changed?
650 if self._state[1] != self._gitstate(): # version checked out changed?
651 return True
651 return True
652 # check for staged changes or modified files; ignore untracked files
652 # check for staged changes or modified files; ignore untracked files
653 # docs say --porcelain flag is future-proof format
653 # docs say --porcelain flag is future-proof format
654 changed = self._gitcommand(['status', '--porcelain',
654 changed = self._gitcommand(['status', '--porcelain',
655 '--untracked-files=no'])
655 '--untracked-files=no'])
656 return bool(changed.strip())
656 return bool(changed.strip())
657
657
658 def get(self, state):
658 def get(self, state):
659 source, revision, kind = state
659 source, revision, kind = state
660 self._fetch(source, revision)
660 self._fetch(source, revision)
661 if self._gitstate() != revision:
661 if self._gitstate() != revision:
662 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
662 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
663 self._relpath)
663 self._relpath)
664 self._ui.warn(_('check out a git branch if you intend '
664 self._ui.warn(_('check out a git branch if you intend '
665 'to make changes\n'))
665 'to make changes\n'))
666 self._gitcommand(['checkout', '-q', revision])
666 self._gitcommand(['checkout', '-q', revision])
667
667
668 def commit(self, text, user, date):
668 def commit(self, text, user, date):
669 cmd = ['commit', '-a', '-m', text]
669 cmd = ['commit', '-a', '-m', text]
670 if user:
670 if user:
671 cmd += ['--author', user]
671 cmd += ['--author', user]
672 if date:
672 if date:
673 # git's date parser silently ignores when seconds < 1e9
673 # git's date parser silently ignores when seconds < 1e9
674 # convert to ISO8601
674 # convert to ISO8601
675 cmd += ['--date', util.datestr(date, '%Y-%m-%dT%H:%M:%S %1%2')]
675 cmd += ['--date', util.datestr(date, '%Y-%m-%dT%H:%M:%S %1%2')]
676 self._gitcommand(cmd)
676 self._gitcommand(cmd)
677 # make sure commit works otherwise HEAD might not exist under certain
677 # make sure commit works otherwise HEAD might not exist under certain
678 # circumstances
678 # circumstances
679 return self._gitstate()
679 return self._gitstate()
680
680
681 def merge(self, state):
682 source, revision, kind = state
683 self._fetch(source, revision)
684 base = self._gitcommand(['merge-base', revision,
685 self._state[1]]).strip()
686 if base == revision:
687 self.get(state) # fast forward merge
688 elif base != self._state[1]:
689 self._gitcommand(['merge', '--no-commit', revision])
690
691 def push(self, force):
692 cmd = ['push']
693 if force:
694 cmd.append('--force')
695 # as subrepos have no notion of "where to push to" we
696 # assume origin master. This is git's default
697 self._gitcommand(cmd + ['origin', 'master', '-q'])
698 return True
699
681 types = {
700 types = {
682 'hg': hgsubrepo,
701 'hg': hgsubrepo,
683 'svn': svnsubrepo,
702 'svn': svnsubrepo,
684 'git': gitsubrepo,
703 'git': gitsubrepo,
685 }
704 }
@@ -1,83 +1,173
1 $ "$TESTDIR/hghave" git || exit 80
1 $ "$TESTDIR/hghave" git || exit 80
2
2
3 make git commits repeatable
3 make git commits repeatable
4
4
5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
5 $ GIT_AUTHOR_NAME='test'; export GIT_AUTHOR_NAME
6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
6 $ GIT_AUTHOR_EMAIL='test@example.org'; export GIT_AUTHOR_EMAIL
7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
7 $ GIT_AUTHOR_DATE='1234567891 +0000'; export GIT_AUTHOR_DATE
8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
8 $ GIT_COMMITTER_NAME="$GIT_AUTHOR_NAME"; export GIT_COMMITTER_NAME
9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
9 $ GIT_COMMITTER_EMAIL="$GIT_AUTHOR_EMAIL"; export GIT_COMMITTER_EMAIL
10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
10 $ GIT_COMMITTER_DATE="$GIT_AUTHOR_DATE"; export GIT_COMMITTER_DATE
11
11
12 root hg repo
12 root hg repo
13
13
14 $ hg init t
14 $ hg init t
15 $ cd t
15 $ cd t
16 $ echo a > a
16 $ echo a > a
17 $ hg add a
17 $ hg add a
18 $ hg commit -m a
18 $ hg commit -m a
19 $ cd ..
19 $ cd ..
20
20
21 new external git repo
21 new external git repo
22
22
23 $ mkdir gitroot
23 $ mkdir gitroot
24 $ cd gitroot
24 $ cd gitroot
25 $ git init -q
25 $ git init -q
26 $ echo g > g
26 $ echo g > g
27 $ git add g
27 $ git add g
28 $ git commit -q -m g
28 $ git commit -q -m g
29
29
30 add subrepo clone
30 add subrepo clone
31
31
32 $ cd ../t
32 $ cd ../t
33 $ echo 's = [git]../gitroot' > .hgsub
33 $ echo 's = [git]../gitroot' > .hgsub
34 $ git clone -q ../gitroot s
34 $ git clone -q ../gitroot s
35 $ hg add .hgsub
35 $ hg add .hgsub
36 $ hg commit -m 'new git subrepo'
36 $ hg commit -m 'new git subrepo'
37 committing subrepository $TESTTMP/t/s
37 committing subrepository $TESTTMP/t/s
38 $ hg debugsub
38 $ hg debugsub
39 path s
39 path s
40 source ../gitroot
40 source ../gitroot
41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
41 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
42
42
43 record a new commit from upstream
43 record a new commit from upstream
44
44
45 $ cd ../gitroot
45 $ cd ../gitroot
46 $ echo gg >> g
46 $ echo gg >> g
47 $ git commit -q -a -m gg
47 $ git commit -q -a -m gg
48
48
49 $ cd ../t/s
49 $ cd ../t/s
50 $ git pull -q
50 $ git pull -q
51
51
52 $ cd ..
52 $ cd ..
53 $ hg commit -m 'update git subrepo'
53 $ hg commit -m 'update git subrepo'
54 committing subrepository $TESTTMP/t/s
54 committing subrepository $TESTTMP/t/s
55 $ hg debugsub
55 $ hg debugsub
56 path s
56 path s
57 source ../gitroot
57 source ../gitroot
58 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
58 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
59
59
60 clone root
60 clone root
61
61
62 $ hg clone . ../tc
62 $ hg clone . ../tc
63 updating to branch default
63 updating to branch default
64 cloning subrepo s
64 cloning subrepo s
65 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
65 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
66 $ cd ../tc
66 $ cd ../tc
67 $ hg debugsub
67 $ hg debugsub
68 path s
68 path s
69 source ../gitroot
69 source ../gitroot
70 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
70 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
71
71
72 update to previous substate
72 update to previous substate
73
73
74 $ hg update 1
74 $ hg update 1
75 checking out detached HEAD in subrepo s
75 checking out detached HEAD in subrepo s
76 check out a git branch if you intend to make changes
76 check out a git branch if you intend to make changes
77 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
77 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
78 $ cat s/g
78 $ cat s/g
79 g
79 g
80 $ hg debugsub
80 $ hg debugsub
81 path s
81 path s
82 source ../gitroot
82 source ../gitroot
83 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
83 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
84
85 make $GITROOT pushable, by replacing it with a clone with nothing checked out
86
87 $ cd ..
88 $ git clone gitroot gitrootbare --bare -q
89 $ rm -rf gitroot
90 $ mv gitrootbare gitroot
91
92 clone root, make local change
93
94 $ cd t
95 $ hg clone . ../ta
96 updating to branch default
97 cloning subrepo s
98 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
99
100 $ cd ../ta
101 $ echo ggg >> s/g
102 $ hg commit -m ggg
103 committing subrepository $TESTTMP/ta/s
104 $ hg debugsub
105 path s
106 source ../gitroot
107 revision 79695940086840c99328513acbe35f90fcd55e57
108
109 clone root separately, make different local change
110
111 $ cd ../t
112 $ hg clone . ../tb
113 updating to branch default
114 cloning subrepo s
115 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
116
117 $ cd ../tb/s
118 $ echo f > f
119 $ git add f
120 $ cd ..
121
122 $ hg commit -m f
123 committing subrepository $TESTTMP/tb/s
124 $ hg debugsub
125 path s
126 source ../gitroot
127 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
128
129 user b push changes
130
131 $ hg push
132 pushing to $TESTTMP/t
133 searching for changes
134 adding changesets
135 adding manifests
136 adding file changes
137 added 1 changesets with 1 changes to 1 files
138
139 user a pulls, merges, commits
140
141 $ cd ../ta
142 $ hg pull
143 pulling from $TESTTMP/t
144 searching for changes
145 adding changesets
146 adding manifests
147 adding file changes
148 added 1 changesets with 1 changes to 1 files (+1 heads)
149 (run 'hg heads' to see heads, 'hg merge' to merge)
150 $ hg merge
151 Automatic merge went well; stopped before committing as requested
152 pulling subrepo s
153 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
154 (branch merge, don't forget to commit)
155 $ cat s/f
156 f
157 $ cat s/g
158 g
159 gg
160 ggg
161 $ hg commit -m 'merge'
162 committing subrepository $TESTTMP/ta/s
163 $ hg debugsub
164 path s
165 source ../gitroot
166 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
167 $ hg push
168 pushing to $TESTTMP/t
169 searching for changes
170 adding changesets
171 adding manifests
172 adding file changes
173 added 2 changesets with 2 changes to 1 files
General Comments 0
You need to be logged in to leave comments. Login now