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