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