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