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