##// END OF EJS Templates
subrepo: fix for merge inconsistencies...
Friedrich Kastner-Masilko -
r16196:8ae7626d stable
parent child Browse files
Show More
@@ -1,1149 +1,1149 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 checknested(self, path):
278 def checknested(self, path):
279 """check if path is a subrepository within this repository"""
279 """check if path is a subrepository within this repository"""
280 return False
280 return False
281
281
282 def commit(self, text, user, date):
282 def commit(self, text, user, date):
283 """commit the current changes to the subrepo with the given
283 """commit the current changes to the subrepo with the given
284 log message. Use given user and date if possible. Return the
284 log message. Use given user and date if possible. Return the
285 new state of the subrepo.
285 new state of the subrepo.
286 """
286 """
287 raise NotImplementedError
287 raise NotImplementedError
288
288
289 def remove(self):
289 def remove(self):
290 """remove the subrepo
290 """remove the subrepo
291
291
292 (should verify the dirstate is not dirty first)
292 (should verify the dirstate is not dirty first)
293 """
293 """
294 raise NotImplementedError
294 raise NotImplementedError
295
295
296 def get(self, state, overwrite=False):
296 def get(self, state, overwrite=False):
297 """run whatever commands are needed to put the subrepo into
297 """run whatever commands are needed to put the subrepo into
298 this state
298 this state
299 """
299 """
300 raise NotImplementedError
300 raise NotImplementedError
301
301
302 def merge(self, state):
302 def merge(self, state):
303 """merge currently-saved state with the new state."""
303 """merge currently-saved state with the new state."""
304 raise NotImplementedError
304 raise NotImplementedError
305
305
306 def push(self, opts):
306 def push(self, opts):
307 """perform whatever action is analogous to 'hg push'
307 """perform whatever action is analogous to 'hg push'
308
308
309 This may be a no-op on some systems.
309 This may be a no-op on some systems.
310 """
310 """
311 raise NotImplementedError
311 raise NotImplementedError
312
312
313 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
313 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
314 return []
314 return []
315
315
316 def status(self, rev2, **opts):
316 def status(self, rev2, **opts):
317 return [], [], [], [], [], [], []
317 return [], [], [], [], [], [], []
318
318
319 def diff(self, diffopts, node2, match, prefix, **opts):
319 def diff(self, diffopts, node2, match, prefix, **opts):
320 pass
320 pass
321
321
322 def outgoing(self, ui, dest, opts):
322 def outgoing(self, ui, dest, opts):
323 return 1
323 return 1
324
324
325 def incoming(self, ui, source, opts):
325 def incoming(self, ui, source, opts):
326 return 1
326 return 1
327
327
328 def files(self):
328 def files(self):
329 """return filename iterator"""
329 """return filename iterator"""
330 raise NotImplementedError
330 raise NotImplementedError
331
331
332 def filedata(self, name):
332 def filedata(self, name):
333 """return file data"""
333 """return file data"""
334 raise NotImplementedError
334 raise NotImplementedError
335
335
336 def fileflags(self, name):
336 def fileflags(self, name):
337 """return file flags"""
337 """return file flags"""
338 return ''
338 return ''
339
339
340 def archive(self, ui, archiver, prefix):
340 def archive(self, ui, archiver, prefix):
341 files = self.files()
341 files = self.files()
342 total = len(files)
342 total = len(files)
343 relpath = subrelpath(self)
343 relpath = subrelpath(self)
344 ui.progress(_('archiving (%s)') % relpath, 0,
344 ui.progress(_('archiving (%s)') % relpath, 0,
345 unit=_('files'), total=total)
345 unit=_('files'), total=total)
346 for i, name in enumerate(files):
346 for i, name in enumerate(files):
347 flags = self.fileflags(name)
347 flags = self.fileflags(name)
348 mode = 'x' in flags and 0755 or 0644
348 mode = 'x' in flags and 0755 or 0644
349 symlink = 'l' in flags
349 symlink = 'l' in flags
350 archiver.addfile(os.path.join(prefix, self._path, name),
350 archiver.addfile(os.path.join(prefix, self._path, name),
351 mode, symlink, self.filedata(name))
351 mode, symlink, self.filedata(name))
352 ui.progress(_('archiving (%s)') % relpath, i + 1,
352 ui.progress(_('archiving (%s)') % relpath, i + 1,
353 unit=_('files'), total=total)
353 unit=_('files'), total=total)
354 ui.progress(_('archiving (%s)') % relpath, None)
354 ui.progress(_('archiving (%s)') % relpath, None)
355
355
356 def walk(self, match):
356 def walk(self, match):
357 '''
357 '''
358 walk recursively through the directory tree, finding all files
358 walk recursively through the directory tree, finding all files
359 matched by the match function
359 matched by the match function
360 '''
360 '''
361 pass
361 pass
362
362
363 def forget(self, ui, match, prefix):
363 def forget(self, ui, match, prefix):
364 return []
364 return []
365
365
366 class hgsubrepo(abstractsubrepo):
366 class hgsubrepo(abstractsubrepo):
367 def __init__(self, ctx, path, state):
367 def __init__(self, ctx, path, state):
368 self._path = path
368 self._path = path
369 self._state = state
369 self._state = state
370 r = ctx._repo
370 r = ctx._repo
371 root = r.wjoin(path)
371 root = r.wjoin(path)
372 create = False
372 create = False
373 if not os.path.exists(os.path.join(root, '.hg')):
373 if not os.path.exists(os.path.join(root, '.hg')):
374 create = True
374 create = True
375 util.makedirs(root)
375 util.makedirs(root)
376 self._repo = hg.repository(r.ui, root, create=create)
376 self._repo = hg.repository(r.ui, root, create=create)
377 self._initrepo(r, state[0], create)
377 self._initrepo(r, state[0], create)
378
378
379 def _initrepo(self, parentrepo, source, create):
379 def _initrepo(self, parentrepo, source, create):
380 self._repo._subparent = parentrepo
380 self._repo._subparent = parentrepo
381 self._repo._subsource = source
381 self._repo._subsource = source
382
382
383 if create:
383 if create:
384 fp = self._repo.opener("hgrc", "w", text=True)
384 fp = self._repo.opener("hgrc", "w", text=True)
385 fp.write('[paths]\n')
385 fp.write('[paths]\n')
386
386
387 def addpathconfig(key, value):
387 def addpathconfig(key, value):
388 if value:
388 if value:
389 fp.write('%s = %s\n' % (key, value))
389 fp.write('%s = %s\n' % (key, value))
390 self._repo.ui.setconfig('paths', key, value)
390 self._repo.ui.setconfig('paths', key, value)
391
391
392 defpath = _abssource(self._repo, abort=False)
392 defpath = _abssource(self._repo, abort=False)
393 defpushpath = _abssource(self._repo, True, abort=False)
393 defpushpath = _abssource(self._repo, True, abort=False)
394 addpathconfig('default', defpath)
394 addpathconfig('default', defpath)
395 if defpath != defpushpath:
395 if defpath != defpushpath:
396 addpathconfig('default-push', defpushpath)
396 addpathconfig('default-push', defpushpath)
397 fp.close()
397 fp.close()
398
398
399 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
399 def add(self, ui, match, dryrun, listsubrepos, prefix, explicitonly):
400 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
400 return cmdutil.add(ui, self._repo, match, dryrun, listsubrepos,
401 os.path.join(prefix, self._path), explicitonly)
401 os.path.join(prefix, self._path), explicitonly)
402
402
403 def status(self, rev2, **opts):
403 def status(self, rev2, **opts):
404 try:
404 try:
405 rev1 = self._state[1]
405 rev1 = self._state[1]
406 ctx1 = self._repo[rev1]
406 ctx1 = self._repo[rev1]
407 ctx2 = self._repo[rev2]
407 ctx2 = self._repo[rev2]
408 return self._repo.status(ctx1, ctx2, **opts)
408 return self._repo.status(ctx1, ctx2, **opts)
409 except error.RepoLookupError, inst:
409 except error.RepoLookupError, inst:
410 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
410 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
411 % (inst, subrelpath(self)))
411 % (inst, subrelpath(self)))
412 return [], [], [], [], [], [], []
412 return [], [], [], [], [], [], []
413
413
414 def diff(self, diffopts, node2, match, prefix, **opts):
414 def diff(self, diffopts, node2, match, prefix, **opts):
415 try:
415 try:
416 node1 = node.bin(self._state[1])
416 node1 = node.bin(self._state[1])
417 # We currently expect node2 to come from substate and be
417 # We currently expect node2 to come from substate and be
418 # in hex format
418 # in hex format
419 if node2 is not None:
419 if node2 is not None:
420 node2 = node.bin(node2)
420 node2 = node.bin(node2)
421 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
421 cmdutil.diffordiffstat(self._repo.ui, self._repo, diffopts,
422 node1, node2, match,
422 node1, node2, match,
423 prefix=os.path.join(prefix, self._path),
423 prefix=os.path.join(prefix, self._path),
424 listsubrepos=True, **opts)
424 listsubrepos=True, **opts)
425 except error.RepoLookupError, inst:
425 except error.RepoLookupError, inst:
426 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
426 self._repo.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
427 % (inst, subrelpath(self)))
427 % (inst, subrelpath(self)))
428
428
429 def archive(self, ui, archiver, prefix):
429 def archive(self, ui, archiver, prefix):
430 self._get(self._state + ('hg',))
430 self._get(self._state + ('hg',))
431 abstractsubrepo.archive(self, ui, archiver, prefix)
431 abstractsubrepo.archive(self, ui, archiver, prefix)
432
432
433 rev = self._state[1]
433 rev = self._state[1]
434 ctx = self._repo[rev]
434 ctx = self._repo[rev]
435 for subpath in ctx.substate:
435 for subpath in ctx.substate:
436 s = subrepo(ctx, subpath)
436 s = subrepo(ctx, subpath)
437 s.archive(ui, archiver, os.path.join(prefix, self._path))
437 s.archive(ui, archiver, os.path.join(prefix, self._path))
438
438
439 def dirty(self, ignoreupdate=False):
439 def dirty(self, ignoreupdate=False):
440 r = self._state[1]
440 r = self._state[1]
441 if r == '' and not ignoreupdate: # no state recorded
441 if r == '' and not ignoreupdate: # no state recorded
442 return True
442 return True
443 w = self._repo[None]
443 w = self._repo[None]
444 if r != w.p1().hex() and not ignoreupdate:
444 if r != w.p1().hex() and not ignoreupdate:
445 # different version checked out
445 # different version checked out
446 return True
446 return True
447 return w.dirty() # working directory changed
447 return w.dirty() # working directory changed
448
448
449 def checknested(self, path):
449 def checknested(self, path):
450 return self._repo._checknested(self._repo.wjoin(path))
450 return self._repo._checknested(self._repo.wjoin(path))
451
451
452 def commit(self, text, user, date):
452 def commit(self, text, user, date):
453 # don't bother committing in the subrepo if it's only been
453 # don't bother committing in the subrepo if it's only been
454 # updated
454 # updated
455 if not self.dirty(True):
455 if not self.dirty(True):
456 return self._repo['.'].hex()
456 return self._repo['.'].hex()
457 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
457 self._repo.ui.debug("committing subrepo %s\n" % subrelpath(self))
458 n = self._repo.commit(text, user, date)
458 n = self._repo.commit(text, user, date)
459 if not n:
459 if not n:
460 return self._repo['.'].hex() # different version checked out
460 return self._repo['.'].hex() # different version checked out
461 return node.hex(n)
461 return node.hex(n)
462
462
463 def remove(self):
463 def remove(self):
464 # we can't fully delete the repository as it may contain
464 # we can't fully delete the repository as it may contain
465 # local-only history
465 # local-only history
466 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
466 self._repo.ui.note(_('removing subrepo %s\n') % subrelpath(self))
467 hg.clean(self._repo, node.nullid, False)
467 hg.clean(self._repo, node.nullid, False)
468
468
469 def _get(self, state):
469 def _get(self, state):
470 source, revision, kind = state
470 source, revision, kind = state
471 if revision not in self._repo:
471 if revision not in self._repo:
472 self._repo._subsource = source
472 self._repo._subsource = source
473 srcurl = _abssource(self._repo)
473 srcurl = _abssource(self._repo)
474 other = hg.peer(self._repo.ui, {}, srcurl)
474 other = hg.peer(self._repo.ui, {}, srcurl)
475 if len(self._repo) == 0:
475 if len(self._repo) == 0:
476 self._repo.ui.status(_('cloning subrepo %s from %s\n')
476 self._repo.ui.status(_('cloning subrepo %s from %s\n')
477 % (subrelpath(self), srcurl))
477 % (subrelpath(self), srcurl))
478 parentrepo = self._repo._subparent
478 parentrepo = self._repo._subparent
479 shutil.rmtree(self._repo.path)
479 shutil.rmtree(self._repo.path)
480 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
480 other, self._repo = hg.clone(self._repo._subparent.ui, {}, other,
481 self._repo.root, update=False)
481 self._repo.root, update=False)
482 self._initrepo(parentrepo, source, create=True)
482 self._initrepo(parentrepo, source, create=True)
483 else:
483 else:
484 self._repo.ui.status(_('pulling subrepo %s from %s\n')
484 self._repo.ui.status(_('pulling subrepo %s from %s\n')
485 % (subrelpath(self), srcurl))
485 % (subrelpath(self), srcurl))
486 self._repo.pull(other)
486 self._repo.pull(other)
487 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
487 bookmarks.updatefromremote(self._repo.ui, self._repo, other,
488 srcurl)
488 srcurl)
489
489
490 def get(self, state, overwrite=False):
490 def get(self, state, overwrite=False):
491 self._get(state)
491 self._get(state)
492 source, revision, kind = state
492 source, revision, kind = state
493 self._repo.ui.debug("getting subrepo %s\n" % self._path)
493 self._repo.ui.debug("getting subrepo %s\n" % self._path)
494 hg.clean(self._repo, revision, False)
494 hg.clean(self._repo, revision, False)
495
495
496 def merge(self, state):
496 def merge(self, state):
497 self._get(state)
497 self._get(state)
498 cur = self._repo['.']
498 cur = self._repo['.']
499 dst = self._repo[state[1]]
499 dst = self._repo[state[1]]
500 anc = dst.ancestor(cur)
500 anc = dst.ancestor(cur)
501
501
502 def mergefunc():
502 def mergefunc():
503 if anc == cur:
503 if anc == cur and dst.branch() == cur.branch():
504 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
504 self._repo.ui.debug("updating subrepo %s\n" % subrelpath(self))
505 hg.update(self._repo, state[1])
505 hg.update(self._repo, state[1])
506 elif anc == dst:
506 elif anc == dst:
507 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
507 self._repo.ui.debug("skipping subrepo %s\n" % subrelpath(self))
508 else:
508 else:
509 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
509 self._repo.ui.debug("merging subrepo %s\n" % subrelpath(self))
510 hg.merge(self._repo, state[1], remind=False)
510 hg.merge(self._repo, state[1], remind=False)
511
511
512 wctx = self._repo[None]
512 wctx = self._repo[None]
513 if self.dirty():
513 if self.dirty():
514 if anc != dst:
514 if anc != dst:
515 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
515 if _updateprompt(self._repo.ui, self, wctx.dirty(), cur, dst):
516 mergefunc()
516 mergefunc()
517 else:
517 else:
518 mergefunc()
518 mergefunc()
519 else:
519 else:
520 mergefunc()
520 mergefunc()
521
521
522 def push(self, opts):
522 def push(self, opts):
523 force = opts.get('force')
523 force = opts.get('force')
524 newbranch = opts.get('new_branch')
524 newbranch = opts.get('new_branch')
525 ssh = opts.get('ssh')
525 ssh = opts.get('ssh')
526
526
527 # push subrepos depth-first for coherent ordering
527 # push subrepos depth-first for coherent ordering
528 c = self._repo['']
528 c = self._repo['']
529 subs = c.substate # only repos that are committed
529 subs = c.substate # only repos that are committed
530 for s in sorted(subs):
530 for s in sorted(subs):
531 if c.sub(s).push(opts) == 0:
531 if c.sub(s).push(opts) == 0:
532 return False
532 return False
533
533
534 dsturl = _abssource(self._repo, True)
534 dsturl = _abssource(self._repo, True)
535 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
535 self._repo.ui.status(_('pushing subrepo %s to %s\n') %
536 (subrelpath(self), dsturl))
536 (subrelpath(self), dsturl))
537 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
537 other = hg.peer(self._repo.ui, {'ssh': ssh}, dsturl)
538 return self._repo.push(other, force, newbranch=newbranch)
538 return self._repo.push(other, force, newbranch=newbranch)
539
539
540 def outgoing(self, ui, dest, opts):
540 def outgoing(self, ui, dest, opts):
541 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
541 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
542
542
543 def incoming(self, ui, source, opts):
543 def incoming(self, ui, source, opts):
544 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
544 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
545
545
546 def files(self):
546 def files(self):
547 rev = self._state[1]
547 rev = self._state[1]
548 ctx = self._repo[rev]
548 ctx = self._repo[rev]
549 return ctx.manifest()
549 return ctx.manifest()
550
550
551 def filedata(self, name):
551 def filedata(self, name):
552 rev = self._state[1]
552 rev = self._state[1]
553 return self._repo[rev][name].data()
553 return self._repo[rev][name].data()
554
554
555 def fileflags(self, name):
555 def fileflags(self, name):
556 rev = self._state[1]
556 rev = self._state[1]
557 ctx = self._repo[rev]
557 ctx = self._repo[rev]
558 return ctx.flags(name)
558 return ctx.flags(name)
559
559
560 def walk(self, match):
560 def walk(self, match):
561 ctx = self._repo[None]
561 ctx = self._repo[None]
562 return ctx.walk(match)
562 return ctx.walk(match)
563
563
564 def forget(self, ui, match, prefix):
564 def forget(self, ui, match, prefix):
565 return cmdutil.forget(ui, self._repo, match,
565 return cmdutil.forget(ui, self._repo, match,
566 os.path.join(prefix, self._path), True)
566 os.path.join(prefix, self._path), True)
567
567
568 class svnsubrepo(abstractsubrepo):
568 class svnsubrepo(abstractsubrepo):
569 def __init__(self, ctx, path, state):
569 def __init__(self, ctx, path, state):
570 self._path = path
570 self._path = path
571 self._state = state
571 self._state = state
572 self._ctx = ctx
572 self._ctx = ctx
573 self._ui = ctx._repo.ui
573 self._ui = ctx._repo.ui
574 self._exe = util.findexe('svn')
574 self._exe = util.findexe('svn')
575 if not self._exe:
575 if not self._exe:
576 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
576 raise util.Abort(_("'svn' executable not found for subrepo '%s'")
577 % self._path)
577 % self._path)
578
578
579 def _svncommand(self, commands, filename='', failok=False):
579 def _svncommand(self, commands, filename='', failok=False):
580 cmd = [self._exe]
580 cmd = [self._exe]
581 extrakw = {}
581 extrakw = {}
582 if not self._ui.interactive():
582 if not self._ui.interactive():
583 # Making stdin be a pipe should prevent svn from behaving
583 # Making stdin be a pipe should prevent svn from behaving
584 # interactively even if we can't pass --non-interactive.
584 # interactively even if we can't pass --non-interactive.
585 extrakw['stdin'] = subprocess.PIPE
585 extrakw['stdin'] = subprocess.PIPE
586 # Starting in svn 1.5 --non-interactive is a global flag
586 # Starting in svn 1.5 --non-interactive is a global flag
587 # instead of being per-command, but we need to support 1.4 so
587 # instead of being per-command, but we need to support 1.4 so
588 # we have to be intelligent about what commands take
588 # we have to be intelligent about what commands take
589 # --non-interactive.
589 # --non-interactive.
590 if commands[0] in ('update', 'checkout', 'commit'):
590 if commands[0] in ('update', 'checkout', 'commit'):
591 cmd.append('--non-interactive')
591 cmd.append('--non-interactive')
592 cmd.extend(commands)
592 cmd.extend(commands)
593 if filename is not None:
593 if filename is not None:
594 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
594 path = os.path.join(self._ctx._repo.origroot, self._path, filename)
595 cmd.append(path)
595 cmd.append(path)
596 env = dict(os.environ)
596 env = dict(os.environ)
597 # Avoid localized output, preserve current locale for everything else.
597 # Avoid localized output, preserve current locale for everything else.
598 env['LC_MESSAGES'] = 'C'
598 env['LC_MESSAGES'] = 'C'
599 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
599 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
600 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
600 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
601 universal_newlines=True, env=env, **extrakw)
601 universal_newlines=True, env=env, **extrakw)
602 stdout, stderr = p.communicate()
602 stdout, stderr = p.communicate()
603 stderr = stderr.strip()
603 stderr = stderr.strip()
604 if not failok:
604 if not failok:
605 if p.returncode:
605 if p.returncode:
606 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
606 raise util.Abort(stderr or 'exited with code %d' % p.returncode)
607 if stderr:
607 if stderr:
608 self._ui.warn(stderr + '\n')
608 self._ui.warn(stderr + '\n')
609 return stdout, stderr
609 return stdout, stderr
610
610
611 @propertycache
611 @propertycache
612 def _svnversion(self):
612 def _svnversion(self):
613 output, err = self._svncommand(['--version'], filename=None)
613 output, err = self._svncommand(['--version'], filename=None)
614 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
614 m = re.search(r'^svn,\s+version\s+(\d+)\.(\d+)', output)
615 if not m:
615 if not m:
616 raise util.Abort(_('cannot retrieve svn tool version'))
616 raise util.Abort(_('cannot retrieve svn tool version'))
617 return (int(m.group(1)), int(m.group(2)))
617 return (int(m.group(1)), int(m.group(2)))
618
618
619 def _wcrevs(self):
619 def _wcrevs(self):
620 # Get the working directory revision as well as the last
620 # Get the working directory revision as well as the last
621 # commit revision so we can compare the subrepo state with
621 # commit revision so we can compare the subrepo state with
622 # both. We used to store the working directory one.
622 # both. We used to store the working directory one.
623 output, err = self._svncommand(['info', '--xml'])
623 output, err = self._svncommand(['info', '--xml'])
624 doc = xml.dom.minidom.parseString(output)
624 doc = xml.dom.minidom.parseString(output)
625 entries = doc.getElementsByTagName('entry')
625 entries = doc.getElementsByTagName('entry')
626 lastrev, rev = '0', '0'
626 lastrev, rev = '0', '0'
627 if entries:
627 if entries:
628 rev = str(entries[0].getAttribute('revision')) or '0'
628 rev = str(entries[0].getAttribute('revision')) or '0'
629 commits = entries[0].getElementsByTagName('commit')
629 commits = entries[0].getElementsByTagName('commit')
630 if commits:
630 if commits:
631 lastrev = str(commits[0].getAttribute('revision')) or '0'
631 lastrev = str(commits[0].getAttribute('revision')) or '0'
632 return (lastrev, rev)
632 return (lastrev, rev)
633
633
634 def _wcrev(self):
634 def _wcrev(self):
635 return self._wcrevs()[0]
635 return self._wcrevs()[0]
636
636
637 def _wcchanged(self):
637 def _wcchanged(self):
638 """Return (changes, extchanges) where changes is True
638 """Return (changes, extchanges) where changes is True
639 if the working directory was changed, and extchanges is
639 if the working directory was changed, and extchanges is
640 True if any of these changes concern an external entry.
640 True if any of these changes concern an external entry.
641 """
641 """
642 output, err = self._svncommand(['status', '--xml'])
642 output, err = self._svncommand(['status', '--xml'])
643 externals, changes = [], []
643 externals, changes = [], []
644 doc = xml.dom.minidom.parseString(output)
644 doc = xml.dom.minidom.parseString(output)
645 for e in doc.getElementsByTagName('entry'):
645 for e in doc.getElementsByTagName('entry'):
646 s = e.getElementsByTagName('wc-status')
646 s = e.getElementsByTagName('wc-status')
647 if not s:
647 if not s:
648 continue
648 continue
649 item = s[0].getAttribute('item')
649 item = s[0].getAttribute('item')
650 props = s[0].getAttribute('props')
650 props = s[0].getAttribute('props')
651 path = e.getAttribute('path')
651 path = e.getAttribute('path')
652 if item == 'external':
652 if item == 'external':
653 externals.append(path)
653 externals.append(path)
654 if (item not in ('', 'normal', 'unversioned', 'external')
654 if (item not in ('', 'normal', 'unversioned', 'external')
655 or props not in ('', 'none', 'normal')):
655 or props not in ('', 'none', 'normal')):
656 changes.append(path)
656 changes.append(path)
657 for path in changes:
657 for path in changes:
658 for ext in externals:
658 for ext in externals:
659 if path == ext or path.startswith(ext + os.sep):
659 if path == ext or path.startswith(ext + os.sep):
660 return True, True
660 return True, True
661 return bool(changes), False
661 return bool(changes), False
662
662
663 def dirty(self, ignoreupdate=False):
663 def dirty(self, ignoreupdate=False):
664 if not self._wcchanged()[0]:
664 if not self._wcchanged()[0]:
665 if self._state[1] in self._wcrevs() or ignoreupdate:
665 if self._state[1] in self._wcrevs() or ignoreupdate:
666 return False
666 return False
667 return True
667 return True
668
668
669 def commit(self, text, user, date):
669 def commit(self, text, user, date):
670 # user and date are out of our hands since svn is centralized
670 # user and date are out of our hands since svn is centralized
671 changed, extchanged = self._wcchanged()
671 changed, extchanged = self._wcchanged()
672 if not changed:
672 if not changed:
673 return self._wcrev()
673 return self._wcrev()
674 if extchanged:
674 if extchanged:
675 # Do not try to commit externals
675 # Do not try to commit externals
676 raise util.Abort(_('cannot commit svn externals'))
676 raise util.Abort(_('cannot commit svn externals'))
677 commitinfo, err = self._svncommand(['commit', '-m', text])
677 commitinfo, err = self._svncommand(['commit', '-m', text])
678 self._ui.status(commitinfo)
678 self._ui.status(commitinfo)
679 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
679 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
680 if not newrev:
680 if not newrev:
681 raise util.Abort(commitinfo.splitlines()[-1])
681 raise util.Abort(commitinfo.splitlines()[-1])
682 newrev = newrev.groups()[0]
682 newrev = newrev.groups()[0]
683 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
683 self._ui.status(self._svncommand(['update', '-r', newrev])[0])
684 return newrev
684 return newrev
685
685
686 def remove(self):
686 def remove(self):
687 if self.dirty():
687 if self.dirty():
688 self._ui.warn(_('not removing repo %s because '
688 self._ui.warn(_('not removing repo %s because '
689 'it has changes.\n' % self._path))
689 'it has changes.\n' % self._path))
690 return
690 return
691 self._ui.note(_('removing subrepo %s\n') % self._path)
691 self._ui.note(_('removing subrepo %s\n') % self._path)
692
692
693 def onerror(function, path, excinfo):
693 def onerror(function, path, excinfo):
694 if function is not os.remove:
694 if function is not os.remove:
695 raise
695 raise
696 # read-only files cannot be unlinked under Windows
696 # read-only files cannot be unlinked under Windows
697 s = os.stat(path)
697 s = os.stat(path)
698 if (s.st_mode & stat.S_IWRITE) != 0:
698 if (s.st_mode & stat.S_IWRITE) != 0:
699 raise
699 raise
700 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
700 os.chmod(path, stat.S_IMODE(s.st_mode) | stat.S_IWRITE)
701 os.remove(path)
701 os.remove(path)
702
702
703 path = self._ctx._repo.wjoin(self._path)
703 path = self._ctx._repo.wjoin(self._path)
704 shutil.rmtree(path, onerror=onerror)
704 shutil.rmtree(path, onerror=onerror)
705 try:
705 try:
706 os.removedirs(os.path.dirname(path))
706 os.removedirs(os.path.dirname(path))
707 except OSError:
707 except OSError:
708 pass
708 pass
709
709
710 def get(self, state, overwrite=False):
710 def get(self, state, overwrite=False):
711 if overwrite:
711 if overwrite:
712 self._svncommand(['revert', '--recursive'])
712 self._svncommand(['revert', '--recursive'])
713 args = ['checkout']
713 args = ['checkout']
714 if self._svnversion >= (1, 5):
714 if self._svnversion >= (1, 5):
715 args.append('--force')
715 args.append('--force')
716 # The revision must be specified at the end of the URL to properly
716 # The revision must be specified at the end of the URL to properly
717 # update to a directory which has since been deleted and recreated.
717 # update to a directory which has since been deleted and recreated.
718 args.append('%s@%s' % (state[0], state[1]))
718 args.append('%s@%s' % (state[0], state[1]))
719 status, err = self._svncommand(args, failok=True)
719 status, err = self._svncommand(args, failok=True)
720 if not re.search('Checked out revision [0-9]+.', status):
720 if not re.search('Checked out revision [0-9]+.', status):
721 if ('is already a working copy for a different URL' in err
721 if ('is already a working copy for a different URL' in err
722 and (self._wcchanged() == (False, False))):
722 and (self._wcchanged() == (False, False))):
723 # obstructed but clean working copy, so just blow it away.
723 # obstructed but clean working copy, so just blow it away.
724 self.remove()
724 self.remove()
725 self.get(state, overwrite=False)
725 self.get(state, overwrite=False)
726 return
726 return
727 raise util.Abort((status or err).splitlines()[-1])
727 raise util.Abort((status or err).splitlines()[-1])
728 self._ui.status(status)
728 self._ui.status(status)
729
729
730 def merge(self, state):
730 def merge(self, state):
731 old = self._state[1]
731 old = self._state[1]
732 new = state[1]
732 new = state[1]
733 if new != self._wcrev():
733 if new != self._wcrev():
734 dirty = old == self._wcrev() or self._wcchanged()[0]
734 dirty = old == self._wcrev() or self._wcchanged()[0]
735 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
735 if _updateprompt(self._ui, self, dirty, self._wcrev(), new):
736 self.get(state, False)
736 self.get(state, False)
737
737
738 def push(self, opts):
738 def push(self, opts):
739 # push is a no-op for SVN
739 # push is a no-op for SVN
740 return True
740 return True
741
741
742 def files(self):
742 def files(self):
743 output = self._svncommand(['list'])
743 output = self._svncommand(['list'])
744 # This works because svn forbids \n in filenames.
744 # This works because svn forbids \n in filenames.
745 return output.splitlines()
745 return output.splitlines()
746
746
747 def filedata(self, name):
747 def filedata(self, name):
748 return self._svncommand(['cat'], name)
748 return self._svncommand(['cat'], name)
749
749
750
750
751 class gitsubrepo(abstractsubrepo):
751 class gitsubrepo(abstractsubrepo):
752 def __init__(self, ctx, path, state):
752 def __init__(self, ctx, path, state):
753 # TODO add git version check.
753 # TODO add git version check.
754 self._state = state
754 self._state = state
755 self._ctx = ctx
755 self._ctx = ctx
756 self._path = path
756 self._path = path
757 self._relpath = os.path.join(reporelpath(ctx._repo), path)
757 self._relpath = os.path.join(reporelpath(ctx._repo), path)
758 self._abspath = ctx._repo.wjoin(path)
758 self._abspath = ctx._repo.wjoin(path)
759 self._subparent = ctx._repo
759 self._subparent = ctx._repo
760 self._ui = ctx._repo.ui
760 self._ui = ctx._repo.ui
761
761
762 def _gitcommand(self, commands, env=None, stream=False):
762 def _gitcommand(self, commands, env=None, stream=False):
763 return self._gitdir(commands, env=env, stream=stream)[0]
763 return self._gitdir(commands, env=env, stream=stream)[0]
764
764
765 def _gitdir(self, commands, env=None, stream=False):
765 def _gitdir(self, commands, env=None, stream=False):
766 return self._gitnodir(commands, env=env, stream=stream,
766 return self._gitnodir(commands, env=env, stream=stream,
767 cwd=self._abspath)
767 cwd=self._abspath)
768
768
769 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
769 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
770 """Calls the git command
770 """Calls the git command
771
771
772 The methods tries to call the git command. versions previor to 1.6.0
772 The methods tries to call the git command. versions previor to 1.6.0
773 are not supported and very probably fail.
773 are not supported and very probably fail.
774 """
774 """
775 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
775 self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
776 # unless ui.quiet is set, print git's stderr,
776 # unless ui.quiet is set, print git's stderr,
777 # which is mostly progress and useful info
777 # which is mostly progress and useful info
778 errpipe = None
778 errpipe = None
779 if self._ui.quiet:
779 if self._ui.quiet:
780 errpipe = open(os.devnull, 'w')
780 errpipe = open(os.devnull, 'w')
781 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
781 p = subprocess.Popen(['git'] + commands, bufsize=-1, cwd=cwd, env=env,
782 close_fds=util.closefds,
782 close_fds=util.closefds,
783 stdout=subprocess.PIPE, stderr=errpipe)
783 stdout=subprocess.PIPE, stderr=errpipe)
784 if stream:
784 if stream:
785 return p.stdout, None
785 return p.stdout, None
786
786
787 retdata = p.stdout.read().strip()
787 retdata = p.stdout.read().strip()
788 # wait for the child to exit to avoid race condition.
788 # wait for the child to exit to avoid race condition.
789 p.wait()
789 p.wait()
790
790
791 if p.returncode != 0 and p.returncode != 1:
791 if p.returncode != 0 and p.returncode != 1:
792 # there are certain error codes that are ok
792 # there are certain error codes that are ok
793 command = commands[0]
793 command = commands[0]
794 if command in ('cat-file', 'symbolic-ref'):
794 if command in ('cat-file', 'symbolic-ref'):
795 return retdata, p.returncode
795 return retdata, p.returncode
796 # for all others, abort
796 # for all others, abort
797 raise util.Abort('git %s error %d in %s' %
797 raise util.Abort('git %s error %d in %s' %
798 (command, p.returncode, self._relpath))
798 (command, p.returncode, self._relpath))
799
799
800 return retdata, p.returncode
800 return retdata, p.returncode
801
801
802 def _gitmissing(self):
802 def _gitmissing(self):
803 return not os.path.exists(os.path.join(self._abspath, '.git'))
803 return not os.path.exists(os.path.join(self._abspath, '.git'))
804
804
805 def _gitstate(self):
805 def _gitstate(self):
806 return self._gitcommand(['rev-parse', 'HEAD'])
806 return self._gitcommand(['rev-parse', 'HEAD'])
807
807
808 def _gitcurrentbranch(self):
808 def _gitcurrentbranch(self):
809 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
809 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
810 if err:
810 if err:
811 current = None
811 current = None
812 return current
812 return current
813
813
814 def _gitremote(self, remote):
814 def _gitremote(self, remote):
815 out = self._gitcommand(['remote', 'show', '-n', remote])
815 out = self._gitcommand(['remote', 'show', '-n', remote])
816 line = out.split('\n')[1]
816 line = out.split('\n')[1]
817 i = line.index('URL: ') + len('URL: ')
817 i = line.index('URL: ') + len('URL: ')
818 return line[i:]
818 return line[i:]
819
819
820 def _githavelocally(self, revision):
820 def _githavelocally(self, revision):
821 out, code = self._gitdir(['cat-file', '-e', revision])
821 out, code = self._gitdir(['cat-file', '-e', revision])
822 return code == 0
822 return code == 0
823
823
824 def _gitisancestor(self, r1, r2):
824 def _gitisancestor(self, r1, r2):
825 base = self._gitcommand(['merge-base', r1, r2])
825 base = self._gitcommand(['merge-base', r1, r2])
826 return base == r1
826 return base == r1
827
827
828 def _gitisbare(self):
828 def _gitisbare(self):
829 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
829 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
830
830
831 def _gitupdatestat(self):
831 def _gitupdatestat(self):
832 """This must be run before git diff-index.
832 """This must be run before git diff-index.
833 diff-index only looks at changes to file stat;
833 diff-index only looks at changes to file stat;
834 this command looks at file contents and updates the stat."""
834 this command looks at file contents and updates the stat."""
835 self._gitcommand(['update-index', '-q', '--refresh'])
835 self._gitcommand(['update-index', '-q', '--refresh'])
836
836
837 def _gitbranchmap(self):
837 def _gitbranchmap(self):
838 '''returns 2 things:
838 '''returns 2 things:
839 a map from git branch to revision
839 a map from git branch to revision
840 a map from revision to branches'''
840 a map from revision to branches'''
841 branch2rev = {}
841 branch2rev = {}
842 rev2branch = {}
842 rev2branch = {}
843
843
844 out = self._gitcommand(['for-each-ref', '--format',
844 out = self._gitcommand(['for-each-ref', '--format',
845 '%(objectname) %(refname)'])
845 '%(objectname) %(refname)'])
846 for line in out.split('\n'):
846 for line in out.split('\n'):
847 revision, ref = line.split(' ')
847 revision, ref = line.split(' ')
848 if (not ref.startswith('refs/heads/') and
848 if (not ref.startswith('refs/heads/') and
849 not ref.startswith('refs/remotes/')):
849 not ref.startswith('refs/remotes/')):
850 continue
850 continue
851 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
851 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
852 continue # ignore remote/HEAD redirects
852 continue # ignore remote/HEAD redirects
853 branch2rev[ref] = revision
853 branch2rev[ref] = revision
854 rev2branch.setdefault(revision, []).append(ref)
854 rev2branch.setdefault(revision, []).append(ref)
855 return branch2rev, rev2branch
855 return branch2rev, rev2branch
856
856
857 def _gittracking(self, branches):
857 def _gittracking(self, branches):
858 'return map of remote branch to local tracking branch'
858 'return map of remote branch to local tracking branch'
859 # assumes no more than one local tracking branch for each remote
859 # assumes no more than one local tracking branch for each remote
860 tracking = {}
860 tracking = {}
861 for b in branches:
861 for b in branches:
862 if b.startswith('refs/remotes/'):
862 if b.startswith('refs/remotes/'):
863 continue
863 continue
864 bname = b.split('/', 2)[2]
864 bname = b.split('/', 2)[2]
865 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
865 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
866 if remote:
866 if remote:
867 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
867 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
868 tracking['refs/remotes/%s/%s' %
868 tracking['refs/remotes/%s/%s' %
869 (remote, ref.split('/', 2)[2])] = b
869 (remote, ref.split('/', 2)[2])] = b
870 return tracking
870 return tracking
871
871
872 def _abssource(self, source):
872 def _abssource(self, source):
873 if '://' not in source:
873 if '://' not in source:
874 # recognize the scp syntax as an absolute source
874 # recognize the scp syntax as an absolute source
875 colon = source.find(':')
875 colon = source.find(':')
876 if colon != -1 and '/' not in source[:colon]:
876 if colon != -1 and '/' not in source[:colon]:
877 return source
877 return source
878 self._subsource = source
878 self._subsource = source
879 return _abssource(self)
879 return _abssource(self)
880
880
881 def _fetch(self, source, revision):
881 def _fetch(self, source, revision):
882 if self._gitmissing():
882 if self._gitmissing():
883 source = self._abssource(source)
883 source = self._abssource(source)
884 self._ui.status(_('cloning subrepo %s from %s\n') %
884 self._ui.status(_('cloning subrepo %s from %s\n') %
885 (self._relpath, source))
885 (self._relpath, source))
886 self._gitnodir(['clone', source, self._abspath])
886 self._gitnodir(['clone', source, self._abspath])
887 if self._githavelocally(revision):
887 if self._githavelocally(revision):
888 return
888 return
889 self._ui.status(_('pulling subrepo %s from %s\n') %
889 self._ui.status(_('pulling subrepo %s from %s\n') %
890 (self._relpath, self._gitremote('origin')))
890 (self._relpath, self._gitremote('origin')))
891 # try only origin: the originally cloned repo
891 # try only origin: the originally cloned repo
892 self._gitcommand(['fetch'])
892 self._gitcommand(['fetch'])
893 if not self._githavelocally(revision):
893 if not self._githavelocally(revision):
894 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
894 raise util.Abort(_("revision %s does not exist in subrepo %s\n") %
895 (revision, self._relpath))
895 (revision, self._relpath))
896
896
897 def dirty(self, ignoreupdate=False):
897 def dirty(self, ignoreupdate=False):
898 if self._gitmissing():
898 if self._gitmissing():
899 return self._state[1] != ''
899 return self._state[1] != ''
900 if self._gitisbare():
900 if self._gitisbare():
901 return True
901 return True
902 if not ignoreupdate and self._state[1] != self._gitstate():
902 if not ignoreupdate and self._state[1] != self._gitstate():
903 # different version checked out
903 # different version checked out
904 return True
904 return True
905 # check for staged changes or modified files; ignore untracked files
905 # check for staged changes or modified files; ignore untracked files
906 self._gitupdatestat()
906 self._gitupdatestat()
907 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
907 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
908 return code == 1
908 return code == 1
909
909
910 def get(self, state, overwrite=False):
910 def get(self, state, overwrite=False):
911 source, revision, kind = state
911 source, revision, kind = state
912 if not revision:
912 if not revision:
913 self.remove()
913 self.remove()
914 return
914 return
915 self._fetch(source, revision)
915 self._fetch(source, revision)
916 # if the repo was set to be bare, unbare it
916 # if the repo was set to be bare, unbare it
917 if self._gitisbare():
917 if self._gitisbare():
918 self._gitcommand(['config', 'core.bare', 'false'])
918 self._gitcommand(['config', 'core.bare', 'false'])
919 if self._gitstate() == revision:
919 if self._gitstate() == revision:
920 self._gitcommand(['reset', '--hard', 'HEAD'])
920 self._gitcommand(['reset', '--hard', 'HEAD'])
921 return
921 return
922 elif self._gitstate() == revision:
922 elif self._gitstate() == revision:
923 if overwrite:
923 if overwrite:
924 # first reset the index to unmark new files for commit, because
924 # first reset the index to unmark new files for commit, because
925 # reset --hard will otherwise throw away files added for commit,
925 # reset --hard will otherwise throw away files added for commit,
926 # not just unmark them.
926 # not just unmark them.
927 self._gitcommand(['reset', 'HEAD'])
927 self._gitcommand(['reset', 'HEAD'])
928 self._gitcommand(['reset', '--hard', 'HEAD'])
928 self._gitcommand(['reset', '--hard', 'HEAD'])
929 return
929 return
930 branch2rev, rev2branch = self._gitbranchmap()
930 branch2rev, rev2branch = self._gitbranchmap()
931
931
932 def checkout(args):
932 def checkout(args):
933 cmd = ['checkout']
933 cmd = ['checkout']
934 if overwrite:
934 if overwrite:
935 # first reset the index to unmark new files for commit, because
935 # first reset the index to unmark new files for commit, because
936 # the -f option will otherwise throw away files added for
936 # the -f option will otherwise throw away files added for
937 # commit, not just unmark them.
937 # commit, not just unmark them.
938 self._gitcommand(['reset', 'HEAD'])
938 self._gitcommand(['reset', 'HEAD'])
939 cmd.append('-f')
939 cmd.append('-f')
940 self._gitcommand(cmd + args)
940 self._gitcommand(cmd + args)
941
941
942 def rawcheckout():
942 def rawcheckout():
943 # no branch to checkout, check it out with no branch
943 # no branch to checkout, check it out with no branch
944 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
944 self._ui.warn(_('checking out detached HEAD in subrepo %s\n') %
945 self._relpath)
945 self._relpath)
946 self._ui.warn(_('check out a git branch if you intend '
946 self._ui.warn(_('check out a git branch if you intend '
947 'to make changes\n'))
947 'to make changes\n'))
948 checkout(['-q', revision])
948 checkout(['-q', revision])
949
949
950 if revision not in rev2branch:
950 if revision not in rev2branch:
951 rawcheckout()
951 rawcheckout()
952 return
952 return
953 branches = rev2branch[revision]
953 branches = rev2branch[revision]
954 firstlocalbranch = None
954 firstlocalbranch = None
955 for b in branches:
955 for b in branches:
956 if b == 'refs/heads/master':
956 if b == 'refs/heads/master':
957 # master trumps all other branches
957 # master trumps all other branches
958 checkout(['refs/heads/master'])
958 checkout(['refs/heads/master'])
959 return
959 return
960 if not firstlocalbranch and not b.startswith('refs/remotes/'):
960 if not firstlocalbranch and not b.startswith('refs/remotes/'):
961 firstlocalbranch = b
961 firstlocalbranch = b
962 if firstlocalbranch:
962 if firstlocalbranch:
963 checkout([firstlocalbranch])
963 checkout([firstlocalbranch])
964 return
964 return
965
965
966 tracking = self._gittracking(branch2rev.keys())
966 tracking = self._gittracking(branch2rev.keys())
967 # choose a remote branch already tracked if possible
967 # choose a remote branch already tracked if possible
968 remote = branches[0]
968 remote = branches[0]
969 if remote not in tracking:
969 if remote not in tracking:
970 for b in branches:
970 for b in branches:
971 if b in tracking:
971 if b in tracking:
972 remote = b
972 remote = b
973 break
973 break
974
974
975 if remote not in tracking:
975 if remote not in tracking:
976 # create a new local tracking branch
976 # create a new local tracking branch
977 local = remote.split('/', 2)[2]
977 local = remote.split('/', 2)[2]
978 checkout(['-b', local, remote])
978 checkout(['-b', local, remote])
979 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
979 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
980 # When updating to a tracked remote branch,
980 # When updating to a tracked remote branch,
981 # if the local tracking branch is downstream of it,
981 # if the local tracking branch is downstream of it,
982 # a normal `git pull` would have performed a "fast-forward merge"
982 # a normal `git pull` would have performed a "fast-forward merge"
983 # which is equivalent to updating the local branch to the remote.
983 # which is equivalent to updating the local branch to the remote.
984 # Since we are only looking at branching at update, we need to
984 # Since we are only looking at branching at update, we need to
985 # detect this situation and perform this action lazily.
985 # detect this situation and perform this action lazily.
986 if tracking[remote] != self._gitcurrentbranch():
986 if tracking[remote] != self._gitcurrentbranch():
987 checkout([tracking[remote]])
987 checkout([tracking[remote]])
988 self._gitcommand(['merge', '--ff', remote])
988 self._gitcommand(['merge', '--ff', remote])
989 else:
989 else:
990 # a real merge would be required, just checkout the revision
990 # a real merge would be required, just checkout the revision
991 rawcheckout()
991 rawcheckout()
992
992
993 def commit(self, text, user, date):
993 def commit(self, text, user, date):
994 if self._gitmissing():
994 if self._gitmissing():
995 raise util.Abort(_("subrepo %s is missing") % self._relpath)
995 raise util.Abort(_("subrepo %s is missing") % self._relpath)
996 cmd = ['commit', '-a', '-m', text]
996 cmd = ['commit', '-a', '-m', text]
997 env = os.environ.copy()
997 env = os.environ.copy()
998 if user:
998 if user:
999 cmd += ['--author', user]
999 cmd += ['--author', user]
1000 if date:
1000 if date:
1001 # git's date parser silently ignores when seconds < 1e9
1001 # git's date parser silently ignores when seconds < 1e9
1002 # convert to ISO8601
1002 # convert to ISO8601
1003 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1003 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1004 '%Y-%m-%dT%H:%M:%S %1%2')
1004 '%Y-%m-%dT%H:%M:%S %1%2')
1005 self._gitcommand(cmd, env=env)
1005 self._gitcommand(cmd, env=env)
1006 # make sure commit works otherwise HEAD might not exist under certain
1006 # make sure commit works otherwise HEAD might not exist under certain
1007 # circumstances
1007 # circumstances
1008 return self._gitstate()
1008 return self._gitstate()
1009
1009
1010 def merge(self, state):
1010 def merge(self, state):
1011 source, revision, kind = state
1011 source, revision, kind = state
1012 self._fetch(source, revision)
1012 self._fetch(source, revision)
1013 base = self._gitcommand(['merge-base', revision, self._state[1]])
1013 base = self._gitcommand(['merge-base', revision, self._state[1]])
1014 self._gitupdatestat()
1014 self._gitupdatestat()
1015 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1015 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1016
1016
1017 def mergefunc():
1017 def mergefunc():
1018 if base == revision:
1018 if base == revision:
1019 self.get(state) # fast forward merge
1019 self.get(state) # fast forward merge
1020 elif base != self._state[1]:
1020 elif base != self._state[1]:
1021 self._gitcommand(['merge', '--no-commit', revision])
1021 self._gitcommand(['merge', '--no-commit', revision])
1022
1022
1023 if self.dirty():
1023 if self.dirty():
1024 if self._gitstate() != revision:
1024 if self._gitstate() != revision:
1025 dirty = self._gitstate() == self._state[1] or code != 0
1025 dirty = self._gitstate() == self._state[1] or code != 0
1026 if _updateprompt(self._ui, self, dirty,
1026 if _updateprompt(self._ui, self, dirty,
1027 self._state[1][:7], revision[:7]):
1027 self._state[1][:7], revision[:7]):
1028 mergefunc()
1028 mergefunc()
1029 else:
1029 else:
1030 mergefunc()
1030 mergefunc()
1031
1031
1032 def push(self, opts):
1032 def push(self, opts):
1033 force = opts.get('force')
1033 force = opts.get('force')
1034
1034
1035 if not self._state[1]:
1035 if not self._state[1]:
1036 return True
1036 return True
1037 if self._gitmissing():
1037 if self._gitmissing():
1038 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1038 raise util.Abort(_("subrepo %s is missing") % self._relpath)
1039 # if a branch in origin contains the revision, nothing to do
1039 # if a branch in origin contains the revision, nothing to do
1040 branch2rev, rev2branch = self._gitbranchmap()
1040 branch2rev, rev2branch = self._gitbranchmap()
1041 if self._state[1] in rev2branch:
1041 if self._state[1] in rev2branch:
1042 for b in rev2branch[self._state[1]]:
1042 for b in rev2branch[self._state[1]]:
1043 if b.startswith('refs/remotes/origin/'):
1043 if b.startswith('refs/remotes/origin/'):
1044 return True
1044 return True
1045 for b, revision in branch2rev.iteritems():
1045 for b, revision in branch2rev.iteritems():
1046 if b.startswith('refs/remotes/origin/'):
1046 if b.startswith('refs/remotes/origin/'):
1047 if self._gitisancestor(self._state[1], revision):
1047 if self._gitisancestor(self._state[1], revision):
1048 return True
1048 return True
1049 # otherwise, try to push the currently checked out branch
1049 # otherwise, try to push the currently checked out branch
1050 cmd = ['push']
1050 cmd = ['push']
1051 if force:
1051 if force:
1052 cmd.append('--force')
1052 cmd.append('--force')
1053
1053
1054 current = self._gitcurrentbranch()
1054 current = self._gitcurrentbranch()
1055 if current:
1055 if current:
1056 # determine if the current branch is even useful
1056 # determine if the current branch is even useful
1057 if not self._gitisancestor(self._state[1], current):
1057 if not self._gitisancestor(self._state[1], current):
1058 self._ui.warn(_('unrelated git branch checked out '
1058 self._ui.warn(_('unrelated git branch checked out '
1059 'in subrepo %s\n') % self._relpath)
1059 'in subrepo %s\n') % self._relpath)
1060 return False
1060 return False
1061 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1061 self._ui.status(_('pushing branch %s of subrepo %s\n') %
1062 (current.split('/', 2)[2], self._relpath))
1062 (current.split('/', 2)[2], self._relpath))
1063 self._gitcommand(cmd + ['origin', current])
1063 self._gitcommand(cmd + ['origin', current])
1064 return True
1064 return True
1065 else:
1065 else:
1066 self._ui.warn(_('no branch checked out in subrepo %s\n'
1066 self._ui.warn(_('no branch checked out in subrepo %s\n'
1067 'cannot push revision %s') %
1067 'cannot push revision %s') %
1068 (self._relpath, self._state[1]))
1068 (self._relpath, self._state[1]))
1069 return False
1069 return False
1070
1070
1071 def remove(self):
1071 def remove(self):
1072 if self._gitmissing():
1072 if self._gitmissing():
1073 return
1073 return
1074 if self.dirty():
1074 if self.dirty():
1075 self._ui.warn(_('not removing repo %s because '
1075 self._ui.warn(_('not removing repo %s because '
1076 'it has changes.\n') % self._relpath)
1076 'it has changes.\n') % self._relpath)
1077 return
1077 return
1078 # we can't fully delete the repository as it may contain
1078 # we can't fully delete the repository as it may contain
1079 # local-only history
1079 # local-only history
1080 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1080 self._ui.note(_('removing subrepo %s\n') % self._relpath)
1081 self._gitcommand(['config', 'core.bare', 'true'])
1081 self._gitcommand(['config', 'core.bare', 'true'])
1082 for f in os.listdir(self._abspath):
1082 for f in os.listdir(self._abspath):
1083 if f == '.git':
1083 if f == '.git':
1084 continue
1084 continue
1085 path = os.path.join(self._abspath, f)
1085 path = os.path.join(self._abspath, f)
1086 if os.path.isdir(path) and not os.path.islink(path):
1086 if os.path.isdir(path) and not os.path.islink(path):
1087 shutil.rmtree(path)
1087 shutil.rmtree(path)
1088 else:
1088 else:
1089 os.remove(path)
1089 os.remove(path)
1090
1090
1091 def archive(self, ui, archiver, prefix):
1091 def archive(self, ui, archiver, prefix):
1092 source, revision = self._state
1092 source, revision = self._state
1093 if not revision:
1093 if not revision:
1094 return
1094 return
1095 self._fetch(source, revision)
1095 self._fetch(source, revision)
1096
1096
1097 # Parse git's native archive command.
1097 # Parse git's native archive command.
1098 # This should be much faster than manually traversing the trees
1098 # This should be much faster than manually traversing the trees
1099 # and objects with many subprocess calls.
1099 # and objects with many subprocess calls.
1100 tarstream = self._gitcommand(['archive', revision], stream=True)
1100 tarstream = self._gitcommand(['archive', revision], stream=True)
1101 tar = tarfile.open(fileobj=tarstream, mode='r|')
1101 tar = tarfile.open(fileobj=tarstream, mode='r|')
1102 relpath = subrelpath(self)
1102 relpath = subrelpath(self)
1103 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1103 ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1104 for i, info in enumerate(tar):
1104 for i, info in enumerate(tar):
1105 if info.isdir():
1105 if info.isdir():
1106 continue
1106 continue
1107 if info.issym():
1107 if info.issym():
1108 data = info.linkname
1108 data = info.linkname
1109 else:
1109 else:
1110 data = tar.extractfile(info).read()
1110 data = tar.extractfile(info).read()
1111 archiver.addfile(os.path.join(prefix, self._path, info.name),
1111 archiver.addfile(os.path.join(prefix, self._path, info.name),
1112 info.mode, info.issym(), data)
1112 info.mode, info.issym(), data)
1113 ui.progress(_('archiving (%s)') % relpath, i + 1,
1113 ui.progress(_('archiving (%s)') % relpath, i + 1,
1114 unit=_('files'))
1114 unit=_('files'))
1115 ui.progress(_('archiving (%s)') % relpath, None)
1115 ui.progress(_('archiving (%s)') % relpath, None)
1116
1116
1117
1117
1118 def status(self, rev2, **opts):
1118 def status(self, rev2, **opts):
1119 rev1 = self._state[1]
1119 rev1 = self._state[1]
1120 if self._gitmissing() or not rev1:
1120 if self._gitmissing() or not rev1:
1121 # if the repo is missing, return no results
1121 # if the repo is missing, return no results
1122 return [], [], [], [], [], [], []
1122 return [], [], [], [], [], [], []
1123 modified, added, removed = [], [], []
1123 modified, added, removed = [], [], []
1124 self._gitupdatestat()
1124 self._gitupdatestat()
1125 if rev2:
1125 if rev2:
1126 command = ['diff-tree', rev1, rev2]
1126 command = ['diff-tree', rev1, rev2]
1127 else:
1127 else:
1128 command = ['diff-index', rev1]
1128 command = ['diff-index', rev1]
1129 out = self._gitcommand(command)
1129 out = self._gitcommand(command)
1130 for line in out.split('\n'):
1130 for line in out.split('\n'):
1131 tab = line.find('\t')
1131 tab = line.find('\t')
1132 if tab == -1:
1132 if tab == -1:
1133 continue
1133 continue
1134 status, f = line[tab - 1], line[tab + 1:]
1134 status, f = line[tab - 1], line[tab + 1:]
1135 if status == 'M':
1135 if status == 'M':
1136 modified.append(f)
1136 modified.append(f)
1137 elif status == 'A':
1137 elif status == 'A':
1138 added.append(f)
1138 added.append(f)
1139 elif status == 'D':
1139 elif status == 'D':
1140 removed.append(f)
1140 removed.append(f)
1141
1141
1142 deleted = unknown = ignored = clean = []
1142 deleted = unknown = ignored = clean = []
1143 return modified, added, removed, deleted, unknown, ignored, clean
1143 return modified, added, removed, deleted, unknown, ignored, clean
1144
1144
1145 types = {
1145 types = {
1146 'hg': hgsubrepo,
1146 'hg': hgsubrepo,
1147 'svn': svnsubrepo,
1147 'svn': svnsubrepo,
1148 'git': gitsubrepo,
1148 'git': gitsubrepo,
1149 }
1149 }
General Comments 0
You need to be logged in to leave comments. Login now