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