##// END OF EJS Templates
error: make hintable exceptions reject unknown keyword arguments (API)...
Yuya Nishihara -
r29510:19205a0e default
parent child Browse files
Show More
@@ -1,242 +1,242
1 # error.py - Mercurial exceptions
1 # error.py - Mercurial exceptions
2 #
2 #
3 # Copyright 2005-2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2008 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 """Mercurial exceptions.
8 """Mercurial exceptions.
9
9
10 This allows us to catch exceptions at higher levels without forcing
10 This allows us to catch exceptions at higher levels without forcing
11 imports.
11 imports.
12 """
12 """
13
13
14 from __future__ import absolute_import
14 from __future__ import absolute_import
15
15
16 # Do not import anything here, please
16 # Do not import anything here, please
17
17
18 class Hint(object):
18 class Hint(object):
19 """Mix-in to provide a hint of an error
19 """Mix-in to provide a hint of an error
20
20
21 This should come first in the inheritance list to consume **kw and pass
21 This should come first in the inheritance list to consume a hint and
22 only *args to the exception class.
22 pass remaining arguments to the exception class.
23 """
23 """
24 def __init__(self, *args, **kw):
24 def __init__(self, *args, **kw):
25 super(Hint, self).__init__(*args)
25 self.hint = kw.pop('hint', None)
26 self.hint = kw.get('hint')
26 super(Hint, self).__init__(*args, **kw)
27
27
28 class RevlogError(Hint, Exception):
28 class RevlogError(Hint, Exception):
29 pass
29 pass
30
30
31 class FilteredIndexError(IndexError):
31 class FilteredIndexError(IndexError):
32 pass
32 pass
33
33
34 class LookupError(RevlogError, KeyError):
34 class LookupError(RevlogError, KeyError):
35 def __init__(self, name, index, message):
35 def __init__(self, name, index, message):
36 self.name = name
36 self.name = name
37 self.index = index
37 self.index = index
38 # this can't be called 'message' because at least some installs of
38 # this can't be called 'message' because at least some installs of
39 # Python 2.6+ complain about the 'message' property being deprecated
39 # Python 2.6+ complain about the 'message' property being deprecated
40 self.lookupmessage = message
40 self.lookupmessage = message
41 if isinstance(name, str) and len(name) == 20:
41 if isinstance(name, str) and len(name) == 20:
42 from .node import short
42 from .node import short
43 name = short(name)
43 name = short(name)
44 RevlogError.__init__(self, '%s@%s: %s' % (index, name, message))
44 RevlogError.__init__(self, '%s@%s: %s' % (index, name, message))
45
45
46 def __str__(self):
46 def __str__(self):
47 return RevlogError.__str__(self)
47 return RevlogError.__str__(self)
48
48
49 class FilteredLookupError(LookupError):
49 class FilteredLookupError(LookupError):
50 pass
50 pass
51
51
52 class ManifestLookupError(LookupError):
52 class ManifestLookupError(LookupError):
53 pass
53 pass
54
54
55 class CommandError(Exception):
55 class CommandError(Exception):
56 """Exception raised on errors in parsing the command line."""
56 """Exception raised on errors in parsing the command line."""
57
57
58 class InterventionRequired(Hint, Exception):
58 class InterventionRequired(Hint, Exception):
59 """Exception raised when a command requires human intervention."""
59 """Exception raised when a command requires human intervention."""
60
60
61 class Abort(Hint, Exception):
61 class Abort(Hint, Exception):
62 """Raised if a command needs to print an error and exit."""
62 """Raised if a command needs to print an error and exit."""
63
63
64 class HookLoadError(Abort):
64 class HookLoadError(Abort):
65 """raised when loading a hook fails, aborting an operation
65 """raised when loading a hook fails, aborting an operation
66
66
67 Exists to allow more specialized catching."""
67 Exists to allow more specialized catching."""
68
68
69 class HookAbort(Abort):
69 class HookAbort(Abort):
70 """raised when a validation hook fails, aborting an operation
70 """raised when a validation hook fails, aborting an operation
71
71
72 Exists to allow more specialized catching."""
72 Exists to allow more specialized catching."""
73
73
74 class ConfigError(Abort):
74 class ConfigError(Abort):
75 """Exception raised when parsing config files"""
75 """Exception raised when parsing config files"""
76
76
77 class UpdateAbort(Abort):
77 class UpdateAbort(Abort):
78 """Raised when an update is aborted for destination issue"""
78 """Raised when an update is aborted for destination issue"""
79
79
80 class MergeDestAbort(Abort):
80 class MergeDestAbort(Abort):
81 """Raised when an update is aborted for destination issues"""
81 """Raised when an update is aborted for destination issues"""
82
82
83 class NoMergeDestAbort(MergeDestAbort):
83 class NoMergeDestAbort(MergeDestAbort):
84 """Raised when an update is aborted because there is nothing to merge"""
84 """Raised when an update is aborted because there is nothing to merge"""
85
85
86 class ManyMergeDestAbort(MergeDestAbort):
86 class ManyMergeDestAbort(MergeDestAbort):
87 """Raised when an update is aborted because destination is ambigious"""
87 """Raised when an update is aborted because destination is ambigious"""
88
88
89 class ResponseExpected(Abort):
89 class ResponseExpected(Abort):
90 """Raised when an EOF is received for a prompt"""
90 """Raised when an EOF is received for a prompt"""
91 def __init__(self):
91 def __init__(self):
92 from .i18n import _
92 from .i18n import _
93 Abort.__init__(self, _('response expected'))
93 Abort.__init__(self, _('response expected'))
94
94
95 class OutOfBandError(Hint, Exception):
95 class OutOfBandError(Hint, Exception):
96 """Exception raised when a remote repo reports failure"""
96 """Exception raised when a remote repo reports failure"""
97
97
98 class ParseError(Hint, Exception):
98 class ParseError(Hint, Exception):
99 """Raised when parsing config files and {rev,file}sets (msg[, pos])"""
99 """Raised when parsing config files and {rev,file}sets (msg[, pos])"""
100
100
101 class UnknownIdentifier(ParseError):
101 class UnknownIdentifier(ParseError):
102 """Exception raised when a {rev,file}set references an unknown identifier"""
102 """Exception raised when a {rev,file}set references an unknown identifier"""
103
103
104 def __init__(self, function, symbols):
104 def __init__(self, function, symbols):
105 from .i18n import _
105 from .i18n import _
106 ParseError.__init__(self, _("unknown identifier: %s") % function)
106 ParseError.__init__(self, _("unknown identifier: %s") % function)
107 self.function = function
107 self.function = function
108 self.symbols = symbols
108 self.symbols = symbols
109
109
110 class RepoError(Hint, Exception):
110 class RepoError(Hint, Exception):
111 pass
111 pass
112
112
113 class RepoLookupError(RepoError):
113 class RepoLookupError(RepoError):
114 pass
114 pass
115
115
116 class FilteredRepoLookupError(RepoLookupError):
116 class FilteredRepoLookupError(RepoLookupError):
117 pass
117 pass
118
118
119 class CapabilityError(RepoError):
119 class CapabilityError(RepoError):
120 pass
120 pass
121
121
122 class RequirementError(RepoError):
122 class RequirementError(RepoError):
123 """Exception raised if .hg/requires has an unknown entry."""
123 """Exception raised if .hg/requires has an unknown entry."""
124
124
125 class UnsupportedMergeRecords(Abort):
125 class UnsupportedMergeRecords(Abort):
126 def __init__(self, recordtypes):
126 def __init__(self, recordtypes):
127 from .i18n import _
127 from .i18n import _
128 self.recordtypes = sorted(recordtypes)
128 self.recordtypes = sorted(recordtypes)
129 s = ' '.join(self.recordtypes)
129 s = ' '.join(self.recordtypes)
130 Abort.__init__(
130 Abort.__init__(
131 self, _('unsupported merge state records: %s') % s,
131 self, _('unsupported merge state records: %s') % s,
132 hint=_('see https://mercurial-scm.org/wiki/MergeStateRecords for '
132 hint=_('see https://mercurial-scm.org/wiki/MergeStateRecords for '
133 'more information'))
133 'more information'))
134
134
135 class LockError(IOError):
135 class LockError(IOError):
136 def __init__(self, errno, strerror, filename, desc):
136 def __init__(self, errno, strerror, filename, desc):
137 IOError.__init__(self, errno, strerror, filename)
137 IOError.__init__(self, errno, strerror, filename)
138 self.desc = desc
138 self.desc = desc
139
139
140 class LockHeld(LockError):
140 class LockHeld(LockError):
141 def __init__(self, errno, filename, desc, locker):
141 def __init__(self, errno, filename, desc, locker):
142 LockError.__init__(self, errno, 'Lock held', filename, desc)
142 LockError.__init__(self, errno, 'Lock held', filename, desc)
143 self.locker = locker
143 self.locker = locker
144
144
145 class LockUnavailable(LockError):
145 class LockUnavailable(LockError):
146 pass
146 pass
147
147
148 # LockError is for errors while acquiring the lock -- this is unrelated
148 # LockError is for errors while acquiring the lock -- this is unrelated
149 class LockInheritanceContractViolation(RuntimeError):
149 class LockInheritanceContractViolation(RuntimeError):
150 pass
150 pass
151
151
152 class ResponseError(Exception):
152 class ResponseError(Exception):
153 """Raised to print an error with part of output and exit."""
153 """Raised to print an error with part of output and exit."""
154
154
155 class UnknownCommand(Exception):
155 class UnknownCommand(Exception):
156 """Exception raised if command is not in the command table."""
156 """Exception raised if command is not in the command table."""
157
157
158 class AmbiguousCommand(Exception):
158 class AmbiguousCommand(Exception):
159 """Exception raised if command shortcut matches more than one command."""
159 """Exception raised if command shortcut matches more than one command."""
160
160
161 # derived from KeyboardInterrupt to simplify some breakout code
161 # derived from KeyboardInterrupt to simplify some breakout code
162 class SignalInterrupt(KeyboardInterrupt):
162 class SignalInterrupt(KeyboardInterrupt):
163 """Exception raised on SIGTERM and SIGHUP."""
163 """Exception raised on SIGTERM and SIGHUP."""
164
164
165 class SignatureError(Exception):
165 class SignatureError(Exception):
166 pass
166 pass
167
167
168 class PushRaced(RuntimeError):
168 class PushRaced(RuntimeError):
169 """An exception raised during unbundling that indicate a push race"""
169 """An exception raised during unbundling that indicate a push race"""
170
170
171 # bundle2 related errors
171 # bundle2 related errors
172 class BundleValueError(ValueError):
172 class BundleValueError(ValueError):
173 """error raised when bundle2 cannot be processed"""
173 """error raised when bundle2 cannot be processed"""
174
174
175 class BundleUnknownFeatureError(BundleValueError):
175 class BundleUnknownFeatureError(BundleValueError):
176 def __init__(self, parttype=None, params=(), values=()):
176 def __init__(self, parttype=None, params=(), values=()):
177 self.parttype = parttype
177 self.parttype = parttype
178 self.params = params
178 self.params = params
179 self.values = values
179 self.values = values
180 if self.parttype is None:
180 if self.parttype is None:
181 msg = 'Stream Parameter'
181 msg = 'Stream Parameter'
182 else:
182 else:
183 msg = parttype
183 msg = parttype
184 entries = self.params
184 entries = self.params
185 if self.params and self.values:
185 if self.params and self.values:
186 assert len(self.params) == len(self.values)
186 assert len(self.params) == len(self.values)
187 entries = []
187 entries = []
188 for idx, par in enumerate(self.params):
188 for idx, par in enumerate(self.params):
189 val = self.values[idx]
189 val = self.values[idx]
190 if val is None:
190 if val is None:
191 entries.append(val)
191 entries.append(val)
192 else:
192 else:
193 entries.append("%s=%r" % (par, val))
193 entries.append("%s=%r" % (par, val))
194 if entries:
194 if entries:
195 msg = '%s - %s' % (msg, ', '.join(entries))
195 msg = '%s - %s' % (msg, ', '.join(entries))
196 ValueError.__init__(self, msg)
196 ValueError.__init__(self, msg)
197
197
198 class ReadOnlyPartError(RuntimeError):
198 class ReadOnlyPartError(RuntimeError):
199 """error raised when code tries to alter a part being generated"""
199 """error raised when code tries to alter a part being generated"""
200
200
201 class PushkeyFailed(Abort):
201 class PushkeyFailed(Abort):
202 """error raised when a pushkey part failed to update a value"""
202 """error raised when a pushkey part failed to update a value"""
203
203
204 def __init__(self, partid, namespace=None, key=None, new=None, old=None,
204 def __init__(self, partid, namespace=None, key=None, new=None, old=None,
205 ret=None):
205 ret=None):
206 self.partid = partid
206 self.partid = partid
207 self.namespace = namespace
207 self.namespace = namespace
208 self.key = key
208 self.key = key
209 self.new = new
209 self.new = new
210 self.old = old
210 self.old = old
211 self.ret = ret
211 self.ret = ret
212 # no i18n expected to be processed into a better message
212 # no i18n expected to be processed into a better message
213 Abort.__init__(self, 'failed to update value for "%s/%s"'
213 Abort.__init__(self, 'failed to update value for "%s/%s"'
214 % (namespace, key))
214 % (namespace, key))
215
215
216 class CensoredNodeError(RevlogError):
216 class CensoredNodeError(RevlogError):
217 """error raised when content verification fails on a censored node
217 """error raised when content verification fails on a censored node
218
218
219 Also contains the tombstone data substituted for the uncensored data.
219 Also contains the tombstone data substituted for the uncensored data.
220 """
220 """
221
221
222 def __init__(self, filename, node, tombstone):
222 def __init__(self, filename, node, tombstone):
223 from .node import short
223 from .node import short
224 RevlogError.__init__(self, '%s:%s' % (filename, short(node)))
224 RevlogError.__init__(self, '%s:%s' % (filename, short(node)))
225 self.tombstone = tombstone
225 self.tombstone = tombstone
226
226
227 class CensoredBaseError(RevlogError):
227 class CensoredBaseError(RevlogError):
228 """error raised when a delta is rejected because its base is censored
228 """error raised when a delta is rejected because its base is censored
229
229
230 A delta based on a censored revision must be formed as single patch
230 A delta based on a censored revision must be formed as single patch
231 operation which replaces the entire base with new content. This ensures
231 operation which replaces the entire base with new content. This ensures
232 the delta may be applied by clones which have not censored the base.
232 the delta may be applied by clones which have not censored the base.
233 """
233 """
234
234
235 class InvalidBundleSpecification(Exception):
235 class InvalidBundleSpecification(Exception):
236 """error raised when a bundle specification is invalid.
236 """error raised when a bundle specification is invalid.
237
237
238 This is used for syntax errors as opposed to support errors.
238 This is used for syntax errors as opposed to support errors.
239 """
239 """
240
240
241 class UnsupportedBundleSpecification(Exception):
241 class UnsupportedBundleSpecification(Exception):
242 """error raised when a bundle specification is not supported."""
242 """error raised when a bundle specification is not supported."""
@@ -1,1948 +1,1948
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 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import os
13 import os
14 import posixpath
14 import posixpath
15 import re
15 import re
16 import stat
16 import stat
17 import subprocess
17 import subprocess
18 import sys
18 import sys
19 import tarfile
19 import tarfile
20 import xml.dom.minidom
20 import xml.dom.minidom
21
21
22
22
23 from .i18n import _
23 from .i18n import _
24 from . import (
24 from . import (
25 cmdutil,
25 cmdutil,
26 config,
26 config,
27 error,
27 error,
28 exchange,
28 exchange,
29 match as matchmod,
29 match as matchmod,
30 node,
30 node,
31 pathutil,
31 pathutil,
32 phases,
32 phases,
33 scmutil,
33 scmutil,
34 util,
34 util,
35 )
35 )
36
36
37 hg = None
37 hg = None
38 propertycache = util.propertycache
38 propertycache = util.propertycache
39
39
40 nullstate = ('', '', 'empty')
40 nullstate = ('', '', 'empty')
41
41
42 def _expandedabspath(path):
42 def _expandedabspath(path):
43 '''
43 '''
44 get a path or url and if it is a path expand it and return an absolute path
44 get a path or url and if it is a path expand it and return an absolute path
45 '''
45 '''
46 expandedpath = util.urllocalpath(util.expandpath(path))
46 expandedpath = util.urllocalpath(util.expandpath(path))
47 u = util.url(expandedpath)
47 u = util.url(expandedpath)
48 if not u.scheme:
48 if not u.scheme:
49 path = util.normpath(os.path.abspath(u.path))
49 path = util.normpath(os.path.abspath(u.path))
50 return path
50 return path
51
51
52 def _getstorehashcachename(remotepath):
52 def _getstorehashcachename(remotepath):
53 '''get a unique filename for the store hash cache of a remote repository'''
53 '''get a unique filename for the store hash cache of a remote repository'''
54 return hashlib.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
54 return hashlib.sha1(_expandedabspath(remotepath)).hexdigest()[0:12]
55
55
56 class SubrepoAbort(error.Abort):
56 class SubrepoAbort(error.Abort):
57 """Exception class used to avoid handling a subrepo error more than once"""
57 """Exception class used to avoid handling a subrepo error more than once"""
58 def __init__(self, *args, **kw):
58 def __init__(self, *args, **kw):
59 self.subrepo = kw.pop('subrepo', None)
60 self.cause = kw.pop('cause', None)
59 error.Abort.__init__(self, *args, **kw)
61 error.Abort.__init__(self, *args, **kw)
60 self.subrepo = kw.get('subrepo')
61 self.cause = kw.get('cause')
62
62
63 def annotatesubrepoerror(func):
63 def annotatesubrepoerror(func):
64 def decoratedmethod(self, *args, **kargs):
64 def decoratedmethod(self, *args, **kargs):
65 try:
65 try:
66 res = func(self, *args, **kargs)
66 res = func(self, *args, **kargs)
67 except SubrepoAbort as ex:
67 except SubrepoAbort as ex:
68 # This exception has already been handled
68 # This exception has already been handled
69 raise ex
69 raise ex
70 except error.Abort as ex:
70 except error.Abort as ex:
71 subrepo = subrelpath(self)
71 subrepo = subrelpath(self)
72 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
72 errormsg = str(ex) + ' ' + _('(in subrepo %s)') % subrepo
73 # avoid handling this exception by raising a SubrepoAbort exception
73 # avoid handling this exception by raising a SubrepoAbort exception
74 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
74 raise SubrepoAbort(errormsg, hint=ex.hint, subrepo=subrepo,
75 cause=sys.exc_info())
75 cause=sys.exc_info())
76 return res
76 return res
77 return decoratedmethod
77 return decoratedmethod
78
78
79 def state(ctx, ui):
79 def state(ctx, ui):
80 """return a state dict, mapping subrepo paths configured in .hgsub
80 """return a state dict, mapping subrepo paths configured in .hgsub
81 to tuple: (source from .hgsub, revision from .hgsubstate, kind
81 to tuple: (source from .hgsub, revision from .hgsubstate, kind
82 (key in types dict))
82 (key in types dict))
83 """
83 """
84 p = config.config()
84 p = config.config()
85 repo = ctx.repo()
85 repo = ctx.repo()
86 def read(f, sections=None, remap=None):
86 def read(f, sections=None, remap=None):
87 if f in ctx:
87 if f in ctx:
88 try:
88 try:
89 data = ctx[f].data()
89 data = ctx[f].data()
90 except IOError as err:
90 except IOError as err:
91 if err.errno != errno.ENOENT:
91 if err.errno != errno.ENOENT:
92 raise
92 raise
93 # handle missing subrepo spec files as removed
93 # handle missing subrepo spec files as removed
94 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
94 ui.warn(_("warning: subrepo spec file \'%s\' not found\n") %
95 repo.pathto(f))
95 repo.pathto(f))
96 return
96 return
97 p.parse(f, data, sections, remap, read)
97 p.parse(f, data, sections, remap, read)
98 else:
98 else:
99 raise error.Abort(_("subrepo spec file \'%s\' not found") %
99 raise error.Abort(_("subrepo spec file \'%s\' not found") %
100 repo.pathto(f))
100 repo.pathto(f))
101 if '.hgsub' in ctx:
101 if '.hgsub' in ctx:
102 read('.hgsub')
102 read('.hgsub')
103
103
104 for path, src in ui.configitems('subpaths'):
104 for path, src in ui.configitems('subpaths'):
105 p.set('subpaths', path, src, ui.configsource('subpaths', path))
105 p.set('subpaths', path, src, ui.configsource('subpaths', path))
106
106
107 rev = {}
107 rev = {}
108 if '.hgsubstate' in ctx:
108 if '.hgsubstate' in ctx:
109 try:
109 try:
110 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
110 for i, l in enumerate(ctx['.hgsubstate'].data().splitlines()):
111 l = l.lstrip()
111 l = l.lstrip()
112 if not l:
112 if not l:
113 continue
113 continue
114 try:
114 try:
115 revision, path = l.split(" ", 1)
115 revision, path = l.split(" ", 1)
116 except ValueError:
116 except ValueError:
117 raise error.Abort(_("invalid subrepository revision "
117 raise error.Abort(_("invalid subrepository revision "
118 "specifier in \'%s\' line %d")
118 "specifier in \'%s\' line %d")
119 % (repo.pathto('.hgsubstate'), (i + 1)))
119 % (repo.pathto('.hgsubstate'), (i + 1)))
120 rev[path] = revision
120 rev[path] = revision
121 except IOError as err:
121 except IOError as err:
122 if err.errno != errno.ENOENT:
122 if err.errno != errno.ENOENT:
123 raise
123 raise
124
124
125 def remap(src):
125 def remap(src):
126 for pattern, repl in p.items('subpaths'):
126 for pattern, repl in p.items('subpaths'):
127 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
127 # Turn r'C:\foo\bar' into r'C:\\foo\\bar' since re.sub
128 # does a string decode.
128 # does a string decode.
129 repl = repl.encode('string-escape')
129 repl = repl.encode('string-escape')
130 # However, we still want to allow back references to go
130 # However, we still want to allow back references to go
131 # through unharmed, so we turn r'\\1' into r'\1'. Again,
131 # through unharmed, so we turn r'\\1' into r'\1'. Again,
132 # extra escapes are needed because re.sub string decodes.
132 # extra escapes are needed because re.sub string decodes.
133 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
133 repl = re.sub(r'\\\\([0-9]+)', r'\\\1', repl)
134 try:
134 try:
135 src = re.sub(pattern, repl, src, 1)
135 src = re.sub(pattern, repl, src, 1)
136 except re.error as e:
136 except re.error as e:
137 raise error.Abort(_("bad subrepository pattern in %s: %s")
137 raise error.Abort(_("bad subrepository pattern in %s: %s")
138 % (p.source('subpaths', pattern), e))
138 % (p.source('subpaths', pattern), e))
139 return src
139 return src
140
140
141 state = {}
141 state = {}
142 for path, src in p[''].items():
142 for path, src in p[''].items():
143 kind = 'hg'
143 kind = 'hg'
144 if src.startswith('['):
144 if src.startswith('['):
145 if ']' not in src:
145 if ']' not in src:
146 raise error.Abort(_('missing ] in subrepo source'))
146 raise error.Abort(_('missing ] in subrepo source'))
147 kind, src = src.split(']', 1)
147 kind, src = src.split(']', 1)
148 kind = kind[1:]
148 kind = kind[1:]
149 src = src.lstrip() # strip any extra whitespace after ']'
149 src = src.lstrip() # strip any extra whitespace after ']'
150
150
151 if not util.url(src).isabs():
151 if not util.url(src).isabs():
152 parent = _abssource(repo, abort=False)
152 parent = _abssource(repo, abort=False)
153 if parent:
153 if parent:
154 parent = util.url(parent)
154 parent = util.url(parent)
155 parent.path = posixpath.join(parent.path or '', src)
155 parent.path = posixpath.join(parent.path or '', src)
156 parent.path = posixpath.normpath(parent.path)
156 parent.path = posixpath.normpath(parent.path)
157 joined = str(parent)
157 joined = str(parent)
158 # Remap the full joined path and use it if it changes,
158 # Remap the full joined path and use it if it changes,
159 # else remap the original source.
159 # else remap the original source.
160 remapped = remap(joined)
160 remapped = remap(joined)
161 if remapped == joined:
161 if remapped == joined:
162 src = remap(src)
162 src = remap(src)
163 else:
163 else:
164 src = remapped
164 src = remapped
165
165
166 src = remap(src)
166 src = remap(src)
167 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
167 state[util.pconvert(path)] = (src.strip(), rev.get(path, ''), kind)
168
168
169 return state
169 return state
170
170
171 def writestate(repo, state):
171 def writestate(repo, state):
172 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
172 """rewrite .hgsubstate in (outer) repo with these subrepo states"""
173 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
173 lines = ['%s %s\n' % (state[s][1], s) for s in sorted(state)
174 if state[s][1] != nullstate[1]]
174 if state[s][1] != nullstate[1]]
175 repo.wwrite('.hgsubstate', ''.join(lines), '')
175 repo.wwrite('.hgsubstate', ''.join(lines), '')
176
176
177 def submerge(repo, wctx, mctx, actx, overwrite):
177 def submerge(repo, wctx, mctx, actx, overwrite):
178 """delegated from merge.applyupdates: merging of .hgsubstate file
178 """delegated from merge.applyupdates: merging of .hgsubstate file
179 in working context, merging context and ancestor context"""
179 in working context, merging context and ancestor context"""
180 if mctx == actx: # backwards?
180 if mctx == actx: # backwards?
181 actx = wctx.p1()
181 actx = wctx.p1()
182 s1 = wctx.substate
182 s1 = wctx.substate
183 s2 = mctx.substate
183 s2 = mctx.substate
184 sa = actx.substate
184 sa = actx.substate
185 sm = {}
185 sm = {}
186
186
187 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
187 repo.ui.debug("subrepo merge %s %s %s\n" % (wctx, mctx, actx))
188
188
189 def debug(s, msg, r=""):
189 def debug(s, msg, r=""):
190 if r:
190 if r:
191 r = "%s:%s:%s" % r
191 r = "%s:%s:%s" % r
192 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
192 repo.ui.debug(" subrepo %s: %s %s\n" % (s, msg, r))
193
193
194 for s, l in sorted(s1.iteritems()):
194 for s, l in sorted(s1.iteritems()):
195 a = sa.get(s, nullstate)
195 a = sa.get(s, nullstate)
196 ld = l # local state with possible dirty flag for compares
196 ld = l # local state with possible dirty flag for compares
197 if wctx.sub(s).dirty():
197 if wctx.sub(s).dirty():
198 ld = (l[0], l[1] + "+")
198 ld = (l[0], l[1] + "+")
199 if wctx == actx: # overwrite
199 if wctx == actx: # overwrite
200 a = ld
200 a = ld
201
201
202 if s in s2:
202 if s in s2:
203 r = s2[s]
203 r = s2[s]
204 if ld == r or r == a: # no change or local is newer
204 if ld == r or r == a: # no change or local is newer
205 sm[s] = l
205 sm[s] = l
206 continue
206 continue
207 elif ld == a: # other side changed
207 elif ld == a: # other side changed
208 debug(s, "other changed, get", r)
208 debug(s, "other changed, get", r)
209 wctx.sub(s).get(r, overwrite)
209 wctx.sub(s).get(r, overwrite)
210 sm[s] = r
210 sm[s] = r
211 elif ld[0] != r[0]: # sources differ
211 elif ld[0] != r[0]: # sources differ
212 if repo.ui.promptchoice(
212 if repo.ui.promptchoice(
213 _(' subrepository sources for %s differ\n'
213 _(' subrepository sources for %s differ\n'
214 'use (l)ocal source (%s) or (r)emote source (%s)?'
214 'use (l)ocal source (%s) or (r)emote source (%s)?'
215 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
215 '$$ &Local $$ &Remote') % (s, l[0], r[0]), 0):
216 debug(s, "prompt changed, get", r)
216 debug(s, "prompt changed, get", r)
217 wctx.sub(s).get(r, overwrite)
217 wctx.sub(s).get(r, overwrite)
218 sm[s] = r
218 sm[s] = r
219 elif ld[1] == a[1]: # local side is unchanged
219 elif ld[1] == a[1]: # local side is unchanged
220 debug(s, "other side changed, get", r)
220 debug(s, "other side changed, get", r)
221 wctx.sub(s).get(r, overwrite)
221 wctx.sub(s).get(r, overwrite)
222 sm[s] = r
222 sm[s] = r
223 else:
223 else:
224 debug(s, "both sides changed")
224 debug(s, "both sides changed")
225 srepo = wctx.sub(s)
225 srepo = wctx.sub(s)
226 option = repo.ui.promptchoice(
226 option = repo.ui.promptchoice(
227 _(' subrepository %s diverged (local revision: %s, '
227 _(' subrepository %s diverged (local revision: %s, '
228 'remote revision: %s)\n'
228 'remote revision: %s)\n'
229 '(M)erge, keep (l)ocal or keep (r)emote?'
229 '(M)erge, keep (l)ocal or keep (r)emote?'
230 '$$ &Merge $$ &Local $$ &Remote')
230 '$$ &Merge $$ &Local $$ &Remote')
231 % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
231 % (s, srepo.shortid(l[1]), srepo.shortid(r[1])), 0)
232 if option == 0:
232 if option == 0:
233 wctx.sub(s).merge(r)
233 wctx.sub(s).merge(r)
234 sm[s] = l
234 sm[s] = l
235 debug(s, "merge with", r)
235 debug(s, "merge with", r)
236 elif option == 1:
236 elif option == 1:
237 sm[s] = l
237 sm[s] = l
238 debug(s, "keep local subrepo revision", l)
238 debug(s, "keep local subrepo revision", l)
239 else:
239 else:
240 wctx.sub(s).get(r, overwrite)
240 wctx.sub(s).get(r, overwrite)
241 sm[s] = r
241 sm[s] = r
242 debug(s, "get remote subrepo revision", r)
242 debug(s, "get remote subrepo revision", r)
243 elif ld == a: # remote removed, local unchanged
243 elif ld == a: # remote removed, local unchanged
244 debug(s, "remote removed, remove")
244 debug(s, "remote removed, remove")
245 wctx.sub(s).remove()
245 wctx.sub(s).remove()
246 elif a == nullstate: # not present in remote or ancestor
246 elif a == nullstate: # not present in remote or ancestor
247 debug(s, "local added, keep")
247 debug(s, "local added, keep")
248 sm[s] = l
248 sm[s] = l
249 continue
249 continue
250 else:
250 else:
251 if repo.ui.promptchoice(
251 if repo.ui.promptchoice(
252 _(' local changed subrepository %s which remote removed\n'
252 _(' local changed subrepository %s which remote removed\n'
253 'use (c)hanged version or (d)elete?'
253 'use (c)hanged version or (d)elete?'
254 '$$ &Changed $$ &Delete') % s, 0):
254 '$$ &Changed $$ &Delete') % s, 0):
255 debug(s, "prompt remove")
255 debug(s, "prompt remove")
256 wctx.sub(s).remove()
256 wctx.sub(s).remove()
257
257
258 for s, r in sorted(s2.items()):
258 for s, r in sorted(s2.items()):
259 if s in s1:
259 if s in s1:
260 continue
260 continue
261 elif s not in sa:
261 elif s not in sa:
262 debug(s, "remote added, get", r)
262 debug(s, "remote added, get", r)
263 mctx.sub(s).get(r)
263 mctx.sub(s).get(r)
264 sm[s] = r
264 sm[s] = r
265 elif r != sa[s]:
265 elif r != sa[s]:
266 if repo.ui.promptchoice(
266 if repo.ui.promptchoice(
267 _(' remote changed subrepository %s which local removed\n'
267 _(' remote changed subrepository %s which local removed\n'
268 'use (c)hanged version or (d)elete?'
268 'use (c)hanged version or (d)elete?'
269 '$$ &Changed $$ &Delete') % s, 0) == 0:
269 '$$ &Changed $$ &Delete') % s, 0) == 0:
270 debug(s, "prompt recreate", r)
270 debug(s, "prompt recreate", r)
271 mctx.sub(s).get(r)
271 mctx.sub(s).get(r)
272 sm[s] = r
272 sm[s] = r
273
273
274 # record merged .hgsubstate
274 # record merged .hgsubstate
275 writestate(repo, sm)
275 writestate(repo, sm)
276 return sm
276 return sm
277
277
278 def _updateprompt(ui, sub, dirty, local, remote):
278 def _updateprompt(ui, sub, dirty, local, remote):
279 if dirty:
279 if dirty:
280 msg = (_(' subrepository sources for %s differ\n'
280 msg = (_(' subrepository sources for %s differ\n'
281 'use (l)ocal source (%s) or (r)emote source (%s)?'
281 'use (l)ocal source (%s) or (r)emote source (%s)?'
282 '$$ &Local $$ &Remote')
282 '$$ &Local $$ &Remote')
283 % (subrelpath(sub), local, remote))
283 % (subrelpath(sub), local, remote))
284 else:
284 else:
285 msg = (_(' subrepository sources for %s differ (in checked out '
285 msg = (_(' subrepository sources for %s differ (in checked out '
286 'version)\n'
286 'version)\n'
287 'use (l)ocal source (%s) or (r)emote source (%s)?'
287 'use (l)ocal source (%s) or (r)emote source (%s)?'
288 '$$ &Local $$ &Remote')
288 '$$ &Local $$ &Remote')
289 % (subrelpath(sub), local, remote))
289 % (subrelpath(sub), local, remote))
290 return ui.promptchoice(msg, 0)
290 return ui.promptchoice(msg, 0)
291
291
292 def reporelpath(repo):
292 def reporelpath(repo):
293 """return path to this (sub)repo as seen from outermost repo"""
293 """return path to this (sub)repo as seen from outermost repo"""
294 parent = repo
294 parent = repo
295 while util.safehasattr(parent, '_subparent'):
295 while util.safehasattr(parent, '_subparent'):
296 parent = parent._subparent
296 parent = parent._subparent
297 return repo.root[len(pathutil.normasprefix(parent.root)):]
297 return repo.root[len(pathutil.normasprefix(parent.root)):]
298
298
299 def subrelpath(sub):
299 def subrelpath(sub):
300 """return path to this subrepo as seen from outermost repo"""
300 """return path to this subrepo as seen from outermost repo"""
301 return sub._relpath
301 return sub._relpath
302
302
303 def _abssource(repo, push=False, abort=True):
303 def _abssource(repo, push=False, abort=True):
304 """return pull/push path of repo - either based on parent repo .hgsub info
304 """return pull/push path of repo - either based on parent repo .hgsub info
305 or on the top repo config. Abort or return None if no source found."""
305 or on the top repo config. Abort or return None if no source found."""
306 if util.safehasattr(repo, '_subparent'):
306 if util.safehasattr(repo, '_subparent'):
307 source = util.url(repo._subsource)
307 source = util.url(repo._subsource)
308 if source.isabs():
308 if source.isabs():
309 return str(source)
309 return str(source)
310 source.path = posixpath.normpath(source.path)
310 source.path = posixpath.normpath(source.path)
311 parent = _abssource(repo._subparent, push, abort=False)
311 parent = _abssource(repo._subparent, push, abort=False)
312 if parent:
312 if parent:
313 parent = util.url(util.pconvert(parent))
313 parent = util.url(util.pconvert(parent))
314 parent.path = posixpath.join(parent.path or '', source.path)
314 parent.path = posixpath.join(parent.path or '', source.path)
315 parent.path = posixpath.normpath(parent.path)
315 parent.path = posixpath.normpath(parent.path)
316 return str(parent)
316 return str(parent)
317 else: # recursion reached top repo
317 else: # recursion reached top repo
318 if util.safehasattr(repo, '_subtoppath'):
318 if util.safehasattr(repo, '_subtoppath'):
319 return repo._subtoppath
319 return repo._subtoppath
320 if push and repo.ui.config('paths', 'default-push'):
320 if push and repo.ui.config('paths', 'default-push'):
321 return repo.ui.config('paths', 'default-push')
321 return repo.ui.config('paths', 'default-push')
322 if repo.ui.config('paths', 'default'):
322 if repo.ui.config('paths', 'default'):
323 return repo.ui.config('paths', 'default')
323 return repo.ui.config('paths', 'default')
324 if repo.shared():
324 if repo.shared():
325 # chop off the .hg component to get the default path form
325 # chop off the .hg component to get the default path form
326 return os.path.dirname(repo.sharedpath)
326 return os.path.dirname(repo.sharedpath)
327 if abort:
327 if abort:
328 raise error.Abort(_("default path for subrepository not found"))
328 raise error.Abort(_("default path for subrepository not found"))
329
329
330 def _sanitize(ui, vfs, ignore):
330 def _sanitize(ui, vfs, ignore):
331 for dirname, dirs, names in vfs.walk():
331 for dirname, dirs, names in vfs.walk():
332 for i, d in enumerate(dirs):
332 for i, d in enumerate(dirs):
333 if d.lower() == ignore:
333 if d.lower() == ignore:
334 del dirs[i]
334 del dirs[i]
335 break
335 break
336 if vfs.basename(dirname).lower() != '.hg':
336 if vfs.basename(dirname).lower() != '.hg':
337 continue
337 continue
338 for f in names:
338 for f in names:
339 if f.lower() == 'hgrc':
339 if f.lower() == 'hgrc':
340 ui.warn(_("warning: removing potentially hostile 'hgrc' "
340 ui.warn(_("warning: removing potentially hostile 'hgrc' "
341 "in '%s'\n") % vfs.join(dirname))
341 "in '%s'\n") % vfs.join(dirname))
342 vfs.unlink(vfs.reljoin(dirname, f))
342 vfs.unlink(vfs.reljoin(dirname, f))
343
343
344 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
344 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
345 """return instance of the right subrepo class for subrepo in path"""
345 """return instance of the right subrepo class for subrepo in path"""
346 # subrepo inherently violates our import layering rules
346 # subrepo inherently violates our import layering rules
347 # because it wants to make repo objects from deep inside the stack
347 # because it wants to make repo objects from deep inside the stack
348 # so we manually delay the circular imports to not break
348 # so we manually delay the circular imports to not break
349 # scripts that don't use our demand-loading
349 # scripts that don't use our demand-loading
350 global hg
350 global hg
351 from . import hg as h
351 from . import hg as h
352 hg = h
352 hg = h
353
353
354 pathutil.pathauditor(ctx.repo().root)(path)
354 pathutil.pathauditor(ctx.repo().root)(path)
355 state = ctx.substate[path]
355 state = ctx.substate[path]
356 if state[2] not in types:
356 if state[2] not in types:
357 raise error.Abort(_('unknown subrepo type %s') % state[2])
357 raise error.Abort(_('unknown subrepo type %s') % state[2])
358 if allowwdir:
358 if allowwdir:
359 state = (state[0], ctx.subrev(path), state[2])
359 state = (state[0], ctx.subrev(path), state[2])
360 return types[state[2]](ctx, path, state[:2], allowcreate)
360 return types[state[2]](ctx, path, state[:2], allowcreate)
361
361
362 def nullsubrepo(ctx, path, pctx):
362 def nullsubrepo(ctx, path, pctx):
363 """return an empty subrepo in pctx for the extant subrepo in ctx"""
363 """return an empty subrepo in pctx for the extant subrepo in ctx"""
364 # subrepo inherently violates our import layering rules
364 # subrepo inherently violates our import layering rules
365 # because it wants to make repo objects from deep inside the stack
365 # because it wants to make repo objects from deep inside the stack
366 # so we manually delay the circular imports to not break
366 # so we manually delay the circular imports to not break
367 # scripts that don't use our demand-loading
367 # scripts that don't use our demand-loading
368 global hg
368 global hg
369 from . import hg as h
369 from . import hg as h
370 hg = h
370 hg = h
371
371
372 pathutil.pathauditor(ctx.repo().root)(path)
372 pathutil.pathauditor(ctx.repo().root)(path)
373 state = ctx.substate[path]
373 state = ctx.substate[path]
374 if state[2] not in types:
374 if state[2] not in types:
375 raise error.Abort(_('unknown subrepo type %s') % state[2])
375 raise error.Abort(_('unknown subrepo type %s') % state[2])
376 subrev = ''
376 subrev = ''
377 if state[2] == 'hg':
377 if state[2] == 'hg':
378 subrev = "0" * 40
378 subrev = "0" * 40
379 return types[state[2]](pctx, path, (state[0], subrev), True)
379 return types[state[2]](pctx, path, (state[0], subrev), True)
380
380
381 def newcommitphase(ui, ctx):
381 def newcommitphase(ui, ctx):
382 commitphase = phases.newcommitphase(ui)
382 commitphase = phases.newcommitphase(ui)
383 substate = getattr(ctx, "substate", None)
383 substate = getattr(ctx, "substate", None)
384 if not substate:
384 if not substate:
385 return commitphase
385 return commitphase
386 check = ui.config('phases', 'checksubrepos', 'follow')
386 check = ui.config('phases', 'checksubrepos', 'follow')
387 if check not in ('ignore', 'follow', 'abort'):
387 if check not in ('ignore', 'follow', 'abort'):
388 raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
388 raise error.Abort(_('invalid phases.checksubrepos configuration: %s')
389 % (check))
389 % (check))
390 if check == 'ignore':
390 if check == 'ignore':
391 return commitphase
391 return commitphase
392 maxphase = phases.public
392 maxphase = phases.public
393 maxsub = None
393 maxsub = None
394 for s in sorted(substate):
394 for s in sorted(substate):
395 sub = ctx.sub(s)
395 sub = ctx.sub(s)
396 subphase = sub.phase(substate[s][1])
396 subphase = sub.phase(substate[s][1])
397 if maxphase < subphase:
397 if maxphase < subphase:
398 maxphase = subphase
398 maxphase = subphase
399 maxsub = s
399 maxsub = s
400 if commitphase < maxphase:
400 if commitphase < maxphase:
401 if check == 'abort':
401 if check == 'abort':
402 raise error.Abort(_("can't commit in %s phase"
402 raise error.Abort(_("can't commit in %s phase"
403 " conflicting %s from subrepository %s") %
403 " conflicting %s from subrepository %s") %
404 (phases.phasenames[commitphase],
404 (phases.phasenames[commitphase],
405 phases.phasenames[maxphase], maxsub))
405 phases.phasenames[maxphase], maxsub))
406 ui.warn(_("warning: changes are committed in"
406 ui.warn(_("warning: changes are committed in"
407 " %s phase from subrepository %s\n") %
407 " %s phase from subrepository %s\n") %
408 (phases.phasenames[maxphase], maxsub))
408 (phases.phasenames[maxphase], maxsub))
409 return maxphase
409 return maxphase
410 return commitphase
410 return commitphase
411
411
412 # subrepo classes need to implement the following abstract class:
412 # subrepo classes need to implement the following abstract class:
413
413
414 class abstractsubrepo(object):
414 class abstractsubrepo(object):
415
415
416 def __init__(self, ctx, path):
416 def __init__(self, ctx, path):
417 """Initialize abstractsubrepo part
417 """Initialize abstractsubrepo part
418
418
419 ``ctx`` is the context referring this subrepository in the
419 ``ctx`` is the context referring this subrepository in the
420 parent repository.
420 parent repository.
421
421
422 ``path`` is the path to this subrepository as seen from
422 ``path`` is the path to this subrepository as seen from
423 innermost repository.
423 innermost repository.
424 """
424 """
425 self.ui = ctx.repo().ui
425 self.ui = ctx.repo().ui
426 self._ctx = ctx
426 self._ctx = ctx
427 self._path = path
427 self._path = path
428
428
429 def storeclean(self, path):
429 def storeclean(self, path):
430 """
430 """
431 returns true if the repository has not changed since it was last
431 returns true if the repository has not changed since it was last
432 cloned from or pushed to a given repository.
432 cloned from or pushed to a given repository.
433 """
433 """
434 return False
434 return False
435
435
436 def dirty(self, ignoreupdate=False):
436 def dirty(self, ignoreupdate=False):
437 """returns true if the dirstate of the subrepo is dirty or does not
437 """returns true if the dirstate of the subrepo is dirty or does not
438 match current stored state. If ignoreupdate is true, only check
438 match current stored state. If ignoreupdate is true, only check
439 whether the subrepo has uncommitted changes in its dirstate.
439 whether the subrepo has uncommitted changes in its dirstate.
440 """
440 """
441 raise NotImplementedError
441 raise NotImplementedError
442
442
443 def dirtyreason(self, ignoreupdate=False):
443 def dirtyreason(self, ignoreupdate=False):
444 """return reason string if it is ``dirty()``
444 """return reason string if it is ``dirty()``
445
445
446 Returned string should have enough information for the message
446 Returned string should have enough information for the message
447 of exception.
447 of exception.
448
448
449 This returns None, otherwise.
449 This returns None, otherwise.
450 """
450 """
451 if self.dirty(ignoreupdate=ignoreupdate):
451 if self.dirty(ignoreupdate=ignoreupdate):
452 return _("uncommitted changes in subrepository '%s'"
452 return _("uncommitted changes in subrepository '%s'"
453 ) % subrelpath(self)
453 ) % subrelpath(self)
454
454
455 def bailifchanged(self, ignoreupdate=False):
455 def bailifchanged(self, ignoreupdate=False):
456 """raise Abort if subrepository is ``dirty()``
456 """raise Abort if subrepository is ``dirty()``
457 """
457 """
458 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
458 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate)
459 if dirtyreason:
459 if dirtyreason:
460 raise error.Abort(dirtyreason)
460 raise error.Abort(dirtyreason)
461
461
462 def basestate(self):
462 def basestate(self):
463 """current working directory base state, disregarding .hgsubstate
463 """current working directory base state, disregarding .hgsubstate
464 state and working directory modifications"""
464 state and working directory modifications"""
465 raise NotImplementedError
465 raise NotImplementedError
466
466
467 def checknested(self, path):
467 def checknested(self, path):
468 """check if path is a subrepository within this repository"""
468 """check if path is a subrepository within this repository"""
469 return False
469 return False
470
470
471 def commit(self, text, user, date):
471 def commit(self, text, user, date):
472 """commit the current changes to the subrepo with the given
472 """commit the current changes to the subrepo with the given
473 log message. Use given user and date if possible. Return the
473 log message. Use given user and date if possible. Return the
474 new state of the subrepo.
474 new state of the subrepo.
475 """
475 """
476 raise NotImplementedError
476 raise NotImplementedError
477
477
478 def phase(self, state):
478 def phase(self, state):
479 """returns phase of specified state in the subrepository.
479 """returns phase of specified state in the subrepository.
480 """
480 """
481 return phases.public
481 return phases.public
482
482
483 def remove(self):
483 def remove(self):
484 """remove the subrepo
484 """remove the subrepo
485
485
486 (should verify the dirstate is not dirty first)
486 (should verify the dirstate is not dirty first)
487 """
487 """
488 raise NotImplementedError
488 raise NotImplementedError
489
489
490 def get(self, state, overwrite=False):
490 def get(self, state, overwrite=False):
491 """run whatever commands are needed to put the subrepo into
491 """run whatever commands are needed to put the subrepo into
492 this state
492 this state
493 """
493 """
494 raise NotImplementedError
494 raise NotImplementedError
495
495
496 def merge(self, state):
496 def merge(self, state):
497 """merge currently-saved state with the new state."""
497 """merge currently-saved state with the new state."""
498 raise NotImplementedError
498 raise NotImplementedError
499
499
500 def push(self, opts):
500 def push(self, opts):
501 """perform whatever action is analogous to 'hg push'
501 """perform whatever action is analogous to 'hg push'
502
502
503 This may be a no-op on some systems.
503 This may be a no-op on some systems.
504 """
504 """
505 raise NotImplementedError
505 raise NotImplementedError
506
506
507 def add(self, ui, match, prefix, explicitonly, **opts):
507 def add(self, ui, match, prefix, explicitonly, **opts):
508 return []
508 return []
509
509
510 def addremove(self, matcher, prefix, opts, dry_run, similarity):
510 def addremove(self, matcher, prefix, opts, dry_run, similarity):
511 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
511 self.ui.warn("%s: %s" % (prefix, _("addremove is not supported")))
512 return 1
512 return 1
513
513
514 def cat(self, match, prefix, **opts):
514 def cat(self, match, prefix, **opts):
515 return 1
515 return 1
516
516
517 def status(self, rev2, **opts):
517 def status(self, rev2, **opts):
518 return scmutil.status([], [], [], [], [], [], [])
518 return scmutil.status([], [], [], [], [], [], [])
519
519
520 def diff(self, ui, diffopts, node2, match, prefix, **opts):
520 def diff(self, ui, diffopts, node2, match, prefix, **opts):
521 pass
521 pass
522
522
523 def outgoing(self, ui, dest, opts):
523 def outgoing(self, ui, dest, opts):
524 return 1
524 return 1
525
525
526 def incoming(self, ui, source, opts):
526 def incoming(self, ui, source, opts):
527 return 1
527 return 1
528
528
529 def files(self):
529 def files(self):
530 """return filename iterator"""
530 """return filename iterator"""
531 raise NotImplementedError
531 raise NotImplementedError
532
532
533 def filedata(self, name):
533 def filedata(self, name):
534 """return file data"""
534 """return file data"""
535 raise NotImplementedError
535 raise NotImplementedError
536
536
537 def fileflags(self, name):
537 def fileflags(self, name):
538 """return file flags"""
538 """return file flags"""
539 return ''
539 return ''
540
540
541 def getfileset(self, expr):
541 def getfileset(self, expr):
542 """Resolve the fileset expression for this repo"""
542 """Resolve the fileset expression for this repo"""
543 return set()
543 return set()
544
544
545 def printfiles(self, ui, m, fm, fmt, subrepos):
545 def printfiles(self, ui, m, fm, fmt, subrepos):
546 """handle the files command for this subrepo"""
546 """handle the files command for this subrepo"""
547 return 1
547 return 1
548
548
549 def archive(self, archiver, prefix, match=None):
549 def archive(self, archiver, prefix, match=None):
550 if match is not None:
550 if match is not None:
551 files = [f for f in self.files() if match(f)]
551 files = [f for f in self.files() if match(f)]
552 else:
552 else:
553 files = self.files()
553 files = self.files()
554 total = len(files)
554 total = len(files)
555 relpath = subrelpath(self)
555 relpath = subrelpath(self)
556 self.ui.progress(_('archiving (%s)') % relpath, 0,
556 self.ui.progress(_('archiving (%s)') % relpath, 0,
557 unit=_('files'), total=total)
557 unit=_('files'), total=total)
558 for i, name in enumerate(files):
558 for i, name in enumerate(files):
559 flags = self.fileflags(name)
559 flags = self.fileflags(name)
560 mode = 'x' in flags and 0o755 or 0o644
560 mode = 'x' in flags and 0o755 or 0o644
561 symlink = 'l' in flags
561 symlink = 'l' in flags
562 archiver.addfile(prefix + self._path + '/' + name,
562 archiver.addfile(prefix + self._path + '/' + name,
563 mode, symlink, self.filedata(name))
563 mode, symlink, self.filedata(name))
564 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
564 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
565 unit=_('files'), total=total)
565 unit=_('files'), total=total)
566 self.ui.progress(_('archiving (%s)') % relpath, None)
566 self.ui.progress(_('archiving (%s)') % relpath, None)
567 return total
567 return total
568
568
569 def walk(self, match):
569 def walk(self, match):
570 '''
570 '''
571 walk recursively through the directory tree, finding all files
571 walk recursively through the directory tree, finding all files
572 matched by the match function
572 matched by the match function
573 '''
573 '''
574 pass
574 pass
575
575
576 def forget(self, match, prefix):
576 def forget(self, match, prefix):
577 return ([], [])
577 return ([], [])
578
578
579 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
579 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
580 """remove the matched files from the subrepository and the filesystem,
580 """remove the matched files from the subrepository and the filesystem,
581 possibly by force and/or after the file has been removed from the
581 possibly by force and/or after the file has been removed from the
582 filesystem. Return 0 on success, 1 on any warning.
582 filesystem. Return 0 on success, 1 on any warning.
583 """
583 """
584 warnings.append(_("warning: removefiles not implemented (%s)")
584 warnings.append(_("warning: removefiles not implemented (%s)")
585 % self._path)
585 % self._path)
586 return 1
586 return 1
587
587
588 def revert(self, substate, *pats, **opts):
588 def revert(self, substate, *pats, **opts):
589 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
589 self.ui.warn(_('%s: reverting %s subrepos is unsupported\n') \
590 % (substate[0], substate[2]))
590 % (substate[0], substate[2]))
591 return []
591 return []
592
592
593 def shortid(self, revid):
593 def shortid(self, revid):
594 return revid
594 return revid
595
595
596 def verify(self):
596 def verify(self):
597 '''verify the integrity of the repository. Return 0 on success or
597 '''verify the integrity of the repository. Return 0 on success or
598 warning, 1 on any error.
598 warning, 1 on any error.
599 '''
599 '''
600 return 0
600 return 0
601
601
602 @propertycache
602 @propertycache
603 def wvfs(self):
603 def wvfs(self):
604 """return vfs to access the working directory of this subrepository
604 """return vfs to access the working directory of this subrepository
605 """
605 """
606 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
606 return scmutil.vfs(self._ctx.repo().wvfs.join(self._path))
607
607
608 @propertycache
608 @propertycache
609 def _relpath(self):
609 def _relpath(self):
610 """return path to this subrepository as seen from outermost repository
610 """return path to this subrepository as seen from outermost repository
611 """
611 """
612 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
612 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
613
613
614 class hgsubrepo(abstractsubrepo):
614 class hgsubrepo(abstractsubrepo):
615 def __init__(self, ctx, path, state, allowcreate):
615 def __init__(self, ctx, path, state, allowcreate):
616 super(hgsubrepo, self).__init__(ctx, path)
616 super(hgsubrepo, self).__init__(ctx, path)
617 self._state = state
617 self._state = state
618 r = ctx.repo()
618 r = ctx.repo()
619 root = r.wjoin(path)
619 root = r.wjoin(path)
620 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
620 create = allowcreate and not r.wvfs.exists('%s/.hg' % path)
621 self._repo = hg.repository(r.baseui, root, create=create)
621 self._repo = hg.repository(r.baseui, root, create=create)
622
622
623 # Propagate the parent's --hidden option
623 # Propagate the parent's --hidden option
624 if r is r.unfiltered():
624 if r is r.unfiltered():
625 self._repo = self._repo.unfiltered()
625 self._repo = self._repo.unfiltered()
626
626
627 self.ui = self._repo.ui
627 self.ui = self._repo.ui
628 for s, k in [('ui', 'commitsubrepos')]:
628 for s, k in [('ui', 'commitsubrepos')]:
629 v = r.ui.config(s, k)
629 v = r.ui.config(s, k)
630 if v:
630 if v:
631 self.ui.setconfig(s, k, v, 'subrepo')
631 self.ui.setconfig(s, k, v, 'subrepo')
632 # internal config: ui._usedassubrepo
632 # internal config: ui._usedassubrepo
633 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
633 self.ui.setconfig('ui', '_usedassubrepo', 'True', 'subrepo')
634 self._initrepo(r, state[0], create)
634 self._initrepo(r, state[0], create)
635
635
636 def storeclean(self, path):
636 def storeclean(self, path):
637 with self._repo.lock():
637 with self._repo.lock():
638 return self._storeclean(path)
638 return self._storeclean(path)
639
639
640 def _storeclean(self, path):
640 def _storeclean(self, path):
641 clean = True
641 clean = True
642 itercache = self._calcstorehash(path)
642 itercache = self._calcstorehash(path)
643 for filehash in self._readstorehashcache(path):
643 for filehash in self._readstorehashcache(path):
644 if filehash != next(itercache, None):
644 if filehash != next(itercache, None):
645 clean = False
645 clean = False
646 break
646 break
647 if clean:
647 if clean:
648 # if not empty:
648 # if not empty:
649 # the cached and current pull states have a different size
649 # the cached and current pull states have a different size
650 clean = next(itercache, None) is None
650 clean = next(itercache, None) is None
651 return clean
651 return clean
652
652
653 def _calcstorehash(self, remotepath):
653 def _calcstorehash(self, remotepath):
654 '''calculate a unique "store hash"
654 '''calculate a unique "store hash"
655
655
656 This method is used to to detect when there are changes that may
656 This method is used to to detect when there are changes that may
657 require a push to a given remote path.'''
657 require a push to a given remote path.'''
658 # sort the files that will be hashed in increasing (likely) file size
658 # sort the files that will be hashed in increasing (likely) file size
659 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
659 filelist = ('bookmarks', 'store/phaseroots', 'store/00changelog.i')
660 yield '# %s\n' % _expandedabspath(remotepath)
660 yield '# %s\n' % _expandedabspath(remotepath)
661 vfs = self._repo.vfs
661 vfs = self._repo.vfs
662 for relname in filelist:
662 for relname in filelist:
663 filehash = hashlib.sha1(vfs.tryread(relname)).hexdigest()
663 filehash = hashlib.sha1(vfs.tryread(relname)).hexdigest()
664 yield '%s = %s\n' % (relname, filehash)
664 yield '%s = %s\n' % (relname, filehash)
665
665
666 @propertycache
666 @propertycache
667 def _cachestorehashvfs(self):
667 def _cachestorehashvfs(self):
668 return scmutil.vfs(self._repo.join('cache/storehash'))
668 return scmutil.vfs(self._repo.join('cache/storehash'))
669
669
670 def _readstorehashcache(self, remotepath):
670 def _readstorehashcache(self, remotepath):
671 '''read the store hash cache for a given remote repository'''
671 '''read the store hash cache for a given remote repository'''
672 cachefile = _getstorehashcachename(remotepath)
672 cachefile = _getstorehashcachename(remotepath)
673 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
673 return self._cachestorehashvfs.tryreadlines(cachefile, 'r')
674
674
675 def _cachestorehash(self, remotepath):
675 def _cachestorehash(self, remotepath):
676 '''cache the current store hash
676 '''cache the current store hash
677
677
678 Each remote repo requires its own store hash cache, because a subrepo
678 Each remote repo requires its own store hash cache, because a subrepo
679 store may be "clean" versus a given remote repo, but not versus another
679 store may be "clean" versus a given remote repo, but not versus another
680 '''
680 '''
681 cachefile = _getstorehashcachename(remotepath)
681 cachefile = _getstorehashcachename(remotepath)
682 with self._repo.lock():
682 with self._repo.lock():
683 storehash = list(self._calcstorehash(remotepath))
683 storehash = list(self._calcstorehash(remotepath))
684 vfs = self._cachestorehashvfs
684 vfs = self._cachestorehashvfs
685 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
685 vfs.writelines(cachefile, storehash, mode='w', notindexed=True)
686
686
687 def _getctx(self):
687 def _getctx(self):
688 '''fetch the context for this subrepo revision, possibly a workingctx
688 '''fetch the context for this subrepo revision, possibly a workingctx
689 '''
689 '''
690 if self._ctx.rev() is None:
690 if self._ctx.rev() is None:
691 return self._repo[None] # workingctx if parent is workingctx
691 return self._repo[None] # workingctx if parent is workingctx
692 else:
692 else:
693 rev = self._state[1]
693 rev = self._state[1]
694 return self._repo[rev]
694 return self._repo[rev]
695
695
696 @annotatesubrepoerror
696 @annotatesubrepoerror
697 def _initrepo(self, parentrepo, source, create):
697 def _initrepo(self, parentrepo, source, create):
698 self._repo._subparent = parentrepo
698 self._repo._subparent = parentrepo
699 self._repo._subsource = source
699 self._repo._subsource = source
700
700
701 if create:
701 if create:
702 lines = ['[paths]\n']
702 lines = ['[paths]\n']
703
703
704 def addpathconfig(key, value):
704 def addpathconfig(key, value):
705 if value:
705 if value:
706 lines.append('%s = %s\n' % (key, value))
706 lines.append('%s = %s\n' % (key, value))
707 self.ui.setconfig('paths', key, value, 'subrepo')
707 self.ui.setconfig('paths', key, value, 'subrepo')
708
708
709 defpath = _abssource(self._repo, abort=False)
709 defpath = _abssource(self._repo, abort=False)
710 defpushpath = _abssource(self._repo, True, abort=False)
710 defpushpath = _abssource(self._repo, True, abort=False)
711 addpathconfig('default', defpath)
711 addpathconfig('default', defpath)
712 if defpath != defpushpath:
712 if defpath != defpushpath:
713 addpathconfig('default-push', defpushpath)
713 addpathconfig('default-push', defpushpath)
714
714
715 fp = self._repo.vfs("hgrc", "w", text=True)
715 fp = self._repo.vfs("hgrc", "w", text=True)
716 try:
716 try:
717 fp.write(''.join(lines))
717 fp.write(''.join(lines))
718 finally:
718 finally:
719 fp.close()
719 fp.close()
720
720
721 @annotatesubrepoerror
721 @annotatesubrepoerror
722 def add(self, ui, match, prefix, explicitonly, **opts):
722 def add(self, ui, match, prefix, explicitonly, **opts):
723 return cmdutil.add(ui, self._repo, match,
723 return cmdutil.add(ui, self._repo, match,
724 self.wvfs.reljoin(prefix, self._path),
724 self.wvfs.reljoin(prefix, self._path),
725 explicitonly, **opts)
725 explicitonly, **opts)
726
726
727 @annotatesubrepoerror
727 @annotatesubrepoerror
728 def addremove(self, m, prefix, opts, dry_run, similarity):
728 def addremove(self, m, prefix, opts, dry_run, similarity):
729 # In the same way as sub directories are processed, once in a subrepo,
729 # In the same way as sub directories are processed, once in a subrepo,
730 # always entry any of its subrepos. Don't corrupt the options that will
730 # always entry any of its subrepos. Don't corrupt the options that will
731 # be used to process sibling subrepos however.
731 # be used to process sibling subrepos however.
732 opts = copy.copy(opts)
732 opts = copy.copy(opts)
733 opts['subrepos'] = True
733 opts['subrepos'] = True
734 return scmutil.addremove(self._repo, m,
734 return scmutil.addremove(self._repo, m,
735 self.wvfs.reljoin(prefix, self._path), opts,
735 self.wvfs.reljoin(prefix, self._path), opts,
736 dry_run, similarity)
736 dry_run, similarity)
737
737
738 @annotatesubrepoerror
738 @annotatesubrepoerror
739 def cat(self, match, prefix, **opts):
739 def cat(self, match, prefix, **opts):
740 rev = self._state[1]
740 rev = self._state[1]
741 ctx = self._repo[rev]
741 ctx = self._repo[rev]
742 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
742 return cmdutil.cat(self.ui, self._repo, ctx, match, prefix, **opts)
743
743
744 @annotatesubrepoerror
744 @annotatesubrepoerror
745 def status(self, rev2, **opts):
745 def status(self, rev2, **opts):
746 try:
746 try:
747 rev1 = self._state[1]
747 rev1 = self._state[1]
748 ctx1 = self._repo[rev1]
748 ctx1 = self._repo[rev1]
749 ctx2 = self._repo[rev2]
749 ctx2 = self._repo[rev2]
750 return self._repo.status(ctx1, ctx2, **opts)
750 return self._repo.status(ctx1, ctx2, **opts)
751 except error.RepoLookupError as inst:
751 except error.RepoLookupError as inst:
752 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
752 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
753 % (inst, subrelpath(self)))
753 % (inst, subrelpath(self)))
754 return scmutil.status([], [], [], [], [], [], [])
754 return scmutil.status([], [], [], [], [], [], [])
755
755
756 @annotatesubrepoerror
756 @annotatesubrepoerror
757 def diff(self, ui, diffopts, node2, match, prefix, **opts):
757 def diff(self, ui, diffopts, node2, match, prefix, **opts):
758 try:
758 try:
759 node1 = node.bin(self._state[1])
759 node1 = node.bin(self._state[1])
760 # We currently expect node2 to come from substate and be
760 # We currently expect node2 to come from substate and be
761 # in hex format
761 # in hex format
762 if node2 is not None:
762 if node2 is not None:
763 node2 = node.bin(node2)
763 node2 = node.bin(node2)
764 cmdutil.diffordiffstat(ui, self._repo, diffopts,
764 cmdutil.diffordiffstat(ui, self._repo, diffopts,
765 node1, node2, match,
765 node1, node2, match,
766 prefix=posixpath.join(prefix, self._path),
766 prefix=posixpath.join(prefix, self._path),
767 listsubrepos=True, **opts)
767 listsubrepos=True, **opts)
768 except error.RepoLookupError as inst:
768 except error.RepoLookupError as inst:
769 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
769 self.ui.warn(_('warning: error "%s" in subrepository "%s"\n')
770 % (inst, subrelpath(self)))
770 % (inst, subrelpath(self)))
771
771
772 @annotatesubrepoerror
772 @annotatesubrepoerror
773 def archive(self, archiver, prefix, match=None):
773 def archive(self, archiver, prefix, match=None):
774 self._get(self._state + ('hg',))
774 self._get(self._state + ('hg',))
775 total = abstractsubrepo.archive(self, archiver, prefix, match)
775 total = abstractsubrepo.archive(self, archiver, prefix, match)
776 rev = self._state[1]
776 rev = self._state[1]
777 ctx = self._repo[rev]
777 ctx = self._repo[rev]
778 for subpath in ctx.substate:
778 for subpath in ctx.substate:
779 s = subrepo(ctx, subpath, True)
779 s = subrepo(ctx, subpath, True)
780 submatch = matchmod.subdirmatcher(subpath, match)
780 submatch = matchmod.subdirmatcher(subpath, match)
781 total += s.archive(archiver, prefix + self._path + '/', submatch)
781 total += s.archive(archiver, prefix + self._path + '/', submatch)
782 return total
782 return total
783
783
784 @annotatesubrepoerror
784 @annotatesubrepoerror
785 def dirty(self, ignoreupdate=False):
785 def dirty(self, ignoreupdate=False):
786 r = self._state[1]
786 r = self._state[1]
787 if r == '' and not ignoreupdate: # no state recorded
787 if r == '' and not ignoreupdate: # no state recorded
788 return True
788 return True
789 w = self._repo[None]
789 w = self._repo[None]
790 if r != w.p1().hex() and not ignoreupdate:
790 if r != w.p1().hex() and not ignoreupdate:
791 # different version checked out
791 # different version checked out
792 return True
792 return True
793 return w.dirty() # working directory changed
793 return w.dirty() # working directory changed
794
794
795 def basestate(self):
795 def basestate(self):
796 return self._repo['.'].hex()
796 return self._repo['.'].hex()
797
797
798 def checknested(self, path):
798 def checknested(self, path):
799 return self._repo._checknested(self._repo.wjoin(path))
799 return self._repo._checknested(self._repo.wjoin(path))
800
800
801 @annotatesubrepoerror
801 @annotatesubrepoerror
802 def commit(self, text, user, date):
802 def commit(self, text, user, date):
803 # don't bother committing in the subrepo if it's only been
803 # don't bother committing in the subrepo if it's only been
804 # updated
804 # updated
805 if not self.dirty(True):
805 if not self.dirty(True):
806 return self._repo['.'].hex()
806 return self._repo['.'].hex()
807 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
807 self.ui.debug("committing subrepo %s\n" % subrelpath(self))
808 n = self._repo.commit(text, user, date)
808 n = self._repo.commit(text, user, date)
809 if not n:
809 if not n:
810 return self._repo['.'].hex() # different version checked out
810 return self._repo['.'].hex() # different version checked out
811 return node.hex(n)
811 return node.hex(n)
812
812
813 @annotatesubrepoerror
813 @annotatesubrepoerror
814 def phase(self, state):
814 def phase(self, state):
815 return self._repo[state].phase()
815 return self._repo[state].phase()
816
816
817 @annotatesubrepoerror
817 @annotatesubrepoerror
818 def remove(self):
818 def remove(self):
819 # we can't fully delete the repository as it may contain
819 # we can't fully delete the repository as it may contain
820 # local-only history
820 # local-only history
821 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
821 self.ui.note(_('removing subrepo %s\n') % subrelpath(self))
822 hg.clean(self._repo, node.nullid, False)
822 hg.clean(self._repo, node.nullid, False)
823
823
824 def _get(self, state):
824 def _get(self, state):
825 source, revision, kind = state
825 source, revision, kind = state
826 if revision in self._repo.unfiltered():
826 if revision in self._repo.unfiltered():
827 return True
827 return True
828 self._repo._subsource = source
828 self._repo._subsource = source
829 srcurl = _abssource(self._repo)
829 srcurl = _abssource(self._repo)
830 other = hg.peer(self._repo, {}, srcurl)
830 other = hg.peer(self._repo, {}, srcurl)
831 if len(self._repo) == 0:
831 if len(self._repo) == 0:
832 self.ui.status(_('cloning subrepo %s from %s\n')
832 self.ui.status(_('cloning subrepo %s from %s\n')
833 % (subrelpath(self), srcurl))
833 % (subrelpath(self), srcurl))
834 parentrepo = self._repo._subparent
834 parentrepo = self._repo._subparent
835 # use self._repo.vfs instead of self.wvfs to remove .hg only
835 # use self._repo.vfs instead of self.wvfs to remove .hg only
836 self._repo.vfs.rmtree()
836 self._repo.vfs.rmtree()
837 other, cloned = hg.clone(self._repo._subparent.baseui, {},
837 other, cloned = hg.clone(self._repo._subparent.baseui, {},
838 other, self._repo.root,
838 other, self._repo.root,
839 update=False)
839 update=False)
840 self._repo = cloned.local()
840 self._repo = cloned.local()
841 self._initrepo(parentrepo, source, create=True)
841 self._initrepo(parentrepo, source, create=True)
842 self._cachestorehash(srcurl)
842 self._cachestorehash(srcurl)
843 else:
843 else:
844 self.ui.status(_('pulling subrepo %s from %s\n')
844 self.ui.status(_('pulling subrepo %s from %s\n')
845 % (subrelpath(self), srcurl))
845 % (subrelpath(self), srcurl))
846 cleansub = self.storeclean(srcurl)
846 cleansub = self.storeclean(srcurl)
847 exchange.pull(self._repo, other)
847 exchange.pull(self._repo, other)
848 if cleansub:
848 if cleansub:
849 # keep the repo clean after pull
849 # keep the repo clean after pull
850 self._cachestorehash(srcurl)
850 self._cachestorehash(srcurl)
851 return False
851 return False
852
852
853 @annotatesubrepoerror
853 @annotatesubrepoerror
854 def get(self, state, overwrite=False):
854 def get(self, state, overwrite=False):
855 inrepo = self._get(state)
855 inrepo = self._get(state)
856 source, revision, kind = state
856 source, revision, kind = state
857 repo = self._repo
857 repo = self._repo
858 repo.ui.debug("getting subrepo %s\n" % self._path)
858 repo.ui.debug("getting subrepo %s\n" % self._path)
859 if inrepo:
859 if inrepo:
860 urepo = repo.unfiltered()
860 urepo = repo.unfiltered()
861 ctx = urepo[revision]
861 ctx = urepo[revision]
862 if ctx.hidden():
862 if ctx.hidden():
863 urepo.ui.warn(
863 urepo.ui.warn(
864 _('revision %s in subrepo %s is hidden\n') \
864 _('revision %s in subrepo %s is hidden\n') \
865 % (revision[0:12], self._path))
865 % (revision[0:12], self._path))
866 repo = urepo
866 repo = urepo
867 hg.updaterepo(repo, revision, overwrite)
867 hg.updaterepo(repo, revision, overwrite)
868
868
869 @annotatesubrepoerror
869 @annotatesubrepoerror
870 def merge(self, state):
870 def merge(self, state):
871 self._get(state)
871 self._get(state)
872 cur = self._repo['.']
872 cur = self._repo['.']
873 dst = self._repo[state[1]]
873 dst = self._repo[state[1]]
874 anc = dst.ancestor(cur)
874 anc = dst.ancestor(cur)
875
875
876 def mergefunc():
876 def mergefunc():
877 if anc == cur and dst.branch() == cur.branch():
877 if anc == cur and dst.branch() == cur.branch():
878 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
878 self.ui.debug("updating subrepo %s\n" % subrelpath(self))
879 hg.update(self._repo, state[1])
879 hg.update(self._repo, state[1])
880 elif anc == dst:
880 elif anc == dst:
881 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
881 self.ui.debug("skipping subrepo %s\n" % subrelpath(self))
882 else:
882 else:
883 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
883 self.ui.debug("merging subrepo %s\n" % subrelpath(self))
884 hg.merge(self._repo, state[1], remind=False)
884 hg.merge(self._repo, state[1], remind=False)
885
885
886 wctx = self._repo[None]
886 wctx = self._repo[None]
887 if self.dirty():
887 if self.dirty():
888 if anc != dst:
888 if anc != dst:
889 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
889 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
890 mergefunc()
890 mergefunc()
891 else:
891 else:
892 mergefunc()
892 mergefunc()
893 else:
893 else:
894 mergefunc()
894 mergefunc()
895
895
896 @annotatesubrepoerror
896 @annotatesubrepoerror
897 def push(self, opts):
897 def push(self, opts):
898 force = opts.get('force')
898 force = opts.get('force')
899 newbranch = opts.get('new_branch')
899 newbranch = opts.get('new_branch')
900 ssh = opts.get('ssh')
900 ssh = opts.get('ssh')
901
901
902 # push subrepos depth-first for coherent ordering
902 # push subrepos depth-first for coherent ordering
903 c = self._repo['']
903 c = self._repo['']
904 subs = c.substate # only repos that are committed
904 subs = c.substate # only repos that are committed
905 for s in sorted(subs):
905 for s in sorted(subs):
906 if c.sub(s).push(opts) == 0:
906 if c.sub(s).push(opts) == 0:
907 return False
907 return False
908
908
909 dsturl = _abssource(self._repo, True)
909 dsturl = _abssource(self._repo, True)
910 if not force:
910 if not force:
911 if self.storeclean(dsturl):
911 if self.storeclean(dsturl):
912 self.ui.status(
912 self.ui.status(
913 _('no changes made to subrepo %s since last push to %s\n')
913 _('no changes made to subrepo %s since last push to %s\n')
914 % (subrelpath(self), dsturl))
914 % (subrelpath(self), dsturl))
915 return None
915 return None
916 self.ui.status(_('pushing subrepo %s to %s\n') %
916 self.ui.status(_('pushing subrepo %s to %s\n') %
917 (subrelpath(self), dsturl))
917 (subrelpath(self), dsturl))
918 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
918 other = hg.peer(self._repo, {'ssh': ssh}, dsturl)
919 res = exchange.push(self._repo, other, force, newbranch=newbranch)
919 res = exchange.push(self._repo, other, force, newbranch=newbranch)
920
920
921 # the repo is now clean
921 # the repo is now clean
922 self._cachestorehash(dsturl)
922 self._cachestorehash(dsturl)
923 return res.cgresult
923 return res.cgresult
924
924
925 @annotatesubrepoerror
925 @annotatesubrepoerror
926 def outgoing(self, ui, dest, opts):
926 def outgoing(self, ui, dest, opts):
927 if 'rev' in opts or 'branch' in opts:
927 if 'rev' in opts or 'branch' in opts:
928 opts = copy.copy(opts)
928 opts = copy.copy(opts)
929 opts.pop('rev', None)
929 opts.pop('rev', None)
930 opts.pop('branch', None)
930 opts.pop('branch', None)
931 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
931 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
932
932
933 @annotatesubrepoerror
933 @annotatesubrepoerror
934 def incoming(self, ui, source, opts):
934 def incoming(self, ui, source, opts):
935 if 'rev' in opts or 'branch' in opts:
935 if 'rev' in opts or 'branch' in opts:
936 opts = copy.copy(opts)
936 opts = copy.copy(opts)
937 opts.pop('rev', None)
937 opts.pop('rev', None)
938 opts.pop('branch', None)
938 opts.pop('branch', None)
939 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
939 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
940
940
941 @annotatesubrepoerror
941 @annotatesubrepoerror
942 def files(self):
942 def files(self):
943 rev = self._state[1]
943 rev = self._state[1]
944 ctx = self._repo[rev]
944 ctx = self._repo[rev]
945 return ctx.manifest().keys()
945 return ctx.manifest().keys()
946
946
947 def filedata(self, name):
947 def filedata(self, name):
948 rev = self._state[1]
948 rev = self._state[1]
949 return self._repo[rev][name].data()
949 return self._repo[rev][name].data()
950
950
951 def fileflags(self, name):
951 def fileflags(self, name):
952 rev = self._state[1]
952 rev = self._state[1]
953 ctx = self._repo[rev]
953 ctx = self._repo[rev]
954 return ctx.flags(name)
954 return ctx.flags(name)
955
955
956 @annotatesubrepoerror
956 @annotatesubrepoerror
957 def printfiles(self, ui, m, fm, fmt, subrepos):
957 def printfiles(self, ui, m, fm, fmt, subrepos):
958 # If the parent context is a workingctx, use the workingctx here for
958 # If the parent context is a workingctx, use the workingctx here for
959 # consistency.
959 # consistency.
960 if self._ctx.rev() is None:
960 if self._ctx.rev() is None:
961 ctx = self._repo[None]
961 ctx = self._repo[None]
962 else:
962 else:
963 rev = self._state[1]
963 rev = self._state[1]
964 ctx = self._repo[rev]
964 ctx = self._repo[rev]
965 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
965 return cmdutil.files(ui, ctx, m, fm, fmt, subrepos)
966
966
967 @annotatesubrepoerror
967 @annotatesubrepoerror
968 def getfileset(self, expr):
968 def getfileset(self, expr):
969 if self._ctx.rev() is None:
969 if self._ctx.rev() is None:
970 ctx = self._repo[None]
970 ctx = self._repo[None]
971 else:
971 else:
972 rev = self._state[1]
972 rev = self._state[1]
973 ctx = self._repo[rev]
973 ctx = self._repo[rev]
974
974
975 files = ctx.getfileset(expr)
975 files = ctx.getfileset(expr)
976
976
977 for subpath in ctx.substate:
977 for subpath in ctx.substate:
978 sub = ctx.sub(subpath)
978 sub = ctx.sub(subpath)
979
979
980 try:
980 try:
981 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
981 files.extend(subpath + '/' + f for f in sub.getfileset(expr))
982 except error.LookupError:
982 except error.LookupError:
983 self.ui.status(_("skipping missing subrepository: %s\n")
983 self.ui.status(_("skipping missing subrepository: %s\n")
984 % self.wvfs.reljoin(reporelpath(self), subpath))
984 % self.wvfs.reljoin(reporelpath(self), subpath))
985 return files
985 return files
986
986
987 def walk(self, match):
987 def walk(self, match):
988 ctx = self._repo[None]
988 ctx = self._repo[None]
989 return ctx.walk(match)
989 return ctx.walk(match)
990
990
991 @annotatesubrepoerror
991 @annotatesubrepoerror
992 def forget(self, match, prefix):
992 def forget(self, match, prefix):
993 return cmdutil.forget(self.ui, self._repo, match,
993 return cmdutil.forget(self.ui, self._repo, match,
994 self.wvfs.reljoin(prefix, self._path), True)
994 self.wvfs.reljoin(prefix, self._path), True)
995
995
996 @annotatesubrepoerror
996 @annotatesubrepoerror
997 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
997 def removefiles(self, matcher, prefix, after, force, subrepos, warnings):
998 return cmdutil.remove(self.ui, self._repo, matcher,
998 return cmdutil.remove(self.ui, self._repo, matcher,
999 self.wvfs.reljoin(prefix, self._path),
999 self.wvfs.reljoin(prefix, self._path),
1000 after, force, subrepos)
1000 after, force, subrepos)
1001
1001
1002 @annotatesubrepoerror
1002 @annotatesubrepoerror
1003 def revert(self, substate, *pats, **opts):
1003 def revert(self, substate, *pats, **opts):
1004 # reverting a subrepo is a 2 step process:
1004 # reverting a subrepo is a 2 step process:
1005 # 1. if the no_backup is not set, revert all modified
1005 # 1. if the no_backup is not set, revert all modified
1006 # files inside the subrepo
1006 # files inside the subrepo
1007 # 2. update the subrepo to the revision specified in
1007 # 2. update the subrepo to the revision specified in
1008 # the corresponding substate dictionary
1008 # the corresponding substate dictionary
1009 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1009 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1010 if not opts.get('no_backup'):
1010 if not opts.get('no_backup'):
1011 # Revert all files on the subrepo, creating backups
1011 # Revert all files on the subrepo, creating backups
1012 # Note that this will not recursively revert subrepos
1012 # Note that this will not recursively revert subrepos
1013 # We could do it if there was a set:subrepos() predicate
1013 # We could do it if there was a set:subrepos() predicate
1014 opts = opts.copy()
1014 opts = opts.copy()
1015 opts['date'] = None
1015 opts['date'] = None
1016 opts['rev'] = substate[1]
1016 opts['rev'] = substate[1]
1017
1017
1018 self.filerevert(*pats, **opts)
1018 self.filerevert(*pats, **opts)
1019
1019
1020 # Update the repo to the revision specified in the given substate
1020 # Update the repo to the revision specified in the given substate
1021 if not opts.get('dry_run'):
1021 if not opts.get('dry_run'):
1022 self.get(substate, overwrite=True)
1022 self.get(substate, overwrite=True)
1023
1023
1024 def filerevert(self, *pats, **opts):
1024 def filerevert(self, *pats, **opts):
1025 ctx = self._repo[opts['rev']]
1025 ctx = self._repo[opts['rev']]
1026 parents = self._repo.dirstate.parents()
1026 parents = self._repo.dirstate.parents()
1027 if opts.get('all'):
1027 if opts.get('all'):
1028 pats = ['set:modified()']
1028 pats = ['set:modified()']
1029 else:
1029 else:
1030 pats = []
1030 pats = []
1031 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1031 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
1032
1032
1033 def shortid(self, revid):
1033 def shortid(self, revid):
1034 return revid[:12]
1034 return revid[:12]
1035
1035
1036 def verify(self):
1036 def verify(self):
1037 try:
1037 try:
1038 rev = self._state[1]
1038 rev = self._state[1]
1039 ctx = self._repo.unfiltered()[rev]
1039 ctx = self._repo.unfiltered()[rev]
1040 if ctx.hidden():
1040 if ctx.hidden():
1041 # Since hidden revisions aren't pushed/pulled, it seems worth an
1041 # Since hidden revisions aren't pushed/pulled, it seems worth an
1042 # explicit warning.
1042 # explicit warning.
1043 ui = self._repo.ui
1043 ui = self._repo.ui
1044 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1044 ui.warn(_("subrepo '%s' is hidden in revision %s\n") %
1045 (self._relpath, node.short(self._ctx.node())))
1045 (self._relpath, node.short(self._ctx.node())))
1046 return 0
1046 return 0
1047 except error.RepoLookupError:
1047 except error.RepoLookupError:
1048 # A missing subrepo revision may be a case of needing to pull it, so
1048 # A missing subrepo revision may be a case of needing to pull it, so
1049 # don't treat this as an error.
1049 # don't treat this as an error.
1050 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1050 self._repo.ui.warn(_("subrepo '%s' not found in revision %s\n") %
1051 (self._relpath, node.short(self._ctx.node())))
1051 (self._relpath, node.short(self._ctx.node())))
1052 return 0
1052 return 0
1053
1053
1054 @propertycache
1054 @propertycache
1055 def wvfs(self):
1055 def wvfs(self):
1056 """return own wvfs for efficiency and consistency
1056 """return own wvfs for efficiency and consistency
1057 """
1057 """
1058 return self._repo.wvfs
1058 return self._repo.wvfs
1059
1059
1060 @propertycache
1060 @propertycache
1061 def _relpath(self):
1061 def _relpath(self):
1062 """return path to this subrepository as seen from outermost repository
1062 """return path to this subrepository as seen from outermost repository
1063 """
1063 """
1064 # Keep consistent dir separators by avoiding vfs.join(self._path)
1064 # Keep consistent dir separators by avoiding vfs.join(self._path)
1065 return reporelpath(self._repo)
1065 return reporelpath(self._repo)
1066
1066
1067 class svnsubrepo(abstractsubrepo):
1067 class svnsubrepo(abstractsubrepo):
1068 def __init__(self, ctx, path, state, allowcreate):
1068 def __init__(self, ctx, path, state, allowcreate):
1069 super(svnsubrepo, self).__init__(ctx, path)
1069 super(svnsubrepo, self).__init__(ctx, path)
1070 self._state = state
1070 self._state = state
1071 self._exe = util.findexe('svn')
1071 self._exe = util.findexe('svn')
1072 if not self._exe:
1072 if not self._exe:
1073 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1073 raise error.Abort(_("'svn' executable not found for subrepo '%s'")
1074 % self._path)
1074 % self._path)
1075
1075
1076 def _svncommand(self, commands, filename='', failok=False):
1076 def _svncommand(self, commands, filename='', failok=False):
1077 cmd = [self._exe]
1077 cmd = [self._exe]
1078 extrakw = {}
1078 extrakw = {}
1079 if not self.ui.interactive():
1079 if not self.ui.interactive():
1080 # Making stdin be a pipe should prevent svn from behaving
1080 # Making stdin be a pipe should prevent svn from behaving
1081 # interactively even if we can't pass --non-interactive.
1081 # interactively even if we can't pass --non-interactive.
1082 extrakw['stdin'] = subprocess.PIPE
1082 extrakw['stdin'] = subprocess.PIPE
1083 # Starting in svn 1.5 --non-interactive is a global flag
1083 # Starting in svn 1.5 --non-interactive is a global flag
1084 # instead of being per-command, but we need to support 1.4 so
1084 # instead of being per-command, but we need to support 1.4 so
1085 # we have to be intelligent about what commands take
1085 # we have to be intelligent about what commands take
1086 # --non-interactive.
1086 # --non-interactive.
1087 if commands[0] in ('update', 'checkout', 'commit'):
1087 if commands[0] in ('update', 'checkout', 'commit'):
1088 cmd.append('--non-interactive')
1088 cmd.append('--non-interactive')
1089 cmd.extend(commands)
1089 cmd.extend(commands)
1090 if filename is not None:
1090 if filename is not None:
1091 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1091 path = self.wvfs.reljoin(self._ctx.repo().origroot,
1092 self._path, filename)
1092 self._path, filename)
1093 cmd.append(path)
1093 cmd.append(path)
1094 env = dict(os.environ)
1094 env = dict(os.environ)
1095 # Avoid localized output, preserve current locale for everything else.
1095 # Avoid localized output, preserve current locale for everything else.
1096 lc_all = env.get('LC_ALL')
1096 lc_all = env.get('LC_ALL')
1097 if lc_all:
1097 if lc_all:
1098 env['LANG'] = lc_all
1098 env['LANG'] = lc_all
1099 del env['LC_ALL']
1099 del env['LC_ALL']
1100 env['LC_MESSAGES'] = 'C'
1100 env['LC_MESSAGES'] = 'C'
1101 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1101 p = subprocess.Popen(cmd, bufsize=-1, close_fds=util.closefds,
1102 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1102 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
1103 universal_newlines=True, env=env, **extrakw)
1103 universal_newlines=True, env=env, **extrakw)
1104 stdout, stderr = p.communicate()
1104 stdout, stderr = p.communicate()
1105 stderr = stderr.strip()
1105 stderr = stderr.strip()
1106 if not failok:
1106 if not failok:
1107 if p.returncode:
1107 if p.returncode:
1108 raise error.Abort(stderr or 'exited with code %d'
1108 raise error.Abort(stderr or 'exited with code %d'
1109 % p.returncode)
1109 % p.returncode)
1110 if stderr:
1110 if stderr:
1111 self.ui.warn(stderr + '\n')
1111 self.ui.warn(stderr + '\n')
1112 return stdout, stderr
1112 return stdout, stderr
1113
1113
1114 @propertycache
1114 @propertycache
1115 def _svnversion(self):
1115 def _svnversion(self):
1116 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1116 output, err = self._svncommand(['--version', '--quiet'], filename=None)
1117 m = re.search(r'^(\d+)\.(\d+)', output)
1117 m = re.search(r'^(\d+)\.(\d+)', output)
1118 if not m:
1118 if not m:
1119 raise error.Abort(_('cannot retrieve svn tool version'))
1119 raise error.Abort(_('cannot retrieve svn tool version'))
1120 return (int(m.group(1)), int(m.group(2)))
1120 return (int(m.group(1)), int(m.group(2)))
1121
1121
1122 def _wcrevs(self):
1122 def _wcrevs(self):
1123 # Get the working directory revision as well as the last
1123 # Get the working directory revision as well as the last
1124 # commit revision so we can compare the subrepo state with
1124 # commit revision so we can compare the subrepo state with
1125 # both. We used to store the working directory one.
1125 # both. We used to store the working directory one.
1126 output, err = self._svncommand(['info', '--xml'])
1126 output, err = self._svncommand(['info', '--xml'])
1127 doc = xml.dom.minidom.parseString(output)
1127 doc = xml.dom.minidom.parseString(output)
1128 entries = doc.getElementsByTagName('entry')
1128 entries = doc.getElementsByTagName('entry')
1129 lastrev, rev = '0', '0'
1129 lastrev, rev = '0', '0'
1130 if entries:
1130 if entries:
1131 rev = str(entries[0].getAttribute('revision')) or '0'
1131 rev = str(entries[0].getAttribute('revision')) or '0'
1132 commits = entries[0].getElementsByTagName('commit')
1132 commits = entries[0].getElementsByTagName('commit')
1133 if commits:
1133 if commits:
1134 lastrev = str(commits[0].getAttribute('revision')) or '0'
1134 lastrev = str(commits[0].getAttribute('revision')) or '0'
1135 return (lastrev, rev)
1135 return (lastrev, rev)
1136
1136
1137 def _wcrev(self):
1137 def _wcrev(self):
1138 return self._wcrevs()[0]
1138 return self._wcrevs()[0]
1139
1139
1140 def _wcchanged(self):
1140 def _wcchanged(self):
1141 """Return (changes, extchanges, missing) where changes is True
1141 """Return (changes, extchanges, missing) where changes is True
1142 if the working directory was changed, extchanges is
1142 if the working directory was changed, extchanges is
1143 True if any of these changes concern an external entry and missing
1143 True if any of these changes concern an external entry and missing
1144 is True if any change is a missing entry.
1144 is True if any change is a missing entry.
1145 """
1145 """
1146 output, err = self._svncommand(['status', '--xml'])
1146 output, err = self._svncommand(['status', '--xml'])
1147 externals, changes, missing = [], [], []
1147 externals, changes, missing = [], [], []
1148 doc = xml.dom.minidom.parseString(output)
1148 doc = xml.dom.minidom.parseString(output)
1149 for e in doc.getElementsByTagName('entry'):
1149 for e in doc.getElementsByTagName('entry'):
1150 s = e.getElementsByTagName('wc-status')
1150 s = e.getElementsByTagName('wc-status')
1151 if not s:
1151 if not s:
1152 continue
1152 continue
1153 item = s[0].getAttribute('item')
1153 item = s[0].getAttribute('item')
1154 props = s[0].getAttribute('props')
1154 props = s[0].getAttribute('props')
1155 path = e.getAttribute('path')
1155 path = e.getAttribute('path')
1156 if item == 'external':
1156 if item == 'external':
1157 externals.append(path)
1157 externals.append(path)
1158 elif item == 'missing':
1158 elif item == 'missing':
1159 missing.append(path)
1159 missing.append(path)
1160 if (item not in ('', 'normal', 'unversioned', 'external')
1160 if (item not in ('', 'normal', 'unversioned', 'external')
1161 or props not in ('', 'none', 'normal')):
1161 or props not in ('', 'none', 'normal')):
1162 changes.append(path)
1162 changes.append(path)
1163 for path in changes:
1163 for path in changes:
1164 for ext in externals:
1164 for ext in externals:
1165 if path == ext or path.startswith(ext + os.sep):
1165 if path == ext or path.startswith(ext + os.sep):
1166 return True, True, bool(missing)
1166 return True, True, bool(missing)
1167 return bool(changes), False, bool(missing)
1167 return bool(changes), False, bool(missing)
1168
1168
1169 def dirty(self, ignoreupdate=False):
1169 def dirty(self, ignoreupdate=False):
1170 if not self._wcchanged()[0]:
1170 if not self._wcchanged()[0]:
1171 if self._state[1] in self._wcrevs() or ignoreupdate:
1171 if self._state[1] in self._wcrevs() or ignoreupdate:
1172 return False
1172 return False
1173 return True
1173 return True
1174
1174
1175 def basestate(self):
1175 def basestate(self):
1176 lastrev, rev = self._wcrevs()
1176 lastrev, rev = self._wcrevs()
1177 if lastrev != rev:
1177 if lastrev != rev:
1178 # Last committed rev is not the same than rev. We would
1178 # Last committed rev is not the same than rev. We would
1179 # like to take lastrev but we do not know if the subrepo
1179 # like to take lastrev but we do not know if the subrepo
1180 # URL exists at lastrev. Test it and fallback to rev it
1180 # URL exists at lastrev. Test it and fallback to rev it
1181 # is not there.
1181 # is not there.
1182 try:
1182 try:
1183 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1183 self._svncommand(['list', '%s@%s' % (self._state[0], lastrev)])
1184 return lastrev
1184 return lastrev
1185 except error.Abort:
1185 except error.Abort:
1186 pass
1186 pass
1187 return rev
1187 return rev
1188
1188
1189 @annotatesubrepoerror
1189 @annotatesubrepoerror
1190 def commit(self, text, user, date):
1190 def commit(self, text, user, date):
1191 # user and date are out of our hands since svn is centralized
1191 # user and date are out of our hands since svn is centralized
1192 changed, extchanged, missing = self._wcchanged()
1192 changed, extchanged, missing = self._wcchanged()
1193 if not changed:
1193 if not changed:
1194 return self.basestate()
1194 return self.basestate()
1195 if extchanged:
1195 if extchanged:
1196 # Do not try to commit externals
1196 # Do not try to commit externals
1197 raise error.Abort(_('cannot commit svn externals'))
1197 raise error.Abort(_('cannot commit svn externals'))
1198 if missing:
1198 if missing:
1199 # svn can commit with missing entries but aborting like hg
1199 # svn can commit with missing entries but aborting like hg
1200 # seems a better approach.
1200 # seems a better approach.
1201 raise error.Abort(_('cannot commit missing svn entries'))
1201 raise error.Abort(_('cannot commit missing svn entries'))
1202 commitinfo, err = self._svncommand(['commit', '-m', text])
1202 commitinfo, err = self._svncommand(['commit', '-m', text])
1203 self.ui.status(commitinfo)
1203 self.ui.status(commitinfo)
1204 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1204 newrev = re.search('Committed revision ([0-9]+).', commitinfo)
1205 if not newrev:
1205 if not newrev:
1206 if not commitinfo.strip():
1206 if not commitinfo.strip():
1207 # Sometimes, our definition of "changed" differs from
1207 # Sometimes, our definition of "changed" differs from
1208 # svn one. For instance, svn ignores missing files
1208 # svn one. For instance, svn ignores missing files
1209 # when committing. If there are only missing files, no
1209 # when committing. If there are only missing files, no
1210 # commit is made, no output and no error code.
1210 # commit is made, no output and no error code.
1211 raise error.Abort(_('failed to commit svn changes'))
1211 raise error.Abort(_('failed to commit svn changes'))
1212 raise error.Abort(commitinfo.splitlines()[-1])
1212 raise error.Abort(commitinfo.splitlines()[-1])
1213 newrev = newrev.groups()[0]
1213 newrev = newrev.groups()[0]
1214 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1214 self.ui.status(self._svncommand(['update', '-r', newrev])[0])
1215 return newrev
1215 return newrev
1216
1216
1217 @annotatesubrepoerror
1217 @annotatesubrepoerror
1218 def remove(self):
1218 def remove(self):
1219 if self.dirty():
1219 if self.dirty():
1220 self.ui.warn(_('not removing repo %s because '
1220 self.ui.warn(_('not removing repo %s because '
1221 'it has changes.\n') % self._path)
1221 'it has changes.\n') % self._path)
1222 return
1222 return
1223 self.ui.note(_('removing subrepo %s\n') % self._path)
1223 self.ui.note(_('removing subrepo %s\n') % self._path)
1224
1224
1225 self.wvfs.rmtree(forcibly=True)
1225 self.wvfs.rmtree(forcibly=True)
1226 try:
1226 try:
1227 pwvfs = self._ctx.repo().wvfs
1227 pwvfs = self._ctx.repo().wvfs
1228 pwvfs.removedirs(pwvfs.dirname(self._path))
1228 pwvfs.removedirs(pwvfs.dirname(self._path))
1229 except OSError:
1229 except OSError:
1230 pass
1230 pass
1231
1231
1232 @annotatesubrepoerror
1232 @annotatesubrepoerror
1233 def get(self, state, overwrite=False):
1233 def get(self, state, overwrite=False):
1234 if overwrite:
1234 if overwrite:
1235 self._svncommand(['revert', '--recursive'])
1235 self._svncommand(['revert', '--recursive'])
1236 args = ['checkout']
1236 args = ['checkout']
1237 if self._svnversion >= (1, 5):
1237 if self._svnversion >= (1, 5):
1238 args.append('--force')
1238 args.append('--force')
1239 # The revision must be specified at the end of the URL to properly
1239 # The revision must be specified at the end of the URL to properly
1240 # update to a directory which has since been deleted and recreated.
1240 # update to a directory which has since been deleted and recreated.
1241 args.append('%s@%s' % (state[0], state[1]))
1241 args.append('%s@%s' % (state[0], state[1]))
1242 status, err = self._svncommand(args, failok=True)
1242 status, err = self._svncommand(args, failok=True)
1243 _sanitize(self.ui, self.wvfs, '.svn')
1243 _sanitize(self.ui, self.wvfs, '.svn')
1244 if not re.search('Checked out revision [0-9]+.', status):
1244 if not re.search('Checked out revision [0-9]+.', status):
1245 if ('is already a working copy for a different URL' in err
1245 if ('is already a working copy for a different URL' in err
1246 and (self._wcchanged()[:2] == (False, False))):
1246 and (self._wcchanged()[:2] == (False, False))):
1247 # obstructed but clean working copy, so just blow it away.
1247 # obstructed but clean working copy, so just blow it away.
1248 self.remove()
1248 self.remove()
1249 self.get(state, overwrite=False)
1249 self.get(state, overwrite=False)
1250 return
1250 return
1251 raise error.Abort((status or err).splitlines()[-1])
1251 raise error.Abort((status or err).splitlines()[-1])
1252 self.ui.status(status)
1252 self.ui.status(status)
1253
1253
1254 @annotatesubrepoerror
1254 @annotatesubrepoerror
1255 def merge(self, state):
1255 def merge(self, state):
1256 old = self._state[1]
1256 old = self._state[1]
1257 new = state[1]
1257 new = state[1]
1258 wcrev = self._wcrev()
1258 wcrev = self._wcrev()
1259 if new != wcrev:
1259 if new != wcrev:
1260 dirty = old == wcrev or self._wcchanged()[0]
1260 dirty = old == wcrev or self._wcchanged()[0]
1261 if _updateprompt(self.ui, self, dirty, wcrev, new):
1261 if _updateprompt(self.ui, self, dirty, wcrev, new):
1262 self.get(state, False)
1262 self.get(state, False)
1263
1263
1264 def push(self, opts):
1264 def push(self, opts):
1265 # push is a no-op for SVN
1265 # push is a no-op for SVN
1266 return True
1266 return True
1267
1267
1268 @annotatesubrepoerror
1268 @annotatesubrepoerror
1269 def files(self):
1269 def files(self):
1270 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1270 output = self._svncommand(['list', '--recursive', '--xml'])[0]
1271 doc = xml.dom.minidom.parseString(output)
1271 doc = xml.dom.minidom.parseString(output)
1272 paths = []
1272 paths = []
1273 for e in doc.getElementsByTagName('entry'):
1273 for e in doc.getElementsByTagName('entry'):
1274 kind = str(e.getAttribute('kind'))
1274 kind = str(e.getAttribute('kind'))
1275 if kind != 'file':
1275 if kind != 'file':
1276 continue
1276 continue
1277 name = ''.join(c.data for c
1277 name = ''.join(c.data for c
1278 in e.getElementsByTagName('name')[0].childNodes
1278 in e.getElementsByTagName('name')[0].childNodes
1279 if c.nodeType == c.TEXT_NODE)
1279 if c.nodeType == c.TEXT_NODE)
1280 paths.append(name.encode('utf-8'))
1280 paths.append(name.encode('utf-8'))
1281 return paths
1281 return paths
1282
1282
1283 def filedata(self, name):
1283 def filedata(self, name):
1284 return self._svncommand(['cat'], name)[0]
1284 return self._svncommand(['cat'], name)[0]
1285
1285
1286
1286
1287 class gitsubrepo(abstractsubrepo):
1287 class gitsubrepo(abstractsubrepo):
1288 def __init__(self, ctx, path, state, allowcreate):
1288 def __init__(self, ctx, path, state, allowcreate):
1289 super(gitsubrepo, self).__init__(ctx, path)
1289 super(gitsubrepo, self).__init__(ctx, path)
1290 self._state = state
1290 self._state = state
1291 self._abspath = ctx.repo().wjoin(path)
1291 self._abspath = ctx.repo().wjoin(path)
1292 self._subparent = ctx.repo()
1292 self._subparent = ctx.repo()
1293 self._ensuregit()
1293 self._ensuregit()
1294
1294
1295 def _ensuregit(self):
1295 def _ensuregit(self):
1296 try:
1296 try:
1297 self._gitexecutable = 'git'
1297 self._gitexecutable = 'git'
1298 out, err = self._gitnodir(['--version'])
1298 out, err = self._gitnodir(['--version'])
1299 except OSError as e:
1299 except OSError as e:
1300 genericerror = _("error executing git for subrepo '%s': %s")
1300 genericerror = _("error executing git for subrepo '%s': %s")
1301 notfoundhint = _("check git is installed and in your PATH")
1301 notfoundhint = _("check git is installed and in your PATH")
1302 if e.errno != errno.ENOENT:
1302 if e.errno != errno.ENOENT:
1303 raise error.Abort(genericerror % (self._path, e.strerror))
1303 raise error.Abort(genericerror % (self._path, e.strerror))
1304 elif os.name == 'nt':
1304 elif os.name == 'nt':
1305 try:
1305 try:
1306 self._gitexecutable = 'git.cmd'
1306 self._gitexecutable = 'git.cmd'
1307 out, err = self._gitnodir(['--version'])
1307 out, err = self._gitnodir(['--version'])
1308 except OSError as e2:
1308 except OSError as e2:
1309 if e2.errno == errno.ENOENT:
1309 if e2.errno == errno.ENOENT:
1310 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1310 raise error.Abort(_("couldn't find 'git' or 'git.cmd'"
1311 " for subrepo '%s'") % self._path,
1311 " for subrepo '%s'") % self._path,
1312 hint=notfoundhint)
1312 hint=notfoundhint)
1313 else:
1313 else:
1314 raise error.Abort(genericerror % (self._path,
1314 raise error.Abort(genericerror % (self._path,
1315 e2.strerror))
1315 e2.strerror))
1316 else:
1316 else:
1317 raise error.Abort(_("couldn't find git for subrepo '%s'")
1317 raise error.Abort(_("couldn't find git for subrepo '%s'")
1318 % self._path, hint=notfoundhint)
1318 % self._path, hint=notfoundhint)
1319 versionstatus = self._checkversion(out)
1319 versionstatus = self._checkversion(out)
1320 if versionstatus == 'unknown':
1320 if versionstatus == 'unknown':
1321 self.ui.warn(_('cannot retrieve git version\n'))
1321 self.ui.warn(_('cannot retrieve git version\n'))
1322 elif versionstatus == 'abort':
1322 elif versionstatus == 'abort':
1323 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1323 raise error.Abort(_('git subrepo requires at least 1.6.0 or later'))
1324 elif versionstatus == 'warning':
1324 elif versionstatus == 'warning':
1325 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1325 self.ui.warn(_('git subrepo requires at least 1.6.0 or later\n'))
1326
1326
1327 @staticmethod
1327 @staticmethod
1328 def _gitversion(out):
1328 def _gitversion(out):
1329 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1329 m = re.search(r'^git version (\d+)\.(\d+)\.(\d+)', out)
1330 if m:
1330 if m:
1331 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1331 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1332
1332
1333 m = re.search(r'^git version (\d+)\.(\d+)', out)
1333 m = re.search(r'^git version (\d+)\.(\d+)', out)
1334 if m:
1334 if m:
1335 return (int(m.group(1)), int(m.group(2)), 0)
1335 return (int(m.group(1)), int(m.group(2)), 0)
1336
1336
1337 return -1
1337 return -1
1338
1338
1339 @staticmethod
1339 @staticmethod
1340 def _checkversion(out):
1340 def _checkversion(out):
1341 '''ensure git version is new enough
1341 '''ensure git version is new enough
1342
1342
1343 >>> _checkversion = gitsubrepo._checkversion
1343 >>> _checkversion = gitsubrepo._checkversion
1344 >>> _checkversion('git version 1.6.0')
1344 >>> _checkversion('git version 1.6.0')
1345 'ok'
1345 'ok'
1346 >>> _checkversion('git version 1.8.5')
1346 >>> _checkversion('git version 1.8.5')
1347 'ok'
1347 'ok'
1348 >>> _checkversion('git version 1.4.0')
1348 >>> _checkversion('git version 1.4.0')
1349 'abort'
1349 'abort'
1350 >>> _checkversion('git version 1.5.0')
1350 >>> _checkversion('git version 1.5.0')
1351 'warning'
1351 'warning'
1352 >>> _checkversion('git version 1.9-rc0')
1352 >>> _checkversion('git version 1.9-rc0')
1353 'ok'
1353 'ok'
1354 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1354 >>> _checkversion('git version 1.9.0.265.g81cdec2')
1355 'ok'
1355 'ok'
1356 >>> _checkversion('git version 1.9.0.GIT')
1356 >>> _checkversion('git version 1.9.0.GIT')
1357 'ok'
1357 'ok'
1358 >>> _checkversion('git version 12345')
1358 >>> _checkversion('git version 12345')
1359 'unknown'
1359 'unknown'
1360 >>> _checkversion('no')
1360 >>> _checkversion('no')
1361 'unknown'
1361 'unknown'
1362 '''
1362 '''
1363 version = gitsubrepo._gitversion(out)
1363 version = gitsubrepo._gitversion(out)
1364 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1364 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1365 # despite the docstring comment. For now, error on 1.4.0, warn on
1365 # despite the docstring comment. For now, error on 1.4.0, warn on
1366 # 1.5.0 but attempt to continue.
1366 # 1.5.0 but attempt to continue.
1367 if version == -1:
1367 if version == -1:
1368 return 'unknown'
1368 return 'unknown'
1369 if version < (1, 5, 0):
1369 if version < (1, 5, 0):
1370 return 'abort'
1370 return 'abort'
1371 elif version < (1, 6, 0):
1371 elif version < (1, 6, 0):
1372 return 'warning'
1372 return 'warning'
1373 return 'ok'
1373 return 'ok'
1374
1374
1375 def _gitcommand(self, commands, env=None, stream=False):
1375 def _gitcommand(self, commands, env=None, stream=False):
1376 return self._gitdir(commands, env=env, stream=stream)[0]
1376 return self._gitdir(commands, env=env, stream=stream)[0]
1377
1377
1378 def _gitdir(self, commands, env=None, stream=False):
1378 def _gitdir(self, commands, env=None, stream=False):
1379 return self._gitnodir(commands, env=env, stream=stream,
1379 return self._gitnodir(commands, env=env, stream=stream,
1380 cwd=self._abspath)
1380 cwd=self._abspath)
1381
1381
1382 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1382 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1383 """Calls the git command
1383 """Calls the git command
1384
1384
1385 The methods tries to call the git command. versions prior to 1.6.0
1385 The methods tries to call the git command. versions prior to 1.6.0
1386 are not supported and very probably fail.
1386 are not supported and very probably fail.
1387 """
1387 """
1388 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1388 self.ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands)))
1389 if env is None:
1389 if env is None:
1390 env = os.environ.copy()
1390 env = os.environ.copy()
1391 # disable localization for Git output (issue5176)
1391 # disable localization for Git output (issue5176)
1392 env['LC_ALL'] = 'C'
1392 env['LC_ALL'] = 'C'
1393 # fix for Git CVE-2015-7545
1393 # fix for Git CVE-2015-7545
1394 if 'GIT_ALLOW_PROTOCOL' not in env:
1394 if 'GIT_ALLOW_PROTOCOL' not in env:
1395 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1395 env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh'
1396 # unless ui.quiet is set, print git's stderr,
1396 # unless ui.quiet is set, print git's stderr,
1397 # which is mostly progress and useful info
1397 # which is mostly progress and useful info
1398 errpipe = None
1398 errpipe = None
1399 if self.ui.quiet:
1399 if self.ui.quiet:
1400 errpipe = open(os.devnull, 'w')
1400 errpipe = open(os.devnull, 'w')
1401 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1401 p = subprocess.Popen([self._gitexecutable] + commands, bufsize=-1,
1402 cwd=cwd, env=env, close_fds=util.closefds,
1402 cwd=cwd, env=env, close_fds=util.closefds,
1403 stdout=subprocess.PIPE, stderr=errpipe)
1403 stdout=subprocess.PIPE, stderr=errpipe)
1404 if stream:
1404 if stream:
1405 return p.stdout, None
1405 return p.stdout, None
1406
1406
1407 retdata = p.stdout.read().strip()
1407 retdata = p.stdout.read().strip()
1408 # wait for the child to exit to avoid race condition.
1408 # wait for the child to exit to avoid race condition.
1409 p.wait()
1409 p.wait()
1410
1410
1411 if p.returncode != 0 and p.returncode != 1:
1411 if p.returncode != 0 and p.returncode != 1:
1412 # there are certain error codes that are ok
1412 # there are certain error codes that are ok
1413 command = commands[0]
1413 command = commands[0]
1414 if command in ('cat-file', 'symbolic-ref'):
1414 if command in ('cat-file', 'symbolic-ref'):
1415 return retdata, p.returncode
1415 return retdata, p.returncode
1416 # for all others, abort
1416 # for all others, abort
1417 raise error.Abort(_('git %s error %d in %s') %
1417 raise error.Abort(_('git %s error %d in %s') %
1418 (command, p.returncode, self._relpath))
1418 (command, p.returncode, self._relpath))
1419
1419
1420 return retdata, p.returncode
1420 return retdata, p.returncode
1421
1421
1422 def _gitmissing(self):
1422 def _gitmissing(self):
1423 return not self.wvfs.exists('.git')
1423 return not self.wvfs.exists('.git')
1424
1424
1425 def _gitstate(self):
1425 def _gitstate(self):
1426 return self._gitcommand(['rev-parse', 'HEAD'])
1426 return self._gitcommand(['rev-parse', 'HEAD'])
1427
1427
1428 def _gitcurrentbranch(self):
1428 def _gitcurrentbranch(self):
1429 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1429 current, err = self._gitdir(['symbolic-ref', 'HEAD', '--quiet'])
1430 if err:
1430 if err:
1431 current = None
1431 current = None
1432 return current
1432 return current
1433
1433
1434 def _gitremote(self, remote):
1434 def _gitremote(self, remote):
1435 out = self._gitcommand(['remote', 'show', '-n', remote])
1435 out = self._gitcommand(['remote', 'show', '-n', remote])
1436 line = out.split('\n')[1]
1436 line = out.split('\n')[1]
1437 i = line.index('URL: ') + len('URL: ')
1437 i = line.index('URL: ') + len('URL: ')
1438 return line[i:]
1438 return line[i:]
1439
1439
1440 def _githavelocally(self, revision):
1440 def _githavelocally(self, revision):
1441 out, code = self._gitdir(['cat-file', '-e', revision])
1441 out, code = self._gitdir(['cat-file', '-e', revision])
1442 return code == 0
1442 return code == 0
1443
1443
1444 def _gitisancestor(self, r1, r2):
1444 def _gitisancestor(self, r1, r2):
1445 base = self._gitcommand(['merge-base', r1, r2])
1445 base = self._gitcommand(['merge-base', r1, r2])
1446 return base == r1
1446 return base == r1
1447
1447
1448 def _gitisbare(self):
1448 def _gitisbare(self):
1449 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1449 return self._gitcommand(['config', '--bool', 'core.bare']) == 'true'
1450
1450
1451 def _gitupdatestat(self):
1451 def _gitupdatestat(self):
1452 """This must be run before git diff-index.
1452 """This must be run before git diff-index.
1453 diff-index only looks at changes to file stat;
1453 diff-index only looks at changes to file stat;
1454 this command looks at file contents and updates the stat."""
1454 this command looks at file contents and updates the stat."""
1455 self._gitcommand(['update-index', '-q', '--refresh'])
1455 self._gitcommand(['update-index', '-q', '--refresh'])
1456
1456
1457 def _gitbranchmap(self):
1457 def _gitbranchmap(self):
1458 '''returns 2 things:
1458 '''returns 2 things:
1459 a map from git branch to revision
1459 a map from git branch to revision
1460 a map from revision to branches'''
1460 a map from revision to branches'''
1461 branch2rev = {}
1461 branch2rev = {}
1462 rev2branch = {}
1462 rev2branch = {}
1463
1463
1464 out = self._gitcommand(['for-each-ref', '--format',
1464 out = self._gitcommand(['for-each-ref', '--format',
1465 '%(objectname) %(refname)'])
1465 '%(objectname) %(refname)'])
1466 for line in out.split('\n'):
1466 for line in out.split('\n'):
1467 revision, ref = line.split(' ')
1467 revision, ref = line.split(' ')
1468 if (not ref.startswith('refs/heads/') and
1468 if (not ref.startswith('refs/heads/') and
1469 not ref.startswith('refs/remotes/')):
1469 not ref.startswith('refs/remotes/')):
1470 continue
1470 continue
1471 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1471 if ref.startswith('refs/remotes/') and ref.endswith('/HEAD'):
1472 continue # ignore remote/HEAD redirects
1472 continue # ignore remote/HEAD redirects
1473 branch2rev[ref] = revision
1473 branch2rev[ref] = revision
1474 rev2branch.setdefault(revision, []).append(ref)
1474 rev2branch.setdefault(revision, []).append(ref)
1475 return branch2rev, rev2branch
1475 return branch2rev, rev2branch
1476
1476
1477 def _gittracking(self, branches):
1477 def _gittracking(self, branches):
1478 'return map of remote branch to local tracking branch'
1478 'return map of remote branch to local tracking branch'
1479 # assumes no more than one local tracking branch for each remote
1479 # assumes no more than one local tracking branch for each remote
1480 tracking = {}
1480 tracking = {}
1481 for b in branches:
1481 for b in branches:
1482 if b.startswith('refs/remotes/'):
1482 if b.startswith('refs/remotes/'):
1483 continue
1483 continue
1484 bname = b.split('/', 2)[2]
1484 bname = b.split('/', 2)[2]
1485 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1485 remote = self._gitcommand(['config', 'branch.%s.remote' % bname])
1486 if remote:
1486 if remote:
1487 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1487 ref = self._gitcommand(['config', 'branch.%s.merge' % bname])
1488 tracking['refs/remotes/%s/%s' %
1488 tracking['refs/remotes/%s/%s' %
1489 (remote, ref.split('/', 2)[2])] = b
1489 (remote, ref.split('/', 2)[2])] = b
1490 return tracking
1490 return tracking
1491
1491
1492 def _abssource(self, source):
1492 def _abssource(self, source):
1493 if '://' not in source:
1493 if '://' not in source:
1494 # recognize the scp syntax as an absolute source
1494 # recognize the scp syntax as an absolute source
1495 colon = source.find(':')
1495 colon = source.find(':')
1496 if colon != -1 and '/' not in source[:colon]:
1496 if colon != -1 and '/' not in source[:colon]:
1497 return source
1497 return source
1498 self._subsource = source
1498 self._subsource = source
1499 return _abssource(self)
1499 return _abssource(self)
1500
1500
1501 def _fetch(self, source, revision):
1501 def _fetch(self, source, revision):
1502 if self._gitmissing():
1502 if self._gitmissing():
1503 source = self._abssource(source)
1503 source = self._abssource(source)
1504 self.ui.status(_('cloning subrepo %s from %s\n') %
1504 self.ui.status(_('cloning subrepo %s from %s\n') %
1505 (self._relpath, source))
1505 (self._relpath, source))
1506 self._gitnodir(['clone', source, self._abspath])
1506 self._gitnodir(['clone', source, self._abspath])
1507 if self._githavelocally(revision):
1507 if self._githavelocally(revision):
1508 return
1508 return
1509 self.ui.status(_('pulling subrepo %s from %s\n') %
1509 self.ui.status(_('pulling subrepo %s from %s\n') %
1510 (self._relpath, self._gitremote('origin')))
1510 (self._relpath, self._gitremote('origin')))
1511 # try only origin: the originally cloned repo
1511 # try only origin: the originally cloned repo
1512 self._gitcommand(['fetch'])
1512 self._gitcommand(['fetch'])
1513 if not self._githavelocally(revision):
1513 if not self._githavelocally(revision):
1514 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1514 raise error.Abort(_("revision %s does not exist in subrepo %s\n") %
1515 (revision, self._relpath))
1515 (revision, self._relpath))
1516
1516
1517 @annotatesubrepoerror
1517 @annotatesubrepoerror
1518 def dirty(self, ignoreupdate=False):
1518 def dirty(self, ignoreupdate=False):
1519 if self._gitmissing():
1519 if self._gitmissing():
1520 return self._state[1] != ''
1520 return self._state[1] != ''
1521 if self._gitisbare():
1521 if self._gitisbare():
1522 return True
1522 return True
1523 if not ignoreupdate and self._state[1] != self._gitstate():
1523 if not ignoreupdate and self._state[1] != self._gitstate():
1524 # different version checked out
1524 # different version checked out
1525 return True
1525 return True
1526 # check for staged changes or modified files; ignore untracked files
1526 # check for staged changes or modified files; ignore untracked files
1527 self._gitupdatestat()
1527 self._gitupdatestat()
1528 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1528 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1529 return code == 1
1529 return code == 1
1530
1530
1531 def basestate(self):
1531 def basestate(self):
1532 return self._gitstate()
1532 return self._gitstate()
1533
1533
1534 @annotatesubrepoerror
1534 @annotatesubrepoerror
1535 def get(self, state, overwrite=False):
1535 def get(self, state, overwrite=False):
1536 source, revision, kind = state
1536 source, revision, kind = state
1537 if not revision:
1537 if not revision:
1538 self.remove()
1538 self.remove()
1539 return
1539 return
1540 self._fetch(source, revision)
1540 self._fetch(source, revision)
1541 # if the repo was set to be bare, unbare it
1541 # if the repo was set to be bare, unbare it
1542 if self._gitisbare():
1542 if self._gitisbare():
1543 self._gitcommand(['config', 'core.bare', 'false'])
1543 self._gitcommand(['config', 'core.bare', 'false'])
1544 if self._gitstate() == revision:
1544 if self._gitstate() == revision:
1545 self._gitcommand(['reset', '--hard', 'HEAD'])
1545 self._gitcommand(['reset', '--hard', 'HEAD'])
1546 return
1546 return
1547 elif self._gitstate() == revision:
1547 elif self._gitstate() == revision:
1548 if overwrite:
1548 if overwrite:
1549 # first reset the index to unmark new files for commit, because
1549 # first reset the index to unmark new files for commit, because
1550 # reset --hard will otherwise throw away files added for commit,
1550 # reset --hard will otherwise throw away files added for commit,
1551 # not just unmark them.
1551 # not just unmark them.
1552 self._gitcommand(['reset', 'HEAD'])
1552 self._gitcommand(['reset', 'HEAD'])
1553 self._gitcommand(['reset', '--hard', 'HEAD'])
1553 self._gitcommand(['reset', '--hard', 'HEAD'])
1554 return
1554 return
1555 branch2rev, rev2branch = self._gitbranchmap()
1555 branch2rev, rev2branch = self._gitbranchmap()
1556
1556
1557 def checkout(args):
1557 def checkout(args):
1558 cmd = ['checkout']
1558 cmd = ['checkout']
1559 if overwrite:
1559 if overwrite:
1560 # first reset the index to unmark new files for commit, because
1560 # first reset the index to unmark new files for commit, because
1561 # the -f option will otherwise throw away files added for
1561 # the -f option will otherwise throw away files added for
1562 # commit, not just unmark them.
1562 # commit, not just unmark them.
1563 self._gitcommand(['reset', 'HEAD'])
1563 self._gitcommand(['reset', 'HEAD'])
1564 cmd.append('-f')
1564 cmd.append('-f')
1565 self._gitcommand(cmd + args)
1565 self._gitcommand(cmd + args)
1566 _sanitize(self.ui, self.wvfs, '.git')
1566 _sanitize(self.ui, self.wvfs, '.git')
1567
1567
1568 def rawcheckout():
1568 def rawcheckout():
1569 # no branch to checkout, check it out with no branch
1569 # no branch to checkout, check it out with no branch
1570 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1570 self.ui.warn(_('checking out detached HEAD in subrepo %s\n') %
1571 self._relpath)
1571 self._relpath)
1572 self.ui.warn(_('check out a git branch if you intend '
1572 self.ui.warn(_('check out a git branch if you intend '
1573 'to make changes\n'))
1573 'to make changes\n'))
1574 checkout(['-q', revision])
1574 checkout(['-q', revision])
1575
1575
1576 if revision not in rev2branch:
1576 if revision not in rev2branch:
1577 rawcheckout()
1577 rawcheckout()
1578 return
1578 return
1579 branches = rev2branch[revision]
1579 branches = rev2branch[revision]
1580 firstlocalbranch = None
1580 firstlocalbranch = None
1581 for b in branches:
1581 for b in branches:
1582 if b == 'refs/heads/master':
1582 if b == 'refs/heads/master':
1583 # master trumps all other branches
1583 # master trumps all other branches
1584 checkout(['refs/heads/master'])
1584 checkout(['refs/heads/master'])
1585 return
1585 return
1586 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1586 if not firstlocalbranch and not b.startswith('refs/remotes/'):
1587 firstlocalbranch = b
1587 firstlocalbranch = b
1588 if firstlocalbranch:
1588 if firstlocalbranch:
1589 checkout([firstlocalbranch])
1589 checkout([firstlocalbranch])
1590 return
1590 return
1591
1591
1592 tracking = self._gittracking(branch2rev.keys())
1592 tracking = self._gittracking(branch2rev.keys())
1593 # choose a remote branch already tracked if possible
1593 # choose a remote branch already tracked if possible
1594 remote = branches[0]
1594 remote = branches[0]
1595 if remote not in tracking:
1595 if remote not in tracking:
1596 for b in branches:
1596 for b in branches:
1597 if b in tracking:
1597 if b in tracking:
1598 remote = b
1598 remote = b
1599 break
1599 break
1600
1600
1601 if remote not in tracking:
1601 if remote not in tracking:
1602 # create a new local tracking branch
1602 # create a new local tracking branch
1603 local = remote.split('/', 3)[3]
1603 local = remote.split('/', 3)[3]
1604 checkout(['-b', local, remote])
1604 checkout(['-b', local, remote])
1605 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1605 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1606 # When updating to a tracked remote branch,
1606 # When updating to a tracked remote branch,
1607 # if the local tracking branch is downstream of it,
1607 # if the local tracking branch is downstream of it,
1608 # a normal `git pull` would have performed a "fast-forward merge"
1608 # a normal `git pull` would have performed a "fast-forward merge"
1609 # which is equivalent to updating the local branch to the remote.
1609 # which is equivalent to updating the local branch to the remote.
1610 # Since we are only looking at branching at update, we need to
1610 # Since we are only looking at branching at update, we need to
1611 # detect this situation and perform this action lazily.
1611 # detect this situation and perform this action lazily.
1612 if tracking[remote] != self._gitcurrentbranch():
1612 if tracking[remote] != self._gitcurrentbranch():
1613 checkout([tracking[remote]])
1613 checkout([tracking[remote]])
1614 self._gitcommand(['merge', '--ff', remote])
1614 self._gitcommand(['merge', '--ff', remote])
1615 _sanitize(self.ui, self.wvfs, '.git')
1615 _sanitize(self.ui, self.wvfs, '.git')
1616 else:
1616 else:
1617 # a real merge would be required, just checkout the revision
1617 # a real merge would be required, just checkout the revision
1618 rawcheckout()
1618 rawcheckout()
1619
1619
1620 @annotatesubrepoerror
1620 @annotatesubrepoerror
1621 def commit(self, text, user, date):
1621 def commit(self, text, user, date):
1622 if self._gitmissing():
1622 if self._gitmissing():
1623 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1623 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1624 cmd = ['commit', '-a', '-m', text]
1624 cmd = ['commit', '-a', '-m', text]
1625 env = os.environ.copy()
1625 env = os.environ.copy()
1626 if user:
1626 if user:
1627 cmd += ['--author', user]
1627 cmd += ['--author', user]
1628 if date:
1628 if date:
1629 # git's date parser silently ignores when seconds < 1e9
1629 # git's date parser silently ignores when seconds < 1e9
1630 # convert to ISO8601
1630 # convert to ISO8601
1631 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1631 env['GIT_AUTHOR_DATE'] = util.datestr(date,
1632 '%Y-%m-%dT%H:%M:%S %1%2')
1632 '%Y-%m-%dT%H:%M:%S %1%2')
1633 self._gitcommand(cmd, env=env)
1633 self._gitcommand(cmd, env=env)
1634 # make sure commit works otherwise HEAD might not exist under certain
1634 # make sure commit works otherwise HEAD might not exist under certain
1635 # circumstances
1635 # circumstances
1636 return self._gitstate()
1636 return self._gitstate()
1637
1637
1638 @annotatesubrepoerror
1638 @annotatesubrepoerror
1639 def merge(self, state):
1639 def merge(self, state):
1640 source, revision, kind = state
1640 source, revision, kind = state
1641 self._fetch(source, revision)
1641 self._fetch(source, revision)
1642 base = self._gitcommand(['merge-base', revision, self._state[1]])
1642 base = self._gitcommand(['merge-base', revision, self._state[1]])
1643 self._gitupdatestat()
1643 self._gitupdatestat()
1644 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1644 out, code = self._gitdir(['diff-index', '--quiet', 'HEAD'])
1645
1645
1646 def mergefunc():
1646 def mergefunc():
1647 if base == revision:
1647 if base == revision:
1648 self.get(state) # fast forward merge
1648 self.get(state) # fast forward merge
1649 elif base != self._state[1]:
1649 elif base != self._state[1]:
1650 self._gitcommand(['merge', '--no-commit', revision])
1650 self._gitcommand(['merge', '--no-commit', revision])
1651 _sanitize(self.ui, self.wvfs, '.git')
1651 _sanitize(self.ui, self.wvfs, '.git')
1652
1652
1653 if self.dirty():
1653 if self.dirty():
1654 if self._gitstate() != revision:
1654 if self._gitstate() != revision:
1655 dirty = self._gitstate() == self._state[1] or code != 0
1655 dirty = self._gitstate() == self._state[1] or code != 0
1656 if _updateprompt(self.ui, self, dirty,
1656 if _updateprompt(self.ui, self, dirty,
1657 self._state[1][:7], revision[:7]):
1657 self._state[1][:7], revision[:7]):
1658 mergefunc()
1658 mergefunc()
1659 else:
1659 else:
1660 mergefunc()
1660 mergefunc()
1661
1661
1662 @annotatesubrepoerror
1662 @annotatesubrepoerror
1663 def push(self, opts):
1663 def push(self, opts):
1664 force = opts.get('force')
1664 force = opts.get('force')
1665
1665
1666 if not self._state[1]:
1666 if not self._state[1]:
1667 return True
1667 return True
1668 if self._gitmissing():
1668 if self._gitmissing():
1669 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1669 raise error.Abort(_("subrepo %s is missing") % self._relpath)
1670 # if a branch in origin contains the revision, nothing to do
1670 # if a branch in origin contains the revision, nothing to do
1671 branch2rev, rev2branch = self._gitbranchmap()
1671 branch2rev, rev2branch = self._gitbranchmap()
1672 if self._state[1] in rev2branch:
1672 if self._state[1] in rev2branch:
1673 for b in rev2branch[self._state[1]]:
1673 for b in rev2branch[self._state[1]]:
1674 if b.startswith('refs/remotes/origin/'):
1674 if b.startswith('refs/remotes/origin/'):
1675 return True
1675 return True
1676 for b, revision in branch2rev.iteritems():
1676 for b, revision in branch2rev.iteritems():
1677 if b.startswith('refs/remotes/origin/'):
1677 if b.startswith('refs/remotes/origin/'):
1678 if self._gitisancestor(self._state[1], revision):
1678 if self._gitisancestor(self._state[1], revision):
1679 return True
1679 return True
1680 # otherwise, try to push the currently checked out branch
1680 # otherwise, try to push the currently checked out branch
1681 cmd = ['push']
1681 cmd = ['push']
1682 if force:
1682 if force:
1683 cmd.append('--force')
1683 cmd.append('--force')
1684
1684
1685 current = self._gitcurrentbranch()
1685 current = self._gitcurrentbranch()
1686 if current:
1686 if current:
1687 # determine if the current branch is even useful
1687 # determine if the current branch is even useful
1688 if not self._gitisancestor(self._state[1], current):
1688 if not self._gitisancestor(self._state[1], current):
1689 self.ui.warn(_('unrelated git branch checked out '
1689 self.ui.warn(_('unrelated git branch checked out '
1690 'in subrepo %s\n') % self._relpath)
1690 'in subrepo %s\n') % self._relpath)
1691 return False
1691 return False
1692 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1692 self.ui.status(_('pushing branch %s of subrepo %s\n') %
1693 (current.split('/', 2)[2], self._relpath))
1693 (current.split('/', 2)[2], self._relpath))
1694 ret = self._gitdir(cmd + ['origin', current])
1694 ret = self._gitdir(cmd + ['origin', current])
1695 return ret[1] == 0
1695 return ret[1] == 0
1696 else:
1696 else:
1697 self.ui.warn(_('no branch checked out in subrepo %s\n'
1697 self.ui.warn(_('no branch checked out in subrepo %s\n'
1698 'cannot push revision %s\n') %
1698 'cannot push revision %s\n') %
1699 (self._relpath, self._state[1]))
1699 (self._relpath, self._state[1]))
1700 return False
1700 return False
1701
1701
1702 @annotatesubrepoerror
1702 @annotatesubrepoerror
1703 def add(self, ui, match, prefix, explicitonly, **opts):
1703 def add(self, ui, match, prefix, explicitonly, **opts):
1704 if self._gitmissing():
1704 if self._gitmissing():
1705 return []
1705 return []
1706
1706
1707 (modified, added, removed,
1707 (modified, added, removed,
1708 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1708 deleted, unknown, ignored, clean) = self.status(None, unknown=True,
1709 clean=True)
1709 clean=True)
1710
1710
1711 tracked = set()
1711 tracked = set()
1712 # dirstates 'amn' warn, 'r' is added again
1712 # dirstates 'amn' warn, 'r' is added again
1713 for l in (modified, added, deleted, clean):
1713 for l in (modified, added, deleted, clean):
1714 tracked.update(l)
1714 tracked.update(l)
1715
1715
1716 # Unknown files not of interest will be rejected by the matcher
1716 # Unknown files not of interest will be rejected by the matcher
1717 files = unknown
1717 files = unknown
1718 files.extend(match.files())
1718 files.extend(match.files())
1719
1719
1720 rejected = []
1720 rejected = []
1721
1721
1722 files = [f for f in sorted(set(files)) if match(f)]
1722 files = [f for f in sorted(set(files)) if match(f)]
1723 for f in files:
1723 for f in files:
1724 exact = match.exact(f)
1724 exact = match.exact(f)
1725 command = ["add"]
1725 command = ["add"]
1726 if exact:
1726 if exact:
1727 command.append("-f") #should be added, even if ignored
1727 command.append("-f") #should be added, even if ignored
1728 if ui.verbose or not exact:
1728 if ui.verbose or not exact:
1729 ui.status(_('adding %s\n') % match.rel(f))
1729 ui.status(_('adding %s\n') % match.rel(f))
1730
1730
1731 if f in tracked: # hg prints 'adding' even if already tracked
1731 if f in tracked: # hg prints 'adding' even if already tracked
1732 if exact:
1732 if exact:
1733 rejected.append(f)
1733 rejected.append(f)
1734 continue
1734 continue
1735 if not opts.get('dry_run'):
1735 if not opts.get('dry_run'):
1736 self._gitcommand(command + [f])
1736 self._gitcommand(command + [f])
1737
1737
1738 for f in rejected:
1738 for f in rejected:
1739 ui.warn(_("%s already tracked!\n") % match.abs(f))
1739 ui.warn(_("%s already tracked!\n") % match.abs(f))
1740
1740
1741 return rejected
1741 return rejected
1742
1742
1743 @annotatesubrepoerror
1743 @annotatesubrepoerror
1744 def remove(self):
1744 def remove(self):
1745 if self._gitmissing():
1745 if self._gitmissing():
1746 return
1746 return
1747 if self.dirty():
1747 if self.dirty():
1748 self.ui.warn(_('not removing repo %s because '
1748 self.ui.warn(_('not removing repo %s because '
1749 'it has changes.\n') % self._relpath)
1749 'it has changes.\n') % self._relpath)
1750 return
1750 return
1751 # we can't fully delete the repository as it may contain
1751 # we can't fully delete the repository as it may contain
1752 # local-only history
1752 # local-only history
1753 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1753 self.ui.note(_('removing subrepo %s\n') % self._relpath)
1754 self._gitcommand(['config', 'core.bare', 'true'])
1754 self._gitcommand(['config', 'core.bare', 'true'])
1755 for f, kind in self.wvfs.readdir():
1755 for f, kind in self.wvfs.readdir():
1756 if f == '.git':
1756 if f == '.git':
1757 continue
1757 continue
1758 if kind == stat.S_IFDIR:
1758 if kind == stat.S_IFDIR:
1759 self.wvfs.rmtree(f)
1759 self.wvfs.rmtree(f)
1760 else:
1760 else:
1761 self.wvfs.unlink(f)
1761 self.wvfs.unlink(f)
1762
1762
1763 def archive(self, archiver, prefix, match=None):
1763 def archive(self, archiver, prefix, match=None):
1764 total = 0
1764 total = 0
1765 source, revision = self._state
1765 source, revision = self._state
1766 if not revision:
1766 if not revision:
1767 return total
1767 return total
1768 self._fetch(source, revision)
1768 self._fetch(source, revision)
1769
1769
1770 # Parse git's native archive command.
1770 # Parse git's native archive command.
1771 # This should be much faster than manually traversing the trees
1771 # This should be much faster than manually traversing the trees
1772 # and objects with many subprocess calls.
1772 # and objects with many subprocess calls.
1773 tarstream = self._gitcommand(['archive', revision], stream=True)
1773 tarstream = self._gitcommand(['archive', revision], stream=True)
1774 tar = tarfile.open(fileobj=tarstream, mode='r|')
1774 tar = tarfile.open(fileobj=tarstream, mode='r|')
1775 relpath = subrelpath(self)
1775 relpath = subrelpath(self)
1776 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1776 self.ui.progress(_('archiving (%s)') % relpath, 0, unit=_('files'))
1777 for i, info in enumerate(tar):
1777 for i, info in enumerate(tar):
1778 if info.isdir():
1778 if info.isdir():
1779 continue
1779 continue
1780 if match and not match(info.name):
1780 if match and not match(info.name):
1781 continue
1781 continue
1782 if info.issym():
1782 if info.issym():
1783 data = info.linkname
1783 data = info.linkname
1784 else:
1784 else:
1785 data = tar.extractfile(info).read()
1785 data = tar.extractfile(info).read()
1786 archiver.addfile(prefix + self._path + '/' + info.name,
1786 archiver.addfile(prefix + self._path + '/' + info.name,
1787 info.mode, info.issym(), data)
1787 info.mode, info.issym(), data)
1788 total += 1
1788 total += 1
1789 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1789 self.ui.progress(_('archiving (%s)') % relpath, i + 1,
1790 unit=_('files'))
1790 unit=_('files'))
1791 self.ui.progress(_('archiving (%s)') % relpath, None)
1791 self.ui.progress(_('archiving (%s)') % relpath, None)
1792 return total
1792 return total
1793
1793
1794
1794
1795 @annotatesubrepoerror
1795 @annotatesubrepoerror
1796 def cat(self, match, prefix, **opts):
1796 def cat(self, match, prefix, **opts):
1797 rev = self._state[1]
1797 rev = self._state[1]
1798 if match.anypats():
1798 if match.anypats():
1799 return 1 #No support for include/exclude yet
1799 return 1 #No support for include/exclude yet
1800
1800
1801 if not match.files():
1801 if not match.files():
1802 return 1
1802 return 1
1803
1803
1804 for f in match.files():
1804 for f in match.files():
1805 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1805 output = self._gitcommand(["show", "%s:%s" % (rev, f)])
1806 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1806 fp = cmdutil.makefileobj(self._subparent, opts.get('output'),
1807 self._ctx.node(),
1807 self._ctx.node(),
1808 pathname=self.wvfs.reljoin(prefix, f))
1808 pathname=self.wvfs.reljoin(prefix, f))
1809 fp.write(output)
1809 fp.write(output)
1810 fp.close()
1810 fp.close()
1811 return 0
1811 return 0
1812
1812
1813
1813
1814 @annotatesubrepoerror
1814 @annotatesubrepoerror
1815 def status(self, rev2, **opts):
1815 def status(self, rev2, **opts):
1816 rev1 = self._state[1]
1816 rev1 = self._state[1]
1817 if self._gitmissing() or not rev1:
1817 if self._gitmissing() or not rev1:
1818 # if the repo is missing, return no results
1818 # if the repo is missing, return no results
1819 return scmutil.status([], [], [], [], [], [], [])
1819 return scmutil.status([], [], [], [], [], [], [])
1820 modified, added, removed = [], [], []
1820 modified, added, removed = [], [], []
1821 self._gitupdatestat()
1821 self._gitupdatestat()
1822 if rev2:
1822 if rev2:
1823 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1823 command = ['diff-tree', '--no-renames', '-r', rev1, rev2]
1824 else:
1824 else:
1825 command = ['diff-index', '--no-renames', rev1]
1825 command = ['diff-index', '--no-renames', rev1]
1826 out = self._gitcommand(command)
1826 out = self._gitcommand(command)
1827 for line in out.split('\n'):
1827 for line in out.split('\n'):
1828 tab = line.find('\t')
1828 tab = line.find('\t')
1829 if tab == -1:
1829 if tab == -1:
1830 continue
1830 continue
1831 status, f = line[tab - 1], line[tab + 1:]
1831 status, f = line[tab - 1], line[tab + 1:]
1832 if status == 'M':
1832 if status == 'M':
1833 modified.append(f)
1833 modified.append(f)
1834 elif status == 'A':
1834 elif status == 'A':
1835 added.append(f)
1835 added.append(f)
1836 elif status == 'D':
1836 elif status == 'D':
1837 removed.append(f)
1837 removed.append(f)
1838
1838
1839 deleted, unknown, ignored, clean = [], [], [], []
1839 deleted, unknown, ignored, clean = [], [], [], []
1840
1840
1841 command = ['status', '--porcelain', '-z']
1841 command = ['status', '--porcelain', '-z']
1842 if opts.get('unknown'):
1842 if opts.get('unknown'):
1843 command += ['--untracked-files=all']
1843 command += ['--untracked-files=all']
1844 if opts.get('ignored'):
1844 if opts.get('ignored'):
1845 command += ['--ignored']
1845 command += ['--ignored']
1846 out = self._gitcommand(command)
1846 out = self._gitcommand(command)
1847
1847
1848 changedfiles = set()
1848 changedfiles = set()
1849 changedfiles.update(modified)
1849 changedfiles.update(modified)
1850 changedfiles.update(added)
1850 changedfiles.update(added)
1851 changedfiles.update(removed)
1851 changedfiles.update(removed)
1852 for line in out.split('\0'):
1852 for line in out.split('\0'):
1853 if not line:
1853 if not line:
1854 continue
1854 continue
1855 st = line[0:2]
1855 st = line[0:2]
1856 #moves and copies show 2 files on one line
1856 #moves and copies show 2 files on one line
1857 if line.find('\0') >= 0:
1857 if line.find('\0') >= 0:
1858 filename1, filename2 = line[3:].split('\0')
1858 filename1, filename2 = line[3:].split('\0')
1859 else:
1859 else:
1860 filename1 = line[3:]
1860 filename1 = line[3:]
1861 filename2 = None
1861 filename2 = None
1862
1862
1863 changedfiles.add(filename1)
1863 changedfiles.add(filename1)
1864 if filename2:
1864 if filename2:
1865 changedfiles.add(filename2)
1865 changedfiles.add(filename2)
1866
1866
1867 if st == '??':
1867 if st == '??':
1868 unknown.append(filename1)
1868 unknown.append(filename1)
1869 elif st == '!!':
1869 elif st == '!!':
1870 ignored.append(filename1)
1870 ignored.append(filename1)
1871
1871
1872 if opts.get('clean'):
1872 if opts.get('clean'):
1873 out = self._gitcommand(['ls-files'])
1873 out = self._gitcommand(['ls-files'])
1874 for f in out.split('\n'):
1874 for f in out.split('\n'):
1875 if not f in changedfiles:
1875 if not f in changedfiles:
1876 clean.append(f)
1876 clean.append(f)
1877
1877
1878 return scmutil.status(modified, added, removed, deleted,
1878 return scmutil.status(modified, added, removed, deleted,
1879 unknown, ignored, clean)
1879 unknown, ignored, clean)
1880
1880
1881 @annotatesubrepoerror
1881 @annotatesubrepoerror
1882 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1882 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1883 node1 = self._state[1]
1883 node1 = self._state[1]
1884 cmd = ['diff', '--no-renames']
1884 cmd = ['diff', '--no-renames']
1885 if opts['stat']:
1885 if opts['stat']:
1886 cmd.append('--stat')
1886 cmd.append('--stat')
1887 else:
1887 else:
1888 # for Git, this also implies '-p'
1888 # for Git, this also implies '-p'
1889 cmd.append('-U%d' % diffopts.context)
1889 cmd.append('-U%d' % diffopts.context)
1890
1890
1891 gitprefix = self.wvfs.reljoin(prefix, self._path)
1891 gitprefix = self.wvfs.reljoin(prefix, self._path)
1892
1892
1893 if diffopts.noprefix:
1893 if diffopts.noprefix:
1894 cmd.extend(['--src-prefix=%s/' % gitprefix,
1894 cmd.extend(['--src-prefix=%s/' % gitprefix,
1895 '--dst-prefix=%s/' % gitprefix])
1895 '--dst-prefix=%s/' % gitprefix])
1896 else:
1896 else:
1897 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1897 cmd.extend(['--src-prefix=a/%s/' % gitprefix,
1898 '--dst-prefix=b/%s/' % gitprefix])
1898 '--dst-prefix=b/%s/' % gitprefix])
1899
1899
1900 if diffopts.ignorews:
1900 if diffopts.ignorews:
1901 cmd.append('--ignore-all-space')
1901 cmd.append('--ignore-all-space')
1902 if diffopts.ignorewsamount:
1902 if diffopts.ignorewsamount:
1903 cmd.append('--ignore-space-change')
1903 cmd.append('--ignore-space-change')
1904 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1904 if self._gitversion(self._gitcommand(['--version'])) >= (1, 8, 4) \
1905 and diffopts.ignoreblanklines:
1905 and diffopts.ignoreblanklines:
1906 cmd.append('--ignore-blank-lines')
1906 cmd.append('--ignore-blank-lines')
1907
1907
1908 cmd.append(node1)
1908 cmd.append(node1)
1909 if node2:
1909 if node2:
1910 cmd.append(node2)
1910 cmd.append(node2)
1911
1911
1912 output = ""
1912 output = ""
1913 if match.always():
1913 if match.always():
1914 output += self._gitcommand(cmd) + '\n'
1914 output += self._gitcommand(cmd) + '\n'
1915 else:
1915 else:
1916 st = self.status(node2)[:3]
1916 st = self.status(node2)[:3]
1917 files = [f for sublist in st for f in sublist]
1917 files = [f for sublist in st for f in sublist]
1918 for f in files:
1918 for f in files:
1919 if match(f):
1919 if match(f):
1920 output += self._gitcommand(cmd + ['--', f]) + '\n'
1920 output += self._gitcommand(cmd + ['--', f]) + '\n'
1921
1921
1922 if output.strip():
1922 if output.strip():
1923 ui.write(output)
1923 ui.write(output)
1924
1924
1925 @annotatesubrepoerror
1925 @annotatesubrepoerror
1926 def revert(self, substate, *pats, **opts):
1926 def revert(self, substate, *pats, **opts):
1927 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1927 self.ui.status(_('reverting subrepo %s\n') % substate[0])
1928 if not opts.get('no_backup'):
1928 if not opts.get('no_backup'):
1929 status = self.status(None)
1929 status = self.status(None)
1930 names = status.modified
1930 names = status.modified
1931 for name in names:
1931 for name in names:
1932 bakname = scmutil.origpath(self.ui, self._subparent, name)
1932 bakname = scmutil.origpath(self.ui, self._subparent, name)
1933 self.ui.note(_('saving current version of %s as %s\n') %
1933 self.ui.note(_('saving current version of %s as %s\n') %
1934 (name, bakname))
1934 (name, bakname))
1935 self.wvfs.rename(name, bakname)
1935 self.wvfs.rename(name, bakname)
1936
1936
1937 if not opts.get('dry_run'):
1937 if not opts.get('dry_run'):
1938 self.get(substate, overwrite=True)
1938 self.get(substate, overwrite=True)
1939 return []
1939 return []
1940
1940
1941 def shortid(self, revid):
1941 def shortid(self, revid):
1942 return revid[:7]
1942 return revid[:7]
1943
1943
1944 types = {
1944 types = {
1945 'hg': hgsubrepo,
1945 'hg': hgsubrepo,
1946 'svn': svnsubrepo,
1946 'svn': svnsubrepo,
1947 'git': gitsubrepo,
1947 'git': gitsubrepo,
1948 }
1948 }
General Comments 0
You need to be logged in to leave comments. Login now