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