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