##// END OF EJS Templates
subrepo: lazily update git's local tracking branches...
Eric Eisner -
r13087:cca0779b default
parent child Browse files
Show More
@@ -1,836 +1,877 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 fp.write('%s = %s\n' % (key, value))
336 fp.write('%s = %s\n' % (key, value))
337 self._repo.ui.setconfig('paths', key, value)
337 self._repo.ui.setconfig('paths', key, value)
338
338
339 defpath = _abssource(self._repo, abort=False)
339 defpath = _abssource(self._repo, abort=False)
340 defpushpath = _abssource(self._repo, True, abort=False)
340 defpushpath = _abssource(self._repo, True, abort=False)
341 addpathconfig('default', defpath)
341 addpathconfig('default', defpath)
342 if defpath != defpushpath:
342 if defpath != defpushpath:
343 addpathconfig('default-push', defpushpath)
343 addpathconfig('default-push', defpushpath)
344 fp.close()
344 fp.close()
345
345
346 def add(self, ui, match, dryrun, prefix):
346 def add(self, ui, match, dryrun, prefix):
347 return cmdutil.add(ui, self._repo, match, dryrun, True,
347 return cmdutil.add(ui, self._repo, match, dryrun, True,
348 os.path.join(prefix, self._path))
348 os.path.join(prefix, self._path))
349
349
350 def status(self, rev2, **opts):
350 def status(self, rev2, **opts):
351 try:
351 try:
352 rev1 = self._state[1]
352 rev1 = self._state[1]
353 ctx1 = self._repo[rev1]
353 ctx1 = self._repo[rev1]
354 ctx2 = self._repo[rev2]
354 ctx2 = self._repo[rev2]
355 return self._repo.status(ctx1, ctx2, **opts)
355 return self._repo.status(ctx1, ctx2, **opts)
356 except error.RepoLookupError, inst:
356 except error.RepoLookupError, inst:
357 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
357 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
358 % (inst, subrelpath(self)))
358 % (inst, subrelpath(self)))
359 return [], [], [], [], [], [], []
359 return [], [], [], [], [], [], []
360
360
361 def diff(self, diffopts, node2, match, prefix, **opts):
361 def diff(self, diffopts, node2, match, prefix, **opts):
362 try:
362 try:
363 node1 = node.bin(self._state[1])
363 node1 = node.bin(self._state[1])
364 # We currently expect node2 to come from substate and be
364 # We currently expect node2 to come from substate and be
365 # in hex format
365 # in hex format
366 if node2 is not None:
366 if node2 is not None:
367 node2 = node.bin(node2)
367 node2 = node.bin(node2)
368 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
368 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
369 node1, node2, match,
369 node1, node2, match,
370 prefix=os.path.join(prefix, self._path),
370 prefix=os.path.join(prefix, self._path),
371 listsubrepos=True, **opts)
371 listsubrepos=True, **opts)
372 except error.RepoLookupError, inst:
372 except error.RepoLookupError, inst:
373 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
373 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
374 % (inst, subrelpath(self)))
374 % (inst, subrelpath(self)))
375
375
376 def archive(self, archiver, prefix):
376 def archive(self, archiver, prefix):
377 abstractsubrepo.archive(self, archiver, prefix)
377 abstractsubrepo.archive(self, archiver, prefix)
378
378
379 rev = self._state[1]
379 rev = self._state[1]
380 ctx = self._repo[rev]
380 ctx = self._repo[rev]
381 for subpath in ctx.substate:
381 for subpath in ctx.substate:
382 s = subrepo(ctx, subpath)
382 s = subrepo(ctx, subpath)
383 s.archive(archiver, os.path.join(prefix, self._path))
383 s.archive(archiver, os.path.join(prefix, self._path))
384
384
385 def dirty(self):
385 def dirty(self):
386 r = self._state[1]
386 r = self._state[1]
387 if r == '':
387 if r == '':
388 return True
388 return True
389 w = self._repo[None]
389 w = self._repo[None]
390 if w.p1() != self._repo[r]: # version checked out change
390 if w.p1() != self._repo[r]: # version checked out change
391 return True
391 return True
392 return w.dirty() # working directory changed
392 return w.dirty() # working directory changed
393
393
394 def checknested(self, path):
394 def checknested(self, path):
395 return self._repo._checknested(self._repo.wjoin(path))
395 return self._repo._checknested(self._repo.wjoin(path))
396
396
397 def commit(self, text, user, date):
397 def commit(self, text, user, date):
398 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
398 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
399 n = self._repo.commit(text, user, date)
399 n = self._repo.commit(text, user, date)
400 if not n:
400 if not n:
401 return self._repo['.'].hex() # different version checked out
401 return self._repo['.'].hex() # different version checked out
402 return node.hex(n)
402 return node.hex(n)
403
403
404 def remove(self):
404 def remove(self):
405 # we can't fully delete the repository as it may contain
405 # we can't fully delete the repository as it may contain
406 # local-only history
406 # local-only history
407 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
407 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
408 hg.clean(self._repo, node.nullid, False)
408 hg.clean(self._repo, node.nullid, False)
409
409
410 def _get(self, state):
410 def _get(self, state):
411 source, revision, kind = state
411 source, revision, kind = state
412 try:
412 try:
413 self._repo.lookup(revision)
413 self._repo.lookup(revision)
414 except error.RepoError:
414 except error.RepoError:
415 self._repo._subsource = source
415 self._repo._subsource = source
416 srcurl = _abssource(self._repo)
416 srcurl = _abssource(self._repo)
417 self._repo.ui.status(_('pulling subrepo %s from %s\n')
417 self._repo.ui.status(_('pulling subrepo %s from %s\n')
418 % (subrelpath(self), srcurl))
418 % (subrelpath(self), srcurl))
419 other = hg.repository(self._repo.ui, srcurl)
419 other = hg.repository(self._repo.ui, srcurl)
420 self._repo.pull(other)
420 self._repo.pull(other)
421
421
422 def get(self, state):
422 def get(self, state):
423 self._get(state)
423 self._get(state)
424 source, revision, kind = state
424 source, revision, kind = state
425 self._repo.ui.debug("getting subrepo %s\n" % self._path)
425 self._repo.ui.debug("getting subrepo %s\n" % self._path)
426 hg.clean(self._repo, revision, False)
426 hg.clean(self._repo, revision, False)
427
427
428 def merge(self, state):
428 def merge(self, state):
429 self._get(state)
429 self._get(state)
430 cur = self._repo['.']
430 cur = self._repo['.']
431 dst = self._repo[state[1]]
431 dst = self._repo[state[1]]
432 anc = dst.ancestor(cur)
432 anc = dst.ancestor(cur)
433 if anc == cur:
433 if anc == cur:
434 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
434 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
435 hg.update(self._repo, state[1])
435 hg.update(self._repo, state[1])
436 elif anc == dst:
436 elif anc == dst:
437 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
437 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
438 else:
438 else:
439 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
439 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
440 hg.merge(self._repo, state[1], remind=False)
440 hg.merge(self._repo, state[1], remind=False)
441
441
442 def push(self, force):
442 def push(self, force):
443 # push subrepos depth-first for coherent ordering
443 # push subrepos depth-first for coherent ordering
444 c = self._repo['']
444 c = self._repo['']
445 subs = c.substate # only repos that are committed
445 subs = c.substate # only repos that are committed
446 for s in sorted(subs):
446 for s in sorted(subs):
447 if not c.sub(s).push(force):
447 if not c.sub(s).push(force):
448 return False
448 return False
449
449
450 dsturl = _abssource(self._repo, True)
450 dsturl = _abssource(self._repo, True)
451 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
451 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
452 (subrelpath(self), dsturl))
452 (subrelpath(self), dsturl))
453 other = hg.repository(self._repo.ui, dsturl)
453 other = hg.repository(self._repo.ui, dsturl)
454 return self._repo.push(other, force)
454 return self._repo.push(other, force)
455
455
456 def outgoing(self, ui, dest, opts):
456 def outgoing(self, ui, dest, opts):
457 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
457 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
458
458
459 def incoming(self, ui, source, opts):
459 def incoming(self, ui, source, opts):
460 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
460 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
461
461
462 def files(self):
462 def files(self):
463 rev = self._state[1]
463 rev = self._state[1]
464 ctx = self._repo[rev]
464 ctx = self._repo[rev]
465 return ctx.manifest()
465 return ctx.manifest()
466
466
467 def filedata(self, name):
467 def filedata(self, name):
468 rev = self._state[1]
468 rev = self._state[1]
469 return self._repo[rev][name].data()
469 return self._repo[rev][name].data()
470
470
471 def fileflags(self, name):
471 def fileflags(self, name):
472 rev = self._state[1]
472 rev = self._state[1]
473 ctx = self._repo[rev]
473 ctx = self._repo[rev]
474 return ctx.flags(name)
474 return ctx.flags(name)
475
475
476
476
477 class svnsubrepo(abstractsubrepo):
477 class svnsubrepo(abstractsubrepo):
478 def __init__(self, ctx, path, state):
478 def __init__(self, ctx, path, state):
479 self._path = path
479 self._path = path
480 self._state = state
480 self._state = state
481 self._ctx = ctx
481 self._ctx = ctx
482 self._ui = ctx._repo.ui
482 self._ui = ctx._repo.ui
483
483
484 def _svncommand(self, commands, filename=''):
484 def _svncommand(self, commands, filename=''):
485 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
485 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
486 cmd = ['svn'] + commands + [path]
486 cmd = ['svn'] + commands + [path]
487 cmd = [util.shellquote(arg) for arg in cmd]
487 cmd = [util.shellquote(arg) for arg in cmd]
488 cmd = util.quotecommand(' '.join(cmd))
488 cmd = util.quotecommand(' '.join(cmd))
489 env = dict(os.environ)
489 env = dict(os.environ)
490 # Avoid localized output, preserve current locale for everything else.
490 # Avoid localized output, preserve current locale for everything else.
491 env['LC_MESSAGES'] = 'C'
491 env['LC_MESSAGES'] = 'C'
492 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
492 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
493 close_fds=util.closefds,
493 close_fds=util.closefds,
494 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
494 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
495 universal_newlines=True, env=env)
495 universal_newlines=True, env=env)
496 stdout, stderr = p.communicate()
496 stdout, stderr = p.communicate()
497 stderr = stderr.strip()
497 stderr = stderr.strip()
498 if stderr:
498 if stderr:
499 raise util.Abort(stderr)
499 raise util.Abort(stderr)
500 return stdout
500 return stdout
501
501
502 def _wcrev(self):
502 def _wcrev(self):
503 output = self._svncommand(['info', '--xml'])
503 output = self._svncommand(['info', '--xml'])
504 doc = xml.dom.minidom.parseString(output)
504 doc = xml.dom.minidom.parseString(output)
505 entries = doc.getElementsByTagName('entry')
505 entries = doc.getElementsByTagName('entry')
506 if not entries:
506 if not entries:
507 return '0'
507 return '0'
508 return str(entries[0].getAttribute('revision')) or '0'
508 return str(entries[0].getAttribute('revision')) or '0'
509
509
510 def _wcchanged(self):
510 def _wcchanged(self):
511 """Return (changes, extchanges) where changes is True
511 """Return (changes, extchanges) where changes is True
512 if the working directory was changed, and extchanges is
512 if the working directory was changed, and extchanges is
513 True if any of these changes concern an external entry.
513 True if any of these changes concern an external entry.
514 """
514 """
515 output = self._svncommand(['status', '--xml'])
515 output = self._svncommand(['status', '--xml'])
516 externals, changes = [], []
516 externals, changes = [], []
517 doc = xml.dom.minidom.parseString(output)
517 doc = xml.dom.minidom.parseString(output)
518 for e in doc.getElementsByTagName('entry'):
518 for e in doc.getElementsByTagName('entry'):
519 s = e.getElementsByTagName('wc-status')
519 s = e.getElementsByTagName('wc-status')
520 if not s:
520 if not s:
521 continue
521 continue
522 item = s[0].getAttribute('item')
522 item = s[0].getAttribute('item')
523 props = s[0].getAttribute('props')
523 props = s[0].getAttribute('props')
524 path = e.getAttribute('path')
524 path = e.getAttribute('path')
525 if item == 'external':
525 if item == 'external':
526 externals.append(path)
526 externals.append(path)
527 if (item not in ('', 'normal', 'unversioned', 'external')
527 if (item not in ('', 'normal', 'unversioned', 'external')
528 or props not in ('', 'none')):
528 or props not in ('', 'none')):
529 changes.append(path)
529 changes.append(path)
530 for path in changes:
530 for path in changes:
531 for ext in externals:
531 for ext in externals:
532 if path == ext or path.startswith(ext + os.sep):
532 if path == ext or path.startswith(ext + os.sep):
533 return True, True
533 return True, True
534 return bool(changes), False
534 return bool(changes), False
535
535
536 def dirty(self):
536 def dirty(self):
537 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
537 if self._wcrev() == self._state[1] and not self._wcchanged()[0]:
538 return False
538 return False
539 return True
539 return True
540
540
541 def commit(self, text, user, date):
541 def commit(self, text, user, date):
542 # user and date are out of our hands since svn is centralized
542 # user and date are out of our hands since svn is centralized
543 changed, extchanged = self._wcchanged()
543 changed, extchanged = self._wcchanged()
544 if not changed:
544 if not changed:
545 return self._wcrev()
545 return self._wcrev()
546 if extchanged:
546 if extchanged:
547 # Do not try to commit externals
547 # Do not try to commit externals
548 raise util.Abort(_('cannot commit svn externals'))
548 raise util.Abort(_('cannot commit svn externals'))
549 commitinfo = self._svncommand(['commit', '-m', text])
549 commitinfo = self._svncommand(['commit', '-m', text])
550 self._ui.status(commitinfo)
550 self._ui.status(commitinfo)
551 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
551 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
552 if not newrev:
552 if not newrev:
553 raise util.Abort(commitinfo.splitlines()[-1])
553 raise util.Abort(commitinfo.splitlines()[-1])
554 newrev = newrev.groups()[0]
554 newrev = newrev.groups()[0]
555 self._ui.status(self._svncommand(['update', '-r', newrev]))
555 self._ui.status(self._svncommand(['update', '-r', newrev]))
556 return newrev
556 return newrev
557
557
558 def remove(self):
558 def remove(self):
559 if self.dirty():
559 if self.dirty():
560 self._ui.warn(_('not removing repo %s because '
560 self._ui.warn(_('not removing repo %s because '
561 'it has changes.\n' % self._path))
561 'it has changes.\n' % self._path))
562 return
562 return
563 self._ui.note(_('removing subrepo %s\n') % self._path)
563 self._ui.note(_('removing subrepo %s\n') % self._path)
564
564
565 def onerror(function, path, excinfo):
565 def onerror(function, path, excinfo):
566 if function is not os.remove:
566 if function is not os.remove:
567 raise
567 raise
568 # read-only files cannot be unlinked under Windows
568 # read-only files cannot be unlinked under Windows
569 s = os.stat(path)
569 s = os.stat(path)
570 if (s.st_mode & stat.S_IWRITE) != 0:
570 if (s.st_mode & stat.S_IWRITE) != 0:
571 raise
571 raise
572 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
572 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
573 os.remove(path)
573 os.remove(path)
574
574
575 path = self._ctx._repo.wjoin(self._path)
575 path = self._ctx._repo.wjoin(self._path)
576 shutil.rmtree(path, onerror=onerror)
576 shutil.rmtree(path, onerror=onerror)
577 try:
577 try:
578 os.removedirs(os.path.dirname(path))
578 os.removedirs(os.path.dirname(path))
579 except OSError:
579 except OSError:
580 pass
580 pass
581
581
582 def get(self, state):
582 def get(self, state):
583 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
583 status = self._svncommand(['checkout', state[0], '--revision', state[1]])
584 if not re.search('Checked out revision [0-9]+.', status):
584 if not re.search('Checked out revision [0-9]+.', status):
585 raise util.Abort(status.splitlines()[-1])
585 raise util.Abort(status.splitlines()[-1])
586 self._ui.status(status)
586 self._ui.status(status)
587
587
588 def merge(self, state):
588 def merge(self, state):
589 old = int(self._state[1])
589 old = int(self._state[1])
590 new = int(state[1])
590 new = int(state[1])
591 if new > old:
591 if new > old:
592 self.get(state)
592 self.get(state)
593
593
594 def push(self, force):
594 def push(self, force):
595 # push is a no-op for SVN
595 # push is a no-op for SVN
596 return True
596 return True
597
597
598 def files(self):
598 def files(self):
599 output = self._svncommand(['list'])
599 output = self._svncommand(['list'])
600 # This works because svn forbids \n in filenames.
600 # This works because svn forbids \n in filenames.
601 return output.splitlines()
601 return output.splitlines()
602
602
603 def filedata(self, name):
603 def filedata(self, name):
604 return self._svncommand(['cat'], name)
604 return self._svncommand(['cat'], name)
605
605
606
606
607 class gitsubrepo(object):
607 class gitsubrepo(object):
608 def __init__(self, ctx, path, state):
608 def __init__(self, ctx, path, state):
609 # TODO add git version check.
609 # TODO add git version check.
610 self._state = state
610 self._state = state
611 self._ctx = ctx
611 self._ctx = ctx
612 self._relpath = path
612 self._relpath = path
613 self._path = ctx._repo.wjoin(path)
613 self._path = ctx._repo.wjoin(path)
614 self._ui = ctx._repo.ui
614 self._ui = ctx._repo.ui
615
615
616 def _gitcommand(self, commands, stream=False):
616 def _gitcommand(self, commands, stream=False):
617 return self._gitdir(commands, stream=stream)[0]
617 return self._gitdir(commands, stream=stream)[0]
618
618
619 def _gitdir(self, commands, stream=False):
619 def _gitdir(self, commands, stream=False):
620 commands = ['--no-pager', '--git-dir=%s/.git' % self._path,
620 commands = ['--no-pager', '--git-dir=%s/.git' % self._path,
621 '--work-tree=%s' % self._path] + commands
621 '--work-tree=%s' % self._path] + commands
622 return self._gitnodir(commands, stream=stream)
622 return self._gitnodir(commands, stream=stream)
623
623
624 def _gitnodir(self, commands, stream=False):
624 def _gitnodir(self, commands, stream=False):
625 """Calls the git command
625 """Calls the git command
626
626
627 The methods tries to call the git command. versions previor to 1.6.0
627 The methods tries to call the git command. versions previor to 1.6.0
628 are not supported and very probably fail.
628 are not supported and very probably fail.
629 """
629 """
630 cmd = ['git'] + commands
630 cmd = ['git'] + commands
631 cmd = [util.shellquote(arg) for arg in cmd]
631 cmd = [util.shellquote(arg) for arg in cmd]
632 cmd = util.quotecommand(' '.join(cmd))
632 cmd = util.quotecommand(' '.join(cmd))
633
633
634 # print git's stderr, which is mostly progress and useful info
634 # print git's stderr, which is mostly progress and useful info
635 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
635 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
636 close_fds=util.closefds,
636 close_fds=util.closefds,
637 stdout=subprocess.PIPE)
637 stdout=subprocess.PIPE)
638 if stream:
638 if stream:
639 return p.stdout, None
639 return p.stdout, None
640
640
641 retdata = p.stdout.read().strip()
641 retdata = p.stdout.read().strip()
642 # wait for the child to exit to avoid race condition.
642 # wait for the child to exit to avoid race condition.
643 p.wait()
643 p.wait()
644
644
645 if p.returncode != 0:
645 if p.returncode != 0:
646 # there are certain error codes that are ok
646 # there are certain error codes that are ok
647 command = None
647 command = None
648 for arg in commands:
648 for arg in commands:
649 if not arg.startswith('-'):
649 if not arg.startswith('-'):
650 command = arg
650 command = arg
651 break
651 break
652 if command == 'cat-file':
652 if command == 'cat-file':
653 return retdata, p.returncode
653 return retdata, p.returncode
654 if command in ('commit', 'status') and p.returncode == 1:
654 if command in ('commit', 'status') and p.returncode == 1:
655 return retdata, p.returncode
655 return retdata, p.returncode
656 # for all others, abort
656 # for all others, abort
657 raise util.Abort('git %s error %d' % (command, p.returncode))
657 raise util.Abort('git %s error %d' % (command, p.returncode))
658
658
659 return retdata, p.returncode
659 return retdata, p.returncode
660
660
661 def _gitstate(self):
661 def _gitstate(self):
662 return self._gitcommand(['rev-parse', 'HEAD'])
662 return self._gitcommand(['rev-parse', 'HEAD'])
663
663
664 def _githavelocally(self, revision):
664 def _githavelocally(self, revision):
665 out, code = self._gitdir(['cat-file', '-e', revision])
665 out, code = self._gitdir(['cat-file', '-e', revision])
666 return code == 0
666 return code == 0
667
667
668 def _gitisancestor(self, r1, r2):
668 def _gitisancestor(self, r1, r2):
669 base = self._gitcommand(['merge-base', r1, r2])
669 base = self._gitcommand(['merge-base', r1, r2])
670 return base == r1
670 return base == r1
671
671
672 def _gitbranchmap(self):
672 def _gitbranchmap(self):
673 '''returns 3 things:
673 '''returns 3 things:
674 the current branch,
674 the current branch,
675 a map from git branch to revision
675 a map from git branch to revision
676 a map from revision to branches'''
676 a map from revision to branches'''
677 branch2rev = {}
677 branch2rev = {}
678 rev2branch = {}
678 rev2branch = {}
679 current = None
679 current = None
680 out = self._gitcommand(['branch', '-a', '--no-color',
680 out = self._gitcommand(['branch', '-a', '--no-color',
681 '--verbose', '--no-abbrev'])
681 '--verbose', '--no-abbrev'])
682 for line in out.split('\n'):
682 for line in out.split('\n'):
683 if line[2:].startswith('(no branch)'):
683 if line[2:].startswith('(no branch)'):
684 continue
684 continue
685 branch, revision = line[2:].split()[:2]
685 branch, revision = line[2:].split()[:2]
686 if revision == '->':
686 if revision == '->':
687 continue # ignore remote/HEAD redirects
687 continue # ignore remote/HEAD redirects
688 if line[0] == '*':
688 if line[0] == '*':
689 current = branch
689 current = branch
690 branch2rev[branch] = revision
690 branch2rev[branch] = revision
691 rev2branch.setdefault(revision, []).append(branch)
691 rev2branch.setdefault(revision, []).append(branch)
692 return current, branch2rev, rev2branch
692 return current, branch2rev, rev2branch
693
693
694 def _gittracking(self, branches):
695 'return map of remote branch to local tracking branch'
696 # assumes no more than one local tracking branch for each remote
697 tracking = {}
698 for b in branches:
699 if b.startswith('remotes/'):
700 continue
701 remote = self._gitcommand(['config', 'branch.%s.remote' % b])
702 if remote:
703 ref = self._gitcommand(['config', 'branch.%s.merge' % b])
704 tracking['remotes/%s/%s' % (remote, ref.split('/')[-1])] = b
705 return tracking
706
694 def _fetch(self, source, revision):
707 def _fetch(self, source, revision):
695 if not os.path.exists('%s/.git' % self._path):
708 if not os.path.exists('%s/.git' % self._path):
696 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
709 self._ui.status(_('cloning subrepo %s\n') % self._relpath)
697 self._gitnodir(['clone', source, self._path])
710 self._gitnodir(['clone', source, self._path])
698 if self._githavelocally(revision):
711 if self._githavelocally(revision):
699 return
712 return
700 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
713 self._ui.status(_('pulling subrepo %s\n') % self._relpath)
701 self._gitcommand(['fetch', '--all', '-q'])
714 self._gitcommand(['fetch', '--all', '-q'])
702 if not self._githavelocally(revision):
715 if not self._githavelocally(revision):
703 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
716 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
704 (revision, self._path))
717 (revision, self._path))
705
718
706 def dirty(self):
719 def dirty(self):
707 if self._state[1] != self._gitstate(): # version checked out changed?
720 if self._state[1] != self._gitstate(): # version checked out changed?
708 return True
721 return True
709 # check for staged changes or modified files; ignore untracked files
722 # check for staged changes or modified files; ignore untracked files
710 # docs say --porcelain flag is future-proof format
723 # docs say --porcelain flag is future-proof format
711 changed = self._gitcommand(['status', '--porcelain',
724 changed = self._gitcommand(['status', '--porcelain',
712 '--untracked-files=no'])
725 '--untracked-files=no'])
713 return bool(changed)
726 return bool(changed)
714
727
715 def get(self, state):
728 def get(self, state):
716 source, revision, kind = state
729 source, revision, kind = state
717 self._fetch(source, revision)
730 self._fetch(source, revision)
718 # if the repo was set to be bare, unbare it
731 # if the repo was set to be bare, unbare it
719 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
732 if self._gitcommand(['config', '--bool', 'core.bare']) == 'true':
720 self._gitcommand(['config', 'core.bare', 'false'])
733 self._gitcommand(['config', 'core.bare', 'false'])
721 if self._gitstate() == revision:
734 if self._gitstate() == revision:
722 self._gitcommand(['reset', '--hard', 'HEAD'])
735 self._gitcommand(['reset', '--hard', 'HEAD'])
723 return
736 return
724 elif self._gitstate() == revision:
737 elif self._gitstate() == revision:
725 return
738 return
726 current, branch2rev, rev2branch = self._gitbranchmap()
739 current, branch2rev, rev2branch = self._gitbranchmap()
727 if revision not in rev2branch:
740
741 def rawcheckout():
728 # no branch to checkout, check it out with no branch
742 # no branch to checkout, check it out with no branch
729 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
743 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
730 self._relpath)
744 self._relpath)
731 self._ui.warn(_('check out a git branch if you intend '
745 self._ui.warn(_('check out a git branch if you intend '
732 'to make changes\n'))
746 'to make changes\n'))
733 self._gitcommand(['checkout', '-q', revision])
747 self._gitcommand(['checkout', '-q', revision])
748
749 if revision not in rev2branch:
750 rawcheckout()
734 return
751 return
735 branches = rev2branch[revision]
752 branches = rev2branch[revision]
736 firstlocalbranch = None
753 firstlocalbranch = None
737 for b in branches:
754 for b in branches:
738 if b == 'master':
755 if b == 'master':
739 # master trumps all other branches
756 # master trumps all other branches
740 self._gitcommand(['checkout', 'master'])
757 self._gitcommand(['checkout', 'master'])
741 return
758 return
742 if not firstlocalbranch and not b.startswith('remotes/'):
759 if not firstlocalbranch and not b.startswith('remotes/'):
743 firstlocalbranch = b
760 firstlocalbranch = b
744 if firstlocalbranch:
761 if firstlocalbranch:
745 self._gitcommand(['checkout', firstlocalbranch])
762 self._gitcommand(['checkout', firstlocalbranch])
746 else:
763 return
764
765 tracking = self._gittracking(branch2rev.keys())
766 # choose a remote branch already tracked if possible
747 remote = branches[0]
767 remote = branches[0]
768 if remote not in tracking:
769 for b in branches:
770 if b in tracking:
771 remote = b
772 break
773
774 if remote not in tracking:
775 # create a new local tracking branch
748 local = remote.split('/')[-1]
776 local = remote.split('/')[-1]
749 self._gitcommand(['checkout', '-b', local, remote])
777 self._gitcommand(['checkout', '-b', local, remote])
778 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
779 # When updating to a tracked remote branch,
780 # if the local tracking branch is downstream of it,
781 # a normal `git pull` would have performed a "fast-forward merge"
782 # which is equivalent to updating the local branch to the remote.
783 # Since we are only looking at branching at update, we need to
784 # detect this situation and perform this action lazily.
785 if tracking[remote] != current:
786 self._gitcommand(['checkout', tracking[remote]])
787 self._gitcommand(['merge', '--ff', remote])
788 else:
789 # a real merge would be required, just checkout the revision
790 rawcheckout()
750
791
751 def commit(self, text, user, date):
792 def commit(self, text, user, date):
752 cmd = ['commit', '-a', '-m', text]
793 cmd = ['commit', '-a', '-m', text]
753 if user:
794 if user:
754 cmd += ['--author', user]
795 cmd += ['--author', user]
755 if date:
796 if date:
756 # git's date parser silently ignores when seconds < 1e9
797 # git's date parser silently ignores when seconds < 1e9
757 # convert to ISO8601
798 # convert to ISO8601
758 cmd += ['--date', util.datestr(date, '%Y-%m-%dT%H:%M:%S %1%2')]
799 cmd += ['--date', util.datestr(date, '%Y-%m-%dT%H:%M:%S %1%2')]
759 self._gitcommand(cmd)
800 self._gitcommand(cmd)
760 # make sure commit works otherwise HEAD might not exist under certain
801 # make sure commit works otherwise HEAD might not exist under certain
761 # circumstances
802 # circumstances
762 return self._gitstate()
803 return self._gitstate()
763
804
764 def merge(self, state):
805 def merge(self, state):
765 source, revision, kind = state
806 source, revision, kind = state
766 self._fetch(source, revision)
807 self._fetch(source, revision)
767 base = self._gitcommand(['merge-base', revision, self._state[1]])
808 base = self._gitcommand(['merge-base', revision, self._state[1]])
768 if base == revision:
809 if base == revision:
769 self.get(state) # fast forward merge
810 self.get(state) # fast forward merge
770 elif base != self._state[1]:
811 elif base != self._state[1]:
771 self._gitcommand(['merge', '--no-commit', revision])
812 self._gitcommand(['merge', '--no-commit', revision])
772
813
773 def push(self, force):
814 def push(self, force):
774 # if a branch in origin contains the revision, nothing to do
815 # if a branch in origin contains the revision, nothing to do
775 current, branch2rev, rev2branch = self._gitbranchmap()
816 current, branch2rev, rev2branch = self._gitbranchmap()
776 for b, revision in branch2rev.iteritems():
817 for b, revision in branch2rev.iteritems():
777 if b.startswith('remotes/origin'):
818 if b.startswith('remotes/origin'):
778 if self._gitisancestor(self._state[1], revision):
819 if self._gitisancestor(self._state[1], revision):
779 return True
820 return True
780 # otherwise, try to push the currently checked out branch
821 # otherwise, try to push the currently checked out branch
781 cmd = ['push']
822 cmd = ['push']
782 if force:
823 if force:
783 cmd.append('--force')
824 cmd.append('--force')
784 if current:
825 if current:
785 # determine if the current branch is even useful
826 # determine if the current branch is even useful
786 if not self._gitisancestor(self._state[1], current):
827 if not self._gitisancestor(self._state[1], current):
787 self._ui.warn(_('unrelated git branch checked out '
828 self._ui.warn(_('unrelated git branch checked out '
788 'in subrepo %s\n') % self._relpath)
829 'in subrepo %s\n') % self._relpath)
789 return False
830 return False
790 self._ui.status(_('pushing branch %s of subrepo %s\n') %
831 self._ui.status(_('pushing branch %s of subrepo %s\n') %
791 (current, self._relpath))
832 (current, self._relpath))
792 self._gitcommand(cmd + ['origin', current, '-q'])
833 self._gitcommand(cmd + ['origin', current, '-q'])
793 return True
834 return True
794 else:
835 else:
795 self._ui.warn(_('no branch checked out in subrepo %s\n'
836 self._ui.warn(_('no branch checked out in subrepo %s\n'
796 'cannot push revision %s') %
837 'cannot push revision %s') %
797 (self._relpath, self._state[1]))
838 (self._relpath, self._state[1]))
798 return False
839 return False
799
840
800 def remove(self):
841 def remove(self):
801 if self.dirty():
842 if self.dirty():
802 self._ui.warn(_('not removing repo %s because '
843 self._ui.warn(_('not removing repo %s because '
803 'it has changes.\n') % self._path)
844 'it has changes.\n') % self._path)
804 return
845 return
805 # we can't fully delete the repository as it may contain
846 # we can't fully delete the repository as it may contain
806 # local-only history
847 # local-only history
807 self._ui.note(_('removing subrepo %s\n') % self._path)
848 self._ui.note(_('removing subrepo %s\n') % self._path)
808 self._gitcommand(['config', 'core.bare', 'true'])
849 self._gitcommand(['config', 'core.bare', 'true'])
809 for f in os.listdir(self._path):
850 for f in os.listdir(self._path):
810 if f == '.git':
851 if f == '.git':
811 continue
852 continue
812 path = os.path.join(self._path, f)
853 path = os.path.join(self._path, f)
813 if os.path.isdir(path) and not os.path.islink(path):
854 if os.path.isdir(path) and not os.path.islink(path):
814 shutil.rmtree(path)
855 shutil.rmtree(path)
815 else:
856 else:
816 os.remove(path)
857 os.remove(path)
817
858
818 def archive(self, archiver, prefix):
859 def archive(self, archiver, prefix):
819 source, revision = self._state
860 source, revision = self._state
820 self._fetch(source, revision)
861 self._fetch(source, revision)
821
862
822 # Parse git's native archive command.
863 # Parse git's native archive command.
823 # This should be much faster than manually traversing the trees
864 # This should be much faster than manually traversing the trees
824 # and objects with many subprocess calls.
865 # and objects with many subprocess calls.
825 tarstream = self._gitcommand(['archive', revision], stream=True)
866 tarstream = self._gitcommand(['archive', revision], stream=True)
826 tar = tarfile.open(fileobj=tarstream, mode='r|')
867 tar = tarfile.open(fileobj=tarstream, mode='r|')
827 for info in tar:
868 for info in tar:
828 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
869 archiver.addfile(os.path.join(prefix, self._relpath, info.name),
829 info.mode, info.issym(),
870 info.mode, info.issym(),
830 tar.extractfile(info).read())
871 tar.extractfile(info).read())
831
872
832 types = {
873 types = {
833 'hg': hgsubrepo,
874 'hg': hgsubrepo,
834 'svn': svnsubrepo,
875 'svn': svnsubrepo,
835 'git': gitsubrepo,
876 'git': gitsubrepo,
836 }
877 }
@@ -1,242 +1,266 b''
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 from a different branch
43 record a new commit from upstream from a different branch
44
44
45 $ cd ../gitroot
45 $ cd ../gitroot
46 $ git checkout -b testing
46 $ git checkout -b testing
47 Switched to a new branch 'testing'
47 Switched to a new branch 'testing'
48 $ echo gg >> g
48 $ echo gg >> g
49 $ git commit -q -a -m gg
49 $ git commit -q -a -m gg
50
50
51 $ cd ../t/s
51 $ cd ../t/s
52 $ git pull -q
52 $ git pull -q
53 $ git checkout -b testing origin/testing
53 $ git checkout -b testing origin/testing
54 Switched to a new branch 'testing'
54 Switched to a new branch 'testing'
55 Branch testing set up to track remote branch testing from origin.
55 Branch testing set up to track remote branch testing from origin.
56
56
57 $ cd ..
57 $ cd ..
58 $ hg commit -m 'update git subrepo'
58 $ hg commit -m 'update git subrepo'
59 committing subrepository $TESTTMP/t/s
59 committing subrepository $TESTTMP/t/s
60 $ hg debugsub
60 $ hg debugsub
61 path s
61 path s
62 source ../gitroot
62 source ../gitroot
63 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
63 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
64
64
65 clone root
65 clone root
66
66
67 $ hg clone . ../tc
67 $ hg clone . ../tc
68 updating to branch default
68 updating to branch default
69 cloning subrepo s
69 cloning subrepo s
70 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
70 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
71 $ cd ../tc
71 $ cd ../tc
72 $ hg debugsub
72 $ hg debugsub
73 path s
73 path s
74 source ../gitroot
74 source ../gitroot
75 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
75 revision 126f2a14290cd5ce061fdedc430170e8d39e1c5a
76
76
77 update to previous substate
77 update to previous substate
78
78
79 $ hg update 1
79 $ hg update 1
80 Switched to a new branch 'master'
80 Switched to a new branch 'master'
81 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
81 1 files updated, 0 files merged, 0 files removed, 0 files unresolved
82 $ cat s/g
82 $ cat s/g
83 g
83 g
84 $ hg debugsub
84 $ hg debugsub
85 path s
85 path s
86 source ../gitroot
86 source ../gitroot
87 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
87 revision da5f5b1d8ffcf62fb8327bcd3c89a4367a6018e7
88
88
89 make $GITROOT pushable, by replacing it with a clone with nothing checked out
89 make $GITROOT pushable, by replacing it with a clone with nothing checked out
90
90
91 $ cd ..
91 $ cd ..
92 $ git clone gitroot gitrootbare --bare -q
92 $ git clone gitroot gitrootbare --bare -q
93 $ rm -rf gitroot
93 $ rm -rf gitroot
94 $ mv gitrootbare gitroot
94 $ mv gitrootbare gitroot
95
95
96 clone root, make local change
96 clone root, make local change
97
97
98 $ cd t
98 $ cd t
99 $ hg clone . ../ta
99 $ hg clone . ../ta
100 updating to branch default
100 updating to branch default
101 cloning subrepo s
101 cloning subrepo s
102 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
102 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
103
103
104 $ cd ../ta
104 $ cd ../ta
105 $ echo ggg >> s/g
105 $ echo ggg >> s/g
106 $ hg commit -m ggg
106 $ hg commit -m ggg
107 committing subrepository $TESTTMP/ta/s
107 committing subrepository $TESTTMP/ta/s
108 $ hg debugsub
108 $ hg debugsub
109 path s
109 path s
110 source ../gitroot
110 source ../gitroot
111 revision 79695940086840c99328513acbe35f90fcd55e57
111 revision 79695940086840c99328513acbe35f90fcd55e57
112
112
113 clone root separately, make different local change
113 clone root separately, make different local change
114
114
115 $ cd ../t
115 $ cd ../t
116 $ hg clone . ../tb
116 $ hg clone . ../tb
117 updating to branch default
117 updating to branch default
118 cloning subrepo s
118 cloning subrepo s
119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
119 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
120
120
121 $ cd ../tb/s
121 $ cd ../tb/s
122 $ echo f > f
122 $ echo f > f
123 $ git add f
123 $ git add f
124 $ cd ..
124 $ cd ..
125
125
126 $ hg commit -m f
126 $ hg commit -m f
127 committing subrepository $TESTTMP/tb/s
127 committing subrepository $TESTTMP/tb/s
128 $ hg debugsub
128 $ hg debugsub
129 path s
129 path s
130 source ../gitroot
130 source ../gitroot
131 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
131 revision aa84837ccfbdfedcdcdeeedc309d73e6eb069edc
132
132
133 user b push changes
133 user b push changes
134
134
135 $ hg push
135 $ hg push
136 pushing to $TESTTMP/t
136 pushing to $TESTTMP/t
137 pushing branch testing of subrepo s
137 pushing branch testing of subrepo s
138 searching for changes
138 searching for changes
139 adding changesets
139 adding changesets
140 adding manifests
140 adding manifests
141 adding file changes
141 adding file changes
142 added 1 changesets with 1 changes to 1 files
142 added 1 changesets with 1 changes to 1 files
143
143
144 user a pulls, merges, commits
144 user a pulls, merges, commits
145
145
146 $ cd ../ta
146 $ cd ../ta
147 $ hg pull
147 $ hg pull
148 pulling from $TESTTMP/t
148 pulling from $TESTTMP/t
149 searching for changes
149 searching for changes
150 adding changesets
150 adding changesets
151 adding manifests
151 adding manifests
152 adding file changes
152 adding file changes
153 added 1 changesets with 1 changes to 1 files (+1 heads)
153 added 1 changesets with 1 changes to 1 files (+1 heads)
154 (run 'hg heads' to see heads, 'hg merge' to merge)
154 (run 'hg heads' to see heads, 'hg merge' to merge)
155 $ hg merge
155 $ hg merge
156 Automatic merge went well; stopped before committing as requested
156 Automatic merge went well; stopped before committing as requested
157 pulling subrepo s
157 pulling subrepo s
158 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
158 0 files updated, 0 files merged, 0 files removed, 0 files unresolved
159 (branch merge, don't forget to commit)
159 (branch merge, don't forget to commit)
160 $ cat s/f
160 $ cat s/f
161 f
161 f
162 $ cat s/g
162 $ cat s/g
163 g
163 g
164 gg
164 gg
165 ggg
165 ggg
166 $ hg commit -m 'merge'
166 $ hg commit -m 'merge'
167 committing subrepository $TESTTMP/ta/s
167 committing subrepository $TESTTMP/ta/s
168 $ hg debugsub
168 $ hg debugsub
169 path s
169 path s
170 source ../gitroot
170 source ../gitroot
171 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
171 revision f47b465e1bce645dbf37232a00574aa1546ca8d3
172 $ hg push
172 $ hg push
173 pushing to $TESTTMP/t
173 pushing to $TESTTMP/t
174 pushing branch testing of subrepo s
174 pushing branch testing of subrepo s
175 searching for changes
175 searching for changes
176 adding changesets
176 adding changesets
177 adding manifests
177 adding manifests
178 adding file changes
178 adding file changes
179 added 2 changesets with 2 changes to 1 files
179 added 2 changesets with 2 changes to 1 files
180
180
181 make upstream git changes
181 make upstream git changes
182
182
183 $ cd ..
183 $ cd ..
184 $ git clone -q gitroot gitclone
184 $ git clone -q gitroot gitclone
185 $ cd gitclone
185 $ cd gitclone
186 $ echo ff >> f
186 $ echo ff >> f
187 $ git commit -q -a -m ff
187 $ git commit -q -a -m ff
188 $ echo fff >> f
188 $ echo fff >> f
189 $ git commit -q -a -m fff
189 $ git commit -q -a -m fff
190 $ git push -q origin testing
190 $ git push -q origin testing
191
191
192 make and push changes to hg without updating the subrepo
192 make and push changes to hg without updating the subrepo
193
193
194 $ cd ../t
194 $ cd ../t
195 $ hg clone . ../td
195 $ hg clone . ../td
196 updating to branch default
196 updating to branch default
197 cloning subrepo s
197 cloning subrepo s
198 checking out detached HEAD in subrepo s
198 checking out detached HEAD in subrepo s
199 check out a git branch if you intend to make changes
199 check out a git branch if you intend to make changes
200 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
200 3 files updated, 0 files merged, 0 files removed, 0 files unresolved
201 $ cd ../td
201 $ cd ../td
202 $ echo aa >> a
202 $ echo aa >> a
203 $ hg commit -m aa
203 $ hg commit -m aa
204 $ hg push
204 $ hg push
205 pushing to $TESTTMP/t
205 pushing to $TESTTMP/t
206 searching for changes
206 searching for changes
207 adding changesets
207 adding changesets
208 adding manifests
208 adding manifests
209 adding file changes
209 adding file changes
210 added 1 changesets with 1 changes to 1 files
210 added 1 changesets with 1 changes to 1 files
211
211
212 sync to upstream git, distribute changes
213
214 $ cd ../ta
215 $ hg pull -u -q
216 $ cd s
217 $ git pull -q
218 $ cd ..
219 $ hg commit -m 'git upstream sync'
220 committing subrepository $TESTTMP/ta/s
221 $ hg debugsub
222 path s
223 source ../gitroot
224 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
225 $ hg push -q
226
227 $ cd ../tb
228 $ hg pull -q
229 $ hg update
230 pulling subrepo s
231 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
232 $ hg debugsub
233 path s
234 source ../gitroot
235 revision 32a343883b74769118bb1d3b4b1fbf9156f4dddc
236
212 update to a revision without the subrepo, keeping the local git repository
237 update to a revision without the subrepo, keeping the local git repository
213
238
214 $ cd ../t
239 $ cd ../t
215 $ hg up 0
240 $ hg up 0
216 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
241 0 files updated, 0 files merged, 2 files removed, 0 files unresolved
217 $ ls -a s
242 $ ls -a s
218 .
243 .
219 ..
244 ..
220 .git
245 .git
221
246
222 $ hg up 2
247 $ hg up 2
223 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
248 2 files updated, 0 files merged, 0 files removed, 0 files unresolved
224 $ ls -a s
249 $ ls -a s
225 .
250 .
226 ..
251 ..
227 .git
252 .git
228 g
253 g
229
254
230 archive subrepos
255 archive subrepos
231
256
232 $ cd ../t
257 $ cd ../t
233 $ hg archive --subrepos -r tip ../archive
258 $ hg archive --subrepos -r 5 ../archive
234 pulling subrepo s
259 pulling subrepo s
235 $ cd ../archive
260 $ cd ../archive
236 $ cat s/f
261 $ cat s/f
237 f
262 f
238 $ cat s/g
263 $ cat s/g
239 g
264 g
240 gg
265 gg
241 ggg
266 ggg
242
General Comments 0
You need to be logged in to leave comments. Login now