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