##// END OF EJS Templates
mercurial: switch to util.timer for all interval timings...
Simon Farnsworth -
r30975:22fbca1d default
parent child Browse files
Show More
@@ -1,100 +1,98 b''
1 #!/usr/bin/env python
1 #!/usr/bin/env python
2 #
2 #
3 # hgperf - measure performance of Mercurial commands
3 # hgperf - measure performance of Mercurial commands
4 #
4 #
5 # Copyright 2014 Matt Mackall <mpm@selenic.com>
5 # Copyright 2014 Matt Mackall <mpm@selenic.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 '''measure performance of Mercurial commands
10 '''measure performance of Mercurial commands
11
11
12 Using ``hgperf`` instead of ``hg`` measures performance of the target
12 Using ``hgperf`` instead of ``hg`` measures performance of the target
13 Mercurial command. For example, the execution below measures
13 Mercurial command. For example, the execution below measures
14 performance of :hg:`heads --topo`::
14 performance of :hg:`heads --topo`::
15
15
16 $ hgperf heads --topo
16 $ hgperf heads --topo
17
17
18 All command output via ``ui`` is suppressed, and just measurement
18 All command output via ``ui`` is suppressed, and just measurement
19 result is displayed: see also "perf" extension in "contrib".
19 result is displayed: see also "perf" extension in "contrib".
20
20
21 Costs of processing before dispatching to the command function like
21 Costs of processing before dispatching to the command function like
22 below are not measured::
22 below are not measured::
23
23
24 - parsing command line (e.g. option validity check)
24 - parsing command line (e.g. option validity check)
25 - reading configuration files in
25 - reading configuration files in
26
26
27 But ``pre-`` and ``post-`` hook invocation for the target command is
27 But ``pre-`` and ``post-`` hook invocation for the target command is
28 measured, even though these are invoked before or after dispatching to
28 measured, even though these are invoked before or after dispatching to
29 the command function, because these may be required to repeat
29 the command function, because these may be required to repeat
30 execution of the target command correctly.
30 execution of the target command correctly.
31 '''
31 '''
32
32
33 import os
33 import os
34 import sys
34 import sys
35
35
36 libdir = '@LIBDIR@'
36 libdir = '@LIBDIR@'
37
37
38 if libdir != '@' 'LIBDIR' '@':
38 if libdir != '@' 'LIBDIR' '@':
39 if not os.path.isabs(libdir):
39 if not os.path.isabs(libdir):
40 libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
40 libdir = os.path.join(os.path.dirname(os.path.realpath(__file__)),
41 libdir)
41 libdir)
42 libdir = os.path.abspath(libdir)
42 libdir = os.path.abspath(libdir)
43 sys.path.insert(0, libdir)
43 sys.path.insert(0, libdir)
44
44
45 # enable importing on demand to reduce startup time
45 # enable importing on demand to reduce startup time
46 try:
46 try:
47 from mercurial import demandimport; demandimport.enable()
47 from mercurial import demandimport; demandimport.enable()
48 except ImportError:
48 except ImportError:
49 import sys
49 import sys
50 sys.stderr.write("abort: couldn't find mercurial libraries in [%s]\n" %
50 sys.stderr.write("abort: couldn't find mercurial libraries in [%s]\n" %
51 ' '.join(sys.path))
51 ' '.join(sys.path))
52 sys.stderr.write("(check your install and PYTHONPATH)\n")
52 sys.stderr.write("(check your install and PYTHONPATH)\n")
53 sys.exit(-1)
53 sys.exit(-1)
54
54
55 import mercurial.util
55 import mercurial.util
56 import mercurial.dispatch
56 import mercurial.dispatch
57
57
58 import time
59
60 def timer(func, title=None):
58 def timer(func, title=None):
61 results = []
59 results = []
62 begin = time.time()
60 begin = mercurial.util.timer()
63 count = 0
61 count = 0
64 while True:
62 while True:
65 ostart = os.times()
63 ostart = os.times()
66 cstart = time.time()
64 cstart = mercurial.util.timer()
67 r = func()
65 r = func()
68 cstop = time.time()
66 cstop = mercurial.util.timer()
69 ostop = os.times()
67 ostop = os.times()
70 count += 1
68 count += 1
71 a, b = ostart, ostop
69 a, b = ostart, ostop
72 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
70 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
73 if cstop - begin > 3 and count >= 100:
71 if cstop - begin > 3 and count >= 100:
74 break
72 break
75 if cstop - begin > 10 and count >= 3:
73 if cstop - begin > 10 and count >= 3:
76 break
74 break
77 if title:
75 if title:
78 sys.stderr.write("! %s\n" % title)
76 sys.stderr.write("! %s\n" % title)
79 if r:
77 if r:
80 sys.stderr.write("! result: %s\n" % r)
78 sys.stderr.write("! result: %s\n" % r)
81 m = min(results)
79 m = min(results)
82 sys.stderr.write("! wall %f comb %f user %f sys %f (best of %d)\n"
80 sys.stderr.write("! wall %f comb %f user %f sys %f (best of %d)\n"
83 % (m[0], m[1] + m[2], m[1], m[2], count))
81 % (m[0], m[1] + m[2], m[1], m[2], count))
84
82
85 orgruncommand = mercurial.dispatch.runcommand
83 orgruncommand = mercurial.dispatch.runcommand
86
84
87 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
85 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
88 ui.pushbuffer()
86 ui.pushbuffer()
89 lui.pushbuffer()
87 lui.pushbuffer()
90 timer(lambda : orgruncommand(lui, repo, cmd, fullargs, ui,
88 timer(lambda : orgruncommand(lui, repo, cmd, fullargs, ui,
91 options, d, cmdpats, cmdoptions))
89 options, d, cmdpats, cmdoptions))
92 ui.popbuffer()
90 ui.popbuffer()
93 lui.popbuffer()
91 lui.popbuffer()
94
92
95 mercurial.dispatch.runcommand = runcommand
93 mercurial.dispatch.runcommand = runcommand
96
94
97 for fp in (sys.stdin, sys.stdout, sys.stderr):
95 for fp in (sys.stdin, sys.stdout, sys.stderr):
98 mercurial.util.setbinary(fp)
96 mercurial.util.setbinary(fp)
99
97
100 mercurial.dispatch.run()
98 mercurial.dispatch.run()
@@ -1,1285 +1,1285 b''
1 # perf.py - performance test routines
1 # perf.py - performance test routines
2 '''helper extension to measure performance'''
2 '''helper extension to measure performance'''
3
3
4 # "historical portability" policy of perf.py:
4 # "historical portability" policy of perf.py:
5 #
5 #
6 # We have to do:
6 # We have to do:
7 # - make perf.py "loadable" with as wide Mercurial version as possible
7 # - make perf.py "loadable" with as wide Mercurial version as possible
8 # This doesn't mean that perf commands work correctly with that Mercurial.
8 # This doesn't mean that perf commands work correctly with that Mercurial.
9 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
9 # BTW, perf.py itself has been available since 1.1 (or eb240755386d).
10 # - make historical perf command work correctly with as wide Mercurial
10 # - make historical perf command work correctly with as wide Mercurial
11 # version as possible
11 # version as possible
12 #
12 #
13 # We have to do, if possible with reasonable cost:
13 # We have to do, if possible with reasonable cost:
14 # - make recent perf command for historical feature work correctly
14 # - make recent perf command for historical feature work correctly
15 # with early Mercurial
15 # with early Mercurial
16 #
16 #
17 # We don't have to do:
17 # We don't have to do:
18 # - make perf command for recent feature work correctly with early
18 # - make perf command for recent feature work correctly with early
19 # Mercurial
19 # Mercurial
20
20
21 from __future__ import absolute_import
21 from __future__ import absolute_import
22 import functools
22 import functools
23 import os
23 import os
24 import random
24 import random
25 import sys
25 import sys
26 import time
26 import time
27 from mercurial import (
27 from mercurial import (
28 bdiff,
28 bdiff,
29 changegroup,
29 changegroup,
30 cmdutil,
30 cmdutil,
31 commands,
31 commands,
32 copies,
32 copies,
33 error,
33 error,
34 extensions,
34 extensions,
35 mdiff,
35 mdiff,
36 merge,
36 merge,
37 util,
37 util,
38 )
38 )
39
39
40 # for "historical portability":
40 # for "historical portability":
41 # try to import modules separately (in dict order), and ignore
41 # try to import modules separately (in dict order), and ignore
42 # failure, because these aren't available with early Mercurial
42 # failure, because these aren't available with early Mercurial
43 try:
43 try:
44 from mercurial import branchmap # since 2.5 (or bcee63733aad)
44 from mercurial import branchmap # since 2.5 (or bcee63733aad)
45 except ImportError:
45 except ImportError:
46 pass
46 pass
47 try:
47 try:
48 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
48 from mercurial import obsolete # since 2.3 (or ad0d6c2b3279)
49 except ImportError:
49 except ImportError:
50 pass
50 pass
51 try:
51 try:
52 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
52 from mercurial import repoview # since 2.5 (or 3a6ddacb7198)
53 except ImportError:
53 except ImportError:
54 pass
54 pass
55 try:
55 try:
56 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
56 from mercurial import scmutil # since 1.9 (or 8b252e826c68)
57 except ImportError:
57 except ImportError:
58 pass
58 pass
59
59
60 # for "historical portability":
60 # for "historical portability":
61 # define util.safehasattr forcibly, because util.safehasattr has been
61 # define util.safehasattr forcibly, because util.safehasattr has been
62 # available since 1.9.3 (or 94b200a11cf7)
62 # available since 1.9.3 (or 94b200a11cf7)
63 _undefined = object()
63 _undefined = object()
64 def safehasattr(thing, attr):
64 def safehasattr(thing, attr):
65 return getattr(thing, attr, _undefined) is not _undefined
65 return getattr(thing, attr, _undefined) is not _undefined
66 setattr(util, 'safehasattr', safehasattr)
66 setattr(util, 'safehasattr', safehasattr)
67
67
68 # for "historical portability":
68 # for "historical portability":
69 # use locally defined empty option list, if formatteropts isn't
69 # use locally defined empty option list, if formatteropts isn't
70 # available, because commands.formatteropts has been available since
70 # available, because commands.formatteropts has been available since
71 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
71 # 3.2 (or 7a7eed5176a4), even though formatting itself has been
72 # available since 2.2 (or ae5f92e154d3)
72 # available since 2.2 (or ae5f92e154d3)
73 formatteropts = getattr(commands, "formatteropts", [])
73 formatteropts = getattr(commands, "formatteropts", [])
74
74
75 # for "historical portability":
75 # for "historical portability":
76 # use locally defined option list, if debugrevlogopts isn't available,
76 # use locally defined option list, if debugrevlogopts isn't available,
77 # because commands.debugrevlogopts has been available since 3.7 (or
77 # because commands.debugrevlogopts has been available since 3.7 (or
78 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
78 # 5606f7d0d063), even though cmdutil.openrevlog() has been available
79 # since 1.9 (or a79fea6b3e77).
79 # since 1.9 (or a79fea6b3e77).
80 revlogopts = getattr(commands, "debugrevlogopts", [
80 revlogopts = getattr(commands, "debugrevlogopts", [
81 ('c', 'changelog', False, ('open changelog')),
81 ('c', 'changelog', False, ('open changelog')),
82 ('m', 'manifest', False, ('open manifest')),
82 ('m', 'manifest', False, ('open manifest')),
83 ('', 'dir', False, ('open directory manifest')),
83 ('', 'dir', False, ('open directory manifest')),
84 ])
84 ])
85
85
86 cmdtable = {}
86 cmdtable = {}
87
87
88 # for "historical portability":
88 # for "historical portability":
89 # define parsealiases locally, because cmdutil.parsealiases has been
89 # define parsealiases locally, because cmdutil.parsealiases has been
90 # available since 1.5 (or 6252852b4332)
90 # available since 1.5 (or 6252852b4332)
91 def parsealiases(cmd):
91 def parsealiases(cmd):
92 return cmd.lstrip("^").split("|")
92 return cmd.lstrip("^").split("|")
93
93
94 if safehasattr(cmdutil, 'command'):
94 if safehasattr(cmdutil, 'command'):
95 import inspect
95 import inspect
96 command = cmdutil.command(cmdtable)
96 command = cmdutil.command(cmdtable)
97 if 'norepo' not in inspect.getargspec(command)[0]:
97 if 'norepo' not in inspect.getargspec(command)[0]:
98 # for "historical portability":
98 # for "historical portability":
99 # wrap original cmdutil.command, because "norepo" option has
99 # wrap original cmdutil.command, because "norepo" option has
100 # been available since 3.1 (or 75a96326cecb)
100 # been available since 3.1 (or 75a96326cecb)
101 _command = command
101 _command = command
102 def command(name, options=(), synopsis=None, norepo=False):
102 def command(name, options=(), synopsis=None, norepo=False):
103 if norepo:
103 if norepo:
104 commands.norepo += ' %s' % ' '.join(parsealiases(name))
104 commands.norepo += ' %s' % ' '.join(parsealiases(name))
105 return _command(name, list(options), synopsis)
105 return _command(name, list(options), synopsis)
106 else:
106 else:
107 # for "historical portability":
107 # for "historical portability":
108 # define "@command" annotation locally, because cmdutil.command
108 # define "@command" annotation locally, because cmdutil.command
109 # has been available since 1.9 (or 2daa5179e73f)
109 # has been available since 1.9 (or 2daa5179e73f)
110 def command(name, options=(), synopsis=None, norepo=False):
110 def command(name, options=(), synopsis=None, norepo=False):
111 def decorator(func):
111 def decorator(func):
112 if synopsis:
112 if synopsis:
113 cmdtable[name] = func, list(options), synopsis
113 cmdtable[name] = func, list(options), synopsis
114 else:
114 else:
115 cmdtable[name] = func, list(options)
115 cmdtable[name] = func, list(options)
116 if norepo:
116 if norepo:
117 commands.norepo += ' %s' % ' '.join(parsealiases(name))
117 commands.norepo += ' %s' % ' '.join(parsealiases(name))
118 return func
118 return func
119 return decorator
119 return decorator
120
120
121 def getlen(ui):
121 def getlen(ui):
122 if ui.configbool("perf", "stub"):
122 if ui.configbool("perf", "stub"):
123 return lambda x: 1
123 return lambda x: 1
124 return len
124 return len
125
125
126 def gettimer(ui, opts=None):
126 def gettimer(ui, opts=None):
127 """return a timer function and formatter: (timer, formatter)
127 """return a timer function and formatter: (timer, formatter)
128
128
129 This function exists to gather the creation of formatter in a single
129 This function exists to gather the creation of formatter in a single
130 place instead of duplicating it in all performance commands."""
130 place instead of duplicating it in all performance commands."""
131
131
132 # enforce an idle period before execution to counteract power management
132 # enforce an idle period before execution to counteract power management
133 # experimental config: perf.presleep
133 # experimental config: perf.presleep
134 time.sleep(getint(ui, "perf", "presleep", 1))
134 time.sleep(getint(ui, "perf", "presleep", 1))
135
135
136 if opts is None:
136 if opts is None:
137 opts = {}
137 opts = {}
138 # redirect all to stderr unless buffer api is in use
138 # redirect all to stderr unless buffer api is in use
139 if not ui._buffers:
139 if not ui._buffers:
140 ui = ui.copy()
140 ui = ui.copy()
141 uifout = safeattrsetter(ui, 'fout', ignoremissing=True)
141 uifout = safeattrsetter(ui, 'fout', ignoremissing=True)
142 if uifout:
142 if uifout:
143 # for "historical portability":
143 # for "historical portability":
144 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
144 # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d)
145 uifout.set(ui.ferr)
145 uifout.set(ui.ferr)
146
146
147 # get a formatter
147 # get a formatter
148 uiformatter = getattr(ui, 'formatter', None)
148 uiformatter = getattr(ui, 'formatter', None)
149 if uiformatter:
149 if uiformatter:
150 fm = uiformatter('perf', opts)
150 fm = uiformatter('perf', opts)
151 else:
151 else:
152 # for "historical portability":
152 # for "historical portability":
153 # define formatter locally, because ui.formatter has been
153 # define formatter locally, because ui.formatter has been
154 # available since 2.2 (or ae5f92e154d3)
154 # available since 2.2 (or ae5f92e154d3)
155 from mercurial import node
155 from mercurial import node
156 class defaultformatter(object):
156 class defaultformatter(object):
157 """Minimized composition of baseformatter and plainformatter
157 """Minimized composition of baseformatter and plainformatter
158 """
158 """
159 def __init__(self, ui, topic, opts):
159 def __init__(self, ui, topic, opts):
160 self._ui = ui
160 self._ui = ui
161 if ui.debugflag:
161 if ui.debugflag:
162 self.hexfunc = node.hex
162 self.hexfunc = node.hex
163 else:
163 else:
164 self.hexfunc = node.short
164 self.hexfunc = node.short
165 def __nonzero__(self):
165 def __nonzero__(self):
166 return False
166 return False
167 def startitem(self):
167 def startitem(self):
168 pass
168 pass
169 def data(self, **data):
169 def data(self, **data):
170 pass
170 pass
171 def write(self, fields, deftext, *fielddata, **opts):
171 def write(self, fields, deftext, *fielddata, **opts):
172 self._ui.write(deftext % fielddata, **opts)
172 self._ui.write(deftext % fielddata, **opts)
173 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
173 def condwrite(self, cond, fields, deftext, *fielddata, **opts):
174 if cond:
174 if cond:
175 self._ui.write(deftext % fielddata, **opts)
175 self._ui.write(deftext % fielddata, **opts)
176 def plain(self, text, **opts):
176 def plain(self, text, **opts):
177 self._ui.write(text, **opts)
177 self._ui.write(text, **opts)
178 def end(self):
178 def end(self):
179 pass
179 pass
180 fm = defaultformatter(ui, 'perf', opts)
180 fm = defaultformatter(ui, 'perf', opts)
181
181
182 # stub function, runs code only once instead of in a loop
182 # stub function, runs code only once instead of in a loop
183 # experimental config: perf.stub
183 # experimental config: perf.stub
184 if ui.configbool("perf", "stub"):
184 if ui.configbool("perf", "stub"):
185 return functools.partial(stub_timer, fm), fm
185 return functools.partial(stub_timer, fm), fm
186 return functools.partial(_timer, fm), fm
186 return functools.partial(_timer, fm), fm
187
187
188 def stub_timer(fm, func, title=None):
188 def stub_timer(fm, func, title=None):
189 func()
189 func()
190
190
191 def _timer(fm, func, title=None):
191 def _timer(fm, func, title=None):
192 results = []
192 results = []
193 begin = time.time()
193 begin = util.timer()
194 count = 0
194 count = 0
195 while True:
195 while True:
196 ostart = os.times()
196 ostart = os.times()
197 cstart = time.time()
197 cstart = util.timer()
198 r = func()
198 r = func()
199 cstop = time.time()
199 cstop = util.timer()
200 ostop = os.times()
200 ostop = os.times()
201 count += 1
201 count += 1
202 a, b = ostart, ostop
202 a, b = ostart, ostop
203 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
203 results.append((cstop - cstart, b[0] - a[0], b[1]-a[1]))
204 if cstop - begin > 3 and count >= 100:
204 if cstop - begin > 3 and count >= 100:
205 break
205 break
206 if cstop - begin > 10 and count >= 3:
206 if cstop - begin > 10 and count >= 3:
207 break
207 break
208
208
209 fm.startitem()
209 fm.startitem()
210
210
211 if title:
211 if title:
212 fm.write('title', '! %s\n', title)
212 fm.write('title', '! %s\n', title)
213 if r:
213 if r:
214 fm.write('result', '! result: %s\n', r)
214 fm.write('result', '! result: %s\n', r)
215 m = min(results)
215 m = min(results)
216 fm.plain('!')
216 fm.plain('!')
217 fm.write('wall', ' wall %f', m[0])
217 fm.write('wall', ' wall %f', m[0])
218 fm.write('comb', ' comb %f', m[1] + m[2])
218 fm.write('comb', ' comb %f', m[1] + m[2])
219 fm.write('user', ' user %f', m[1])
219 fm.write('user', ' user %f', m[1])
220 fm.write('sys', ' sys %f', m[2])
220 fm.write('sys', ' sys %f', m[2])
221 fm.write('count', ' (best of %d)', count)
221 fm.write('count', ' (best of %d)', count)
222 fm.plain('\n')
222 fm.plain('\n')
223
223
224 # utilities for historical portability
224 # utilities for historical portability
225
225
226 def getint(ui, section, name, default):
226 def getint(ui, section, name, default):
227 # for "historical portability":
227 # for "historical portability":
228 # ui.configint has been available since 1.9 (or fa2b596db182)
228 # ui.configint has been available since 1.9 (or fa2b596db182)
229 v = ui.config(section, name, None)
229 v = ui.config(section, name, None)
230 if v is None:
230 if v is None:
231 return default
231 return default
232 try:
232 try:
233 return int(v)
233 return int(v)
234 except ValueError:
234 except ValueError:
235 raise error.ConfigError(("%s.%s is not an integer ('%s')")
235 raise error.ConfigError(("%s.%s is not an integer ('%s')")
236 % (section, name, v))
236 % (section, name, v))
237
237
238 def safeattrsetter(obj, name, ignoremissing=False):
238 def safeattrsetter(obj, name, ignoremissing=False):
239 """Ensure that 'obj' has 'name' attribute before subsequent setattr
239 """Ensure that 'obj' has 'name' attribute before subsequent setattr
240
240
241 This function is aborted, if 'obj' doesn't have 'name' attribute
241 This function is aborted, if 'obj' doesn't have 'name' attribute
242 at runtime. This avoids overlooking removal of an attribute, which
242 at runtime. This avoids overlooking removal of an attribute, which
243 breaks assumption of performance measurement, in the future.
243 breaks assumption of performance measurement, in the future.
244
244
245 This function returns the object to (1) assign a new value, and
245 This function returns the object to (1) assign a new value, and
246 (2) restore an original value to the attribute.
246 (2) restore an original value to the attribute.
247
247
248 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
248 If 'ignoremissing' is true, missing 'name' attribute doesn't cause
249 abortion, and this function returns None. This is useful to
249 abortion, and this function returns None. This is useful to
250 examine an attribute, which isn't ensured in all Mercurial
250 examine an attribute, which isn't ensured in all Mercurial
251 versions.
251 versions.
252 """
252 """
253 if not util.safehasattr(obj, name):
253 if not util.safehasattr(obj, name):
254 if ignoremissing:
254 if ignoremissing:
255 return None
255 return None
256 raise error.Abort(("missing attribute %s of %s might break assumption"
256 raise error.Abort(("missing attribute %s of %s might break assumption"
257 " of performance measurement") % (name, obj))
257 " of performance measurement") % (name, obj))
258
258
259 origvalue = getattr(obj, name)
259 origvalue = getattr(obj, name)
260 class attrutil(object):
260 class attrutil(object):
261 def set(self, newvalue):
261 def set(self, newvalue):
262 setattr(obj, name, newvalue)
262 setattr(obj, name, newvalue)
263 def restore(self):
263 def restore(self):
264 setattr(obj, name, origvalue)
264 setattr(obj, name, origvalue)
265
265
266 return attrutil()
266 return attrutil()
267
267
268 # utilities to examine each internal API changes
268 # utilities to examine each internal API changes
269
269
270 def getbranchmapsubsettable():
270 def getbranchmapsubsettable():
271 # for "historical portability":
271 # for "historical portability":
272 # subsettable is defined in:
272 # subsettable is defined in:
273 # - branchmap since 2.9 (or 175c6fd8cacc)
273 # - branchmap since 2.9 (or 175c6fd8cacc)
274 # - repoview since 2.5 (or 59a9f18d4587)
274 # - repoview since 2.5 (or 59a9f18d4587)
275 for mod in (branchmap, repoview):
275 for mod in (branchmap, repoview):
276 subsettable = getattr(mod, 'subsettable', None)
276 subsettable = getattr(mod, 'subsettable', None)
277 if subsettable:
277 if subsettable:
278 return subsettable
278 return subsettable
279
279
280 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
280 # bisecting in bcee63733aad::59a9f18d4587 can reach here (both
281 # branchmap and repoview modules exist, but subsettable attribute
281 # branchmap and repoview modules exist, but subsettable attribute
282 # doesn't)
282 # doesn't)
283 raise error.Abort(("perfbranchmap not available with this Mercurial"),
283 raise error.Abort(("perfbranchmap not available with this Mercurial"),
284 hint="use 2.5 or later")
284 hint="use 2.5 or later")
285
285
286 def getsvfs(repo):
286 def getsvfs(repo):
287 """Return appropriate object to access files under .hg/store
287 """Return appropriate object to access files under .hg/store
288 """
288 """
289 # for "historical portability":
289 # for "historical portability":
290 # repo.svfs has been available since 2.3 (or 7034365089bf)
290 # repo.svfs has been available since 2.3 (or 7034365089bf)
291 svfs = getattr(repo, 'svfs', None)
291 svfs = getattr(repo, 'svfs', None)
292 if svfs:
292 if svfs:
293 return svfs
293 return svfs
294 else:
294 else:
295 return getattr(repo, 'sopener')
295 return getattr(repo, 'sopener')
296
296
297 def getvfs(repo):
297 def getvfs(repo):
298 """Return appropriate object to access files under .hg
298 """Return appropriate object to access files under .hg
299 """
299 """
300 # for "historical portability":
300 # for "historical portability":
301 # repo.vfs has been available since 2.3 (or 7034365089bf)
301 # repo.vfs has been available since 2.3 (or 7034365089bf)
302 vfs = getattr(repo, 'vfs', None)
302 vfs = getattr(repo, 'vfs', None)
303 if vfs:
303 if vfs:
304 return vfs
304 return vfs
305 else:
305 else:
306 return getattr(repo, 'opener')
306 return getattr(repo, 'opener')
307
307
308 def repocleartagscachefunc(repo):
308 def repocleartagscachefunc(repo):
309 """Return the function to clear tags cache according to repo internal API
309 """Return the function to clear tags cache according to repo internal API
310 """
310 """
311 if util.safehasattr(repo, '_tagscache'): # since 2.0 (or 9dca7653b525)
311 if util.safehasattr(repo, '_tagscache'): # since 2.0 (or 9dca7653b525)
312 # in this case, setattr(repo, '_tagscache', None) or so isn't
312 # in this case, setattr(repo, '_tagscache', None) or so isn't
313 # correct way to clear tags cache, because existing code paths
313 # correct way to clear tags cache, because existing code paths
314 # expect _tagscache to be a structured object.
314 # expect _tagscache to be a structured object.
315 def clearcache():
315 def clearcache():
316 # _tagscache has been filteredpropertycache since 2.5 (or
316 # _tagscache has been filteredpropertycache since 2.5 (or
317 # 98c867ac1330), and delattr() can't work in such case
317 # 98c867ac1330), and delattr() can't work in such case
318 if '_tagscache' in vars(repo):
318 if '_tagscache' in vars(repo):
319 del repo.__dict__['_tagscache']
319 del repo.__dict__['_tagscache']
320 return clearcache
320 return clearcache
321
321
322 repotags = safeattrsetter(repo, '_tags', ignoremissing=True)
322 repotags = safeattrsetter(repo, '_tags', ignoremissing=True)
323 if repotags: # since 1.4 (or 5614a628d173)
323 if repotags: # since 1.4 (or 5614a628d173)
324 return lambda : repotags.set(None)
324 return lambda : repotags.set(None)
325
325
326 repotagscache = safeattrsetter(repo, 'tagscache', ignoremissing=True)
326 repotagscache = safeattrsetter(repo, 'tagscache', ignoremissing=True)
327 if repotagscache: # since 0.6 (or d7df759d0e97)
327 if repotagscache: # since 0.6 (or d7df759d0e97)
328 return lambda : repotagscache.set(None)
328 return lambda : repotagscache.set(None)
329
329
330 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
330 # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches
331 # this point, but it isn't so problematic, because:
331 # this point, but it isn't so problematic, because:
332 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
332 # - repo.tags of such Mercurial isn't "callable", and repo.tags()
333 # in perftags() causes failure soon
333 # in perftags() causes failure soon
334 # - perf.py itself has been available since 1.1 (or eb240755386d)
334 # - perf.py itself has been available since 1.1 (or eb240755386d)
335 raise error.Abort(("tags API of this hg command is unknown"))
335 raise error.Abort(("tags API of this hg command is unknown"))
336
336
337 # perf commands
337 # perf commands
338
338
339 @command('perfwalk', formatteropts)
339 @command('perfwalk', formatteropts)
340 def perfwalk(ui, repo, *pats, **opts):
340 def perfwalk(ui, repo, *pats, **opts):
341 timer, fm = gettimer(ui, opts)
341 timer, fm = gettimer(ui, opts)
342 try:
342 try:
343 m = scmutil.match(repo[None], pats, {})
343 m = scmutil.match(repo[None], pats, {})
344 timer(lambda: len(list(repo.dirstate.walk(m, [], True, False))))
344 timer(lambda: len(list(repo.dirstate.walk(m, [], True, False))))
345 except Exception:
345 except Exception:
346 try:
346 try:
347 m = scmutil.match(repo[None], pats, {})
347 m = scmutil.match(repo[None], pats, {})
348 timer(lambda: len([b for a, b, c in repo.dirstate.statwalk([], m)]))
348 timer(lambda: len([b for a, b, c in repo.dirstate.statwalk([], m)]))
349 except Exception:
349 except Exception:
350 timer(lambda: len(list(cmdutil.walk(repo, pats, {}))))
350 timer(lambda: len(list(cmdutil.walk(repo, pats, {}))))
351 fm.end()
351 fm.end()
352
352
353 @command('perfannotate', formatteropts)
353 @command('perfannotate', formatteropts)
354 def perfannotate(ui, repo, f, **opts):
354 def perfannotate(ui, repo, f, **opts):
355 timer, fm = gettimer(ui, opts)
355 timer, fm = gettimer(ui, opts)
356 fc = repo['.'][f]
356 fc = repo['.'][f]
357 timer(lambda: len(fc.annotate(True)))
357 timer(lambda: len(fc.annotate(True)))
358 fm.end()
358 fm.end()
359
359
360 @command('perfstatus',
360 @command('perfstatus',
361 [('u', 'unknown', False,
361 [('u', 'unknown', False,
362 'ask status to look for unknown files')] + formatteropts)
362 'ask status to look for unknown files')] + formatteropts)
363 def perfstatus(ui, repo, **opts):
363 def perfstatus(ui, repo, **opts):
364 #m = match.always(repo.root, repo.getcwd())
364 #m = match.always(repo.root, repo.getcwd())
365 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
365 #timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False,
366 # False))))
366 # False))))
367 timer, fm = gettimer(ui, opts)
367 timer, fm = gettimer(ui, opts)
368 timer(lambda: sum(map(len, repo.status(unknown=opts['unknown']))))
368 timer(lambda: sum(map(len, repo.status(unknown=opts['unknown']))))
369 fm.end()
369 fm.end()
370
370
371 @command('perfaddremove', formatteropts)
371 @command('perfaddremove', formatteropts)
372 def perfaddremove(ui, repo, **opts):
372 def perfaddremove(ui, repo, **opts):
373 timer, fm = gettimer(ui, opts)
373 timer, fm = gettimer(ui, opts)
374 try:
374 try:
375 oldquiet = repo.ui.quiet
375 oldquiet = repo.ui.quiet
376 repo.ui.quiet = True
376 repo.ui.quiet = True
377 matcher = scmutil.match(repo[None])
377 matcher = scmutil.match(repo[None])
378 timer(lambda: scmutil.addremove(repo, matcher, "", dry_run=True))
378 timer(lambda: scmutil.addremove(repo, matcher, "", dry_run=True))
379 finally:
379 finally:
380 repo.ui.quiet = oldquiet
380 repo.ui.quiet = oldquiet
381 fm.end()
381 fm.end()
382
382
383 def clearcaches(cl):
383 def clearcaches(cl):
384 # behave somewhat consistently across internal API changes
384 # behave somewhat consistently across internal API changes
385 if util.safehasattr(cl, 'clearcaches'):
385 if util.safehasattr(cl, 'clearcaches'):
386 cl.clearcaches()
386 cl.clearcaches()
387 elif util.safehasattr(cl, '_nodecache'):
387 elif util.safehasattr(cl, '_nodecache'):
388 from mercurial.node import nullid, nullrev
388 from mercurial.node import nullid, nullrev
389 cl._nodecache = {nullid: nullrev}
389 cl._nodecache = {nullid: nullrev}
390 cl._nodepos = None
390 cl._nodepos = None
391
391
392 @command('perfheads', formatteropts)
392 @command('perfheads', formatteropts)
393 def perfheads(ui, repo, **opts):
393 def perfheads(ui, repo, **opts):
394 timer, fm = gettimer(ui, opts)
394 timer, fm = gettimer(ui, opts)
395 cl = repo.changelog
395 cl = repo.changelog
396 def d():
396 def d():
397 len(cl.headrevs())
397 len(cl.headrevs())
398 clearcaches(cl)
398 clearcaches(cl)
399 timer(d)
399 timer(d)
400 fm.end()
400 fm.end()
401
401
402 @command('perftags', formatteropts)
402 @command('perftags', formatteropts)
403 def perftags(ui, repo, **opts):
403 def perftags(ui, repo, **opts):
404 import mercurial.changelog
404 import mercurial.changelog
405 import mercurial.manifest
405 import mercurial.manifest
406 timer, fm = gettimer(ui, opts)
406 timer, fm = gettimer(ui, opts)
407 svfs = getsvfs(repo)
407 svfs = getsvfs(repo)
408 repocleartagscache = repocleartagscachefunc(repo)
408 repocleartagscache = repocleartagscachefunc(repo)
409 def t():
409 def t():
410 repo.changelog = mercurial.changelog.changelog(svfs)
410 repo.changelog = mercurial.changelog.changelog(svfs)
411 repo.manifestlog = mercurial.manifest.manifestlog(svfs, repo)
411 repo.manifestlog = mercurial.manifest.manifestlog(svfs, repo)
412 repocleartagscache()
412 repocleartagscache()
413 return len(repo.tags())
413 return len(repo.tags())
414 timer(t)
414 timer(t)
415 fm.end()
415 fm.end()
416
416
417 @command('perfancestors', formatteropts)
417 @command('perfancestors', formatteropts)
418 def perfancestors(ui, repo, **opts):
418 def perfancestors(ui, repo, **opts):
419 timer, fm = gettimer(ui, opts)
419 timer, fm = gettimer(ui, opts)
420 heads = repo.changelog.headrevs()
420 heads = repo.changelog.headrevs()
421 def d():
421 def d():
422 for a in repo.changelog.ancestors(heads):
422 for a in repo.changelog.ancestors(heads):
423 pass
423 pass
424 timer(d)
424 timer(d)
425 fm.end()
425 fm.end()
426
426
427 @command('perfancestorset', formatteropts)
427 @command('perfancestorset', formatteropts)
428 def perfancestorset(ui, repo, revset, **opts):
428 def perfancestorset(ui, repo, revset, **opts):
429 timer, fm = gettimer(ui, opts)
429 timer, fm = gettimer(ui, opts)
430 revs = repo.revs(revset)
430 revs = repo.revs(revset)
431 heads = repo.changelog.headrevs()
431 heads = repo.changelog.headrevs()
432 def d():
432 def d():
433 s = repo.changelog.ancestors(heads)
433 s = repo.changelog.ancestors(heads)
434 for rev in revs:
434 for rev in revs:
435 rev in s
435 rev in s
436 timer(d)
436 timer(d)
437 fm.end()
437 fm.end()
438
438
439 @command('perfchangegroupchangelog', formatteropts +
439 @command('perfchangegroupchangelog', formatteropts +
440 [('', 'version', '02', 'changegroup version'),
440 [('', 'version', '02', 'changegroup version'),
441 ('r', 'rev', '', 'revisions to add to changegroup')])
441 ('r', 'rev', '', 'revisions to add to changegroup')])
442 def perfchangegroupchangelog(ui, repo, version='02', rev=None, **opts):
442 def perfchangegroupchangelog(ui, repo, version='02', rev=None, **opts):
443 """Benchmark producing a changelog group for a changegroup.
443 """Benchmark producing a changelog group for a changegroup.
444
444
445 This measures the time spent processing the changelog during a
445 This measures the time spent processing the changelog during a
446 bundle operation. This occurs during `hg bundle` and on a server
446 bundle operation. This occurs during `hg bundle` and on a server
447 processing a `getbundle` wire protocol request (handles clones
447 processing a `getbundle` wire protocol request (handles clones
448 and pull requests).
448 and pull requests).
449
449
450 By default, all revisions are added to the changegroup.
450 By default, all revisions are added to the changegroup.
451 """
451 """
452 cl = repo.changelog
452 cl = repo.changelog
453 revs = [cl.lookup(r) for r in repo.revs(rev or 'all()')]
453 revs = [cl.lookup(r) for r in repo.revs(rev or 'all()')]
454 bundler = changegroup.getbundler(version, repo)
454 bundler = changegroup.getbundler(version, repo)
455
455
456 def lookup(node):
456 def lookup(node):
457 # The real bundler reads the revision in order to access the
457 # The real bundler reads the revision in order to access the
458 # manifest node and files list. Do that here.
458 # manifest node and files list. Do that here.
459 cl.read(node)
459 cl.read(node)
460 return node
460 return node
461
461
462 def d():
462 def d():
463 for chunk in bundler.group(revs, cl, lookup):
463 for chunk in bundler.group(revs, cl, lookup):
464 pass
464 pass
465
465
466 timer, fm = gettimer(ui, opts)
466 timer, fm = gettimer(ui, opts)
467 timer(d)
467 timer(d)
468 fm.end()
468 fm.end()
469
469
470 @command('perfdirs', formatteropts)
470 @command('perfdirs', formatteropts)
471 def perfdirs(ui, repo, **opts):
471 def perfdirs(ui, repo, **opts):
472 timer, fm = gettimer(ui, opts)
472 timer, fm = gettimer(ui, opts)
473 dirstate = repo.dirstate
473 dirstate = repo.dirstate
474 'a' in dirstate
474 'a' in dirstate
475 def d():
475 def d():
476 dirstate.dirs()
476 dirstate.dirs()
477 del dirstate._dirs
477 del dirstate._dirs
478 timer(d)
478 timer(d)
479 fm.end()
479 fm.end()
480
480
481 @command('perfdirstate', formatteropts)
481 @command('perfdirstate', formatteropts)
482 def perfdirstate(ui, repo, **opts):
482 def perfdirstate(ui, repo, **opts):
483 timer, fm = gettimer(ui, opts)
483 timer, fm = gettimer(ui, opts)
484 "a" in repo.dirstate
484 "a" in repo.dirstate
485 def d():
485 def d():
486 repo.dirstate.invalidate()
486 repo.dirstate.invalidate()
487 "a" in repo.dirstate
487 "a" in repo.dirstate
488 timer(d)
488 timer(d)
489 fm.end()
489 fm.end()
490
490
491 @command('perfdirstatedirs', formatteropts)
491 @command('perfdirstatedirs', formatteropts)
492 def perfdirstatedirs(ui, repo, **opts):
492 def perfdirstatedirs(ui, repo, **opts):
493 timer, fm = gettimer(ui, opts)
493 timer, fm = gettimer(ui, opts)
494 "a" in repo.dirstate
494 "a" in repo.dirstate
495 def d():
495 def d():
496 "a" in repo.dirstate._dirs
496 "a" in repo.dirstate._dirs
497 del repo.dirstate._dirs
497 del repo.dirstate._dirs
498 timer(d)
498 timer(d)
499 fm.end()
499 fm.end()
500
500
501 @command('perfdirstatefoldmap', formatteropts)
501 @command('perfdirstatefoldmap', formatteropts)
502 def perfdirstatefoldmap(ui, repo, **opts):
502 def perfdirstatefoldmap(ui, repo, **opts):
503 timer, fm = gettimer(ui, opts)
503 timer, fm = gettimer(ui, opts)
504 dirstate = repo.dirstate
504 dirstate = repo.dirstate
505 'a' in dirstate
505 'a' in dirstate
506 def d():
506 def d():
507 dirstate._filefoldmap.get('a')
507 dirstate._filefoldmap.get('a')
508 del dirstate._filefoldmap
508 del dirstate._filefoldmap
509 timer(d)
509 timer(d)
510 fm.end()
510 fm.end()
511
511
512 @command('perfdirfoldmap', formatteropts)
512 @command('perfdirfoldmap', formatteropts)
513 def perfdirfoldmap(ui, repo, **opts):
513 def perfdirfoldmap(ui, repo, **opts):
514 timer, fm = gettimer(ui, opts)
514 timer, fm = gettimer(ui, opts)
515 dirstate = repo.dirstate
515 dirstate = repo.dirstate
516 'a' in dirstate
516 'a' in dirstate
517 def d():
517 def d():
518 dirstate._dirfoldmap.get('a')
518 dirstate._dirfoldmap.get('a')
519 del dirstate._dirfoldmap
519 del dirstate._dirfoldmap
520 del dirstate._dirs
520 del dirstate._dirs
521 timer(d)
521 timer(d)
522 fm.end()
522 fm.end()
523
523
524 @command('perfdirstatewrite', formatteropts)
524 @command('perfdirstatewrite', formatteropts)
525 def perfdirstatewrite(ui, repo, **opts):
525 def perfdirstatewrite(ui, repo, **opts):
526 timer, fm = gettimer(ui, opts)
526 timer, fm = gettimer(ui, opts)
527 ds = repo.dirstate
527 ds = repo.dirstate
528 "a" in ds
528 "a" in ds
529 def d():
529 def d():
530 ds._dirty = True
530 ds._dirty = True
531 ds.write(repo.currenttransaction())
531 ds.write(repo.currenttransaction())
532 timer(d)
532 timer(d)
533 fm.end()
533 fm.end()
534
534
535 @command('perfmergecalculate',
535 @command('perfmergecalculate',
536 [('r', 'rev', '.', 'rev to merge against')] + formatteropts)
536 [('r', 'rev', '.', 'rev to merge against')] + formatteropts)
537 def perfmergecalculate(ui, repo, rev, **opts):
537 def perfmergecalculate(ui, repo, rev, **opts):
538 timer, fm = gettimer(ui, opts)
538 timer, fm = gettimer(ui, opts)
539 wctx = repo[None]
539 wctx = repo[None]
540 rctx = scmutil.revsingle(repo, rev, rev)
540 rctx = scmutil.revsingle(repo, rev, rev)
541 ancestor = wctx.ancestor(rctx)
541 ancestor = wctx.ancestor(rctx)
542 # we don't want working dir files to be stat'd in the benchmark, so prime
542 # we don't want working dir files to be stat'd in the benchmark, so prime
543 # that cache
543 # that cache
544 wctx.dirty()
544 wctx.dirty()
545 def d():
545 def d():
546 # acceptremote is True because we don't want prompts in the middle of
546 # acceptremote is True because we don't want prompts in the middle of
547 # our benchmark
547 # our benchmark
548 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
548 merge.calculateupdates(repo, wctx, rctx, [ancestor], False, False,
549 acceptremote=True, followcopies=True)
549 acceptremote=True, followcopies=True)
550 timer(d)
550 timer(d)
551 fm.end()
551 fm.end()
552
552
553 @command('perfpathcopies', [], "REV REV")
553 @command('perfpathcopies', [], "REV REV")
554 def perfpathcopies(ui, repo, rev1, rev2, **opts):
554 def perfpathcopies(ui, repo, rev1, rev2, **opts):
555 timer, fm = gettimer(ui, opts)
555 timer, fm = gettimer(ui, opts)
556 ctx1 = scmutil.revsingle(repo, rev1, rev1)
556 ctx1 = scmutil.revsingle(repo, rev1, rev1)
557 ctx2 = scmutil.revsingle(repo, rev2, rev2)
557 ctx2 = scmutil.revsingle(repo, rev2, rev2)
558 def d():
558 def d():
559 copies.pathcopies(ctx1, ctx2)
559 copies.pathcopies(ctx1, ctx2)
560 timer(d)
560 timer(d)
561 fm.end()
561 fm.end()
562
562
563 @command('perfmanifest', [], 'REV')
563 @command('perfmanifest', [], 'REV')
564 def perfmanifest(ui, repo, rev, **opts):
564 def perfmanifest(ui, repo, rev, **opts):
565 timer, fm = gettimer(ui, opts)
565 timer, fm = gettimer(ui, opts)
566 ctx = scmutil.revsingle(repo, rev, rev)
566 ctx = scmutil.revsingle(repo, rev, rev)
567 t = ctx.manifestnode()
567 t = ctx.manifestnode()
568 def d():
568 def d():
569 repo.manifestlog.clearcaches()
569 repo.manifestlog.clearcaches()
570 repo.manifestlog[t].read()
570 repo.manifestlog[t].read()
571 timer(d)
571 timer(d)
572 fm.end()
572 fm.end()
573
573
574 @command('perfchangeset', formatteropts)
574 @command('perfchangeset', formatteropts)
575 def perfchangeset(ui, repo, rev, **opts):
575 def perfchangeset(ui, repo, rev, **opts):
576 timer, fm = gettimer(ui, opts)
576 timer, fm = gettimer(ui, opts)
577 n = repo[rev].node()
577 n = repo[rev].node()
578 def d():
578 def d():
579 repo.changelog.read(n)
579 repo.changelog.read(n)
580 #repo.changelog._cache = None
580 #repo.changelog._cache = None
581 timer(d)
581 timer(d)
582 fm.end()
582 fm.end()
583
583
584 @command('perfindex', formatteropts)
584 @command('perfindex', formatteropts)
585 def perfindex(ui, repo, **opts):
585 def perfindex(ui, repo, **opts):
586 import mercurial.revlog
586 import mercurial.revlog
587 timer, fm = gettimer(ui, opts)
587 timer, fm = gettimer(ui, opts)
588 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
588 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
589 n = repo["tip"].node()
589 n = repo["tip"].node()
590 svfs = getsvfs(repo)
590 svfs = getsvfs(repo)
591 def d():
591 def d():
592 cl = mercurial.revlog.revlog(svfs, "00changelog.i")
592 cl = mercurial.revlog.revlog(svfs, "00changelog.i")
593 cl.rev(n)
593 cl.rev(n)
594 timer(d)
594 timer(d)
595 fm.end()
595 fm.end()
596
596
597 @command('perfstartup', formatteropts)
597 @command('perfstartup', formatteropts)
598 def perfstartup(ui, repo, **opts):
598 def perfstartup(ui, repo, **opts):
599 timer, fm = gettimer(ui, opts)
599 timer, fm = gettimer(ui, opts)
600 cmd = sys.argv[0]
600 cmd = sys.argv[0]
601 def d():
601 def d():
602 if os.name != 'nt':
602 if os.name != 'nt':
603 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
603 os.system("HGRCPATH= %s version -q > /dev/null" % cmd)
604 else:
604 else:
605 os.environ['HGRCPATH'] = ''
605 os.environ['HGRCPATH'] = ''
606 os.system("%s version -q > NUL" % cmd)
606 os.system("%s version -q > NUL" % cmd)
607 timer(d)
607 timer(d)
608 fm.end()
608 fm.end()
609
609
610 @command('perfparents', formatteropts)
610 @command('perfparents', formatteropts)
611 def perfparents(ui, repo, **opts):
611 def perfparents(ui, repo, **opts):
612 timer, fm = gettimer(ui, opts)
612 timer, fm = gettimer(ui, opts)
613 # control the number of commits perfparents iterates over
613 # control the number of commits perfparents iterates over
614 # experimental config: perf.parentscount
614 # experimental config: perf.parentscount
615 count = getint(ui, "perf", "parentscount", 1000)
615 count = getint(ui, "perf", "parentscount", 1000)
616 if len(repo.changelog) < count:
616 if len(repo.changelog) < count:
617 raise error.Abort("repo needs %d commits for this test" % count)
617 raise error.Abort("repo needs %d commits for this test" % count)
618 repo = repo.unfiltered()
618 repo = repo.unfiltered()
619 nl = [repo.changelog.node(i) for i in xrange(count)]
619 nl = [repo.changelog.node(i) for i in xrange(count)]
620 def d():
620 def d():
621 for n in nl:
621 for n in nl:
622 repo.changelog.parents(n)
622 repo.changelog.parents(n)
623 timer(d)
623 timer(d)
624 fm.end()
624 fm.end()
625
625
626 @command('perfctxfiles', formatteropts)
626 @command('perfctxfiles', formatteropts)
627 def perfctxfiles(ui, repo, x, **opts):
627 def perfctxfiles(ui, repo, x, **opts):
628 x = int(x)
628 x = int(x)
629 timer, fm = gettimer(ui, opts)
629 timer, fm = gettimer(ui, opts)
630 def d():
630 def d():
631 len(repo[x].files())
631 len(repo[x].files())
632 timer(d)
632 timer(d)
633 fm.end()
633 fm.end()
634
634
635 @command('perfrawfiles', formatteropts)
635 @command('perfrawfiles', formatteropts)
636 def perfrawfiles(ui, repo, x, **opts):
636 def perfrawfiles(ui, repo, x, **opts):
637 x = int(x)
637 x = int(x)
638 timer, fm = gettimer(ui, opts)
638 timer, fm = gettimer(ui, opts)
639 cl = repo.changelog
639 cl = repo.changelog
640 def d():
640 def d():
641 len(cl.read(x)[3])
641 len(cl.read(x)[3])
642 timer(d)
642 timer(d)
643 fm.end()
643 fm.end()
644
644
645 @command('perflookup', formatteropts)
645 @command('perflookup', formatteropts)
646 def perflookup(ui, repo, rev, **opts):
646 def perflookup(ui, repo, rev, **opts):
647 timer, fm = gettimer(ui, opts)
647 timer, fm = gettimer(ui, opts)
648 timer(lambda: len(repo.lookup(rev)))
648 timer(lambda: len(repo.lookup(rev)))
649 fm.end()
649 fm.end()
650
650
651 @command('perfrevrange', formatteropts)
651 @command('perfrevrange', formatteropts)
652 def perfrevrange(ui, repo, *specs, **opts):
652 def perfrevrange(ui, repo, *specs, **opts):
653 timer, fm = gettimer(ui, opts)
653 timer, fm = gettimer(ui, opts)
654 revrange = scmutil.revrange
654 revrange = scmutil.revrange
655 timer(lambda: len(revrange(repo, specs)))
655 timer(lambda: len(revrange(repo, specs)))
656 fm.end()
656 fm.end()
657
657
658 @command('perfnodelookup', formatteropts)
658 @command('perfnodelookup', formatteropts)
659 def perfnodelookup(ui, repo, rev, **opts):
659 def perfnodelookup(ui, repo, rev, **opts):
660 timer, fm = gettimer(ui, opts)
660 timer, fm = gettimer(ui, opts)
661 import mercurial.revlog
661 import mercurial.revlog
662 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
662 mercurial.revlog._prereadsize = 2**24 # disable lazy parser in old hg
663 n = repo[rev].node()
663 n = repo[rev].node()
664 cl = mercurial.revlog.revlog(getsvfs(repo), "00changelog.i")
664 cl = mercurial.revlog.revlog(getsvfs(repo), "00changelog.i")
665 def d():
665 def d():
666 cl.rev(n)
666 cl.rev(n)
667 clearcaches(cl)
667 clearcaches(cl)
668 timer(d)
668 timer(d)
669 fm.end()
669 fm.end()
670
670
671 @command('perflog',
671 @command('perflog',
672 [('', 'rename', False, 'ask log to follow renames')] + formatteropts)
672 [('', 'rename', False, 'ask log to follow renames')] + formatteropts)
673 def perflog(ui, repo, rev=None, **opts):
673 def perflog(ui, repo, rev=None, **opts):
674 if rev is None:
674 if rev is None:
675 rev=[]
675 rev=[]
676 timer, fm = gettimer(ui, opts)
676 timer, fm = gettimer(ui, opts)
677 ui.pushbuffer()
677 ui.pushbuffer()
678 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
678 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
679 copies=opts.get('rename')))
679 copies=opts.get('rename')))
680 ui.popbuffer()
680 ui.popbuffer()
681 fm.end()
681 fm.end()
682
682
683 @command('perfmoonwalk', formatteropts)
683 @command('perfmoonwalk', formatteropts)
684 def perfmoonwalk(ui, repo, **opts):
684 def perfmoonwalk(ui, repo, **opts):
685 """benchmark walking the changelog backwards
685 """benchmark walking the changelog backwards
686
686
687 This also loads the changelog data for each revision in the changelog.
687 This also loads the changelog data for each revision in the changelog.
688 """
688 """
689 timer, fm = gettimer(ui, opts)
689 timer, fm = gettimer(ui, opts)
690 def moonwalk():
690 def moonwalk():
691 for i in xrange(len(repo), -1, -1):
691 for i in xrange(len(repo), -1, -1):
692 ctx = repo[i]
692 ctx = repo[i]
693 ctx.branch() # read changelog data (in addition to the index)
693 ctx.branch() # read changelog data (in addition to the index)
694 timer(moonwalk)
694 timer(moonwalk)
695 fm.end()
695 fm.end()
696
696
697 @command('perftemplating', formatteropts)
697 @command('perftemplating', formatteropts)
698 def perftemplating(ui, repo, rev=None, **opts):
698 def perftemplating(ui, repo, rev=None, **opts):
699 if rev is None:
699 if rev is None:
700 rev=[]
700 rev=[]
701 timer, fm = gettimer(ui, opts)
701 timer, fm = gettimer(ui, opts)
702 ui.pushbuffer()
702 ui.pushbuffer()
703 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
703 timer(lambda: commands.log(ui, repo, rev=rev, date='', user='',
704 template='{date|shortdate} [{rev}:{node|short}]'
704 template='{date|shortdate} [{rev}:{node|short}]'
705 ' {author|person}: {desc|firstline}\n'))
705 ' {author|person}: {desc|firstline}\n'))
706 ui.popbuffer()
706 ui.popbuffer()
707 fm.end()
707 fm.end()
708
708
709 @command('perfcca', formatteropts)
709 @command('perfcca', formatteropts)
710 def perfcca(ui, repo, **opts):
710 def perfcca(ui, repo, **opts):
711 timer, fm = gettimer(ui, opts)
711 timer, fm = gettimer(ui, opts)
712 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
712 timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate))
713 fm.end()
713 fm.end()
714
714
715 @command('perffncacheload', formatteropts)
715 @command('perffncacheload', formatteropts)
716 def perffncacheload(ui, repo, **opts):
716 def perffncacheload(ui, repo, **opts):
717 timer, fm = gettimer(ui, opts)
717 timer, fm = gettimer(ui, opts)
718 s = repo.store
718 s = repo.store
719 def d():
719 def d():
720 s.fncache._load()
720 s.fncache._load()
721 timer(d)
721 timer(d)
722 fm.end()
722 fm.end()
723
723
724 @command('perffncachewrite', formatteropts)
724 @command('perffncachewrite', formatteropts)
725 def perffncachewrite(ui, repo, **opts):
725 def perffncachewrite(ui, repo, **opts):
726 timer, fm = gettimer(ui, opts)
726 timer, fm = gettimer(ui, opts)
727 s = repo.store
727 s = repo.store
728 s.fncache._load()
728 s.fncache._load()
729 lock = repo.lock()
729 lock = repo.lock()
730 tr = repo.transaction('perffncachewrite')
730 tr = repo.transaction('perffncachewrite')
731 def d():
731 def d():
732 s.fncache._dirty = True
732 s.fncache._dirty = True
733 s.fncache.write(tr)
733 s.fncache.write(tr)
734 timer(d)
734 timer(d)
735 tr.close()
735 tr.close()
736 lock.release()
736 lock.release()
737 fm.end()
737 fm.end()
738
738
739 @command('perffncacheencode', formatteropts)
739 @command('perffncacheencode', formatteropts)
740 def perffncacheencode(ui, repo, **opts):
740 def perffncacheencode(ui, repo, **opts):
741 timer, fm = gettimer(ui, opts)
741 timer, fm = gettimer(ui, opts)
742 s = repo.store
742 s = repo.store
743 s.fncache._load()
743 s.fncache._load()
744 def d():
744 def d():
745 for p in s.fncache.entries:
745 for p in s.fncache.entries:
746 s.encode(p)
746 s.encode(p)
747 timer(d)
747 timer(d)
748 fm.end()
748 fm.end()
749
749
750 @command('perfbdiff', revlogopts + formatteropts + [
750 @command('perfbdiff', revlogopts + formatteropts + [
751 ('', 'count', 1, 'number of revisions to test (when using --startrev)'),
751 ('', 'count', 1, 'number of revisions to test (when using --startrev)'),
752 ('', 'alldata', False, 'test bdiffs for all associated revisions')],
752 ('', 'alldata', False, 'test bdiffs for all associated revisions')],
753 '-c|-m|FILE REV')
753 '-c|-m|FILE REV')
754 def perfbdiff(ui, repo, file_, rev=None, count=None, **opts):
754 def perfbdiff(ui, repo, file_, rev=None, count=None, **opts):
755 """benchmark a bdiff between revisions
755 """benchmark a bdiff between revisions
756
756
757 By default, benchmark a bdiff between its delta parent and itself.
757 By default, benchmark a bdiff between its delta parent and itself.
758
758
759 With ``--count``, benchmark bdiffs between delta parents and self for N
759 With ``--count``, benchmark bdiffs between delta parents and self for N
760 revisions starting at the specified revision.
760 revisions starting at the specified revision.
761
761
762 With ``--alldata``, assume the requested revision is a changeset and
762 With ``--alldata``, assume the requested revision is a changeset and
763 measure bdiffs for all changes related to that changeset (manifest
763 measure bdiffs for all changes related to that changeset (manifest
764 and filelogs).
764 and filelogs).
765 """
765 """
766 if opts['alldata']:
766 if opts['alldata']:
767 opts['changelog'] = True
767 opts['changelog'] = True
768
768
769 if opts.get('changelog') or opts.get('manifest'):
769 if opts.get('changelog') or opts.get('manifest'):
770 file_, rev = None, file_
770 file_, rev = None, file_
771 elif rev is None:
771 elif rev is None:
772 raise error.CommandError('perfbdiff', 'invalid arguments')
772 raise error.CommandError('perfbdiff', 'invalid arguments')
773
773
774 textpairs = []
774 textpairs = []
775
775
776 r = cmdutil.openrevlog(repo, 'perfbdiff', file_, opts)
776 r = cmdutil.openrevlog(repo, 'perfbdiff', file_, opts)
777
777
778 startrev = r.rev(r.lookup(rev))
778 startrev = r.rev(r.lookup(rev))
779 for rev in range(startrev, min(startrev + count, len(r) - 1)):
779 for rev in range(startrev, min(startrev + count, len(r) - 1)):
780 if opts['alldata']:
780 if opts['alldata']:
781 # Load revisions associated with changeset.
781 # Load revisions associated with changeset.
782 ctx = repo[rev]
782 ctx = repo[rev]
783 mtext = repo.manifestlog._revlog.revision(ctx.manifestnode())
783 mtext = repo.manifestlog._revlog.revision(ctx.manifestnode())
784 for pctx in ctx.parents():
784 for pctx in ctx.parents():
785 pman = repo.manifestlog._revlog.revision(pctx.manifestnode())
785 pman = repo.manifestlog._revlog.revision(pctx.manifestnode())
786 textpairs.append((pman, mtext))
786 textpairs.append((pman, mtext))
787
787
788 # Load filelog revisions by iterating manifest delta.
788 # Load filelog revisions by iterating manifest delta.
789 man = ctx.manifest()
789 man = ctx.manifest()
790 pman = ctx.p1().manifest()
790 pman = ctx.p1().manifest()
791 for filename, change in pman.diff(man).items():
791 for filename, change in pman.diff(man).items():
792 fctx = repo.file(filename)
792 fctx = repo.file(filename)
793 f1 = fctx.revision(change[0][0] or -1)
793 f1 = fctx.revision(change[0][0] or -1)
794 f2 = fctx.revision(change[1][0] or -1)
794 f2 = fctx.revision(change[1][0] or -1)
795 textpairs.append((f1, f2))
795 textpairs.append((f1, f2))
796 else:
796 else:
797 dp = r.deltaparent(rev)
797 dp = r.deltaparent(rev)
798 textpairs.append((r.revision(dp), r.revision(rev)))
798 textpairs.append((r.revision(dp), r.revision(rev)))
799
799
800 def d():
800 def d():
801 for pair in textpairs:
801 for pair in textpairs:
802 bdiff.bdiff(*pair)
802 bdiff.bdiff(*pair)
803
803
804 timer, fm = gettimer(ui, opts)
804 timer, fm = gettimer(ui, opts)
805 timer(d)
805 timer(d)
806 fm.end()
806 fm.end()
807
807
808 @command('perfdiffwd', formatteropts)
808 @command('perfdiffwd', formatteropts)
809 def perfdiffwd(ui, repo, **opts):
809 def perfdiffwd(ui, repo, **opts):
810 """Profile diff of working directory changes"""
810 """Profile diff of working directory changes"""
811 timer, fm = gettimer(ui, opts)
811 timer, fm = gettimer(ui, opts)
812 options = {
812 options = {
813 'w': 'ignore_all_space',
813 'w': 'ignore_all_space',
814 'b': 'ignore_space_change',
814 'b': 'ignore_space_change',
815 'B': 'ignore_blank_lines',
815 'B': 'ignore_blank_lines',
816 }
816 }
817
817
818 for diffopt in ('', 'w', 'b', 'B', 'wB'):
818 for diffopt in ('', 'w', 'b', 'B', 'wB'):
819 opts = dict((options[c], '1') for c in diffopt)
819 opts = dict((options[c], '1') for c in diffopt)
820 def d():
820 def d():
821 ui.pushbuffer()
821 ui.pushbuffer()
822 commands.diff(ui, repo, **opts)
822 commands.diff(ui, repo, **opts)
823 ui.popbuffer()
823 ui.popbuffer()
824 title = 'diffopts: %s' % (diffopt and ('-' + diffopt) or 'none')
824 title = 'diffopts: %s' % (diffopt and ('-' + diffopt) or 'none')
825 timer(d, title)
825 timer(d, title)
826 fm.end()
826 fm.end()
827
827
828 @command('perfrevlog', revlogopts + formatteropts +
828 @command('perfrevlog', revlogopts + formatteropts +
829 [('d', 'dist', 100, 'distance between the revisions'),
829 [('d', 'dist', 100, 'distance between the revisions'),
830 ('s', 'startrev', 0, 'revision to start reading at'),
830 ('s', 'startrev', 0, 'revision to start reading at'),
831 ('', 'reverse', False, 'read in reverse')],
831 ('', 'reverse', False, 'read in reverse')],
832 '-c|-m|FILE')
832 '-c|-m|FILE')
833 def perfrevlog(ui, repo, file_=None, startrev=0, reverse=False, **opts):
833 def perfrevlog(ui, repo, file_=None, startrev=0, reverse=False, **opts):
834 """Benchmark reading a series of revisions from a revlog.
834 """Benchmark reading a series of revisions from a revlog.
835
835
836 By default, we read every ``-d/--dist`` revision from 0 to tip of
836 By default, we read every ``-d/--dist`` revision from 0 to tip of
837 the specified revlog.
837 the specified revlog.
838
838
839 The start revision can be defined via ``-s/--startrev``.
839 The start revision can be defined via ``-s/--startrev``.
840 """
840 """
841 timer, fm = gettimer(ui, opts)
841 timer, fm = gettimer(ui, opts)
842 _len = getlen(ui)
842 _len = getlen(ui)
843
843
844 def d():
844 def d():
845 r = cmdutil.openrevlog(repo, 'perfrevlog', file_, opts)
845 r = cmdutil.openrevlog(repo, 'perfrevlog', file_, opts)
846
846
847 startrev = 0
847 startrev = 0
848 endrev = _len(r)
848 endrev = _len(r)
849 dist = opts['dist']
849 dist = opts['dist']
850
850
851 if reverse:
851 if reverse:
852 startrev, endrev = endrev, startrev
852 startrev, endrev = endrev, startrev
853 dist = -1 * dist
853 dist = -1 * dist
854
854
855 for x in xrange(startrev, endrev, dist):
855 for x in xrange(startrev, endrev, dist):
856 r.revision(r.node(x))
856 r.revision(r.node(x))
857
857
858 timer(d)
858 timer(d)
859 fm.end()
859 fm.end()
860
860
861 @command('perfrevlogchunks', revlogopts + formatteropts +
861 @command('perfrevlogchunks', revlogopts + formatteropts +
862 [('e', 'engines', '', 'compression engines to use'),
862 [('e', 'engines', '', 'compression engines to use'),
863 ('s', 'startrev', 0, 'revision to start at')],
863 ('s', 'startrev', 0, 'revision to start at')],
864 '-c|-m|FILE')
864 '-c|-m|FILE')
865 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
865 def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts):
866 """Benchmark operations on revlog chunks.
866 """Benchmark operations on revlog chunks.
867
867
868 Logically, each revlog is a collection of fulltext revisions. However,
868 Logically, each revlog is a collection of fulltext revisions. However,
869 stored within each revlog are "chunks" of possibly compressed data. This
869 stored within each revlog are "chunks" of possibly compressed data. This
870 data needs to be read and decompressed or compressed and written.
870 data needs to be read and decompressed or compressed and written.
871
871
872 This command measures the time it takes to read+decompress and recompress
872 This command measures the time it takes to read+decompress and recompress
873 chunks in a revlog. It effectively isolates I/O and compression performance.
873 chunks in a revlog. It effectively isolates I/O and compression performance.
874 For measurements of higher-level operations like resolving revisions,
874 For measurements of higher-level operations like resolving revisions,
875 see ``perfrevlog`` and ``perfrevlogrevision``.
875 see ``perfrevlog`` and ``perfrevlogrevision``.
876 """
876 """
877 rl = cmdutil.openrevlog(repo, 'perfrevlogchunks', file_, opts)
877 rl = cmdutil.openrevlog(repo, 'perfrevlogchunks', file_, opts)
878
878
879 # Verify engines argument.
879 # Verify engines argument.
880 if engines:
880 if engines:
881 engines = set(e.strip() for e in engines.split(','))
881 engines = set(e.strip() for e in engines.split(','))
882 for engine in engines:
882 for engine in engines:
883 try:
883 try:
884 util.compressionengines[engine]
884 util.compressionengines[engine]
885 except KeyError:
885 except KeyError:
886 raise error.Abort('unknown compression engine: %s' % engine)
886 raise error.Abort('unknown compression engine: %s' % engine)
887 else:
887 else:
888 engines = []
888 engines = []
889 for e in util.compengines:
889 for e in util.compengines:
890 engine = util.compengines[e]
890 engine = util.compengines[e]
891 try:
891 try:
892 if engine.available():
892 if engine.available():
893 engine.revlogcompressor().compress('dummy')
893 engine.revlogcompressor().compress('dummy')
894 engines.append(e)
894 engines.append(e)
895 except NotImplementedError:
895 except NotImplementedError:
896 pass
896 pass
897
897
898 revs = list(rl.revs(startrev, len(rl) - 1))
898 revs = list(rl.revs(startrev, len(rl) - 1))
899
899
900 def rlfh(rl):
900 def rlfh(rl):
901 if rl._inline:
901 if rl._inline:
902 return getsvfs(repo)(rl.indexfile)
902 return getsvfs(repo)(rl.indexfile)
903 else:
903 else:
904 return getsvfs(repo)(rl.datafile)
904 return getsvfs(repo)(rl.datafile)
905
905
906 def doread():
906 def doread():
907 rl.clearcaches()
907 rl.clearcaches()
908 for rev in revs:
908 for rev in revs:
909 rl._chunkraw(rev, rev)
909 rl._chunkraw(rev, rev)
910
910
911 def doreadcachedfh():
911 def doreadcachedfh():
912 rl.clearcaches()
912 rl.clearcaches()
913 fh = rlfh(rl)
913 fh = rlfh(rl)
914 for rev in revs:
914 for rev in revs:
915 rl._chunkraw(rev, rev, df=fh)
915 rl._chunkraw(rev, rev, df=fh)
916
916
917 def doreadbatch():
917 def doreadbatch():
918 rl.clearcaches()
918 rl.clearcaches()
919 rl._chunkraw(revs[0], revs[-1])
919 rl._chunkraw(revs[0], revs[-1])
920
920
921 def doreadbatchcachedfh():
921 def doreadbatchcachedfh():
922 rl.clearcaches()
922 rl.clearcaches()
923 fh = rlfh(rl)
923 fh = rlfh(rl)
924 rl._chunkraw(revs[0], revs[-1], df=fh)
924 rl._chunkraw(revs[0], revs[-1], df=fh)
925
925
926 def dochunk():
926 def dochunk():
927 rl.clearcaches()
927 rl.clearcaches()
928 fh = rlfh(rl)
928 fh = rlfh(rl)
929 for rev in revs:
929 for rev in revs:
930 rl._chunk(rev, df=fh)
930 rl._chunk(rev, df=fh)
931
931
932 chunks = [None]
932 chunks = [None]
933
933
934 def dochunkbatch():
934 def dochunkbatch():
935 rl.clearcaches()
935 rl.clearcaches()
936 fh = rlfh(rl)
936 fh = rlfh(rl)
937 # Save chunks as a side-effect.
937 # Save chunks as a side-effect.
938 chunks[0] = rl._chunks(revs, df=fh)
938 chunks[0] = rl._chunks(revs, df=fh)
939
939
940 def docompress(compressor):
940 def docompress(compressor):
941 rl.clearcaches()
941 rl.clearcaches()
942
942
943 try:
943 try:
944 # Swap in the requested compression engine.
944 # Swap in the requested compression engine.
945 oldcompressor = rl._compressor
945 oldcompressor = rl._compressor
946 rl._compressor = compressor
946 rl._compressor = compressor
947 for chunk in chunks[0]:
947 for chunk in chunks[0]:
948 rl.compress(chunk)
948 rl.compress(chunk)
949 finally:
949 finally:
950 rl._compressor = oldcompressor
950 rl._compressor = oldcompressor
951
951
952 benches = [
952 benches = [
953 (lambda: doread(), 'read'),
953 (lambda: doread(), 'read'),
954 (lambda: doreadcachedfh(), 'read w/ reused fd'),
954 (lambda: doreadcachedfh(), 'read w/ reused fd'),
955 (lambda: doreadbatch(), 'read batch'),
955 (lambda: doreadbatch(), 'read batch'),
956 (lambda: doreadbatchcachedfh(), 'read batch w/ reused fd'),
956 (lambda: doreadbatchcachedfh(), 'read batch w/ reused fd'),
957 (lambda: dochunk(), 'chunk'),
957 (lambda: dochunk(), 'chunk'),
958 (lambda: dochunkbatch(), 'chunk batch'),
958 (lambda: dochunkbatch(), 'chunk batch'),
959 ]
959 ]
960
960
961 for engine in sorted(engines):
961 for engine in sorted(engines):
962 compressor = util.compengines[engine].revlogcompressor()
962 compressor = util.compengines[engine].revlogcompressor()
963 benches.append((functools.partial(docompress, compressor),
963 benches.append((functools.partial(docompress, compressor),
964 'compress w/ %s' % engine))
964 'compress w/ %s' % engine))
965
965
966 for fn, title in benches:
966 for fn, title in benches:
967 timer, fm = gettimer(ui, opts)
967 timer, fm = gettimer(ui, opts)
968 timer(fn, title=title)
968 timer(fn, title=title)
969 fm.end()
969 fm.end()
970
970
971 @command('perfrevlogrevision', revlogopts + formatteropts +
971 @command('perfrevlogrevision', revlogopts + formatteropts +
972 [('', 'cache', False, 'use caches instead of clearing')],
972 [('', 'cache', False, 'use caches instead of clearing')],
973 '-c|-m|FILE REV')
973 '-c|-m|FILE REV')
974 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
974 def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts):
975 """Benchmark obtaining a revlog revision.
975 """Benchmark obtaining a revlog revision.
976
976
977 Obtaining a revlog revision consists of roughly the following steps:
977 Obtaining a revlog revision consists of roughly the following steps:
978
978
979 1. Compute the delta chain
979 1. Compute the delta chain
980 2. Obtain the raw chunks for that delta chain
980 2. Obtain the raw chunks for that delta chain
981 3. Decompress each raw chunk
981 3. Decompress each raw chunk
982 4. Apply binary patches to obtain fulltext
982 4. Apply binary patches to obtain fulltext
983 5. Verify hash of fulltext
983 5. Verify hash of fulltext
984
984
985 This command measures the time spent in each of these phases.
985 This command measures the time spent in each of these phases.
986 """
986 """
987 if opts.get('changelog') or opts.get('manifest'):
987 if opts.get('changelog') or opts.get('manifest'):
988 file_, rev = None, file_
988 file_, rev = None, file_
989 elif rev is None:
989 elif rev is None:
990 raise error.CommandError('perfrevlogrevision', 'invalid arguments')
990 raise error.CommandError('perfrevlogrevision', 'invalid arguments')
991
991
992 r = cmdutil.openrevlog(repo, 'perfrevlogrevision', file_, opts)
992 r = cmdutil.openrevlog(repo, 'perfrevlogrevision', file_, opts)
993 node = r.lookup(rev)
993 node = r.lookup(rev)
994 rev = r.rev(node)
994 rev = r.rev(node)
995
995
996 def getrawchunks(data, chain):
996 def getrawchunks(data, chain):
997 start = r.start
997 start = r.start
998 length = r.length
998 length = r.length
999 inline = r._inline
999 inline = r._inline
1000 iosize = r._io.size
1000 iosize = r._io.size
1001 buffer = util.buffer
1001 buffer = util.buffer
1002 offset = start(chain[0])
1002 offset = start(chain[0])
1003
1003
1004 chunks = []
1004 chunks = []
1005 ladd = chunks.append
1005 ladd = chunks.append
1006
1006
1007 for rev in chain:
1007 for rev in chain:
1008 chunkstart = start(rev)
1008 chunkstart = start(rev)
1009 if inline:
1009 if inline:
1010 chunkstart += (rev + 1) * iosize
1010 chunkstart += (rev + 1) * iosize
1011 chunklength = length(rev)
1011 chunklength = length(rev)
1012 ladd(buffer(data, chunkstart - offset, chunklength))
1012 ladd(buffer(data, chunkstart - offset, chunklength))
1013
1013
1014 return chunks
1014 return chunks
1015
1015
1016 def dodeltachain(rev):
1016 def dodeltachain(rev):
1017 if not cache:
1017 if not cache:
1018 r.clearcaches()
1018 r.clearcaches()
1019 r._deltachain(rev)
1019 r._deltachain(rev)
1020
1020
1021 def doread(chain):
1021 def doread(chain):
1022 if not cache:
1022 if not cache:
1023 r.clearcaches()
1023 r.clearcaches()
1024 r._chunkraw(chain[0], chain[-1])
1024 r._chunkraw(chain[0], chain[-1])
1025
1025
1026 def dorawchunks(data, chain):
1026 def dorawchunks(data, chain):
1027 if not cache:
1027 if not cache:
1028 r.clearcaches()
1028 r.clearcaches()
1029 getrawchunks(data, chain)
1029 getrawchunks(data, chain)
1030
1030
1031 def dodecompress(chunks):
1031 def dodecompress(chunks):
1032 decomp = r.decompress
1032 decomp = r.decompress
1033 for chunk in chunks:
1033 for chunk in chunks:
1034 decomp(chunk)
1034 decomp(chunk)
1035
1035
1036 def dopatch(text, bins):
1036 def dopatch(text, bins):
1037 if not cache:
1037 if not cache:
1038 r.clearcaches()
1038 r.clearcaches()
1039 mdiff.patches(text, bins)
1039 mdiff.patches(text, bins)
1040
1040
1041 def dohash(text):
1041 def dohash(text):
1042 if not cache:
1042 if not cache:
1043 r.clearcaches()
1043 r.clearcaches()
1044 r.checkhash(text, node, rev=rev)
1044 r.checkhash(text, node, rev=rev)
1045
1045
1046 def dorevision():
1046 def dorevision():
1047 if not cache:
1047 if not cache:
1048 r.clearcaches()
1048 r.clearcaches()
1049 r.revision(node)
1049 r.revision(node)
1050
1050
1051 chain = r._deltachain(rev)[0]
1051 chain = r._deltachain(rev)[0]
1052 data = r._chunkraw(chain[0], chain[-1])[1]
1052 data = r._chunkraw(chain[0], chain[-1])[1]
1053 rawchunks = getrawchunks(data, chain)
1053 rawchunks = getrawchunks(data, chain)
1054 bins = r._chunks(chain)
1054 bins = r._chunks(chain)
1055 text = str(bins[0])
1055 text = str(bins[0])
1056 bins = bins[1:]
1056 bins = bins[1:]
1057 text = mdiff.patches(text, bins)
1057 text = mdiff.patches(text, bins)
1058
1058
1059 benches = [
1059 benches = [
1060 (lambda: dorevision(), 'full'),
1060 (lambda: dorevision(), 'full'),
1061 (lambda: dodeltachain(rev), 'deltachain'),
1061 (lambda: dodeltachain(rev), 'deltachain'),
1062 (lambda: doread(chain), 'read'),
1062 (lambda: doread(chain), 'read'),
1063 (lambda: dorawchunks(data, chain), 'rawchunks'),
1063 (lambda: dorawchunks(data, chain), 'rawchunks'),
1064 (lambda: dodecompress(rawchunks), 'decompress'),
1064 (lambda: dodecompress(rawchunks), 'decompress'),
1065 (lambda: dopatch(text, bins), 'patch'),
1065 (lambda: dopatch(text, bins), 'patch'),
1066 (lambda: dohash(text), 'hash'),
1066 (lambda: dohash(text), 'hash'),
1067 ]
1067 ]
1068
1068
1069 for fn, title in benches:
1069 for fn, title in benches:
1070 timer, fm = gettimer(ui, opts)
1070 timer, fm = gettimer(ui, opts)
1071 timer(fn, title=title)
1071 timer(fn, title=title)
1072 fm.end()
1072 fm.end()
1073
1073
1074 @command('perfrevset',
1074 @command('perfrevset',
1075 [('C', 'clear', False, 'clear volatile cache between each call.'),
1075 [('C', 'clear', False, 'clear volatile cache between each call.'),
1076 ('', 'contexts', False, 'obtain changectx for each revision')]
1076 ('', 'contexts', False, 'obtain changectx for each revision')]
1077 + formatteropts, "REVSET")
1077 + formatteropts, "REVSET")
1078 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
1078 def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts):
1079 """benchmark the execution time of a revset
1079 """benchmark the execution time of a revset
1080
1080
1081 Use the --clean option if need to evaluate the impact of build volatile
1081 Use the --clean option if need to evaluate the impact of build volatile
1082 revisions set cache on the revset execution. Volatile cache hold filtered
1082 revisions set cache on the revset execution. Volatile cache hold filtered
1083 and obsolete related cache."""
1083 and obsolete related cache."""
1084 timer, fm = gettimer(ui, opts)
1084 timer, fm = gettimer(ui, opts)
1085 def d():
1085 def d():
1086 if clear:
1086 if clear:
1087 repo.invalidatevolatilesets()
1087 repo.invalidatevolatilesets()
1088 if contexts:
1088 if contexts:
1089 for ctx in repo.set(expr): pass
1089 for ctx in repo.set(expr): pass
1090 else:
1090 else:
1091 for r in repo.revs(expr): pass
1091 for r in repo.revs(expr): pass
1092 timer(d)
1092 timer(d)
1093 fm.end()
1093 fm.end()
1094
1094
1095 @command('perfvolatilesets', formatteropts)
1095 @command('perfvolatilesets', formatteropts)
1096 def perfvolatilesets(ui, repo, *names, **opts):
1096 def perfvolatilesets(ui, repo, *names, **opts):
1097 """benchmark the computation of various volatile set
1097 """benchmark the computation of various volatile set
1098
1098
1099 Volatile set computes element related to filtering and obsolescence."""
1099 Volatile set computes element related to filtering and obsolescence."""
1100 timer, fm = gettimer(ui, opts)
1100 timer, fm = gettimer(ui, opts)
1101 repo = repo.unfiltered()
1101 repo = repo.unfiltered()
1102
1102
1103 def getobs(name):
1103 def getobs(name):
1104 def d():
1104 def d():
1105 repo.invalidatevolatilesets()
1105 repo.invalidatevolatilesets()
1106 obsolete.getrevs(repo, name)
1106 obsolete.getrevs(repo, name)
1107 return d
1107 return d
1108
1108
1109 allobs = sorted(obsolete.cachefuncs)
1109 allobs = sorted(obsolete.cachefuncs)
1110 if names:
1110 if names:
1111 allobs = [n for n in allobs if n in names]
1111 allobs = [n for n in allobs if n in names]
1112
1112
1113 for name in allobs:
1113 for name in allobs:
1114 timer(getobs(name), title=name)
1114 timer(getobs(name), title=name)
1115
1115
1116 def getfiltered(name):
1116 def getfiltered(name):
1117 def d():
1117 def d():
1118 repo.invalidatevolatilesets()
1118 repo.invalidatevolatilesets()
1119 repoview.filterrevs(repo, name)
1119 repoview.filterrevs(repo, name)
1120 return d
1120 return d
1121
1121
1122 allfilter = sorted(repoview.filtertable)
1122 allfilter = sorted(repoview.filtertable)
1123 if names:
1123 if names:
1124 allfilter = [n for n in allfilter if n in names]
1124 allfilter = [n for n in allfilter if n in names]
1125
1125
1126 for name in allfilter:
1126 for name in allfilter:
1127 timer(getfiltered(name), title=name)
1127 timer(getfiltered(name), title=name)
1128 fm.end()
1128 fm.end()
1129
1129
1130 @command('perfbranchmap',
1130 @command('perfbranchmap',
1131 [('f', 'full', False,
1131 [('f', 'full', False,
1132 'Includes build time of subset'),
1132 'Includes build time of subset'),
1133 ] + formatteropts)
1133 ] + formatteropts)
1134 def perfbranchmap(ui, repo, full=False, **opts):
1134 def perfbranchmap(ui, repo, full=False, **opts):
1135 """benchmark the update of a branchmap
1135 """benchmark the update of a branchmap
1136
1136
1137 This benchmarks the full repo.branchmap() call with read and write disabled
1137 This benchmarks the full repo.branchmap() call with read and write disabled
1138 """
1138 """
1139 timer, fm = gettimer(ui, opts)
1139 timer, fm = gettimer(ui, opts)
1140 def getbranchmap(filtername):
1140 def getbranchmap(filtername):
1141 """generate a benchmark function for the filtername"""
1141 """generate a benchmark function for the filtername"""
1142 if filtername is None:
1142 if filtername is None:
1143 view = repo
1143 view = repo
1144 else:
1144 else:
1145 view = repo.filtered(filtername)
1145 view = repo.filtered(filtername)
1146 def d():
1146 def d():
1147 if full:
1147 if full:
1148 view._branchcaches.clear()
1148 view._branchcaches.clear()
1149 else:
1149 else:
1150 view._branchcaches.pop(filtername, None)
1150 view._branchcaches.pop(filtername, None)
1151 view.branchmap()
1151 view.branchmap()
1152 return d
1152 return d
1153 # add filter in smaller subset to bigger subset
1153 # add filter in smaller subset to bigger subset
1154 possiblefilters = set(repoview.filtertable)
1154 possiblefilters = set(repoview.filtertable)
1155 subsettable = getbranchmapsubsettable()
1155 subsettable = getbranchmapsubsettable()
1156 allfilters = []
1156 allfilters = []
1157 while possiblefilters:
1157 while possiblefilters:
1158 for name in possiblefilters:
1158 for name in possiblefilters:
1159 subset = subsettable.get(name)
1159 subset = subsettable.get(name)
1160 if subset not in possiblefilters:
1160 if subset not in possiblefilters:
1161 break
1161 break
1162 else:
1162 else:
1163 assert False, 'subset cycle %s!' % possiblefilters
1163 assert False, 'subset cycle %s!' % possiblefilters
1164 allfilters.append(name)
1164 allfilters.append(name)
1165 possiblefilters.remove(name)
1165 possiblefilters.remove(name)
1166
1166
1167 # warm the cache
1167 # warm the cache
1168 if not full:
1168 if not full:
1169 for name in allfilters:
1169 for name in allfilters:
1170 repo.filtered(name).branchmap()
1170 repo.filtered(name).branchmap()
1171 # add unfiltered
1171 # add unfiltered
1172 allfilters.append(None)
1172 allfilters.append(None)
1173
1173
1174 branchcacheread = safeattrsetter(branchmap, 'read')
1174 branchcacheread = safeattrsetter(branchmap, 'read')
1175 branchcachewrite = safeattrsetter(branchmap.branchcache, 'write')
1175 branchcachewrite = safeattrsetter(branchmap.branchcache, 'write')
1176 branchcacheread.set(lambda repo: None)
1176 branchcacheread.set(lambda repo: None)
1177 branchcachewrite.set(lambda bc, repo: None)
1177 branchcachewrite.set(lambda bc, repo: None)
1178 try:
1178 try:
1179 for name in allfilters:
1179 for name in allfilters:
1180 timer(getbranchmap(name), title=str(name))
1180 timer(getbranchmap(name), title=str(name))
1181 finally:
1181 finally:
1182 branchcacheread.restore()
1182 branchcacheread.restore()
1183 branchcachewrite.restore()
1183 branchcachewrite.restore()
1184 fm.end()
1184 fm.end()
1185
1185
1186 @command('perfloadmarkers')
1186 @command('perfloadmarkers')
1187 def perfloadmarkers(ui, repo):
1187 def perfloadmarkers(ui, repo):
1188 """benchmark the time to parse the on-disk markers for a repo
1188 """benchmark the time to parse the on-disk markers for a repo
1189
1189
1190 Result is the number of markers in the repo."""
1190 Result is the number of markers in the repo."""
1191 timer, fm = gettimer(ui)
1191 timer, fm = gettimer(ui)
1192 svfs = getsvfs(repo)
1192 svfs = getsvfs(repo)
1193 timer(lambda: len(obsolete.obsstore(svfs)))
1193 timer(lambda: len(obsolete.obsstore(svfs)))
1194 fm.end()
1194 fm.end()
1195
1195
1196 @command('perflrucachedict', formatteropts +
1196 @command('perflrucachedict', formatteropts +
1197 [('', 'size', 4, 'size of cache'),
1197 [('', 'size', 4, 'size of cache'),
1198 ('', 'gets', 10000, 'number of key lookups'),
1198 ('', 'gets', 10000, 'number of key lookups'),
1199 ('', 'sets', 10000, 'number of key sets'),
1199 ('', 'sets', 10000, 'number of key sets'),
1200 ('', 'mixed', 10000, 'number of mixed mode operations'),
1200 ('', 'mixed', 10000, 'number of mixed mode operations'),
1201 ('', 'mixedgetfreq', 50, 'frequency of get vs set ops in mixed mode')],
1201 ('', 'mixedgetfreq', 50, 'frequency of get vs set ops in mixed mode')],
1202 norepo=True)
1202 norepo=True)
1203 def perflrucache(ui, size=4, gets=10000, sets=10000, mixed=10000,
1203 def perflrucache(ui, size=4, gets=10000, sets=10000, mixed=10000,
1204 mixedgetfreq=50, **opts):
1204 mixedgetfreq=50, **opts):
1205 def doinit():
1205 def doinit():
1206 for i in xrange(10000):
1206 for i in xrange(10000):
1207 util.lrucachedict(size)
1207 util.lrucachedict(size)
1208
1208
1209 values = []
1209 values = []
1210 for i in xrange(size):
1210 for i in xrange(size):
1211 values.append(random.randint(0, sys.maxint))
1211 values.append(random.randint(0, sys.maxint))
1212
1212
1213 # Get mode fills the cache and tests raw lookup performance with no
1213 # Get mode fills the cache and tests raw lookup performance with no
1214 # eviction.
1214 # eviction.
1215 getseq = []
1215 getseq = []
1216 for i in xrange(gets):
1216 for i in xrange(gets):
1217 getseq.append(random.choice(values))
1217 getseq.append(random.choice(values))
1218
1218
1219 def dogets():
1219 def dogets():
1220 d = util.lrucachedict(size)
1220 d = util.lrucachedict(size)
1221 for v in values:
1221 for v in values:
1222 d[v] = v
1222 d[v] = v
1223 for key in getseq:
1223 for key in getseq:
1224 value = d[key]
1224 value = d[key]
1225 value # silence pyflakes warning
1225 value # silence pyflakes warning
1226
1226
1227 # Set mode tests insertion speed with cache eviction.
1227 # Set mode tests insertion speed with cache eviction.
1228 setseq = []
1228 setseq = []
1229 for i in xrange(sets):
1229 for i in xrange(sets):
1230 setseq.append(random.randint(0, sys.maxint))
1230 setseq.append(random.randint(0, sys.maxint))
1231
1231
1232 def dosets():
1232 def dosets():
1233 d = util.lrucachedict(size)
1233 d = util.lrucachedict(size)
1234 for v in setseq:
1234 for v in setseq:
1235 d[v] = v
1235 d[v] = v
1236
1236
1237 # Mixed mode randomly performs gets and sets with eviction.
1237 # Mixed mode randomly performs gets and sets with eviction.
1238 mixedops = []
1238 mixedops = []
1239 for i in xrange(mixed):
1239 for i in xrange(mixed):
1240 r = random.randint(0, 100)
1240 r = random.randint(0, 100)
1241 if r < mixedgetfreq:
1241 if r < mixedgetfreq:
1242 op = 0
1242 op = 0
1243 else:
1243 else:
1244 op = 1
1244 op = 1
1245
1245
1246 mixedops.append((op, random.randint(0, size * 2)))
1246 mixedops.append((op, random.randint(0, size * 2)))
1247
1247
1248 def domixed():
1248 def domixed():
1249 d = util.lrucachedict(size)
1249 d = util.lrucachedict(size)
1250
1250
1251 for op, v in mixedops:
1251 for op, v in mixedops:
1252 if op == 0:
1252 if op == 0:
1253 try:
1253 try:
1254 d[v]
1254 d[v]
1255 except KeyError:
1255 except KeyError:
1256 pass
1256 pass
1257 else:
1257 else:
1258 d[v] = v
1258 d[v] = v
1259
1259
1260 benches = [
1260 benches = [
1261 (doinit, 'init'),
1261 (doinit, 'init'),
1262 (dogets, 'gets'),
1262 (dogets, 'gets'),
1263 (dosets, 'sets'),
1263 (dosets, 'sets'),
1264 (domixed, 'mixed')
1264 (domixed, 'mixed')
1265 ]
1265 ]
1266
1266
1267 for fn, title in benches:
1267 for fn, title in benches:
1268 timer, fm = gettimer(ui, opts)
1268 timer, fm = gettimer(ui, opts)
1269 timer(fn, title=title)
1269 timer(fn, title=title)
1270 fm.end()
1270 fm.end()
1271
1271
1272 def uisetup(ui):
1272 def uisetup(ui):
1273 if (util.safehasattr(cmdutil, 'openrevlog') and
1273 if (util.safehasattr(cmdutil, 'openrevlog') and
1274 not util.safehasattr(commands, 'debugrevlogopts')):
1274 not util.safehasattr(commands, 'debugrevlogopts')):
1275 # for "historical portability":
1275 # for "historical portability":
1276 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
1276 # In this case, Mercurial should be 1.9 (or a79fea6b3e77) -
1277 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
1277 # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for
1278 # openrevlog() should cause failure, because it has been
1278 # openrevlog() should cause failure, because it has been
1279 # available since 3.5 (or 49c583ca48c4).
1279 # available since 3.5 (or 49c583ca48c4).
1280 def openrevlog(orig, repo, cmd, file_, opts):
1280 def openrevlog(orig, repo, cmd, file_, opts):
1281 if opts.get('dir') and not util.safehasattr(repo, 'dirlog'):
1281 if opts.get('dir') and not util.safehasattr(repo, 'dirlog'):
1282 raise error.Abort("This version doesn't support --dir option",
1282 raise error.Abort("This version doesn't support --dir option",
1283 hint="use 3.5 or later")
1283 hint="use 3.5 or later")
1284 return orig(repo, cmd, file_, opts)
1284 return orig(repo, cmd, file_, opts)
1285 extensions.wrapfunction(cmdutil, 'openrevlog', openrevlog)
1285 extensions.wrapfunction(cmdutil, 'openrevlog', openrevlog)
@@ -1,522 +1,522 b''
1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
1 # branchmap.py - logic to computes, maintain and stores branchmap for local repo
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 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 array
10 import array
11 import struct
11 import struct
12 import time
13
12
14 from .node import (
13 from .node import (
15 bin,
14 bin,
16 hex,
15 hex,
17 nullid,
16 nullid,
18 nullrev,
17 nullrev,
19 )
18 )
20 from . import (
19 from . import (
21 encoding,
20 encoding,
22 error,
21 error,
23 scmutil,
22 scmutil,
23 util,
24 )
24 )
25
25
26 array = array.array
26 array = array.array
27 calcsize = struct.calcsize
27 calcsize = struct.calcsize
28 pack = struct.pack
28 pack = struct.pack
29 unpack = struct.unpack
29 unpack = struct.unpack
30
30
31 def _filename(repo):
31 def _filename(repo):
32 """name of a branchcache file for a given repo or repoview"""
32 """name of a branchcache file for a given repo or repoview"""
33 filename = "cache/branch2"
33 filename = "cache/branch2"
34 if repo.filtername:
34 if repo.filtername:
35 filename = '%s-%s' % (filename, repo.filtername)
35 filename = '%s-%s' % (filename, repo.filtername)
36 return filename
36 return filename
37
37
38 def read(repo):
38 def read(repo):
39 try:
39 try:
40 f = repo.vfs(_filename(repo))
40 f = repo.vfs(_filename(repo))
41 lines = f.read().split('\n')
41 lines = f.read().split('\n')
42 f.close()
42 f.close()
43 except (IOError, OSError):
43 except (IOError, OSError):
44 return None
44 return None
45
45
46 try:
46 try:
47 cachekey = lines.pop(0).split(" ", 2)
47 cachekey = lines.pop(0).split(" ", 2)
48 last, lrev = cachekey[:2]
48 last, lrev = cachekey[:2]
49 last, lrev = bin(last), int(lrev)
49 last, lrev = bin(last), int(lrev)
50 filteredhash = None
50 filteredhash = None
51 if len(cachekey) > 2:
51 if len(cachekey) > 2:
52 filteredhash = bin(cachekey[2])
52 filteredhash = bin(cachekey[2])
53 partial = branchcache(tipnode=last, tiprev=lrev,
53 partial = branchcache(tipnode=last, tiprev=lrev,
54 filteredhash=filteredhash)
54 filteredhash=filteredhash)
55 if not partial.validfor(repo):
55 if not partial.validfor(repo):
56 # invalidate the cache
56 # invalidate the cache
57 raise ValueError('tip differs')
57 raise ValueError('tip differs')
58 cl = repo.changelog
58 cl = repo.changelog
59 for l in lines:
59 for l in lines:
60 if not l:
60 if not l:
61 continue
61 continue
62 node, state, label = l.split(" ", 2)
62 node, state, label = l.split(" ", 2)
63 if state not in 'oc':
63 if state not in 'oc':
64 raise ValueError('invalid branch state')
64 raise ValueError('invalid branch state')
65 label = encoding.tolocal(label.strip())
65 label = encoding.tolocal(label.strip())
66 node = bin(node)
66 node = bin(node)
67 if not cl.hasnode(node):
67 if not cl.hasnode(node):
68 raise ValueError('node %s does not exist' % hex(node))
68 raise ValueError('node %s does not exist' % hex(node))
69 partial.setdefault(label, []).append(node)
69 partial.setdefault(label, []).append(node)
70 if state == 'c':
70 if state == 'c':
71 partial._closednodes.add(node)
71 partial._closednodes.add(node)
72 except KeyboardInterrupt:
72 except KeyboardInterrupt:
73 raise
73 raise
74 except Exception as inst:
74 except Exception as inst:
75 if repo.ui.debugflag:
75 if repo.ui.debugflag:
76 msg = 'invalid branchheads cache'
76 msg = 'invalid branchheads cache'
77 if repo.filtername is not None:
77 if repo.filtername is not None:
78 msg += ' (%s)' % repo.filtername
78 msg += ' (%s)' % repo.filtername
79 msg += ': %s\n'
79 msg += ': %s\n'
80 repo.ui.debug(msg % inst)
80 repo.ui.debug(msg % inst)
81 partial = None
81 partial = None
82 return partial
82 return partial
83
83
84 ### Nearest subset relation
84 ### Nearest subset relation
85 # Nearest subset of filter X is a filter Y so that:
85 # Nearest subset of filter X is a filter Y so that:
86 # * Y is included in X,
86 # * Y is included in X,
87 # * X - Y is as small as possible.
87 # * X - Y is as small as possible.
88 # This create and ordering used for branchmap purpose.
88 # This create and ordering used for branchmap purpose.
89 # the ordering may be partial
89 # the ordering may be partial
90 subsettable = {None: 'visible',
90 subsettable = {None: 'visible',
91 'visible': 'served',
91 'visible': 'served',
92 'served': 'immutable',
92 'served': 'immutable',
93 'immutable': 'base'}
93 'immutable': 'base'}
94
94
95 def updatecache(repo):
95 def updatecache(repo):
96 cl = repo.changelog
96 cl = repo.changelog
97 filtername = repo.filtername
97 filtername = repo.filtername
98 partial = repo._branchcaches.get(filtername)
98 partial = repo._branchcaches.get(filtername)
99
99
100 revs = []
100 revs = []
101 if partial is None or not partial.validfor(repo):
101 if partial is None or not partial.validfor(repo):
102 partial = read(repo)
102 partial = read(repo)
103 if partial is None:
103 if partial is None:
104 subsetname = subsettable.get(filtername)
104 subsetname = subsettable.get(filtername)
105 if subsetname is None:
105 if subsetname is None:
106 partial = branchcache()
106 partial = branchcache()
107 else:
107 else:
108 subset = repo.filtered(subsetname)
108 subset = repo.filtered(subsetname)
109 partial = subset.branchmap().copy()
109 partial = subset.branchmap().copy()
110 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
110 extrarevs = subset.changelog.filteredrevs - cl.filteredrevs
111 revs.extend(r for r in extrarevs if r <= partial.tiprev)
111 revs.extend(r for r in extrarevs if r <= partial.tiprev)
112 revs.extend(cl.revs(start=partial.tiprev + 1))
112 revs.extend(cl.revs(start=partial.tiprev + 1))
113 if revs:
113 if revs:
114 partial.update(repo, revs)
114 partial.update(repo, revs)
115 partial.write(repo)
115 partial.write(repo)
116
116
117 assert partial.validfor(repo), filtername
117 assert partial.validfor(repo), filtername
118 repo._branchcaches[repo.filtername] = partial
118 repo._branchcaches[repo.filtername] = partial
119
119
120 def replacecache(repo, bm):
120 def replacecache(repo, bm):
121 """Replace the branchmap cache for a repo with a branch mapping.
121 """Replace the branchmap cache for a repo with a branch mapping.
122
122
123 This is likely only called during clone with a branch map from a remote.
123 This is likely only called during clone with a branch map from a remote.
124 """
124 """
125 rbheads = []
125 rbheads = []
126 closed = []
126 closed = []
127 for bheads in bm.itervalues():
127 for bheads in bm.itervalues():
128 rbheads.extend(bheads)
128 rbheads.extend(bheads)
129 for h in bheads:
129 for h in bheads:
130 r = repo.changelog.rev(h)
130 r = repo.changelog.rev(h)
131 b, c = repo.changelog.branchinfo(r)
131 b, c = repo.changelog.branchinfo(r)
132 if c:
132 if c:
133 closed.append(h)
133 closed.append(h)
134
134
135 if rbheads:
135 if rbheads:
136 rtiprev = max((int(repo.changelog.rev(node))
136 rtiprev = max((int(repo.changelog.rev(node))
137 for node in rbheads))
137 for node in rbheads))
138 cache = branchcache(bm,
138 cache = branchcache(bm,
139 repo[rtiprev].node(),
139 repo[rtiprev].node(),
140 rtiprev,
140 rtiprev,
141 closednodes=closed)
141 closednodes=closed)
142
142
143 # Try to stick it as low as possible
143 # Try to stick it as low as possible
144 # filter above served are unlikely to be fetch from a clone
144 # filter above served are unlikely to be fetch from a clone
145 for candidate in ('base', 'immutable', 'served'):
145 for candidate in ('base', 'immutable', 'served'):
146 rview = repo.filtered(candidate)
146 rview = repo.filtered(candidate)
147 if cache.validfor(rview):
147 if cache.validfor(rview):
148 repo._branchcaches[candidate] = cache
148 repo._branchcaches[candidate] = cache
149 cache.write(rview)
149 cache.write(rview)
150 break
150 break
151
151
152 class branchcache(dict):
152 class branchcache(dict):
153 """A dict like object that hold branches heads cache.
153 """A dict like object that hold branches heads cache.
154
154
155 This cache is used to avoid costly computations to determine all the
155 This cache is used to avoid costly computations to determine all the
156 branch heads of a repo.
156 branch heads of a repo.
157
157
158 The cache is serialized on disk in the following format:
158 The cache is serialized on disk in the following format:
159
159
160 <tip hex node> <tip rev number> [optional filtered repo hex hash]
160 <tip hex node> <tip rev number> [optional filtered repo hex hash]
161 <branch head hex node> <open/closed state> <branch name>
161 <branch head hex node> <open/closed state> <branch name>
162 <branch head hex node> <open/closed state> <branch name>
162 <branch head hex node> <open/closed state> <branch name>
163 ...
163 ...
164
164
165 The first line is used to check if the cache is still valid. If the
165 The first line is used to check if the cache is still valid. If the
166 branch cache is for a filtered repo view, an optional third hash is
166 branch cache is for a filtered repo view, an optional third hash is
167 included that hashes the hashes of all filtered revisions.
167 included that hashes the hashes of all filtered revisions.
168
168
169 The open/closed state is represented by a single letter 'o' or 'c'.
169 The open/closed state is represented by a single letter 'o' or 'c'.
170 This field can be used to avoid changelog reads when determining if a
170 This field can be used to avoid changelog reads when determining if a
171 branch head closes a branch or not.
171 branch head closes a branch or not.
172 """
172 """
173
173
174 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
174 def __init__(self, entries=(), tipnode=nullid, tiprev=nullrev,
175 filteredhash=None, closednodes=None):
175 filteredhash=None, closednodes=None):
176 super(branchcache, self).__init__(entries)
176 super(branchcache, self).__init__(entries)
177 self.tipnode = tipnode
177 self.tipnode = tipnode
178 self.tiprev = tiprev
178 self.tiprev = tiprev
179 self.filteredhash = filteredhash
179 self.filteredhash = filteredhash
180 # closednodes is a set of nodes that close their branch. If the branch
180 # closednodes is a set of nodes that close their branch. If the branch
181 # cache has been updated, it may contain nodes that are no longer
181 # cache has been updated, it may contain nodes that are no longer
182 # heads.
182 # heads.
183 if closednodes is None:
183 if closednodes is None:
184 self._closednodes = set()
184 self._closednodes = set()
185 else:
185 else:
186 self._closednodes = closednodes
186 self._closednodes = closednodes
187
187
188 def validfor(self, repo):
188 def validfor(self, repo):
189 """Is the cache content valid regarding a repo
189 """Is the cache content valid regarding a repo
190
190
191 - False when cached tipnode is unknown or if we detect a strip.
191 - False when cached tipnode is unknown or if we detect a strip.
192 - True when cache is up to date or a subset of current repo."""
192 - True when cache is up to date or a subset of current repo."""
193 try:
193 try:
194 return ((self.tipnode == repo.changelog.node(self.tiprev))
194 return ((self.tipnode == repo.changelog.node(self.tiprev))
195 and (self.filteredhash == \
195 and (self.filteredhash == \
196 scmutil.filteredhash(repo, self.tiprev)))
196 scmutil.filteredhash(repo, self.tiprev)))
197 except IndexError:
197 except IndexError:
198 return False
198 return False
199
199
200 def _branchtip(self, heads):
200 def _branchtip(self, heads):
201 '''Return tuple with last open head in heads and false,
201 '''Return tuple with last open head in heads and false,
202 otherwise return last closed head and true.'''
202 otherwise return last closed head and true.'''
203 tip = heads[-1]
203 tip = heads[-1]
204 closed = True
204 closed = True
205 for h in reversed(heads):
205 for h in reversed(heads):
206 if h not in self._closednodes:
206 if h not in self._closednodes:
207 tip = h
207 tip = h
208 closed = False
208 closed = False
209 break
209 break
210 return tip, closed
210 return tip, closed
211
211
212 def branchtip(self, branch):
212 def branchtip(self, branch):
213 '''Return the tipmost open head on branch head, otherwise return the
213 '''Return the tipmost open head on branch head, otherwise return the
214 tipmost closed head on branch.
214 tipmost closed head on branch.
215 Raise KeyError for unknown branch.'''
215 Raise KeyError for unknown branch.'''
216 return self._branchtip(self[branch])[0]
216 return self._branchtip(self[branch])[0]
217
217
218 def branchheads(self, branch, closed=False):
218 def branchheads(self, branch, closed=False):
219 heads = self[branch]
219 heads = self[branch]
220 if not closed:
220 if not closed:
221 heads = [h for h in heads if h not in self._closednodes]
221 heads = [h for h in heads if h not in self._closednodes]
222 return heads
222 return heads
223
223
224 def iterbranches(self):
224 def iterbranches(self):
225 for bn, heads in self.iteritems():
225 for bn, heads in self.iteritems():
226 yield (bn, heads) + self._branchtip(heads)
226 yield (bn, heads) + self._branchtip(heads)
227
227
228 def copy(self):
228 def copy(self):
229 """return an deep copy of the branchcache object"""
229 """return an deep copy of the branchcache object"""
230 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
230 return branchcache(self, self.tipnode, self.tiprev, self.filteredhash,
231 self._closednodes)
231 self._closednodes)
232
232
233 def write(self, repo):
233 def write(self, repo):
234 try:
234 try:
235 f = repo.vfs(_filename(repo), "w", atomictemp=True)
235 f = repo.vfs(_filename(repo), "w", atomictemp=True)
236 cachekey = [hex(self.tipnode), str(self.tiprev)]
236 cachekey = [hex(self.tipnode), str(self.tiprev)]
237 if self.filteredhash is not None:
237 if self.filteredhash is not None:
238 cachekey.append(hex(self.filteredhash))
238 cachekey.append(hex(self.filteredhash))
239 f.write(" ".join(cachekey) + '\n')
239 f.write(" ".join(cachekey) + '\n')
240 nodecount = 0
240 nodecount = 0
241 for label, nodes in sorted(self.iteritems()):
241 for label, nodes in sorted(self.iteritems()):
242 for node in nodes:
242 for node in nodes:
243 nodecount += 1
243 nodecount += 1
244 if node in self._closednodes:
244 if node in self._closednodes:
245 state = 'c'
245 state = 'c'
246 else:
246 else:
247 state = 'o'
247 state = 'o'
248 f.write("%s %s %s\n" % (hex(node), state,
248 f.write("%s %s %s\n" % (hex(node), state,
249 encoding.fromlocal(label)))
249 encoding.fromlocal(label)))
250 f.close()
250 f.close()
251 repo.ui.log('branchcache',
251 repo.ui.log('branchcache',
252 'wrote %s branch cache with %d labels and %d nodes\n',
252 'wrote %s branch cache with %d labels and %d nodes\n',
253 repo.filtername, len(self), nodecount)
253 repo.filtername, len(self), nodecount)
254 except (IOError, OSError, error.Abort) as inst:
254 except (IOError, OSError, error.Abort) as inst:
255 repo.ui.debug("couldn't write branch cache: %s\n" % inst)
255 repo.ui.debug("couldn't write branch cache: %s\n" % inst)
256 # Abort may be raise by read only opener
256 # Abort may be raise by read only opener
257 pass
257 pass
258
258
259 def update(self, repo, revgen):
259 def update(self, repo, revgen):
260 """Given a branchhead cache, self, that may have extra nodes or be
260 """Given a branchhead cache, self, that may have extra nodes or be
261 missing heads, and a generator of nodes that are strictly a superset of
261 missing heads, and a generator of nodes that are strictly a superset of
262 heads missing, this function updates self to be correct.
262 heads missing, this function updates self to be correct.
263 """
263 """
264 starttime = time.time()
264 starttime = util.timer()
265 cl = repo.changelog
265 cl = repo.changelog
266 # collect new branch entries
266 # collect new branch entries
267 newbranches = {}
267 newbranches = {}
268 getbranchinfo = repo.revbranchcache().branchinfo
268 getbranchinfo = repo.revbranchcache().branchinfo
269 for r in revgen:
269 for r in revgen:
270 branch, closesbranch = getbranchinfo(r)
270 branch, closesbranch = getbranchinfo(r)
271 newbranches.setdefault(branch, []).append(r)
271 newbranches.setdefault(branch, []).append(r)
272 if closesbranch:
272 if closesbranch:
273 self._closednodes.add(cl.node(r))
273 self._closednodes.add(cl.node(r))
274
274
275 # fetch current topological heads to speed up filtering
275 # fetch current topological heads to speed up filtering
276 topoheads = set(cl.headrevs())
276 topoheads = set(cl.headrevs())
277
277
278 # if older branchheads are reachable from new ones, they aren't
278 # if older branchheads are reachable from new ones, they aren't
279 # really branchheads. Note checking parents is insufficient:
279 # really branchheads. Note checking parents is insufficient:
280 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
280 # 1 (branch a) -> 2 (branch b) -> 3 (branch a)
281 for branch, newheadrevs in newbranches.iteritems():
281 for branch, newheadrevs in newbranches.iteritems():
282 bheads = self.setdefault(branch, [])
282 bheads = self.setdefault(branch, [])
283 bheadset = set(cl.rev(node) for node in bheads)
283 bheadset = set(cl.rev(node) for node in bheads)
284
284
285 # This have been tested True on all internal usage of this function.
285 # This have been tested True on all internal usage of this function.
286 # run it again in case of doubt
286 # run it again in case of doubt
287 # assert not (set(bheadrevs) & set(newheadrevs))
287 # assert not (set(bheadrevs) & set(newheadrevs))
288 newheadrevs.sort()
288 newheadrevs.sort()
289 bheadset.update(newheadrevs)
289 bheadset.update(newheadrevs)
290
290
291 # This prunes out two kinds of heads - heads that are superseded by
291 # This prunes out two kinds of heads - heads that are superseded by
292 # a head in newheadrevs, and newheadrevs that are not heads because
292 # a head in newheadrevs, and newheadrevs that are not heads because
293 # an existing head is their descendant.
293 # an existing head is their descendant.
294 uncertain = bheadset - topoheads
294 uncertain = bheadset - topoheads
295 if uncertain:
295 if uncertain:
296 floorrev = min(uncertain)
296 floorrev = min(uncertain)
297 ancestors = set(cl.ancestors(newheadrevs, floorrev))
297 ancestors = set(cl.ancestors(newheadrevs, floorrev))
298 bheadset -= ancestors
298 bheadset -= ancestors
299 bheadrevs = sorted(bheadset)
299 bheadrevs = sorted(bheadset)
300 self[branch] = [cl.node(rev) for rev in bheadrevs]
300 self[branch] = [cl.node(rev) for rev in bheadrevs]
301 tiprev = bheadrevs[-1]
301 tiprev = bheadrevs[-1]
302 if tiprev > self.tiprev:
302 if tiprev > self.tiprev:
303 self.tipnode = cl.node(tiprev)
303 self.tipnode = cl.node(tiprev)
304 self.tiprev = tiprev
304 self.tiprev = tiprev
305
305
306 if not self.validfor(repo):
306 if not self.validfor(repo):
307 # cache key are not valid anymore
307 # cache key are not valid anymore
308 self.tipnode = nullid
308 self.tipnode = nullid
309 self.tiprev = nullrev
309 self.tiprev = nullrev
310 for heads in self.values():
310 for heads in self.values():
311 tiprev = max(cl.rev(node) for node in heads)
311 tiprev = max(cl.rev(node) for node in heads)
312 if tiprev > self.tiprev:
312 if tiprev > self.tiprev:
313 self.tipnode = cl.node(tiprev)
313 self.tipnode = cl.node(tiprev)
314 self.tiprev = tiprev
314 self.tiprev = tiprev
315 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
315 self.filteredhash = scmutil.filteredhash(repo, self.tiprev)
316
316
317 duration = time.time() - starttime
317 duration = util.timer() - starttime
318 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
318 repo.ui.log('branchcache', 'updated %s branch cache in %.4f seconds\n',
319 repo.filtername, duration)
319 repo.filtername, duration)
320
320
321 # Revision branch info cache
321 # Revision branch info cache
322
322
323 _rbcversion = '-v1'
323 _rbcversion = '-v1'
324 _rbcnames = 'cache/rbc-names' + _rbcversion
324 _rbcnames = 'cache/rbc-names' + _rbcversion
325 _rbcrevs = 'cache/rbc-revs' + _rbcversion
325 _rbcrevs = 'cache/rbc-revs' + _rbcversion
326 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
326 # [4 byte hash prefix][4 byte branch name number with sign bit indicating open]
327 _rbcrecfmt = '>4sI'
327 _rbcrecfmt = '>4sI'
328 _rbcrecsize = calcsize(_rbcrecfmt)
328 _rbcrecsize = calcsize(_rbcrecfmt)
329 _rbcnodelen = 4
329 _rbcnodelen = 4
330 _rbcbranchidxmask = 0x7fffffff
330 _rbcbranchidxmask = 0x7fffffff
331 _rbccloseflag = 0x80000000
331 _rbccloseflag = 0x80000000
332
332
333 class revbranchcache(object):
333 class revbranchcache(object):
334 """Persistent cache, mapping from revision number to branch name and close.
334 """Persistent cache, mapping from revision number to branch name and close.
335 This is a low level cache, independent of filtering.
335 This is a low level cache, independent of filtering.
336
336
337 Branch names are stored in rbc-names in internal encoding separated by 0.
337 Branch names are stored in rbc-names in internal encoding separated by 0.
338 rbc-names is append-only, and each branch name is only stored once and will
338 rbc-names is append-only, and each branch name is only stored once and will
339 thus have a unique index.
339 thus have a unique index.
340
340
341 The branch info for each revision is stored in rbc-revs as constant size
341 The branch info for each revision is stored in rbc-revs as constant size
342 records. The whole file is read into memory, but it is only 'parsed' on
342 records. The whole file is read into memory, but it is only 'parsed' on
343 demand. The file is usually append-only but will be truncated if repo
343 demand. The file is usually append-only but will be truncated if repo
344 modification is detected.
344 modification is detected.
345 The record for each revision contains the first 4 bytes of the
345 The record for each revision contains the first 4 bytes of the
346 corresponding node hash, and the record is only used if it still matches.
346 corresponding node hash, and the record is only used if it still matches.
347 Even a completely trashed rbc-revs fill thus still give the right result
347 Even a completely trashed rbc-revs fill thus still give the right result
348 while converging towards full recovery ... assuming no incorrectly matching
348 while converging towards full recovery ... assuming no incorrectly matching
349 node hashes.
349 node hashes.
350 The record also contains 4 bytes where 31 bits contains the index of the
350 The record also contains 4 bytes where 31 bits contains the index of the
351 branch and the last bit indicate that it is a branch close commit.
351 branch and the last bit indicate that it is a branch close commit.
352 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
352 The usage pattern for rbc-revs is thus somewhat similar to 00changelog.i
353 and will grow with it but be 1/8th of its size.
353 and will grow with it but be 1/8th of its size.
354 """
354 """
355
355
356 def __init__(self, repo, readonly=True):
356 def __init__(self, repo, readonly=True):
357 assert repo.filtername is None
357 assert repo.filtername is None
358 self._repo = repo
358 self._repo = repo
359 self._names = [] # branch names in local encoding with static index
359 self._names = [] # branch names in local encoding with static index
360 self._rbcrevs = array('c') # structs of type _rbcrecfmt
360 self._rbcrevs = array('c') # structs of type _rbcrecfmt
361 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
361 self._rbcsnameslen = 0 # length of names read at _rbcsnameslen
362 try:
362 try:
363 bndata = repo.vfs.read(_rbcnames)
363 bndata = repo.vfs.read(_rbcnames)
364 self._rbcsnameslen = len(bndata) # for verification before writing
364 self._rbcsnameslen = len(bndata) # for verification before writing
365 self._names = [encoding.tolocal(bn) for bn in bndata.split('\0')]
365 self._names = [encoding.tolocal(bn) for bn in bndata.split('\0')]
366 except (IOError, OSError):
366 except (IOError, OSError):
367 if readonly:
367 if readonly:
368 # don't try to use cache - fall back to the slow path
368 # don't try to use cache - fall back to the slow path
369 self.branchinfo = self._branchinfo
369 self.branchinfo = self._branchinfo
370
370
371 if self._names:
371 if self._names:
372 try:
372 try:
373 data = repo.vfs.read(_rbcrevs)
373 data = repo.vfs.read(_rbcrevs)
374 self._rbcrevs.fromstring(data)
374 self._rbcrevs.fromstring(data)
375 except (IOError, OSError) as inst:
375 except (IOError, OSError) as inst:
376 repo.ui.debug("couldn't read revision branch cache: %s\n" %
376 repo.ui.debug("couldn't read revision branch cache: %s\n" %
377 inst)
377 inst)
378 # remember number of good records on disk
378 # remember number of good records on disk
379 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
379 self._rbcrevslen = min(len(self._rbcrevs) // _rbcrecsize,
380 len(repo.changelog))
380 len(repo.changelog))
381 if self._rbcrevslen == 0:
381 if self._rbcrevslen == 0:
382 self._names = []
382 self._names = []
383 self._rbcnamescount = len(self._names) # number of names read at
383 self._rbcnamescount = len(self._names) # number of names read at
384 # _rbcsnameslen
384 # _rbcsnameslen
385 self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
385 self._namesreverse = dict((b, r) for r, b in enumerate(self._names))
386
386
387 def _clear(self):
387 def _clear(self):
388 self._rbcsnameslen = 0
388 self._rbcsnameslen = 0
389 del self._names[:]
389 del self._names[:]
390 self._rbcnamescount = 0
390 self._rbcnamescount = 0
391 self._namesreverse.clear()
391 self._namesreverse.clear()
392 self._rbcrevslen = len(self._repo.changelog)
392 self._rbcrevslen = len(self._repo.changelog)
393 self._rbcrevs = array('c')
393 self._rbcrevs = array('c')
394 self._rbcrevs.fromstring('\0' * (self._rbcrevslen * _rbcrecsize))
394 self._rbcrevs.fromstring('\0' * (self._rbcrevslen * _rbcrecsize))
395
395
396 def branchinfo(self, rev):
396 def branchinfo(self, rev):
397 """Return branch name and close flag for rev, using and updating
397 """Return branch name and close flag for rev, using and updating
398 persistent cache."""
398 persistent cache."""
399 changelog = self._repo.changelog
399 changelog = self._repo.changelog
400 rbcrevidx = rev * _rbcrecsize
400 rbcrevidx = rev * _rbcrecsize
401
401
402 # avoid negative index, changelog.read(nullrev) is fast without cache
402 # avoid negative index, changelog.read(nullrev) is fast without cache
403 if rev == nullrev:
403 if rev == nullrev:
404 return changelog.branchinfo(rev)
404 return changelog.branchinfo(rev)
405
405
406 # if requested rev isn't allocated, grow and cache the rev info
406 # if requested rev isn't allocated, grow and cache the rev info
407 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
407 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
408 return self._branchinfo(rev)
408 return self._branchinfo(rev)
409
409
410 # fast path: extract data from cache, use it if node is matching
410 # fast path: extract data from cache, use it if node is matching
411 reponode = changelog.node(rev)[:_rbcnodelen]
411 reponode = changelog.node(rev)[:_rbcnodelen]
412 cachenode, branchidx = unpack(
412 cachenode, branchidx = unpack(
413 _rbcrecfmt, buffer(self._rbcrevs, rbcrevidx, _rbcrecsize))
413 _rbcrecfmt, buffer(self._rbcrevs, rbcrevidx, _rbcrecsize))
414 close = bool(branchidx & _rbccloseflag)
414 close = bool(branchidx & _rbccloseflag)
415 if close:
415 if close:
416 branchidx &= _rbcbranchidxmask
416 branchidx &= _rbcbranchidxmask
417 if cachenode == '\0\0\0\0':
417 if cachenode == '\0\0\0\0':
418 pass
418 pass
419 elif cachenode == reponode:
419 elif cachenode == reponode:
420 try:
420 try:
421 return self._names[branchidx], close
421 return self._names[branchidx], close
422 except IndexError:
422 except IndexError:
423 # recover from invalid reference to unknown branch
423 # recover from invalid reference to unknown branch
424 self._repo.ui.debug("referenced branch names not found"
424 self._repo.ui.debug("referenced branch names not found"
425 " - rebuilding revision branch cache from scratch\n")
425 " - rebuilding revision branch cache from scratch\n")
426 self._clear()
426 self._clear()
427 else:
427 else:
428 # rev/node map has changed, invalidate the cache from here up
428 # rev/node map has changed, invalidate the cache from here up
429 self._repo.ui.debug("history modification detected - truncating "
429 self._repo.ui.debug("history modification detected - truncating "
430 "revision branch cache to revision %s\n" % rev)
430 "revision branch cache to revision %s\n" % rev)
431 truncate = rbcrevidx + _rbcrecsize
431 truncate = rbcrevidx + _rbcrecsize
432 del self._rbcrevs[truncate:]
432 del self._rbcrevs[truncate:]
433 self._rbcrevslen = min(self._rbcrevslen, truncate)
433 self._rbcrevslen = min(self._rbcrevslen, truncate)
434
434
435 # fall back to slow path and make sure it will be written to disk
435 # fall back to slow path and make sure it will be written to disk
436 return self._branchinfo(rev)
436 return self._branchinfo(rev)
437
437
438 def _branchinfo(self, rev):
438 def _branchinfo(self, rev):
439 """Retrieve branch info from changelog and update _rbcrevs"""
439 """Retrieve branch info from changelog and update _rbcrevs"""
440 changelog = self._repo.changelog
440 changelog = self._repo.changelog
441 b, close = changelog.branchinfo(rev)
441 b, close = changelog.branchinfo(rev)
442 if b in self._namesreverse:
442 if b in self._namesreverse:
443 branchidx = self._namesreverse[b]
443 branchidx = self._namesreverse[b]
444 else:
444 else:
445 branchidx = len(self._names)
445 branchidx = len(self._names)
446 self._names.append(b)
446 self._names.append(b)
447 self._namesreverse[b] = branchidx
447 self._namesreverse[b] = branchidx
448 reponode = changelog.node(rev)
448 reponode = changelog.node(rev)
449 if close:
449 if close:
450 branchidx |= _rbccloseflag
450 branchidx |= _rbccloseflag
451 self._setcachedata(rev, reponode, branchidx)
451 self._setcachedata(rev, reponode, branchidx)
452 return b, close
452 return b, close
453
453
454 def _setcachedata(self, rev, node, branchidx):
454 def _setcachedata(self, rev, node, branchidx):
455 """Writes the node's branch data to the in-memory cache data."""
455 """Writes the node's branch data to the in-memory cache data."""
456 rbcrevidx = rev * _rbcrecsize
456 rbcrevidx = rev * _rbcrecsize
457 rec = array('c')
457 rec = array('c')
458 rec.fromstring(pack(_rbcrecfmt, node, branchidx))
458 rec.fromstring(pack(_rbcrecfmt, node, branchidx))
459 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
459 if len(self._rbcrevs) < rbcrevidx + _rbcrecsize:
460 self._rbcrevs.extend('\0' *
460 self._rbcrevs.extend('\0' *
461 (len(self._repo.changelog) * _rbcrecsize -
461 (len(self._repo.changelog) * _rbcrecsize -
462 len(self._rbcrevs)))
462 len(self._rbcrevs)))
463 self._rbcrevs[rbcrevidx:rbcrevidx + _rbcrecsize] = rec
463 self._rbcrevs[rbcrevidx:rbcrevidx + _rbcrecsize] = rec
464 self._rbcrevslen = min(self._rbcrevslen, rev)
464 self._rbcrevslen = min(self._rbcrevslen, rev)
465
465
466 tr = self._repo.currenttransaction()
466 tr = self._repo.currenttransaction()
467 if tr:
467 if tr:
468 tr.addfinalize('write-revbranchcache', self.write)
468 tr.addfinalize('write-revbranchcache', self.write)
469
469
470 def write(self, tr=None):
470 def write(self, tr=None):
471 """Save branch cache if it is dirty."""
471 """Save branch cache if it is dirty."""
472 repo = self._repo
472 repo = self._repo
473 wlock = None
473 wlock = None
474 step = ''
474 step = ''
475 try:
475 try:
476 if self._rbcnamescount < len(self._names):
476 if self._rbcnamescount < len(self._names):
477 step = ' names'
477 step = ' names'
478 wlock = repo.wlock(wait=False)
478 wlock = repo.wlock(wait=False)
479 if self._rbcnamescount != 0:
479 if self._rbcnamescount != 0:
480 f = repo.vfs.open(_rbcnames, 'ab')
480 f = repo.vfs.open(_rbcnames, 'ab')
481 if f.tell() == self._rbcsnameslen:
481 if f.tell() == self._rbcsnameslen:
482 f.write('\0')
482 f.write('\0')
483 else:
483 else:
484 f.close()
484 f.close()
485 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
485 repo.ui.debug("%s changed - rewriting it\n" % _rbcnames)
486 self._rbcnamescount = 0
486 self._rbcnamescount = 0
487 self._rbcrevslen = 0
487 self._rbcrevslen = 0
488 if self._rbcnamescount == 0:
488 if self._rbcnamescount == 0:
489 # before rewriting names, make sure references are removed
489 # before rewriting names, make sure references are removed
490 repo.vfs.unlinkpath(_rbcrevs, ignoremissing=True)
490 repo.vfs.unlinkpath(_rbcrevs, ignoremissing=True)
491 f = repo.vfs.open(_rbcnames, 'wb')
491 f = repo.vfs.open(_rbcnames, 'wb')
492 f.write('\0'.join(encoding.fromlocal(b)
492 f.write('\0'.join(encoding.fromlocal(b)
493 for b in self._names[self._rbcnamescount:]))
493 for b in self._names[self._rbcnamescount:]))
494 self._rbcsnameslen = f.tell()
494 self._rbcsnameslen = f.tell()
495 f.close()
495 f.close()
496 self._rbcnamescount = len(self._names)
496 self._rbcnamescount = len(self._names)
497
497
498 start = self._rbcrevslen * _rbcrecsize
498 start = self._rbcrevslen * _rbcrecsize
499 if start != len(self._rbcrevs):
499 if start != len(self._rbcrevs):
500 step = ''
500 step = ''
501 if wlock is None:
501 if wlock is None:
502 wlock = repo.wlock(wait=False)
502 wlock = repo.wlock(wait=False)
503 revs = min(len(repo.changelog),
503 revs = min(len(repo.changelog),
504 len(self._rbcrevs) // _rbcrecsize)
504 len(self._rbcrevs) // _rbcrecsize)
505 f = repo.vfs.open(_rbcrevs, 'ab')
505 f = repo.vfs.open(_rbcrevs, 'ab')
506 if f.tell() != start:
506 if f.tell() != start:
507 repo.ui.debug("truncating %s to %s\n" % (_rbcrevs, start))
507 repo.ui.debug("truncating %s to %s\n" % (_rbcrevs, start))
508 f.seek(start)
508 f.seek(start)
509 if f.tell() != start:
509 if f.tell() != start:
510 start = 0
510 start = 0
511 f.seek(start)
511 f.seek(start)
512 f.truncate()
512 f.truncate()
513 end = revs * _rbcrecsize
513 end = revs * _rbcrecsize
514 f.write(self._rbcrevs[start:end])
514 f.write(self._rbcrevs[start:end])
515 f.close()
515 f.close()
516 self._rbcrevslen = revs
516 self._rbcrevslen = revs
517 except (IOError, OSError, error.Abort, error.LockError) as inst:
517 except (IOError, OSError, error.Abort, error.LockError) as inst:
518 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
518 repo.ui.debug("couldn't write revision branch cache%s: %s\n"
519 % (step, inst))
519 % (step, inst))
520 finally:
520 finally:
521 if wlock is not None:
521 if wlock is not None:
522 wlock.release()
522 wlock.release()
@@ -1,889 +1,889 b''
1 # dispatch.py - command dispatching for mercurial
1 # dispatch.py - command dispatching for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 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, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import atexit
10 import atexit
11 import difflib
11 import difflib
12 import errno
12 import errno
13 import getopt
13 import getopt
14 import os
14 import os
15 import pdb
15 import pdb
16 import re
16 import re
17 import signal
17 import signal
18 import sys
18 import sys
19 import time
19 import time
20 import traceback
20 import traceback
21
21
22
22
23 from .i18n import _
23 from .i18n import _
24
24
25 from . import (
25 from . import (
26 cmdutil,
26 cmdutil,
27 color,
27 color,
28 commands,
28 commands,
29 debugcommands,
29 debugcommands,
30 demandimport,
30 demandimport,
31 encoding,
31 encoding,
32 error,
32 error,
33 extensions,
33 extensions,
34 fancyopts,
34 fancyopts,
35 fileset,
35 fileset,
36 hg,
36 hg,
37 hook,
37 hook,
38 profiling,
38 profiling,
39 pycompat,
39 pycompat,
40 revset,
40 revset,
41 scmutil,
41 scmutil,
42 templatefilters,
42 templatefilters,
43 templatekw,
43 templatekw,
44 templater,
44 templater,
45 ui as uimod,
45 ui as uimod,
46 util,
46 util,
47 )
47 )
48
48
49 class request(object):
49 class request(object):
50 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
50 def __init__(self, args, ui=None, repo=None, fin=None, fout=None,
51 ferr=None):
51 ferr=None):
52 self.args = args
52 self.args = args
53 self.ui = ui
53 self.ui = ui
54 self.repo = repo
54 self.repo = repo
55
55
56 # input/output/error streams
56 # input/output/error streams
57 self.fin = fin
57 self.fin = fin
58 self.fout = fout
58 self.fout = fout
59 self.ferr = ferr
59 self.ferr = ferr
60
60
61 def run():
61 def run():
62 "run the command in sys.argv"
62 "run the command in sys.argv"
63 sys.exit((dispatch(request(pycompat.sysargv[1:])) or 0) & 255)
63 sys.exit((dispatch(request(pycompat.sysargv[1:])) or 0) & 255)
64
64
65 def _getsimilar(symbols, value):
65 def _getsimilar(symbols, value):
66 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
66 sim = lambda x: difflib.SequenceMatcher(None, value, x).ratio()
67 # The cutoff for similarity here is pretty arbitrary. It should
67 # The cutoff for similarity here is pretty arbitrary. It should
68 # probably be investigated and tweaked.
68 # probably be investigated and tweaked.
69 return [s for s in symbols if sim(s) > 0.6]
69 return [s for s in symbols if sim(s) > 0.6]
70
70
71 def _reportsimilar(write, similar):
71 def _reportsimilar(write, similar):
72 if len(similar) == 1:
72 if len(similar) == 1:
73 write(_("(did you mean %s?)\n") % similar[0])
73 write(_("(did you mean %s?)\n") % similar[0])
74 elif similar:
74 elif similar:
75 ss = ", ".join(sorted(similar))
75 ss = ", ".join(sorted(similar))
76 write(_("(did you mean one of %s?)\n") % ss)
76 write(_("(did you mean one of %s?)\n") % ss)
77
77
78 def _formatparse(write, inst):
78 def _formatparse(write, inst):
79 similar = []
79 similar = []
80 if isinstance(inst, error.UnknownIdentifier):
80 if isinstance(inst, error.UnknownIdentifier):
81 # make sure to check fileset first, as revset can invoke fileset
81 # make sure to check fileset first, as revset can invoke fileset
82 similar = _getsimilar(inst.symbols, inst.function)
82 similar = _getsimilar(inst.symbols, inst.function)
83 if len(inst.args) > 1:
83 if len(inst.args) > 1:
84 write(_("hg: parse error at %s: %s\n") %
84 write(_("hg: parse error at %s: %s\n") %
85 (inst.args[1], inst.args[0]))
85 (inst.args[1], inst.args[0]))
86 if (inst.args[0][0] == ' '):
86 if (inst.args[0][0] == ' '):
87 write(_("unexpected leading whitespace\n"))
87 write(_("unexpected leading whitespace\n"))
88 else:
88 else:
89 write(_("hg: parse error: %s\n") % inst.args[0])
89 write(_("hg: parse error: %s\n") % inst.args[0])
90 _reportsimilar(write, similar)
90 _reportsimilar(write, similar)
91 if inst.hint:
91 if inst.hint:
92 write(_("(%s)\n") % inst.hint)
92 write(_("(%s)\n") % inst.hint)
93
93
94 def dispatch(req):
94 def dispatch(req):
95 "run the command specified in req.args"
95 "run the command specified in req.args"
96 if req.ferr:
96 if req.ferr:
97 ferr = req.ferr
97 ferr = req.ferr
98 elif req.ui:
98 elif req.ui:
99 ferr = req.ui.ferr
99 ferr = req.ui.ferr
100 else:
100 else:
101 ferr = util.stderr
101 ferr = util.stderr
102
102
103 try:
103 try:
104 if not req.ui:
104 if not req.ui:
105 req.ui = uimod.ui.load()
105 req.ui = uimod.ui.load()
106 if '--traceback' in req.args:
106 if '--traceback' in req.args:
107 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
107 req.ui.setconfig('ui', 'traceback', 'on', '--traceback')
108
108
109 # set ui streams from the request
109 # set ui streams from the request
110 if req.fin:
110 if req.fin:
111 req.ui.fin = req.fin
111 req.ui.fin = req.fin
112 if req.fout:
112 if req.fout:
113 req.ui.fout = req.fout
113 req.ui.fout = req.fout
114 if req.ferr:
114 if req.ferr:
115 req.ui.ferr = req.ferr
115 req.ui.ferr = req.ferr
116 except error.Abort as inst:
116 except error.Abort as inst:
117 ferr.write(_("abort: %s\n") % inst)
117 ferr.write(_("abort: %s\n") % inst)
118 if inst.hint:
118 if inst.hint:
119 ferr.write(_("(%s)\n") % inst.hint)
119 ferr.write(_("(%s)\n") % inst.hint)
120 return -1
120 return -1
121 except error.ParseError as inst:
121 except error.ParseError as inst:
122 _formatparse(ferr.write, inst)
122 _formatparse(ferr.write, inst)
123 return -1
123 return -1
124
124
125 msg = ' '.join(' ' in a and repr(a) or a for a in req.args)
125 msg = ' '.join(' ' in a and repr(a) or a for a in req.args)
126 starttime = time.time()
126 starttime = util.timer()
127 ret = None
127 ret = None
128 try:
128 try:
129 ret = _runcatch(req)
129 ret = _runcatch(req)
130 except KeyboardInterrupt:
130 except KeyboardInterrupt:
131 try:
131 try:
132 req.ui.warn(_("interrupted!\n"))
132 req.ui.warn(_("interrupted!\n"))
133 except IOError as inst:
133 except IOError as inst:
134 if inst.errno != errno.EPIPE:
134 if inst.errno != errno.EPIPE:
135 raise
135 raise
136 ret = -1
136 ret = -1
137 finally:
137 finally:
138 duration = time.time() - starttime
138 duration = util.timer() - starttime
139 req.ui.flush()
139 req.ui.flush()
140 req.ui.log("commandfinish", "%s exited %s after %0.2f seconds\n",
140 req.ui.log("commandfinish", "%s exited %s after %0.2f seconds\n",
141 msg, ret or 0, duration)
141 msg, ret or 0, duration)
142 return ret
142 return ret
143
143
144 def _runcatch(req):
144 def _runcatch(req):
145 def catchterm(*args):
145 def catchterm(*args):
146 raise error.SignalInterrupt
146 raise error.SignalInterrupt
147
147
148 ui = req.ui
148 ui = req.ui
149 try:
149 try:
150 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
150 for name in 'SIGBREAK', 'SIGHUP', 'SIGTERM':
151 num = getattr(signal, name, None)
151 num = getattr(signal, name, None)
152 if num:
152 if num:
153 signal.signal(num, catchterm)
153 signal.signal(num, catchterm)
154 except ValueError:
154 except ValueError:
155 pass # happens if called in a thread
155 pass # happens if called in a thread
156
156
157 def _runcatchfunc():
157 def _runcatchfunc():
158 try:
158 try:
159 debugger = 'pdb'
159 debugger = 'pdb'
160 debugtrace = {
160 debugtrace = {
161 'pdb' : pdb.set_trace
161 'pdb' : pdb.set_trace
162 }
162 }
163 debugmortem = {
163 debugmortem = {
164 'pdb' : pdb.post_mortem
164 'pdb' : pdb.post_mortem
165 }
165 }
166
166
167 # read --config before doing anything else
167 # read --config before doing anything else
168 # (e.g. to change trust settings for reading .hg/hgrc)
168 # (e.g. to change trust settings for reading .hg/hgrc)
169 cfgs = _parseconfig(req.ui, _earlygetopt(['--config'], req.args))
169 cfgs = _parseconfig(req.ui, _earlygetopt(['--config'], req.args))
170
170
171 if req.repo:
171 if req.repo:
172 # copy configs that were passed on the cmdline (--config) to
172 # copy configs that were passed on the cmdline (--config) to
173 # the repo ui
173 # the repo ui
174 for sec, name, val in cfgs:
174 for sec, name, val in cfgs:
175 req.repo.ui.setconfig(sec, name, val, source='--config')
175 req.repo.ui.setconfig(sec, name, val, source='--config')
176
176
177 # developer config: ui.debugger
177 # developer config: ui.debugger
178 debugger = ui.config("ui", "debugger")
178 debugger = ui.config("ui", "debugger")
179 debugmod = pdb
179 debugmod = pdb
180 if not debugger or ui.plain():
180 if not debugger or ui.plain():
181 # if we are in HGPLAIN mode, then disable custom debugging
181 # if we are in HGPLAIN mode, then disable custom debugging
182 debugger = 'pdb'
182 debugger = 'pdb'
183 elif '--debugger' in req.args:
183 elif '--debugger' in req.args:
184 # This import can be slow for fancy debuggers, so only
184 # This import can be slow for fancy debuggers, so only
185 # do it when absolutely necessary, i.e. when actual
185 # do it when absolutely necessary, i.e. when actual
186 # debugging has been requested
186 # debugging has been requested
187 with demandimport.deactivated():
187 with demandimport.deactivated():
188 try:
188 try:
189 debugmod = __import__(debugger)
189 debugmod = __import__(debugger)
190 except ImportError:
190 except ImportError:
191 pass # Leave debugmod = pdb
191 pass # Leave debugmod = pdb
192
192
193 debugtrace[debugger] = debugmod.set_trace
193 debugtrace[debugger] = debugmod.set_trace
194 debugmortem[debugger] = debugmod.post_mortem
194 debugmortem[debugger] = debugmod.post_mortem
195
195
196 # enter the debugger before command execution
196 # enter the debugger before command execution
197 if '--debugger' in req.args:
197 if '--debugger' in req.args:
198 ui.warn(_("entering debugger - "
198 ui.warn(_("entering debugger - "
199 "type c to continue starting hg or h for help\n"))
199 "type c to continue starting hg or h for help\n"))
200
200
201 if (debugger != 'pdb' and
201 if (debugger != 'pdb' and
202 debugtrace[debugger] == debugtrace['pdb']):
202 debugtrace[debugger] == debugtrace['pdb']):
203 ui.warn(_("%s debugger specified "
203 ui.warn(_("%s debugger specified "
204 "but its module was not found\n") % debugger)
204 "but its module was not found\n") % debugger)
205 with demandimport.deactivated():
205 with demandimport.deactivated():
206 debugtrace[debugger]()
206 debugtrace[debugger]()
207 try:
207 try:
208 return _dispatch(req)
208 return _dispatch(req)
209 finally:
209 finally:
210 ui.flush()
210 ui.flush()
211 except: # re-raises
211 except: # re-raises
212 # enter the debugger when we hit an exception
212 # enter the debugger when we hit an exception
213 if '--debugger' in req.args:
213 if '--debugger' in req.args:
214 traceback.print_exc()
214 traceback.print_exc()
215 debugmortem[debugger](sys.exc_info()[2])
215 debugmortem[debugger](sys.exc_info()[2])
216 ui.traceback()
216 ui.traceback()
217 raise
217 raise
218
218
219 return callcatch(ui, _runcatchfunc)
219 return callcatch(ui, _runcatchfunc)
220
220
221 def callcatch(ui, func):
221 def callcatch(ui, func):
222 """like scmutil.callcatch but handles more high-level exceptions about
222 """like scmutil.callcatch but handles more high-level exceptions about
223 config parsing and commands. besides, use handlecommandexception to handle
223 config parsing and commands. besides, use handlecommandexception to handle
224 uncaught exceptions.
224 uncaught exceptions.
225 """
225 """
226 try:
226 try:
227 return scmutil.callcatch(ui, func)
227 return scmutil.callcatch(ui, func)
228 except error.AmbiguousCommand as inst:
228 except error.AmbiguousCommand as inst:
229 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
229 ui.warn(_("hg: command '%s' is ambiguous:\n %s\n") %
230 (inst.args[0], " ".join(inst.args[1])))
230 (inst.args[0], " ".join(inst.args[1])))
231 except error.CommandError as inst:
231 except error.CommandError as inst:
232 if inst.args[0]:
232 if inst.args[0]:
233 ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1]))
233 ui.warn(_("hg %s: %s\n") % (inst.args[0], inst.args[1]))
234 commands.help_(ui, inst.args[0], full=False, command=True)
234 commands.help_(ui, inst.args[0], full=False, command=True)
235 else:
235 else:
236 ui.warn(_("hg: %s\n") % inst.args[1])
236 ui.warn(_("hg: %s\n") % inst.args[1])
237 commands.help_(ui, 'shortlist')
237 commands.help_(ui, 'shortlist')
238 except error.ParseError as inst:
238 except error.ParseError as inst:
239 _formatparse(ui.warn, inst)
239 _formatparse(ui.warn, inst)
240 return -1
240 return -1
241 except error.UnknownCommand as inst:
241 except error.UnknownCommand as inst:
242 ui.warn(_("hg: unknown command '%s'\n") % inst.args[0])
242 ui.warn(_("hg: unknown command '%s'\n") % inst.args[0])
243 try:
243 try:
244 # check if the command is in a disabled extension
244 # check if the command is in a disabled extension
245 # (but don't check for extensions themselves)
245 # (but don't check for extensions themselves)
246 commands.help_(ui, inst.args[0], unknowncmd=True)
246 commands.help_(ui, inst.args[0], unknowncmd=True)
247 except (error.UnknownCommand, error.Abort):
247 except (error.UnknownCommand, error.Abort):
248 suggested = False
248 suggested = False
249 if len(inst.args) == 2:
249 if len(inst.args) == 2:
250 sim = _getsimilar(inst.args[1], inst.args[0])
250 sim = _getsimilar(inst.args[1], inst.args[0])
251 if sim:
251 if sim:
252 _reportsimilar(ui.warn, sim)
252 _reportsimilar(ui.warn, sim)
253 suggested = True
253 suggested = True
254 if not suggested:
254 if not suggested:
255 commands.help_(ui, 'shortlist')
255 commands.help_(ui, 'shortlist')
256 except IOError:
256 except IOError:
257 raise
257 raise
258 except KeyboardInterrupt:
258 except KeyboardInterrupt:
259 raise
259 raise
260 except: # probably re-raises
260 except: # probably re-raises
261 if not handlecommandexception(ui):
261 if not handlecommandexception(ui):
262 raise
262 raise
263
263
264 return -1
264 return -1
265
265
266 def aliasargs(fn, givenargs):
266 def aliasargs(fn, givenargs):
267 args = getattr(fn, 'args', [])
267 args = getattr(fn, 'args', [])
268 if args:
268 if args:
269 cmd = ' '.join(map(util.shellquote, args))
269 cmd = ' '.join(map(util.shellquote, args))
270
270
271 nums = []
271 nums = []
272 def replacer(m):
272 def replacer(m):
273 num = int(m.group(1)) - 1
273 num = int(m.group(1)) - 1
274 nums.append(num)
274 nums.append(num)
275 if num < len(givenargs):
275 if num < len(givenargs):
276 return givenargs[num]
276 return givenargs[num]
277 raise error.Abort(_('too few arguments for command alias'))
277 raise error.Abort(_('too few arguments for command alias'))
278 cmd = re.sub(r'\$(\d+|\$)', replacer, cmd)
278 cmd = re.sub(r'\$(\d+|\$)', replacer, cmd)
279 givenargs = [x for i, x in enumerate(givenargs)
279 givenargs = [x for i, x in enumerate(givenargs)
280 if i not in nums]
280 if i not in nums]
281 args = pycompat.shlexsplit(cmd)
281 args = pycompat.shlexsplit(cmd)
282 return args + givenargs
282 return args + givenargs
283
283
284 def aliasinterpolate(name, args, cmd):
284 def aliasinterpolate(name, args, cmd):
285 '''interpolate args into cmd for shell aliases
285 '''interpolate args into cmd for shell aliases
286
286
287 This also handles $0, $@ and "$@".
287 This also handles $0, $@ and "$@".
288 '''
288 '''
289 # util.interpolate can't deal with "$@" (with quotes) because it's only
289 # util.interpolate can't deal with "$@" (with quotes) because it's only
290 # built to match prefix + patterns.
290 # built to match prefix + patterns.
291 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
291 replacemap = dict(('$%d' % (i + 1), arg) for i, arg in enumerate(args))
292 replacemap['$0'] = name
292 replacemap['$0'] = name
293 replacemap['$$'] = '$'
293 replacemap['$$'] = '$'
294 replacemap['$@'] = ' '.join(args)
294 replacemap['$@'] = ' '.join(args)
295 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
295 # Typical Unix shells interpolate "$@" (with quotes) as all the positional
296 # parameters, separated out into words. Emulate the same behavior here by
296 # parameters, separated out into words. Emulate the same behavior here by
297 # quoting the arguments individually. POSIX shells will then typically
297 # quoting the arguments individually. POSIX shells will then typically
298 # tokenize each argument into exactly one word.
298 # tokenize each argument into exactly one word.
299 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
299 replacemap['"$@"'] = ' '.join(util.shellquote(arg) for arg in args)
300 # escape '\$' for regex
300 # escape '\$' for regex
301 regex = '|'.join(replacemap.keys()).replace('$', r'\$')
301 regex = '|'.join(replacemap.keys()).replace('$', r'\$')
302 r = re.compile(regex)
302 r = re.compile(regex)
303 return r.sub(lambda x: replacemap[x.group()], cmd)
303 return r.sub(lambda x: replacemap[x.group()], cmd)
304
304
305 class cmdalias(object):
305 class cmdalias(object):
306 def __init__(self, name, definition, cmdtable, source):
306 def __init__(self, name, definition, cmdtable, source):
307 self.name = self.cmd = name
307 self.name = self.cmd = name
308 self.cmdname = ''
308 self.cmdname = ''
309 self.definition = definition
309 self.definition = definition
310 self.fn = None
310 self.fn = None
311 self.givenargs = []
311 self.givenargs = []
312 self.opts = []
312 self.opts = []
313 self.help = ''
313 self.help = ''
314 self.badalias = None
314 self.badalias = None
315 self.unknowncmd = False
315 self.unknowncmd = False
316 self.source = source
316 self.source = source
317
317
318 try:
318 try:
319 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
319 aliases, entry = cmdutil.findcmd(self.name, cmdtable)
320 for alias, e in cmdtable.iteritems():
320 for alias, e in cmdtable.iteritems():
321 if e is entry:
321 if e is entry:
322 self.cmd = alias
322 self.cmd = alias
323 break
323 break
324 self.shadows = True
324 self.shadows = True
325 except error.UnknownCommand:
325 except error.UnknownCommand:
326 self.shadows = False
326 self.shadows = False
327
327
328 if not self.definition:
328 if not self.definition:
329 self.badalias = _("no definition for alias '%s'") % self.name
329 self.badalias = _("no definition for alias '%s'") % self.name
330 return
330 return
331
331
332 if self.definition.startswith('!'):
332 if self.definition.startswith('!'):
333 self.shell = True
333 self.shell = True
334 def fn(ui, *args):
334 def fn(ui, *args):
335 env = {'HG_ARGS': ' '.join((self.name,) + args)}
335 env = {'HG_ARGS': ' '.join((self.name,) + args)}
336 def _checkvar(m):
336 def _checkvar(m):
337 if m.groups()[0] == '$':
337 if m.groups()[0] == '$':
338 return m.group()
338 return m.group()
339 elif int(m.groups()[0]) <= len(args):
339 elif int(m.groups()[0]) <= len(args):
340 return m.group()
340 return m.group()
341 else:
341 else:
342 ui.debug("No argument found for substitution "
342 ui.debug("No argument found for substitution "
343 "of %i variable in alias '%s' definition."
343 "of %i variable in alias '%s' definition."
344 % (int(m.groups()[0]), self.name))
344 % (int(m.groups()[0]), self.name))
345 return ''
345 return ''
346 cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:])
346 cmd = re.sub(r'\$(\d+|\$)', _checkvar, self.definition[1:])
347 cmd = aliasinterpolate(self.name, args, cmd)
347 cmd = aliasinterpolate(self.name, args, cmd)
348 return ui.system(cmd, environ=env)
348 return ui.system(cmd, environ=env)
349 self.fn = fn
349 self.fn = fn
350 return
350 return
351
351
352 try:
352 try:
353 args = pycompat.shlexsplit(self.definition)
353 args = pycompat.shlexsplit(self.definition)
354 except ValueError as inst:
354 except ValueError as inst:
355 self.badalias = (_("error in definition for alias '%s': %s")
355 self.badalias = (_("error in definition for alias '%s': %s")
356 % (self.name, inst))
356 % (self.name, inst))
357 return
357 return
358 self.cmdname = cmd = args.pop(0)
358 self.cmdname = cmd = args.pop(0)
359 self.givenargs = args
359 self.givenargs = args
360
360
361 for invalidarg in ("--cwd", "-R", "--repository", "--repo", "--config"):
361 for invalidarg in ("--cwd", "-R", "--repository", "--repo", "--config"):
362 if _earlygetopt([invalidarg], args):
362 if _earlygetopt([invalidarg], args):
363 self.badalias = (_("error in definition for alias '%s': %s may "
363 self.badalias = (_("error in definition for alias '%s': %s may "
364 "only be given on the command line")
364 "only be given on the command line")
365 % (self.name, invalidarg))
365 % (self.name, invalidarg))
366 return
366 return
367
367
368 try:
368 try:
369 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
369 tableentry = cmdutil.findcmd(cmd, cmdtable, False)[1]
370 if len(tableentry) > 2:
370 if len(tableentry) > 2:
371 self.fn, self.opts, self.help = tableentry
371 self.fn, self.opts, self.help = tableentry
372 else:
372 else:
373 self.fn, self.opts = tableentry
373 self.fn, self.opts = tableentry
374
374
375 if self.help.startswith("hg " + cmd):
375 if self.help.startswith("hg " + cmd):
376 # drop prefix in old-style help lines so hg shows the alias
376 # drop prefix in old-style help lines so hg shows the alias
377 self.help = self.help[4 + len(cmd):]
377 self.help = self.help[4 + len(cmd):]
378 self.__doc__ = self.fn.__doc__
378 self.__doc__ = self.fn.__doc__
379
379
380 except error.UnknownCommand:
380 except error.UnknownCommand:
381 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
381 self.badalias = (_("alias '%s' resolves to unknown command '%s'")
382 % (self.name, cmd))
382 % (self.name, cmd))
383 self.unknowncmd = True
383 self.unknowncmd = True
384 except error.AmbiguousCommand:
384 except error.AmbiguousCommand:
385 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
385 self.badalias = (_("alias '%s' resolves to ambiguous command '%s'")
386 % (self.name, cmd))
386 % (self.name, cmd))
387
387
388 @property
388 @property
389 def args(self):
389 def args(self):
390 args = map(util.expandpath, self.givenargs)
390 args = map(util.expandpath, self.givenargs)
391 return aliasargs(self.fn, args)
391 return aliasargs(self.fn, args)
392
392
393 def __getattr__(self, name):
393 def __getattr__(self, name):
394 adefaults = {'norepo': True, 'optionalrepo': False, 'inferrepo': False}
394 adefaults = {'norepo': True, 'optionalrepo': False, 'inferrepo': False}
395 if name not in adefaults:
395 if name not in adefaults:
396 raise AttributeError(name)
396 raise AttributeError(name)
397 if self.badalias or util.safehasattr(self, 'shell'):
397 if self.badalias or util.safehasattr(self, 'shell'):
398 return adefaults[name]
398 return adefaults[name]
399 return getattr(self.fn, name)
399 return getattr(self.fn, name)
400
400
401 def __call__(self, ui, *args, **opts):
401 def __call__(self, ui, *args, **opts):
402 if self.badalias:
402 if self.badalias:
403 hint = None
403 hint = None
404 if self.unknowncmd:
404 if self.unknowncmd:
405 try:
405 try:
406 # check if the command is in a disabled extension
406 # check if the command is in a disabled extension
407 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
407 cmd, ext = extensions.disabledcmd(ui, self.cmdname)[:2]
408 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
408 hint = _("'%s' is provided by '%s' extension") % (cmd, ext)
409 except error.UnknownCommand:
409 except error.UnknownCommand:
410 pass
410 pass
411 raise error.Abort(self.badalias, hint=hint)
411 raise error.Abort(self.badalias, hint=hint)
412 if self.shadows:
412 if self.shadows:
413 ui.debug("alias '%s' shadows command '%s'\n" %
413 ui.debug("alias '%s' shadows command '%s'\n" %
414 (self.name, self.cmdname))
414 (self.name, self.cmdname))
415
415
416 ui.log('commandalias', "alias '%s' expands to '%s'\n",
416 ui.log('commandalias', "alias '%s' expands to '%s'\n",
417 self.name, self.definition)
417 self.name, self.definition)
418 if util.safehasattr(self, 'shell'):
418 if util.safehasattr(self, 'shell'):
419 return self.fn(ui, *args, **opts)
419 return self.fn(ui, *args, **opts)
420 else:
420 else:
421 try:
421 try:
422 return util.checksignature(self.fn)(ui, *args, **opts)
422 return util.checksignature(self.fn)(ui, *args, **opts)
423 except error.SignatureError:
423 except error.SignatureError:
424 args = ' '.join([self.cmdname] + self.args)
424 args = ' '.join([self.cmdname] + self.args)
425 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
425 ui.debug("alias '%s' expands to '%s'\n" % (self.name, args))
426 raise
426 raise
427
427
428 def addaliases(ui, cmdtable):
428 def addaliases(ui, cmdtable):
429 # aliases are processed after extensions have been loaded, so they
429 # aliases are processed after extensions have been loaded, so they
430 # may use extension commands. Aliases can also use other alias definitions,
430 # may use extension commands. Aliases can also use other alias definitions,
431 # but only if they have been defined prior to the current definition.
431 # but only if they have been defined prior to the current definition.
432 for alias, definition in ui.configitems('alias'):
432 for alias, definition in ui.configitems('alias'):
433 source = ui.configsource('alias', alias)
433 source = ui.configsource('alias', alias)
434 aliasdef = cmdalias(alias, definition, cmdtable, source)
434 aliasdef = cmdalias(alias, definition, cmdtable, source)
435
435
436 try:
436 try:
437 olddef = cmdtable[aliasdef.cmd][0]
437 olddef = cmdtable[aliasdef.cmd][0]
438 if olddef.definition == aliasdef.definition:
438 if olddef.definition == aliasdef.definition:
439 continue
439 continue
440 except (KeyError, AttributeError):
440 except (KeyError, AttributeError):
441 # definition might not exist or it might not be a cmdalias
441 # definition might not exist or it might not be a cmdalias
442 pass
442 pass
443
443
444 cmdtable[aliasdef.name] = (aliasdef, aliasdef.opts, aliasdef.help)
444 cmdtable[aliasdef.name] = (aliasdef, aliasdef.opts, aliasdef.help)
445
445
446 def _parse(ui, args):
446 def _parse(ui, args):
447 options = {}
447 options = {}
448 cmdoptions = {}
448 cmdoptions = {}
449
449
450 try:
450 try:
451 args = fancyopts.fancyopts(args, commands.globalopts, options)
451 args = fancyopts.fancyopts(args, commands.globalopts, options)
452 except getopt.GetoptError as inst:
452 except getopt.GetoptError as inst:
453 raise error.CommandError(None, inst)
453 raise error.CommandError(None, inst)
454
454
455 if args:
455 if args:
456 cmd, args = args[0], args[1:]
456 cmd, args = args[0], args[1:]
457 aliases, entry = cmdutil.findcmd(cmd, commands.table,
457 aliases, entry = cmdutil.findcmd(cmd, commands.table,
458 ui.configbool("ui", "strict"))
458 ui.configbool("ui", "strict"))
459 cmd = aliases[0]
459 cmd = aliases[0]
460 args = aliasargs(entry[0], args)
460 args = aliasargs(entry[0], args)
461 defaults = ui.config("defaults", cmd)
461 defaults = ui.config("defaults", cmd)
462 if defaults:
462 if defaults:
463 args = map(util.expandpath, pycompat.shlexsplit(defaults)) + args
463 args = map(util.expandpath, pycompat.shlexsplit(defaults)) + args
464 c = list(entry[1])
464 c = list(entry[1])
465 else:
465 else:
466 cmd = None
466 cmd = None
467 c = []
467 c = []
468
468
469 # combine global options into local
469 # combine global options into local
470 for o in commands.globalopts:
470 for o in commands.globalopts:
471 c.append((o[0], o[1], options[o[1]], o[3]))
471 c.append((o[0], o[1], options[o[1]], o[3]))
472
472
473 try:
473 try:
474 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
474 args = fancyopts.fancyopts(args, c, cmdoptions, gnu=True)
475 except getopt.GetoptError as inst:
475 except getopt.GetoptError as inst:
476 raise error.CommandError(cmd, inst)
476 raise error.CommandError(cmd, inst)
477
477
478 # separate global options back out
478 # separate global options back out
479 for o in commands.globalopts:
479 for o in commands.globalopts:
480 n = o[1]
480 n = o[1]
481 options[n] = cmdoptions[n]
481 options[n] = cmdoptions[n]
482 del cmdoptions[n]
482 del cmdoptions[n]
483
483
484 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
484 return (cmd, cmd and entry[0] or None, args, options, cmdoptions)
485
485
486 def _parseconfig(ui, config):
486 def _parseconfig(ui, config):
487 """parse the --config options from the command line"""
487 """parse the --config options from the command line"""
488 configs = []
488 configs = []
489
489
490 for cfg in config:
490 for cfg in config:
491 try:
491 try:
492 name, value = [cfgelem.strip()
492 name, value = [cfgelem.strip()
493 for cfgelem in cfg.split('=', 1)]
493 for cfgelem in cfg.split('=', 1)]
494 section, name = name.split('.', 1)
494 section, name = name.split('.', 1)
495 if not section or not name:
495 if not section or not name:
496 raise IndexError
496 raise IndexError
497 ui.setconfig(section, name, value, '--config')
497 ui.setconfig(section, name, value, '--config')
498 configs.append((section, name, value))
498 configs.append((section, name, value))
499 except (IndexError, ValueError):
499 except (IndexError, ValueError):
500 raise error.Abort(_('malformed --config option: %r '
500 raise error.Abort(_('malformed --config option: %r '
501 '(use --config section.name=value)') % cfg)
501 '(use --config section.name=value)') % cfg)
502
502
503 return configs
503 return configs
504
504
505 def _earlygetopt(aliases, args):
505 def _earlygetopt(aliases, args):
506 """Return list of values for an option (or aliases).
506 """Return list of values for an option (or aliases).
507
507
508 The values are listed in the order they appear in args.
508 The values are listed in the order they appear in args.
509 The options and values are removed from args.
509 The options and values are removed from args.
510
510
511 >>> args = ['x', '--cwd', 'foo', 'y']
511 >>> args = ['x', '--cwd', 'foo', 'y']
512 >>> _earlygetopt(['--cwd'], args), args
512 >>> _earlygetopt(['--cwd'], args), args
513 (['foo'], ['x', 'y'])
513 (['foo'], ['x', 'y'])
514
514
515 >>> args = ['x', '--cwd=bar', 'y']
515 >>> args = ['x', '--cwd=bar', 'y']
516 >>> _earlygetopt(['--cwd'], args), args
516 >>> _earlygetopt(['--cwd'], args), args
517 (['bar'], ['x', 'y'])
517 (['bar'], ['x', 'y'])
518
518
519 >>> args = ['x', '-R', 'foo', 'y']
519 >>> args = ['x', '-R', 'foo', 'y']
520 >>> _earlygetopt(['-R'], args), args
520 >>> _earlygetopt(['-R'], args), args
521 (['foo'], ['x', 'y'])
521 (['foo'], ['x', 'y'])
522
522
523 >>> args = ['x', '-Rbar', 'y']
523 >>> args = ['x', '-Rbar', 'y']
524 >>> _earlygetopt(['-R'], args), args
524 >>> _earlygetopt(['-R'], args), args
525 (['bar'], ['x', 'y'])
525 (['bar'], ['x', 'y'])
526 """
526 """
527 try:
527 try:
528 argcount = args.index("--")
528 argcount = args.index("--")
529 except ValueError:
529 except ValueError:
530 argcount = len(args)
530 argcount = len(args)
531 shortopts = [opt for opt in aliases if len(opt) == 2]
531 shortopts = [opt for opt in aliases if len(opt) == 2]
532 values = []
532 values = []
533 pos = 0
533 pos = 0
534 while pos < argcount:
534 while pos < argcount:
535 fullarg = arg = args[pos]
535 fullarg = arg = args[pos]
536 equals = arg.find('=')
536 equals = arg.find('=')
537 if equals > -1:
537 if equals > -1:
538 arg = arg[:equals]
538 arg = arg[:equals]
539 if arg in aliases:
539 if arg in aliases:
540 del args[pos]
540 del args[pos]
541 if equals > -1:
541 if equals > -1:
542 values.append(fullarg[equals + 1:])
542 values.append(fullarg[equals + 1:])
543 argcount -= 1
543 argcount -= 1
544 else:
544 else:
545 if pos + 1 >= argcount:
545 if pos + 1 >= argcount:
546 # ignore and let getopt report an error if there is no value
546 # ignore and let getopt report an error if there is no value
547 break
547 break
548 values.append(args.pop(pos))
548 values.append(args.pop(pos))
549 argcount -= 2
549 argcount -= 2
550 elif arg[:2] in shortopts:
550 elif arg[:2] in shortopts:
551 # short option can have no following space, e.g. hg log -Rfoo
551 # short option can have no following space, e.g. hg log -Rfoo
552 values.append(args.pop(pos)[2:])
552 values.append(args.pop(pos)[2:])
553 argcount -= 1
553 argcount -= 1
554 else:
554 else:
555 pos += 1
555 pos += 1
556 return values
556 return values
557
557
558 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
558 def runcommand(lui, repo, cmd, fullargs, ui, options, d, cmdpats, cmdoptions):
559 # run pre-hook, and abort if it fails
559 # run pre-hook, and abort if it fails
560 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
560 hook.hook(lui, repo, "pre-%s" % cmd, True, args=" ".join(fullargs),
561 pats=cmdpats, opts=cmdoptions)
561 pats=cmdpats, opts=cmdoptions)
562 try:
562 try:
563 ret = _runcommand(ui, options, cmd, d)
563 ret = _runcommand(ui, options, cmd, d)
564 # run post-hook, passing command result
564 # run post-hook, passing command result
565 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
565 hook.hook(lui, repo, "post-%s" % cmd, False, args=" ".join(fullargs),
566 result=ret, pats=cmdpats, opts=cmdoptions)
566 result=ret, pats=cmdpats, opts=cmdoptions)
567 except Exception:
567 except Exception:
568 # run failure hook and re-raise
568 # run failure hook and re-raise
569 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
569 hook.hook(lui, repo, "fail-%s" % cmd, False, args=" ".join(fullargs),
570 pats=cmdpats, opts=cmdoptions)
570 pats=cmdpats, opts=cmdoptions)
571 raise
571 raise
572 return ret
572 return ret
573
573
574 def _getlocal(ui, rpath, wd=None):
574 def _getlocal(ui, rpath, wd=None):
575 """Return (path, local ui object) for the given target path.
575 """Return (path, local ui object) for the given target path.
576
576
577 Takes paths in [cwd]/.hg/hgrc into account."
577 Takes paths in [cwd]/.hg/hgrc into account."
578 """
578 """
579 if wd is None:
579 if wd is None:
580 try:
580 try:
581 wd = pycompat.getcwd()
581 wd = pycompat.getcwd()
582 except OSError as e:
582 except OSError as e:
583 raise error.Abort(_("error getting current working directory: %s") %
583 raise error.Abort(_("error getting current working directory: %s") %
584 e.strerror)
584 e.strerror)
585 path = cmdutil.findrepo(wd) or ""
585 path = cmdutil.findrepo(wd) or ""
586 if not path:
586 if not path:
587 lui = ui
587 lui = ui
588 else:
588 else:
589 lui = ui.copy()
589 lui = ui.copy()
590 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
590 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
591
591
592 if rpath and rpath[-1]:
592 if rpath and rpath[-1]:
593 path = lui.expandpath(rpath[-1])
593 path = lui.expandpath(rpath[-1])
594 lui = ui.copy()
594 lui = ui.copy()
595 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
595 lui.readconfig(os.path.join(path, ".hg", "hgrc"), path)
596
596
597 return path, lui
597 return path, lui
598
598
599 def _checkshellalias(lui, ui, args):
599 def _checkshellalias(lui, ui, args):
600 """Return the function to run the shell alias, if it is required"""
600 """Return the function to run the shell alias, if it is required"""
601 options = {}
601 options = {}
602
602
603 try:
603 try:
604 args = fancyopts.fancyopts(args, commands.globalopts, options)
604 args = fancyopts.fancyopts(args, commands.globalopts, options)
605 except getopt.GetoptError:
605 except getopt.GetoptError:
606 return
606 return
607
607
608 if not args:
608 if not args:
609 return
609 return
610
610
611 cmdtable = commands.table
611 cmdtable = commands.table
612
612
613 cmd = args[0]
613 cmd = args[0]
614 try:
614 try:
615 strict = ui.configbool("ui", "strict")
615 strict = ui.configbool("ui", "strict")
616 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
616 aliases, entry = cmdutil.findcmd(cmd, cmdtable, strict)
617 except (error.AmbiguousCommand, error.UnknownCommand):
617 except (error.AmbiguousCommand, error.UnknownCommand):
618 return
618 return
619
619
620 cmd = aliases[0]
620 cmd = aliases[0]
621 fn = entry[0]
621 fn = entry[0]
622
622
623 if cmd and util.safehasattr(fn, 'shell'):
623 if cmd and util.safehasattr(fn, 'shell'):
624 d = lambda: fn(ui, *args[1:])
624 d = lambda: fn(ui, *args[1:])
625 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
625 return lambda: runcommand(lui, None, cmd, args[:1], ui, options, d,
626 [], {})
626 [], {})
627
627
628 _loaded = set()
628 _loaded = set()
629
629
630 # list of (objname, loadermod, loadername) tuple:
630 # list of (objname, loadermod, loadername) tuple:
631 # - objname is the name of an object in extension module, from which
631 # - objname is the name of an object in extension module, from which
632 # extra information is loaded
632 # extra information is loaded
633 # - loadermod is the module where loader is placed
633 # - loadermod is the module where loader is placed
634 # - loadername is the name of the function, which takes (ui, extensionname,
634 # - loadername is the name of the function, which takes (ui, extensionname,
635 # extraobj) arguments
635 # extraobj) arguments
636 extraloaders = [
636 extraloaders = [
637 ('cmdtable', commands, 'loadcmdtable'),
637 ('cmdtable', commands, 'loadcmdtable'),
638 ('colortable', color, 'loadcolortable'),
638 ('colortable', color, 'loadcolortable'),
639 ('filesetpredicate', fileset, 'loadpredicate'),
639 ('filesetpredicate', fileset, 'loadpredicate'),
640 ('revsetpredicate', revset, 'loadpredicate'),
640 ('revsetpredicate', revset, 'loadpredicate'),
641 ('templatefilter', templatefilters, 'loadfilter'),
641 ('templatefilter', templatefilters, 'loadfilter'),
642 ('templatefunc', templater, 'loadfunction'),
642 ('templatefunc', templater, 'loadfunction'),
643 ('templatekeyword', templatekw, 'loadkeyword'),
643 ('templatekeyword', templatekw, 'loadkeyword'),
644 ]
644 ]
645
645
646 def _dispatch(req):
646 def _dispatch(req):
647 args = req.args
647 args = req.args
648 ui = req.ui
648 ui = req.ui
649
649
650 # check for cwd
650 # check for cwd
651 cwd = _earlygetopt(['--cwd'], args)
651 cwd = _earlygetopt(['--cwd'], args)
652 if cwd:
652 if cwd:
653 os.chdir(cwd[-1])
653 os.chdir(cwd[-1])
654
654
655 rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
655 rpath = _earlygetopt(["-R", "--repository", "--repo"], args)
656 path, lui = _getlocal(ui, rpath)
656 path, lui = _getlocal(ui, rpath)
657
657
658 # Side-effect of accessing is debugcommands module is guaranteed to be
658 # Side-effect of accessing is debugcommands module is guaranteed to be
659 # imported and commands.table is populated.
659 # imported and commands.table is populated.
660 debugcommands.command
660 debugcommands.command
661
661
662 uis = set([ui, lui])
662 uis = set([ui, lui])
663
663
664 if req.repo:
664 if req.repo:
665 uis.add(req.repo.ui)
665 uis.add(req.repo.ui)
666
666
667 if '--profile' in args:
667 if '--profile' in args:
668 for ui_ in uis:
668 for ui_ in uis:
669 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
669 ui_.setconfig('profiling', 'enabled', 'true', '--profile')
670
670
671 with profiling.maybeprofile(lui):
671 with profiling.maybeprofile(lui):
672 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
672 # Configure extensions in phases: uisetup, extsetup, cmdtable, and
673 # reposetup. Programs like TortoiseHg will call _dispatch several
673 # reposetup. Programs like TortoiseHg will call _dispatch several
674 # times so we keep track of configured extensions in _loaded.
674 # times so we keep track of configured extensions in _loaded.
675 extensions.loadall(lui)
675 extensions.loadall(lui)
676 exts = [ext for ext in extensions.extensions() if ext[0] not in _loaded]
676 exts = [ext for ext in extensions.extensions() if ext[0] not in _loaded]
677 # Propagate any changes to lui.__class__ by extensions
677 # Propagate any changes to lui.__class__ by extensions
678 ui.__class__ = lui.__class__
678 ui.__class__ = lui.__class__
679
679
680 # (uisetup and extsetup are handled in extensions.loadall)
680 # (uisetup and extsetup are handled in extensions.loadall)
681
681
682 for name, module in exts:
682 for name, module in exts:
683 for objname, loadermod, loadername in extraloaders:
683 for objname, loadermod, loadername in extraloaders:
684 extraobj = getattr(module, objname, None)
684 extraobj = getattr(module, objname, None)
685 if extraobj is not None:
685 if extraobj is not None:
686 getattr(loadermod, loadername)(ui, name, extraobj)
686 getattr(loadermod, loadername)(ui, name, extraobj)
687 _loaded.add(name)
687 _loaded.add(name)
688
688
689 # (reposetup is handled in hg.repository)
689 # (reposetup is handled in hg.repository)
690
690
691 addaliases(lui, commands.table)
691 addaliases(lui, commands.table)
692
692
693 # All aliases and commands are completely defined, now.
693 # All aliases and commands are completely defined, now.
694 # Check abbreviation/ambiguity of shell alias.
694 # Check abbreviation/ambiguity of shell alias.
695 shellaliasfn = _checkshellalias(lui, ui, args)
695 shellaliasfn = _checkshellalias(lui, ui, args)
696 if shellaliasfn:
696 if shellaliasfn:
697 return shellaliasfn()
697 return shellaliasfn()
698
698
699 # check for fallback encoding
699 # check for fallback encoding
700 fallback = lui.config('ui', 'fallbackencoding')
700 fallback = lui.config('ui', 'fallbackencoding')
701 if fallback:
701 if fallback:
702 encoding.fallbackencoding = fallback
702 encoding.fallbackencoding = fallback
703
703
704 fullargs = args
704 fullargs = args
705 cmd, func, args, options, cmdoptions = _parse(lui, args)
705 cmd, func, args, options, cmdoptions = _parse(lui, args)
706
706
707 if options["config"]:
707 if options["config"]:
708 raise error.Abort(_("option --config may not be abbreviated!"))
708 raise error.Abort(_("option --config may not be abbreviated!"))
709 if options["cwd"]:
709 if options["cwd"]:
710 raise error.Abort(_("option --cwd may not be abbreviated!"))
710 raise error.Abort(_("option --cwd may not be abbreviated!"))
711 if options["repository"]:
711 if options["repository"]:
712 raise error.Abort(_(
712 raise error.Abort(_(
713 "option -R has to be separated from other options (e.g. not "
713 "option -R has to be separated from other options (e.g. not "
714 "-qR) and --repository may only be abbreviated as --repo!"))
714 "-qR) and --repository may only be abbreviated as --repo!"))
715
715
716 if options["encoding"]:
716 if options["encoding"]:
717 encoding.encoding = options["encoding"]
717 encoding.encoding = options["encoding"]
718 if options["encodingmode"]:
718 if options["encodingmode"]:
719 encoding.encodingmode = options["encodingmode"]
719 encoding.encodingmode = options["encodingmode"]
720 if options["time"]:
720 if options["time"]:
721 def get_times():
721 def get_times():
722 t = os.times()
722 t = os.times()
723 if t[4] == 0.0:
723 if t[4] == 0.0:
724 # Windows leaves this as zero, so use time.clock()
724 # Windows leaves this as zero, so use time.clock()
725 t = (t[0], t[1], t[2], t[3], time.clock())
725 t = (t[0], t[1], t[2], t[3], time.clock())
726 return t
726 return t
727 s = get_times()
727 s = get_times()
728 def print_time():
728 def print_time():
729 t = get_times()
729 t = get_times()
730 ui.warn(
730 ui.warn(
731 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
731 _("time: real %.3f secs (user %.3f+%.3f sys %.3f+%.3f)\n") %
732 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
732 (t[4]-s[4], t[0]-s[0], t[2]-s[2], t[1]-s[1], t[3]-s[3]))
733 atexit.register(print_time)
733 atexit.register(print_time)
734
734
735 if options['verbose'] or options['debug'] or options['quiet']:
735 if options['verbose'] or options['debug'] or options['quiet']:
736 for opt in ('verbose', 'debug', 'quiet'):
736 for opt in ('verbose', 'debug', 'quiet'):
737 val = str(bool(options[opt]))
737 val = str(bool(options[opt]))
738 for ui_ in uis:
738 for ui_ in uis:
739 ui_.setconfig('ui', opt, val, '--' + opt)
739 ui_.setconfig('ui', opt, val, '--' + opt)
740
740
741 if options['traceback']:
741 if options['traceback']:
742 for ui_ in uis:
742 for ui_ in uis:
743 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
743 ui_.setconfig('ui', 'traceback', 'on', '--traceback')
744
744
745 if options['noninteractive']:
745 if options['noninteractive']:
746 for ui_ in uis:
746 for ui_ in uis:
747 ui_.setconfig('ui', 'interactive', 'off', '-y')
747 ui_.setconfig('ui', 'interactive', 'off', '-y')
748
748
749 if cmdoptions.get('insecure', False):
749 if cmdoptions.get('insecure', False):
750 for ui_ in uis:
750 for ui_ in uis:
751 ui_.insecureconnections = True
751 ui_.insecureconnections = True
752
752
753 if options['version']:
753 if options['version']:
754 return commands.version_(ui)
754 return commands.version_(ui)
755 if options['help']:
755 if options['help']:
756 return commands.help_(ui, cmd, command=cmd is not None)
756 return commands.help_(ui, cmd, command=cmd is not None)
757 elif not cmd:
757 elif not cmd:
758 return commands.help_(ui, 'shortlist')
758 return commands.help_(ui, 'shortlist')
759
759
760 repo = None
760 repo = None
761 cmdpats = args[:]
761 cmdpats = args[:]
762 if not func.norepo:
762 if not func.norepo:
763 # use the repo from the request only if we don't have -R
763 # use the repo from the request only if we don't have -R
764 if not rpath and not cwd:
764 if not rpath and not cwd:
765 repo = req.repo
765 repo = req.repo
766
766
767 if repo:
767 if repo:
768 # set the descriptors of the repo ui to those of ui
768 # set the descriptors of the repo ui to those of ui
769 repo.ui.fin = ui.fin
769 repo.ui.fin = ui.fin
770 repo.ui.fout = ui.fout
770 repo.ui.fout = ui.fout
771 repo.ui.ferr = ui.ferr
771 repo.ui.ferr = ui.ferr
772 else:
772 else:
773 try:
773 try:
774 repo = hg.repository(ui, path=path)
774 repo = hg.repository(ui, path=path)
775 if not repo.local():
775 if not repo.local():
776 raise error.Abort(_("repository '%s' is not local")
776 raise error.Abort(_("repository '%s' is not local")
777 % path)
777 % path)
778 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
778 repo.ui.setconfig("bundle", "mainreporoot", repo.root,
779 'repo')
779 'repo')
780 except error.RequirementError:
780 except error.RequirementError:
781 raise
781 raise
782 except error.RepoError:
782 except error.RepoError:
783 if rpath and rpath[-1]: # invalid -R path
783 if rpath and rpath[-1]: # invalid -R path
784 raise
784 raise
785 if not func.optionalrepo:
785 if not func.optionalrepo:
786 if func.inferrepo and args and not path:
786 if func.inferrepo and args and not path:
787 # try to infer -R from command args
787 # try to infer -R from command args
788 repos = map(cmdutil.findrepo, args)
788 repos = map(cmdutil.findrepo, args)
789 guess = repos[0]
789 guess = repos[0]
790 if guess and repos.count(guess) == len(repos):
790 if guess and repos.count(guess) == len(repos):
791 req.args = ['--repository', guess] + fullargs
791 req.args = ['--repository', guess] + fullargs
792 return _dispatch(req)
792 return _dispatch(req)
793 if not path:
793 if not path:
794 raise error.RepoError(_("no repository found in"
794 raise error.RepoError(_("no repository found in"
795 " '%s' (.hg not found)")
795 " '%s' (.hg not found)")
796 % pycompat.getcwd())
796 % pycompat.getcwd())
797 raise
797 raise
798 if repo:
798 if repo:
799 ui = repo.ui
799 ui = repo.ui
800 if options['hidden']:
800 if options['hidden']:
801 repo = repo.unfiltered()
801 repo = repo.unfiltered()
802 args.insert(0, repo)
802 args.insert(0, repo)
803 elif rpath:
803 elif rpath:
804 ui.warn(_("warning: --repository ignored\n"))
804 ui.warn(_("warning: --repository ignored\n"))
805
805
806 msg = ' '.join(' ' in a and repr(a) or a for a in fullargs)
806 msg = ' '.join(' ' in a and repr(a) or a for a in fullargs)
807 ui.log("command", '%s\n', msg)
807 ui.log("command", '%s\n', msg)
808 strcmdopt = pycompat.strkwargs(cmdoptions)
808 strcmdopt = pycompat.strkwargs(cmdoptions)
809 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
809 d = lambda: util.checksignature(func)(ui, *args, **strcmdopt)
810 try:
810 try:
811 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
811 return runcommand(lui, repo, cmd, fullargs, ui, options, d,
812 cmdpats, cmdoptions)
812 cmdpats, cmdoptions)
813 finally:
813 finally:
814 if repo and repo != req.repo:
814 if repo and repo != req.repo:
815 repo.close()
815 repo.close()
816
816
817 def _runcommand(ui, options, cmd, cmdfunc):
817 def _runcommand(ui, options, cmd, cmdfunc):
818 """Run a command function, possibly with profiling enabled."""
818 """Run a command function, possibly with profiling enabled."""
819 try:
819 try:
820 return cmdfunc()
820 return cmdfunc()
821 except error.SignatureError:
821 except error.SignatureError:
822 raise error.CommandError(cmd, _('invalid arguments'))
822 raise error.CommandError(cmd, _('invalid arguments'))
823
823
824 def _exceptionwarning(ui):
824 def _exceptionwarning(ui):
825 """Produce a warning message for the current active exception"""
825 """Produce a warning message for the current active exception"""
826
826
827 # For compatibility checking, we discard the portion of the hg
827 # For compatibility checking, we discard the portion of the hg
828 # version after the + on the assumption that if a "normal
828 # version after the + on the assumption that if a "normal
829 # user" is running a build with a + in it the packager
829 # user" is running a build with a + in it the packager
830 # probably built from fairly close to a tag and anyone with a
830 # probably built from fairly close to a tag and anyone with a
831 # 'make local' copy of hg (where the version number can be out
831 # 'make local' copy of hg (where the version number can be out
832 # of date) will be clueful enough to notice the implausible
832 # of date) will be clueful enough to notice the implausible
833 # version number and try updating.
833 # version number and try updating.
834 ct = util.versiontuple(n=2)
834 ct = util.versiontuple(n=2)
835 worst = None, ct, ''
835 worst = None, ct, ''
836 if ui.config('ui', 'supportcontact', None) is None:
836 if ui.config('ui', 'supportcontact', None) is None:
837 for name, mod in extensions.extensions():
837 for name, mod in extensions.extensions():
838 testedwith = getattr(mod, 'testedwith', '')
838 testedwith = getattr(mod, 'testedwith', '')
839 report = getattr(mod, 'buglink', _('the extension author.'))
839 report = getattr(mod, 'buglink', _('the extension author.'))
840 if not testedwith.strip():
840 if not testedwith.strip():
841 # We found an untested extension. It's likely the culprit.
841 # We found an untested extension. It's likely the culprit.
842 worst = name, 'unknown', report
842 worst = name, 'unknown', report
843 break
843 break
844
844
845 # Never blame on extensions bundled with Mercurial.
845 # Never blame on extensions bundled with Mercurial.
846 if extensions.ismoduleinternal(mod):
846 if extensions.ismoduleinternal(mod):
847 continue
847 continue
848
848
849 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
849 tested = [util.versiontuple(t, 2) for t in testedwith.split()]
850 if ct in tested:
850 if ct in tested:
851 continue
851 continue
852
852
853 lower = [t for t in tested if t < ct]
853 lower = [t for t in tested if t < ct]
854 nearest = max(lower or tested)
854 nearest = max(lower or tested)
855 if worst[0] is None or nearest < worst[1]:
855 if worst[0] is None or nearest < worst[1]:
856 worst = name, nearest, report
856 worst = name, nearest, report
857 if worst[0] is not None:
857 if worst[0] is not None:
858 name, testedwith, report = worst
858 name, testedwith, report = worst
859 if not isinstance(testedwith, str):
859 if not isinstance(testedwith, str):
860 testedwith = '.'.join([str(c) for c in testedwith])
860 testedwith = '.'.join([str(c) for c in testedwith])
861 warning = (_('** Unknown exception encountered with '
861 warning = (_('** Unknown exception encountered with '
862 'possibly-broken third-party extension %s\n'
862 'possibly-broken third-party extension %s\n'
863 '** which supports versions %s of Mercurial.\n'
863 '** which supports versions %s of Mercurial.\n'
864 '** Please disable %s and try your action again.\n'
864 '** Please disable %s and try your action again.\n'
865 '** If that fixes the bug please report it to %s\n')
865 '** If that fixes the bug please report it to %s\n')
866 % (name, testedwith, name, report))
866 % (name, testedwith, name, report))
867 else:
867 else:
868 bugtracker = ui.config('ui', 'supportcontact', None)
868 bugtracker = ui.config('ui', 'supportcontact', None)
869 if bugtracker is None:
869 if bugtracker is None:
870 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
870 bugtracker = _("https://mercurial-scm.org/wiki/BugTracker")
871 warning = (_("** unknown exception encountered, "
871 warning = (_("** unknown exception encountered, "
872 "please report by visiting\n** ") + bugtracker + '\n')
872 "please report by visiting\n** ") + bugtracker + '\n')
873 warning += ((_("** Python %s\n") % sys.version.replace('\n', '')) +
873 warning += ((_("** Python %s\n") % sys.version.replace('\n', '')) +
874 (_("** Mercurial Distributed SCM (version %s)\n") %
874 (_("** Mercurial Distributed SCM (version %s)\n") %
875 util.version()) +
875 util.version()) +
876 (_("** Extensions loaded: %s\n") %
876 (_("** Extensions loaded: %s\n") %
877 ", ".join([x[0] for x in extensions.extensions()])))
877 ", ".join([x[0] for x in extensions.extensions()])))
878 return warning
878 return warning
879
879
880 def handlecommandexception(ui):
880 def handlecommandexception(ui):
881 """Produce a warning message for broken commands
881 """Produce a warning message for broken commands
882
882
883 Called when handling an exception; the exception is reraised if
883 Called when handling an exception; the exception is reraised if
884 this function returns False, ignored otherwise.
884 this function returns False, ignored otherwise.
885 """
885 """
886 warning = _exceptionwarning(ui)
886 warning = _exceptionwarning(ui)
887 ui.log("commandexception", "%s\n%s\n", warning, traceback.format_exc())
887 ui.log("commandexception", "%s\n%s\n", warning, traceback.format_exc())
888 ui.warn(warning)
888 ui.warn(warning)
889 return False # re-raise the exception
889 return False # re-raise the exception
@@ -1,266 +1,265 b''
1 # hook.py - hook support for mercurial
1 # hook.py - hook support for mercurial
2 #
2 #
3 # Copyright 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2007 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 os
10 import os
11 import sys
11 import sys
12 import time
13
12
14 from .i18n import _
13 from .i18n import _
15 from . import (
14 from . import (
16 demandimport,
15 demandimport,
17 error,
16 error,
18 extensions,
17 extensions,
19 pycompat,
18 pycompat,
20 util,
19 util,
21 )
20 )
22
21
23 def _pythonhook(ui, repo, name, hname, funcname, args, throw):
22 def _pythonhook(ui, repo, name, hname, funcname, args, throw):
24 '''call python hook. hook is callable object, looked up as
23 '''call python hook. hook is callable object, looked up as
25 name in python module. if callable returns "true", hook
24 name in python module. if callable returns "true", hook
26 fails, else passes. if hook raises exception, treated as
25 fails, else passes. if hook raises exception, treated as
27 hook failure. exception propagates if throw is "true".
26 hook failure. exception propagates if throw is "true".
28
27
29 reason for "true" meaning "hook failed" is so that
28 reason for "true" meaning "hook failed" is so that
30 unmodified commands (e.g. mercurial.commands.update) can
29 unmodified commands (e.g. mercurial.commands.update) can
31 be run as hooks without wrappers to convert return values.'''
30 be run as hooks without wrappers to convert return values.'''
32
31
33 if callable(funcname):
32 if callable(funcname):
34 obj = funcname
33 obj = funcname
35 funcname = obj.__module__ + "." + obj.__name__
34 funcname = obj.__module__ + "." + obj.__name__
36 else:
35 else:
37 d = funcname.rfind('.')
36 d = funcname.rfind('.')
38 if d == -1:
37 if d == -1:
39 raise error.HookLoadError(
38 raise error.HookLoadError(
40 _('%s hook is invalid: "%s" not in a module')
39 _('%s hook is invalid: "%s" not in a module')
41 % (hname, funcname))
40 % (hname, funcname))
42 modname = funcname[:d]
41 modname = funcname[:d]
43 oldpaths = sys.path
42 oldpaths = sys.path
44 if util.mainfrozen():
43 if util.mainfrozen():
45 # binary installs require sys.path manipulation
44 # binary installs require sys.path manipulation
46 modpath, modfile = os.path.split(modname)
45 modpath, modfile = os.path.split(modname)
47 if modpath and modfile:
46 if modpath and modfile:
48 sys.path = sys.path[:] + [modpath]
47 sys.path = sys.path[:] + [modpath]
49 modname = modfile
48 modname = modfile
50 with demandimport.deactivated():
49 with demandimport.deactivated():
51 try:
50 try:
52 obj = __import__(modname)
51 obj = __import__(modname)
53 except (ImportError, SyntaxError):
52 except (ImportError, SyntaxError):
54 e1 = sys.exc_info()
53 e1 = sys.exc_info()
55 try:
54 try:
56 # extensions are loaded with hgext_ prefix
55 # extensions are loaded with hgext_ prefix
57 obj = __import__("hgext_%s" % modname)
56 obj = __import__("hgext_%s" % modname)
58 except (ImportError, SyntaxError):
57 except (ImportError, SyntaxError):
59 e2 = sys.exc_info()
58 e2 = sys.exc_info()
60 if ui.tracebackflag:
59 if ui.tracebackflag:
61 ui.warn(_('exception from first failed import '
60 ui.warn(_('exception from first failed import '
62 'attempt:\n'))
61 'attempt:\n'))
63 ui.traceback(e1)
62 ui.traceback(e1)
64 if ui.tracebackflag:
63 if ui.tracebackflag:
65 ui.warn(_('exception from second failed import '
64 ui.warn(_('exception from second failed import '
66 'attempt:\n'))
65 'attempt:\n'))
67 ui.traceback(e2)
66 ui.traceback(e2)
68
67
69 if not ui.tracebackflag:
68 if not ui.tracebackflag:
70 tracebackhint = _(
69 tracebackhint = _(
71 'run with --traceback for stack trace')
70 'run with --traceback for stack trace')
72 else:
71 else:
73 tracebackhint = None
72 tracebackhint = None
74 raise error.HookLoadError(
73 raise error.HookLoadError(
75 _('%s hook is invalid: import of "%s" failed') %
74 _('%s hook is invalid: import of "%s" failed') %
76 (hname, modname), hint=tracebackhint)
75 (hname, modname), hint=tracebackhint)
77 sys.path = oldpaths
76 sys.path = oldpaths
78 try:
77 try:
79 for p in funcname.split('.')[1:]:
78 for p in funcname.split('.')[1:]:
80 obj = getattr(obj, p)
79 obj = getattr(obj, p)
81 except AttributeError:
80 except AttributeError:
82 raise error.HookLoadError(
81 raise error.HookLoadError(
83 _('%s hook is invalid: "%s" is not defined')
82 _('%s hook is invalid: "%s" is not defined')
84 % (hname, funcname))
83 % (hname, funcname))
85 if not callable(obj):
84 if not callable(obj):
86 raise error.HookLoadError(
85 raise error.HookLoadError(
87 _('%s hook is invalid: "%s" is not callable')
86 _('%s hook is invalid: "%s" is not callable')
88 % (hname, funcname))
87 % (hname, funcname))
89
88
90 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
89 ui.note(_("calling hook %s: %s\n") % (hname, funcname))
91 starttime = time.time()
90 starttime = util.timer()
92
91
93 try:
92 try:
94 r = obj(ui=ui, repo=repo, hooktype=name, **args)
93 r = obj(ui=ui, repo=repo, hooktype=name, **args)
95 except Exception as exc:
94 except Exception as exc:
96 if isinstance(exc, error.Abort):
95 if isinstance(exc, error.Abort):
97 ui.warn(_('error: %s hook failed: %s\n') %
96 ui.warn(_('error: %s hook failed: %s\n') %
98 (hname, exc.args[0]))
97 (hname, exc.args[0]))
99 else:
98 else:
100 ui.warn(_('error: %s hook raised an exception: '
99 ui.warn(_('error: %s hook raised an exception: '
101 '%s\n') % (hname, exc))
100 '%s\n') % (hname, exc))
102 if throw:
101 if throw:
103 raise
102 raise
104 if not ui.tracebackflag:
103 if not ui.tracebackflag:
105 ui.warn(_('(run with --traceback for stack trace)\n'))
104 ui.warn(_('(run with --traceback for stack trace)\n'))
106 ui.traceback()
105 ui.traceback()
107 return True, True
106 return True, True
108 finally:
107 finally:
109 duration = time.time() - starttime
108 duration = util.timer() - starttime
110 ui.log('pythonhook', 'pythonhook-%s: %s finished in %0.2f seconds\n',
109 ui.log('pythonhook', 'pythonhook-%s: %s finished in %0.2f seconds\n',
111 name, funcname, duration)
110 name, funcname, duration)
112 if r:
111 if r:
113 if throw:
112 if throw:
114 raise error.HookAbort(_('%s hook failed') % hname)
113 raise error.HookAbort(_('%s hook failed') % hname)
115 ui.warn(_('warning: %s hook failed\n') % hname)
114 ui.warn(_('warning: %s hook failed\n') % hname)
116 return r, False
115 return r, False
117
116
118 def _exthook(ui, repo, name, cmd, args, throw):
117 def _exthook(ui, repo, name, cmd, args, throw):
119 ui.note(_("running hook %s: %s\n") % (name, cmd))
118 ui.note(_("running hook %s: %s\n") % (name, cmd))
120
119
121 starttime = time.time()
120 starttime = util.timer()
122 env = {}
121 env = {}
123
122
124 # make in-memory changes visible to external process
123 # make in-memory changes visible to external process
125 if repo is not None:
124 if repo is not None:
126 tr = repo.currenttransaction()
125 tr = repo.currenttransaction()
127 repo.dirstate.write(tr)
126 repo.dirstate.write(tr)
128 if tr and tr.writepending():
127 if tr and tr.writepending():
129 env['HG_PENDING'] = repo.root
128 env['HG_PENDING'] = repo.root
130
129
131 for k, v in args.iteritems():
130 for k, v in args.iteritems():
132 if callable(v):
131 if callable(v):
133 v = v()
132 v = v()
134 if isinstance(v, dict):
133 if isinstance(v, dict):
135 # make the dictionary element order stable across Python
134 # make the dictionary element order stable across Python
136 # implementations
135 # implementations
137 v = ('{' +
136 v = ('{' +
138 ', '.join('%r: %r' % i for i in sorted(v.iteritems())) +
137 ', '.join('%r: %r' % i for i in sorted(v.iteritems())) +
139 '}')
138 '}')
140 env['HG_' + k.upper()] = v
139 env['HG_' + k.upper()] = v
141
140
142 if repo:
141 if repo:
143 cwd = repo.root
142 cwd = repo.root
144 else:
143 else:
145 cwd = pycompat.getcwd()
144 cwd = pycompat.getcwd()
146 r = ui.system(cmd, environ=env, cwd=cwd)
145 r = ui.system(cmd, environ=env, cwd=cwd)
147
146
148 duration = time.time() - starttime
147 duration = util.timer() - starttime
149 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
148 ui.log('exthook', 'exthook-%s: %s finished in %0.2f seconds\n',
150 name, cmd, duration)
149 name, cmd, duration)
151 if r:
150 if r:
152 desc, r = util.explainexit(r)
151 desc, r = util.explainexit(r)
153 if throw:
152 if throw:
154 raise error.HookAbort(_('%s hook %s') % (name, desc))
153 raise error.HookAbort(_('%s hook %s') % (name, desc))
155 ui.warn(_('warning: %s hook %s\n') % (name, desc))
154 ui.warn(_('warning: %s hook %s\n') % (name, desc))
156 return r
155 return r
157
156
158 # represent an untrusted hook command
157 # represent an untrusted hook command
159 _fromuntrusted = object()
158 _fromuntrusted = object()
160
159
161 def _allhooks(ui):
160 def _allhooks(ui):
162 """return a list of (hook-id, cmd) pairs sorted by priority"""
161 """return a list of (hook-id, cmd) pairs sorted by priority"""
163 hooks = _hookitems(ui)
162 hooks = _hookitems(ui)
164 # Be careful in this section, propagating the real commands from untrusted
163 # Be careful in this section, propagating the real commands from untrusted
165 # sources would create a security vulnerability, make sure anything altered
164 # sources would create a security vulnerability, make sure anything altered
166 # in that section uses "_fromuntrusted" as its command.
165 # in that section uses "_fromuntrusted" as its command.
167 untrustedhooks = _hookitems(ui, _untrusted=True)
166 untrustedhooks = _hookitems(ui, _untrusted=True)
168 for name, value in untrustedhooks.items():
167 for name, value in untrustedhooks.items():
169 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
168 trustedvalue = hooks.get(name, (None, None, name, _fromuntrusted))
170 if value != trustedvalue:
169 if value != trustedvalue:
171 (lp, lo, lk, lv) = trustedvalue
170 (lp, lo, lk, lv) = trustedvalue
172 hooks[name] = (lp, lo, lk, _fromuntrusted)
171 hooks[name] = (lp, lo, lk, _fromuntrusted)
173 # (end of the security sensitive section)
172 # (end of the security sensitive section)
174 return [(k, v) for p, o, k, v in sorted(hooks.values())]
173 return [(k, v) for p, o, k, v in sorted(hooks.values())]
175
174
176 def _hookitems(ui, _untrusted=False):
175 def _hookitems(ui, _untrusted=False):
177 """return all hooks items ready to be sorted"""
176 """return all hooks items ready to be sorted"""
178 hooks = {}
177 hooks = {}
179 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
178 for name, cmd in ui.configitems('hooks', untrusted=_untrusted):
180 if not name.startswith('priority'):
179 if not name.startswith('priority'):
181 priority = ui.configint('hooks', 'priority.%s' % name, 0)
180 priority = ui.configint('hooks', 'priority.%s' % name, 0)
182 hooks[name] = (-priority, len(hooks), name, cmd)
181 hooks[name] = (-priority, len(hooks), name, cmd)
183 return hooks
182 return hooks
184
183
185 _redirect = False
184 _redirect = False
186 def redirect(state):
185 def redirect(state):
187 global _redirect
186 global _redirect
188 _redirect = state
187 _redirect = state
189
188
190 def hook(ui, repo, name, throw=False, **args):
189 def hook(ui, repo, name, throw=False, **args):
191 if not ui.callhooks:
190 if not ui.callhooks:
192 return False
191 return False
193
192
194 hooks = []
193 hooks = []
195 for hname, cmd in _allhooks(ui):
194 for hname, cmd in _allhooks(ui):
196 if hname.split('.')[0] == name and cmd:
195 if hname.split('.')[0] == name and cmd:
197 hooks.append((hname, cmd))
196 hooks.append((hname, cmd))
198
197
199 res = runhooks(ui, repo, name, hooks, throw=throw, **args)
198 res = runhooks(ui, repo, name, hooks, throw=throw, **args)
200 r = False
199 r = False
201 for hname, cmd in hooks:
200 for hname, cmd in hooks:
202 r = res[hname][0] or r
201 r = res[hname][0] or r
203 return r
202 return r
204
203
205 def runhooks(ui, repo, name, hooks, throw=False, **args):
204 def runhooks(ui, repo, name, hooks, throw=False, **args):
206 res = {}
205 res = {}
207 oldstdout = -1
206 oldstdout = -1
208
207
209 try:
208 try:
210 for hname, cmd in hooks:
209 for hname, cmd in hooks:
211 if oldstdout == -1 and _redirect:
210 if oldstdout == -1 and _redirect:
212 try:
211 try:
213 stdoutno = util.stdout.fileno()
212 stdoutno = util.stdout.fileno()
214 stderrno = util.stderr.fileno()
213 stderrno = util.stderr.fileno()
215 # temporarily redirect stdout to stderr, if possible
214 # temporarily redirect stdout to stderr, if possible
216 if stdoutno >= 0 and stderrno >= 0:
215 if stdoutno >= 0 and stderrno >= 0:
217 util.stdout.flush()
216 util.stdout.flush()
218 oldstdout = os.dup(stdoutno)
217 oldstdout = os.dup(stdoutno)
219 os.dup2(stderrno, stdoutno)
218 os.dup2(stderrno, stdoutno)
220 except (OSError, AttributeError):
219 except (OSError, AttributeError):
221 # files seem to be bogus, give up on redirecting (WSGI, etc)
220 # files seem to be bogus, give up on redirecting (WSGI, etc)
222 pass
221 pass
223
222
224 if cmd is _fromuntrusted:
223 if cmd is _fromuntrusted:
225 if throw:
224 if throw:
226 raise error.HookAbort(
225 raise error.HookAbort(
227 _('untrusted hook %s not executed') % name,
226 _('untrusted hook %s not executed') % name,
228 hint = _("see 'hg help config.trusted'"))
227 hint = _("see 'hg help config.trusted'"))
229 ui.warn(_('warning: untrusted hook %s not executed\n') % name)
228 ui.warn(_('warning: untrusted hook %s not executed\n') % name)
230 r = 1
229 r = 1
231 raised = False
230 raised = False
232 elif callable(cmd):
231 elif callable(cmd):
233 r, raised = _pythonhook(ui, repo, name, hname, cmd, args, throw)
232 r, raised = _pythonhook(ui, repo, name, hname, cmd, args, throw)
234 elif cmd.startswith('python:'):
233 elif cmd.startswith('python:'):
235 if cmd.count(':') >= 2:
234 if cmd.count(':') >= 2:
236 path, cmd = cmd[7:].rsplit(':', 1)
235 path, cmd = cmd[7:].rsplit(':', 1)
237 path = util.expandpath(path)
236 path = util.expandpath(path)
238 if repo:
237 if repo:
239 path = os.path.join(repo.root, path)
238 path = os.path.join(repo.root, path)
240 try:
239 try:
241 mod = extensions.loadpath(path, 'hghook.%s' % hname)
240 mod = extensions.loadpath(path, 'hghook.%s' % hname)
242 except Exception:
241 except Exception:
243 ui.write(_("loading %s hook failed:\n") % hname)
242 ui.write(_("loading %s hook failed:\n") % hname)
244 raise
243 raise
245 hookfn = getattr(mod, cmd)
244 hookfn = getattr(mod, cmd)
246 else:
245 else:
247 hookfn = cmd[7:].strip()
246 hookfn = cmd[7:].strip()
248 r, raised = _pythonhook(ui, repo, name, hname, hookfn, args,
247 r, raised = _pythonhook(ui, repo, name, hname, hookfn, args,
249 throw)
248 throw)
250 else:
249 else:
251 r = _exthook(ui, repo, hname, cmd, args, throw)
250 r = _exthook(ui, repo, hname, cmd, args, throw)
252 raised = False
251 raised = False
253
252
254 res[hname] = r, raised
253 res[hname] = r, raised
255
254
256 # The stderr is fully buffered on Windows when connected to a pipe.
255 # The stderr is fully buffered on Windows when connected to a pipe.
257 # A forcible flush is required to make small stderr data in the
256 # A forcible flush is required to make small stderr data in the
258 # remote side available to the client immediately.
257 # remote side available to the client immediately.
259 util.stderr.flush()
258 util.stderr.flush()
260 finally:
259 finally:
261 if _redirect and oldstdout >= 0:
260 if _redirect and oldstdout >= 0:
262 util.stdout.flush() # write hook output to stderr fd
261 util.stdout.flush() # write hook output to stderr fd
263 os.dup2(oldstdout, stdoutno)
262 os.dup2(oldstdout, stdoutno)
264 os.close(oldstdout)
263 os.close(oldstdout)
265
264
266 return res
265 return res
@@ -1,193 +1,192 b''
1 # profiling.py - profiling functions
1 # profiling.py - profiling functions
2 #
2 #
3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2016 Gregory Szorc <gregory.szorc@gmail.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, print_function
8 from __future__ import absolute_import, print_function
9
9
10 import contextlib
10 import contextlib
11 import time
12
11
13 from .i18n import _
12 from .i18n import _
14 from . import (
13 from . import (
15 encoding,
14 encoding,
16 error,
15 error,
17 util,
16 util,
18 )
17 )
19
18
20 @contextlib.contextmanager
19 @contextlib.contextmanager
21 def lsprofile(ui, fp):
20 def lsprofile(ui, fp):
22 format = ui.config('profiling', 'format', default='text')
21 format = ui.config('profiling', 'format', default='text')
23 field = ui.config('profiling', 'sort', default='inlinetime')
22 field = ui.config('profiling', 'sort', default='inlinetime')
24 limit = ui.configint('profiling', 'limit', default=30)
23 limit = ui.configint('profiling', 'limit', default=30)
25 climit = ui.configint('profiling', 'nested', default=0)
24 climit = ui.configint('profiling', 'nested', default=0)
26
25
27 if format not in ['text', 'kcachegrind']:
26 if format not in ['text', 'kcachegrind']:
28 ui.warn(_("unrecognized profiling format '%s'"
27 ui.warn(_("unrecognized profiling format '%s'"
29 " - Ignored\n") % format)
28 " - Ignored\n") % format)
30 format = 'text'
29 format = 'text'
31
30
32 try:
31 try:
33 from . import lsprof
32 from . import lsprof
34 except ImportError:
33 except ImportError:
35 raise error.Abort(_(
34 raise error.Abort(_(
36 'lsprof not available - install from '
35 'lsprof not available - install from '
37 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
36 'http://codespeak.net/svn/user/arigo/hack/misc/lsprof/'))
38 p = lsprof.Profiler()
37 p = lsprof.Profiler()
39 p.enable(subcalls=True)
38 p.enable(subcalls=True)
40 try:
39 try:
41 yield
40 yield
42 finally:
41 finally:
43 p.disable()
42 p.disable()
44
43
45 if format == 'kcachegrind':
44 if format == 'kcachegrind':
46 from . import lsprofcalltree
45 from . import lsprofcalltree
47 calltree = lsprofcalltree.KCacheGrind(p)
46 calltree = lsprofcalltree.KCacheGrind(p)
48 calltree.output(fp)
47 calltree.output(fp)
49 else:
48 else:
50 # format == 'text'
49 # format == 'text'
51 stats = lsprof.Stats(p.getstats())
50 stats = lsprof.Stats(p.getstats())
52 stats.sort(field)
51 stats.sort(field)
53 stats.pprint(limit=limit, file=fp, climit=climit)
52 stats.pprint(limit=limit, file=fp, climit=climit)
54
53
55 @contextlib.contextmanager
54 @contextlib.contextmanager
56 def flameprofile(ui, fp):
55 def flameprofile(ui, fp):
57 try:
56 try:
58 from flamegraph import flamegraph
57 from flamegraph import flamegraph
59 except ImportError:
58 except ImportError:
60 raise error.Abort(_(
59 raise error.Abort(_(
61 'flamegraph not available - install from '
60 'flamegraph not available - install from '
62 'https://github.com/evanhempel/python-flamegraph'))
61 'https://github.com/evanhempel/python-flamegraph'))
63 # developer config: profiling.freq
62 # developer config: profiling.freq
64 freq = ui.configint('profiling', 'freq', default=1000)
63 freq = ui.configint('profiling', 'freq', default=1000)
65 filter_ = None
64 filter_ = None
66 collapse_recursion = True
65 collapse_recursion = True
67 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
66 thread = flamegraph.ProfileThread(fp, 1.0 / freq,
68 filter_, collapse_recursion)
67 filter_, collapse_recursion)
69 start_time = time.clock()
68 start_time = util.timer()
70 try:
69 try:
71 thread.start()
70 thread.start()
72 yield
71 yield
73 finally:
72 finally:
74 thread.stop()
73 thread.stop()
75 thread.join()
74 thread.join()
76 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
75 print('Collected %d stack frames (%d unique) in %2.2f seconds.' % (
77 time.clock() - start_time, thread.num_frames(),
76 util.timer() - start_time, thread.num_frames(),
78 thread.num_frames(unique=True)))
77 thread.num_frames(unique=True)))
79
78
80 @contextlib.contextmanager
79 @contextlib.contextmanager
81 def statprofile(ui, fp):
80 def statprofile(ui, fp):
82 from . import statprof
81 from . import statprof
83
82
84 freq = ui.configint('profiling', 'freq', default=1000)
83 freq = ui.configint('profiling', 'freq', default=1000)
85 if freq > 0:
84 if freq > 0:
86 # Cannot reset when profiler is already active. So silently no-op.
85 # Cannot reset when profiler is already active. So silently no-op.
87 if statprof.state.profile_level == 0:
86 if statprof.state.profile_level == 0:
88 statprof.reset(freq)
87 statprof.reset(freq)
89 else:
88 else:
90 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
89 ui.warn(_("invalid sampling frequency '%s' - ignoring\n") % freq)
91
90
92 statprof.start(mechanism='thread')
91 statprof.start(mechanism='thread')
93
92
94 try:
93 try:
95 yield
94 yield
96 finally:
95 finally:
97 data = statprof.stop()
96 data = statprof.stop()
98
97
99 profformat = ui.config('profiling', 'statformat', 'hotpath')
98 profformat = ui.config('profiling', 'statformat', 'hotpath')
100
99
101 formats = {
100 formats = {
102 'byline': statprof.DisplayFormats.ByLine,
101 'byline': statprof.DisplayFormats.ByLine,
103 'bymethod': statprof.DisplayFormats.ByMethod,
102 'bymethod': statprof.DisplayFormats.ByMethod,
104 'hotpath': statprof.DisplayFormats.Hotpath,
103 'hotpath': statprof.DisplayFormats.Hotpath,
105 'json': statprof.DisplayFormats.Json,
104 'json': statprof.DisplayFormats.Json,
106 'chrome': statprof.DisplayFormats.Chrome,
105 'chrome': statprof.DisplayFormats.Chrome,
107 }
106 }
108
107
109 if profformat in formats:
108 if profformat in formats:
110 displayformat = formats[profformat]
109 displayformat = formats[profformat]
111 else:
110 else:
112 ui.warn(_('unknown profiler output format: %s\n') % profformat)
111 ui.warn(_('unknown profiler output format: %s\n') % profformat)
113 displayformat = statprof.DisplayFormats.Hotpath
112 displayformat = statprof.DisplayFormats.Hotpath
114
113
115 kwargs = {}
114 kwargs = {}
116
115
117 def fraction(s):
116 def fraction(s):
118 if s.endswith('%'):
117 if s.endswith('%'):
119 v = float(s[:-1]) / 100
118 v = float(s[:-1]) / 100
120 else:
119 else:
121 v = float(s)
120 v = float(s)
122 if 0 <= v <= 1:
121 if 0 <= v <= 1:
123 return v
122 return v
124 raise ValueError(s)
123 raise ValueError(s)
125
124
126 if profformat == 'chrome':
125 if profformat == 'chrome':
127 showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005)
126 showmin = ui.configwith(fraction, 'profiling', 'showmin', 0.005)
128 showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999)
127 showmax = ui.configwith(fraction, 'profiling', 'showmax', 0.999)
129 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
128 kwargs.update(minthreshold=showmin, maxthreshold=showmax)
130
129
131 statprof.display(fp, data=data, format=displayformat, **kwargs)
130 statprof.display(fp, data=data, format=displayformat, **kwargs)
132
131
133 @contextlib.contextmanager
132 @contextlib.contextmanager
134 def profile(ui):
133 def profile(ui):
135 """Start profiling.
134 """Start profiling.
136
135
137 Profiling is active when the context manager is active. When the context
136 Profiling is active when the context manager is active. When the context
138 manager exits, profiling results will be written to the configured output.
137 manager exits, profiling results will be written to the configured output.
139 """
138 """
140 profiler = encoding.environ.get('HGPROF')
139 profiler = encoding.environ.get('HGPROF')
141 if profiler is None:
140 if profiler is None:
142 profiler = ui.config('profiling', 'type', default='stat')
141 profiler = ui.config('profiling', 'type', default='stat')
143 if profiler not in ('ls', 'stat', 'flame'):
142 if profiler not in ('ls', 'stat', 'flame'):
144 ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
143 ui.warn(_("unrecognized profiler '%s' - ignored\n") % profiler)
145 profiler = 'stat'
144 profiler = 'stat'
146
145
147 output = ui.config('profiling', 'output')
146 output = ui.config('profiling', 'output')
148
147
149 if output == 'blackbox':
148 if output == 'blackbox':
150 fp = util.stringio()
149 fp = util.stringio()
151 elif output:
150 elif output:
152 path = ui.expandpath(output)
151 path = ui.expandpath(output)
153 fp = open(path, 'wb')
152 fp = open(path, 'wb')
154 else:
153 else:
155 fp = ui.ferr
154 fp = ui.ferr
156
155
157 try:
156 try:
158 if profiler == 'ls':
157 if profiler == 'ls':
159 proffn = lsprofile
158 proffn = lsprofile
160 elif profiler == 'flame':
159 elif profiler == 'flame':
161 proffn = flameprofile
160 proffn = flameprofile
162 else:
161 else:
163 proffn = statprofile
162 proffn = statprofile
164
163
165 with proffn(ui, fp):
164 with proffn(ui, fp):
166 yield
165 yield
167
166
168 finally:
167 finally:
169 if output:
168 if output:
170 if output == 'blackbox':
169 if output == 'blackbox':
171 val = 'Profile:\n%s' % fp.getvalue()
170 val = 'Profile:\n%s' % fp.getvalue()
172 # ui.log treats the input as a format string,
171 # ui.log treats the input as a format string,
173 # so we need to escape any % signs.
172 # so we need to escape any % signs.
174 val = val.replace('%', '%%')
173 val = val.replace('%', '%%')
175 ui.log('profile', val)
174 ui.log('profile', val)
176 fp.close()
175 fp.close()
177
176
178 @contextlib.contextmanager
177 @contextlib.contextmanager
179 def maybeprofile(ui):
178 def maybeprofile(ui):
180 """Profile if enabled, else do nothing.
179 """Profile if enabled, else do nothing.
181
180
182 This context manager can be used to optionally profile if profiling
181 This context manager can be used to optionally profile if profiling
183 is enabled. Otherwise, it does nothing.
182 is enabled. Otherwise, it does nothing.
184
183
185 The purpose of this context manager is to make calling code simpler:
184 The purpose of this context manager is to make calling code simpler:
186 just use a single code path for calling into code you may want to profile
185 just use a single code path for calling into code you may want to profile
187 and this function determines whether to start profiling.
186 and this function determines whether to start profiling.
188 """
187 """
189 if ui.configbool('profiling', 'enabled'):
188 if ui.configbool('profiling', 'enabled'):
190 with profile(ui):
189 with profile(ui):
191 yield
190 yield
192 else:
191 else:
193 yield
192 yield
@@ -1,1102 +1,1101 b''
1 # repair.py - functions for repository repair for mercurial
1 # repair.py - functions for repository repair for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 # Copyright 2007 Matt Mackall
4 # Copyright 2007 Matt Mackall
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import hashlib
12 import hashlib
13 import stat
13 import stat
14 import tempfile
14 import tempfile
15 import time
16
15
17 from .i18n import _
16 from .i18n import _
18 from .node import short
17 from .node import short
19 from . import (
18 from . import (
20 bundle2,
19 bundle2,
21 changegroup,
20 changegroup,
22 changelog,
21 changelog,
23 error,
22 error,
24 exchange,
23 exchange,
25 manifest,
24 manifest,
26 obsolete,
25 obsolete,
27 revlog,
26 revlog,
28 scmutil,
27 scmutil,
29 util,
28 util,
30 )
29 )
31
30
32 def _bundle(repo, bases, heads, node, suffix, compress=True):
31 def _bundle(repo, bases, heads, node, suffix, compress=True):
33 """create a bundle with the specified revisions as a backup"""
32 """create a bundle with the specified revisions as a backup"""
34 cgversion = changegroup.safeversion(repo)
33 cgversion = changegroup.safeversion(repo)
35
34
36 cg = changegroup.changegroupsubset(repo, bases, heads, 'strip',
35 cg = changegroup.changegroupsubset(repo, bases, heads, 'strip',
37 version=cgversion)
36 version=cgversion)
38 backupdir = "strip-backup"
37 backupdir = "strip-backup"
39 vfs = repo.vfs
38 vfs = repo.vfs
40 if not vfs.isdir(backupdir):
39 if not vfs.isdir(backupdir):
41 vfs.mkdir(backupdir)
40 vfs.mkdir(backupdir)
42
41
43 # Include a hash of all the nodes in the filename for uniqueness
42 # Include a hash of all the nodes in the filename for uniqueness
44 allcommits = repo.set('%ln::%ln', bases, heads)
43 allcommits = repo.set('%ln::%ln', bases, heads)
45 allhashes = sorted(c.hex() for c in allcommits)
44 allhashes = sorted(c.hex() for c in allcommits)
46 totalhash = hashlib.sha1(''.join(allhashes)).hexdigest()
45 totalhash = hashlib.sha1(''.join(allhashes)).hexdigest()
47 name = "%s/%s-%s-%s.hg" % (backupdir, short(node), totalhash[:8], suffix)
46 name = "%s/%s-%s-%s.hg" % (backupdir, short(node), totalhash[:8], suffix)
48
47
49 comp = None
48 comp = None
50 if cgversion != '01':
49 if cgversion != '01':
51 bundletype = "HG20"
50 bundletype = "HG20"
52 if compress:
51 if compress:
53 comp = 'BZ'
52 comp = 'BZ'
54 elif compress:
53 elif compress:
55 bundletype = "HG10BZ"
54 bundletype = "HG10BZ"
56 else:
55 else:
57 bundletype = "HG10UN"
56 bundletype = "HG10UN"
58 return bundle2.writebundle(repo.ui, cg, name, bundletype, vfs,
57 return bundle2.writebundle(repo.ui, cg, name, bundletype, vfs,
59 compression=comp)
58 compression=comp)
60
59
61 def _collectfiles(repo, striprev):
60 def _collectfiles(repo, striprev):
62 """find out the filelogs affected by the strip"""
61 """find out the filelogs affected by the strip"""
63 files = set()
62 files = set()
64
63
65 for x in xrange(striprev, len(repo)):
64 for x in xrange(striprev, len(repo)):
66 files.update(repo[x].files())
65 files.update(repo[x].files())
67
66
68 return sorted(files)
67 return sorted(files)
69
68
70 def _collectbrokencsets(repo, files, striprev):
69 def _collectbrokencsets(repo, files, striprev):
71 """return the changesets which will be broken by the truncation"""
70 """return the changesets which will be broken by the truncation"""
72 s = set()
71 s = set()
73 def collectone(revlog):
72 def collectone(revlog):
74 _, brokenset = revlog.getstrippoint(striprev)
73 _, brokenset = revlog.getstrippoint(striprev)
75 s.update([revlog.linkrev(r) for r in brokenset])
74 s.update([revlog.linkrev(r) for r in brokenset])
76
75
77 collectone(repo.manifestlog._revlog)
76 collectone(repo.manifestlog._revlog)
78 for fname in files:
77 for fname in files:
79 collectone(repo.file(fname))
78 collectone(repo.file(fname))
80
79
81 return s
80 return s
82
81
83 def strip(ui, repo, nodelist, backup=True, topic='backup'):
82 def strip(ui, repo, nodelist, backup=True, topic='backup'):
84 # This function operates within a transaction of its own, but does
83 # This function operates within a transaction of its own, but does
85 # not take any lock on the repo.
84 # not take any lock on the repo.
86 # Simple way to maintain backwards compatibility for this
85 # Simple way to maintain backwards compatibility for this
87 # argument.
86 # argument.
88 if backup in ['none', 'strip']:
87 if backup in ['none', 'strip']:
89 backup = False
88 backup = False
90
89
91 repo = repo.unfiltered()
90 repo = repo.unfiltered()
92 repo.destroying()
91 repo.destroying()
93
92
94 cl = repo.changelog
93 cl = repo.changelog
95 # TODO handle undo of merge sets
94 # TODO handle undo of merge sets
96 if isinstance(nodelist, str):
95 if isinstance(nodelist, str):
97 nodelist = [nodelist]
96 nodelist = [nodelist]
98 striplist = [cl.rev(node) for node in nodelist]
97 striplist = [cl.rev(node) for node in nodelist]
99 striprev = min(striplist)
98 striprev = min(striplist)
100
99
101 files = _collectfiles(repo, striprev)
100 files = _collectfiles(repo, striprev)
102 saverevs = _collectbrokencsets(repo, files, striprev)
101 saverevs = _collectbrokencsets(repo, files, striprev)
103
102
104 # Some revisions with rev > striprev may not be descendants of striprev.
103 # Some revisions with rev > striprev may not be descendants of striprev.
105 # We have to find these revisions and put them in a bundle, so that
104 # We have to find these revisions and put them in a bundle, so that
106 # we can restore them after the truncations.
105 # we can restore them after the truncations.
107 # To create the bundle we use repo.changegroupsubset which requires
106 # To create the bundle we use repo.changegroupsubset which requires
108 # the list of heads and bases of the set of interesting revisions.
107 # the list of heads and bases of the set of interesting revisions.
109 # (head = revision in the set that has no descendant in the set;
108 # (head = revision in the set that has no descendant in the set;
110 # base = revision in the set that has no ancestor in the set)
109 # base = revision in the set that has no ancestor in the set)
111 tostrip = set(striplist)
110 tostrip = set(striplist)
112 saveheads = set(saverevs)
111 saveheads = set(saverevs)
113 for r in cl.revs(start=striprev + 1):
112 for r in cl.revs(start=striprev + 1):
114 if any(p in tostrip for p in cl.parentrevs(r)):
113 if any(p in tostrip for p in cl.parentrevs(r)):
115 tostrip.add(r)
114 tostrip.add(r)
116
115
117 if r not in tostrip:
116 if r not in tostrip:
118 saverevs.add(r)
117 saverevs.add(r)
119 saveheads.difference_update(cl.parentrevs(r))
118 saveheads.difference_update(cl.parentrevs(r))
120 saveheads.add(r)
119 saveheads.add(r)
121 saveheads = [cl.node(r) for r in saveheads]
120 saveheads = [cl.node(r) for r in saveheads]
122
121
123 # compute base nodes
122 # compute base nodes
124 if saverevs:
123 if saverevs:
125 descendants = set(cl.descendants(saverevs))
124 descendants = set(cl.descendants(saverevs))
126 saverevs.difference_update(descendants)
125 saverevs.difference_update(descendants)
127 savebases = [cl.node(r) for r in saverevs]
126 savebases = [cl.node(r) for r in saverevs]
128 stripbases = [cl.node(r) for r in tostrip]
127 stripbases = [cl.node(r) for r in tostrip]
129
128
130 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)), but
129 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)), but
131 # is much faster
130 # is much faster
132 newbmtarget = repo.revs('max(parents(%ld) - (%ld))', tostrip, tostrip)
131 newbmtarget = repo.revs('max(parents(%ld) - (%ld))', tostrip, tostrip)
133 if newbmtarget:
132 if newbmtarget:
134 newbmtarget = repo[newbmtarget.first()].node()
133 newbmtarget = repo[newbmtarget.first()].node()
135 else:
134 else:
136 newbmtarget = '.'
135 newbmtarget = '.'
137
136
138 bm = repo._bookmarks
137 bm = repo._bookmarks
139 updatebm = []
138 updatebm = []
140 for m in bm:
139 for m in bm:
141 rev = repo[bm[m]].rev()
140 rev = repo[bm[m]].rev()
142 if rev in tostrip:
141 if rev in tostrip:
143 updatebm.append(m)
142 updatebm.append(m)
144
143
145 # create a changegroup for all the branches we need to keep
144 # create a changegroup for all the branches we need to keep
146 backupfile = None
145 backupfile = None
147 vfs = repo.vfs
146 vfs = repo.vfs
148 node = nodelist[-1]
147 node = nodelist[-1]
149 if backup:
148 if backup:
150 backupfile = _bundle(repo, stripbases, cl.heads(), node, topic)
149 backupfile = _bundle(repo, stripbases, cl.heads(), node, topic)
151 repo.ui.status(_("saved backup bundle to %s\n") %
150 repo.ui.status(_("saved backup bundle to %s\n") %
152 vfs.join(backupfile))
151 vfs.join(backupfile))
153 repo.ui.log("backupbundle", "saved backup bundle to %s\n",
152 repo.ui.log("backupbundle", "saved backup bundle to %s\n",
154 vfs.join(backupfile))
153 vfs.join(backupfile))
155 tmpbundlefile = None
154 tmpbundlefile = None
156 if saveheads:
155 if saveheads:
157 # do not compress temporary bundle if we remove it from disk later
156 # do not compress temporary bundle if we remove it from disk later
158 tmpbundlefile = _bundle(repo, savebases, saveheads, node, 'temp',
157 tmpbundlefile = _bundle(repo, savebases, saveheads, node, 'temp',
159 compress=False)
158 compress=False)
160
159
161 mfst = repo.manifestlog._revlog
160 mfst = repo.manifestlog._revlog
162
161
163 curtr = repo.currenttransaction()
162 curtr = repo.currenttransaction()
164 if curtr is not None:
163 if curtr is not None:
165 del curtr # avoid carrying reference to transaction for nothing
164 del curtr # avoid carrying reference to transaction for nothing
166 msg = _('programming error: cannot strip from inside a transaction')
165 msg = _('programming error: cannot strip from inside a transaction')
167 raise error.Abort(msg, hint=_('contact your extension maintainer'))
166 raise error.Abort(msg, hint=_('contact your extension maintainer'))
168
167
169 try:
168 try:
170 with repo.transaction("strip") as tr:
169 with repo.transaction("strip") as tr:
171 offset = len(tr.entries)
170 offset = len(tr.entries)
172
171
173 tr.startgroup()
172 tr.startgroup()
174 cl.strip(striprev, tr)
173 cl.strip(striprev, tr)
175 mfst.strip(striprev, tr)
174 mfst.strip(striprev, tr)
176 if 'treemanifest' in repo.requirements: # safe but unnecessary
175 if 'treemanifest' in repo.requirements: # safe but unnecessary
177 # otherwise
176 # otherwise
178 for unencoded, encoded, size in repo.store.datafiles():
177 for unencoded, encoded, size in repo.store.datafiles():
179 if (unencoded.startswith('meta/') and
178 if (unencoded.startswith('meta/') and
180 unencoded.endswith('00manifest.i')):
179 unencoded.endswith('00manifest.i')):
181 dir = unencoded[5:-12]
180 dir = unencoded[5:-12]
182 repo.manifestlog._revlog.dirlog(dir).strip(striprev, tr)
181 repo.manifestlog._revlog.dirlog(dir).strip(striprev, tr)
183 for fn in files:
182 for fn in files:
184 repo.file(fn).strip(striprev, tr)
183 repo.file(fn).strip(striprev, tr)
185 tr.endgroup()
184 tr.endgroup()
186
185
187 for i in xrange(offset, len(tr.entries)):
186 for i in xrange(offset, len(tr.entries)):
188 file, troffset, ignore = tr.entries[i]
187 file, troffset, ignore = tr.entries[i]
189 with repo.svfs(file, 'a', checkambig=True) as fp:
188 with repo.svfs(file, 'a', checkambig=True) as fp:
190 fp.truncate(troffset)
189 fp.truncate(troffset)
191 if troffset == 0:
190 if troffset == 0:
192 repo.store.markremoved(file)
191 repo.store.markremoved(file)
193
192
194 if tmpbundlefile:
193 if tmpbundlefile:
195 ui.note(_("adding branch\n"))
194 ui.note(_("adding branch\n"))
196 f = vfs.open(tmpbundlefile, "rb")
195 f = vfs.open(tmpbundlefile, "rb")
197 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
196 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
198 if not repo.ui.verbose:
197 if not repo.ui.verbose:
199 # silence internal shuffling chatter
198 # silence internal shuffling chatter
200 repo.ui.pushbuffer()
199 repo.ui.pushbuffer()
201 if isinstance(gen, bundle2.unbundle20):
200 if isinstance(gen, bundle2.unbundle20):
202 with repo.transaction('strip') as tr:
201 with repo.transaction('strip') as tr:
203 tr.hookargs = {'source': 'strip',
202 tr.hookargs = {'source': 'strip',
204 'url': 'bundle:' + vfs.join(tmpbundlefile)}
203 'url': 'bundle:' + vfs.join(tmpbundlefile)}
205 bundle2.applybundle(repo, gen, tr, source='strip',
204 bundle2.applybundle(repo, gen, tr, source='strip',
206 url='bundle:' + vfs.join(tmpbundlefile))
205 url='bundle:' + vfs.join(tmpbundlefile))
207 else:
206 else:
208 gen.apply(repo, 'strip', 'bundle:' + vfs.join(tmpbundlefile),
207 gen.apply(repo, 'strip', 'bundle:' + vfs.join(tmpbundlefile),
209 True)
208 True)
210 if not repo.ui.verbose:
209 if not repo.ui.verbose:
211 repo.ui.popbuffer()
210 repo.ui.popbuffer()
212 f.close()
211 f.close()
213 repo._phasecache.invalidate()
212 repo._phasecache.invalidate()
214
213
215 for m in updatebm:
214 for m in updatebm:
216 bm[m] = repo[newbmtarget].node()
215 bm[m] = repo[newbmtarget].node()
217 lock = tr = None
216 lock = tr = None
218 try:
217 try:
219 lock = repo.lock()
218 lock = repo.lock()
220 tr = repo.transaction('repair')
219 tr = repo.transaction('repair')
221 bm.recordchange(tr)
220 bm.recordchange(tr)
222 tr.close()
221 tr.close()
223 finally:
222 finally:
224 tr.release()
223 tr.release()
225 lock.release()
224 lock.release()
226
225
227 # remove undo files
226 # remove undo files
228 for undovfs, undofile in repo.undofiles():
227 for undovfs, undofile in repo.undofiles():
229 try:
228 try:
230 undovfs.unlink(undofile)
229 undovfs.unlink(undofile)
231 except OSError as e:
230 except OSError as e:
232 if e.errno != errno.ENOENT:
231 if e.errno != errno.ENOENT:
233 ui.warn(_('error removing %s: %s\n') %
232 ui.warn(_('error removing %s: %s\n') %
234 (undovfs.join(undofile), str(e)))
233 (undovfs.join(undofile), str(e)))
235
234
236 except: # re-raises
235 except: # re-raises
237 if backupfile:
236 if backupfile:
238 ui.warn(_("strip failed, backup bundle stored in '%s'\n")
237 ui.warn(_("strip failed, backup bundle stored in '%s'\n")
239 % vfs.join(backupfile))
238 % vfs.join(backupfile))
240 if tmpbundlefile:
239 if tmpbundlefile:
241 ui.warn(_("strip failed, unrecovered changes stored in '%s'\n")
240 ui.warn(_("strip failed, unrecovered changes stored in '%s'\n")
242 % vfs.join(tmpbundlefile))
241 % vfs.join(tmpbundlefile))
243 ui.warn(_("(fix the problem, then recover the changesets with "
242 ui.warn(_("(fix the problem, then recover the changesets with "
244 "\"hg unbundle '%s'\")\n") % vfs.join(tmpbundlefile))
243 "\"hg unbundle '%s'\")\n") % vfs.join(tmpbundlefile))
245 raise
244 raise
246 else:
245 else:
247 if tmpbundlefile:
246 if tmpbundlefile:
248 # Remove temporary bundle only if there were no exceptions
247 # Remove temporary bundle only if there were no exceptions
249 vfs.unlink(tmpbundlefile)
248 vfs.unlink(tmpbundlefile)
250
249
251 repo.destroyed()
250 repo.destroyed()
252 # return the backup file path (or None if 'backup' was False) so
251 # return the backup file path (or None if 'backup' was False) so
253 # extensions can use it
252 # extensions can use it
254 return backupfile
253 return backupfile
255
254
256 def rebuildfncache(ui, repo):
255 def rebuildfncache(ui, repo):
257 """Rebuilds the fncache file from repo history.
256 """Rebuilds the fncache file from repo history.
258
257
259 Missing entries will be added. Extra entries will be removed.
258 Missing entries will be added. Extra entries will be removed.
260 """
259 """
261 repo = repo.unfiltered()
260 repo = repo.unfiltered()
262
261
263 if 'fncache' not in repo.requirements:
262 if 'fncache' not in repo.requirements:
264 ui.warn(_('(not rebuilding fncache because repository does not '
263 ui.warn(_('(not rebuilding fncache because repository does not '
265 'support fncache)\n'))
264 'support fncache)\n'))
266 return
265 return
267
266
268 with repo.lock():
267 with repo.lock():
269 fnc = repo.store.fncache
268 fnc = repo.store.fncache
270 # Trigger load of fncache.
269 # Trigger load of fncache.
271 if 'irrelevant' in fnc:
270 if 'irrelevant' in fnc:
272 pass
271 pass
273
272
274 oldentries = set(fnc.entries)
273 oldentries = set(fnc.entries)
275 newentries = set()
274 newentries = set()
276 seenfiles = set()
275 seenfiles = set()
277
276
278 repolen = len(repo)
277 repolen = len(repo)
279 for rev in repo:
278 for rev in repo:
280 ui.progress(_('rebuilding'), rev, total=repolen,
279 ui.progress(_('rebuilding'), rev, total=repolen,
281 unit=_('changesets'))
280 unit=_('changesets'))
282
281
283 ctx = repo[rev]
282 ctx = repo[rev]
284 for f in ctx.files():
283 for f in ctx.files():
285 # This is to minimize I/O.
284 # This is to minimize I/O.
286 if f in seenfiles:
285 if f in seenfiles:
287 continue
286 continue
288 seenfiles.add(f)
287 seenfiles.add(f)
289
288
290 i = 'data/%s.i' % f
289 i = 'data/%s.i' % f
291 d = 'data/%s.d' % f
290 d = 'data/%s.d' % f
292
291
293 if repo.store._exists(i):
292 if repo.store._exists(i):
294 newentries.add(i)
293 newentries.add(i)
295 if repo.store._exists(d):
294 if repo.store._exists(d):
296 newentries.add(d)
295 newentries.add(d)
297
296
298 ui.progress(_('rebuilding'), None)
297 ui.progress(_('rebuilding'), None)
299
298
300 if 'treemanifest' in repo.requirements: # safe but unnecessary otherwise
299 if 'treemanifest' in repo.requirements: # safe but unnecessary otherwise
301 for dir in util.dirs(seenfiles):
300 for dir in util.dirs(seenfiles):
302 i = 'meta/%s/00manifest.i' % dir
301 i = 'meta/%s/00manifest.i' % dir
303 d = 'meta/%s/00manifest.d' % dir
302 d = 'meta/%s/00manifest.d' % dir
304
303
305 if repo.store._exists(i):
304 if repo.store._exists(i):
306 newentries.add(i)
305 newentries.add(i)
307 if repo.store._exists(d):
306 if repo.store._exists(d):
308 newentries.add(d)
307 newentries.add(d)
309
308
310 addcount = len(newentries - oldentries)
309 addcount = len(newentries - oldentries)
311 removecount = len(oldentries - newentries)
310 removecount = len(oldentries - newentries)
312 for p in sorted(oldentries - newentries):
311 for p in sorted(oldentries - newentries):
313 ui.write(_('removing %s\n') % p)
312 ui.write(_('removing %s\n') % p)
314 for p in sorted(newentries - oldentries):
313 for p in sorted(newentries - oldentries):
315 ui.write(_('adding %s\n') % p)
314 ui.write(_('adding %s\n') % p)
316
315
317 if addcount or removecount:
316 if addcount or removecount:
318 ui.write(_('%d items added, %d removed from fncache\n') %
317 ui.write(_('%d items added, %d removed from fncache\n') %
319 (addcount, removecount))
318 (addcount, removecount))
320 fnc.entries = newentries
319 fnc.entries = newentries
321 fnc._dirty = True
320 fnc._dirty = True
322
321
323 with repo.transaction('fncache') as tr:
322 with repo.transaction('fncache') as tr:
324 fnc.write(tr)
323 fnc.write(tr)
325 else:
324 else:
326 ui.write(_('fncache already up to date\n'))
325 ui.write(_('fncache already up to date\n'))
327
326
328 def stripbmrevset(repo, mark):
327 def stripbmrevset(repo, mark):
329 """
328 """
330 The revset to strip when strip is called with -B mark
329 The revset to strip when strip is called with -B mark
331
330
332 Needs to live here so extensions can use it and wrap it even when strip is
331 Needs to live here so extensions can use it and wrap it even when strip is
333 not enabled or not present on a box.
332 not enabled or not present on a box.
334 """
333 """
335 return repo.revs("ancestors(bookmark(%s)) - "
334 return repo.revs("ancestors(bookmark(%s)) - "
336 "ancestors(head() and not bookmark(%s)) - "
335 "ancestors(head() and not bookmark(%s)) - "
337 "ancestors(bookmark() and not bookmark(%s))",
336 "ancestors(bookmark() and not bookmark(%s))",
338 mark, mark, mark)
337 mark, mark, mark)
339
338
340 def deleteobsmarkers(obsstore, indices):
339 def deleteobsmarkers(obsstore, indices):
341 """Delete some obsmarkers from obsstore and return how many were deleted
340 """Delete some obsmarkers from obsstore and return how many were deleted
342
341
343 'indices' is a list of ints which are the indices
342 'indices' is a list of ints which are the indices
344 of the markers to be deleted.
343 of the markers to be deleted.
345
344
346 Every invocation of this function completely rewrites the obsstore file,
345 Every invocation of this function completely rewrites the obsstore file,
347 skipping the markers we want to be removed. The new temporary file is
346 skipping the markers we want to be removed. The new temporary file is
348 created, remaining markers are written there and on .close() this file
347 created, remaining markers are written there and on .close() this file
349 gets atomically renamed to obsstore, thus guaranteeing consistency."""
348 gets atomically renamed to obsstore, thus guaranteeing consistency."""
350 if not indices:
349 if not indices:
351 # we don't want to rewrite the obsstore with the same content
350 # we don't want to rewrite the obsstore with the same content
352 return
351 return
353
352
354 left = []
353 left = []
355 current = obsstore._all
354 current = obsstore._all
356 n = 0
355 n = 0
357 for i, m in enumerate(current):
356 for i, m in enumerate(current):
358 if i in indices:
357 if i in indices:
359 n += 1
358 n += 1
360 continue
359 continue
361 left.append(m)
360 left.append(m)
362
361
363 newobsstorefile = obsstore.svfs('obsstore', 'w', atomictemp=True)
362 newobsstorefile = obsstore.svfs('obsstore', 'w', atomictemp=True)
364 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
363 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
365 newobsstorefile.write(bytes)
364 newobsstorefile.write(bytes)
366 newobsstorefile.close()
365 newobsstorefile.close()
367 return n
366 return n
368
367
369 def upgraderequiredsourcerequirements(repo):
368 def upgraderequiredsourcerequirements(repo):
370 """Obtain requirements required to be present to upgrade a repo.
369 """Obtain requirements required to be present to upgrade a repo.
371
370
372 An upgrade will not be allowed if the repository doesn't have the
371 An upgrade will not be allowed if the repository doesn't have the
373 requirements returned by this function.
372 requirements returned by this function.
374 """
373 """
375 return set([
374 return set([
376 # Introduced in Mercurial 0.9.2.
375 # Introduced in Mercurial 0.9.2.
377 'revlogv1',
376 'revlogv1',
378 # Introduced in Mercurial 0.9.2.
377 # Introduced in Mercurial 0.9.2.
379 'store',
378 'store',
380 ])
379 ])
381
380
382 def upgradeblocksourcerequirements(repo):
381 def upgradeblocksourcerequirements(repo):
383 """Obtain requirements that will prevent an upgrade from occurring.
382 """Obtain requirements that will prevent an upgrade from occurring.
384
383
385 An upgrade cannot be performed if the source repository contains a
384 An upgrade cannot be performed if the source repository contains a
386 requirements in the returned set.
385 requirements in the returned set.
387 """
386 """
388 return set([
387 return set([
389 # The upgrade code does not yet support these experimental features.
388 # The upgrade code does not yet support these experimental features.
390 # This is an artificial limitation.
389 # This is an artificial limitation.
391 'manifestv2',
390 'manifestv2',
392 'treemanifest',
391 'treemanifest',
393 # This was a precursor to generaldelta and was never enabled by default.
392 # This was a precursor to generaldelta and was never enabled by default.
394 # It should (hopefully) not exist in the wild.
393 # It should (hopefully) not exist in the wild.
395 'parentdelta',
394 'parentdelta',
396 # Upgrade should operate on the actual store, not the shared link.
395 # Upgrade should operate on the actual store, not the shared link.
397 'shared',
396 'shared',
398 ])
397 ])
399
398
400 def upgradesupportremovedrequirements(repo):
399 def upgradesupportremovedrequirements(repo):
401 """Obtain requirements that can be removed during an upgrade.
400 """Obtain requirements that can be removed during an upgrade.
402
401
403 If an upgrade were to create a repository that dropped a requirement,
402 If an upgrade were to create a repository that dropped a requirement,
404 the dropped requirement must appear in the returned set for the upgrade
403 the dropped requirement must appear in the returned set for the upgrade
405 to be allowed.
404 to be allowed.
406 """
405 """
407 return set()
406 return set()
408
407
409 def upgradesupporteddestrequirements(repo):
408 def upgradesupporteddestrequirements(repo):
410 """Obtain requirements that upgrade supports in the destination.
409 """Obtain requirements that upgrade supports in the destination.
411
410
412 If the result of the upgrade would create requirements not in this set,
411 If the result of the upgrade would create requirements not in this set,
413 the upgrade is disallowed.
412 the upgrade is disallowed.
414
413
415 Extensions should monkeypatch this to add their custom requirements.
414 Extensions should monkeypatch this to add their custom requirements.
416 """
415 """
417 return set([
416 return set([
418 'dotencode',
417 'dotencode',
419 'fncache',
418 'fncache',
420 'generaldelta',
419 'generaldelta',
421 'revlogv1',
420 'revlogv1',
422 'store',
421 'store',
423 ])
422 ])
424
423
425 def upgradeallowednewrequirements(repo):
424 def upgradeallowednewrequirements(repo):
426 """Obtain requirements that can be added to a repository during upgrade.
425 """Obtain requirements that can be added to a repository during upgrade.
427
426
428 This is used to disallow proposed requirements from being added when
427 This is used to disallow proposed requirements from being added when
429 they weren't present before.
428 they weren't present before.
430
429
431 We use a list of allowed requirement additions instead of a list of known
430 We use a list of allowed requirement additions instead of a list of known
432 bad additions because the whitelist approach is safer and will prevent
431 bad additions because the whitelist approach is safer and will prevent
433 future, unknown requirements from accidentally being added.
432 future, unknown requirements from accidentally being added.
434 """
433 """
435 return set([
434 return set([
436 'dotencode',
435 'dotencode',
437 'fncache',
436 'fncache',
438 'generaldelta',
437 'generaldelta',
439 ])
438 ])
440
439
441 deficiency = 'deficiency'
440 deficiency = 'deficiency'
442 optimisation = 'optimization'
441 optimisation = 'optimization'
443
442
444 class upgradeimprovement(object):
443 class upgradeimprovement(object):
445 """Represents an improvement that can be made as part of an upgrade.
444 """Represents an improvement that can be made as part of an upgrade.
446
445
447 The following attributes are defined on each instance:
446 The following attributes are defined on each instance:
448
447
449 name
448 name
450 Machine-readable string uniquely identifying this improvement. It
449 Machine-readable string uniquely identifying this improvement. It
451 will be mapped to an action later in the upgrade process.
450 will be mapped to an action later in the upgrade process.
452
451
453 type
452 type
454 Either ``deficiency`` or ``optimisation``. A deficiency is an obvious
453 Either ``deficiency`` or ``optimisation``. A deficiency is an obvious
455 problem. An optimization is an action (sometimes optional) that
454 problem. An optimization is an action (sometimes optional) that
456 can be taken to further improve the state of the repository.
455 can be taken to further improve the state of the repository.
457
456
458 description
457 description
459 Message intended for humans explaining the improvement in more detail,
458 Message intended for humans explaining the improvement in more detail,
460 including the implications of it. For ``deficiency`` types, should be
459 including the implications of it. For ``deficiency`` types, should be
461 worded in the present tense. For ``optimisation`` types, should be
460 worded in the present tense. For ``optimisation`` types, should be
462 worded in the future tense.
461 worded in the future tense.
463
462
464 upgrademessage
463 upgrademessage
465 Message intended for humans explaining what an upgrade addressing this
464 Message intended for humans explaining what an upgrade addressing this
466 issue will do. Should be worded in the future tense.
465 issue will do. Should be worded in the future tense.
467
466
468 fromdefault (``deficiency`` types only)
467 fromdefault (``deficiency`` types only)
469 Boolean indicating whether the current (deficient) state deviates
468 Boolean indicating whether the current (deficient) state deviates
470 from Mercurial's default configuration.
469 from Mercurial's default configuration.
471
470
472 fromconfig (``deficiency`` types only)
471 fromconfig (``deficiency`` types only)
473 Boolean indicating whether the current (deficient) state deviates
472 Boolean indicating whether the current (deficient) state deviates
474 from the current Mercurial configuration.
473 from the current Mercurial configuration.
475 """
474 """
476 def __init__(self, name, type, description, upgrademessage, **kwargs):
475 def __init__(self, name, type, description, upgrademessage, **kwargs):
477 self.name = name
476 self.name = name
478 self.type = type
477 self.type = type
479 self.description = description
478 self.description = description
480 self.upgrademessage = upgrademessage
479 self.upgrademessage = upgrademessage
481
480
482 for k, v in kwargs.items():
481 for k, v in kwargs.items():
483 setattr(self, k, v)
482 setattr(self, k, v)
484
483
485 def upgradefindimprovements(repo):
484 def upgradefindimprovements(repo):
486 """Determine improvements that can be made to the repo during upgrade.
485 """Determine improvements that can be made to the repo during upgrade.
487
486
488 Returns a list of ``upgradeimprovement`` describing repository deficiencies
487 Returns a list of ``upgradeimprovement`` describing repository deficiencies
489 and optimizations.
488 and optimizations.
490 """
489 """
491 # Avoid cycle: cmdutil -> repair -> localrepo -> cmdutil
490 # Avoid cycle: cmdutil -> repair -> localrepo -> cmdutil
492 from . import localrepo
491 from . import localrepo
493
492
494 newreporeqs = localrepo.newreporequirements(repo)
493 newreporeqs = localrepo.newreporequirements(repo)
495
494
496 improvements = []
495 improvements = []
497
496
498 # We could detect lack of revlogv1 and store here, but they were added
497 # We could detect lack of revlogv1 and store here, but they were added
499 # in 0.9.2 and we don't support upgrading repos without these
498 # in 0.9.2 and we don't support upgrading repos without these
500 # requirements, so let's not bother.
499 # requirements, so let's not bother.
501
500
502 if 'fncache' not in repo.requirements:
501 if 'fncache' not in repo.requirements:
503 improvements.append(upgradeimprovement(
502 improvements.append(upgradeimprovement(
504 name='fncache',
503 name='fncache',
505 type=deficiency,
504 type=deficiency,
506 description=_('long and reserved filenames may not work correctly; '
505 description=_('long and reserved filenames may not work correctly; '
507 'repository performance is sub-optimal'),
506 'repository performance is sub-optimal'),
508 upgrademessage=_('repository will be more resilient to storing '
507 upgrademessage=_('repository will be more resilient to storing '
509 'certain paths and performance of certain '
508 'certain paths and performance of certain '
510 'operations should be improved'),
509 'operations should be improved'),
511 fromdefault=True,
510 fromdefault=True,
512 fromconfig='fncache' in newreporeqs))
511 fromconfig='fncache' in newreporeqs))
513
512
514 if 'dotencode' not in repo.requirements:
513 if 'dotencode' not in repo.requirements:
515 improvements.append(upgradeimprovement(
514 improvements.append(upgradeimprovement(
516 name='dotencode',
515 name='dotencode',
517 type=deficiency,
516 type=deficiency,
518 description=_('storage of filenames beginning with a period or '
517 description=_('storage of filenames beginning with a period or '
519 'space may not work correctly'),
518 'space may not work correctly'),
520 upgrademessage=_('repository will be better able to store files '
519 upgrademessage=_('repository will be better able to store files '
521 'beginning with a space or period'),
520 'beginning with a space or period'),
522 fromdefault=True,
521 fromdefault=True,
523 fromconfig='dotencode' in newreporeqs))
522 fromconfig='dotencode' in newreporeqs))
524
523
525 if 'generaldelta' not in repo.requirements:
524 if 'generaldelta' not in repo.requirements:
526 improvements.append(upgradeimprovement(
525 improvements.append(upgradeimprovement(
527 name='generaldelta',
526 name='generaldelta',
528 type=deficiency,
527 type=deficiency,
529 description=_('deltas within internal storage are unable to '
528 description=_('deltas within internal storage are unable to '
530 'choose optimal revisions; repository is larger and '
529 'choose optimal revisions; repository is larger and '
531 'slower than it could be; interaction with other '
530 'slower than it could be; interaction with other '
532 'repositories may require extra network and CPU '
531 'repositories may require extra network and CPU '
533 'resources, making "hg push" and "hg pull" slower'),
532 'resources, making "hg push" and "hg pull" slower'),
534 upgrademessage=_('repository storage will be able to create '
533 upgrademessage=_('repository storage will be able to create '
535 'optimal deltas; new repository data will be '
534 'optimal deltas; new repository data will be '
536 'smaller and read times should decrease; '
535 'smaller and read times should decrease; '
537 'interacting with other repositories using this '
536 'interacting with other repositories using this '
538 'storage model should require less network and '
537 'storage model should require less network and '
539 'CPU resources, making "hg push" and "hg pull" '
538 'CPU resources, making "hg push" and "hg pull" '
540 'faster'),
539 'faster'),
541 fromdefault=True,
540 fromdefault=True,
542 fromconfig='generaldelta' in newreporeqs))
541 fromconfig='generaldelta' in newreporeqs))
543
542
544 # Mercurial 4.0 changed changelogs to not use delta chains. Search for
543 # Mercurial 4.0 changed changelogs to not use delta chains. Search for
545 # changelogs with deltas.
544 # changelogs with deltas.
546 cl = repo.changelog
545 cl = repo.changelog
547 for rev in cl:
546 for rev in cl:
548 chainbase = cl.chainbase(rev)
547 chainbase = cl.chainbase(rev)
549 if chainbase != rev:
548 if chainbase != rev:
550 improvements.append(upgradeimprovement(
549 improvements.append(upgradeimprovement(
551 name='removecldeltachain',
550 name='removecldeltachain',
552 type=deficiency,
551 type=deficiency,
553 description=_('changelog storage is using deltas instead of '
552 description=_('changelog storage is using deltas instead of '
554 'raw entries; changelog reading and any '
553 'raw entries; changelog reading and any '
555 'operation relying on changelog data are slower '
554 'operation relying on changelog data are slower '
556 'than they could be'),
555 'than they could be'),
557 upgrademessage=_('changelog storage will be reformated to '
556 upgrademessage=_('changelog storage will be reformated to '
558 'store raw entries; changelog reading will be '
557 'store raw entries; changelog reading will be '
559 'faster; changelog size may be reduced'),
558 'faster; changelog size may be reduced'),
560 fromdefault=True,
559 fromdefault=True,
561 fromconfig=True))
560 fromconfig=True))
562 break
561 break
563
562
564 # Now for the optimizations.
563 # Now for the optimizations.
565
564
566 # These are unconditionally added. There is logic later that figures out
565 # These are unconditionally added. There is logic later that figures out
567 # which ones to apply.
566 # which ones to apply.
568
567
569 improvements.append(upgradeimprovement(
568 improvements.append(upgradeimprovement(
570 name='redeltaparent',
569 name='redeltaparent',
571 type=optimisation,
570 type=optimisation,
572 description=_('deltas within internal storage will be recalculated to '
571 description=_('deltas within internal storage will be recalculated to '
573 'choose an optimal base revision where this was not '
572 'choose an optimal base revision where this was not '
574 'already done; the size of the repository may shrink and '
573 'already done; the size of the repository may shrink and '
575 'various operations may become faster; the first time '
574 'various operations may become faster; the first time '
576 'this optimization is performed could slow down upgrade '
575 'this optimization is performed could slow down upgrade '
577 'execution considerably; subsequent invocations should '
576 'execution considerably; subsequent invocations should '
578 'not run noticeably slower'),
577 'not run noticeably slower'),
579 upgrademessage=_('deltas within internal storage will choose a new '
578 upgrademessage=_('deltas within internal storage will choose a new '
580 'base revision if needed')))
579 'base revision if needed')))
581
580
582 improvements.append(upgradeimprovement(
581 improvements.append(upgradeimprovement(
583 name='redeltamultibase',
582 name='redeltamultibase',
584 type=optimisation,
583 type=optimisation,
585 description=_('deltas within internal storage will be recalculated '
584 description=_('deltas within internal storage will be recalculated '
586 'against multiple base revision and the smallest '
585 'against multiple base revision and the smallest '
587 'difference will be used; the size of the repository may '
586 'difference will be used; the size of the repository may '
588 'shrink significantly when there are many merges; this '
587 'shrink significantly when there are many merges; this '
589 'optimization will slow down execution in proportion to '
588 'optimization will slow down execution in proportion to '
590 'the number of merges in the repository and the amount '
589 'the number of merges in the repository and the amount '
591 'of files in the repository; this slow down should not '
590 'of files in the repository; this slow down should not '
592 'be significant unless there are tens of thousands of '
591 'be significant unless there are tens of thousands of '
593 'files and thousands of merges'),
592 'files and thousands of merges'),
594 upgrademessage=_('deltas within internal storage will choose an '
593 upgrademessage=_('deltas within internal storage will choose an '
595 'optimal delta by computing deltas against multiple '
594 'optimal delta by computing deltas against multiple '
596 'parents; may slow down execution time '
595 'parents; may slow down execution time '
597 'significantly')))
596 'significantly')))
598
597
599 improvements.append(upgradeimprovement(
598 improvements.append(upgradeimprovement(
600 name='redeltaall',
599 name='redeltaall',
601 type=optimisation,
600 type=optimisation,
602 description=_('deltas within internal storage will always be '
601 description=_('deltas within internal storage will always be '
603 'recalculated without reusing prior deltas; this will '
602 'recalculated without reusing prior deltas; this will '
604 'likely make execution run several times slower; this '
603 'likely make execution run several times slower; this '
605 'optimization is typically not needed'),
604 'optimization is typically not needed'),
606 upgrademessage=_('deltas within internal storage will be fully '
605 upgrademessage=_('deltas within internal storage will be fully '
607 'recomputed; this will likely drastically slow down '
606 'recomputed; this will likely drastically slow down '
608 'execution time')))
607 'execution time')))
609
608
610 return improvements
609 return improvements
611
610
612 def upgradedetermineactions(repo, improvements, sourcereqs, destreqs,
611 def upgradedetermineactions(repo, improvements, sourcereqs, destreqs,
613 optimize):
612 optimize):
614 """Determine upgrade actions that will be performed.
613 """Determine upgrade actions that will be performed.
615
614
616 Given a list of improvements as returned by ``upgradefindimprovements``,
615 Given a list of improvements as returned by ``upgradefindimprovements``,
617 determine the list of upgrade actions that will be performed.
616 determine the list of upgrade actions that will be performed.
618
617
619 The role of this function is to filter improvements if needed, apply
618 The role of this function is to filter improvements if needed, apply
620 recommended optimizations from the improvements list that make sense,
619 recommended optimizations from the improvements list that make sense,
621 etc.
620 etc.
622
621
623 Returns a list of action names.
622 Returns a list of action names.
624 """
623 """
625 newactions = []
624 newactions = []
626
625
627 knownreqs = upgradesupporteddestrequirements(repo)
626 knownreqs = upgradesupporteddestrequirements(repo)
628
627
629 for i in improvements:
628 for i in improvements:
630 name = i.name
629 name = i.name
631
630
632 # If the action is a requirement that doesn't show up in the
631 # If the action is a requirement that doesn't show up in the
633 # destination requirements, prune the action.
632 # destination requirements, prune the action.
634 if name in knownreqs and name not in destreqs:
633 if name in knownreqs and name not in destreqs:
635 continue
634 continue
636
635
637 if i.type == deficiency:
636 if i.type == deficiency:
638 newactions.append(name)
637 newactions.append(name)
639
638
640 newactions.extend(o for o in sorted(optimize) if o not in newactions)
639 newactions.extend(o for o in sorted(optimize) if o not in newactions)
641
640
642 # FUTURE consider adding some optimizations here for certain transitions.
641 # FUTURE consider adding some optimizations here for certain transitions.
643 # e.g. adding generaldelta could schedule parent redeltas.
642 # e.g. adding generaldelta could schedule parent redeltas.
644
643
645 return newactions
644 return newactions
646
645
647 def _revlogfrompath(repo, path):
646 def _revlogfrompath(repo, path):
648 """Obtain a revlog from a repo path.
647 """Obtain a revlog from a repo path.
649
648
650 An instance of the appropriate class is returned.
649 An instance of the appropriate class is returned.
651 """
650 """
652 if path == '00changelog.i':
651 if path == '00changelog.i':
653 return changelog.changelog(repo.svfs)
652 return changelog.changelog(repo.svfs)
654 elif path.endswith('00manifest.i'):
653 elif path.endswith('00manifest.i'):
655 mandir = path[:-len('00manifest.i')]
654 mandir = path[:-len('00manifest.i')]
656 return manifest.manifestrevlog(repo.svfs, dir=mandir)
655 return manifest.manifestrevlog(repo.svfs, dir=mandir)
657 else:
656 else:
658 # Filelogs don't do anything special with settings. So we can use a
657 # Filelogs don't do anything special with settings. So we can use a
659 # vanilla revlog.
658 # vanilla revlog.
660 return revlog.revlog(repo.svfs, path)
659 return revlog.revlog(repo.svfs, path)
661
660
662 def _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse, aggressivemergedeltas):
661 def _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse, aggressivemergedeltas):
663 """Copy revlogs between 2 repos."""
662 """Copy revlogs between 2 repos."""
664 revcount = 0
663 revcount = 0
665 srcsize = 0
664 srcsize = 0
666 srcrawsize = 0
665 srcrawsize = 0
667 dstsize = 0
666 dstsize = 0
668 fcount = 0
667 fcount = 0
669 frevcount = 0
668 frevcount = 0
670 fsrcsize = 0
669 fsrcsize = 0
671 frawsize = 0
670 frawsize = 0
672 fdstsize = 0
671 fdstsize = 0
673 mcount = 0
672 mcount = 0
674 mrevcount = 0
673 mrevcount = 0
675 msrcsize = 0
674 msrcsize = 0
676 mrawsize = 0
675 mrawsize = 0
677 mdstsize = 0
676 mdstsize = 0
678 crevcount = 0
677 crevcount = 0
679 csrcsize = 0
678 csrcsize = 0
680 crawsize = 0
679 crawsize = 0
681 cdstsize = 0
680 cdstsize = 0
682
681
683 # Perform a pass to collect metadata. This validates we can open all
682 # Perform a pass to collect metadata. This validates we can open all
684 # source files and allows a unified progress bar to be displayed.
683 # source files and allows a unified progress bar to be displayed.
685 for unencoded, encoded, size in srcrepo.store.walk():
684 for unencoded, encoded, size in srcrepo.store.walk():
686 if unencoded.endswith('.d'):
685 if unencoded.endswith('.d'):
687 continue
686 continue
688
687
689 rl = _revlogfrompath(srcrepo, unencoded)
688 rl = _revlogfrompath(srcrepo, unencoded)
690 revcount += len(rl)
689 revcount += len(rl)
691
690
692 datasize = 0
691 datasize = 0
693 rawsize = 0
692 rawsize = 0
694 idx = rl.index
693 idx = rl.index
695 for rev in rl:
694 for rev in rl:
696 e = idx[rev]
695 e = idx[rev]
697 datasize += e[1]
696 datasize += e[1]
698 rawsize += e[2]
697 rawsize += e[2]
699
698
700 srcsize += datasize
699 srcsize += datasize
701 srcrawsize += rawsize
700 srcrawsize += rawsize
702
701
703 # This is for the separate progress bars.
702 # This is for the separate progress bars.
704 if isinstance(rl, changelog.changelog):
703 if isinstance(rl, changelog.changelog):
705 crevcount += len(rl)
704 crevcount += len(rl)
706 csrcsize += datasize
705 csrcsize += datasize
707 crawsize += rawsize
706 crawsize += rawsize
708 elif isinstance(rl, manifest.manifestrevlog):
707 elif isinstance(rl, manifest.manifestrevlog):
709 mcount += 1
708 mcount += 1
710 mrevcount += len(rl)
709 mrevcount += len(rl)
711 msrcsize += datasize
710 msrcsize += datasize
712 mrawsize += rawsize
711 mrawsize += rawsize
713 elif isinstance(rl, revlog.revlog):
712 elif isinstance(rl, revlog.revlog):
714 fcount += 1
713 fcount += 1
715 frevcount += len(rl)
714 frevcount += len(rl)
716 fsrcsize += datasize
715 fsrcsize += datasize
717 frawsize += rawsize
716 frawsize += rawsize
718
717
719 if not revcount:
718 if not revcount:
720 return
719 return
721
720
722 ui.write(_('migrating %d total revisions (%d in filelogs, %d in manifests, '
721 ui.write(_('migrating %d total revisions (%d in filelogs, %d in manifests, '
723 '%d in changelog)\n') %
722 '%d in changelog)\n') %
724 (revcount, frevcount, mrevcount, crevcount))
723 (revcount, frevcount, mrevcount, crevcount))
725 ui.write(_('migrating %s in store; %s tracked data\n') % (
724 ui.write(_('migrating %s in store; %s tracked data\n') % (
726 (util.bytecount(srcsize), util.bytecount(srcrawsize))))
725 (util.bytecount(srcsize), util.bytecount(srcrawsize))))
727
726
728 # Used to keep track of progress.
727 # Used to keep track of progress.
729 progress = []
728 progress = []
730 def oncopiedrevision(rl, rev, node):
729 def oncopiedrevision(rl, rev, node):
731 progress[1] += 1
730 progress[1] += 1
732 srcrepo.ui.progress(progress[0], progress[1], total=progress[2])
731 srcrepo.ui.progress(progress[0], progress[1], total=progress[2])
733
732
734 # Do the actual copying.
733 # Do the actual copying.
735 # FUTURE this operation can be farmed off to worker processes.
734 # FUTURE this operation can be farmed off to worker processes.
736 seen = set()
735 seen = set()
737 for unencoded, encoded, size in srcrepo.store.walk():
736 for unencoded, encoded, size in srcrepo.store.walk():
738 if unencoded.endswith('.d'):
737 if unencoded.endswith('.d'):
739 continue
738 continue
740
739
741 oldrl = _revlogfrompath(srcrepo, unencoded)
740 oldrl = _revlogfrompath(srcrepo, unencoded)
742 newrl = _revlogfrompath(dstrepo, unencoded)
741 newrl = _revlogfrompath(dstrepo, unencoded)
743
742
744 if isinstance(oldrl, changelog.changelog) and 'c' not in seen:
743 if isinstance(oldrl, changelog.changelog) and 'c' not in seen:
745 ui.write(_('finished migrating %d manifest revisions across %d '
744 ui.write(_('finished migrating %d manifest revisions across %d '
746 'manifests; change in size: %s\n') %
745 'manifests; change in size: %s\n') %
747 (mrevcount, mcount, util.bytecount(mdstsize - msrcsize)))
746 (mrevcount, mcount, util.bytecount(mdstsize - msrcsize)))
748
747
749 ui.write(_('migrating changelog containing %d revisions '
748 ui.write(_('migrating changelog containing %d revisions '
750 '(%s in store; %s tracked data)\n') %
749 '(%s in store; %s tracked data)\n') %
751 (crevcount, util.bytecount(csrcsize),
750 (crevcount, util.bytecount(csrcsize),
752 util.bytecount(crawsize)))
751 util.bytecount(crawsize)))
753 seen.add('c')
752 seen.add('c')
754 progress[:] = [_('changelog revisions'), 0, crevcount]
753 progress[:] = [_('changelog revisions'), 0, crevcount]
755 elif isinstance(oldrl, manifest.manifestrevlog) and 'm' not in seen:
754 elif isinstance(oldrl, manifest.manifestrevlog) and 'm' not in seen:
756 ui.write(_('finished migrating %d filelog revisions across %d '
755 ui.write(_('finished migrating %d filelog revisions across %d '
757 'filelogs; change in size: %s\n') %
756 'filelogs; change in size: %s\n') %
758 (frevcount, fcount, util.bytecount(fdstsize - fsrcsize)))
757 (frevcount, fcount, util.bytecount(fdstsize - fsrcsize)))
759
758
760 ui.write(_('migrating %d manifests containing %d revisions '
759 ui.write(_('migrating %d manifests containing %d revisions '
761 '(%s in store; %s tracked data)\n') %
760 '(%s in store; %s tracked data)\n') %
762 (mcount, mrevcount, util.bytecount(msrcsize),
761 (mcount, mrevcount, util.bytecount(msrcsize),
763 util.bytecount(mrawsize)))
762 util.bytecount(mrawsize)))
764 seen.add('m')
763 seen.add('m')
765 progress[:] = [_('manifest revisions'), 0, mrevcount]
764 progress[:] = [_('manifest revisions'), 0, mrevcount]
766 elif 'f' not in seen:
765 elif 'f' not in seen:
767 ui.write(_('migrating %d filelogs containing %d revisions '
766 ui.write(_('migrating %d filelogs containing %d revisions '
768 '(%s in store; %s tracked data)\n') %
767 '(%s in store; %s tracked data)\n') %
769 (fcount, frevcount, util.bytecount(fsrcsize),
768 (fcount, frevcount, util.bytecount(fsrcsize),
770 util.bytecount(frawsize)))
769 util.bytecount(frawsize)))
771 seen.add('f')
770 seen.add('f')
772 progress[:] = [_('file revisions'), 0, frevcount]
771 progress[:] = [_('file revisions'), 0, frevcount]
773
772
774 ui.progress(progress[0], progress[1], total=progress[2])
773 ui.progress(progress[0], progress[1], total=progress[2])
775
774
776 ui.note(_('cloning %d revisions from %s\n') % (len(oldrl), unencoded))
775 ui.note(_('cloning %d revisions from %s\n') % (len(oldrl), unencoded))
777 oldrl.clone(tr, newrl, addrevisioncb=oncopiedrevision,
776 oldrl.clone(tr, newrl, addrevisioncb=oncopiedrevision,
778 deltareuse=deltareuse,
777 deltareuse=deltareuse,
779 aggressivemergedeltas=aggressivemergedeltas)
778 aggressivemergedeltas=aggressivemergedeltas)
780
779
781 datasize = 0
780 datasize = 0
782 idx = newrl.index
781 idx = newrl.index
783 for rev in newrl:
782 for rev in newrl:
784 datasize += idx[rev][1]
783 datasize += idx[rev][1]
785
784
786 dstsize += datasize
785 dstsize += datasize
787
786
788 if isinstance(newrl, changelog.changelog):
787 if isinstance(newrl, changelog.changelog):
789 cdstsize += datasize
788 cdstsize += datasize
790 elif isinstance(newrl, manifest.manifestrevlog):
789 elif isinstance(newrl, manifest.manifestrevlog):
791 mdstsize += datasize
790 mdstsize += datasize
792 else:
791 else:
793 fdstsize += datasize
792 fdstsize += datasize
794
793
795 ui.progress(progress[0], None)
794 ui.progress(progress[0], None)
796
795
797 ui.write(_('finished migrating %d changelog revisions; change in size: '
796 ui.write(_('finished migrating %d changelog revisions; change in size: '
798 '%s\n') % (crevcount, util.bytecount(cdstsize - csrcsize)))
797 '%s\n') % (crevcount, util.bytecount(cdstsize - csrcsize)))
799
798
800 ui.write(_('finished migrating %d total revisions; total change in store '
799 ui.write(_('finished migrating %d total revisions; total change in store '
801 'size: %s\n') % (revcount, util.bytecount(dstsize - srcsize)))
800 'size: %s\n') % (revcount, util.bytecount(dstsize - srcsize)))
802
801
803 def _upgradefilterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
802 def _upgradefilterstorefile(srcrepo, dstrepo, requirements, path, mode, st):
804 """Determine whether to copy a store file during upgrade.
803 """Determine whether to copy a store file during upgrade.
805
804
806 This function is called when migrating store files from ``srcrepo`` to
805 This function is called when migrating store files from ``srcrepo`` to
807 ``dstrepo`` as part of upgrading a repository.
806 ``dstrepo`` as part of upgrading a repository.
808
807
809 Args:
808 Args:
810 srcrepo: repo we are copying from
809 srcrepo: repo we are copying from
811 dstrepo: repo we are copying to
810 dstrepo: repo we are copying to
812 requirements: set of requirements for ``dstrepo``
811 requirements: set of requirements for ``dstrepo``
813 path: store file being examined
812 path: store file being examined
814 mode: the ``ST_MODE`` file type of ``path``
813 mode: the ``ST_MODE`` file type of ``path``
815 st: ``stat`` data structure for ``path``
814 st: ``stat`` data structure for ``path``
816
815
817 Function should return ``True`` if the file is to be copied.
816 Function should return ``True`` if the file is to be copied.
818 """
817 """
819 # Skip revlogs.
818 # Skip revlogs.
820 if path.endswith(('.i', '.d')):
819 if path.endswith(('.i', '.d')):
821 return False
820 return False
822 # Skip transaction related files.
821 # Skip transaction related files.
823 if path.startswith('undo'):
822 if path.startswith('undo'):
824 return False
823 return False
825 # Only copy regular files.
824 # Only copy regular files.
826 if mode != stat.S_IFREG:
825 if mode != stat.S_IFREG:
827 return False
826 return False
828 # Skip other skipped files.
827 # Skip other skipped files.
829 if path in ('lock', 'fncache'):
828 if path in ('lock', 'fncache'):
830 return False
829 return False
831
830
832 return True
831 return True
833
832
834 def _upgradefinishdatamigration(ui, srcrepo, dstrepo, requirements):
833 def _upgradefinishdatamigration(ui, srcrepo, dstrepo, requirements):
835 """Hook point for extensions to perform additional actions during upgrade.
834 """Hook point for extensions to perform additional actions during upgrade.
836
835
837 This function is called after revlogs and store files have been copied but
836 This function is called after revlogs and store files have been copied but
838 before the new store is swapped into the original location.
837 before the new store is swapped into the original location.
839 """
838 """
840
839
841 def _upgraderepo(ui, srcrepo, dstrepo, requirements, actions):
840 def _upgraderepo(ui, srcrepo, dstrepo, requirements, actions):
842 """Do the low-level work of upgrading a repository.
841 """Do the low-level work of upgrading a repository.
843
842
844 The upgrade is effectively performed as a copy between a source
843 The upgrade is effectively performed as a copy between a source
845 repository and a temporary destination repository.
844 repository and a temporary destination repository.
846
845
847 The source repository is unmodified for as long as possible so the
846 The source repository is unmodified for as long as possible so the
848 upgrade can abort at any time without causing loss of service for
847 upgrade can abort at any time without causing loss of service for
849 readers and without corrupting the source repository.
848 readers and without corrupting the source repository.
850 """
849 """
851 assert srcrepo.currentwlock()
850 assert srcrepo.currentwlock()
852 assert dstrepo.currentwlock()
851 assert dstrepo.currentwlock()
853
852
854 ui.write(_('(it is safe to interrupt this process any time before '
853 ui.write(_('(it is safe to interrupt this process any time before '
855 'data migration completes)\n'))
854 'data migration completes)\n'))
856
855
857 if 'redeltaall' in actions:
856 if 'redeltaall' in actions:
858 deltareuse = revlog.revlog.DELTAREUSENEVER
857 deltareuse = revlog.revlog.DELTAREUSENEVER
859 elif 'redeltaparent' in actions:
858 elif 'redeltaparent' in actions:
860 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
859 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
861 elif 'redeltamultibase' in actions:
860 elif 'redeltamultibase' in actions:
862 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
861 deltareuse = revlog.revlog.DELTAREUSESAMEREVS
863 else:
862 else:
864 deltareuse = revlog.revlog.DELTAREUSEALWAYS
863 deltareuse = revlog.revlog.DELTAREUSEALWAYS
865
864
866 with dstrepo.transaction('upgrade') as tr:
865 with dstrepo.transaction('upgrade') as tr:
867 _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse,
866 _copyrevlogs(ui, srcrepo, dstrepo, tr, deltareuse,
868 'redeltamultibase' in actions)
867 'redeltamultibase' in actions)
869
868
870 # Now copy other files in the store directory.
869 # Now copy other files in the store directory.
871 for p, kind, st in srcrepo.store.vfs.readdir('', stat=True):
870 for p, kind, st in srcrepo.store.vfs.readdir('', stat=True):
872 if not _upgradefilterstorefile(srcrepo, dstrepo, requirements,
871 if not _upgradefilterstorefile(srcrepo, dstrepo, requirements,
873 p, kind, st):
872 p, kind, st):
874 continue
873 continue
875
874
876 srcrepo.ui.write(_('copying %s\n') % p)
875 srcrepo.ui.write(_('copying %s\n') % p)
877 src = srcrepo.store.vfs.join(p)
876 src = srcrepo.store.vfs.join(p)
878 dst = dstrepo.store.vfs.join(p)
877 dst = dstrepo.store.vfs.join(p)
879 util.copyfile(src, dst, copystat=True)
878 util.copyfile(src, dst, copystat=True)
880
879
881 _upgradefinishdatamigration(ui, srcrepo, dstrepo, requirements)
880 _upgradefinishdatamigration(ui, srcrepo, dstrepo, requirements)
882
881
883 ui.write(_('data fully migrated to temporary repository\n'))
882 ui.write(_('data fully migrated to temporary repository\n'))
884
883
885 backuppath = tempfile.mkdtemp(prefix='upgradebackup.', dir=srcrepo.path)
884 backuppath = tempfile.mkdtemp(prefix='upgradebackup.', dir=srcrepo.path)
886 backupvfs = scmutil.vfs(backuppath)
885 backupvfs = scmutil.vfs(backuppath)
887
886
888 # Make a backup of requires file first, as it is the first to be modified.
887 # Make a backup of requires file first, as it is the first to be modified.
889 util.copyfile(srcrepo.join('requires'), backupvfs.join('requires'))
888 util.copyfile(srcrepo.join('requires'), backupvfs.join('requires'))
890
889
891 # We install an arbitrary requirement that clients must not support
890 # We install an arbitrary requirement that clients must not support
892 # as a mechanism to lock out new clients during the data swap. This is
891 # as a mechanism to lock out new clients during the data swap. This is
893 # better than allowing a client to continue while the repository is in
892 # better than allowing a client to continue while the repository is in
894 # an inconsistent state.
893 # an inconsistent state.
895 ui.write(_('marking source repository as being upgraded; clients will be '
894 ui.write(_('marking source repository as being upgraded; clients will be '
896 'unable to read from repository\n'))
895 'unable to read from repository\n'))
897 scmutil.writerequires(srcrepo.vfs,
896 scmutil.writerequires(srcrepo.vfs,
898 srcrepo.requirements | set(['upgradeinprogress']))
897 srcrepo.requirements | set(['upgradeinprogress']))
899
898
900 ui.write(_('starting in-place swap of repository data\n'))
899 ui.write(_('starting in-place swap of repository data\n'))
901 ui.write(_('replaced files will be backed up at %s\n') %
900 ui.write(_('replaced files will be backed up at %s\n') %
902 backuppath)
901 backuppath)
903
902
904 # Now swap in the new store directory. Doing it as a rename should make
903 # Now swap in the new store directory. Doing it as a rename should make
905 # the operation nearly instantaneous and atomic (at least in well-behaved
904 # the operation nearly instantaneous and atomic (at least in well-behaved
906 # environments).
905 # environments).
907 ui.write(_('replacing store...\n'))
906 ui.write(_('replacing store...\n'))
908 tstart = time.time()
907 tstart = util.timer()
909 util.rename(srcrepo.spath, backupvfs.join('store'))
908 util.rename(srcrepo.spath, backupvfs.join('store'))
910 util.rename(dstrepo.spath, srcrepo.spath)
909 util.rename(dstrepo.spath, srcrepo.spath)
911 elapsed = time.time() - tstart
910 elapsed = util.timer() - tstart
912 ui.write(_('store replacement complete; repository was inconsistent for '
911 ui.write(_('store replacement complete; repository was inconsistent for '
913 '%0.1fs\n') % elapsed)
912 '%0.1fs\n') % elapsed)
914
913
915 # We first write the requirements file. Any new requirements will lock
914 # We first write the requirements file. Any new requirements will lock
916 # out legacy clients.
915 # out legacy clients.
917 ui.write(_('finalizing requirements file and making repository readable '
916 ui.write(_('finalizing requirements file and making repository readable '
918 'again\n'))
917 'again\n'))
919 scmutil.writerequires(srcrepo.vfs, requirements)
918 scmutil.writerequires(srcrepo.vfs, requirements)
920
919
921 # The lock file from the old store won't be removed because nothing has a
920 # The lock file from the old store won't be removed because nothing has a
922 # reference to its new location. So clean it up manually. Alternatively, we
921 # reference to its new location. So clean it up manually. Alternatively, we
923 # could update srcrepo.svfs and other variables to point to the new
922 # could update srcrepo.svfs and other variables to point to the new
924 # location. This is simpler.
923 # location. This is simpler.
925 backupvfs.unlink('store/lock')
924 backupvfs.unlink('store/lock')
926
925
927 return backuppath
926 return backuppath
928
927
929 def upgraderepo(ui, repo, run=False, optimize=None):
928 def upgraderepo(ui, repo, run=False, optimize=None):
930 """Upgrade a repository in place."""
929 """Upgrade a repository in place."""
931 # Avoid cycle: cmdutil -> repair -> localrepo -> cmdutil
930 # Avoid cycle: cmdutil -> repair -> localrepo -> cmdutil
932 from . import localrepo
931 from . import localrepo
933
932
934 optimize = set(optimize or [])
933 optimize = set(optimize or [])
935 repo = repo.unfiltered()
934 repo = repo.unfiltered()
936
935
937 # Ensure the repository can be upgraded.
936 # Ensure the repository can be upgraded.
938 missingreqs = upgraderequiredsourcerequirements(repo) - repo.requirements
937 missingreqs = upgraderequiredsourcerequirements(repo) - repo.requirements
939 if missingreqs:
938 if missingreqs:
940 raise error.Abort(_('cannot upgrade repository; requirement '
939 raise error.Abort(_('cannot upgrade repository; requirement '
941 'missing: %s') % _(', ').join(sorted(missingreqs)))
940 'missing: %s') % _(', ').join(sorted(missingreqs)))
942
941
943 blockedreqs = upgradeblocksourcerequirements(repo) & repo.requirements
942 blockedreqs = upgradeblocksourcerequirements(repo) & repo.requirements
944 if blockedreqs:
943 if blockedreqs:
945 raise error.Abort(_('cannot upgrade repository; unsupported source '
944 raise error.Abort(_('cannot upgrade repository; unsupported source '
946 'requirement: %s') %
945 'requirement: %s') %
947 _(', ').join(sorted(blockedreqs)))
946 _(', ').join(sorted(blockedreqs)))
948
947
949 # FUTURE there is potentially a need to control the wanted requirements via
948 # FUTURE there is potentially a need to control the wanted requirements via
950 # command arguments or via an extension hook point.
949 # command arguments or via an extension hook point.
951 newreqs = localrepo.newreporequirements(repo)
950 newreqs = localrepo.newreporequirements(repo)
952
951
953 noremovereqs = (repo.requirements - newreqs -
952 noremovereqs = (repo.requirements - newreqs -
954 upgradesupportremovedrequirements(repo))
953 upgradesupportremovedrequirements(repo))
955 if noremovereqs:
954 if noremovereqs:
956 raise error.Abort(_('cannot upgrade repository; requirement would be '
955 raise error.Abort(_('cannot upgrade repository; requirement would be '
957 'removed: %s') % _(', ').join(sorted(noremovereqs)))
956 'removed: %s') % _(', ').join(sorted(noremovereqs)))
958
957
959 noaddreqs = (newreqs - repo.requirements -
958 noaddreqs = (newreqs - repo.requirements -
960 upgradeallowednewrequirements(repo))
959 upgradeallowednewrequirements(repo))
961 if noaddreqs:
960 if noaddreqs:
962 raise error.Abort(_('cannot upgrade repository; do not support adding '
961 raise error.Abort(_('cannot upgrade repository; do not support adding '
963 'requirement: %s') %
962 'requirement: %s') %
964 _(', ').join(sorted(noaddreqs)))
963 _(', ').join(sorted(noaddreqs)))
965
964
966 unsupportedreqs = newreqs - upgradesupporteddestrequirements(repo)
965 unsupportedreqs = newreqs - upgradesupporteddestrequirements(repo)
967 if unsupportedreqs:
966 if unsupportedreqs:
968 raise error.Abort(_('cannot upgrade repository; do not support '
967 raise error.Abort(_('cannot upgrade repository; do not support '
969 'destination requirement: %s') %
968 'destination requirement: %s') %
970 _(', ').join(sorted(unsupportedreqs)))
969 _(', ').join(sorted(unsupportedreqs)))
971
970
972 # Find and validate all improvements that can be made.
971 # Find and validate all improvements that can be made.
973 improvements = upgradefindimprovements(repo)
972 improvements = upgradefindimprovements(repo)
974 for i in improvements:
973 for i in improvements:
975 if i.type not in (deficiency, optimisation):
974 if i.type not in (deficiency, optimisation):
976 raise error.Abort(_('unexpected improvement type %s for %s') % (
975 raise error.Abort(_('unexpected improvement type %s for %s') % (
977 i.type, i.name))
976 i.type, i.name))
978
977
979 # Validate arguments.
978 # Validate arguments.
980 unknownoptimize = optimize - set(i.name for i in improvements
979 unknownoptimize = optimize - set(i.name for i in improvements
981 if i.type == optimisation)
980 if i.type == optimisation)
982 if unknownoptimize:
981 if unknownoptimize:
983 raise error.Abort(_('unknown optimization action requested: %s') %
982 raise error.Abort(_('unknown optimization action requested: %s') %
984 ', '.join(sorted(unknownoptimize)),
983 ', '.join(sorted(unknownoptimize)),
985 hint=_('run without arguments to see valid '
984 hint=_('run without arguments to see valid '
986 'optimizations'))
985 'optimizations'))
987
986
988 actions = upgradedetermineactions(repo, improvements, repo.requirements,
987 actions = upgradedetermineactions(repo, improvements, repo.requirements,
989 newreqs, optimize)
988 newreqs, optimize)
990
989
991 def printrequirements():
990 def printrequirements():
992 ui.write(_('requirements\n'))
991 ui.write(_('requirements\n'))
993 ui.write(_(' preserved: %s\n') %
992 ui.write(_(' preserved: %s\n') %
994 _(', ').join(sorted(newreqs & repo.requirements)))
993 _(', ').join(sorted(newreqs & repo.requirements)))
995
994
996 if repo.requirements - newreqs:
995 if repo.requirements - newreqs:
997 ui.write(_(' removed: %s\n') %
996 ui.write(_(' removed: %s\n') %
998 _(', ').join(sorted(repo.requirements - newreqs)))
997 _(', ').join(sorted(repo.requirements - newreqs)))
999
998
1000 if newreqs - repo.requirements:
999 if newreqs - repo.requirements:
1001 ui.write(_(' added: %s\n') %
1000 ui.write(_(' added: %s\n') %
1002 _(', ').join(sorted(newreqs - repo.requirements)))
1001 _(', ').join(sorted(newreqs - repo.requirements)))
1003
1002
1004 ui.write('\n')
1003 ui.write('\n')
1005
1004
1006 def printupgradeactions():
1005 def printupgradeactions():
1007 for action in actions:
1006 for action in actions:
1008 for i in improvements:
1007 for i in improvements:
1009 if i.name == action:
1008 if i.name == action:
1010 ui.write('%s\n %s\n\n' %
1009 ui.write('%s\n %s\n\n' %
1011 (i.name, i.upgrademessage))
1010 (i.name, i.upgrademessage))
1012
1011
1013 if not run:
1012 if not run:
1014 fromdefault = []
1013 fromdefault = []
1015 fromconfig = []
1014 fromconfig = []
1016 optimizations = []
1015 optimizations = []
1017
1016
1018 for i in improvements:
1017 for i in improvements:
1019 assert i.type in (deficiency, optimisation)
1018 assert i.type in (deficiency, optimisation)
1020 if i.type == deficiency:
1019 if i.type == deficiency:
1021 if i.fromdefault:
1020 if i.fromdefault:
1022 fromdefault.append(i)
1021 fromdefault.append(i)
1023 if i.fromconfig:
1022 if i.fromconfig:
1024 fromconfig.append(i)
1023 fromconfig.append(i)
1025 else:
1024 else:
1026 optimizations.append(i)
1025 optimizations.append(i)
1027
1026
1028 if fromdefault or fromconfig:
1027 if fromdefault or fromconfig:
1029 fromconfignames = set(x.name for x in fromconfig)
1028 fromconfignames = set(x.name for x in fromconfig)
1030 onlydefault = [i for i in fromdefault
1029 onlydefault = [i for i in fromdefault
1031 if i.name not in fromconfignames]
1030 if i.name not in fromconfignames]
1032
1031
1033 if fromconfig:
1032 if fromconfig:
1034 ui.write(_('repository lacks features recommended by '
1033 ui.write(_('repository lacks features recommended by '
1035 'current config options:\n\n'))
1034 'current config options:\n\n'))
1036 for i in fromconfig:
1035 for i in fromconfig:
1037 ui.write('%s\n %s\n\n' % (i.name, i.description))
1036 ui.write('%s\n %s\n\n' % (i.name, i.description))
1038
1037
1039 if onlydefault:
1038 if onlydefault:
1040 ui.write(_('repository lacks features used by the default '
1039 ui.write(_('repository lacks features used by the default '
1041 'config options:\n\n'))
1040 'config options:\n\n'))
1042 for i in onlydefault:
1041 for i in onlydefault:
1043 ui.write('%s\n %s\n\n' % (i.name, i.description))
1042 ui.write('%s\n %s\n\n' % (i.name, i.description))
1044
1043
1045 ui.write('\n')
1044 ui.write('\n')
1046 else:
1045 else:
1047 ui.write(_('(no feature deficiencies found in existing '
1046 ui.write(_('(no feature deficiencies found in existing '
1048 'repository)\n'))
1047 'repository)\n'))
1049
1048
1050 ui.write(_('performing an upgrade with "--run" will make the following '
1049 ui.write(_('performing an upgrade with "--run" will make the following '
1051 'changes:\n\n'))
1050 'changes:\n\n'))
1052
1051
1053 printrequirements()
1052 printrequirements()
1054 printupgradeactions()
1053 printupgradeactions()
1055
1054
1056 unusedoptimize = [i for i in improvements
1055 unusedoptimize = [i for i in improvements
1057 if i.name not in actions and i.type == optimisation]
1056 if i.name not in actions and i.type == optimisation]
1058 if unusedoptimize:
1057 if unusedoptimize:
1059 ui.write(_('additional optimizations are available by specifying '
1058 ui.write(_('additional optimizations are available by specifying '
1060 '"--optimize <name>":\n\n'))
1059 '"--optimize <name>":\n\n'))
1061 for i in unusedoptimize:
1060 for i in unusedoptimize:
1062 ui.write(_('%s\n %s\n\n') % (i.name, i.description))
1061 ui.write(_('%s\n %s\n\n') % (i.name, i.description))
1063 return
1062 return
1064
1063
1065 # Else we're in the run=true case.
1064 # Else we're in the run=true case.
1066 ui.write(_('upgrade will perform the following actions:\n\n'))
1065 ui.write(_('upgrade will perform the following actions:\n\n'))
1067 printrequirements()
1066 printrequirements()
1068 printupgradeactions()
1067 printupgradeactions()
1069
1068
1070 ui.write(_('beginning upgrade...\n'))
1069 ui.write(_('beginning upgrade...\n'))
1071 with repo.wlock():
1070 with repo.wlock():
1072 with repo.lock():
1071 with repo.lock():
1073 ui.write(_('repository locked and read-only\n'))
1072 ui.write(_('repository locked and read-only\n'))
1074 # Our strategy for upgrading the repository is to create a new,
1073 # Our strategy for upgrading the repository is to create a new,
1075 # temporary repository, write data to it, then do a swap of the
1074 # temporary repository, write data to it, then do a swap of the
1076 # data. There are less heavyweight ways to do this, but it is easier
1075 # data. There are less heavyweight ways to do this, but it is easier
1077 # to create a new repo object than to instantiate all the components
1076 # to create a new repo object than to instantiate all the components
1078 # (like the store) separately.
1077 # (like the store) separately.
1079 tmppath = tempfile.mkdtemp(prefix='upgrade.', dir=repo.path)
1078 tmppath = tempfile.mkdtemp(prefix='upgrade.', dir=repo.path)
1080 backuppath = None
1079 backuppath = None
1081 try:
1080 try:
1082 ui.write(_('creating temporary repository to stage migrated '
1081 ui.write(_('creating temporary repository to stage migrated '
1083 'data: %s\n') % tmppath)
1082 'data: %s\n') % tmppath)
1084 dstrepo = localrepo.localrepository(repo.baseui,
1083 dstrepo = localrepo.localrepository(repo.baseui,
1085 path=tmppath,
1084 path=tmppath,
1086 create=True)
1085 create=True)
1087
1086
1088 with dstrepo.wlock():
1087 with dstrepo.wlock():
1089 with dstrepo.lock():
1088 with dstrepo.lock():
1090 backuppath = _upgraderepo(ui, repo, dstrepo, newreqs,
1089 backuppath = _upgraderepo(ui, repo, dstrepo, newreqs,
1091 actions)
1090 actions)
1092
1091
1093 finally:
1092 finally:
1094 ui.write(_('removing temporary repository %s\n') % tmppath)
1093 ui.write(_('removing temporary repository %s\n') % tmppath)
1095 repo.vfs.rmtree(tmppath, forcibly=True)
1094 repo.vfs.rmtree(tmppath, forcibly=True)
1096
1095
1097 if backuppath:
1096 if backuppath:
1098 ui.warn(_('copy of old repository backed up at %s\n') %
1097 ui.warn(_('copy of old repository backed up at %s\n') %
1099 backuppath)
1098 backuppath)
1100 ui.warn(_('the old repository will not be deleted; remove '
1099 ui.warn(_('the old repository will not be deleted; remove '
1101 'it to free up disk space once the upgraded '
1100 'it to free up disk space once the upgraded '
1102 'repository is verified\n'))
1101 'repository is verified\n'))
@@ -1,398 +1,397 b''
1 # streamclone.py - producing and consuming streaming repository data
1 # streamclone.py - producing and consuming streaming repository data
2 #
2 #
3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2015 Gregory Szorc <gregory.szorc@gmail.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 struct
10 import struct
11 import time
12
11
13 from .i18n import _
12 from .i18n import _
14 from . import (
13 from . import (
15 branchmap,
14 branchmap,
16 error,
15 error,
17 store,
16 store,
18 util,
17 util,
19 )
18 )
20
19
21 def canperformstreamclone(pullop, bailifbundle2supported=False):
20 def canperformstreamclone(pullop, bailifbundle2supported=False):
22 """Whether it is possible to perform a streaming clone as part of pull.
21 """Whether it is possible to perform a streaming clone as part of pull.
23
22
24 ``bailifbundle2supported`` will cause the function to return False if
23 ``bailifbundle2supported`` will cause the function to return False if
25 bundle2 stream clones are supported. It should only be called by the
24 bundle2 stream clones are supported. It should only be called by the
26 legacy stream clone code path.
25 legacy stream clone code path.
27
26
28 Returns a tuple of (supported, requirements). ``supported`` is True if
27 Returns a tuple of (supported, requirements). ``supported`` is True if
29 streaming clone is supported and False otherwise. ``requirements`` is
28 streaming clone is supported and False otherwise. ``requirements`` is
30 a set of repo requirements from the remote, or ``None`` if stream clone
29 a set of repo requirements from the remote, or ``None`` if stream clone
31 isn't supported.
30 isn't supported.
32 """
31 """
33 repo = pullop.repo
32 repo = pullop.repo
34 remote = pullop.remote
33 remote = pullop.remote
35
34
36 bundle2supported = False
35 bundle2supported = False
37 if pullop.canusebundle2:
36 if pullop.canusebundle2:
38 if 'v1' in pullop.remotebundle2caps.get('stream', []):
37 if 'v1' in pullop.remotebundle2caps.get('stream', []):
39 bundle2supported = True
38 bundle2supported = True
40 # else
39 # else
41 # Server doesn't support bundle2 stream clone or doesn't support
40 # Server doesn't support bundle2 stream clone or doesn't support
42 # the versions we support. Fall back and possibly allow legacy.
41 # the versions we support. Fall back and possibly allow legacy.
43
42
44 # Ensures legacy code path uses available bundle2.
43 # Ensures legacy code path uses available bundle2.
45 if bailifbundle2supported and bundle2supported:
44 if bailifbundle2supported and bundle2supported:
46 return False, None
45 return False, None
47 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
46 # Ensures bundle2 doesn't try to do a stream clone if it isn't supported.
48 #elif not bailifbundle2supported and not bundle2supported:
47 #elif not bailifbundle2supported and not bundle2supported:
49 # return False, None
48 # return False, None
50
49
51 # Streaming clone only works on empty repositories.
50 # Streaming clone only works on empty repositories.
52 if len(repo):
51 if len(repo):
53 return False, None
52 return False, None
54
53
55 # Streaming clone only works if all data is being requested.
54 # Streaming clone only works if all data is being requested.
56 if pullop.heads:
55 if pullop.heads:
57 return False, None
56 return False, None
58
57
59 streamrequested = pullop.streamclonerequested
58 streamrequested = pullop.streamclonerequested
60
59
61 # If we don't have a preference, let the server decide for us. This
60 # If we don't have a preference, let the server decide for us. This
62 # likely only comes into play in LANs.
61 # likely only comes into play in LANs.
63 if streamrequested is None:
62 if streamrequested is None:
64 # The server can advertise whether to prefer streaming clone.
63 # The server can advertise whether to prefer streaming clone.
65 streamrequested = remote.capable('stream-preferred')
64 streamrequested = remote.capable('stream-preferred')
66
65
67 if not streamrequested:
66 if not streamrequested:
68 return False, None
67 return False, None
69
68
70 # In order for stream clone to work, the client has to support all the
69 # In order for stream clone to work, the client has to support all the
71 # requirements advertised by the server.
70 # requirements advertised by the server.
72 #
71 #
73 # The server advertises its requirements via the "stream" and "streamreqs"
72 # The server advertises its requirements via the "stream" and "streamreqs"
74 # capability. "stream" (a value-less capability) is advertised if and only
73 # capability. "stream" (a value-less capability) is advertised if and only
75 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
74 # if the only requirement is "revlogv1." Else, the "streamreqs" capability
76 # is advertised and contains a comma-delimited list of requirements.
75 # is advertised and contains a comma-delimited list of requirements.
77 requirements = set()
76 requirements = set()
78 if remote.capable('stream'):
77 if remote.capable('stream'):
79 requirements.add('revlogv1')
78 requirements.add('revlogv1')
80 else:
79 else:
81 streamreqs = remote.capable('streamreqs')
80 streamreqs = remote.capable('streamreqs')
82 # This is weird and shouldn't happen with modern servers.
81 # This is weird and shouldn't happen with modern servers.
83 if not streamreqs:
82 if not streamreqs:
84 return False, None
83 return False, None
85
84
86 streamreqs = set(streamreqs.split(','))
85 streamreqs = set(streamreqs.split(','))
87 # Server requires something we don't support. Bail.
86 # Server requires something we don't support. Bail.
88 if streamreqs - repo.supportedformats:
87 if streamreqs - repo.supportedformats:
89 return False, None
88 return False, None
90 requirements = streamreqs
89 requirements = streamreqs
91
90
92 return True, requirements
91 return True, requirements
93
92
94 def maybeperformlegacystreamclone(pullop):
93 def maybeperformlegacystreamclone(pullop):
95 """Possibly perform a legacy stream clone operation.
94 """Possibly perform a legacy stream clone operation.
96
95
97 Legacy stream clones are performed as part of pull but before all other
96 Legacy stream clones are performed as part of pull but before all other
98 operations.
97 operations.
99
98
100 A legacy stream clone will not be performed if a bundle2 stream clone is
99 A legacy stream clone will not be performed if a bundle2 stream clone is
101 supported.
100 supported.
102 """
101 """
103 supported, requirements = canperformstreamclone(pullop)
102 supported, requirements = canperformstreamclone(pullop)
104
103
105 if not supported:
104 if not supported:
106 return
105 return
107
106
108 repo = pullop.repo
107 repo = pullop.repo
109 remote = pullop.remote
108 remote = pullop.remote
110
109
111 # Save remote branchmap. We will use it later to speed up branchcache
110 # Save remote branchmap. We will use it later to speed up branchcache
112 # creation.
111 # creation.
113 rbranchmap = None
112 rbranchmap = None
114 if remote.capable('branchmap'):
113 if remote.capable('branchmap'):
115 rbranchmap = remote.branchmap()
114 rbranchmap = remote.branchmap()
116
115
117 repo.ui.status(_('streaming all changes\n'))
116 repo.ui.status(_('streaming all changes\n'))
118
117
119 fp = remote.stream_out()
118 fp = remote.stream_out()
120 l = fp.readline()
119 l = fp.readline()
121 try:
120 try:
122 resp = int(l)
121 resp = int(l)
123 except ValueError:
122 except ValueError:
124 raise error.ResponseError(
123 raise error.ResponseError(
125 _('unexpected response from remote server:'), l)
124 _('unexpected response from remote server:'), l)
126 if resp == 1:
125 if resp == 1:
127 raise error.Abort(_('operation forbidden by server'))
126 raise error.Abort(_('operation forbidden by server'))
128 elif resp == 2:
127 elif resp == 2:
129 raise error.Abort(_('locking the remote repository failed'))
128 raise error.Abort(_('locking the remote repository failed'))
130 elif resp != 0:
129 elif resp != 0:
131 raise error.Abort(_('the server sent an unknown error code'))
130 raise error.Abort(_('the server sent an unknown error code'))
132
131
133 l = fp.readline()
132 l = fp.readline()
134 try:
133 try:
135 filecount, bytecount = map(int, l.split(' ', 1))
134 filecount, bytecount = map(int, l.split(' ', 1))
136 except (ValueError, TypeError):
135 except (ValueError, TypeError):
137 raise error.ResponseError(
136 raise error.ResponseError(
138 _('unexpected response from remote server:'), l)
137 _('unexpected response from remote server:'), l)
139
138
140 with repo.lock():
139 with repo.lock():
141 consumev1(repo, fp, filecount, bytecount)
140 consumev1(repo, fp, filecount, bytecount)
142
141
143 # new requirements = old non-format requirements +
142 # new requirements = old non-format requirements +
144 # new format-related remote requirements
143 # new format-related remote requirements
145 # requirements from the streamed-in repository
144 # requirements from the streamed-in repository
146 repo.requirements = requirements | (
145 repo.requirements = requirements | (
147 repo.requirements - repo.supportedformats)
146 repo.requirements - repo.supportedformats)
148 repo._applyopenerreqs()
147 repo._applyopenerreqs()
149 repo._writerequirements()
148 repo._writerequirements()
150
149
151 if rbranchmap:
150 if rbranchmap:
152 branchmap.replacecache(repo, rbranchmap)
151 branchmap.replacecache(repo, rbranchmap)
153
152
154 repo.invalidate()
153 repo.invalidate()
155
154
156 def allowservergeneration(ui):
155 def allowservergeneration(ui):
157 """Whether streaming clones are allowed from the server."""
156 """Whether streaming clones are allowed from the server."""
158 return ui.configbool('server', 'uncompressed', True, untrusted=True)
157 return ui.configbool('server', 'uncompressed', True, untrusted=True)
159
158
160 # This is it's own function so extensions can override it.
159 # This is it's own function so extensions can override it.
161 def _walkstreamfiles(repo):
160 def _walkstreamfiles(repo):
162 return repo.store.walk()
161 return repo.store.walk()
163
162
164 def generatev1(repo):
163 def generatev1(repo):
165 """Emit content for version 1 of a streaming clone.
164 """Emit content for version 1 of a streaming clone.
166
165
167 This returns a 3-tuple of (file count, byte size, data iterator).
166 This returns a 3-tuple of (file count, byte size, data iterator).
168
167
169 The data iterator consists of N entries for each file being transferred.
168 The data iterator consists of N entries for each file being transferred.
170 Each file entry starts as a line with the file name and integer size
169 Each file entry starts as a line with the file name and integer size
171 delimited by a null byte.
170 delimited by a null byte.
172
171
173 The raw file data follows. Following the raw file data is the next file
172 The raw file data follows. Following the raw file data is the next file
174 entry, or EOF.
173 entry, or EOF.
175
174
176 When used on the wire protocol, an additional line indicating protocol
175 When used on the wire protocol, an additional line indicating protocol
177 success will be prepended to the stream. This function is not responsible
176 success will be prepended to the stream. This function is not responsible
178 for adding it.
177 for adding it.
179
178
180 This function will obtain a repository lock to ensure a consistent view of
179 This function will obtain a repository lock to ensure a consistent view of
181 the store is captured. It therefore may raise LockError.
180 the store is captured. It therefore may raise LockError.
182 """
181 """
183 entries = []
182 entries = []
184 total_bytes = 0
183 total_bytes = 0
185 # Get consistent snapshot of repo, lock during scan.
184 # Get consistent snapshot of repo, lock during scan.
186 with repo.lock():
185 with repo.lock():
187 repo.ui.debug('scanning\n')
186 repo.ui.debug('scanning\n')
188 for name, ename, size in _walkstreamfiles(repo):
187 for name, ename, size in _walkstreamfiles(repo):
189 if size:
188 if size:
190 entries.append((name, size))
189 entries.append((name, size))
191 total_bytes += size
190 total_bytes += size
192
191
193 repo.ui.debug('%d files, %d bytes to transfer\n' %
192 repo.ui.debug('%d files, %d bytes to transfer\n' %
194 (len(entries), total_bytes))
193 (len(entries), total_bytes))
195
194
196 svfs = repo.svfs
195 svfs = repo.svfs
197 oldaudit = svfs.mustaudit
196 oldaudit = svfs.mustaudit
198 debugflag = repo.ui.debugflag
197 debugflag = repo.ui.debugflag
199 svfs.mustaudit = False
198 svfs.mustaudit = False
200
199
201 def emitrevlogdata():
200 def emitrevlogdata():
202 try:
201 try:
203 for name, size in entries:
202 for name, size in entries:
204 if debugflag:
203 if debugflag:
205 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
204 repo.ui.debug('sending %s (%d bytes)\n' % (name, size))
206 # partially encode name over the wire for backwards compat
205 # partially encode name over the wire for backwards compat
207 yield '%s\0%d\n' % (store.encodedir(name), size)
206 yield '%s\0%d\n' % (store.encodedir(name), size)
208 if size <= 65536:
207 if size <= 65536:
209 with svfs(name, 'rb') as fp:
208 with svfs(name, 'rb') as fp:
210 yield fp.read(size)
209 yield fp.read(size)
211 else:
210 else:
212 for chunk in util.filechunkiter(svfs(name), limit=size):
211 for chunk in util.filechunkiter(svfs(name), limit=size):
213 yield chunk
212 yield chunk
214 finally:
213 finally:
215 svfs.mustaudit = oldaudit
214 svfs.mustaudit = oldaudit
216
215
217 return len(entries), total_bytes, emitrevlogdata()
216 return len(entries), total_bytes, emitrevlogdata()
218
217
219 def generatev1wireproto(repo):
218 def generatev1wireproto(repo):
220 """Emit content for version 1 of streaming clone suitable for the wire.
219 """Emit content for version 1 of streaming clone suitable for the wire.
221
220
222 This is the data output from ``generatev1()`` with a header line
221 This is the data output from ``generatev1()`` with a header line
223 indicating file count and byte size.
222 indicating file count and byte size.
224 """
223 """
225 filecount, bytecount, it = generatev1(repo)
224 filecount, bytecount, it = generatev1(repo)
226 yield '%d %d\n' % (filecount, bytecount)
225 yield '%d %d\n' % (filecount, bytecount)
227 for chunk in it:
226 for chunk in it:
228 yield chunk
227 yield chunk
229
228
230 def generatebundlev1(repo, compression='UN'):
229 def generatebundlev1(repo, compression='UN'):
231 """Emit content for version 1 of a stream clone bundle.
230 """Emit content for version 1 of a stream clone bundle.
232
231
233 The first 4 bytes of the output ("HGS1") denote this as stream clone
232 The first 4 bytes of the output ("HGS1") denote this as stream clone
234 bundle version 1.
233 bundle version 1.
235
234
236 The next 2 bytes indicate the compression type. Only "UN" is currently
235 The next 2 bytes indicate the compression type. Only "UN" is currently
237 supported.
236 supported.
238
237
239 The next 16 bytes are two 64-bit big endian unsigned integers indicating
238 The next 16 bytes are two 64-bit big endian unsigned integers indicating
240 file count and byte count, respectively.
239 file count and byte count, respectively.
241
240
242 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
241 The next 2 bytes is a 16-bit big endian unsigned short declaring the length
243 of the requirements string, including a trailing \0. The following N bytes
242 of the requirements string, including a trailing \0. The following N bytes
244 are the requirements string, which is ASCII containing a comma-delimited
243 are the requirements string, which is ASCII containing a comma-delimited
245 list of repo requirements that are needed to support the data.
244 list of repo requirements that are needed to support the data.
246
245
247 The remaining content is the output of ``generatev1()`` (which may be
246 The remaining content is the output of ``generatev1()`` (which may be
248 compressed in the future).
247 compressed in the future).
249
248
250 Returns a tuple of (requirements, data generator).
249 Returns a tuple of (requirements, data generator).
251 """
250 """
252 if compression != 'UN':
251 if compression != 'UN':
253 raise ValueError('we do not support the compression argument yet')
252 raise ValueError('we do not support the compression argument yet')
254
253
255 requirements = repo.requirements & repo.supportedformats
254 requirements = repo.requirements & repo.supportedformats
256 requires = ','.join(sorted(requirements))
255 requires = ','.join(sorted(requirements))
257
256
258 def gen():
257 def gen():
259 yield 'HGS1'
258 yield 'HGS1'
260 yield compression
259 yield compression
261
260
262 filecount, bytecount, it = generatev1(repo)
261 filecount, bytecount, it = generatev1(repo)
263 repo.ui.status(_('writing %d bytes for %d files\n') %
262 repo.ui.status(_('writing %d bytes for %d files\n') %
264 (bytecount, filecount))
263 (bytecount, filecount))
265
264
266 yield struct.pack('>QQ', filecount, bytecount)
265 yield struct.pack('>QQ', filecount, bytecount)
267 yield struct.pack('>H', len(requires) + 1)
266 yield struct.pack('>H', len(requires) + 1)
268 yield requires + '\0'
267 yield requires + '\0'
269
268
270 # This is where we'll add compression in the future.
269 # This is where we'll add compression in the future.
271 assert compression == 'UN'
270 assert compression == 'UN'
272
271
273 seen = 0
272 seen = 0
274 repo.ui.progress(_('bundle'), 0, total=bytecount, unit=_('bytes'))
273 repo.ui.progress(_('bundle'), 0, total=bytecount, unit=_('bytes'))
275
274
276 for chunk in it:
275 for chunk in it:
277 seen += len(chunk)
276 seen += len(chunk)
278 repo.ui.progress(_('bundle'), seen, total=bytecount,
277 repo.ui.progress(_('bundle'), seen, total=bytecount,
279 unit=_('bytes'))
278 unit=_('bytes'))
280 yield chunk
279 yield chunk
281
280
282 repo.ui.progress(_('bundle'), None)
281 repo.ui.progress(_('bundle'), None)
283
282
284 return requirements, gen()
283 return requirements, gen()
285
284
286 def consumev1(repo, fp, filecount, bytecount):
285 def consumev1(repo, fp, filecount, bytecount):
287 """Apply the contents from version 1 of a streaming clone file handle.
286 """Apply the contents from version 1 of a streaming clone file handle.
288
287
289 This takes the output from "stream_out" and applies it to the specified
288 This takes the output from "stream_out" and applies it to the specified
290 repository.
289 repository.
291
290
292 Like "stream_out," the status line added by the wire protocol is not
291 Like "stream_out," the status line added by the wire protocol is not
293 handled by this function.
292 handled by this function.
294 """
293 """
295 with repo.lock():
294 with repo.lock():
296 repo.ui.status(_('%d files to transfer, %s of data\n') %
295 repo.ui.status(_('%d files to transfer, %s of data\n') %
297 (filecount, util.bytecount(bytecount)))
296 (filecount, util.bytecount(bytecount)))
298 handled_bytes = 0
297 handled_bytes = 0
299 repo.ui.progress(_('clone'), 0, total=bytecount, unit=_('bytes'))
298 repo.ui.progress(_('clone'), 0, total=bytecount, unit=_('bytes'))
300 start = time.time()
299 start = util.timer()
301
300
302 # TODO: get rid of (potential) inconsistency
301 # TODO: get rid of (potential) inconsistency
303 #
302 #
304 # If transaction is started and any @filecache property is
303 # If transaction is started and any @filecache property is
305 # changed at this point, it causes inconsistency between
304 # changed at this point, it causes inconsistency between
306 # in-memory cached property and streamclone-ed file on the
305 # in-memory cached property and streamclone-ed file on the
307 # disk. Nested transaction prevents transaction scope "clone"
306 # disk. Nested transaction prevents transaction scope "clone"
308 # below from writing in-memory changes out at the end of it,
307 # below from writing in-memory changes out at the end of it,
309 # even though in-memory changes are discarded at the end of it
308 # even though in-memory changes are discarded at the end of it
310 # regardless of transaction nesting.
309 # regardless of transaction nesting.
311 #
310 #
312 # But transaction nesting can't be simply prohibited, because
311 # But transaction nesting can't be simply prohibited, because
313 # nesting occurs also in ordinary case (e.g. enabling
312 # nesting occurs also in ordinary case (e.g. enabling
314 # clonebundles).
313 # clonebundles).
315
314
316 with repo.transaction('clone'):
315 with repo.transaction('clone'):
317 with repo.svfs.backgroundclosing(repo.ui, expectedcount=filecount):
316 with repo.svfs.backgroundclosing(repo.ui, expectedcount=filecount):
318 for i in xrange(filecount):
317 for i in xrange(filecount):
319 # XXX doesn't support '\n' or '\r' in filenames
318 # XXX doesn't support '\n' or '\r' in filenames
320 l = fp.readline()
319 l = fp.readline()
321 try:
320 try:
322 name, size = l.split('\0', 1)
321 name, size = l.split('\0', 1)
323 size = int(size)
322 size = int(size)
324 except (ValueError, TypeError):
323 except (ValueError, TypeError):
325 raise error.ResponseError(
324 raise error.ResponseError(
326 _('unexpected response from remote server:'), l)
325 _('unexpected response from remote server:'), l)
327 if repo.ui.debugflag:
326 if repo.ui.debugflag:
328 repo.ui.debug('adding %s (%s)\n' %
327 repo.ui.debug('adding %s (%s)\n' %
329 (name, util.bytecount(size)))
328 (name, util.bytecount(size)))
330 # for backwards compat, name was partially encoded
329 # for backwards compat, name was partially encoded
331 path = store.decodedir(name)
330 path = store.decodedir(name)
332 with repo.svfs(path, 'w', backgroundclose=True) as ofp:
331 with repo.svfs(path, 'w', backgroundclose=True) as ofp:
333 for chunk in util.filechunkiter(fp, limit=size):
332 for chunk in util.filechunkiter(fp, limit=size):
334 handled_bytes += len(chunk)
333 handled_bytes += len(chunk)
335 repo.ui.progress(_('clone'), handled_bytes,
334 repo.ui.progress(_('clone'), handled_bytes,
336 total=bytecount, unit=_('bytes'))
335 total=bytecount, unit=_('bytes'))
337 ofp.write(chunk)
336 ofp.write(chunk)
338
337
339 # force @filecache properties to be reloaded from
338 # force @filecache properties to be reloaded from
340 # streamclone-ed file at next access
339 # streamclone-ed file at next access
341 repo.invalidate(clearfilecache=True)
340 repo.invalidate(clearfilecache=True)
342
341
343 elapsed = time.time() - start
342 elapsed = util.timer() - start
344 if elapsed <= 0:
343 if elapsed <= 0:
345 elapsed = 0.001
344 elapsed = 0.001
346 repo.ui.progress(_('clone'), None)
345 repo.ui.progress(_('clone'), None)
347 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
346 repo.ui.status(_('transferred %s in %.1f seconds (%s/sec)\n') %
348 (util.bytecount(bytecount), elapsed,
347 (util.bytecount(bytecount), elapsed,
349 util.bytecount(bytecount / elapsed)))
348 util.bytecount(bytecount / elapsed)))
350
349
351 def readbundle1header(fp):
350 def readbundle1header(fp):
352 compression = fp.read(2)
351 compression = fp.read(2)
353 if compression != 'UN':
352 if compression != 'UN':
354 raise error.Abort(_('only uncompressed stream clone bundles are '
353 raise error.Abort(_('only uncompressed stream clone bundles are '
355 'supported; got %s') % compression)
354 'supported; got %s') % compression)
356
355
357 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
356 filecount, bytecount = struct.unpack('>QQ', fp.read(16))
358 requireslen = struct.unpack('>H', fp.read(2))[0]
357 requireslen = struct.unpack('>H', fp.read(2))[0]
359 requires = fp.read(requireslen)
358 requires = fp.read(requireslen)
360
359
361 if not requires.endswith('\0'):
360 if not requires.endswith('\0'):
362 raise error.Abort(_('malformed stream clone bundle: '
361 raise error.Abort(_('malformed stream clone bundle: '
363 'requirements not properly encoded'))
362 'requirements not properly encoded'))
364
363
365 requirements = set(requires.rstrip('\0').split(','))
364 requirements = set(requires.rstrip('\0').split(','))
366
365
367 return filecount, bytecount, requirements
366 return filecount, bytecount, requirements
368
367
369 def applybundlev1(repo, fp):
368 def applybundlev1(repo, fp):
370 """Apply the content from a stream clone bundle version 1.
369 """Apply the content from a stream clone bundle version 1.
371
370
372 We assume the 4 byte header has been read and validated and the file handle
371 We assume the 4 byte header has been read and validated and the file handle
373 is at the 2 byte compression identifier.
372 is at the 2 byte compression identifier.
374 """
373 """
375 if len(repo):
374 if len(repo):
376 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
375 raise error.Abort(_('cannot apply stream clone bundle on non-empty '
377 'repo'))
376 'repo'))
378
377
379 filecount, bytecount, requirements = readbundle1header(fp)
378 filecount, bytecount, requirements = readbundle1header(fp)
380 missingreqs = requirements - repo.supportedformats
379 missingreqs = requirements - repo.supportedformats
381 if missingreqs:
380 if missingreqs:
382 raise error.Abort(_('unable to apply stream clone: '
381 raise error.Abort(_('unable to apply stream clone: '
383 'unsupported format: %s') %
382 'unsupported format: %s') %
384 ', '.join(sorted(missingreqs)))
383 ', '.join(sorted(missingreqs)))
385
384
386 consumev1(repo, fp, filecount, bytecount)
385 consumev1(repo, fp, filecount, bytecount)
387
386
388 class streamcloneapplier(object):
387 class streamcloneapplier(object):
389 """Class to manage applying streaming clone bundles.
388 """Class to manage applying streaming clone bundles.
390
389
391 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
390 We need to wrap ``applybundlev1()`` in a dedicated type to enable bundle
392 readers to perform bundle type-specific functionality.
391 readers to perform bundle type-specific functionality.
393 """
392 """
394 def __init__(self, fh):
393 def __init__(self, fh):
395 self._fh = fh
394 self._fh = fh
396
395
397 def apply(self, repo):
396 def apply(self, repo):
398 return applybundlev1(repo, self._fh)
397 return applybundlev1(repo, self._fh)
@@ -1,571 +1,570 b''
1 # tags.py - read tag info from local repository
1 # tags.py - read tag info from local repository
2 #
2 #
3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009 Matt Mackall <mpm@selenic.com>
4 # Copyright 2009 Greg Ward <greg@gerg.ca>
4 # Copyright 2009 Greg Ward <greg@gerg.ca>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 # Currently this module only deals with reading and caching tags.
9 # Currently this module only deals with reading and caching tags.
10 # Eventually, it could take care of updating (adding/removing/moving)
10 # Eventually, it could take care of updating (adding/removing/moving)
11 # tags too.
11 # tags too.
12
12
13 from __future__ import absolute_import
13 from __future__ import absolute_import
14
14
15 import array
15 import array
16 import errno
16 import errno
17 import time
18
17
19 from .node import (
18 from .node import (
20 bin,
19 bin,
21 hex,
20 hex,
22 nullid,
21 nullid,
23 short,
22 short,
24 )
23 )
25 from . import (
24 from . import (
26 encoding,
25 encoding,
27 error,
26 error,
28 util,
27 util,
29 )
28 )
30
29
31 array = array.array
30 array = array.array
32
31
33 # Tags computation can be expensive and caches exist to make it fast in
32 # Tags computation can be expensive and caches exist to make it fast in
34 # the common case.
33 # the common case.
35 #
34 #
36 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
35 # The "hgtagsfnodes1" cache file caches the .hgtags filenode values for
37 # each revision in the repository. The file is effectively an array of
36 # each revision in the repository. The file is effectively an array of
38 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
37 # fixed length records. Read the docs for "hgtagsfnodescache" for technical
39 # details.
38 # details.
40 #
39 #
41 # The .hgtags filenode cache grows in proportion to the length of the
40 # The .hgtags filenode cache grows in proportion to the length of the
42 # changelog. The file is truncated when the # changelog is stripped.
41 # changelog. The file is truncated when the # changelog is stripped.
43 #
42 #
44 # The purpose of the filenode cache is to avoid the most expensive part
43 # The purpose of the filenode cache is to avoid the most expensive part
45 # of finding global tags, which is looking up the .hgtags filenode in the
44 # of finding global tags, which is looking up the .hgtags filenode in the
46 # manifest for each head. This can take dozens or over 100ms for
45 # manifest for each head. This can take dozens or over 100ms for
47 # repositories with very large manifests. Multiplied by dozens or even
46 # repositories with very large manifests. Multiplied by dozens or even
48 # hundreds of heads and there is a significant performance concern.
47 # hundreds of heads and there is a significant performance concern.
49 #
48 #
50 # There also exist a separate cache file for each repository filter.
49 # There also exist a separate cache file for each repository filter.
51 # These "tags-*" files store information about the history of tags.
50 # These "tags-*" files store information about the history of tags.
52 #
51 #
53 # The tags cache files consists of a cache validation line followed by
52 # The tags cache files consists of a cache validation line followed by
54 # a history of tags.
53 # a history of tags.
55 #
54 #
56 # The cache validation line has the format:
55 # The cache validation line has the format:
57 #
56 #
58 # <tiprev> <tipnode> [<filteredhash>]
57 # <tiprev> <tipnode> [<filteredhash>]
59 #
58 #
60 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
59 # <tiprev> is an integer revision and <tipnode> is a 40 character hex
61 # node for that changeset. These redundantly identify the repository
60 # node for that changeset. These redundantly identify the repository
62 # tip from the time the cache was written. In addition, <filteredhash>,
61 # tip from the time the cache was written. In addition, <filteredhash>,
63 # if present, is a 40 character hex hash of the contents of the filtered
62 # if present, is a 40 character hex hash of the contents of the filtered
64 # revisions for this filter. If the set of filtered revs changes, the
63 # revisions for this filter. If the set of filtered revs changes, the
65 # hash will change and invalidate the cache.
64 # hash will change and invalidate the cache.
66 #
65 #
67 # The history part of the tags cache consists of lines of the form:
66 # The history part of the tags cache consists of lines of the form:
68 #
67 #
69 # <node> <tag>
68 # <node> <tag>
70 #
69 #
71 # (This format is identical to that of .hgtags files.)
70 # (This format is identical to that of .hgtags files.)
72 #
71 #
73 # <tag> is the tag name and <node> is the 40 character hex changeset
72 # <tag> is the tag name and <node> is the 40 character hex changeset
74 # the tag is associated with.
73 # the tag is associated with.
75 #
74 #
76 # Tags are written sorted by tag name.
75 # Tags are written sorted by tag name.
77 #
76 #
78 # Tags associated with multiple changesets have an entry for each changeset.
77 # Tags associated with multiple changesets have an entry for each changeset.
79 # The most recent changeset (in terms of revlog ordering for the head
78 # The most recent changeset (in terms of revlog ordering for the head
80 # setting it) for each tag is last.
79 # setting it) for each tag is last.
81
80
82 def findglobaltags(ui, repo, alltags, tagtypes):
81 def findglobaltags(ui, repo, alltags, tagtypes):
83 '''Find global tags in a repo.
82 '''Find global tags in a repo.
84
83
85 "alltags" maps tag name to (node, hist) 2-tuples.
84 "alltags" maps tag name to (node, hist) 2-tuples.
86
85
87 "tagtypes" maps tag name to tag type. Global tags always have the
86 "tagtypes" maps tag name to tag type. Global tags always have the
88 "global" tag type.
87 "global" tag type.
89
88
90 The "alltags" and "tagtypes" dicts are updated in place. Empty dicts
89 The "alltags" and "tagtypes" dicts are updated in place. Empty dicts
91 should be passed in.
90 should be passed in.
92
91
93 The tags cache is read and updated as a side-effect of calling.
92 The tags cache is read and updated as a side-effect of calling.
94 '''
93 '''
95 # This is so we can be lazy and assume alltags contains only global
94 # This is so we can be lazy and assume alltags contains only global
96 # tags when we pass it to _writetagcache().
95 # tags when we pass it to _writetagcache().
97 assert len(alltags) == len(tagtypes) == 0, \
96 assert len(alltags) == len(tagtypes) == 0, \
98 "findglobaltags() should be called first"
97 "findglobaltags() should be called first"
99
98
100 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
99 (heads, tagfnode, valid, cachetags, shouldwrite) = _readtagcache(ui, repo)
101 if cachetags is not None:
100 if cachetags is not None:
102 assert not shouldwrite
101 assert not shouldwrite
103 # XXX is this really 100% correct? are there oddball special
102 # XXX is this really 100% correct? are there oddball special
104 # cases where a global tag should outrank a local tag but won't,
103 # cases where a global tag should outrank a local tag but won't,
105 # because cachetags does not contain rank info?
104 # because cachetags does not contain rank info?
106 _updatetags(cachetags, 'global', alltags, tagtypes)
105 _updatetags(cachetags, 'global', alltags, tagtypes)
107 return
106 return
108
107
109 seen = set() # set of fnode
108 seen = set() # set of fnode
110 fctx = None
109 fctx = None
111 for head in reversed(heads): # oldest to newest
110 for head in reversed(heads): # oldest to newest
112 assert head in repo.changelog.nodemap, \
111 assert head in repo.changelog.nodemap, \
113 "tag cache returned bogus head %s" % short(head)
112 "tag cache returned bogus head %s" % short(head)
114
113
115 fnode = tagfnode.get(head)
114 fnode = tagfnode.get(head)
116 if fnode and fnode not in seen:
115 if fnode and fnode not in seen:
117 seen.add(fnode)
116 seen.add(fnode)
118 if not fctx:
117 if not fctx:
119 fctx = repo.filectx('.hgtags', fileid=fnode)
118 fctx = repo.filectx('.hgtags', fileid=fnode)
120 else:
119 else:
121 fctx = fctx.filectx(fnode)
120 fctx = fctx.filectx(fnode)
122
121
123 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
122 filetags = _readtags(ui, repo, fctx.data().splitlines(), fctx)
124 _updatetags(filetags, 'global', alltags, tagtypes)
123 _updatetags(filetags, 'global', alltags, tagtypes)
125
124
126 # and update the cache (if necessary)
125 # and update the cache (if necessary)
127 if shouldwrite:
126 if shouldwrite:
128 _writetagcache(ui, repo, valid, alltags)
127 _writetagcache(ui, repo, valid, alltags)
129
128
130 def readlocaltags(ui, repo, alltags, tagtypes):
129 def readlocaltags(ui, repo, alltags, tagtypes):
131 '''Read local tags in repo. Update alltags and tagtypes.'''
130 '''Read local tags in repo. Update alltags and tagtypes.'''
132 try:
131 try:
133 data = repo.vfs.read("localtags")
132 data = repo.vfs.read("localtags")
134 except IOError as inst:
133 except IOError as inst:
135 if inst.errno != errno.ENOENT:
134 if inst.errno != errno.ENOENT:
136 raise
135 raise
137 return
136 return
138
137
139 # localtags is in the local encoding; re-encode to UTF-8 on
138 # localtags is in the local encoding; re-encode to UTF-8 on
140 # input for consistency with the rest of this module.
139 # input for consistency with the rest of this module.
141 filetags = _readtags(
140 filetags = _readtags(
142 ui, repo, data.splitlines(), "localtags",
141 ui, repo, data.splitlines(), "localtags",
143 recode=encoding.fromlocal)
142 recode=encoding.fromlocal)
144
143
145 # remove tags pointing to invalid nodes
144 # remove tags pointing to invalid nodes
146 cl = repo.changelog
145 cl = repo.changelog
147 for t in filetags.keys():
146 for t in filetags.keys():
148 try:
147 try:
149 cl.rev(filetags[t][0])
148 cl.rev(filetags[t][0])
150 except (LookupError, ValueError):
149 except (LookupError, ValueError):
151 del filetags[t]
150 del filetags[t]
152
151
153 _updatetags(filetags, "local", alltags, tagtypes)
152 _updatetags(filetags, "local", alltags, tagtypes)
154
153
155 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
154 def _readtaghist(ui, repo, lines, fn, recode=None, calcnodelines=False):
156 '''Read tag definitions from a file (or any source of lines).
155 '''Read tag definitions from a file (or any source of lines).
157
156
158 This function returns two sortdicts with similar information:
157 This function returns two sortdicts with similar information:
159
158
160 - the first dict, bintaghist, contains the tag information as expected by
159 - the first dict, bintaghist, contains the tag information as expected by
161 the _readtags function, i.e. a mapping from tag name to (node, hist):
160 the _readtags function, i.e. a mapping from tag name to (node, hist):
162 - node is the node id from the last line read for that name,
161 - node is the node id from the last line read for that name,
163 - hist is the list of node ids previously associated with it (in file
162 - hist is the list of node ids previously associated with it (in file
164 order). All node ids are binary, not hex.
163 order). All node ids are binary, not hex.
165
164
166 - the second dict, hextaglines, is a mapping from tag name to a list of
165 - the second dict, hextaglines, is a mapping from tag name to a list of
167 [hexnode, line number] pairs, ordered from the oldest to the newest node.
166 [hexnode, line number] pairs, ordered from the oldest to the newest node.
168
167
169 When calcnodelines is False the hextaglines dict is not calculated (an
168 When calcnodelines is False the hextaglines dict is not calculated (an
170 empty dict is returned). This is done to improve this function's
169 empty dict is returned). This is done to improve this function's
171 performance in cases where the line numbers are not needed.
170 performance in cases where the line numbers are not needed.
172 '''
171 '''
173
172
174 bintaghist = util.sortdict()
173 bintaghist = util.sortdict()
175 hextaglines = util.sortdict()
174 hextaglines = util.sortdict()
176 count = 0
175 count = 0
177
176
178 def dbg(msg):
177 def dbg(msg):
179 ui.debug("%s, line %s: %s\n" % (fn, count, msg))
178 ui.debug("%s, line %s: %s\n" % (fn, count, msg))
180
179
181 for nline, line in enumerate(lines):
180 for nline, line in enumerate(lines):
182 count += 1
181 count += 1
183 if not line:
182 if not line:
184 continue
183 continue
185 try:
184 try:
186 (nodehex, name) = line.split(" ", 1)
185 (nodehex, name) = line.split(" ", 1)
187 except ValueError:
186 except ValueError:
188 dbg("cannot parse entry")
187 dbg("cannot parse entry")
189 continue
188 continue
190 name = name.strip()
189 name = name.strip()
191 if recode:
190 if recode:
192 name = recode(name)
191 name = recode(name)
193 try:
192 try:
194 nodebin = bin(nodehex)
193 nodebin = bin(nodehex)
195 except TypeError:
194 except TypeError:
196 dbg("node '%s' is not well formed" % nodehex)
195 dbg("node '%s' is not well formed" % nodehex)
197 continue
196 continue
198
197
199 # update filetags
198 # update filetags
200 if calcnodelines:
199 if calcnodelines:
201 # map tag name to a list of line numbers
200 # map tag name to a list of line numbers
202 if name not in hextaglines:
201 if name not in hextaglines:
203 hextaglines[name] = []
202 hextaglines[name] = []
204 hextaglines[name].append([nodehex, nline])
203 hextaglines[name].append([nodehex, nline])
205 continue
204 continue
206 # map tag name to (node, hist)
205 # map tag name to (node, hist)
207 if name not in bintaghist:
206 if name not in bintaghist:
208 bintaghist[name] = []
207 bintaghist[name] = []
209 bintaghist[name].append(nodebin)
208 bintaghist[name].append(nodebin)
210 return bintaghist, hextaglines
209 return bintaghist, hextaglines
211
210
212 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
211 def _readtags(ui, repo, lines, fn, recode=None, calcnodelines=False):
213 '''Read tag definitions from a file (or any source of lines).
212 '''Read tag definitions from a file (or any source of lines).
214
213
215 Returns a mapping from tag name to (node, hist).
214 Returns a mapping from tag name to (node, hist).
216
215
217 "node" is the node id from the last line read for that name. "hist"
216 "node" is the node id from the last line read for that name. "hist"
218 is the list of node ids previously associated with it (in file order).
217 is the list of node ids previously associated with it (in file order).
219 All node ids are binary, not hex.
218 All node ids are binary, not hex.
220 '''
219 '''
221 filetags, nodelines = _readtaghist(ui, repo, lines, fn, recode=recode,
220 filetags, nodelines = _readtaghist(ui, repo, lines, fn, recode=recode,
222 calcnodelines=calcnodelines)
221 calcnodelines=calcnodelines)
223 # util.sortdict().__setitem__ is much slower at replacing then inserting
222 # util.sortdict().__setitem__ is much slower at replacing then inserting
224 # new entries. The difference can matter if there are thousands of tags.
223 # new entries. The difference can matter if there are thousands of tags.
225 # Create a new sortdict to avoid the performance penalty.
224 # Create a new sortdict to avoid the performance penalty.
226 newtags = util.sortdict()
225 newtags = util.sortdict()
227 for tag, taghist in filetags.items():
226 for tag, taghist in filetags.items():
228 newtags[tag] = (taghist[-1], taghist[:-1])
227 newtags[tag] = (taghist[-1], taghist[:-1])
229 return newtags
228 return newtags
230
229
231 def _updatetags(filetags, tagtype, alltags, tagtypes):
230 def _updatetags(filetags, tagtype, alltags, tagtypes):
232 '''Incorporate the tag info read from one file into the two
231 '''Incorporate the tag info read from one file into the two
233 dictionaries, alltags and tagtypes, that contain all tag
232 dictionaries, alltags and tagtypes, that contain all tag
234 info (global across all heads plus local).'''
233 info (global across all heads plus local).'''
235
234
236 for name, nodehist in filetags.iteritems():
235 for name, nodehist in filetags.iteritems():
237 if name not in alltags:
236 if name not in alltags:
238 alltags[name] = nodehist
237 alltags[name] = nodehist
239 tagtypes[name] = tagtype
238 tagtypes[name] = tagtype
240 continue
239 continue
241
240
242 # we prefer alltags[name] if:
241 # we prefer alltags[name] if:
243 # it supersedes us OR
242 # it supersedes us OR
244 # mutual supersedes and it has a higher rank
243 # mutual supersedes and it has a higher rank
245 # otherwise we win because we're tip-most
244 # otherwise we win because we're tip-most
246 anode, ahist = nodehist
245 anode, ahist = nodehist
247 bnode, bhist = alltags[name]
246 bnode, bhist = alltags[name]
248 if (bnode != anode and anode in bhist and
247 if (bnode != anode and anode in bhist and
249 (bnode not in ahist or len(bhist) > len(ahist))):
248 (bnode not in ahist or len(bhist) > len(ahist))):
250 anode = bnode
249 anode = bnode
251 else:
250 else:
252 tagtypes[name] = tagtype
251 tagtypes[name] = tagtype
253 ahist.extend([n for n in bhist if n not in ahist])
252 ahist.extend([n for n in bhist if n not in ahist])
254 alltags[name] = anode, ahist
253 alltags[name] = anode, ahist
255
254
256 def _filename(repo):
255 def _filename(repo):
257 """name of a tagcache file for a given repo or repoview"""
256 """name of a tagcache file for a given repo or repoview"""
258 filename = 'cache/tags2'
257 filename = 'cache/tags2'
259 if repo.filtername:
258 if repo.filtername:
260 filename = '%s-%s' % (filename, repo.filtername)
259 filename = '%s-%s' % (filename, repo.filtername)
261 return filename
260 return filename
262
261
263 def _readtagcache(ui, repo):
262 def _readtagcache(ui, repo):
264 '''Read the tag cache.
263 '''Read the tag cache.
265
264
266 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
265 Returns a tuple (heads, fnodes, validinfo, cachetags, shouldwrite).
267
266
268 If the cache is completely up-to-date, "cachetags" is a dict of the
267 If the cache is completely up-to-date, "cachetags" is a dict of the
269 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
268 form returned by _readtags() and "heads", "fnodes", and "validinfo" are
270 None and "shouldwrite" is False.
269 None and "shouldwrite" is False.
271
270
272 If the cache is not up to date, "cachetags" is None. "heads" is a list
271 If the cache is not up to date, "cachetags" is None. "heads" is a list
273 of all heads currently in the repository, ordered from tip to oldest.
272 of all heads currently in the repository, ordered from tip to oldest.
274 "validinfo" is a tuple describing cache validation info. This is used
273 "validinfo" is a tuple describing cache validation info. This is used
275 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
274 when writing the tags cache. "fnodes" is a mapping from head to .hgtags
276 filenode. "shouldwrite" is True.
275 filenode. "shouldwrite" is True.
277
276
278 If the cache is not up to date, the caller is responsible for reading tag
277 If the cache is not up to date, the caller is responsible for reading tag
279 info from each returned head. (See findglobaltags().)
278 info from each returned head. (See findglobaltags().)
280 '''
279 '''
281 from . import scmutil # avoid cycle
280 from . import scmutil # avoid cycle
282
281
283 try:
282 try:
284 cachefile = repo.vfs(_filename(repo), 'r')
283 cachefile = repo.vfs(_filename(repo), 'r')
285 # force reading the file for static-http
284 # force reading the file for static-http
286 cachelines = iter(cachefile)
285 cachelines = iter(cachefile)
287 except IOError:
286 except IOError:
288 cachefile = None
287 cachefile = None
289
288
290 cacherev = None
289 cacherev = None
291 cachenode = None
290 cachenode = None
292 cachehash = None
291 cachehash = None
293 if cachefile:
292 if cachefile:
294 try:
293 try:
295 validline = next(cachelines)
294 validline = next(cachelines)
296 validline = validline.split()
295 validline = validline.split()
297 cacherev = int(validline[0])
296 cacherev = int(validline[0])
298 cachenode = bin(validline[1])
297 cachenode = bin(validline[1])
299 if len(validline) > 2:
298 if len(validline) > 2:
300 cachehash = bin(validline[2])
299 cachehash = bin(validline[2])
301 except Exception:
300 except Exception:
302 # corruption of the cache, just recompute it.
301 # corruption of the cache, just recompute it.
303 pass
302 pass
304
303
305 tipnode = repo.changelog.tip()
304 tipnode = repo.changelog.tip()
306 tiprev = len(repo.changelog) - 1
305 tiprev = len(repo.changelog) - 1
307
306
308 # Case 1 (common): tip is the same, so nothing has changed.
307 # Case 1 (common): tip is the same, so nothing has changed.
309 # (Unchanged tip trivially means no changesets have been added.
308 # (Unchanged tip trivially means no changesets have been added.
310 # But, thanks to localrepository.destroyed(), it also means none
309 # But, thanks to localrepository.destroyed(), it also means none
311 # have been destroyed by strip or rollback.)
310 # have been destroyed by strip or rollback.)
312 if (cacherev == tiprev
311 if (cacherev == tiprev
313 and cachenode == tipnode
312 and cachenode == tipnode
314 and cachehash == scmutil.filteredhash(repo, tiprev)):
313 and cachehash == scmutil.filteredhash(repo, tiprev)):
315 tags = _readtags(ui, repo, cachelines, cachefile.name)
314 tags = _readtags(ui, repo, cachelines, cachefile.name)
316 cachefile.close()
315 cachefile.close()
317 return (None, None, None, tags, False)
316 return (None, None, None, tags, False)
318 if cachefile:
317 if cachefile:
319 cachefile.close() # ignore rest of file
318 cachefile.close() # ignore rest of file
320
319
321 valid = (tiprev, tipnode, scmutil.filteredhash(repo, tiprev))
320 valid = (tiprev, tipnode, scmutil.filteredhash(repo, tiprev))
322
321
323 repoheads = repo.heads()
322 repoheads = repo.heads()
324 # Case 2 (uncommon): empty repo; get out quickly and don't bother
323 # Case 2 (uncommon): empty repo; get out quickly and don't bother
325 # writing an empty cache.
324 # writing an empty cache.
326 if repoheads == [nullid]:
325 if repoheads == [nullid]:
327 return ([], {}, valid, {}, False)
326 return ([], {}, valid, {}, False)
328
327
329 # Case 3 (uncommon): cache file missing or empty.
328 # Case 3 (uncommon): cache file missing or empty.
330
329
331 # Case 4 (uncommon): tip rev decreased. This should only happen
330 # Case 4 (uncommon): tip rev decreased. This should only happen
332 # when we're called from localrepository.destroyed(). Refresh the
331 # when we're called from localrepository.destroyed(). Refresh the
333 # cache so future invocations will not see disappeared heads in the
332 # cache so future invocations will not see disappeared heads in the
334 # cache.
333 # cache.
335
334
336 # Case 5 (common): tip has changed, so we've added/replaced heads.
335 # Case 5 (common): tip has changed, so we've added/replaced heads.
337
336
338 # As it happens, the code to handle cases 3, 4, 5 is the same.
337 # As it happens, the code to handle cases 3, 4, 5 is the same.
339
338
340 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
339 # N.B. in case 4 (nodes destroyed), "new head" really means "newly
341 # exposed".
340 # exposed".
342 if not len(repo.file('.hgtags')):
341 if not len(repo.file('.hgtags')):
343 # No tags have ever been committed, so we can avoid a
342 # No tags have ever been committed, so we can avoid a
344 # potentially expensive search.
343 # potentially expensive search.
345 return ([], {}, valid, None, True)
344 return ([], {}, valid, None, True)
346
345
347 starttime = time.time()
346 starttime = util.timer()
348
347
349 # Now we have to lookup the .hgtags filenode for every new head.
348 # Now we have to lookup the .hgtags filenode for every new head.
350 # This is the most expensive part of finding tags, so performance
349 # This is the most expensive part of finding tags, so performance
351 # depends primarily on the size of newheads. Worst case: no cache
350 # depends primarily on the size of newheads. Worst case: no cache
352 # file, so newheads == repoheads.
351 # file, so newheads == repoheads.
353 fnodescache = hgtagsfnodescache(repo.unfiltered())
352 fnodescache = hgtagsfnodescache(repo.unfiltered())
354 cachefnode = {}
353 cachefnode = {}
355 for head in reversed(repoheads):
354 for head in reversed(repoheads):
356 fnode = fnodescache.getfnode(head)
355 fnode = fnodescache.getfnode(head)
357 if fnode != nullid:
356 if fnode != nullid:
358 cachefnode[head] = fnode
357 cachefnode[head] = fnode
359
358
360 fnodescache.write()
359 fnodescache.write()
361
360
362 duration = time.time() - starttime
361 duration = util.timer() - starttime
363 ui.log('tagscache',
362 ui.log('tagscache',
364 '%d/%d cache hits/lookups in %0.4f '
363 '%d/%d cache hits/lookups in %0.4f '
365 'seconds\n',
364 'seconds\n',
366 fnodescache.hitcount, fnodescache.lookupcount, duration)
365 fnodescache.hitcount, fnodescache.lookupcount, duration)
367
366
368 # Caller has to iterate over all heads, but can use the filenodes in
367 # Caller has to iterate over all heads, but can use the filenodes in
369 # cachefnode to get to each .hgtags revision quickly.
368 # cachefnode to get to each .hgtags revision quickly.
370 return (repoheads, cachefnode, valid, None, True)
369 return (repoheads, cachefnode, valid, None, True)
371
370
372 def _writetagcache(ui, repo, valid, cachetags):
371 def _writetagcache(ui, repo, valid, cachetags):
373 filename = _filename(repo)
372 filename = _filename(repo)
374 try:
373 try:
375 cachefile = repo.vfs(filename, 'w', atomictemp=True)
374 cachefile = repo.vfs(filename, 'w', atomictemp=True)
376 except (OSError, IOError):
375 except (OSError, IOError):
377 return
376 return
378
377
379 ui.log('tagscache', 'writing .hg/%s with %d tags\n',
378 ui.log('tagscache', 'writing .hg/%s with %d tags\n',
380 filename, len(cachetags))
379 filename, len(cachetags))
381
380
382 if valid[2]:
381 if valid[2]:
383 cachefile.write('%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2])))
382 cachefile.write('%d %s %s\n' % (valid[0], hex(valid[1]), hex(valid[2])))
384 else:
383 else:
385 cachefile.write('%d %s\n' % (valid[0], hex(valid[1])))
384 cachefile.write('%d %s\n' % (valid[0], hex(valid[1])))
386
385
387 # Tag names in the cache are in UTF-8 -- which is the whole reason
386 # Tag names in the cache are in UTF-8 -- which is the whole reason
388 # we keep them in UTF-8 throughout this module. If we converted
387 # we keep them in UTF-8 throughout this module. If we converted
389 # them local encoding on input, we would lose info writing them to
388 # them local encoding on input, we would lose info writing them to
390 # the cache.
389 # the cache.
391 for (name, (node, hist)) in sorted(cachetags.iteritems()):
390 for (name, (node, hist)) in sorted(cachetags.iteritems()):
392 for n in hist:
391 for n in hist:
393 cachefile.write("%s %s\n" % (hex(n), name))
392 cachefile.write("%s %s\n" % (hex(n), name))
394 cachefile.write("%s %s\n" % (hex(node), name))
393 cachefile.write("%s %s\n" % (hex(node), name))
395
394
396 try:
395 try:
397 cachefile.close()
396 cachefile.close()
398 except (OSError, IOError):
397 except (OSError, IOError):
399 pass
398 pass
400
399
401 _fnodescachefile = 'cache/hgtagsfnodes1'
400 _fnodescachefile = 'cache/hgtagsfnodes1'
402 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
401 _fnodesrecsize = 4 + 20 # changeset fragment + filenode
403 _fnodesmissingrec = '\xff' * 24
402 _fnodesmissingrec = '\xff' * 24
404
403
405 class hgtagsfnodescache(object):
404 class hgtagsfnodescache(object):
406 """Persistent cache mapping revisions to .hgtags filenodes.
405 """Persistent cache mapping revisions to .hgtags filenodes.
407
406
408 The cache is an array of records. Each item in the array corresponds to
407 The cache is an array of records. Each item in the array corresponds to
409 a changelog revision. Values in the array contain the first 4 bytes of
408 a changelog revision. Values in the array contain the first 4 bytes of
410 the node hash and the 20 bytes .hgtags filenode for that revision.
409 the node hash and the 20 bytes .hgtags filenode for that revision.
411
410
412 The first 4 bytes are present as a form of verification. Repository
411 The first 4 bytes are present as a form of verification. Repository
413 stripping and rewriting may change the node at a numeric revision in the
412 stripping and rewriting may change the node at a numeric revision in the
414 changelog. The changeset fragment serves as a verifier to detect
413 changelog. The changeset fragment serves as a verifier to detect
415 rewriting. This logic is shared with the rev branch cache (see
414 rewriting. This logic is shared with the rev branch cache (see
416 branchmap.py).
415 branchmap.py).
417
416
418 The instance holds in memory the full cache content but entries are
417 The instance holds in memory the full cache content but entries are
419 only parsed on read.
418 only parsed on read.
420
419
421 Instances behave like lists. ``c[i]`` works where i is a rev or
420 Instances behave like lists. ``c[i]`` works where i is a rev or
422 changeset node. Missing indexes are populated automatically on access.
421 changeset node. Missing indexes are populated automatically on access.
423 """
422 """
424 def __init__(self, repo):
423 def __init__(self, repo):
425 assert repo.filtername is None
424 assert repo.filtername is None
426
425
427 self._repo = repo
426 self._repo = repo
428
427
429 # Only for reporting purposes.
428 # Only for reporting purposes.
430 self.lookupcount = 0
429 self.lookupcount = 0
431 self.hitcount = 0
430 self.hitcount = 0
432
431
433 self._raw = array('c')
432 self._raw = array('c')
434
433
435 try:
434 try:
436 data = repo.vfs.read(_fnodescachefile)
435 data = repo.vfs.read(_fnodescachefile)
437 except (OSError, IOError):
436 except (OSError, IOError):
438 data = ""
437 data = ""
439 self._raw.fromstring(data)
438 self._raw.fromstring(data)
440
439
441 # The end state of self._raw is an array that is of the exact length
440 # The end state of self._raw is an array that is of the exact length
442 # required to hold a record for every revision in the repository.
441 # required to hold a record for every revision in the repository.
443 # We truncate or extend the array as necessary. self._dirtyoffset is
442 # We truncate or extend the array as necessary. self._dirtyoffset is
444 # defined to be the start offset at which we need to write the output
443 # defined to be the start offset at which we need to write the output
445 # file. This offset is also adjusted when new entries are calculated
444 # file. This offset is also adjusted when new entries are calculated
446 # for array members.
445 # for array members.
447 cllen = len(repo.changelog)
446 cllen = len(repo.changelog)
448 wantedlen = cllen * _fnodesrecsize
447 wantedlen = cllen * _fnodesrecsize
449 rawlen = len(self._raw)
448 rawlen = len(self._raw)
450
449
451 self._dirtyoffset = None
450 self._dirtyoffset = None
452
451
453 if rawlen < wantedlen:
452 if rawlen < wantedlen:
454 self._dirtyoffset = rawlen
453 self._dirtyoffset = rawlen
455 self._raw.extend('\xff' * (wantedlen - rawlen))
454 self._raw.extend('\xff' * (wantedlen - rawlen))
456 elif rawlen > wantedlen:
455 elif rawlen > wantedlen:
457 # There's no easy way to truncate array instances. This seems
456 # There's no easy way to truncate array instances. This seems
458 # slightly less evil than copying a potentially large array slice.
457 # slightly less evil than copying a potentially large array slice.
459 for i in range(rawlen - wantedlen):
458 for i in range(rawlen - wantedlen):
460 self._raw.pop()
459 self._raw.pop()
461 self._dirtyoffset = len(self._raw)
460 self._dirtyoffset = len(self._raw)
462
461
463 def getfnode(self, node, computemissing=True):
462 def getfnode(self, node, computemissing=True):
464 """Obtain the filenode of the .hgtags file at a specified revision.
463 """Obtain the filenode of the .hgtags file at a specified revision.
465
464
466 If the value is in the cache, the entry will be validated and returned.
465 If the value is in the cache, the entry will be validated and returned.
467 Otherwise, the filenode will be computed and returned unless
466 Otherwise, the filenode will be computed and returned unless
468 "computemissing" is False, in which case None will be returned without
467 "computemissing" is False, in which case None will be returned without
469 any potentially expensive computation being performed.
468 any potentially expensive computation being performed.
470
469
471 If an .hgtags does not exist at the specified revision, nullid is
470 If an .hgtags does not exist at the specified revision, nullid is
472 returned.
471 returned.
473 """
472 """
474 ctx = self._repo[node]
473 ctx = self._repo[node]
475 rev = ctx.rev()
474 rev = ctx.rev()
476
475
477 self.lookupcount += 1
476 self.lookupcount += 1
478
477
479 offset = rev * _fnodesrecsize
478 offset = rev * _fnodesrecsize
480 record = self._raw[offset:offset + _fnodesrecsize].tostring()
479 record = self._raw[offset:offset + _fnodesrecsize].tostring()
481 properprefix = node[0:4]
480 properprefix = node[0:4]
482
481
483 # Validate and return existing entry.
482 # Validate and return existing entry.
484 if record != _fnodesmissingrec:
483 if record != _fnodesmissingrec:
485 fileprefix = record[0:4]
484 fileprefix = record[0:4]
486
485
487 if fileprefix == properprefix:
486 if fileprefix == properprefix:
488 self.hitcount += 1
487 self.hitcount += 1
489 return record[4:]
488 return record[4:]
490
489
491 # Fall through.
490 # Fall through.
492
491
493 # If we get here, the entry is either missing or invalid.
492 # If we get here, the entry is either missing or invalid.
494
493
495 if not computemissing:
494 if not computemissing:
496 return None
495 return None
497
496
498 # Populate missing entry.
497 # Populate missing entry.
499 try:
498 try:
500 fnode = ctx.filenode('.hgtags')
499 fnode = ctx.filenode('.hgtags')
501 except error.LookupError:
500 except error.LookupError:
502 # No .hgtags file on this revision.
501 # No .hgtags file on this revision.
503 fnode = nullid
502 fnode = nullid
504
503
505 self._writeentry(offset, properprefix, fnode)
504 self._writeentry(offset, properprefix, fnode)
506 return fnode
505 return fnode
507
506
508 def setfnode(self, node, fnode):
507 def setfnode(self, node, fnode):
509 """Set the .hgtags filenode for a given changeset."""
508 """Set the .hgtags filenode for a given changeset."""
510 assert len(fnode) == 20
509 assert len(fnode) == 20
511 ctx = self._repo[node]
510 ctx = self._repo[node]
512
511
513 # Do a lookup first to avoid writing if nothing has changed.
512 # Do a lookup first to avoid writing if nothing has changed.
514 if self.getfnode(ctx.node(), computemissing=False) == fnode:
513 if self.getfnode(ctx.node(), computemissing=False) == fnode:
515 return
514 return
516
515
517 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
516 self._writeentry(ctx.rev() * _fnodesrecsize, node[0:4], fnode)
518
517
519 def _writeentry(self, offset, prefix, fnode):
518 def _writeentry(self, offset, prefix, fnode):
520 # Slices on array instances only accept other array.
519 # Slices on array instances only accept other array.
521 entry = array('c', prefix + fnode)
520 entry = array('c', prefix + fnode)
522 self._raw[offset:offset + _fnodesrecsize] = entry
521 self._raw[offset:offset + _fnodesrecsize] = entry
523 # self._dirtyoffset could be None.
522 # self._dirtyoffset could be None.
524 self._dirtyoffset = min(self._dirtyoffset, offset) or 0
523 self._dirtyoffset = min(self._dirtyoffset, offset) or 0
525
524
526 def write(self):
525 def write(self):
527 """Perform all necessary writes to cache file.
526 """Perform all necessary writes to cache file.
528
527
529 This may no-op if no writes are needed or if a write lock could
528 This may no-op if no writes are needed or if a write lock could
530 not be obtained.
529 not be obtained.
531 """
530 """
532 if self._dirtyoffset is None:
531 if self._dirtyoffset is None:
533 return
532 return
534
533
535 data = self._raw[self._dirtyoffset:]
534 data = self._raw[self._dirtyoffset:]
536 if not data:
535 if not data:
537 return
536 return
538
537
539 repo = self._repo
538 repo = self._repo
540
539
541 try:
540 try:
542 lock = repo.wlock(wait=False)
541 lock = repo.wlock(wait=False)
543 except error.LockError:
542 except error.LockError:
544 repo.ui.log('tagscache',
543 repo.ui.log('tagscache',
545 'not writing .hg/%s because lock cannot be acquired\n' %
544 'not writing .hg/%s because lock cannot be acquired\n' %
546 (_fnodescachefile))
545 (_fnodescachefile))
547 return
546 return
548
547
549 try:
548 try:
550 f = repo.vfs.open(_fnodescachefile, 'ab')
549 f = repo.vfs.open(_fnodescachefile, 'ab')
551 try:
550 try:
552 # if the file has been truncated
551 # if the file has been truncated
553 actualoffset = f.tell()
552 actualoffset = f.tell()
554 if actualoffset < self._dirtyoffset:
553 if actualoffset < self._dirtyoffset:
555 self._dirtyoffset = actualoffset
554 self._dirtyoffset = actualoffset
556 data = self._raw[self._dirtyoffset:]
555 data = self._raw[self._dirtyoffset:]
557 f.seek(self._dirtyoffset)
556 f.seek(self._dirtyoffset)
558 f.truncate()
557 f.truncate()
559 repo.ui.log('tagscache',
558 repo.ui.log('tagscache',
560 'writing %d bytes to %s\n' % (
559 'writing %d bytes to %s\n' % (
561 len(data), _fnodescachefile))
560 len(data), _fnodescachefile))
562 f.write(data)
561 f.write(data)
563 self._dirtyoffset = None
562 self._dirtyoffset = None
564 finally:
563 finally:
565 f.close()
564 f.close()
566 except (IOError, OSError) as inst:
565 except (IOError, OSError) as inst:
567 repo.ui.log('tagscache',
566 repo.ui.log('tagscache',
568 "couldn't write %s: %s\n" % (
567 "couldn't write %s: %s\n" % (
569 _fnodescachefile, inst))
568 _fnodescachefile, inst))
570 finally:
569 finally:
571 lock.release()
570 lock.release()
@@ -1,3556 +1,3556 b''
1 # util.py - Mercurial utility functions and platform specific implementations
1 # util.py - Mercurial utility functions and platform specific implementations
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """Mercurial utility functions and platform specific implementations.
10 """Mercurial utility functions and platform specific implementations.
11
11
12 This contains helper routines that are independent of the SCM core and
12 This contains helper routines that are independent of the SCM core and
13 hide platform-specific details from the core.
13 hide platform-specific details from the core.
14 """
14 """
15
15
16 from __future__ import absolute_import
16 from __future__ import absolute_import
17
17
18 import bz2
18 import bz2
19 import calendar
19 import calendar
20 import collections
20 import collections
21 import datetime
21 import datetime
22 import errno
22 import errno
23 import gc
23 import gc
24 import hashlib
24 import hashlib
25 import imp
25 import imp
26 import os
26 import os
27 import platform as pyplatform
27 import platform as pyplatform
28 import re as remod
28 import re as remod
29 import shutil
29 import shutil
30 import signal
30 import signal
31 import socket
31 import socket
32 import stat
32 import stat
33 import string
33 import string
34 import subprocess
34 import subprocess
35 import sys
35 import sys
36 import tempfile
36 import tempfile
37 import textwrap
37 import textwrap
38 import time
38 import time
39 import traceback
39 import traceback
40 import zlib
40 import zlib
41
41
42 from . import (
42 from . import (
43 encoding,
43 encoding,
44 error,
44 error,
45 i18n,
45 i18n,
46 osutil,
46 osutil,
47 parsers,
47 parsers,
48 pycompat,
48 pycompat,
49 )
49 )
50
50
51 empty = pycompat.empty
51 empty = pycompat.empty
52 httplib = pycompat.httplib
52 httplib = pycompat.httplib
53 httpserver = pycompat.httpserver
53 httpserver = pycompat.httpserver
54 pickle = pycompat.pickle
54 pickle = pycompat.pickle
55 queue = pycompat.queue
55 queue = pycompat.queue
56 socketserver = pycompat.socketserver
56 socketserver = pycompat.socketserver
57 stderr = pycompat.stderr
57 stderr = pycompat.stderr
58 stdin = pycompat.stdin
58 stdin = pycompat.stdin
59 stdout = pycompat.stdout
59 stdout = pycompat.stdout
60 stringio = pycompat.stringio
60 stringio = pycompat.stringio
61 urlerr = pycompat.urlerr
61 urlerr = pycompat.urlerr
62 urlparse = pycompat.urlparse
62 urlparse = pycompat.urlparse
63 urlreq = pycompat.urlreq
63 urlreq = pycompat.urlreq
64 xmlrpclib = pycompat.xmlrpclib
64 xmlrpclib = pycompat.xmlrpclib
65
65
66 def isatty(fp):
66 def isatty(fp):
67 try:
67 try:
68 return fp.isatty()
68 return fp.isatty()
69 except AttributeError:
69 except AttributeError:
70 return False
70 return False
71
71
72 # glibc determines buffering on first write to stdout - if we replace a TTY
72 # glibc determines buffering on first write to stdout - if we replace a TTY
73 # destined stdout with a pipe destined stdout (e.g. pager), we want line
73 # destined stdout with a pipe destined stdout (e.g. pager), we want line
74 # buffering
74 # buffering
75 if isatty(stdout):
75 if isatty(stdout):
76 stdout = os.fdopen(stdout.fileno(), 'wb', 1)
76 stdout = os.fdopen(stdout.fileno(), 'wb', 1)
77
77
78 if pycompat.osname == 'nt':
78 if pycompat.osname == 'nt':
79 from . import windows as platform
79 from . import windows as platform
80 stdout = platform.winstdout(stdout)
80 stdout = platform.winstdout(stdout)
81 else:
81 else:
82 from . import posix as platform
82 from . import posix as platform
83
83
84 _ = i18n._
84 _ = i18n._
85
85
86 bindunixsocket = platform.bindunixsocket
86 bindunixsocket = platform.bindunixsocket
87 cachestat = platform.cachestat
87 cachestat = platform.cachestat
88 checkexec = platform.checkexec
88 checkexec = platform.checkexec
89 checklink = platform.checklink
89 checklink = platform.checklink
90 copymode = platform.copymode
90 copymode = platform.copymode
91 executablepath = platform.executablepath
91 executablepath = platform.executablepath
92 expandglobs = platform.expandglobs
92 expandglobs = platform.expandglobs
93 explainexit = platform.explainexit
93 explainexit = platform.explainexit
94 findexe = platform.findexe
94 findexe = platform.findexe
95 gethgcmd = platform.gethgcmd
95 gethgcmd = platform.gethgcmd
96 getuser = platform.getuser
96 getuser = platform.getuser
97 getpid = os.getpid
97 getpid = os.getpid
98 groupmembers = platform.groupmembers
98 groupmembers = platform.groupmembers
99 groupname = platform.groupname
99 groupname = platform.groupname
100 hidewindow = platform.hidewindow
100 hidewindow = platform.hidewindow
101 isexec = platform.isexec
101 isexec = platform.isexec
102 isowner = platform.isowner
102 isowner = platform.isowner
103 localpath = platform.localpath
103 localpath = platform.localpath
104 lookupreg = platform.lookupreg
104 lookupreg = platform.lookupreg
105 makedir = platform.makedir
105 makedir = platform.makedir
106 nlinks = platform.nlinks
106 nlinks = platform.nlinks
107 normpath = platform.normpath
107 normpath = platform.normpath
108 normcase = platform.normcase
108 normcase = platform.normcase
109 normcasespec = platform.normcasespec
109 normcasespec = platform.normcasespec
110 normcasefallback = platform.normcasefallback
110 normcasefallback = platform.normcasefallback
111 openhardlinks = platform.openhardlinks
111 openhardlinks = platform.openhardlinks
112 oslink = platform.oslink
112 oslink = platform.oslink
113 parsepatchoutput = platform.parsepatchoutput
113 parsepatchoutput = platform.parsepatchoutput
114 pconvert = platform.pconvert
114 pconvert = platform.pconvert
115 poll = platform.poll
115 poll = platform.poll
116 popen = platform.popen
116 popen = platform.popen
117 posixfile = platform.posixfile
117 posixfile = platform.posixfile
118 quotecommand = platform.quotecommand
118 quotecommand = platform.quotecommand
119 readpipe = platform.readpipe
119 readpipe = platform.readpipe
120 rename = platform.rename
120 rename = platform.rename
121 removedirs = platform.removedirs
121 removedirs = platform.removedirs
122 samedevice = platform.samedevice
122 samedevice = platform.samedevice
123 samefile = platform.samefile
123 samefile = platform.samefile
124 samestat = platform.samestat
124 samestat = platform.samestat
125 setbinary = platform.setbinary
125 setbinary = platform.setbinary
126 setflags = platform.setflags
126 setflags = platform.setflags
127 setsignalhandler = platform.setsignalhandler
127 setsignalhandler = platform.setsignalhandler
128 shellquote = platform.shellquote
128 shellquote = platform.shellquote
129 spawndetached = platform.spawndetached
129 spawndetached = platform.spawndetached
130 split = platform.split
130 split = platform.split
131 sshargs = platform.sshargs
131 sshargs = platform.sshargs
132 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
132 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
133 statisexec = platform.statisexec
133 statisexec = platform.statisexec
134 statislink = platform.statislink
134 statislink = platform.statislink
135 testpid = platform.testpid
135 testpid = platform.testpid
136 umask = platform.umask
136 umask = platform.umask
137 unlink = platform.unlink
137 unlink = platform.unlink
138 unlinkpath = platform.unlinkpath
138 unlinkpath = platform.unlinkpath
139 username = platform.username
139 username = platform.username
140
140
141 # Python compatibility
141 # Python compatibility
142
142
143 _notset = object()
143 _notset = object()
144
144
145 # disable Python's problematic floating point timestamps (issue4836)
145 # disable Python's problematic floating point timestamps (issue4836)
146 # (Python hypocritically says you shouldn't change this behavior in
146 # (Python hypocritically says you shouldn't change this behavior in
147 # libraries, and sure enough Mercurial is not a library.)
147 # libraries, and sure enough Mercurial is not a library.)
148 os.stat_float_times(False)
148 os.stat_float_times(False)
149
149
150 def safehasattr(thing, attr):
150 def safehasattr(thing, attr):
151 return getattr(thing, attr, _notset) is not _notset
151 return getattr(thing, attr, _notset) is not _notset
152
152
153 def bitsfrom(container):
153 def bitsfrom(container):
154 bits = 0
154 bits = 0
155 for bit in container:
155 for bit in container:
156 bits |= bit
156 bits |= bit
157 return bits
157 return bits
158
158
159 DIGESTS = {
159 DIGESTS = {
160 'md5': hashlib.md5,
160 'md5': hashlib.md5,
161 'sha1': hashlib.sha1,
161 'sha1': hashlib.sha1,
162 'sha512': hashlib.sha512,
162 'sha512': hashlib.sha512,
163 }
163 }
164 # List of digest types from strongest to weakest
164 # List of digest types from strongest to weakest
165 DIGESTS_BY_STRENGTH = ['sha512', 'sha1', 'md5']
165 DIGESTS_BY_STRENGTH = ['sha512', 'sha1', 'md5']
166
166
167 for k in DIGESTS_BY_STRENGTH:
167 for k in DIGESTS_BY_STRENGTH:
168 assert k in DIGESTS
168 assert k in DIGESTS
169
169
170 class digester(object):
170 class digester(object):
171 """helper to compute digests.
171 """helper to compute digests.
172
172
173 This helper can be used to compute one or more digests given their name.
173 This helper can be used to compute one or more digests given their name.
174
174
175 >>> d = digester(['md5', 'sha1'])
175 >>> d = digester(['md5', 'sha1'])
176 >>> d.update('foo')
176 >>> d.update('foo')
177 >>> [k for k in sorted(d)]
177 >>> [k for k in sorted(d)]
178 ['md5', 'sha1']
178 ['md5', 'sha1']
179 >>> d['md5']
179 >>> d['md5']
180 'acbd18db4cc2f85cedef654fccc4a4d8'
180 'acbd18db4cc2f85cedef654fccc4a4d8'
181 >>> d['sha1']
181 >>> d['sha1']
182 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
182 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
183 >>> digester.preferred(['md5', 'sha1'])
183 >>> digester.preferred(['md5', 'sha1'])
184 'sha1'
184 'sha1'
185 """
185 """
186
186
187 def __init__(self, digests, s=''):
187 def __init__(self, digests, s=''):
188 self._hashes = {}
188 self._hashes = {}
189 for k in digests:
189 for k in digests:
190 if k not in DIGESTS:
190 if k not in DIGESTS:
191 raise Abort(_('unknown digest type: %s') % k)
191 raise Abort(_('unknown digest type: %s') % k)
192 self._hashes[k] = DIGESTS[k]()
192 self._hashes[k] = DIGESTS[k]()
193 if s:
193 if s:
194 self.update(s)
194 self.update(s)
195
195
196 def update(self, data):
196 def update(self, data):
197 for h in self._hashes.values():
197 for h in self._hashes.values():
198 h.update(data)
198 h.update(data)
199
199
200 def __getitem__(self, key):
200 def __getitem__(self, key):
201 if key not in DIGESTS:
201 if key not in DIGESTS:
202 raise Abort(_('unknown digest type: %s') % k)
202 raise Abort(_('unknown digest type: %s') % k)
203 return self._hashes[key].hexdigest()
203 return self._hashes[key].hexdigest()
204
204
205 def __iter__(self):
205 def __iter__(self):
206 return iter(self._hashes)
206 return iter(self._hashes)
207
207
208 @staticmethod
208 @staticmethod
209 def preferred(supported):
209 def preferred(supported):
210 """returns the strongest digest type in both supported and DIGESTS."""
210 """returns the strongest digest type in both supported and DIGESTS."""
211
211
212 for k in DIGESTS_BY_STRENGTH:
212 for k in DIGESTS_BY_STRENGTH:
213 if k in supported:
213 if k in supported:
214 return k
214 return k
215 return None
215 return None
216
216
217 class digestchecker(object):
217 class digestchecker(object):
218 """file handle wrapper that additionally checks content against a given
218 """file handle wrapper that additionally checks content against a given
219 size and digests.
219 size and digests.
220
220
221 d = digestchecker(fh, size, {'md5': '...'})
221 d = digestchecker(fh, size, {'md5': '...'})
222
222
223 When multiple digests are given, all of them are validated.
223 When multiple digests are given, all of them are validated.
224 """
224 """
225
225
226 def __init__(self, fh, size, digests):
226 def __init__(self, fh, size, digests):
227 self._fh = fh
227 self._fh = fh
228 self._size = size
228 self._size = size
229 self._got = 0
229 self._got = 0
230 self._digests = dict(digests)
230 self._digests = dict(digests)
231 self._digester = digester(self._digests.keys())
231 self._digester = digester(self._digests.keys())
232
232
233 def read(self, length=-1):
233 def read(self, length=-1):
234 content = self._fh.read(length)
234 content = self._fh.read(length)
235 self._digester.update(content)
235 self._digester.update(content)
236 self._got += len(content)
236 self._got += len(content)
237 return content
237 return content
238
238
239 def validate(self):
239 def validate(self):
240 if self._size != self._got:
240 if self._size != self._got:
241 raise Abort(_('size mismatch: expected %d, got %d') %
241 raise Abort(_('size mismatch: expected %d, got %d') %
242 (self._size, self._got))
242 (self._size, self._got))
243 for k, v in self._digests.items():
243 for k, v in self._digests.items():
244 if v != self._digester[k]:
244 if v != self._digester[k]:
245 # i18n: first parameter is a digest name
245 # i18n: first parameter is a digest name
246 raise Abort(_('%s mismatch: expected %s, got %s') %
246 raise Abort(_('%s mismatch: expected %s, got %s') %
247 (k, v, self._digester[k]))
247 (k, v, self._digester[k]))
248
248
249 try:
249 try:
250 buffer = buffer
250 buffer = buffer
251 except NameError:
251 except NameError:
252 if not pycompat.ispy3:
252 if not pycompat.ispy3:
253 def buffer(sliceable, offset=0, length=None):
253 def buffer(sliceable, offset=0, length=None):
254 if length is not None:
254 if length is not None:
255 return sliceable[offset:offset + length]
255 return sliceable[offset:offset + length]
256 return sliceable[offset:]
256 return sliceable[offset:]
257 else:
257 else:
258 def buffer(sliceable, offset=0, length=None):
258 def buffer(sliceable, offset=0, length=None):
259 if length is not None:
259 if length is not None:
260 return memoryview(sliceable)[offset:offset + length]
260 return memoryview(sliceable)[offset:offset + length]
261 return memoryview(sliceable)[offset:]
261 return memoryview(sliceable)[offset:]
262
262
263 closefds = pycompat.osname == 'posix'
263 closefds = pycompat.osname == 'posix'
264
264
265 _chunksize = 4096
265 _chunksize = 4096
266
266
267 class bufferedinputpipe(object):
267 class bufferedinputpipe(object):
268 """a manually buffered input pipe
268 """a manually buffered input pipe
269
269
270 Python will not let us use buffered IO and lazy reading with 'polling' at
270 Python will not let us use buffered IO and lazy reading with 'polling' at
271 the same time. We cannot probe the buffer state and select will not detect
271 the same time. We cannot probe the buffer state and select will not detect
272 that data are ready to read if they are already buffered.
272 that data are ready to read if they are already buffered.
273
273
274 This class let us work around that by implementing its own buffering
274 This class let us work around that by implementing its own buffering
275 (allowing efficient readline) while offering a way to know if the buffer is
275 (allowing efficient readline) while offering a way to know if the buffer is
276 empty from the output (allowing collaboration of the buffer with polling).
276 empty from the output (allowing collaboration of the buffer with polling).
277
277
278 This class lives in the 'util' module because it makes use of the 'os'
278 This class lives in the 'util' module because it makes use of the 'os'
279 module from the python stdlib.
279 module from the python stdlib.
280 """
280 """
281
281
282 def __init__(self, input):
282 def __init__(self, input):
283 self._input = input
283 self._input = input
284 self._buffer = []
284 self._buffer = []
285 self._eof = False
285 self._eof = False
286 self._lenbuf = 0
286 self._lenbuf = 0
287
287
288 @property
288 @property
289 def hasbuffer(self):
289 def hasbuffer(self):
290 """True is any data is currently buffered
290 """True is any data is currently buffered
291
291
292 This will be used externally a pre-step for polling IO. If there is
292 This will be used externally a pre-step for polling IO. If there is
293 already data then no polling should be set in place."""
293 already data then no polling should be set in place."""
294 return bool(self._buffer)
294 return bool(self._buffer)
295
295
296 @property
296 @property
297 def closed(self):
297 def closed(self):
298 return self._input.closed
298 return self._input.closed
299
299
300 def fileno(self):
300 def fileno(self):
301 return self._input.fileno()
301 return self._input.fileno()
302
302
303 def close(self):
303 def close(self):
304 return self._input.close()
304 return self._input.close()
305
305
306 def read(self, size):
306 def read(self, size):
307 while (not self._eof) and (self._lenbuf < size):
307 while (not self._eof) and (self._lenbuf < size):
308 self._fillbuffer()
308 self._fillbuffer()
309 return self._frombuffer(size)
309 return self._frombuffer(size)
310
310
311 def readline(self, *args, **kwargs):
311 def readline(self, *args, **kwargs):
312 if 1 < len(self._buffer):
312 if 1 < len(self._buffer):
313 # this should not happen because both read and readline end with a
313 # this should not happen because both read and readline end with a
314 # _frombuffer call that collapse it.
314 # _frombuffer call that collapse it.
315 self._buffer = [''.join(self._buffer)]
315 self._buffer = [''.join(self._buffer)]
316 self._lenbuf = len(self._buffer[0])
316 self._lenbuf = len(self._buffer[0])
317 lfi = -1
317 lfi = -1
318 if self._buffer:
318 if self._buffer:
319 lfi = self._buffer[-1].find('\n')
319 lfi = self._buffer[-1].find('\n')
320 while (not self._eof) and lfi < 0:
320 while (not self._eof) and lfi < 0:
321 self._fillbuffer()
321 self._fillbuffer()
322 if self._buffer:
322 if self._buffer:
323 lfi = self._buffer[-1].find('\n')
323 lfi = self._buffer[-1].find('\n')
324 size = lfi + 1
324 size = lfi + 1
325 if lfi < 0: # end of file
325 if lfi < 0: # end of file
326 size = self._lenbuf
326 size = self._lenbuf
327 elif 1 < len(self._buffer):
327 elif 1 < len(self._buffer):
328 # we need to take previous chunks into account
328 # we need to take previous chunks into account
329 size += self._lenbuf - len(self._buffer[-1])
329 size += self._lenbuf - len(self._buffer[-1])
330 return self._frombuffer(size)
330 return self._frombuffer(size)
331
331
332 def _frombuffer(self, size):
332 def _frombuffer(self, size):
333 """return at most 'size' data from the buffer
333 """return at most 'size' data from the buffer
334
334
335 The data are removed from the buffer."""
335 The data are removed from the buffer."""
336 if size == 0 or not self._buffer:
336 if size == 0 or not self._buffer:
337 return ''
337 return ''
338 buf = self._buffer[0]
338 buf = self._buffer[0]
339 if 1 < len(self._buffer):
339 if 1 < len(self._buffer):
340 buf = ''.join(self._buffer)
340 buf = ''.join(self._buffer)
341
341
342 data = buf[:size]
342 data = buf[:size]
343 buf = buf[len(data):]
343 buf = buf[len(data):]
344 if buf:
344 if buf:
345 self._buffer = [buf]
345 self._buffer = [buf]
346 self._lenbuf = len(buf)
346 self._lenbuf = len(buf)
347 else:
347 else:
348 self._buffer = []
348 self._buffer = []
349 self._lenbuf = 0
349 self._lenbuf = 0
350 return data
350 return data
351
351
352 def _fillbuffer(self):
352 def _fillbuffer(self):
353 """read data to the buffer"""
353 """read data to the buffer"""
354 data = os.read(self._input.fileno(), _chunksize)
354 data = os.read(self._input.fileno(), _chunksize)
355 if not data:
355 if not data:
356 self._eof = True
356 self._eof = True
357 else:
357 else:
358 self._lenbuf += len(data)
358 self._lenbuf += len(data)
359 self._buffer.append(data)
359 self._buffer.append(data)
360
360
361 def popen2(cmd, env=None, newlines=False):
361 def popen2(cmd, env=None, newlines=False):
362 # Setting bufsize to -1 lets the system decide the buffer size.
362 # Setting bufsize to -1 lets the system decide the buffer size.
363 # The default for bufsize is 0, meaning unbuffered. This leads to
363 # The default for bufsize is 0, meaning unbuffered. This leads to
364 # poor performance on Mac OS X: http://bugs.python.org/issue4194
364 # poor performance on Mac OS X: http://bugs.python.org/issue4194
365 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
365 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
366 close_fds=closefds,
366 close_fds=closefds,
367 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
367 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
368 universal_newlines=newlines,
368 universal_newlines=newlines,
369 env=env)
369 env=env)
370 return p.stdin, p.stdout
370 return p.stdin, p.stdout
371
371
372 def popen3(cmd, env=None, newlines=False):
372 def popen3(cmd, env=None, newlines=False):
373 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
373 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
374 return stdin, stdout, stderr
374 return stdin, stdout, stderr
375
375
376 def popen4(cmd, env=None, newlines=False, bufsize=-1):
376 def popen4(cmd, env=None, newlines=False, bufsize=-1):
377 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
377 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
378 close_fds=closefds,
378 close_fds=closefds,
379 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
379 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
380 stderr=subprocess.PIPE,
380 stderr=subprocess.PIPE,
381 universal_newlines=newlines,
381 universal_newlines=newlines,
382 env=env)
382 env=env)
383 return p.stdin, p.stdout, p.stderr, p
383 return p.stdin, p.stdout, p.stderr, p
384
384
385 def version():
385 def version():
386 """Return version information if available."""
386 """Return version information if available."""
387 try:
387 try:
388 from . import __version__
388 from . import __version__
389 return __version__.version
389 return __version__.version
390 except ImportError:
390 except ImportError:
391 return 'unknown'
391 return 'unknown'
392
392
393 def versiontuple(v=None, n=4):
393 def versiontuple(v=None, n=4):
394 """Parses a Mercurial version string into an N-tuple.
394 """Parses a Mercurial version string into an N-tuple.
395
395
396 The version string to be parsed is specified with the ``v`` argument.
396 The version string to be parsed is specified with the ``v`` argument.
397 If it isn't defined, the current Mercurial version string will be parsed.
397 If it isn't defined, the current Mercurial version string will be parsed.
398
398
399 ``n`` can be 2, 3, or 4. Here is how some version strings map to
399 ``n`` can be 2, 3, or 4. Here is how some version strings map to
400 returned values:
400 returned values:
401
401
402 >>> v = '3.6.1+190-df9b73d2d444'
402 >>> v = '3.6.1+190-df9b73d2d444'
403 >>> versiontuple(v, 2)
403 >>> versiontuple(v, 2)
404 (3, 6)
404 (3, 6)
405 >>> versiontuple(v, 3)
405 >>> versiontuple(v, 3)
406 (3, 6, 1)
406 (3, 6, 1)
407 >>> versiontuple(v, 4)
407 >>> versiontuple(v, 4)
408 (3, 6, 1, '190-df9b73d2d444')
408 (3, 6, 1, '190-df9b73d2d444')
409
409
410 >>> versiontuple('3.6.1+190-df9b73d2d444+20151118')
410 >>> versiontuple('3.6.1+190-df9b73d2d444+20151118')
411 (3, 6, 1, '190-df9b73d2d444+20151118')
411 (3, 6, 1, '190-df9b73d2d444+20151118')
412
412
413 >>> v = '3.6'
413 >>> v = '3.6'
414 >>> versiontuple(v, 2)
414 >>> versiontuple(v, 2)
415 (3, 6)
415 (3, 6)
416 >>> versiontuple(v, 3)
416 >>> versiontuple(v, 3)
417 (3, 6, None)
417 (3, 6, None)
418 >>> versiontuple(v, 4)
418 >>> versiontuple(v, 4)
419 (3, 6, None, None)
419 (3, 6, None, None)
420
420
421 >>> v = '3.9-rc'
421 >>> v = '3.9-rc'
422 >>> versiontuple(v, 2)
422 >>> versiontuple(v, 2)
423 (3, 9)
423 (3, 9)
424 >>> versiontuple(v, 3)
424 >>> versiontuple(v, 3)
425 (3, 9, None)
425 (3, 9, None)
426 >>> versiontuple(v, 4)
426 >>> versiontuple(v, 4)
427 (3, 9, None, 'rc')
427 (3, 9, None, 'rc')
428
428
429 >>> v = '3.9-rc+2-02a8fea4289b'
429 >>> v = '3.9-rc+2-02a8fea4289b'
430 >>> versiontuple(v, 2)
430 >>> versiontuple(v, 2)
431 (3, 9)
431 (3, 9)
432 >>> versiontuple(v, 3)
432 >>> versiontuple(v, 3)
433 (3, 9, None)
433 (3, 9, None)
434 >>> versiontuple(v, 4)
434 >>> versiontuple(v, 4)
435 (3, 9, None, 'rc+2-02a8fea4289b')
435 (3, 9, None, 'rc+2-02a8fea4289b')
436 """
436 """
437 if not v:
437 if not v:
438 v = version()
438 v = version()
439 parts = remod.split('[\+-]', v, 1)
439 parts = remod.split('[\+-]', v, 1)
440 if len(parts) == 1:
440 if len(parts) == 1:
441 vparts, extra = parts[0], None
441 vparts, extra = parts[0], None
442 else:
442 else:
443 vparts, extra = parts
443 vparts, extra = parts
444
444
445 vints = []
445 vints = []
446 for i in vparts.split('.'):
446 for i in vparts.split('.'):
447 try:
447 try:
448 vints.append(int(i))
448 vints.append(int(i))
449 except ValueError:
449 except ValueError:
450 break
450 break
451 # (3, 6) -> (3, 6, None)
451 # (3, 6) -> (3, 6, None)
452 while len(vints) < 3:
452 while len(vints) < 3:
453 vints.append(None)
453 vints.append(None)
454
454
455 if n == 2:
455 if n == 2:
456 return (vints[0], vints[1])
456 return (vints[0], vints[1])
457 if n == 3:
457 if n == 3:
458 return (vints[0], vints[1], vints[2])
458 return (vints[0], vints[1], vints[2])
459 if n == 4:
459 if n == 4:
460 return (vints[0], vints[1], vints[2], extra)
460 return (vints[0], vints[1], vints[2], extra)
461
461
462 # used by parsedate
462 # used by parsedate
463 defaultdateformats = (
463 defaultdateformats = (
464 '%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
464 '%Y-%m-%dT%H:%M:%S', # the 'real' ISO8601
465 '%Y-%m-%dT%H:%M', # without seconds
465 '%Y-%m-%dT%H:%M', # without seconds
466 '%Y-%m-%dT%H%M%S', # another awful but legal variant without :
466 '%Y-%m-%dT%H%M%S', # another awful but legal variant without :
467 '%Y-%m-%dT%H%M', # without seconds
467 '%Y-%m-%dT%H%M', # without seconds
468 '%Y-%m-%d %H:%M:%S', # our common legal variant
468 '%Y-%m-%d %H:%M:%S', # our common legal variant
469 '%Y-%m-%d %H:%M', # without seconds
469 '%Y-%m-%d %H:%M', # without seconds
470 '%Y-%m-%d %H%M%S', # without :
470 '%Y-%m-%d %H%M%S', # without :
471 '%Y-%m-%d %H%M', # without seconds
471 '%Y-%m-%d %H%M', # without seconds
472 '%Y-%m-%d %I:%M:%S%p',
472 '%Y-%m-%d %I:%M:%S%p',
473 '%Y-%m-%d %H:%M',
473 '%Y-%m-%d %H:%M',
474 '%Y-%m-%d %I:%M%p',
474 '%Y-%m-%d %I:%M%p',
475 '%Y-%m-%d',
475 '%Y-%m-%d',
476 '%m-%d',
476 '%m-%d',
477 '%m/%d',
477 '%m/%d',
478 '%m/%d/%y',
478 '%m/%d/%y',
479 '%m/%d/%Y',
479 '%m/%d/%Y',
480 '%a %b %d %H:%M:%S %Y',
480 '%a %b %d %H:%M:%S %Y',
481 '%a %b %d %I:%M:%S%p %Y',
481 '%a %b %d %I:%M:%S%p %Y',
482 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
482 '%a, %d %b %Y %H:%M:%S', # GNU coreutils "/bin/date --rfc-2822"
483 '%b %d %H:%M:%S %Y',
483 '%b %d %H:%M:%S %Y',
484 '%b %d %I:%M:%S%p %Y',
484 '%b %d %I:%M:%S%p %Y',
485 '%b %d %H:%M:%S',
485 '%b %d %H:%M:%S',
486 '%b %d %I:%M:%S%p',
486 '%b %d %I:%M:%S%p',
487 '%b %d %H:%M',
487 '%b %d %H:%M',
488 '%b %d %I:%M%p',
488 '%b %d %I:%M%p',
489 '%b %d %Y',
489 '%b %d %Y',
490 '%b %d',
490 '%b %d',
491 '%H:%M:%S',
491 '%H:%M:%S',
492 '%I:%M:%S%p',
492 '%I:%M:%S%p',
493 '%H:%M',
493 '%H:%M',
494 '%I:%M%p',
494 '%I:%M%p',
495 )
495 )
496
496
497 extendeddateformats = defaultdateformats + (
497 extendeddateformats = defaultdateformats + (
498 "%Y",
498 "%Y",
499 "%Y-%m",
499 "%Y-%m",
500 "%b",
500 "%b",
501 "%b %Y",
501 "%b %Y",
502 )
502 )
503
503
504 def cachefunc(func):
504 def cachefunc(func):
505 '''cache the result of function calls'''
505 '''cache the result of function calls'''
506 # XXX doesn't handle keywords args
506 # XXX doesn't handle keywords args
507 if func.__code__.co_argcount == 0:
507 if func.__code__.co_argcount == 0:
508 cache = []
508 cache = []
509 def f():
509 def f():
510 if len(cache) == 0:
510 if len(cache) == 0:
511 cache.append(func())
511 cache.append(func())
512 return cache[0]
512 return cache[0]
513 return f
513 return f
514 cache = {}
514 cache = {}
515 if func.__code__.co_argcount == 1:
515 if func.__code__.co_argcount == 1:
516 # we gain a small amount of time because
516 # we gain a small amount of time because
517 # we don't need to pack/unpack the list
517 # we don't need to pack/unpack the list
518 def f(arg):
518 def f(arg):
519 if arg not in cache:
519 if arg not in cache:
520 cache[arg] = func(arg)
520 cache[arg] = func(arg)
521 return cache[arg]
521 return cache[arg]
522 else:
522 else:
523 def f(*args):
523 def f(*args):
524 if args not in cache:
524 if args not in cache:
525 cache[args] = func(*args)
525 cache[args] = func(*args)
526 return cache[args]
526 return cache[args]
527
527
528 return f
528 return f
529
529
530 class sortdict(dict):
530 class sortdict(dict):
531 '''a simple sorted dictionary'''
531 '''a simple sorted dictionary'''
532 def __init__(self, data=None):
532 def __init__(self, data=None):
533 self._list = []
533 self._list = []
534 if data:
534 if data:
535 self.update(data)
535 self.update(data)
536 def copy(self):
536 def copy(self):
537 return sortdict(self)
537 return sortdict(self)
538 def __setitem__(self, key, val):
538 def __setitem__(self, key, val):
539 if key in self:
539 if key in self:
540 self._list.remove(key)
540 self._list.remove(key)
541 self._list.append(key)
541 self._list.append(key)
542 dict.__setitem__(self, key, val)
542 dict.__setitem__(self, key, val)
543 def __iter__(self):
543 def __iter__(self):
544 return self._list.__iter__()
544 return self._list.__iter__()
545 def update(self, src):
545 def update(self, src):
546 if isinstance(src, dict):
546 if isinstance(src, dict):
547 src = src.iteritems()
547 src = src.iteritems()
548 for k, v in src:
548 for k, v in src:
549 self[k] = v
549 self[k] = v
550 def clear(self):
550 def clear(self):
551 dict.clear(self)
551 dict.clear(self)
552 self._list = []
552 self._list = []
553 def items(self):
553 def items(self):
554 return [(k, self[k]) for k in self._list]
554 return [(k, self[k]) for k in self._list]
555 def __delitem__(self, key):
555 def __delitem__(self, key):
556 dict.__delitem__(self, key)
556 dict.__delitem__(self, key)
557 self._list.remove(key)
557 self._list.remove(key)
558 def pop(self, key, *args, **kwargs):
558 def pop(self, key, *args, **kwargs):
559 dict.pop(self, key, *args, **kwargs)
559 dict.pop(self, key, *args, **kwargs)
560 try:
560 try:
561 self._list.remove(key)
561 self._list.remove(key)
562 except ValueError:
562 except ValueError:
563 pass
563 pass
564 def keys(self):
564 def keys(self):
565 return self._list[:]
565 return self._list[:]
566 def iterkeys(self):
566 def iterkeys(self):
567 return self._list.__iter__()
567 return self._list.__iter__()
568 def iteritems(self):
568 def iteritems(self):
569 for k in self._list:
569 for k in self._list:
570 yield k, self[k]
570 yield k, self[k]
571 def insert(self, index, key, val):
571 def insert(self, index, key, val):
572 self._list.insert(index, key)
572 self._list.insert(index, key)
573 dict.__setitem__(self, key, val)
573 dict.__setitem__(self, key, val)
574 def __repr__(self):
574 def __repr__(self):
575 if not self:
575 if not self:
576 return '%s()' % self.__class__.__name__
576 return '%s()' % self.__class__.__name__
577 return '%s(%r)' % (self.__class__.__name__, self.items())
577 return '%s(%r)' % (self.__class__.__name__, self.items())
578
578
579 class _lrucachenode(object):
579 class _lrucachenode(object):
580 """A node in a doubly linked list.
580 """A node in a doubly linked list.
581
581
582 Holds a reference to nodes on either side as well as a key-value
582 Holds a reference to nodes on either side as well as a key-value
583 pair for the dictionary entry.
583 pair for the dictionary entry.
584 """
584 """
585 __slots__ = (u'next', u'prev', u'key', u'value')
585 __slots__ = (u'next', u'prev', u'key', u'value')
586
586
587 def __init__(self):
587 def __init__(self):
588 self.next = None
588 self.next = None
589 self.prev = None
589 self.prev = None
590
590
591 self.key = _notset
591 self.key = _notset
592 self.value = None
592 self.value = None
593
593
594 def markempty(self):
594 def markempty(self):
595 """Mark the node as emptied."""
595 """Mark the node as emptied."""
596 self.key = _notset
596 self.key = _notset
597
597
598 class lrucachedict(object):
598 class lrucachedict(object):
599 """Dict that caches most recent accesses and sets.
599 """Dict that caches most recent accesses and sets.
600
600
601 The dict consists of an actual backing dict - indexed by original
601 The dict consists of an actual backing dict - indexed by original
602 key - and a doubly linked circular list defining the order of entries in
602 key - and a doubly linked circular list defining the order of entries in
603 the cache.
603 the cache.
604
604
605 The head node is the newest entry in the cache. If the cache is full,
605 The head node is the newest entry in the cache. If the cache is full,
606 we recycle head.prev and make it the new head. Cache accesses result in
606 we recycle head.prev and make it the new head. Cache accesses result in
607 the node being moved to before the existing head and being marked as the
607 the node being moved to before the existing head and being marked as the
608 new head node.
608 new head node.
609 """
609 """
610 def __init__(self, max):
610 def __init__(self, max):
611 self._cache = {}
611 self._cache = {}
612
612
613 self._head = head = _lrucachenode()
613 self._head = head = _lrucachenode()
614 head.prev = head
614 head.prev = head
615 head.next = head
615 head.next = head
616 self._size = 1
616 self._size = 1
617 self._capacity = max
617 self._capacity = max
618
618
619 def __len__(self):
619 def __len__(self):
620 return len(self._cache)
620 return len(self._cache)
621
621
622 def __contains__(self, k):
622 def __contains__(self, k):
623 return k in self._cache
623 return k in self._cache
624
624
625 def __iter__(self):
625 def __iter__(self):
626 # We don't have to iterate in cache order, but why not.
626 # We don't have to iterate in cache order, but why not.
627 n = self._head
627 n = self._head
628 for i in range(len(self._cache)):
628 for i in range(len(self._cache)):
629 yield n.key
629 yield n.key
630 n = n.next
630 n = n.next
631
631
632 def __getitem__(self, k):
632 def __getitem__(self, k):
633 node = self._cache[k]
633 node = self._cache[k]
634 self._movetohead(node)
634 self._movetohead(node)
635 return node.value
635 return node.value
636
636
637 def __setitem__(self, k, v):
637 def __setitem__(self, k, v):
638 node = self._cache.get(k)
638 node = self._cache.get(k)
639 # Replace existing value and mark as newest.
639 # Replace existing value and mark as newest.
640 if node is not None:
640 if node is not None:
641 node.value = v
641 node.value = v
642 self._movetohead(node)
642 self._movetohead(node)
643 return
643 return
644
644
645 if self._size < self._capacity:
645 if self._size < self._capacity:
646 node = self._addcapacity()
646 node = self._addcapacity()
647 else:
647 else:
648 # Grab the last/oldest item.
648 # Grab the last/oldest item.
649 node = self._head.prev
649 node = self._head.prev
650
650
651 # At capacity. Kill the old entry.
651 # At capacity. Kill the old entry.
652 if node.key is not _notset:
652 if node.key is not _notset:
653 del self._cache[node.key]
653 del self._cache[node.key]
654
654
655 node.key = k
655 node.key = k
656 node.value = v
656 node.value = v
657 self._cache[k] = node
657 self._cache[k] = node
658 # And mark it as newest entry. No need to adjust order since it
658 # And mark it as newest entry. No need to adjust order since it
659 # is already self._head.prev.
659 # is already self._head.prev.
660 self._head = node
660 self._head = node
661
661
662 def __delitem__(self, k):
662 def __delitem__(self, k):
663 node = self._cache.pop(k)
663 node = self._cache.pop(k)
664 node.markempty()
664 node.markempty()
665
665
666 # Temporarily mark as newest item before re-adjusting head to make
666 # Temporarily mark as newest item before re-adjusting head to make
667 # this node the oldest item.
667 # this node the oldest item.
668 self._movetohead(node)
668 self._movetohead(node)
669 self._head = node.next
669 self._head = node.next
670
670
671 # Additional dict methods.
671 # Additional dict methods.
672
672
673 def get(self, k, default=None):
673 def get(self, k, default=None):
674 try:
674 try:
675 return self._cache[k].value
675 return self._cache[k].value
676 except KeyError:
676 except KeyError:
677 return default
677 return default
678
678
679 def clear(self):
679 def clear(self):
680 n = self._head
680 n = self._head
681 while n.key is not _notset:
681 while n.key is not _notset:
682 n.markempty()
682 n.markempty()
683 n = n.next
683 n = n.next
684
684
685 self._cache.clear()
685 self._cache.clear()
686
686
687 def copy(self):
687 def copy(self):
688 result = lrucachedict(self._capacity)
688 result = lrucachedict(self._capacity)
689 n = self._head.prev
689 n = self._head.prev
690 # Iterate in oldest-to-newest order, so the copy has the right ordering
690 # Iterate in oldest-to-newest order, so the copy has the right ordering
691 for i in range(len(self._cache)):
691 for i in range(len(self._cache)):
692 result[n.key] = n.value
692 result[n.key] = n.value
693 n = n.prev
693 n = n.prev
694 return result
694 return result
695
695
696 def _movetohead(self, node):
696 def _movetohead(self, node):
697 """Mark a node as the newest, making it the new head.
697 """Mark a node as the newest, making it the new head.
698
698
699 When a node is accessed, it becomes the freshest entry in the LRU
699 When a node is accessed, it becomes the freshest entry in the LRU
700 list, which is denoted by self._head.
700 list, which is denoted by self._head.
701
701
702 Visually, let's make ``N`` the new head node (* denotes head):
702 Visually, let's make ``N`` the new head node (* denotes head):
703
703
704 previous/oldest <-> head <-> next/next newest
704 previous/oldest <-> head <-> next/next newest
705
705
706 ----<->--- A* ---<->-----
706 ----<->--- A* ---<->-----
707 | |
707 | |
708 E <-> D <-> N <-> C <-> B
708 E <-> D <-> N <-> C <-> B
709
709
710 To:
710 To:
711
711
712 ----<->--- N* ---<->-----
712 ----<->--- N* ---<->-----
713 | |
713 | |
714 E <-> D <-> C <-> B <-> A
714 E <-> D <-> C <-> B <-> A
715
715
716 This requires the following moves:
716 This requires the following moves:
717
717
718 C.next = D (node.prev.next = node.next)
718 C.next = D (node.prev.next = node.next)
719 D.prev = C (node.next.prev = node.prev)
719 D.prev = C (node.next.prev = node.prev)
720 E.next = N (head.prev.next = node)
720 E.next = N (head.prev.next = node)
721 N.prev = E (node.prev = head.prev)
721 N.prev = E (node.prev = head.prev)
722 N.next = A (node.next = head)
722 N.next = A (node.next = head)
723 A.prev = N (head.prev = node)
723 A.prev = N (head.prev = node)
724 """
724 """
725 head = self._head
725 head = self._head
726 # C.next = D
726 # C.next = D
727 node.prev.next = node.next
727 node.prev.next = node.next
728 # D.prev = C
728 # D.prev = C
729 node.next.prev = node.prev
729 node.next.prev = node.prev
730 # N.prev = E
730 # N.prev = E
731 node.prev = head.prev
731 node.prev = head.prev
732 # N.next = A
732 # N.next = A
733 # It is tempting to do just "head" here, however if node is
733 # It is tempting to do just "head" here, however if node is
734 # adjacent to head, this will do bad things.
734 # adjacent to head, this will do bad things.
735 node.next = head.prev.next
735 node.next = head.prev.next
736 # E.next = N
736 # E.next = N
737 node.next.prev = node
737 node.next.prev = node
738 # A.prev = N
738 # A.prev = N
739 node.prev.next = node
739 node.prev.next = node
740
740
741 self._head = node
741 self._head = node
742
742
743 def _addcapacity(self):
743 def _addcapacity(self):
744 """Add a node to the circular linked list.
744 """Add a node to the circular linked list.
745
745
746 The new node is inserted before the head node.
746 The new node is inserted before the head node.
747 """
747 """
748 head = self._head
748 head = self._head
749 node = _lrucachenode()
749 node = _lrucachenode()
750 head.prev.next = node
750 head.prev.next = node
751 node.prev = head.prev
751 node.prev = head.prev
752 node.next = head
752 node.next = head
753 head.prev = node
753 head.prev = node
754 self._size += 1
754 self._size += 1
755 return node
755 return node
756
756
757 def lrucachefunc(func):
757 def lrucachefunc(func):
758 '''cache most recent results of function calls'''
758 '''cache most recent results of function calls'''
759 cache = {}
759 cache = {}
760 order = collections.deque()
760 order = collections.deque()
761 if func.__code__.co_argcount == 1:
761 if func.__code__.co_argcount == 1:
762 def f(arg):
762 def f(arg):
763 if arg not in cache:
763 if arg not in cache:
764 if len(cache) > 20:
764 if len(cache) > 20:
765 del cache[order.popleft()]
765 del cache[order.popleft()]
766 cache[arg] = func(arg)
766 cache[arg] = func(arg)
767 else:
767 else:
768 order.remove(arg)
768 order.remove(arg)
769 order.append(arg)
769 order.append(arg)
770 return cache[arg]
770 return cache[arg]
771 else:
771 else:
772 def f(*args):
772 def f(*args):
773 if args not in cache:
773 if args not in cache:
774 if len(cache) > 20:
774 if len(cache) > 20:
775 del cache[order.popleft()]
775 del cache[order.popleft()]
776 cache[args] = func(*args)
776 cache[args] = func(*args)
777 else:
777 else:
778 order.remove(args)
778 order.remove(args)
779 order.append(args)
779 order.append(args)
780 return cache[args]
780 return cache[args]
781
781
782 return f
782 return f
783
783
784 class propertycache(object):
784 class propertycache(object):
785 def __init__(self, func):
785 def __init__(self, func):
786 self.func = func
786 self.func = func
787 self.name = func.__name__
787 self.name = func.__name__
788 def __get__(self, obj, type=None):
788 def __get__(self, obj, type=None):
789 result = self.func(obj)
789 result = self.func(obj)
790 self.cachevalue(obj, result)
790 self.cachevalue(obj, result)
791 return result
791 return result
792
792
793 def cachevalue(self, obj, value):
793 def cachevalue(self, obj, value):
794 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
794 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
795 obj.__dict__[self.name] = value
795 obj.__dict__[self.name] = value
796
796
797 def pipefilter(s, cmd):
797 def pipefilter(s, cmd):
798 '''filter string S through command CMD, returning its output'''
798 '''filter string S through command CMD, returning its output'''
799 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
799 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
800 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
800 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
801 pout, perr = p.communicate(s)
801 pout, perr = p.communicate(s)
802 return pout
802 return pout
803
803
804 def tempfilter(s, cmd):
804 def tempfilter(s, cmd):
805 '''filter string S through a pair of temporary files with CMD.
805 '''filter string S through a pair of temporary files with CMD.
806 CMD is used as a template to create the real command to be run,
806 CMD is used as a template to create the real command to be run,
807 with the strings INFILE and OUTFILE replaced by the real names of
807 with the strings INFILE and OUTFILE replaced by the real names of
808 the temporary files generated.'''
808 the temporary files generated.'''
809 inname, outname = None, None
809 inname, outname = None, None
810 try:
810 try:
811 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
811 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
812 fp = os.fdopen(infd, pycompat.sysstr('wb'))
812 fp = os.fdopen(infd, pycompat.sysstr('wb'))
813 fp.write(s)
813 fp.write(s)
814 fp.close()
814 fp.close()
815 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
815 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
816 os.close(outfd)
816 os.close(outfd)
817 cmd = cmd.replace('INFILE', inname)
817 cmd = cmd.replace('INFILE', inname)
818 cmd = cmd.replace('OUTFILE', outname)
818 cmd = cmd.replace('OUTFILE', outname)
819 code = os.system(cmd)
819 code = os.system(cmd)
820 if pycompat.sysplatform == 'OpenVMS' and code & 1:
820 if pycompat.sysplatform == 'OpenVMS' and code & 1:
821 code = 0
821 code = 0
822 if code:
822 if code:
823 raise Abort(_("command '%s' failed: %s") %
823 raise Abort(_("command '%s' failed: %s") %
824 (cmd, explainexit(code)))
824 (cmd, explainexit(code)))
825 return readfile(outname)
825 return readfile(outname)
826 finally:
826 finally:
827 try:
827 try:
828 if inname:
828 if inname:
829 os.unlink(inname)
829 os.unlink(inname)
830 except OSError:
830 except OSError:
831 pass
831 pass
832 try:
832 try:
833 if outname:
833 if outname:
834 os.unlink(outname)
834 os.unlink(outname)
835 except OSError:
835 except OSError:
836 pass
836 pass
837
837
838 filtertable = {
838 filtertable = {
839 'tempfile:': tempfilter,
839 'tempfile:': tempfilter,
840 'pipe:': pipefilter,
840 'pipe:': pipefilter,
841 }
841 }
842
842
843 def filter(s, cmd):
843 def filter(s, cmd):
844 "filter a string through a command that transforms its input to its output"
844 "filter a string through a command that transforms its input to its output"
845 for name, fn in filtertable.iteritems():
845 for name, fn in filtertable.iteritems():
846 if cmd.startswith(name):
846 if cmd.startswith(name):
847 return fn(s, cmd[len(name):].lstrip())
847 return fn(s, cmd[len(name):].lstrip())
848 return pipefilter(s, cmd)
848 return pipefilter(s, cmd)
849
849
850 def binary(s):
850 def binary(s):
851 """return true if a string is binary data"""
851 """return true if a string is binary data"""
852 return bool(s and '\0' in s)
852 return bool(s and '\0' in s)
853
853
854 def increasingchunks(source, min=1024, max=65536):
854 def increasingchunks(source, min=1024, max=65536):
855 '''return no less than min bytes per chunk while data remains,
855 '''return no less than min bytes per chunk while data remains,
856 doubling min after each chunk until it reaches max'''
856 doubling min after each chunk until it reaches max'''
857 def log2(x):
857 def log2(x):
858 if not x:
858 if not x:
859 return 0
859 return 0
860 i = 0
860 i = 0
861 while x:
861 while x:
862 x >>= 1
862 x >>= 1
863 i += 1
863 i += 1
864 return i - 1
864 return i - 1
865
865
866 buf = []
866 buf = []
867 blen = 0
867 blen = 0
868 for chunk in source:
868 for chunk in source:
869 buf.append(chunk)
869 buf.append(chunk)
870 blen += len(chunk)
870 blen += len(chunk)
871 if blen >= min:
871 if blen >= min:
872 if min < max:
872 if min < max:
873 min = min << 1
873 min = min << 1
874 nmin = 1 << log2(blen)
874 nmin = 1 << log2(blen)
875 if nmin > min:
875 if nmin > min:
876 min = nmin
876 min = nmin
877 if min > max:
877 if min > max:
878 min = max
878 min = max
879 yield ''.join(buf)
879 yield ''.join(buf)
880 blen = 0
880 blen = 0
881 buf = []
881 buf = []
882 if buf:
882 if buf:
883 yield ''.join(buf)
883 yield ''.join(buf)
884
884
885 Abort = error.Abort
885 Abort = error.Abort
886
886
887 def always(fn):
887 def always(fn):
888 return True
888 return True
889
889
890 def never(fn):
890 def never(fn):
891 return False
891 return False
892
892
893 def nogc(func):
893 def nogc(func):
894 """disable garbage collector
894 """disable garbage collector
895
895
896 Python's garbage collector triggers a GC each time a certain number of
896 Python's garbage collector triggers a GC each time a certain number of
897 container objects (the number being defined by gc.get_threshold()) are
897 container objects (the number being defined by gc.get_threshold()) are
898 allocated even when marked not to be tracked by the collector. Tracking has
898 allocated even when marked not to be tracked by the collector. Tracking has
899 no effect on when GCs are triggered, only on what objects the GC looks
899 no effect on when GCs are triggered, only on what objects the GC looks
900 into. As a workaround, disable GC while building complex (huge)
900 into. As a workaround, disable GC while building complex (huge)
901 containers.
901 containers.
902
902
903 This garbage collector issue have been fixed in 2.7.
903 This garbage collector issue have been fixed in 2.7.
904 """
904 """
905 if sys.version_info >= (2, 7):
905 if sys.version_info >= (2, 7):
906 return func
906 return func
907 def wrapper(*args, **kwargs):
907 def wrapper(*args, **kwargs):
908 gcenabled = gc.isenabled()
908 gcenabled = gc.isenabled()
909 gc.disable()
909 gc.disable()
910 try:
910 try:
911 return func(*args, **kwargs)
911 return func(*args, **kwargs)
912 finally:
912 finally:
913 if gcenabled:
913 if gcenabled:
914 gc.enable()
914 gc.enable()
915 return wrapper
915 return wrapper
916
916
917 def pathto(root, n1, n2):
917 def pathto(root, n1, n2):
918 '''return the relative path from one place to another.
918 '''return the relative path from one place to another.
919 root should use os.sep to separate directories
919 root should use os.sep to separate directories
920 n1 should use os.sep to separate directories
920 n1 should use os.sep to separate directories
921 n2 should use "/" to separate directories
921 n2 should use "/" to separate directories
922 returns an os.sep-separated path.
922 returns an os.sep-separated path.
923
923
924 If n1 is a relative path, it's assumed it's
924 If n1 is a relative path, it's assumed it's
925 relative to root.
925 relative to root.
926 n2 should always be relative to root.
926 n2 should always be relative to root.
927 '''
927 '''
928 if not n1:
928 if not n1:
929 return localpath(n2)
929 return localpath(n2)
930 if os.path.isabs(n1):
930 if os.path.isabs(n1):
931 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
931 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
932 return os.path.join(root, localpath(n2))
932 return os.path.join(root, localpath(n2))
933 n2 = '/'.join((pconvert(root), n2))
933 n2 = '/'.join((pconvert(root), n2))
934 a, b = splitpath(n1), n2.split('/')
934 a, b = splitpath(n1), n2.split('/')
935 a.reverse()
935 a.reverse()
936 b.reverse()
936 b.reverse()
937 while a and b and a[-1] == b[-1]:
937 while a and b and a[-1] == b[-1]:
938 a.pop()
938 a.pop()
939 b.pop()
939 b.pop()
940 b.reverse()
940 b.reverse()
941 return pycompat.ossep.join((['..'] * len(a)) + b) or '.'
941 return pycompat.ossep.join((['..'] * len(a)) + b) or '.'
942
942
943 def mainfrozen():
943 def mainfrozen():
944 """return True if we are a frozen executable.
944 """return True if we are a frozen executable.
945
945
946 The code supports py2exe (most common, Windows only) and tools/freeze
946 The code supports py2exe (most common, Windows only) and tools/freeze
947 (portable, not much used).
947 (portable, not much used).
948 """
948 """
949 return (safehasattr(sys, "frozen") or # new py2exe
949 return (safehasattr(sys, "frozen") or # new py2exe
950 safehasattr(sys, "importers") or # old py2exe
950 safehasattr(sys, "importers") or # old py2exe
951 imp.is_frozen(u"__main__")) # tools/freeze
951 imp.is_frozen(u"__main__")) # tools/freeze
952
952
953 # the location of data files matching the source code
953 # the location of data files matching the source code
954 if mainfrozen() and getattr(sys, 'frozen', None) != 'macosx_app':
954 if mainfrozen() and getattr(sys, 'frozen', None) != 'macosx_app':
955 # executable version (py2exe) doesn't support __file__
955 # executable version (py2exe) doesn't support __file__
956 datapath = os.path.dirname(pycompat.sysexecutable)
956 datapath = os.path.dirname(pycompat.sysexecutable)
957 else:
957 else:
958 datapath = os.path.dirname(__file__)
958 datapath = os.path.dirname(__file__)
959
959
960 if not isinstance(datapath, bytes):
960 if not isinstance(datapath, bytes):
961 datapath = pycompat.fsencode(datapath)
961 datapath = pycompat.fsencode(datapath)
962
962
963 i18n.setdatapath(datapath)
963 i18n.setdatapath(datapath)
964
964
965 _hgexecutable = None
965 _hgexecutable = None
966
966
967 def hgexecutable():
967 def hgexecutable():
968 """return location of the 'hg' executable.
968 """return location of the 'hg' executable.
969
969
970 Defaults to $HG or 'hg' in the search path.
970 Defaults to $HG or 'hg' in the search path.
971 """
971 """
972 if _hgexecutable is None:
972 if _hgexecutable is None:
973 hg = encoding.environ.get('HG')
973 hg = encoding.environ.get('HG')
974 mainmod = sys.modules['__main__']
974 mainmod = sys.modules['__main__']
975 if hg:
975 if hg:
976 _sethgexecutable(hg)
976 _sethgexecutable(hg)
977 elif mainfrozen():
977 elif mainfrozen():
978 if getattr(sys, 'frozen', None) == 'macosx_app':
978 if getattr(sys, 'frozen', None) == 'macosx_app':
979 # Env variable set by py2app
979 # Env variable set by py2app
980 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
980 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
981 else:
981 else:
982 _sethgexecutable(pycompat.sysexecutable)
982 _sethgexecutable(pycompat.sysexecutable)
983 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
983 elif os.path.basename(getattr(mainmod, '__file__', '')) == 'hg':
984 _sethgexecutable(mainmod.__file__)
984 _sethgexecutable(mainmod.__file__)
985 else:
985 else:
986 exe = findexe('hg') or os.path.basename(sys.argv[0])
986 exe = findexe('hg') or os.path.basename(sys.argv[0])
987 _sethgexecutable(exe)
987 _sethgexecutable(exe)
988 return _hgexecutable
988 return _hgexecutable
989
989
990 def _sethgexecutable(path):
990 def _sethgexecutable(path):
991 """set location of the 'hg' executable"""
991 """set location of the 'hg' executable"""
992 global _hgexecutable
992 global _hgexecutable
993 _hgexecutable = path
993 _hgexecutable = path
994
994
995 def _isstdout(f):
995 def _isstdout(f):
996 fileno = getattr(f, 'fileno', None)
996 fileno = getattr(f, 'fileno', None)
997 return fileno and fileno() == sys.__stdout__.fileno()
997 return fileno and fileno() == sys.__stdout__.fileno()
998
998
999 def shellenviron(environ=None):
999 def shellenviron(environ=None):
1000 """return environ with optional override, useful for shelling out"""
1000 """return environ with optional override, useful for shelling out"""
1001 def py2shell(val):
1001 def py2shell(val):
1002 'convert python object into string that is useful to shell'
1002 'convert python object into string that is useful to shell'
1003 if val is None or val is False:
1003 if val is None or val is False:
1004 return '0'
1004 return '0'
1005 if val is True:
1005 if val is True:
1006 return '1'
1006 return '1'
1007 return str(val)
1007 return str(val)
1008 env = dict(encoding.environ)
1008 env = dict(encoding.environ)
1009 if environ:
1009 if environ:
1010 env.update((k, py2shell(v)) for k, v in environ.iteritems())
1010 env.update((k, py2shell(v)) for k, v in environ.iteritems())
1011 env['HG'] = hgexecutable()
1011 env['HG'] = hgexecutable()
1012 return env
1012 return env
1013
1013
1014 def system(cmd, environ=None, cwd=None, onerr=None, errprefix=None, out=None):
1014 def system(cmd, environ=None, cwd=None, onerr=None, errprefix=None, out=None):
1015 '''enhanced shell command execution.
1015 '''enhanced shell command execution.
1016 run with environment maybe modified, maybe in different dir.
1016 run with environment maybe modified, maybe in different dir.
1017
1017
1018 if command fails and onerr is None, return status, else raise onerr
1018 if command fails and onerr is None, return status, else raise onerr
1019 object as exception.
1019 object as exception.
1020
1020
1021 if out is specified, it is assumed to be a file-like object that has a
1021 if out is specified, it is assumed to be a file-like object that has a
1022 write() method. stdout and stderr will be redirected to out.'''
1022 write() method. stdout and stderr will be redirected to out.'''
1023 try:
1023 try:
1024 stdout.flush()
1024 stdout.flush()
1025 except Exception:
1025 except Exception:
1026 pass
1026 pass
1027 origcmd = cmd
1027 origcmd = cmd
1028 cmd = quotecommand(cmd)
1028 cmd = quotecommand(cmd)
1029 if pycompat.sysplatform == 'plan9' and (sys.version_info[0] == 2
1029 if pycompat.sysplatform == 'plan9' and (sys.version_info[0] == 2
1030 and sys.version_info[1] < 7):
1030 and sys.version_info[1] < 7):
1031 # subprocess kludge to work around issues in half-baked Python
1031 # subprocess kludge to work around issues in half-baked Python
1032 # ports, notably bichued/python:
1032 # ports, notably bichued/python:
1033 if not cwd is None:
1033 if not cwd is None:
1034 os.chdir(cwd)
1034 os.chdir(cwd)
1035 rc = os.system(cmd)
1035 rc = os.system(cmd)
1036 else:
1036 else:
1037 env = shellenviron(environ)
1037 env = shellenviron(environ)
1038 if out is None or _isstdout(out):
1038 if out is None or _isstdout(out):
1039 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
1039 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
1040 env=env, cwd=cwd)
1040 env=env, cwd=cwd)
1041 else:
1041 else:
1042 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
1042 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
1043 env=env, cwd=cwd, stdout=subprocess.PIPE,
1043 env=env, cwd=cwd, stdout=subprocess.PIPE,
1044 stderr=subprocess.STDOUT)
1044 stderr=subprocess.STDOUT)
1045 for line in iter(proc.stdout.readline, ''):
1045 for line in iter(proc.stdout.readline, ''):
1046 out.write(line)
1046 out.write(line)
1047 proc.wait()
1047 proc.wait()
1048 rc = proc.returncode
1048 rc = proc.returncode
1049 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
1049 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
1050 rc = 0
1050 rc = 0
1051 if rc and onerr:
1051 if rc and onerr:
1052 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
1052 errmsg = '%s %s' % (os.path.basename(origcmd.split(None, 1)[0]),
1053 explainexit(rc)[0])
1053 explainexit(rc)[0])
1054 if errprefix:
1054 if errprefix:
1055 errmsg = '%s: %s' % (errprefix, errmsg)
1055 errmsg = '%s: %s' % (errprefix, errmsg)
1056 raise onerr(errmsg)
1056 raise onerr(errmsg)
1057 return rc
1057 return rc
1058
1058
1059 def checksignature(func):
1059 def checksignature(func):
1060 '''wrap a function with code to check for calling errors'''
1060 '''wrap a function with code to check for calling errors'''
1061 def check(*args, **kwargs):
1061 def check(*args, **kwargs):
1062 try:
1062 try:
1063 return func(*args, **kwargs)
1063 return func(*args, **kwargs)
1064 except TypeError:
1064 except TypeError:
1065 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1065 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1066 raise error.SignatureError
1066 raise error.SignatureError
1067 raise
1067 raise
1068
1068
1069 return check
1069 return check
1070
1070
1071 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1071 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1072 '''copy a file, preserving mode and optionally other stat info like
1072 '''copy a file, preserving mode and optionally other stat info like
1073 atime/mtime
1073 atime/mtime
1074
1074
1075 checkambig argument is used with filestat, and is useful only if
1075 checkambig argument is used with filestat, and is useful only if
1076 destination file is guarded by any lock (e.g. repo.lock or
1076 destination file is guarded by any lock (e.g. repo.lock or
1077 repo.wlock).
1077 repo.wlock).
1078
1078
1079 copystat and checkambig should be exclusive.
1079 copystat and checkambig should be exclusive.
1080 '''
1080 '''
1081 assert not (copystat and checkambig)
1081 assert not (copystat and checkambig)
1082 oldstat = None
1082 oldstat = None
1083 if os.path.lexists(dest):
1083 if os.path.lexists(dest):
1084 if checkambig:
1084 if checkambig:
1085 oldstat = checkambig and filestat(dest)
1085 oldstat = checkambig and filestat(dest)
1086 unlink(dest)
1086 unlink(dest)
1087 # hardlinks are problematic on CIFS, quietly ignore this flag
1087 # hardlinks are problematic on CIFS, quietly ignore this flag
1088 # until we find a way to work around it cleanly (issue4546)
1088 # until we find a way to work around it cleanly (issue4546)
1089 if False and hardlink:
1089 if False and hardlink:
1090 try:
1090 try:
1091 oslink(src, dest)
1091 oslink(src, dest)
1092 return
1092 return
1093 except (IOError, OSError):
1093 except (IOError, OSError):
1094 pass # fall back to normal copy
1094 pass # fall back to normal copy
1095 if os.path.islink(src):
1095 if os.path.islink(src):
1096 os.symlink(os.readlink(src), dest)
1096 os.symlink(os.readlink(src), dest)
1097 # copytime is ignored for symlinks, but in general copytime isn't needed
1097 # copytime is ignored for symlinks, but in general copytime isn't needed
1098 # for them anyway
1098 # for them anyway
1099 else:
1099 else:
1100 try:
1100 try:
1101 shutil.copyfile(src, dest)
1101 shutil.copyfile(src, dest)
1102 if copystat:
1102 if copystat:
1103 # copystat also copies mode
1103 # copystat also copies mode
1104 shutil.copystat(src, dest)
1104 shutil.copystat(src, dest)
1105 else:
1105 else:
1106 shutil.copymode(src, dest)
1106 shutil.copymode(src, dest)
1107 if oldstat and oldstat.stat:
1107 if oldstat and oldstat.stat:
1108 newstat = filestat(dest)
1108 newstat = filestat(dest)
1109 if newstat.isambig(oldstat):
1109 if newstat.isambig(oldstat):
1110 # stat of copied file is ambiguous to original one
1110 # stat of copied file is ambiguous to original one
1111 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
1111 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
1112 os.utime(dest, (advanced, advanced))
1112 os.utime(dest, (advanced, advanced))
1113 except shutil.Error as inst:
1113 except shutil.Error as inst:
1114 raise Abort(str(inst))
1114 raise Abort(str(inst))
1115
1115
1116 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
1116 def copyfiles(src, dst, hardlink=None, progress=lambda t, pos: None):
1117 """Copy a directory tree using hardlinks if possible."""
1117 """Copy a directory tree using hardlinks if possible."""
1118 num = 0
1118 num = 0
1119
1119
1120 if hardlink is None:
1120 if hardlink is None:
1121 hardlink = (os.stat(src).st_dev ==
1121 hardlink = (os.stat(src).st_dev ==
1122 os.stat(os.path.dirname(dst)).st_dev)
1122 os.stat(os.path.dirname(dst)).st_dev)
1123 if hardlink:
1123 if hardlink:
1124 topic = _('linking')
1124 topic = _('linking')
1125 else:
1125 else:
1126 topic = _('copying')
1126 topic = _('copying')
1127
1127
1128 if os.path.isdir(src):
1128 if os.path.isdir(src):
1129 os.mkdir(dst)
1129 os.mkdir(dst)
1130 for name, kind in osutil.listdir(src):
1130 for name, kind in osutil.listdir(src):
1131 srcname = os.path.join(src, name)
1131 srcname = os.path.join(src, name)
1132 dstname = os.path.join(dst, name)
1132 dstname = os.path.join(dst, name)
1133 def nprog(t, pos):
1133 def nprog(t, pos):
1134 if pos is not None:
1134 if pos is not None:
1135 return progress(t, pos + num)
1135 return progress(t, pos + num)
1136 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
1136 hardlink, n = copyfiles(srcname, dstname, hardlink, progress=nprog)
1137 num += n
1137 num += n
1138 else:
1138 else:
1139 if hardlink:
1139 if hardlink:
1140 try:
1140 try:
1141 oslink(src, dst)
1141 oslink(src, dst)
1142 except (IOError, OSError):
1142 except (IOError, OSError):
1143 hardlink = False
1143 hardlink = False
1144 shutil.copy(src, dst)
1144 shutil.copy(src, dst)
1145 else:
1145 else:
1146 shutil.copy(src, dst)
1146 shutil.copy(src, dst)
1147 num += 1
1147 num += 1
1148 progress(topic, num)
1148 progress(topic, num)
1149 progress(topic, None)
1149 progress(topic, None)
1150
1150
1151 return hardlink, num
1151 return hardlink, num
1152
1152
1153 _winreservednames = '''con prn aux nul
1153 _winreservednames = '''con prn aux nul
1154 com1 com2 com3 com4 com5 com6 com7 com8 com9
1154 com1 com2 com3 com4 com5 com6 com7 com8 com9
1155 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
1155 lpt1 lpt2 lpt3 lpt4 lpt5 lpt6 lpt7 lpt8 lpt9'''.split()
1156 _winreservedchars = ':*?"<>|'
1156 _winreservedchars = ':*?"<>|'
1157 def checkwinfilename(path):
1157 def checkwinfilename(path):
1158 r'''Check that the base-relative path is a valid filename on Windows.
1158 r'''Check that the base-relative path is a valid filename on Windows.
1159 Returns None if the path is ok, or a UI string describing the problem.
1159 Returns None if the path is ok, or a UI string describing the problem.
1160
1160
1161 >>> checkwinfilename("just/a/normal/path")
1161 >>> checkwinfilename("just/a/normal/path")
1162 >>> checkwinfilename("foo/bar/con.xml")
1162 >>> checkwinfilename("foo/bar/con.xml")
1163 "filename contains 'con', which is reserved on Windows"
1163 "filename contains 'con', which is reserved on Windows"
1164 >>> checkwinfilename("foo/con.xml/bar")
1164 >>> checkwinfilename("foo/con.xml/bar")
1165 "filename contains 'con', which is reserved on Windows"
1165 "filename contains 'con', which is reserved on Windows"
1166 >>> checkwinfilename("foo/bar/xml.con")
1166 >>> checkwinfilename("foo/bar/xml.con")
1167 >>> checkwinfilename("foo/bar/AUX/bla.txt")
1167 >>> checkwinfilename("foo/bar/AUX/bla.txt")
1168 "filename contains 'AUX', which is reserved on Windows"
1168 "filename contains 'AUX', which is reserved on Windows"
1169 >>> checkwinfilename("foo/bar/bla:.txt")
1169 >>> checkwinfilename("foo/bar/bla:.txt")
1170 "filename contains ':', which is reserved on Windows"
1170 "filename contains ':', which is reserved on Windows"
1171 >>> checkwinfilename("foo/bar/b\07la.txt")
1171 >>> checkwinfilename("foo/bar/b\07la.txt")
1172 "filename contains '\\x07', which is invalid on Windows"
1172 "filename contains '\\x07', which is invalid on Windows"
1173 >>> checkwinfilename("foo/bar/bla ")
1173 >>> checkwinfilename("foo/bar/bla ")
1174 "filename ends with ' ', which is not allowed on Windows"
1174 "filename ends with ' ', which is not allowed on Windows"
1175 >>> checkwinfilename("../bar")
1175 >>> checkwinfilename("../bar")
1176 >>> checkwinfilename("foo\\")
1176 >>> checkwinfilename("foo\\")
1177 "filename ends with '\\', which is invalid on Windows"
1177 "filename ends with '\\', which is invalid on Windows"
1178 >>> checkwinfilename("foo\\/bar")
1178 >>> checkwinfilename("foo\\/bar")
1179 "directory name ends with '\\', which is invalid on Windows"
1179 "directory name ends with '\\', which is invalid on Windows"
1180 '''
1180 '''
1181 if path.endswith('\\'):
1181 if path.endswith('\\'):
1182 return _("filename ends with '\\', which is invalid on Windows")
1182 return _("filename ends with '\\', which is invalid on Windows")
1183 if '\\/' in path:
1183 if '\\/' in path:
1184 return _("directory name ends with '\\', which is invalid on Windows")
1184 return _("directory name ends with '\\', which is invalid on Windows")
1185 for n in path.replace('\\', '/').split('/'):
1185 for n in path.replace('\\', '/').split('/'):
1186 if not n:
1186 if not n:
1187 continue
1187 continue
1188 for c in n:
1188 for c in n:
1189 if c in _winreservedchars:
1189 if c in _winreservedchars:
1190 return _("filename contains '%s', which is reserved "
1190 return _("filename contains '%s', which is reserved "
1191 "on Windows") % c
1191 "on Windows") % c
1192 if ord(c) <= 31:
1192 if ord(c) <= 31:
1193 return _("filename contains %r, which is invalid "
1193 return _("filename contains %r, which is invalid "
1194 "on Windows") % c
1194 "on Windows") % c
1195 base = n.split('.')[0]
1195 base = n.split('.')[0]
1196 if base and base.lower() in _winreservednames:
1196 if base and base.lower() in _winreservednames:
1197 return _("filename contains '%s', which is reserved "
1197 return _("filename contains '%s', which is reserved "
1198 "on Windows") % base
1198 "on Windows") % base
1199 t = n[-1]
1199 t = n[-1]
1200 if t in '. ' and n not in '..':
1200 if t in '. ' and n not in '..':
1201 return _("filename ends with '%s', which is not allowed "
1201 return _("filename ends with '%s', which is not allowed "
1202 "on Windows") % t
1202 "on Windows") % t
1203
1203
1204 if pycompat.osname == 'nt':
1204 if pycompat.osname == 'nt':
1205 checkosfilename = checkwinfilename
1205 checkosfilename = checkwinfilename
1206 timer = time.clock
1206 timer = time.clock
1207 else:
1207 else:
1208 checkosfilename = platform.checkosfilename
1208 checkosfilename = platform.checkosfilename
1209 timer = time.time
1209 timer = time.time
1210
1210
1211 if safehasattr(time, "perf_counter"):
1211 if safehasattr(time, "perf_counter"):
1212 timer = time.perf_counter
1212 timer = time.perf_counter
1213
1213
1214 def makelock(info, pathname):
1214 def makelock(info, pathname):
1215 try:
1215 try:
1216 return os.symlink(info, pathname)
1216 return os.symlink(info, pathname)
1217 except OSError as why:
1217 except OSError as why:
1218 if why.errno == errno.EEXIST:
1218 if why.errno == errno.EEXIST:
1219 raise
1219 raise
1220 except AttributeError: # no symlink in os
1220 except AttributeError: # no symlink in os
1221 pass
1221 pass
1222
1222
1223 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
1223 ld = os.open(pathname, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
1224 os.write(ld, info)
1224 os.write(ld, info)
1225 os.close(ld)
1225 os.close(ld)
1226
1226
1227 def readlock(pathname):
1227 def readlock(pathname):
1228 try:
1228 try:
1229 return os.readlink(pathname)
1229 return os.readlink(pathname)
1230 except OSError as why:
1230 except OSError as why:
1231 if why.errno not in (errno.EINVAL, errno.ENOSYS):
1231 if why.errno not in (errno.EINVAL, errno.ENOSYS):
1232 raise
1232 raise
1233 except AttributeError: # no symlink in os
1233 except AttributeError: # no symlink in os
1234 pass
1234 pass
1235 fp = posixfile(pathname)
1235 fp = posixfile(pathname)
1236 r = fp.read()
1236 r = fp.read()
1237 fp.close()
1237 fp.close()
1238 return r
1238 return r
1239
1239
1240 def fstat(fp):
1240 def fstat(fp):
1241 '''stat file object that may not have fileno method.'''
1241 '''stat file object that may not have fileno method.'''
1242 try:
1242 try:
1243 return os.fstat(fp.fileno())
1243 return os.fstat(fp.fileno())
1244 except AttributeError:
1244 except AttributeError:
1245 return os.stat(fp.name)
1245 return os.stat(fp.name)
1246
1246
1247 # File system features
1247 # File system features
1248
1248
1249 def fscasesensitive(path):
1249 def fscasesensitive(path):
1250 """
1250 """
1251 Return true if the given path is on a case-sensitive filesystem
1251 Return true if the given path is on a case-sensitive filesystem
1252
1252
1253 Requires a path (like /foo/.hg) ending with a foldable final
1253 Requires a path (like /foo/.hg) ending with a foldable final
1254 directory component.
1254 directory component.
1255 """
1255 """
1256 s1 = os.lstat(path)
1256 s1 = os.lstat(path)
1257 d, b = os.path.split(path)
1257 d, b = os.path.split(path)
1258 b2 = b.upper()
1258 b2 = b.upper()
1259 if b == b2:
1259 if b == b2:
1260 b2 = b.lower()
1260 b2 = b.lower()
1261 if b == b2:
1261 if b == b2:
1262 return True # no evidence against case sensitivity
1262 return True # no evidence against case sensitivity
1263 p2 = os.path.join(d, b2)
1263 p2 = os.path.join(d, b2)
1264 try:
1264 try:
1265 s2 = os.lstat(p2)
1265 s2 = os.lstat(p2)
1266 if s2 == s1:
1266 if s2 == s1:
1267 return False
1267 return False
1268 return True
1268 return True
1269 except OSError:
1269 except OSError:
1270 return True
1270 return True
1271
1271
1272 try:
1272 try:
1273 import re2
1273 import re2
1274 _re2 = None
1274 _re2 = None
1275 except ImportError:
1275 except ImportError:
1276 _re2 = False
1276 _re2 = False
1277
1277
1278 class _re(object):
1278 class _re(object):
1279 def _checkre2(self):
1279 def _checkre2(self):
1280 global _re2
1280 global _re2
1281 try:
1281 try:
1282 # check if match works, see issue3964
1282 # check if match works, see issue3964
1283 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
1283 _re2 = bool(re2.match(r'\[([^\[]+)\]', '[ui]'))
1284 except ImportError:
1284 except ImportError:
1285 _re2 = False
1285 _re2 = False
1286
1286
1287 def compile(self, pat, flags=0):
1287 def compile(self, pat, flags=0):
1288 '''Compile a regular expression, using re2 if possible
1288 '''Compile a regular expression, using re2 if possible
1289
1289
1290 For best performance, use only re2-compatible regexp features. The
1290 For best performance, use only re2-compatible regexp features. The
1291 only flags from the re module that are re2-compatible are
1291 only flags from the re module that are re2-compatible are
1292 IGNORECASE and MULTILINE.'''
1292 IGNORECASE and MULTILINE.'''
1293 if _re2 is None:
1293 if _re2 is None:
1294 self._checkre2()
1294 self._checkre2()
1295 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
1295 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
1296 if flags & remod.IGNORECASE:
1296 if flags & remod.IGNORECASE:
1297 pat = '(?i)' + pat
1297 pat = '(?i)' + pat
1298 if flags & remod.MULTILINE:
1298 if flags & remod.MULTILINE:
1299 pat = '(?m)' + pat
1299 pat = '(?m)' + pat
1300 try:
1300 try:
1301 return re2.compile(pat)
1301 return re2.compile(pat)
1302 except re2.error:
1302 except re2.error:
1303 pass
1303 pass
1304 return remod.compile(pat, flags)
1304 return remod.compile(pat, flags)
1305
1305
1306 @propertycache
1306 @propertycache
1307 def escape(self):
1307 def escape(self):
1308 '''Return the version of escape corresponding to self.compile.
1308 '''Return the version of escape corresponding to self.compile.
1309
1309
1310 This is imperfect because whether re2 or re is used for a particular
1310 This is imperfect because whether re2 or re is used for a particular
1311 function depends on the flags, etc, but it's the best we can do.
1311 function depends on the flags, etc, but it's the best we can do.
1312 '''
1312 '''
1313 global _re2
1313 global _re2
1314 if _re2 is None:
1314 if _re2 is None:
1315 self._checkre2()
1315 self._checkre2()
1316 if _re2:
1316 if _re2:
1317 return re2.escape
1317 return re2.escape
1318 else:
1318 else:
1319 return remod.escape
1319 return remod.escape
1320
1320
1321 re = _re()
1321 re = _re()
1322
1322
1323 _fspathcache = {}
1323 _fspathcache = {}
1324 def fspath(name, root):
1324 def fspath(name, root):
1325 '''Get name in the case stored in the filesystem
1325 '''Get name in the case stored in the filesystem
1326
1326
1327 The name should be relative to root, and be normcase-ed for efficiency.
1327 The name should be relative to root, and be normcase-ed for efficiency.
1328
1328
1329 Note that this function is unnecessary, and should not be
1329 Note that this function is unnecessary, and should not be
1330 called, for case-sensitive filesystems (simply because it's expensive).
1330 called, for case-sensitive filesystems (simply because it's expensive).
1331
1331
1332 The root should be normcase-ed, too.
1332 The root should be normcase-ed, too.
1333 '''
1333 '''
1334 def _makefspathcacheentry(dir):
1334 def _makefspathcacheentry(dir):
1335 return dict((normcase(n), n) for n in os.listdir(dir))
1335 return dict((normcase(n), n) for n in os.listdir(dir))
1336
1336
1337 seps = pycompat.ossep
1337 seps = pycompat.ossep
1338 if pycompat.osaltsep:
1338 if pycompat.osaltsep:
1339 seps = seps + pycompat.osaltsep
1339 seps = seps + pycompat.osaltsep
1340 # Protect backslashes. This gets silly very quickly.
1340 # Protect backslashes. This gets silly very quickly.
1341 seps.replace('\\','\\\\')
1341 seps.replace('\\','\\\\')
1342 pattern = remod.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1342 pattern = remod.compile(r'([^%s]+)|([%s]+)' % (seps, seps))
1343 dir = os.path.normpath(root)
1343 dir = os.path.normpath(root)
1344 result = []
1344 result = []
1345 for part, sep in pattern.findall(name):
1345 for part, sep in pattern.findall(name):
1346 if sep:
1346 if sep:
1347 result.append(sep)
1347 result.append(sep)
1348 continue
1348 continue
1349
1349
1350 if dir not in _fspathcache:
1350 if dir not in _fspathcache:
1351 _fspathcache[dir] = _makefspathcacheentry(dir)
1351 _fspathcache[dir] = _makefspathcacheentry(dir)
1352 contents = _fspathcache[dir]
1352 contents = _fspathcache[dir]
1353
1353
1354 found = contents.get(part)
1354 found = contents.get(part)
1355 if not found:
1355 if not found:
1356 # retry "once per directory" per "dirstate.walk" which
1356 # retry "once per directory" per "dirstate.walk" which
1357 # may take place for each patches of "hg qpush", for example
1357 # may take place for each patches of "hg qpush", for example
1358 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
1358 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
1359 found = contents.get(part)
1359 found = contents.get(part)
1360
1360
1361 result.append(found or part)
1361 result.append(found or part)
1362 dir = os.path.join(dir, part)
1362 dir = os.path.join(dir, part)
1363
1363
1364 return ''.join(result)
1364 return ''.join(result)
1365
1365
1366 def checknlink(testfile):
1366 def checknlink(testfile):
1367 '''check whether hardlink count reporting works properly'''
1367 '''check whether hardlink count reporting works properly'''
1368
1368
1369 # testfile may be open, so we need a separate file for checking to
1369 # testfile may be open, so we need a separate file for checking to
1370 # work around issue2543 (or testfile may get lost on Samba shares)
1370 # work around issue2543 (or testfile may get lost on Samba shares)
1371 f1 = testfile + ".hgtmp1"
1371 f1 = testfile + ".hgtmp1"
1372 if os.path.lexists(f1):
1372 if os.path.lexists(f1):
1373 return False
1373 return False
1374 try:
1374 try:
1375 posixfile(f1, 'w').close()
1375 posixfile(f1, 'w').close()
1376 except IOError:
1376 except IOError:
1377 try:
1377 try:
1378 os.unlink(f1)
1378 os.unlink(f1)
1379 except OSError:
1379 except OSError:
1380 pass
1380 pass
1381 return False
1381 return False
1382
1382
1383 f2 = testfile + ".hgtmp2"
1383 f2 = testfile + ".hgtmp2"
1384 fd = None
1384 fd = None
1385 try:
1385 try:
1386 oslink(f1, f2)
1386 oslink(f1, f2)
1387 # nlinks() may behave differently for files on Windows shares if
1387 # nlinks() may behave differently for files on Windows shares if
1388 # the file is open.
1388 # the file is open.
1389 fd = posixfile(f2)
1389 fd = posixfile(f2)
1390 return nlinks(f2) > 1
1390 return nlinks(f2) > 1
1391 except OSError:
1391 except OSError:
1392 return False
1392 return False
1393 finally:
1393 finally:
1394 if fd is not None:
1394 if fd is not None:
1395 fd.close()
1395 fd.close()
1396 for f in (f1, f2):
1396 for f in (f1, f2):
1397 try:
1397 try:
1398 os.unlink(f)
1398 os.unlink(f)
1399 except OSError:
1399 except OSError:
1400 pass
1400 pass
1401
1401
1402 def endswithsep(path):
1402 def endswithsep(path):
1403 '''Check path ends with os.sep or os.altsep.'''
1403 '''Check path ends with os.sep or os.altsep.'''
1404 return (path.endswith(pycompat.ossep)
1404 return (path.endswith(pycompat.ossep)
1405 or pycompat.osaltsep and path.endswith(pycompat.osaltsep))
1405 or pycompat.osaltsep and path.endswith(pycompat.osaltsep))
1406
1406
1407 def splitpath(path):
1407 def splitpath(path):
1408 '''Split path by os.sep.
1408 '''Split path by os.sep.
1409 Note that this function does not use os.altsep because this is
1409 Note that this function does not use os.altsep because this is
1410 an alternative of simple "xxx.split(os.sep)".
1410 an alternative of simple "xxx.split(os.sep)".
1411 It is recommended to use os.path.normpath() before using this
1411 It is recommended to use os.path.normpath() before using this
1412 function if need.'''
1412 function if need.'''
1413 return path.split(pycompat.ossep)
1413 return path.split(pycompat.ossep)
1414
1414
1415 def gui():
1415 def gui():
1416 '''Are we running in a GUI?'''
1416 '''Are we running in a GUI?'''
1417 if pycompat.sysplatform == 'darwin':
1417 if pycompat.sysplatform == 'darwin':
1418 if 'SSH_CONNECTION' in encoding.environ:
1418 if 'SSH_CONNECTION' in encoding.environ:
1419 # handle SSH access to a box where the user is logged in
1419 # handle SSH access to a box where the user is logged in
1420 return False
1420 return False
1421 elif getattr(osutil, 'isgui', None):
1421 elif getattr(osutil, 'isgui', None):
1422 # check if a CoreGraphics session is available
1422 # check if a CoreGraphics session is available
1423 return osutil.isgui()
1423 return osutil.isgui()
1424 else:
1424 else:
1425 # pure build; use a safe default
1425 # pure build; use a safe default
1426 return True
1426 return True
1427 else:
1427 else:
1428 return pycompat.osname == "nt" or encoding.environ.get("DISPLAY")
1428 return pycompat.osname == "nt" or encoding.environ.get("DISPLAY")
1429
1429
1430 def mktempcopy(name, emptyok=False, createmode=None):
1430 def mktempcopy(name, emptyok=False, createmode=None):
1431 """Create a temporary file with the same contents from name
1431 """Create a temporary file with the same contents from name
1432
1432
1433 The permission bits are copied from the original file.
1433 The permission bits are copied from the original file.
1434
1434
1435 If the temporary file is going to be truncated immediately, you
1435 If the temporary file is going to be truncated immediately, you
1436 can use emptyok=True as an optimization.
1436 can use emptyok=True as an optimization.
1437
1437
1438 Returns the name of the temporary file.
1438 Returns the name of the temporary file.
1439 """
1439 """
1440 d, fn = os.path.split(name)
1440 d, fn = os.path.split(name)
1441 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1441 fd, temp = tempfile.mkstemp(prefix='.%s-' % fn, dir=d)
1442 os.close(fd)
1442 os.close(fd)
1443 # Temporary files are created with mode 0600, which is usually not
1443 # Temporary files are created with mode 0600, which is usually not
1444 # what we want. If the original file already exists, just copy
1444 # what we want. If the original file already exists, just copy
1445 # its mode. Otherwise, manually obey umask.
1445 # its mode. Otherwise, manually obey umask.
1446 copymode(name, temp, createmode)
1446 copymode(name, temp, createmode)
1447 if emptyok:
1447 if emptyok:
1448 return temp
1448 return temp
1449 try:
1449 try:
1450 try:
1450 try:
1451 ifp = posixfile(name, "rb")
1451 ifp = posixfile(name, "rb")
1452 except IOError as inst:
1452 except IOError as inst:
1453 if inst.errno == errno.ENOENT:
1453 if inst.errno == errno.ENOENT:
1454 return temp
1454 return temp
1455 if not getattr(inst, 'filename', None):
1455 if not getattr(inst, 'filename', None):
1456 inst.filename = name
1456 inst.filename = name
1457 raise
1457 raise
1458 ofp = posixfile(temp, "wb")
1458 ofp = posixfile(temp, "wb")
1459 for chunk in filechunkiter(ifp):
1459 for chunk in filechunkiter(ifp):
1460 ofp.write(chunk)
1460 ofp.write(chunk)
1461 ifp.close()
1461 ifp.close()
1462 ofp.close()
1462 ofp.close()
1463 except: # re-raises
1463 except: # re-raises
1464 try: os.unlink(temp)
1464 try: os.unlink(temp)
1465 except OSError: pass
1465 except OSError: pass
1466 raise
1466 raise
1467 return temp
1467 return temp
1468
1468
1469 class filestat(object):
1469 class filestat(object):
1470 """help to exactly detect change of a file
1470 """help to exactly detect change of a file
1471
1471
1472 'stat' attribute is result of 'os.stat()' if specified 'path'
1472 'stat' attribute is result of 'os.stat()' if specified 'path'
1473 exists. Otherwise, it is None. This can avoid preparative
1473 exists. Otherwise, it is None. This can avoid preparative
1474 'exists()' examination on client side of this class.
1474 'exists()' examination on client side of this class.
1475 """
1475 """
1476 def __init__(self, path):
1476 def __init__(self, path):
1477 try:
1477 try:
1478 self.stat = os.stat(path)
1478 self.stat = os.stat(path)
1479 except OSError as err:
1479 except OSError as err:
1480 if err.errno != errno.ENOENT:
1480 if err.errno != errno.ENOENT:
1481 raise
1481 raise
1482 self.stat = None
1482 self.stat = None
1483
1483
1484 __hash__ = object.__hash__
1484 __hash__ = object.__hash__
1485
1485
1486 def __eq__(self, old):
1486 def __eq__(self, old):
1487 try:
1487 try:
1488 # if ambiguity between stat of new and old file is
1488 # if ambiguity between stat of new and old file is
1489 # avoided, comparison of size, ctime and mtime is enough
1489 # avoided, comparison of size, ctime and mtime is enough
1490 # to exactly detect change of a file regardless of platform
1490 # to exactly detect change of a file regardless of platform
1491 return (self.stat.st_size == old.stat.st_size and
1491 return (self.stat.st_size == old.stat.st_size and
1492 self.stat.st_ctime == old.stat.st_ctime and
1492 self.stat.st_ctime == old.stat.st_ctime and
1493 self.stat.st_mtime == old.stat.st_mtime)
1493 self.stat.st_mtime == old.stat.st_mtime)
1494 except AttributeError:
1494 except AttributeError:
1495 return False
1495 return False
1496
1496
1497 def isambig(self, old):
1497 def isambig(self, old):
1498 """Examine whether new (= self) stat is ambiguous against old one
1498 """Examine whether new (= self) stat is ambiguous against old one
1499
1499
1500 "S[N]" below means stat of a file at N-th change:
1500 "S[N]" below means stat of a file at N-th change:
1501
1501
1502 - S[n-1].ctime < S[n].ctime: can detect change of a file
1502 - S[n-1].ctime < S[n].ctime: can detect change of a file
1503 - S[n-1].ctime == S[n].ctime
1503 - S[n-1].ctime == S[n].ctime
1504 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
1504 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
1505 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
1505 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
1506 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
1506 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
1507 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
1507 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
1508
1508
1509 Case (*2) above means that a file was changed twice or more at
1509 Case (*2) above means that a file was changed twice or more at
1510 same time in sec (= S[n-1].ctime), and comparison of timestamp
1510 same time in sec (= S[n-1].ctime), and comparison of timestamp
1511 is ambiguous.
1511 is ambiguous.
1512
1512
1513 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
1513 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
1514 timestamp is ambiguous".
1514 timestamp is ambiguous".
1515
1515
1516 But advancing mtime only in case (*2) doesn't work as
1516 But advancing mtime only in case (*2) doesn't work as
1517 expected, because naturally advanced S[n].mtime in case (*1)
1517 expected, because naturally advanced S[n].mtime in case (*1)
1518 might be equal to manually advanced S[n-1 or earlier].mtime.
1518 might be equal to manually advanced S[n-1 or earlier].mtime.
1519
1519
1520 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
1520 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
1521 treated as ambiguous regardless of mtime, to avoid overlooking
1521 treated as ambiguous regardless of mtime, to avoid overlooking
1522 by confliction between such mtime.
1522 by confliction between such mtime.
1523
1523
1524 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
1524 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
1525 S[n].mtime", even if size of a file isn't changed.
1525 S[n].mtime", even if size of a file isn't changed.
1526 """
1526 """
1527 try:
1527 try:
1528 return (self.stat.st_ctime == old.stat.st_ctime)
1528 return (self.stat.st_ctime == old.stat.st_ctime)
1529 except AttributeError:
1529 except AttributeError:
1530 return False
1530 return False
1531
1531
1532 def avoidambig(self, path, old):
1532 def avoidambig(self, path, old):
1533 """Change file stat of specified path to avoid ambiguity
1533 """Change file stat of specified path to avoid ambiguity
1534
1534
1535 'old' should be previous filestat of 'path'.
1535 'old' should be previous filestat of 'path'.
1536
1536
1537 This skips avoiding ambiguity, if a process doesn't have
1537 This skips avoiding ambiguity, if a process doesn't have
1538 appropriate privileges for 'path'.
1538 appropriate privileges for 'path'.
1539 """
1539 """
1540 advanced = (old.stat.st_mtime + 1) & 0x7fffffff
1540 advanced = (old.stat.st_mtime + 1) & 0x7fffffff
1541 try:
1541 try:
1542 os.utime(path, (advanced, advanced))
1542 os.utime(path, (advanced, advanced))
1543 except OSError as inst:
1543 except OSError as inst:
1544 if inst.errno == errno.EPERM:
1544 if inst.errno == errno.EPERM:
1545 # utime() on the file created by another user causes EPERM,
1545 # utime() on the file created by another user causes EPERM,
1546 # if a process doesn't have appropriate privileges
1546 # if a process doesn't have appropriate privileges
1547 return
1547 return
1548 raise
1548 raise
1549
1549
1550 def __ne__(self, other):
1550 def __ne__(self, other):
1551 return not self == other
1551 return not self == other
1552
1552
1553 class atomictempfile(object):
1553 class atomictempfile(object):
1554 '''writable file object that atomically updates a file
1554 '''writable file object that atomically updates a file
1555
1555
1556 All writes will go to a temporary copy of the original file. Call
1556 All writes will go to a temporary copy of the original file. Call
1557 close() when you are done writing, and atomictempfile will rename
1557 close() when you are done writing, and atomictempfile will rename
1558 the temporary copy to the original name, making the changes
1558 the temporary copy to the original name, making the changes
1559 visible. If the object is destroyed without being closed, all your
1559 visible. If the object is destroyed without being closed, all your
1560 writes are discarded.
1560 writes are discarded.
1561
1561
1562 checkambig argument of constructor is used with filestat, and is
1562 checkambig argument of constructor is used with filestat, and is
1563 useful only if target file is guarded by any lock (e.g. repo.lock
1563 useful only if target file is guarded by any lock (e.g. repo.lock
1564 or repo.wlock).
1564 or repo.wlock).
1565 '''
1565 '''
1566 def __init__(self, name, mode='w+b', createmode=None, checkambig=False):
1566 def __init__(self, name, mode='w+b', createmode=None, checkambig=False):
1567 self.__name = name # permanent name
1567 self.__name = name # permanent name
1568 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1568 self._tempname = mktempcopy(name, emptyok=('w' in mode),
1569 createmode=createmode)
1569 createmode=createmode)
1570 self._fp = posixfile(self._tempname, mode)
1570 self._fp = posixfile(self._tempname, mode)
1571 self._checkambig = checkambig
1571 self._checkambig = checkambig
1572
1572
1573 # delegated methods
1573 # delegated methods
1574 self.read = self._fp.read
1574 self.read = self._fp.read
1575 self.write = self._fp.write
1575 self.write = self._fp.write
1576 self.seek = self._fp.seek
1576 self.seek = self._fp.seek
1577 self.tell = self._fp.tell
1577 self.tell = self._fp.tell
1578 self.fileno = self._fp.fileno
1578 self.fileno = self._fp.fileno
1579
1579
1580 def close(self):
1580 def close(self):
1581 if not self._fp.closed:
1581 if not self._fp.closed:
1582 self._fp.close()
1582 self._fp.close()
1583 filename = localpath(self.__name)
1583 filename = localpath(self.__name)
1584 oldstat = self._checkambig and filestat(filename)
1584 oldstat = self._checkambig and filestat(filename)
1585 if oldstat and oldstat.stat:
1585 if oldstat and oldstat.stat:
1586 rename(self._tempname, filename)
1586 rename(self._tempname, filename)
1587 newstat = filestat(filename)
1587 newstat = filestat(filename)
1588 if newstat.isambig(oldstat):
1588 if newstat.isambig(oldstat):
1589 # stat of changed file is ambiguous to original one
1589 # stat of changed file is ambiguous to original one
1590 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
1590 advanced = (oldstat.stat.st_mtime + 1) & 0x7fffffff
1591 os.utime(filename, (advanced, advanced))
1591 os.utime(filename, (advanced, advanced))
1592 else:
1592 else:
1593 rename(self._tempname, filename)
1593 rename(self._tempname, filename)
1594
1594
1595 def discard(self):
1595 def discard(self):
1596 if not self._fp.closed:
1596 if not self._fp.closed:
1597 try:
1597 try:
1598 os.unlink(self._tempname)
1598 os.unlink(self._tempname)
1599 except OSError:
1599 except OSError:
1600 pass
1600 pass
1601 self._fp.close()
1601 self._fp.close()
1602
1602
1603 def __del__(self):
1603 def __del__(self):
1604 if safehasattr(self, '_fp'): # constructor actually did something
1604 if safehasattr(self, '_fp'): # constructor actually did something
1605 self.discard()
1605 self.discard()
1606
1606
1607 def __enter__(self):
1607 def __enter__(self):
1608 return self
1608 return self
1609
1609
1610 def __exit__(self, exctype, excvalue, traceback):
1610 def __exit__(self, exctype, excvalue, traceback):
1611 if exctype is not None:
1611 if exctype is not None:
1612 self.discard()
1612 self.discard()
1613 else:
1613 else:
1614 self.close()
1614 self.close()
1615
1615
1616 def makedirs(name, mode=None, notindexed=False):
1616 def makedirs(name, mode=None, notindexed=False):
1617 """recursive directory creation with parent mode inheritance
1617 """recursive directory creation with parent mode inheritance
1618
1618
1619 Newly created directories are marked as "not to be indexed by
1619 Newly created directories are marked as "not to be indexed by
1620 the content indexing service", if ``notindexed`` is specified
1620 the content indexing service", if ``notindexed`` is specified
1621 for "write" mode access.
1621 for "write" mode access.
1622 """
1622 """
1623 try:
1623 try:
1624 makedir(name, notindexed)
1624 makedir(name, notindexed)
1625 except OSError as err:
1625 except OSError as err:
1626 if err.errno == errno.EEXIST:
1626 if err.errno == errno.EEXIST:
1627 return
1627 return
1628 if err.errno != errno.ENOENT or not name:
1628 if err.errno != errno.ENOENT or not name:
1629 raise
1629 raise
1630 parent = os.path.dirname(os.path.abspath(name))
1630 parent = os.path.dirname(os.path.abspath(name))
1631 if parent == name:
1631 if parent == name:
1632 raise
1632 raise
1633 makedirs(parent, mode, notindexed)
1633 makedirs(parent, mode, notindexed)
1634 try:
1634 try:
1635 makedir(name, notindexed)
1635 makedir(name, notindexed)
1636 except OSError as err:
1636 except OSError as err:
1637 # Catch EEXIST to handle races
1637 # Catch EEXIST to handle races
1638 if err.errno == errno.EEXIST:
1638 if err.errno == errno.EEXIST:
1639 return
1639 return
1640 raise
1640 raise
1641 if mode is not None:
1641 if mode is not None:
1642 os.chmod(name, mode)
1642 os.chmod(name, mode)
1643
1643
1644 def readfile(path):
1644 def readfile(path):
1645 with open(path, 'rb') as fp:
1645 with open(path, 'rb') as fp:
1646 return fp.read()
1646 return fp.read()
1647
1647
1648 def writefile(path, text):
1648 def writefile(path, text):
1649 with open(path, 'wb') as fp:
1649 with open(path, 'wb') as fp:
1650 fp.write(text)
1650 fp.write(text)
1651
1651
1652 def appendfile(path, text):
1652 def appendfile(path, text):
1653 with open(path, 'ab') as fp:
1653 with open(path, 'ab') as fp:
1654 fp.write(text)
1654 fp.write(text)
1655
1655
1656 class chunkbuffer(object):
1656 class chunkbuffer(object):
1657 """Allow arbitrary sized chunks of data to be efficiently read from an
1657 """Allow arbitrary sized chunks of data to be efficiently read from an
1658 iterator over chunks of arbitrary size."""
1658 iterator over chunks of arbitrary size."""
1659
1659
1660 def __init__(self, in_iter):
1660 def __init__(self, in_iter):
1661 """in_iter is the iterator that's iterating over the input chunks.
1661 """in_iter is the iterator that's iterating over the input chunks.
1662 targetsize is how big a buffer to try to maintain."""
1662 targetsize is how big a buffer to try to maintain."""
1663 def splitbig(chunks):
1663 def splitbig(chunks):
1664 for chunk in chunks:
1664 for chunk in chunks:
1665 if len(chunk) > 2**20:
1665 if len(chunk) > 2**20:
1666 pos = 0
1666 pos = 0
1667 while pos < len(chunk):
1667 while pos < len(chunk):
1668 end = pos + 2 ** 18
1668 end = pos + 2 ** 18
1669 yield chunk[pos:end]
1669 yield chunk[pos:end]
1670 pos = end
1670 pos = end
1671 else:
1671 else:
1672 yield chunk
1672 yield chunk
1673 self.iter = splitbig(in_iter)
1673 self.iter = splitbig(in_iter)
1674 self._queue = collections.deque()
1674 self._queue = collections.deque()
1675 self._chunkoffset = 0
1675 self._chunkoffset = 0
1676
1676
1677 def read(self, l=None):
1677 def read(self, l=None):
1678 """Read L bytes of data from the iterator of chunks of data.
1678 """Read L bytes of data from the iterator of chunks of data.
1679 Returns less than L bytes if the iterator runs dry.
1679 Returns less than L bytes if the iterator runs dry.
1680
1680
1681 If size parameter is omitted, read everything"""
1681 If size parameter is omitted, read everything"""
1682 if l is None:
1682 if l is None:
1683 return ''.join(self.iter)
1683 return ''.join(self.iter)
1684
1684
1685 left = l
1685 left = l
1686 buf = []
1686 buf = []
1687 queue = self._queue
1687 queue = self._queue
1688 while left > 0:
1688 while left > 0:
1689 # refill the queue
1689 # refill the queue
1690 if not queue:
1690 if not queue:
1691 target = 2**18
1691 target = 2**18
1692 for chunk in self.iter:
1692 for chunk in self.iter:
1693 queue.append(chunk)
1693 queue.append(chunk)
1694 target -= len(chunk)
1694 target -= len(chunk)
1695 if target <= 0:
1695 if target <= 0:
1696 break
1696 break
1697 if not queue:
1697 if not queue:
1698 break
1698 break
1699
1699
1700 # The easy way to do this would be to queue.popleft(), modify the
1700 # The easy way to do this would be to queue.popleft(), modify the
1701 # chunk (if necessary), then queue.appendleft(). However, for cases
1701 # chunk (if necessary), then queue.appendleft(). However, for cases
1702 # where we read partial chunk content, this incurs 2 dequeue
1702 # where we read partial chunk content, this incurs 2 dequeue
1703 # mutations and creates a new str for the remaining chunk in the
1703 # mutations and creates a new str for the remaining chunk in the
1704 # queue. Our code below avoids this overhead.
1704 # queue. Our code below avoids this overhead.
1705
1705
1706 chunk = queue[0]
1706 chunk = queue[0]
1707 chunkl = len(chunk)
1707 chunkl = len(chunk)
1708 offset = self._chunkoffset
1708 offset = self._chunkoffset
1709
1709
1710 # Use full chunk.
1710 # Use full chunk.
1711 if offset == 0 and left >= chunkl:
1711 if offset == 0 and left >= chunkl:
1712 left -= chunkl
1712 left -= chunkl
1713 queue.popleft()
1713 queue.popleft()
1714 buf.append(chunk)
1714 buf.append(chunk)
1715 # self._chunkoffset remains at 0.
1715 # self._chunkoffset remains at 0.
1716 continue
1716 continue
1717
1717
1718 chunkremaining = chunkl - offset
1718 chunkremaining = chunkl - offset
1719
1719
1720 # Use all of unconsumed part of chunk.
1720 # Use all of unconsumed part of chunk.
1721 if left >= chunkremaining:
1721 if left >= chunkremaining:
1722 left -= chunkremaining
1722 left -= chunkremaining
1723 queue.popleft()
1723 queue.popleft()
1724 # offset == 0 is enabled by block above, so this won't merely
1724 # offset == 0 is enabled by block above, so this won't merely
1725 # copy via ``chunk[0:]``.
1725 # copy via ``chunk[0:]``.
1726 buf.append(chunk[offset:])
1726 buf.append(chunk[offset:])
1727 self._chunkoffset = 0
1727 self._chunkoffset = 0
1728
1728
1729 # Partial chunk needed.
1729 # Partial chunk needed.
1730 else:
1730 else:
1731 buf.append(chunk[offset:offset + left])
1731 buf.append(chunk[offset:offset + left])
1732 self._chunkoffset += left
1732 self._chunkoffset += left
1733 left -= chunkremaining
1733 left -= chunkremaining
1734
1734
1735 return ''.join(buf)
1735 return ''.join(buf)
1736
1736
1737 def filechunkiter(f, size=131072, limit=None):
1737 def filechunkiter(f, size=131072, limit=None):
1738 """Create a generator that produces the data in the file size
1738 """Create a generator that produces the data in the file size
1739 (default 131072) bytes at a time, up to optional limit (default is
1739 (default 131072) bytes at a time, up to optional limit (default is
1740 to read all data). Chunks may be less than size bytes if the
1740 to read all data). Chunks may be less than size bytes if the
1741 chunk is the last chunk in the file, or the file is a socket or
1741 chunk is the last chunk in the file, or the file is a socket or
1742 some other type of file that sometimes reads less data than is
1742 some other type of file that sometimes reads less data than is
1743 requested."""
1743 requested."""
1744 assert size >= 0
1744 assert size >= 0
1745 assert limit is None or limit >= 0
1745 assert limit is None or limit >= 0
1746 while True:
1746 while True:
1747 if limit is None:
1747 if limit is None:
1748 nbytes = size
1748 nbytes = size
1749 else:
1749 else:
1750 nbytes = min(limit, size)
1750 nbytes = min(limit, size)
1751 s = nbytes and f.read(nbytes)
1751 s = nbytes and f.read(nbytes)
1752 if not s:
1752 if not s:
1753 break
1753 break
1754 if limit:
1754 if limit:
1755 limit -= len(s)
1755 limit -= len(s)
1756 yield s
1756 yield s
1757
1757
1758 def makedate(timestamp=None):
1758 def makedate(timestamp=None):
1759 '''Return a unix timestamp (or the current time) as a (unixtime,
1759 '''Return a unix timestamp (or the current time) as a (unixtime,
1760 offset) tuple based off the local timezone.'''
1760 offset) tuple based off the local timezone.'''
1761 if timestamp is None:
1761 if timestamp is None:
1762 timestamp = time.time()
1762 timestamp = time.time()
1763 if timestamp < 0:
1763 if timestamp < 0:
1764 hint = _("check your clock")
1764 hint = _("check your clock")
1765 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1765 raise Abort(_("negative timestamp: %d") % timestamp, hint=hint)
1766 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1766 delta = (datetime.datetime.utcfromtimestamp(timestamp) -
1767 datetime.datetime.fromtimestamp(timestamp))
1767 datetime.datetime.fromtimestamp(timestamp))
1768 tz = delta.days * 86400 + delta.seconds
1768 tz = delta.days * 86400 + delta.seconds
1769 return timestamp, tz
1769 return timestamp, tz
1770
1770
1771 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1771 def datestr(date=None, format='%a %b %d %H:%M:%S %Y %1%2'):
1772 """represent a (unixtime, offset) tuple as a localized time.
1772 """represent a (unixtime, offset) tuple as a localized time.
1773 unixtime is seconds since the epoch, and offset is the time zone's
1773 unixtime is seconds since the epoch, and offset is the time zone's
1774 number of seconds away from UTC.
1774 number of seconds away from UTC.
1775
1775
1776 >>> datestr((0, 0))
1776 >>> datestr((0, 0))
1777 'Thu Jan 01 00:00:00 1970 +0000'
1777 'Thu Jan 01 00:00:00 1970 +0000'
1778 >>> datestr((42, 0))
1778 >>> datestr((42, 0))
1779 'Thu Jan 01 00:00:42 1970 +0000'
1779 'Thu Jan 01 00:00:42 1970 +0000'
1780 >>> datestr((-42, 0))
1780 >>> datestr((-42, 0))
1781 'Wed Dec 31 23:59:18 1969 +0000'
1781 'Wed Dec 31 23:59:18 1969 +0000'
1782 >>> datestr((0x7fffffff, 0))
1782 >>> datestr((0x7fffffff, 0))
1783 'Tue Jan 19 03:14:07 2038 +0000'
1783 'Tue Jan 19 03:14:07 2038 +0000'
1784 >>> datestr((-0x80000000, 0))
1784 >>> datestr((-0x80000000, 0))
1785 'Fri Dec 13 20:45:52 1901 +0000'
1785 'Fri Dec 13 20:45:52 1901 +0000'
1786 """
1786 """
1787 t, tz = date or makedate()
1787 t, tz = date or makedate()
1788 if "%1" in format or "%2" in format or "%z" in format:
1788 if "%1" in format or "%2" in format or "%z" in format:
1789 sign = (tz > 0) and "-" or "+"
1789 sign = (tz > 0) and "-" or "+"
1790 minutes = abs(tz) // 60
1790 minutes = abs(tz) // 60
1791 q, r = divmod(minutes, 60)
1791 q, r = divmod(minutes, 60)
1792 format = format.replace("%z", "%1%2")
1792 format = format.replace("%z", "%1%2")
1793 format = format.replace("%1", "%c%02d" % (sign, q))
1793 format = format.replace("%1", "%c%02d" % (sign, q))
1794 format = format.replace("%2", "%02d" % r)
1794 format = format.replace("%2", "%02d" % r)
1795 d = t - tz
1795 d = t - tz
1796 if d > 0x7fffffff:
1796 if d > 0x7fffffff:
1797 d = 0x7fffffff
1797 d = 0x7fffffff
1798 elif d < -0x80000000:
1798 elif d < -0x80000000:
1799 d = -0x80000000
1799 d = -0x80000000
1800 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
1800 # Never use time.gmtime() and datetime.datetime.fromtimestamp()
1801 # because they use the gmtime() system call which is buggy on Windows
1801 # because they use the gmtime() system call which is buggy on Windows
1802 # for negative values.
1802 # for negative values.
1803 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
1803 t = datetime.datetime(1970, 1, 1) + datetime.timedelta(seconds=d)
1804 s = t.strftime(format)
1804 s = t.strftime(format)
1805 return s
1805 return s
1806
1806
1807 def shortdate(date=None):
1807 def shortdate(date=None):
1808 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1808 """turn (timestamp, tzoff) tuple into iso 8631 date."""
1809 return datestr(date, format='%Y-%m-%d')
1809 return datestr(date, format='%Y-%m-%d')
1810
1810
1811 def parsetimezone(s):
1811 def parsetimezone(s):
1812 """find a trailing timezone, if any, in string, and return a
1812 """find a trailing timezone, if any, in string, and return a
1813 (offset, remainder) pair"""
1813 (offset, remainder) pair"""
1814
1814
1815 if s.endswith("GMT") or s.endswith("UTC"):
1815 if s.endswith("GMT") or s.endswith("UTC"):
1816 return 0, s[:-3].rstrip()
1816 return 0, s[:-3].rstrip()
1817
1817
1818 # Unix-style timezones [+-]hhmm
1818 # Unix-style timezones [+-]hhmm
1819 if len(s) >= 5 and s[-5] in "+-" and s[-4:].isdigit():
1819 if len(s) >= 5 and s[-5] in "+-" and s[-4:].isdigit():
1820 sign = (s[-5] == "+") and 1 or -1
1820 sign = (s[-5] == "+") and 1 or -1
1821 hours = int(s[-4:-2])
1821 hours = int(s[-4:-2])
1822 minutes = int(s[-2:])
1822 minutes = int(s[-2:])
1823 return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
1823 return -sign * (hours * 60 + minutes) * 60, s[:-5].rstrip()
1824
1824
1825 # ISO8601 trailing Z
1825 # ISO8601 trailing Z
1826 if s.endswith("Z") and s[-2:-1].isdigit():
1826 if s.endswith("Z") and s[-2:-1].isdigit():
1827 return 0, s[:-1]
1827 return 0, s[:-1]
1828
1828
1829 # ISO8601-style [+-]hh:mm
1829 # ISO8601-style [+-]hh:mm
1830 if (len(s) >= 6 and s[-6] in "+-" and s[-3] == ":" and
1830 if (len(s) >= 6 and s[-6] in "+-" and s[-3] == ":" and
1831 s[-5:-3].isdigit() and s[-2:].isdigit()):
1831 s[-5:-3].isdigit() and s[-2:].isdigit()):
1832 sign = (s[-6] == "+") and 1 or -1
1832 sign = (s[-6] == "+") and 1 or -1
1833 hours = int(s[-5:-3])
1833 hours = int(s[-5:-3])
1834 minutes = int(s[-2:])
1834 minutes = int(s[-2:])
1835 return -sign * (hours * 60 + minutes) * 60, s[:-6]
1835 return -sign * (hours * 60 + minutes) * 60, s[:-6]
1836
1836
1837 return None, s
1837 return None, s
1838
1838
1839 def strdate(string, format, defaults=[]):
1839 def strdate(string, format, defaults=[]):
1840 """parse a localized time string and return a (unixtime, offset) tuple.
1840 """parse a localized time string and return a (unixtime, offset) tuple.
1841 if the string cannot be parsed, ValueError is raised."""
1841 if the string cannot be parsed, ValueError is raised."""
1842 # NOTE: unixtime = localunixtime + offset
1842 # NOTE: unixtime = localunixtime + offset
1843 offset, date = parsetimezone(string)
1843 offset, date = parsetimezone(string)
1844
1844
1845 # add missing elements from defaults
1845 # add missing elements from defaults
1846 usenow = False # default to using biased defaults
1846 usenow = False # default to using biased defaults
1847 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1847 for part in ("S", "M", "HI", "d", "mb", "yY"): # decreasing specificity
1848 found = [True for p in part if ("%"+p) in format]
1848 found = [True for p in part if ("%"+p) in format]
1849 if not found:
1849 if not found:
1850 date += "@" + defaults[part][usenow]
1850 date += "@" + defaults[part][usenow]
1851 format += "@%" + part[0]
1851 format += "@%" + part[0]
1852 else:
1852 else:
1853 # We've found a specific time element, less specific time
1853 # We've found a specific time element, less specific time
1854 # elements are relative to today
1854 # elements are relative to today
1855 usenow = True
1855 usenow = True
1856
1856
1857 timetuple = time.strptime(date, format)
1857 timetuple = time.strptime(date, format)
1858 localunixtime = int(calendar.timegm(timetuple))
1858 localunixtime = int(calendar.timegm(timetuple))
1859 if offset is None:
1859 if offset is None:
1860 # local timezone
1860 # local timezone
1861 unixtime = int(time.mktime(timetuple))
1861 unixtime = int(time.mktime(timetuple))
1862 offset = unixtime - localunixtime
1862 offset = unixtime - localunixtime
1863 else:
1863 else:
1864 unixtime = localunixtime + offset
1864 unixtime = localunixtime + offset
1865 return unixtime, offset
1865 return unixtime, offset
1866
1866
1867 def parsedate(date, formats=None, bias=None):
1867 def parsedate(date, formats=None, bias=None):
1868 """parse a localized date/time and return a (unixtime, offset) tuple.
1868 """parse a localized date/time and return a (unixtime, offset) tuple.
1869
1869
1870 The date may be a "unixtime offset" string or in one of the specified
1870 The date may be a "unixtime offset" string or in one of the specified
1871 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1871 formats. If the date already is a (unixtime, offset) tuple, it is returned.
1872
1872
1873 >>> parsedate(' today ') == parsedate(\
1873 >>> parsedate(' today ') == parsedate(\
1874 datetime.date.today().strftime('%b %d'))
1874 datetime.date.today().strftime('%b %d'))
1875 True
1875 True
1876 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1876 >>> parsedate( 'yesterday ') == parsedate((datetime.date.today() -\
1877 datetime.timedelta(days=1)\
1877 datetime.timedelta(days=1)\
1878 ).strftime('%b %d'))
1878 ).strftime('%b %d'))
1879 True
1879 True
1880 >>> now, tz = makedate()
1880 >>> now, tz = makedate()
1881 >>> strnow, strtz = parsedate('now')
1881 >>> strnow, strtz = parsedate('now')
1882 >>> (strnow - now) < 1
1882 >>> (strnow - now) < 1
1883 True
1883 True
1884 >>> tz == strtz
1884 >>> tz == strtz
1885 True
1885 True
1886 """
1886 """
1887 if bias is None:
1887 if bias is None:
1888 bias = {}
1888 bias = {}
1889 if not date:
1889 if not date:
1890 return 0, 0
1890 return 0, 0
1891 if isinstance(date, tuple) and len(date) == 2:
1891 if isinstance(date, tuple) and len(date) == 2:
1892 return date
1892 return date
1893 if not formats:
1893 if not formats:
1894 formats = defaultdateformats
1894 formats = defaultdateformats
1895 date = date.strip()
1895 date = date.strip()
1896
1896
1897 if date == 'now' or date == _('now'):
1897 if date == 'now' or date == _('now'):
1898 return makedate()
1898 return makedate()
1899 if date == 'today' or date == _('today'):
1899 if date == 'today' or date == _('today'):
1900 date = datetime.date.today().strftime('%b %d')
1900 date = datetime.date.today().strftime('%b %d')
1901 elif date == 'yesterday' or date == _('yesterday'):
1901 elif date == 'yesterday' or date == _('yesterday'):
1902 date = (datetime.date.today() -
1902 date = (datetime.date.today() -
1903 datetime.timedelta(days=1)).strftime('%b %d')
1903 datetime.timedelta(days=1)).strftime('%b %d')
1904
1904
1905 try:
1905 try:
1906 when, offset = map(int, date.split(' '))
1906 when, offset = map(int, date.split(' '))
1907 except ValueError:
1907 except ValueError:
1908 # fill out defaults
1908 # fill out defaults
1909 now = makedate()
1909 now = makedate()
1910 defaults = {}
1910 defaults = {}
1911 for part in ("d", "mb", "yY", "HI", "M", "S"):
1911 for part in ("d", "mb", "yY", "HI", "M", "S"):
1912 # this piece is for rounding the specific end of unknowns
1912 # this piece is for rounding the specific end of unknowns
1913 b = bias.get(part)
1913 b = bias.get(part)
1914 if b is None:
1914 if b is None:
1915 if part[0] in "HMS":
1915 if part[0] in "HMS":
1916 b = "00"
1916 b = "00"
1917 else:
1917 else:
1918 b = "0"
1918 b = "0"
1919
1919
1920 # this piece is for matching the generic end to today's date
1920 # this piece is for matching the generic end to today's date
1921 n = datestr(now, "%" + part[0])
1921 n = datestr(now, "%" + part[0])
1922
1922
1923 defaults[part] = (b, n)
1923 defaults[part] = (b, n)
1924
1924
1925 for format in formats:
1925 for format in formats:
1926 try:
1926 try:
1927 when, offset = strdate(date, format, defaults)
1927 when, offset = strdate(date, format, defaults)
1928 except (ValueError, OverflowError):
1928 except (ValueError, OverflowError):
1929 pass
1929 pass
1930 else:
1930 else:
1931 break
1931 break
1932 else:
1932 else:
1933 raise Abort(_('invalid date: %r') % date)
1933 raise Abort(_('invalid date: %r') % date)
1934 # validate explicit (probably user-specified) date and
1934 # validate explicit (probably user-specified) date and
1935 # time zone offset. values must fit in signed 32 bits for
1935 # time zone offset. values must fit in signed 32 bits for
1936 # current 32-bit linux runtimes. timezones go from UTC-12
1936 # current 32-bit linux runtimes. timezones go from UTC-12
1937 # to UTC+14
1937 # to UTC+14
1938 if when < -0x80000000 or when > 0x7fffffff:
1938 if when < -0x80000000 or when > 0x7fffffff:
1939 raise Abort(_('date exceeds 32 bits: %d') % when)
1939 raise Abort(_('date exceeds 32 bits: %d') % when)
1940 if offset < -50400 or offset > 43200:
1940 if offset < -50400 or offset > 43200:
1941 raise Abort(_('impossible time zone offset: %d') % offset)
1941 raise Abort(_('impossible time zone offset: %d') % offset)
1942 return when, offset
1942 return when, offset
1943
1943
1944 def matchdate(date):
1944 def matchdate(date):
1945 """Return a function that matches a given date match specifier
1945 """Return a function that matches a given date match specifier
1946
1946
1947 Formats include:
1947 Formats include:
1948
1948
1949 '{date}' match a given date to the accuracy provided
1949 '{date}' match a given date to the accuracy provided
1950
1950
1951 '<{date}' on or before a given date
1951 '<{date}' on or before a given date
1952
1952
1953 '>{date}' on or after a given date
1953 '>{date}' on or after a given date
1954
1954
1955 >>> p1 = parsedate("10:29:59")
1955 >>> p1 = parsedate("10:29:59")
1956 >>> p2 = parsedate("10:30:00")
1956 >>> p2 = parsedate("10:30:00")
1957 >>> p3 = parsedate("10:30:59")
1957 >>> p3 = parsedate("10:30:59")
1958 >>> p4 = parsedate("10:31:00")
1958 >>> p4 = parsedate("10:31:00")
1959 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1959 >>> p5 = parsedate("Sep 15 10:30:00 1999")
1960 >>> f = matchdate("10:30")
1960 >>> f = matchdate("10:30")
1961 >>> f(p1[0])
1961 >>> f(p1[0])
1962 False
1962 False
1963 >>> f(p2[0])
1963 >>> f(p2[0])
1964 True
1964 True
1965 >>> f(p3[0])
1965 >>> f(p3[0])
1966 True
1966 True
1967 >>> f(p4[0])
1967 >>> f(p4[0])
1968 False
1968 False
1969 >>> f(p5[0])
1969 >>> f(p5[0])
1970 False
1970 False
1971 """
1971 """
1972
1972
1973 def lower(date):
1973 def lower(date):
1974 d = {'mb': "1", 'd': "1"}
1974 d = {'mb': "1", 'd': "1"}
1975 return parsedate(date, extendeddateformats, d)[0]
1975 return parsedate(date, extendeddateformats, d)[0]
1976
1976
1977 def upper(date):
1977 def upper(date):
1978 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
1978 d = {'mb': "12", 'HI': "23", 'M': "59", 'S': "59"}
1979 for days in ("31", "30", "29"):
1979 for days in ("31", "30", "29"):
1980 try:
1980 try:
1981 d["d"] = days
1981 d["d"] = days
1982 return parsedate(date, extendeddateformats, d)[0]
1982 return parsedate(date, extendeddateformats, d)[0]
1983 except Abort:
1983 except Abort:
1984 pass
1984 pass
1985 d["d"] = "28"
1985 d["d"] = "28"
1986 return parsedate(date, extendeddateformats, d)[0]
1986 return parsedate(date, extendeddateformats, d)[0]
1987
1987
1988 date = date.strip()
1988 date = date.strip()
1989
1989
1990 if not date:
1990 if not date:
1991 raise Abort(_("dates cannot consist entirely of whitespace"))
1991 raise Abort(_("dates cannot consist entirely of whitespace"))
1992 elif date[0] == "<":
1992 elif date[0] == "<":
1993 if not date[1:]:
1993 if not date[1:]:
1994 raise Abort(_("invalid day spec, use '<DATE'"))
1994 raise Abort(_("invalid day spec, use '<DATE'"))
1995 when = upper(date[1:])
1995 when = upper(date[1:])
1996 return lambda x: x <= when
1996 return lambda x: x <= when
1997 elif date[0] == ">":
1997 elif date[0] == ">":
1998 if not date[1:]:
1998 if not date[1:]:
1999 raise Abort(_("invalid day spec, use '>DATE'"))
1999 raise Abort(_("invalid day spec, use '>DATE'"))
2000 when = lower(date[1:])
2000 when = lower(date[1:])
2001 return lambda x: x >= when
2001 return lambda x: x >= when
2002 elif date[0] == "-":
2002 elif date[0] == "-":
2003 try:
2003 try:
2004 days = int(date[1:])
2004 days = int(date[1:])
2005 except ValueError:
2005 except ValueError:
2006 raise Abort(_("invalid day spec: %s") % date[1:])
2006 raise Abort(_("invalid day spec: %s") % date[1:])
2007 if days < 0:
2007 if days < 0:
2008 raise Abort(_("%s must be nonnegative (see 'hg help dates')")
2008 raise Abort(_("%s must be nonnegative (see 'hg help dates')")
2009 % date[1:])
2009 % date[1:])
2010 when = makedate()[0] - days * 3600 * 24
2010 when = makedate()[0] - days * 3600 * 24
2011 return lambda x: x >= when
2011 return lambda x: x >= when
2012 elif " to " in date:
2012 elif " to " in date:
2013 a, b = date.split(" to ")
2013 a, b = date.split(" to ")
2014 start, stop = lower(a), upper(b)
2014 start, stop = lower(a), upper(b)
2015 return lambda x: x >= start and x <= stop
2015 return lambda x: x >= start and x <= stop
2016 else:
2016 else:
2017 start, stop = lower(date), upper(date)
2017 start, stop = lower(date), upper(date)
2018 return lambda x: x >= start and x <= stop
2018 return lambda x: x >= start and x <= stop
2019
2019
2020 def stringmatcher(pattern, casesensitive=True):
2020 def stringmatcher(pattern, casesensitive=True):
2021 """
2021 """
2022 accepts a string, possibly starting with 're:' or 'literal:' prefix.
2022 accepts a string, possibly starting with 're:' or 'literal:' prefix.
2023 returns the matcher name, pattern, and matcher function.
2023 returns the matcher name, pattern, and matcher function.
2024 missing or unknown prefixes are treated as literal matches.
2024 missing or unknown prefixes are treated as literal matches.
2025
2025
2026 helper for tests:
2026 helper for tests:
2027 >>> def test(pattern, *tests):
2027 >>> def test(pattern, *tests):
2028 ... kind, pattern, matcher = stringmatcher(pattern)
2028 ... kind, pattern, matcher = stringmatcher(pattern)
2029 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
2029 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
2030 >>> def itest(pattern, *tests):
2030 >>> def itest(pattern, *tests):
2031 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
2031 ... kind, pattern, matcher = stringmatcher(pattern, casesensitive=False)
2032 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
2032 ... return (kind, pattern, [bool(matcher(t)) for t in tests])
2033
2033
2034 exact matching (no prefix):
2034 exact matching (no prefix):
2035 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
2035 >>> test('abcdefg', 'abc', 'def', 'abcdefg')
2036 ('literal', 'abcdefg', [False, False, True])
2036 ('literal', 'abcdefg', [False, False, True])
2037
2037
2038 regex matching ('re:' prefix)
2038 regex matching ('re:' prefix)
2039 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
2039 >>> test('re:a.+b', 'nomatch', 'fooadef', 'fooadefbar')
2040 ('re', 'a.+b', [False, False, True])
2040 ('re', 'a.+b', [False, False, True])
2041
2041
2042 force exact matches ('literal:' prefix)
2042 force exact matches ('literal:' prefix)
2043 >>> test('literal:re:foobar', 'foobar', 're:foobar')
2043 >>> test('literal:re:foobar', 'foobar', 're:foobar')
2044 ('literal', 're:foobar', [False, True])
2044 ('literal', 're:foobar', [False, True])
2045
2045
2046 unknown prefixes are ignored and treated as literals
2046 unknown prefixes are ignored and treated as literals
2047 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
2047 >>> test('foo:bar', 'foo', 'bar', 'foo:bar')
2048 ('literal', 'foo:bar', [False, False, True])
2048 ('literal', 'foo:bar', [False, False, True])
2049
2049
2050 case insensitive regex matches
2050 case insensitive regex matches
2051 >>> itest('re:A.+b', 'nomatch', 'fooadef', 'fooadefBar')
2051 >>> itest('re:A.+b', 'nomatch', 'fooadef', 'fooadefBar')
2052 ('re', 'A.+b', [False, False, True])
2052 ('re', 'A.+b', [False, False, True])
2053
2053
2054 case insensitive literal matches
2054 case insensitive literal matches
2055 >>> itest('ABCDEFG', 'abc', 'def', 'abcdefg')
2055 >>> itest('ABCDEFG', 'abc', 'def', 'abcdefg')
2056 ('literal', 'ABCDEFG', [False, False, True])
2056 ('literal', 'ABCDEFG', [False, False, True])
2057 """
2057 """
2058 if pattern.startswith('re:'):
2058 if pattern.startswith('re:'):
2059 pattern = pattern[3:]
2059 pattern = pattern[3:]
2060 try:
2060 try:
2061 flags = 0
2061 flags = 0
2062 if not casesensitive:
2062 if not casesensitive:
2063 flags = remod.I
2063 flags = remod.I
2064 regex = remod.compile(pattern, flags)
2064 regex = remod.compile(pattern, flags)
2065 except remod.error as e:
2065 except remod.error as e:
2066 raise error.ParseError(_('invalid regular expression: %s')
2066 raise error.ParseError(_('invalid regular expression: %s')
2067 % e)
2067 % e)
2068 return 're', pattern, regex.search
2068 return 're', pattern, regex.search
2069 elif pattern.startswith('literal:'):
2069 elif pattern.startswith('literal:'):
2070 pattern = pattern[8:]
2070 pattern = pattern[8:]
2071
2071
2072 match = pattern.__eq__
2072 match = pattern.__eq__
2073
2073
2074 if not casesensitive:
2074 if not casesensitive:
2075 ipat = encoding.lower(pattern)
2075 ipat = encoding.lower(pattern)
2076 match = lambda s: ipat == encoding.lower(s)
2076 match = lambda s: ipat == encoding.lower(s)
2077 return 'literal', pattern, match
2077 return 'literal', pattern, match
2078
2078
2079 def shortuser(user):
2079 def shortuser(user):
2080 """Return a short representation of a user name or email address."""
2080 """Return a short representation of a user name or email address."""
2081 f = user.find('@')
2081 f = user.find('@')
2082 if f >= 0:
2082 if f >= 0:
2083 user = user[:f]
2083 user = user[:f]
2084 f = user.find('<')
2084 f = user.find('<')
2085 if f >= 0:
2085 if f >= 0:
2086 user = user[f + 1:]
2086 user = user[f + 1:]
2087 f = user.find(' ')
2087 f = user.find(' ')
2088 if f >= 0:
2088 if f >= 0:
2089 user = user[:f]
2089 user = user[:f]
2090 f = user.find('.')
2090 f = user.find('.')
2091 if f >= 0:
2091 if f >= 0:
2092 user = user[:f]
2092 user = user[:f]
2093 return user
2093 return user
2094
2094
2095 def emailuser(user):
2095 def emailuser(user):
2096 """Return the user portion of an email address."""
2096 """Return the user portion of an email address."""
2097 f = user.find('@')
2097 f = user.find('@')
2098 if f >= 0:
2098 if f >= 0:
2099 user = user[:f]
2099 user = user[:f]
2100 f = user.find('<')
2100 f = user.find('<')
2101 if f >= 0:
2101 if f >= 0:
2102 user = user[f + 1:]
2102 user = user[f + 1:]
2103 return user
2103 return user
2104
2104
2105 def email(author):
2105 def email(author):
2106 '''get email of author.'''
2106 '''get email of author.'''
2107 r = author.find('>')
2107 r = author.find('>')
2108 if r == -1:
2108 if r == -1:
2109 r = None
2109 r = None
2110 return author[author.find('<') + 1:r]
2110 return author[author.find('<') + 1:r]
2111
2111
2112 def ellipsis(text, maxlength=400):
2112 def ellipsis(text, maxlength=400):
2113 """Trim string to at most maxlength (default: 400) columns in display."""
2113 """Trim string to at most maxlength (default: 400) columns in display."""
2114 return encoding.trim(text, maxlength, ellipsis='...')
2114 return encoding.trim(text, maxlength, ellipsis='...')
2115
2115
2116 def unitcountfn(*unittable):
2116 def unitcountfn(*unittable):
2117 '''return a function that renders a readable count of some quantity'''
2117 '''return a function that renders a readable count of some quantity'''
2118
2118
2119 def go(count):
2119 def go(count):
2120 for multiplier, divisor, format in unittable:
2120 for multiplier, divisor, format in unittable:
2121 if count >= divisor * multiplier:
2121 if count >= divisor * multiplier:
2122 return format % (count / float(divisor))
2122 return format % (count / float(divisor))
2123 return unittable[-1][2] % count
2123 return unittable[-1][2] % count
2124
2124
2125 return go
2125 return go
2126
2126
2127 bytecount = unitcountfn(
2127 bytecount = unitcountfn(
2128 (100, 1 << 30, _('%.0f GB')),
2128 (100, 1 << 30, _('%.0f GB')),
2129 (10, 1 << 30, _('%.1f GB')),
2129 (10, 1 << 30, _('%.1f GB')),
2130 (1, 1 << 30, _('%.2f GB')),
2130 (1, 1 << 30, _('%.2f GB')),
2131 (100, 1 << 20, _('%.0f MB')),
2131 (100, 1 << 20, _('%.0f MB')),
2132 (10, 1 << 20, _('%.1f MB')),
2132 (10, 1 << 20, _('%.1f MB')),
2133 (1, 1 << 20, _('%.2f MB')),
2133 (1, 1 << 20, _('%.2f MB')),
2134 (100, 1 << 10, _('%.0f KB')),
2134 (100, 1 << 10, _('%.0f KB')),
2135 (10, 1 << 10, _('%.1f KB')),
2135 (10, 1 << 10, _('%.1f KB')),
2136 (1, 1 << 10, _('%.2f KB')),
2136 (1, 1 << 10, _('%.2f KB')),
2137 (1, 1, _('%.0f bytes')),
2137 (1, 1, _('%.0f bytes')),
2138 )
2138 )
2139
2139
2140 def uirepr(s):
2140 def uirepr(s):
2141 # Avoid double backslash in Windows path repr()
2141 # Avoid double backslash in Windows path repr()
2142 return repr(s).replace('\\\\', '\\')
2142 return repr(s).replace('\\\\', '\\')
2143
2143
2144 # delay import of textwrap
2144 # delay import of textwrap
2145 def MBTextWrapper(**kwargs):
2145 def MBTextWrapper(**kwargs):
2146 class tw(textwrap.TextWrapper):
2146 class tw(textwrap.TextWrapper):
2147 """
2147 """
2148 Extend TextWrapper for width-awareness.
2148 Extend TextWrapper for width-awareness.
2149
2149
2150 Neither number of 'bytes' in any encoding nor 'characters' is
2150 Neither number of 'bytes' in any encoding nor 'characters' is
2151 appropriate to calculate terminal columns for specified string.
2151 appropriate to calculate terminal columns for specified string.
2152
2152
2153 Original TextWrapper implementation uses built-in 'len()' directly,
2153 Original TextWrapper implementation uses built-in 'len()' directly,
2154 so overriding is needed to use width information of each characters.
2154 so overriding is needed to use width information of each characters.
2155
2155
2156 In addition, characters classified into 'ambiguous' width are
2156 In addition, characters classified into 'ambiguous' width are
2157 treated as wide in East Asian area, but as narrow in other.
2157 treated as wide in East Asian area, but as narrow in other.
2158
2158
2159 This requires use decision to determine width of such characters.
2159 This requires use decision to determine width of such characters.
2160 """
2160 """
2161 def _cutdown(self, ucstr, space_left):
2161 def _cutdown(self, ucstr, space_left):
2162 l = 0
2162 l = 0
2163 colwidth = encoding.ucolwidth
2163 colwidth = encoding.ucolwidth
2164 for i in xrange(len(ucstr)):
2164 for i in xrange(len(ucstr)):
2165 l += colwidth(ucstr[i])
2165 l += colwidth(ucstr[i])
2166 if space_left < l:
2166 if space_left < l:
2167 return (ucstr[:i], ucstr[i:])
2167 return (ucstr[:i], ucstr[i:])
2168 return ucstr, ''
2168 return ucstr, ''
2169
2169
2170 # overriding of base class
2170 # overriding of base class
2171 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
2171 def _handle_long_word(self, reversed_chunks, cur_line, cur_len, width):
2172 space_left = max(width - cur_len, 1)
2172 space_left = max(width - cur_len, 1)
2173
2173
2174 if self.break_long_words:
2174 if self.break_long_words:
2175 cut, res = self._cutdown(reversed_chunks[-1], space_left)
2175 cut, res = self._cutdown(reversed_chunks[-1], space_left)
2176 cur_line.append(cut)
2176 cur_line.append(cut)
2177 reversed_chunks[-1] = res
2177 reversed_chunks[-1] = res
2178 elif not cur_line:
2178 elif not cur_line:
2179 cur_line.append(reversed_chunks.pop())
2179 cur_line.append(reversed_chunks.pop())
2180
2180
2181 # this overriding code is imported from TextWrapper of Python 2.6
2181 # this overriding code is imported from TextWrapper of Python 2.6
2182 # to calculate columns of string by 'encoding.ucolwidth()'
2182 # to calculate columns of string by 'encoding.ucolwidth()'
2183 def _wrap_chunks(self, chunks):
2183 def _wrap_chunks(self, chunks):
2184 colwidth = encoding.ucolwidth
2184 colwidth = encoding.ucolwidth
2185
2185
2186 lines = []
2186 lines = []
2187 if self.width <= 0:
2187 if self.width <= 0:
2188 raise ValueError("invalid width %r (must be > 0)" % self.width)
2188 raise ValueError("invalid width %r (must be > 0)" % self.width)
2189
2189
2190 # Arrange in reverse order so items can be efficiently popped
2190 # Arrange in reverse order so items can be efficiently popped
2191 # from a stack of chucks.
2191 # from a stack of chucks.
2192 chunks.reverse()
2192 chunks.reverse()
2193
2193
2194 while chunks:
2194 while chunks:
2195
2195
2196 # Start the list of chunks that will make up the current line.
2196 # Start the list of chunks that will make up the current line.
2197 # cur_len is just the length of all the chunks in cur_line.
2197 # cur_len is just the length of all the chunks in cur_line.
2198 cur_line = []
2198 cur_line = []
2199 cur_len = 0
2199 cur_len = 0
2200
2200
2201 # Figure out which static string will prefix this line.
2201 # Figure out which static string will prefix this line.
2202 if lines:
2202 if lines:
2203 indent = self.subsequent_indent
2203 indent = self.subsequent_indent
2204 else:
2204 else:
2205 indent = self.initial_indent
2205 indent = self.initial_indent
2206
2206
2207 # Maximum width for this line.
2207 # Maximum width for this line.
2208 width = self.width - len(indent)
2208 width = self.width - len(indent)
2209
2209
2210 # First chunk on line is whitespace -- drop it, unless this
2210 # First chunk on line is whitespace -- drop it, unless this
2211 # is the very beginning of the text (i.e. no lines started yet).
2211 # is the very beginning of the text (i.e. no lines started yet).
2212 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
2212 if self.drop_whitespace and chunks[-1].strip() == '' and lines:
2213 del chunks[-1]
2213 del chunks[-1]
2214
2214
2215 while chunks:
2215 while chunks:
2216 l = colwidth(chunks[-1])
2216 l = colwidth(chunks[-1])
2217
2217
2218 # Can at least squeeze this chunk onto the current line.
2218 # Can at least squeeze this chunk onto the current line.
2219 if cur_len + l <= width:
2219 if cur_len + l <= width:
2220 cur_line.append(chunks.pop())
2220 cur_line.append(chunks.pop())
2221 cur_len += l
2221 cur_len += l
2222
2222
2223 # Nope, this line is full.
2223 # Nope, this line is full.
2224 else:
2224 else:
2225 break
2225 break
2226
2226
2227 # The current line is full, and the next chunk is too big to
2227 # The current line is full, and the next chunk is too big to
2228 # fit on *any* line (not just this one).
2228 # fit on *any* line (not just this one).
2229 if chunks and colwidth(chunks[-1]) > width:
2229 if chunks and colwidth(chunks[-1]) > width:
2230 self._handle_long_word(chunks, cur_line, cur_len, width)
2230 self._handle_long_word(chunks, cur_line, cur_len, width)
2231
2231
2232 # If the last chunk on this line is all whitespace, drop it.
2232 # If the last chunk on this line is all whitespace, drop it.
2233 if (self.drop_whitespace and
2233 if (self.drop_whitespace and
2234 cur_line and cur_line[-1].strip() == ''):
2234 cur_line and cur_line[-1].strip() == ''):
2235 del cur_line[-1]
2235 del cur_line[-1]
2236
2236
2237 # Convert current line back to a string and store it in list
2237 # Convert current line back to a string and store it in list
2238 # of all lines (return value).
2238 # of all lines (return value).
2239 if cur_line:
2239 if cur_line:
2240 lines.append(indent + ''.join(cur_line))
2240 lines.append(indent + ''.join(cur_line))
2241
2241
2242 return lines
2242 return lines
2243
2243
2244 global MBTextWrapper
2244 global MBTextWrapper
2245 MBTextWrapper = tw
2245 MBTextWrapper = tw
2246 return tw(**kwargs)
2246 return tw(**kwargs)
2247
2247
2248 def wrap(line, width, initindent='', hangindent=''):
2248 def wrap(line, width, initindent='', hangindent=''):
2249 maxindent = max(len(hangindent), len(initindent))
2249 maxindent = max(len(hangindent), len(initindent))
2250 if width <= maxindent:
2250 if width <= maxindent:
2251 # adjust for weird terminal size
2251 # adjust for weird terminal size
2252 width = max(78, maxindent + 1)
2252 width = max(78, maxindent + 1)
2253 line = line.decode(encoding.encoding, encoding.encodingmode)
2253 line = line.decode(encoding.encoding, encoding.encodingmode)
2254 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
2254 initindent = initindent.decode(encoding.encoding, encoding.encodingmode)
2255 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
2255 hangindent = hangindent.decode(encoding.encoding, encoding.encodingmode)
2256 wrapper = MBTextWrapper(width=width,
2256 wrapper = MBTextWrapper(width=width,
2257 initial_indent=initindent,
2257 initial_indent=initindent,
2258 subsequent_indent=hangindent)
2258 subsequent_indent=hangindent)
2259 return wrapper.fill(line).encode(encoding.encoding)
2259 return wrapper.fill(line).encode(encoding.encoding)
2260
2260
2261 if (pyplatform.python_implementation() == 'CPython' and
2261 if (pyplatform.python_implementation() == 'CPython' and
2262 sys.version_info < (3, 0)):
2262 sys.version_info < (3, 0)):
2263 # There is an issue in CPython that some IO methods do not handle EINTR
2263 # There is an issue in CPython that some IO methods do not handle EINTR
2264 # correctly. The following table shows what CPython version (and functions)
2264 # correctly. The following table shows what CPython version (and functions)
2265 # are affected (buggy: has the EINTR bug, okay: otherwise):
2265 # are affected (buggy: has the EINTR bug, okay: otherwise):
2266 #
2266 #
2267 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2267 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2268 # --------------------------------------------------
2268 # --------------------------------------------------
2269 # fp.__iter__ | buggy | buggy | okay
2269 # fp.__iter__ | buggy | buggy | okay
2270 # fp.read* | buggy | okay [1] | okay
2270 # fp.read* | buggy | okay [1] | okay
2271 #
2271 #
2272 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2272 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2273 #
2273 #
2274 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2274 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2275 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2275 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2276 #
2276 #
2277 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2277 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2278 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2278 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2279 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2279 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2280 # fp.__iter__ but not other fp.read* methods.
2280 # fp.__iter__ but not other fp.read* methods.
2281 #
2281 #
2282 # On modern systems like Linux, the "read" syscall cannot be interrupted
2282 # On modern systems like Linux, the "read" syscall cannot be interrupted
2283 # when reading "fast" files like on-disk files. So the EINTR issue only
2283 # when reading "fast" files like on-disk files. So the EINTR issue only
2284 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2284 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2285 # files approximately as "fast" files and use the fast (unsafe) code path,
2285 # files approximately as "fast" files and use the fast (unsafe) code path,
2286 # to minimize the performance impact.
2286 # to minimize the performance impact.
2287 if sys.version_info >= (2, 7, 4):
2287 if sys.version_info >= (2, 7, 4):
2288 # fp.readline deals with EINTR correctly, use it as a workaround.
2288 # fp.readline deals with EINTR correctly, use it as a workaround.
2289 def _safeiterfile(fp):
2289 def _safeiterfile(fp):
2290 return iter(fp.readline, '')
2290 return iter(fp.readline, '')
2291 else:
2291 else:
2292 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2292 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2293 # note: this may block longer than necessary because of bufsize.
2293 # note: this may block longer than necessary because of bufsize.
2294 def _safeiterfile(fp, bufsize=4096):
2294 def _safeiterfile(fp, bufsize=4096):
2295 fd = fp.fileno()
2295 fd = fp.fileno()
2296 line = ''
2296 line = ''
2297 while True:
2297 while True:
2298 try:
2298 try:
2299 buf = os.read(fd, bufsize)
2299 buf = os.read(fd, bufsize)
2300 except OSError as ex:
2300 except OSError as ex:
2301 # os.read only raises EINTR before any data is read
2301 # os.read only raises EINTR before any data is read
2302 if ex.errno == errno.EINTR:
2302 if ex.errno == errno.EINTR:
2303 continue
2303 continue
2304 else:
2304 else:
2305 raise
2305 raise
2306 line += buf
2306 line += buf
2307 if '\n' in buf:
2307 if '\n' in buf:
2308 splitted = line.splitlines(True)
2308 splitted = line.splitlines(True)
2309 line = ''
2309 line = ''
2310 for l in splitted:
2310 for l in splitted:
2311 if l[-1] == '\n':
2311 if l[-1] == '\n':
2312 yield l
2312 yield l
2313 else:
2313 else:
2314 line = l
2314 line = l
2315 if not buf:
2315 if not buf:
2316 break
2316 break
2317 if line:
2317 if line:
2318 yield line
2318 yield line
2319
2319
2320 def iterfile(fp):
2320 def iterfile(fp):
2321 fastpath = True
2321 fastpath = True
2322 if type(fp) is file:
2322 if type(fp) is file:
2323 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2323 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2324 if fastpath:
2324 if fastpath:
2325 return fp
2325 return fp
2326 else:
2326 else:
2327 return _safeiterfile(fp)
2327 return _safeiterfile(fp)
2328 else:
2328 else:
2329 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2329 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2330 def iterfile(fp):
2330 def iterfile(fp):
2331 return fp
2331 return fp
2332
2332
2333 def iterlines(iterator):
2333 def iterlines(iterator):
2334 for chunk in iterator:
2334 for chunk in iterator:
2335 for line in chunk.splitlines():
2335 for line in chunk.splitlines():
2336 yield line
2336 yield line
2337
2337
2338 def expandpath(path):
2338 def expandpath(path):
2339 return os.path.expanduser(os.path.expandvars(path))
2339 return os.path.expanduser(os.path.expandvars(path))
2340
2340
2341 def hgcmd():
2341 def hgcmd():
2342 """Return the command used to execute current hg
2342 """Return the command used to execute current hg
2343
2343
2344 This is different from hgexecutable() because on Windows we want
2344 This is different from hgexecutable() because on Windows we want
2345 to avoid things opening new shell windows like batch files, so we
2345 to avoid things opening new shell windows like batch files, so we
2346 get either the python call or current executable.
2346 get either the python call or current executable.
2347 """
2347 """
2348 if mainfrozen():
2348 if mainfrozen():
2349 if getattr(sys, 'frozen', None) == 'macosx_app':
2349 if getattr(sys, 'frozen', None) == 'macosx_app':
2350 # Env variable set by py2app
2350 # Env variable set by py2app
2351 return [encoding.environ['EXECUTABLEPATH']]
2351 return [encoding.environ['EXECUTABLEPATH']]
2352 else:
2352 else:
2353 return [pycompat.sysexecutable]
2353 return [pycompat.sysexecutable]
2354 return gethgcmd()
2354 return gethgcmd()
2355
2355
2356 def rundetached(args, condfn):
2356 def rundetached(args, condfn):
2357 """Execute the argument list in a detached process.
2357 """Execute the argument list in a detached process.
2358
2358
2359 condfn is a callable which is called repeatedly and should return
2359 condfn is a callable which is called repeatedly and should return
2360 True once the child process is known to have started successfully.
2360 True once the child process is known to have started successfully.
2361 At this point, the child process PID is returned. If the child
2361 At this point, the child process PID is returned. If the child
2362 process fails to start or finishes before condfn() evaluates to
2362 process fails to start or finishes before condfn() evaluates to
2363 True, return -1.
2363 True, return -1.
2364 """
2364 """
2365 # Windows case is easier because the child process is either
2365 # Windows case is easier because the child process is either
2366 # successfully starting and validating the condition or exiting
2366 # successfully starting and validating the condition or exiting
2367 # on failure. We just poll on its PID. On Unix, if the child
2367 # on failure. We just poll on its PID. On Unix, if the child
2368 # process fails to start, it will be left in a zombie state until
2368 # process fails to start, it will be left in a zombie state until
2369 # the parent wait on it, which we cannot do since we expect a long
2369 # the parent wait on it, which we cannot do since we expect a long
2370 # running process on success. Instead we listen for SIGCHLD telling
2370 # running process on success. Instead we listen for SIGCHLD telling
2371 # us our child process terminated.
2371 # us our child process terminated.
2372 terminated = set()
2372 terminated = set()
2373 def handler(signum, frame):
2373 def handler(signum, frame):
2374 terminated.add(os.wait())
2374 terminated.add(os.wait())
2375 prevhandler = None
2375 prevhandler = None
2376 SIGCHLD = getattr(signal, 'SIGCHLD', None)
2376 SIGCHLD = getattr(signal, 'SIGCHLD', None)
2377 if SIGCHLD is not None:
2377 if SIGCHLD is not None:
2378 prevhandler = signal.signal(SIGCHLD, handler)
2378 prevhandler = signal.signal(SIGCHLD, handler)
2379 try:
2379 try:
2380 pid = spawndetached(args)
2380 pid = spawndetached(args)
2381 while not condfn():
2381 while not condfn():
2382 if ((pid in terminated or not testpid(pid))
2382 if ((pid in terminated or not testpid(pid))
2383 and not condfn()):
2383 and not condfn()):
2384 return -1
2384 return -1
2385 time.sleep(0.1)
2385 time.sleep(0.1)
2386 return pid
2386 return pid
2387 finally:
2387 finally:
2388 if prevhandler is not None:
2388 if prevhandler is not None:
2389 signal.signal(signal.SIGCHLD, prevhandler)
2389 signal.signal(signal.SIGCHLD, prevhandler)
2390
2390
2391 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2391 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2392 """Return the result of interpolating items in the mapping into string s.
2392 """Return the result of interpolating items in the mapping into string s.
2393
2393
2394 prefix is a single character string, or a two character string with
2394 prefix is a single character string, or a two character string with
2395 a backslash as the first character if the prefix needs to be escaped in
2395 a backslash as the first character if the prefix needs to be escaped in
2396 a regular expression.
2396 a regular expression.
2397
2397
2398 fn is an optional function that will be applied to the replacement text
2398 fn is an optional function that will be applied to the replacement text
2399 just before replacement.
2399 just before replacement.
2400
2400
2401 escape_prefix is an optional flag that allows using doubled prefix for
2401 escape_prefix is an optional flag that allows using doubled prefix for
2402 its escaping.
2402 its escaping.
2403 """
2403 """
2404 fn = fn or (lambda s: s)
2404 fn = fn or (lambda s: s)
2405 patterns = '|'.join(mapping.keys())
2405 patterns = '|'.join(mapping.keys())
2406 if escape_prefix:
2406 if escape_prefix:
2407 patterns += '|' + prefix
2407 patterns += '|' + prefix
2408 if len(prefix) > 1:
2408 if len(prefix) > 1:
2409 prefix_char = prefix[1:]
2409 prefix_char = prefix[1:]
2410 else:
2410 else:
2411 prefix_char = prefix
2411 prefix_char = prefix
2412 mapping[prefix_char] = prefix_char
2412 mapping[prefix_char] = prefix_char
2413 r = remod.compile(r'%s(%s)' % (prefix, patterns))
2413 r = remod.compile(r'%s(%s)' % (prefix, patterns))
2414 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2414 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2415
2415
2416 def getport(port):
2416 def getport(port):
2417 """Return the port for a given network service.
2417 """Return the port for a given network service.
2418
2418
2419 If port is an integer, it's returned as is. If it's a string, it's
2419 If port is an integer, it's returned as is. If it's a string, it's
2420 looked up using socket.getservbyname(). If there's no matching
2420 looked up using socket.getservbyname(). If there's no matching
2421 service, error.Abort is raised.
2421 service, error.Abort is raised.
2422 """
2422 """
2423 try:
2423 try:
2424 return int(port)
2424 return int(port)
2425 except ValueError:
2425 except ValueError:
2426 pass
2426 pass
2427
2427
2428 try:
2428 try:
2429 return socket.getservbyname(port)
2429 return socket.getservbyname(port)
2430 except socket.error:
2430 except socket.error:
2431 raise Abort(_("no port number associated with service '%s'") % port)
2431 raise Abort(_("no port number associated with service '%s'") % port)
2432
2432
2433 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
2433 _booleans = {'1': True, 'yes': True, 'true': True, 'on': True, 'always': True,
2434 '0': False, 'no': False, 'false': False, 'off': False,
2434 '0': False, 'no': False, 'false': False, 'off': False,
2435 'never': False}
2435 'never': False}
2436
2436
2437 def parsebool(s):
2437 def parsebool(s):
2438 """Parse s into a boolean.
2438 """Parse s into a boolean.
2439
2439
2440 If s is not a valid boolean, returns None.
2440 If s is not a valid boolean, returns None.
2441 """
2441 """
2442 return _booleans.get(s.lower(), None)
2442 return _booleans.get(s.lower(), None)
2443
2443
2444 _hextochr = dict((a + b, chr(int(a + b, 16)))
2444 _hextochr = dict((a + b, chr(int(a + b, 16)))
2445 for a in string.hexdigits for b in string.hexdigits)
2445 for a in string.hexdigits for b in string.hexdigits)
2446
2446
2447 class url(object):
2447 class url(object):
2448 r"""Reliable URL parser.
2448 r"""Reliable URL parser.
2449
2449
2450 This parses URLs and provides attributes for the following
2450 This parses URLs and provides attributes for the following
2451 components:
2451 components:
2452
2452
2453 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2453 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2454
2454
2455 Missing components are set to None. The only exception is
2455 Missing components are set to None. The only exception is
2456 fragment, which is set to '' if present but empty.
2456 fragment, which is set to '' if present but empty.
2457
2457
2458 If parsefragment is False, fragment is included in query. If
2458 If parsefragment is False, fragment is included in query. If
2459 parsequery is False, query is included in path. If both are
2459 parsequery is False, query is included in path. If both are
2460 False, both fragment and query are included in path.
2460 False, both fragment and query are included in path.
2461
2461
2462 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2462 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2463
2463
2464 Note that for backward compatibility reasons, bundle URLs do not
2464 Note that for backward compatibility reasons, bundle URLs do not
2465 take host names. That means 'bundle://../' has a path of '../'.
2465 take host names. That means 'bundle://../' has a path of '../'.
2466
2466
2467 Examples:
2467 Examples:
2468
2468
2469 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
2469 >>> url('http://www.ietf.org/rfc/rfc2396.txt')
2470 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2470 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2471 >>> url('ssh://[::1]:2200//home/joe/repo')
2471 >>> url('ssh://[::1]:2200//home/joe/repo')
2472 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2472 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2473 >>> url('file:///home/joe/repo')
2473 >>> url('file:///home/joe/repo')
2474 <url scheme: 'file', path: '/home/joe/repo'>
2474 <url scheme: 'file', path: '/home/joe/repo'>
2475 >>> url('file:///c:/temp/foo/')
2475 >>> url('file:///c:/temp/foo/')
2476 <url scheme: 'file', path: 'c:/temp/foo/'>
2476 <url scheme: 'file', path: 'c:/temp/foo/'>
2477 >>> url('bundle:foo')
2477 >>> url('bundle:foo')
2478 <url scheme: 'bundle', path: 'foo'>
2478 <url scheme: 'bundle', path: 'foo'>
2479 >>> url('bundle://../foo')
2479 >>> url('bundle://../foo')
2480 <url scheme: 'bundle', path: '../foo'>
2480 <url scheme: 'bundle', path: '../foo'>
2481 >>> url(r'c:\foo\bar')
2481 >>> url(r'c:\foo\bar')
2482 <url path: 'c:\\foo\\bar'>
2482 <url path: 'c:\\foo\\bar'>
2483 >>> url(r'\\blah\blah\blah')
2483 >>> url(r'\\blah\blah\blah')
2484 <url path: '\\\\blah\\blah\\blah'>
2484 <url path: '\\\\blah\\blah\\blah'>
2485 >>> url(r'\\blah\blah\blah#baz')
2485 >>> url(r'\\blah\blah\blah#baz')
2486 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2486 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2487 >>> url(r'file:///C:\users\me')
2487 >>> url(r'file:///C:\users\me')
2488 <url scheme: 'file', path: 'C:\\users\\me'>
2488 <url scheme: 'file', path: 'C:\\users\\me'>
2489
2489
2490 Authentication credentials:
2490 Authentication credentials:
2491
2491
2492 >>> url('ssh://joe:xyz@x/repo')
2492 >>> url('ssh://joe:xyz@x/repo')
2493 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2493 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
2494 >>> url('ssh://joe@x/repo')
2494 >>> url('ssh://joe@x/repo')
2495 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2495 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
2496
2496
2497 Query strings and fragments:
2497 Query strings and fragments:
2498
2498
2499 >>> url('http://host/a?b#c')
2499 >>> url('http://host/a?b#c')
2500 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2500 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
2501 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
2501 >>> url('http://host/a?b#c', parsequery=False, parsefragment=False)
2502 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2502 <url scheme: 'http', host: 'host', path: 'a?b#c'>
2503
2503
2504 Empty path:
2504 Empty path:
2505
2505
2506 >>> url('')
2506 >>> url('')
2507 <url path: ''>
2507 <url path: ''>
2508 >>> url('#a')
2508 >>> url('#a')
2509 <url path: '', fragment: 'a'>
2509 <url path: '', fragment: 'a'>
2510 >>> url('http://host/')
2510 >>> url('http://host/')
2511 <url scheme: 'http', host: 'host', path: ''>
2511 <url scheme: 'http', host: 'host', path: ''>
2512 >>> url('http://host/#a')
2512 >>> url('http://host/#a')
2513 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
2513 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
2514
2514
2515 Only scheme:
2515 Only scheme:
2516
2516
2517 >>> url('http:')
2517 >>> url('http:')
2518 <url scheme: 'http'>
2518 <url scheme: 'http'>
2519 """
2519 """
2520
2520
2521 _safechars = "!~*'()+"
2521 _safechars = "!~*'()+"
2522 _safepchars = "/!~*'()+:\\"
2522 _safepchars = "/!~*'()+:\\"
2523 _matchscheme = remod.compile('^[a-zA-Z0-9+.\\-]+:').match
2523 _matchscheme = remod.compile('^[a-zA-Z0-9+.\\-]+:').match
2524
2524
2525 def __init__(self, path, parsequery=True, parsefragment=True):
2525 def __init__(self, path, parsequery=True, parsefragment=True):
2526 # We slowly chomp away at path until we have only the path left
2526 # We slowly chomp away at path until we have only the path left
2527 self.scheme = self.user = self.passwd = self.host = None
2527 self.scheme = self.user = self.passwd = self.host = None
2528 self.port = self.path = self.query = self.fragment = None
2528 self.port = self.path = self.query = self.fragment = None
2529 self._localpath = True
2529 self._localpath = True
2530 self._hostport = ''
2530 self._hostport = ''
2531 self._origpath = path
2531 self._origpath = path
2532
2532
2533 if parsefragment and '#' in path:
2533 if parsefragment and '#' in path:
2534 path, self.fragment = path.split('#', 1)
2534 path, self.fragment = path.split('#', 1)
2535
2535
2536 # special case for Windows drive letters and UNC paths
2536 # special case for Windows drive letters and UNC paths
2537 if hasdriveletter(path) or path.startswith('\\\\'):
2537 if hasdriveletter(path) or path.startswith('\\\\'):
2538 self.path = path
2538 self.path = path
2539 return
2539 return
2540
2540
2541 # For compatibility reasons, we can't handle bundle paths as
2541 # For compatibility reasons, we can't handle bundle paths as
2542 # normal URLS
2542 # normal URLS
2543 if path.startswith('bundle:'):
2543 if path.startswith('bundle:'):
2544 self.scheme = 'bundle'
2544 self.scheme = 'bundle'
2545 path = path[7:]
2545 path = path[7:]
2546 if path.startswith('//'):
2546 if path.startswith('//'):
2547 path = path[2:]
2547 path = path[2:]
2548 self.path = path
2548 self.path = path
2549 return
2549 return
2550
2550
2551 if self._matchscheme(path):
2551 if self._matchscheme(path):
2552 parts = path.split(':', 1)
2552 parts = path.split(':', 1)
2553 if parts[0]:
2553 if parts[0]:
2554 self.scheme, path = parts
2554 self.scheme, path = parts
2555 self._localpath = False
2555 self._localpath = False
2556
2556
2557 if not path:
2557 if not path:
2558 path = None
2558 path = None
2559 if self._localpath:
2559 if self._localpath:
2560 self.path = ''
2560 self.path = ''
2561 return
2561 return
2562 else:
2562 else:
2563 if self._localpath:
2563 if self._localpath:
2564 self.path = path
2564 self.path = path
2565 return
2565 return
2566
2566
2567 if parsequery and '?' in path:
2567 if parsequery and '?' in path:
2568 path, self.query = path.split('?', 1)
2568 path, self.query = path.split('?', 1)
2569 if not path:
2569 if not path:
2570 path = None
2570 path = None
2571 if not self.query:
2571 if not self.query:
2572 self.query = None
2572 self.query = None
2573
2573
2574 # // is required to specify a host/authority
2574 # // is required to specify a host/authority
2575 if path and path.startswith('//'):
2575 if path and path.startswith('//'):
2576 parts = path[2:].split('/', 1)
2576 parts = path[2:].split('/', 1)
2577 if len(parts) > 1:
2577 if len(parts) > 1:
2578 self.host, path = parts
2578 self.host, path = parts
2579 else:
2579 else:
2580 self.host = parts[0]
2580 self.host = parts[0]
2581 path = None
2581 path = None
2582 if not self.host:
2582 if not self.host:
2583 self.host = None
2583 self.host = None
2584 # path of file:///d is /d
2584 # path of file:///d is /d
2585 # path of file:///d:/ is d:/, not /d:/
2585 # path of file:///d:/ is d:/, not /d:/
2586 if path and not hasdriveletter(path):
2586 if path and not hasdriveletter(path):
2587 path = '/' + path
2587 path = '/' + path
2588
2588
2589 if self.host and '@' in self.host:
2589 if self.host and '@' in self.host:
2590 self.user, self.host = self.host.rsplit('@', 1)
2590 self.user, self.host = self.host.rsplit('@', 1)
2591 if ':' in self.user:
2591 if ':' in self.user:
2592 self.user, self.passwd = self.user.split(':', 1)
2592 self.user, self.passwd = self.user.split(':', 1)
2593 if not self.host:
2593 if not self.host:
2594 self.host = None
2594 self.host = None
2595
2595
2596 # Don't split on colons in IPv6 addresses without ports
2596 # Don't split on colons in IPv6 addresses without ports
2597 if (self.host and ':' in self.host and
2597 if (self.host and ':' in self.host and
2598 not (self.host.startswith('[') and self.host.endswith(']'))):
2598 not (self.host.startswith('[') and self.host.endswith(']'))):
2599 self._hostport = self.host
2599 self._hostport = self.host
2600 self.host, self.port = self.host.rsplit(':', 1)
2600 self.host, self.port = self.host.rsplit(':', 1)
2601 if not self.host:
2601 if not self.host:
2602 self.host = None
2602 self.host = None
2603
2603
2604 if (self.host and self.scheme == 'file' and
2604 if (self.host and self.scheme == 'file' and
2605 self.host not in ('localhost', '127.0.0.1', '[::1]')):
2605 self.host not in ('localhost', '127.0.0.1', '[::1]')):
2606 raise Abort(_('file:// URLs can only refer to localhost'))
2606 raise Abort(_('file:// URLs can only refer to localhost'))
2607
2607
2608 self.path = path
2608 self.path = path
2609
2609
2610 # leave the query string escaped
2610 # leave the query string escaped
2611 for a in ('user', 'passwd', 'host', 'port',
2611 for a in ('user', 'passwd', 'host', 'port',
2612 'path', 'fragment'):
2612 'path', 'fragment'):
2613 v = getattr(self, a)
2613 v = getattr(self, a)
2614 if v is not None:
2614 if v is not None:
2615 setattr(self, a, pycompat.urlunquote(v))
2615 setattr(self, a, pycompat.urlunquote(v))
2616
2616
2617 def __repr__(self):
2617 def __repr__(self):
2618 attrs = []
2618 attrs = []
2619 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
2619 for a in ('scheme', 'user', 'passwd', 'host', 'port', 'path',
2620 'query', 'fragment'):
2620 'query', 'fragment'):
2621 v = getattr(self, a)
2621 v = getattr(self, a)
2622 if v is not None:
2622 if v is not None:
2623 attrs.append('%s: %r' % (a, v))
2623 attrs.append('%s: %r' % (a, v))
2624 return '<url %s>' % ', '.join(attrs)
2624 return '<url %s>' % ', '.join(attrs)
2625
2625
2626 def __str__(self):
2626 def __str__(self):
2627 r"""Join the URL's components back into a URL string.
2627 r"""Join the URL's components back into a URL string.
2628
2628
2629 Examples:
2629 Examples:
2630
2630
2631 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
2631 >>> str(url('http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
2632 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
2632 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
2633 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
2633 >>> str(url('http://user:pw@host:80/?foo=bar&baz=42'))
2634 'http://user:pw@host:80/?foo=bar&baz=42'
2634 'http://user:pw@host:80/?foo=bar&baz=42'
2635 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
2635 >>> str(url('http://user:pw@host:80/?foo=bar%3dbaz'))
2636 'http://user:pw@host:80/?foo=bar%3dbaz'
2636 'http://user:pw@host:80/?foo=bar%3dbaz'
2637 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
2637 >>> str(url('ssh://user:pw@[::1]:2200//home/joe#'))
2638 'ssh://user:pw@[::1]:2200//home/joe#'
2638 'ssh://user:pw@[::1]:2200//home/joe#'
2639 >>> str(url('http://localhost:80//'))
2639 >>> str(url('http://localhost:80//'))
2640 'http://localhost:80//'
2640 'http://localhost:80//'
2641 >>> str(url('http://localhost:80/'))
2641 >>> str(url('http://localhost:80/'))
2642 'http://localhost:80/'
2642 'http://localhost:80/'
2643 >>> str(url('http://localhost:80'))
2643 >>> str(url('http://localhost:80'))
2644 'http://localhost:80/'
2644 'http://localhost:80/'
2645 >>> str(url('bundle:foo'))
2645 >>> str(url('bundle:foo'))
2646 'bundle:foo'
2646 'bundle:foo'
2647 >>> str(url('bundle://../foo'))
2647 >>> str(url('bundle://../foo'))
2648 'bundle:../foo'
2648 'bundle:../foo'
2649 >>> str(url('path'))
2649 >>> str(url('path'))
2650 'path'
2650 'path'
2651 >>> str(url('file:///tmp/foo/bar'))
2651 >>> str(url('file:///tmp/foo/bar'))
2652 'file:///tmp/foo/bar'
2652 'file:///tmp/foo/bar'
2653 >>> str(url('file:///c:/tmp/foo/bar'))
2653 >>> str(url('file:///c:/tmp/foo/bar'))
2654 'file:///c:/tmp/foo/bar'
2654 'file:///c:/tmp/foo/bar'
2655 >>> print url(r'bundle:foo\bar')
2655 >>> print url(r'bundle:foo\bar')
2656 bundle:foo\bar
2656 bundle:foo\bar
2657 >>> print url(r'file:///D:\data\hg')
2657 >>> print url(r'file:///D:\data\hg')
2658 file:///D:\data\hg
2658 file:///D:\data\hg
2659 """
2659 """
2660 if self._localpath:
2660 if self._localpath:
2661 s = self.path
2661 s = self.path
2662 if self.scheme == 'bundle':
2662 if self.scheme == 'bundle':
2663 s = 'bundle:' + s
2663 s = 'bundle:' + s
2664 if self.fragment:
2664 if self.fragment:
2665 s += '#' + self.fragment
2665 s += '#' + self.fragment
2666 return s
2666 return s
2667
2667
2668 s = self.scheme + ':'
2668 s = self.scheme + ':'
2669 if self.user or self.passwd or self.host:
2669 if self.user or self.passwd or self.host:
2670 s += '//'
2670 s += '//'
2671 elif self.scheme and (not self.path or self.path.startswith('/')
2671 elif self.scheme and (not self.path or self.path.startswith('/')
2672 or hasdriveletter(self.path)):
2672 or hasdriveletter(self.path)):
2673 s += '//'
2673 s += '//'
2674 if hasdriveletter(self.path):
2674 if hasdriveletter(self.path):
2675 s += '/'
2675 s += '/'
2676 if self.user:
2676 if self.user:
2677 s += urlreq.quote(self.user, safe=self._safechars)
2677 s += urlreq.quote(self.user, safe=self._safechars)
2678 if self.passwd:
2678 if self.passwd:
2679 s += ':' + urlreq.quote(self.passwd, safe=self._safechars)
2679 s += ':' + urlreq.quote(self.passwd, safe=self._safechars)
2680 if self.user or self.passwd:
2680 if self.user or self.passwd:
2681 s += '@'
2681 s += '@'
2682 if self.host:
2682 if self.host:
2683 if not (self.host.startswith('[') and self.host.endswith(']')):
2683 if not (self.host.startswith('[') and self.host.endswith(']')):
2684 s += urlreq.quote(self.host)
2684 s += urlreq.quote(self.host)
2685 else:
2685 else:
2686 s += self.host
2686 s += self.host
2687 if self.port:
2687 if self.port:
2688 s += ':' + urlreq.quote(self.port)
2688 s += ':' + urlreq.quote(self.port)
2689 if self.host:
2689 if self.host:
2690 s += '/'
2690 s += '/'
2691 if self.path:
2691 if self.path:
2692 # TODO: similar to the query string, we should not unescape the
2692 # TODO: similar to the query string, we should not unescape the
2693 # path when we store it, the path might contain '%2f' = '/',
2693 # path when we store it, the path might contain '%2f' = '/',
2694 # which we should *not* escape.
2694 # which we should *not* escape.
2695 s += urlreq.quote(self.path, safe=self._safepchars)
2695 s += urlreq.quote(self.path, safe=self._safepchars)
2696 if self.query:
2696 if self.query:
2697 # we store the query in escaped form.
2697 # we store the query in escaped form.
2698 s += '?' + self.query
2698 s += '?' + self.query
2699 if self.fragment is not None:
2699 if self.fragment is not None:
2700 s += '#' + urlreq.quote(self.fragment, safe=self._safepchars)
2700 s += '#' + urlreq.quote(self.fragment, safe=self._safepchars)
2701 return s
2701 return s
2702
2702
2703 def authinfo(self):
2703 def authinfo(self):
2704 user, passwd = self.user, self.passwd
2704 user, passwd = self.user, self.passwd
2705 try:
2705 try:
2706 self.user, self.passwd = None, None
2706 self.user, self.passwd = None, None
2707 s = str(self)
2707 s = str(self)
2708 finally:
2708 finally:
2709 self.user, self.passwd = user, passwd
2709 self.user, self.passwd = user, passwd
2710 if not self.user:
2710 if not self.user:
2711 return (s, None)
2711 return (s, None)
2712 # authinfo[1] is passed to urllib2 password manager, and its
2712 # authinfo[1] is passed to urllib2 password manager, and its
2713 # URIs must not contain credentials. The host is passed in the
2713 # URIs must not contain credentials. The host is passed in the
2714 # URIs list because Python < 2.4.3 uses only that to search for
2714 # URIs list because Python < 2.4.3 uses only that to search for
2715 # a password.
2715 # a password.
2716 return (s, (None, (s, self.host),
2716 return (s, (None, (s, self.host),
2717 self.user, self.passwd or ''))
2717 self.user, self.passwd or ''))
2718
2718
2719 def isabs(self):
2719 def isabs(self):
2720 if self.scheme and self.scheme != 'file':
2720 if self.scheme and self.scheme != 'file':
2721 return True # remote URL
2721 return True # remote URL
2722 if hasdriveletter(self.path):
2722 if hasdriveletter(self.path):
2723 return True # absolute for our purposes - can't be joined()
2723 return True # absolute for our purposes - can't be joined()
2724 if self.path.startswith(r'\\'):
2724 if self.path.startswith(r'\\'):
2725 return True # Windows UNC path
2725 return True # Windows UNC path
2726 if self.path.startswith('/'):
2726 if self.path.startswith('/'):
2727 return True # POSIX-style
2727 return True # POSIX-style
2728 return False
2728 return False
2729
2729
2730 def localpath(self):
2730 def localpath(self):
2731 if self.scheme == 'file' or self.scheme == 'bundle':
2731 if self.scheme == 'file' or self.scheme == 'bundle':
2732 path = self.path or '/'
2732 path = self.path or '/'
2733 # For Windows, we need to promote hosts containing drive
2733 # For Windows, we need to promote hosts containing drive
2734 # letters to paths with drive letters.
2734 # letters to paths with drive letters.
2735 if hasdriveletter(self._hostport):
2735 if hasdriveletter(self._hostport):
2736 path = self._hostport + '/' + self.path
2736 path = self._hostport + '/' + self.path
2737 elif (self.host is not None and self.path
2737 elif (self.host is not None and self.path
2738 and not hasdriveletter(path)):
2738 and not hasdriveletter(path)):
2739 path = '/' + path
2739 path = '/' + path
2740 return path
2740 return path
2741 return self._origpath
2741 return self._origpath
2742
2742
2743 def islocal(self):
2743 def islocal(self):
2744 '''whether localpath will return something that posixfile can open'''
2744 '''whether localpath will return something that posixfile can open'''
2745 return (not self.scheme or self.scheme == 'file'
2745 return (not self.scheme or self.scheme == 'file'
2746 or self.scheme == 'bundle')
2746 or self.scheme == 'bundle')
2747
2747
2748 def hasscheme(path):
2748 def hasscheme(path):
2749 return bool(url(path).scheme)
2749 return bool(url(path).scheme)
2750
2750
2751 def hasdriveletter(path):
2751 def hasdriveletter(path):
2752 return path and path[1:2] == ':' and path[0:1].isalpha()
2752 return path and path[1:2] == ':' and path[0:1].isalpha()
2753
2753
2754 def urllocalpath(path):
2754 def urllocalpath(path):
2755 return url(path, parsequery=False, parsefragment=False).localpath()
2755 return url(path, parsequery=False, parsefragment=False).localpath()
2756
2756
2757 def hidepassword(u):
2757 def hidepassword(u):
2758 '''hide user credential in a url string'''
2758 '''hide user credential in a url string'''
2759 u = url(u)
2759 u = url(u)
2760 if u.passwd:
2760 if u.passwd:
2761 u.passwd = '***'
2761 u.passwd = '***'
2762 return str(u)
2762 return str(u)
2763
2763
2764 def removeauth(u):
2764 def removeauth(u):
2765 '''remove all authentication information from a url string'''
2765 '''remove all authentication information from a url string'''
2766 u = url(u)
2766 u = url(u)
2767 u.user = u.passwd = None
2767 u.user = u.passwd = None
2768 return str(u)
2768 return str(u)
2769
2769
2770 timecount = unitcountfn(
2770 timecount = unitcountfn(
2771 (1, 1e3, _('%.0f s')),
2771 (1, 1e3, _('%.0f s')),
2772 (100, 1, _('%.1f s')),
2772 (100, 1, _('%.1f s')),
2773 (10, 1, _('%.2f s')),
2773 (10, 1, _('%.2f s')),
2774 (1, 1, _('%.3f s')),
2774 (1, 1, _('%.3f s')),
2775 (100, 0.001, _('%.1f ms')),
2775 (100, 0.001, _('%.1f ms')),
2776 (10, 0.001, _('%.2f ms')),
2776 (10, 0.001, _('%.2f ms')),
2777 (1, 0.001, _('%.3f ms')),
2777 (1, 0.001, _('%.3f ms')),
2778 (100, 0.000001, _('%.1f us')),
2778 (100, 0.000001, _('%.1f us')),
2779 (10, 0.000001, _('%.2f us')),
2779 (10, 0.000001, _('%.2f us')),
2780 (1, 0.000001, _('%.3f us')),
2780 (1, 0.000001, _('%.3f us')),
2781 (100, 0.000000001, _('%.1f ns')),
2781 (100, 0.000000001, _('%.1f ns')),
2782 (10, 0.000000001, _('%.2f ns')),
2782 (10, 0.000000001, _('%.2f ns')),
2783 (1, 0.000000001, _('%.3f ns')),
2783 (1, 0.000000001, _('%.3f ns')),
2784 )
2784 )
2785
2785
2786 _timenesting = [0]
2786 _timenesting = [0]
2787
2787
2788 def timed(func):
2788 def timed(func):
2789 '''Report the execution time of a function call to stderr.
2789 '''Report the execution time of a function call to stderr.
2790
2790
2791 During development, use as a decorator when you need to measure
2791 During development, use as a decorator when you need to measure
2792 the cost of a function, e.g. as follows:
2792 the cost of a function, e.g. as follows:
2793
2793
2794 @util.timed
2794 @util.timed
2795 def foo(a, b, c):
2795 def foo(a, b, c):
2796 pass
2796 pass
2797 '''
2797 '''
2798
2798
2799 def wrapper(*args, **kwargs):
2799 def wrapper(*args, **kwargs):
2800 start = time.time()
2800 start = timer()
2801 indent = 2
2801 indent = 2
2802 _timenesting[0] += indent
2802 _timenesting[0] += indent
2803 try:
2803 try:
2804 return func(*args, **kwargs)
2804 return func(*args, **kwargs)
2805 finally:
2805 finally:
2806 elapsed = time.time() - start
2806 elapsed = timer() - start
2807 _timenesting[0] -= indent
2807 _timenesting[0] -= indent
2808 stderr.write('%s%s: %s\n' %
2808 stderr.write('%s%s: %s\n' %
2809 (' ' * _timenesting[0], func.__name__,
2809 (' ' * _timenesting[0], func.__name__,
2810 timecount(elapsed)))
2810 timecount(elapsed)))
2811 return wrapper
2811 return wrapper
2812
2812
2813 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2813 _sizeunits = (('m', 2**20), ('k', 2**10), ('g', 2**30),
2814 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2814 ('kb', 2**10), ('mb', 2**20), ('gb', 2**30), ('b', 1))
2815
2815
2816 def sizetoint(s):
2816 def sizetoint(s):
2817 '''Convert a space specifier to a byte count.
2817 '''Convert a space specifier to a byte count.
2818
2818
2819 >>> sizetoint('30')
2819 >>> sizetoint('30')
2820 30
2820 30
2821 >>> sizetoint('2.2kb')
2821 >>> sizetoint('2.2kb')
2822 2252
2822 2252
2823 >>> sizetoint('6M')
2823 >>> sizetoint('6M')
2824 6291456
2824 6291456
2825 '''
2825 '''
2826 t = s.strip().lower()
2826 t = s.strip().lower()
2827 try:
2827 try:
2828 for k, u in _sizeunits:
2828 for k, u in _sizeunits:
2829 if t.endswith(k):
2829 if t.endswith(k):
2830 return int(float(t[:-len(k)]) * u)
2830 return int(float(t[:-len(k)]) * u)
2831 return int(t)
2831 return int(t)
2832 except ValueError:
2832 except ValueError:
2833 raise error.ParseError(_("couldn't parse size: %s") % s)
2833 raise error.ParseError(_("couldn't parse size: %s") % s)
2834
2834
2835 class hooks(object):
2835 class hooks(object):
2836 '''A collection of hook functions that can be used to extend a
2836 '''A collection of hook functions that can be used to extend a
2837 function's behavior. Hooks are called in lexicographic order,
2837 function's behavior. Hooks are called in lexicographic order,
2838 based on the names of their sources.'''
2838 based on the names of their sources.'''
2839
2839
2840 def __init__(self):
2840 def __init__(self):
2841 self._hooks = []
2841 self._hooks = []
2842
2842
2843 def add(self, source, hook):
2843 def add(self, source, hook):
2844 self._hooks.append((source, hook))
2844 self._hooks.append((source, hook))
2845
2845
2846 def __call__(self, *args):
2846 def __call__(self, *args):
2847 self._hooks.sort(key=lambda x: x[0])
2847 self._hooks.sort(key=lambda x: x[0])
2848 results = []
2848 results = []
2849 for source, hook in self._hooks:
2849 for source, hook in self._hooks:
2850 results.append(hook(*args))
2850 results.append(hook(*args))
2851 return results
2851 return results
2852
2852
2853 def getstackframes(skip=0, line=' %-*s in %s\n', fileline='%s:%s'):
2853 def getstackframes(skip=0, line=' %-*s in %s\n', fileline='%s:%s'):
2854 '''Yields lines for a nicely formatted stacktrace.
2854 '''Yields lines for a nicely formatted stacktrace.
2855 Skips the 'skip' last entries.
2855 Skips the 'skip' last entries.
2856 Each file+linenumber is formatted according to fileline.
2856 Each file+linenumber is formatted according to fileline.
2857 Each line is formatted according to line.
2857 Each line is formatted according to line.
2858 If line is None, it yields:
2858 If line is None, it yields:
2859 length of longest filepath+line number,
2859 length of longest filepath+line number,
2860 filepath+linenumber,
2860 filepath+linenumber,
2861 function
2861 function
2862
2862
2863 Not be used in production code but very convenient while developing.
2863 Not be used in production code but very convenient while developing.
2864 '''
2864 '''
2865 entries = [(fileline % (fn, ln), func)
2865 entries = [(fileline % (fn, ln), func)
2866 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]]
2866 for fn, ln, func, _text in traceback.extract_stack()[:-skip - 1]]
2867 if entries:
2867 if entries:
2868 fnmax = max(len(entry[0]) for entry in entries)
2868 fnmax = max(len(entry[0]) for entry in entries)
2869 for fnln, func in entries:
2869 for fnln, func in entries:
2870 if line is None:
2870 if line is None:
2871 yield (fnmax, fnln, func)
2871 yield (fnmax, fnln, func)
2872 else:
2872 else:
2873 yield line % (fnmax, fnln, func)
2873 yield line % (fnmax, fnln, func)
2874
2874
2875 def debugstacktrace(msg='stacktrace', skip=0, f=stderr, otherf=stdout):
2875 def debugstacktrace(msg='stacktrace', skip=0, f=stderr, otherf=stdout):
2876 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
2876 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
2877 Skips the 'skip' last entries. By default it will flush stdout first.
2877 Skips the 'skip' last entries. By default it will flush stdout first.
2878 It can be used everywhere and intentionally does not require an ui object.
2878 It can be used everywhere and intentionally does not require an ui object.
2879 Not be used in production code but very convenient while developing.
2879 Not be used in production code but very convenient while developing.
2880 '''
2880 '''
2881 if otherf:
2881 if otherf:
2882 otherf.flush()
2882 otherf.flush()
2883 f.write('%s at:\n' % msg)
2883 f.write('%s at:\n' % msg)
2884 for line in getstackframes(skip + 1):
2884 for line in getstackframes(skip + 1):
2885 f.write(line)
2885 f.write(line)
2886 f.flush()
2886 f.flush()
2887
2887
2888 class dirs(object):
2888 class dirs(object):
2889 '''a multiset of directory names from a dirstate or manifest'''
2889 '''a multiset of directory names from a dirstate or manifest'''
2890
2890
2891 def __init__(self, map, skip=None):
2891 def __init__(self, map, skip=None):
2892 self._dirs = {}
2892 self._dirs = {}
2893 addpath = self.addpath
2893 addpath = self.addpath
2894 if safehasattr(map, 'iteritems') and skip is not None:
2894 if safehasattr(map, 'iteritems') and skip is not None:
2895 for f, s in map.iteritems():
2895 for f, s in map.iteritems():
2896 if s[0] != skip:
2896 if s[0] != skip:
2897 addpath(f)
2897 addpath(f)
2898 else:
2898 else:
2899 for f in map:
2899 for f in map:
2900 addpath(f)
2900 addpath(f)
2901
2901
2902 def addpath(self, path):
2902 def addpath(self, path):
2903 dirs = self._dirs
2903 dirs = self._dirs
2904 for base in finddirs(path):
2904 for base in finddirs(path):
2905 if base in dirs:
2905 if base in dirs:
2906 dirs[base] += 1
2906 dirs[base] += 1
2907 return
2907 return
2908 dirs[base] = 1
2908 dirs[base] = 1
2909
2909
2910 def delpath(self, path):
2910 def delpath(self, path):
2911 dirs = self._dirs
2911 dirs = self._dirs
2912 for base in finddirs(path):
2912 for base in finddirs(path):
2913 if dirs[base] > 1:
2913 if dirs[base] > 1:
2914 dirs[base] -= 1
2914 dirs[base] -= 1
2915 return
2915 return
2916 del dirs[base]
2916 del dirs[base]
2917
2917
2918 def __iter__(self):
2918 def __iter__(self):
2919 return self._dirs.iterkeys()
2919 return self._dirs.iterkeys()
2920
2920
2921 def __contains__(self, d):
2921 def __contains__(self, d):
2922 return d in self._dirs
2922 return d in self._dirs
2923
2923
2924 if safehasattr(parsers, 'dirs'):
2924 if safehasattr(parsers, 'dirs'):
2925 dirs = parsers.dirs
2925 dirs = parsers.dirs
2926
2926
2927 def finddirs(path):
2927 def finddirs(path):
2928 pos = path.rfind('/')
2928 pos = path.rfind('/')
2929 while pos != -1:
2929 while pos != -1:
2930 yield path[:pos]
2930 yield path[:pos]
2931 pos = path.rfind('/', 0, pos)
2931 pos = path.rfind('/', 0, pos)
2932
2932
2933 class ctxmanager(object):
2933 class ctxmanager(object):
2934 '''A context manager for use in 'with' blocks to allow multiple
2934 '''A context manager for use in 'with' blocks to allow multiple
2935 contexts to be entered at once. This is both safer and more
2935 contexts to be entered at once. This is both safer and more
2936 flexible than contextlib.nested.
2936 flexible than contextlib.nested.
2937
2937
2938 Once Mercurial supports Python 2.7+, this will become mostly
2938 Once Mercurial supports Python 2.7+, this will become mostly
2939 unnecessary.
2939 unnecessary.
2940 '''
2940 '''
2941
2941
2942 def __init__(self, *args):
2942 def __init__(self, *args):
2943 '''Accepts a list of no-argument functions that return context
2943 '''Accepts a list of no-argument functions that return context
2944 managers. These will be invoked at __call__ time.'''
2944 managers. These will be invoked at __call__ time.'''
2945 self._pending = args
2945 self._pending = args
2946 self._atexit = []
2946 self._atexit = []
2947
2947
2948 def __enter__(self):
2948 def __enter__(self):
2949 return self
2949 return self
2950
2950
2951 def enter(self):
2951 def enter(self):
2952 '''Create and enter context managers in the order in which they were
2952 '''Create and enter context managers in the order in which they were
2953 passed to the constructor.'''
2953 passed to the constructor.'''
2954 values = []
2954 values = []
2955 for func in self._pending:
2955 for func in self._pending:
2956 obj = func()
2956 obj = func()
2957 values.append(obj.__enter__())
2957 values.append(obj.__enter__())
2958 self._atexit.append(obj.__exit__)
2958 self._atexit.append(obj.__exit__)
2959 del self._pending
2959 del self._pending
2960 return values
2960 return values
2961
2961
2962 def atexit(self, func, *args, **kwargs):
2962 def atexit(self, func, *args, **kwargs):
2963 '''Add a function to call when this context manager exits. The
2963 '''Add a function to call when this context manager exits. The
2964 ordering of multiple atexit calls is unspecified, save that
2964 ordering of multiple atexit calls is unspecified, save that
2965 they will happen before any __exit__ functions.'''
2965 they will happen before any __exit__ functions.'''
2966 def wrapper(exc_type, exc_val, exc_tb):
2966 def wrapper(exc_type, exc_val, exc_tb):
2967 func(*args, **kwargs)
2967 func(*args, **kwargs)
2968 self._atexit.append(wrapper)
2968 self._atexit.append(wrapper)
2969 return func
2969 return func
2970
2970
2971 def __exit__(self, exc_type, exc_val, exc_tb):
2971 def __exit__(self, exc_type, exc_val, exc_tb):
2972 '''Context managers are exited in the reverse order from which
2972 '''Context managers are exited in the reverse order from which
2973 they were created.'''
2973 they were created.'''
2974 received = exc_type is not None
2974 received = exc_type is not None
2975 suppressed = False
2975 suppressed = False
2976 pending = None
2976 pending = None
2977 self._atexit.reverse()
2977 self._atexit.reverse()
2978 for exitfunc in self._atexit:
2978 for exitfunc in self._atexit:
2979 try:
2979 try:
2980 if exitfunc(exc_type, exc_val, exc_tb):
2980 if exitfunc(exc_type, exc_val, exc_tb):
2981 suppressed = True
2981 suppressed = True
2982 exc_type = None
2982 exc_type = None
2983 exc_val = None
2983 exc_val = None
2984 exc_tb = None
2984 exc_tb = None
2985 except BaseException:
2985 except BaseException:
2986 pending = sys.exc_info()
2986 pending = sys.exc_info()
2987 exc_type, exc_val, exc_tb = pending = sys.exc_info()
2987 exc_type, exc_val, exc_tb = pending = sys.exc_info()
2988 del self._atexit
2988 del self._atexit
2989 if pending:
2989 if pending:
2990 raise exc_val
2990 raise exc_val
2991 return received and suppressed
2991 return received and suppressed
2992
2992
2993 # compression code
2993 # compression code
2994
2994
2995 SERVERROLE = 'server'
2995 SERVERROLE = 'server'
2996 CLIENTROLE = 'client'
2996 CLIENTROLE = 'client'
2997
2997
2998 compewireprotosupport = collections.namedtuple(u'compenginewireprotosupport',
2998 compewireprotosupport = collections.namedtuple(u'compenginewireprotosupport',
2999 (u'name', u'serverpriority',
2999 (u'name', u'serverpriority',
3000 u'clientpriority'))
3000 u'clientpriority'))
3001
3001
3002 class compressormanager(object):
3002 class compressormanager(object):
3003 """Holds registrations of various compression engines.
3003 """Holds registrations of various compression engines.
3004
3004
3005 This class essentially abstracts the differences between compression
3005 This class essentially abstracts the differences between compression
3006 engines to allow new compression formats to be added easily, possibly from
3006 engines to allow new compression formats to be added easily, possibly from
3007 extensions.
3007 extensions.
3008
3008
3009 Compressors are registered against the global instance by calling its
3009 Compressors are registered against the global instance by calling its
3010 ``register()`` method.
3010 ``register()`` method.
3011 """
3011 """
3012 def __init__(self):
3012 def __init__(self):
3013 self._engines = {}
3013 self._engines = {}
3014 # Bundle spec human name to engine name.
3014 # Bundle spec human name to engine name.
3015 self._bundlenames = {}
3015 self._bundlenames = {}
3016 # Internal bundle identifier to engine name.
3016 # Internal bundle identifier to engine name.
3017 self._bundletypes = {}
3017 self._bundletypes = {}
3018 # Revlog header to engine name.
3018 # Revlog header to engine name.
3019 self._revlogheaders = {}
3019 self._revlogheaders = {}
3020 # Wire proto identifier to engine name.
3020 # Wire proto identifier to engine name.
3021 self._wiretypes = {}
3021 self._wiretypes = {}
3022
3022
3023 def __getitem__(self, key):
3023 def __getitem__(self, key):
3024 return self._engines[key]
3024 return self._engines[key]
3025
3025
3026 def __contains__(self, key):
3026 def __contains__(self, key):
3027 return key in self._engines
3027 return key in self._engines
3028
3028
3029 def __iter__(self):
3029 def __iter__(self):
3030 return iter(self._engines.keys())
3030 return iter(self._engines.keys())
3031
3031
3032 def register(self, engine):
3032 def register(self, engine):
3033 """Register a compression engine with the manager.
3033 """Register a compression engine with the manager.
3034
3034
3035 The argument must be a ``compressionengine`` instance.
3035 The argument must be a ``compressionengine`` instance.
3036 """
3036 """
3037 if not isinstance(engine, compressionengine):
3037 if not isinstance(engine, compressionengine):
3038 raise ValueError(_('argument must be a compressionengine'))
3038 raise ValueError(_('argument must be a compressionengine'))
3039
3039
3040 name = engine.name()
3040 name = engine.name()
3041
3041
3042 if name in self._engines:
3042 if name in self._engines:
3043 raise error.Abort(_('compression engine %s already registered') %
3043 raise error.Abort(_('compression engine %s already registered') %
3044 name)
3044 name)
3045
3045
3046 bundleinfo = engine.bundletype()
3046 bundleinfo = engine.bundletype()
3047 if bundleinfo:
3047 if bundleinfo:
3048 bundlename, bundletype = bundleinfo
3048 bundlename, bundletype = bundleinfo
3049
3049
3050 if bundlename in self._bundlenames:
3050 if bundlename in self._bundlenames:
3051 raise error.Abort(_('bundle name %s already registered') %
3051 raise error.Abort(_('bundle name %s already registered') %
3052 bundlename)
3052 bundlename)
3053 if bundletype in self._bundletypes:
3053 if bundletype in self._bundletypes:
3054 raise error.Abort(_('bundle type %s already registered by %s') %
3054 raise error.Abort(_('bundle type %s already registered by %s') %
3055 (bundletype, self._bundletypes[bundletype]))
3055 (bundletype, self._bundletypes[bundletype]))
3056
3056
3057 # No external facing name declared.
3057 # No external facing name declared.
3058 if bundlename:
3058 if bundlename:
3059 self._bundlenames[bundlename] = name
3059 self._bundlenames[bundlename] = name
3060
3060
3061 self._bundletypes[bundletype] = name
3061 self._bundletypes[bundletype] = name
3062
3062
3063 wiresupport = engine.wireprotosupport()
3063 wiresupport = engine.wireprotosupport()
3064 if wiresupport:
3064 if wiresupport:
3065 wiretype = wiresupport.name
3065 wiretype = wiresupport.name
3066 if wiretype in self._wiretypes:
3066 if wiretype in self._wiretypes:
3067 raise error.Abort(_('wire protocol compression %s already '
3067 raise error.Abort(_('wire protocol compression %s already '
3068 'registered by %s') %
3068 'registered by %s') %
3069 (wiretype, self._wiretypes[wiretype]))
3069 (wiretype, self._wiretypes[wiretype]))
3070
3070
3071 self._wiretypes[wiretype] = name
3071 self._wiretypes[wiretype] = name
3072
3072
3073 revlogheader = engine.revlogheader()
3073 revlogheader = engine.revlogheader()
3074 if revlogheader and revlogheader in self._revlogheaders:
3074 if revlogheader and revlogheader in self._revlogheaders:
3075 raise error.Abort(_('revlog header %s already registered by %s') %
3075 raise error.Abort(_('revlog header %s already registered by %s') %
3076 (revlogheader, self._revlogheaders[revlogheader]))
3076 (revlogheader, self._revlogheaders[revlogheader]))
3077
3077
3078 if revlogheader:
3078 if revlogheader:
3079 self._revlogheaders[revlogheader] = name
3079 self._revlogheaders[revlogheader] = name
3080
3080
3081 self._engines[name] = engine
3081 self._engines[name] = engine
3082
3082
3083 @property
3083 @property
3084 def supportedbundlenames(self):
3084 def supportedbundlenames(self):
3085 return set(self._bundlenames.keys())
3085 return set(self._bundlenames.keys())
3086
3086
3087 @property
3087 @property
3088 def supportedbundletypes(self):
3088 def supportedbundletypes(self):
3089 return set(self._bundletypes.keys())
3089 return set(self._bundletypes.keys())
3090
3090
3091 def forbundlename(self, bundlename):
3091 def forbundlename(self, bundlename):
3092 """Obtain a compression engine registered to a bundle name.
3092 """Obtain a compression engine registered to a bundle name.
3093
3093
3094 Will raise KeyError if the bundle type isn't registered.
3094 Will raise KeyError if the bundle type isn't registered.
3095
3095
3096 Will abort if the engine is known but not available.
3096 Will abort if the engine is known but not available.
3097 """
3097 """
3098 engine = self._engines[self._bundlenames[bundlename]]
3098 engine = self._engines[self._bundlenames[bundlename]]
3099 if not engine.available():
3099 if not engine.available():
3100 raise error.Abort(_('compression engine %s could not be loaded') %
3100 raise error.Abort(_('compression engine %s could not be loaded') %
3101 engine.name())
3101 engine.name())
3102 return engine
3102 return engine
3103
3103
3104 def forbundletype(self, bundletype):
3104 def forbundletype(self, bundletype):
3105 """Obtain a compression engine registered to a bundle type.
3105 """Obtain a compression engine registered to a bundle type.
3106
3106
3107 Will raise KeyError if the bundle type isn't registered.
3107 Will raise KeyError if the bundle type isn't registered.
3108
3108
3109 Will abort if the engine is known but not available.
3109 Will abort if the engine is known but not available.
3110 """
3110 """
3111 engine = self._engines[self._bundletypes[bundletype]]
3111 engine = self._engines[self._bundletypes[bundletype]]
3112 if not engine.available():
3112 if not engine.available():
3113 raise error.Abort(_('compression engine %s could not be loaded') %
3113 raise error.Abort(_('compression engine %s could not be loaded') %
3114 engine.name())
3114 engine.name())
3115 return engine
3115 return engine
3116
3116
3117 def supportedwireengines(self, role, onlyavailable=True):
3117 def supportedwireengines(self, role, onlyavailable=True):
3118 """Obtain compression engines that support the wire protocol.
3118 """Obtain compression engines that support the wire protocol.
3119
3119
3120 Returns a list of engines in prioritized order, most desired first.
3120 Returns a list of engines in prioritized order, most desired first.
3121
3121
3122 If ``onlyavailable`` is set, filter out engines that can't be
3122 If ``onlyavailable`` is set, filter out engines that can't be
3123 loaded.
3123 loaded.
3124 """
3124 """
3125 assert role in (SERVERROLE, CLIENTROLE)
3125 assert role in (SERVERROLE, CLIENTROLE)
3126
3126
3127 attr = 'serverpriority' if role == SERVERROLE else 'clientpriority'
3127 attr = 'serverpriority' if role == SERVERROLE else 'clientpriority'
3128
3128
3129 engines = [self._engines[e] for e in self._wiretypes.values()]
3129 engines = [self._engines[e] for e in self._wiretypes.values()]
3130 if onlyavailable:
3130 if onlyavailable:
3131 engines = [e for e in engines if e.available()]
3131 engines = [e for e in engines if e.available()]
3132
3132
3133 def getkey(e):
3133 def getkey(e):
3134 # Sort first by priority, highest first. In case of tie, sort
3134 # Sort first by priority, highest first. In case of tie, sort
3135 # alphabetically. This is arbitrary, but ensures output is
3135 # alphabetically. This is arbitrary, but ensures output is
3136 # stable.
3136 # stable.
3137 w = e.wireprotosupport()
3137 w = e.wireprotosupport()
3138 return -1 * getattr(w, attr), w.name
3138 return -1 * getattr(w, attr), w.name
3139
3139
3140 return list(sorted(engines, key=getkey))
3140 return list(sorted(engines, key=getkey))
3141
3141
3142 def forwiretype(self, wiretype):
3142 def forwiretype(self, wiretype):
3143 engine = self._engines[self._wiretypes[wiretype]]
3143 engine = self._engines[self._wiretypes[wiretype]]
3144 if not engine.available():
3144 if not engine.available():
3145 raise error.Abort(_('compression engine %s could not be loaded') %
3145 raise error.Abort(_('compression engine %s could not be loaded') %
3146 engine.name())
3146 engine.name())
3147 return engine
3147 return engine
3148
3148
3149 def forrevlogheader(self, header):
3149 def forrevlogheader(self, header):
3150 """Obtain a compression engine registered to a revlog header.
3150 """Obtain a compression engine registered to a revlog header.
3151
3151
3152 Will raise KeyError if the revlog header value isn't registered.
3152 Will raise KeyError if the revlog header value isn't registered.
3153 """
3153 """
3154 return self._engines[self._revlogheaders[header]]
3154 return self._engines[self._revlogheaders[header]]
3155
3155
3156 compengines = compressormanager()
3156 compengines = compressormanager()
3157
3157
3158 class compressionengine(object):
3158 class compressionengine(object):
3159 """Base class for compression engines.
3159 """Base class for compression engines.
3160
3160
3161 Compression engines must implement the interface defined by this class.
3161 Compression engines must implement the interface defined by this class.
3162 """
3162 """
3163 def name(self):
3163 def name(self):
3164 """Returns the name of the compression engine.
3164 """Returns the name of the compression engine.
3165
3165
3166 This is the key the engine is registered under.
3166 This is the key the engine is registered under.
3167
3167
3168 This method must be implemented.
3168 This method must be implemented.
3169 """
3169 """
3170 raise NotImplementedError()
3170 raise NotImplementedError()
3171
3171
3172 def available(self):
3172 def available(self):
3173 """Whether the compression engine is available.
3173 """Whether the compression engine is available.
3174
3174
3175 The intent of this method is to allow optional compression engines
3175 The intent of this method is to allow optional compression engines
3176 that may not be available in all installations (such as engines relying
3176 that may not be available in all installations (such as engines relying
3177 on C extensions that may not be present).
3177 on C extensions that may not be present).
3178 """
3178 """
3179 return True
3179 return True
3180
3180
3181 def bundletype(self):
3181 def bundletype(self):
3182 """Describes bundle identifiers for this engine.
3182 """Describes bundle identifiers for this engine.
3183
3183
3184 If this compression engine isn't supported for bundles, returns None.
3184 If this compression engine isn't supported for bundles, returns None.
3185
3185
3186 If this engine can be used for bundles, returns a 2-tuple of strings of
3186 If this engine can be used for bundles, returns a 2-tuple of strings of
3187 the user-facing "bundle spec" compression name and an internal
3187 the user-facing "bundle spec" compression name and an internal
3188 identifier used to denote the compression format within bundles. To
3188 identifier used to denote the compression format within bundles. To
3189 exclude the name from external usage, set the first element to ``None``.
3189 exclude the name from external usage, set the first element to ``None``.
3190
3190
3191 If bundle compression is supported, the class must also implement
3191 If bundle compression is supported, the class must also implement
3192 ``compressstream`` and `decompressorreader``.
3192 ``compressstream`` and `decompressorreader``.
3193 """
3193 """
3194 return None
3194 return None
3195
3195
3196 def wireprotosupport(self):
3196 def wireprotosupport(self):
3197 """Declare support for this compression format on the wire protocol.
3197 """Declare support for this compression format on the wire protocol.
3198
3198
3199 If this compression engine isn't supported for compressing wire
3199 If this compression engine isn't supported for compressing wire
3200 protocol payloads, returns None.
3200 protocol payloads, returns None.
3201
3201
3202 Otherwise, returns ``compenginewireprotosupport`` with the following
3202 Otherwise, returns ``compenginewireprotosupport`` with the following
3203 fields:
3203 fields:
3204
3204
3205 * String format identifier
3205 * String format identifier
3206 * Integer priority for the server
3206 * Integer priority for the server
3207 * Integer priority for the client
3207 * Integer priority for the client
3208
3208
3209 The integer priorities are used to order the advertisement of format
3209 The integer priorities are used to order the advertisement of format
3210 support by server and client. The highest integer is advertised
3210 support by server and client. The highest integer is advertised
3211 first. Integers with non-positive values aren't advertised.
3211 first. Integers with non-positive values aren't advertised.
3212
3212
3213 The priority values are somewhat arbitrary and only used for default
3213 The priority values are somewhat arbitrary and only used for default
3214 ordering. The relative order can be changed via config options.
3214 ordering. The relative order can be changed via config options.
3215
3215
3216 If wire protocol compression is supported, the class must also implement
3216 If wire protocol compression is supported, the class must also implement
3217 ``compressstream`` and ``decompressorreader``.
3217 ``compressstream`` and ``decompressorreader``.
3218 """
3218 """
3219 return None
3219 return None
3220
3220
3221 def revlogheader(self):
3221 def revlogheader(self):
3222 """Header added to revlog chunks that identifies this engine.
3222 """Header added to revlog chunks that identifies this engine.
3223
3223
3224 If this engine can be used to compress revlogs, this method should
3224 If this engine can be used to compress revlogs, this method should
3225 return the bytes used to identify chunks compressed with this engine.
3225 return the bytes used to identify chunks compressed with this engine.
3226 Else, the method should return ``None`` to indicate it does not
3226 Else, the method should return ``None`` to indicate it does not
3227 participate in revlog compression.
3227 participate in revlog compression.
3228 """
3228 """
3229 return None
3229 return None
3230
3230
3231 def compressstream(self, it, opts=None):
3231 def compressstream(self, it, opts=None):
3232 """Compress an iterator of chunks.
3232 """Compress an iterator of chunks.
3233
3233
3234 The method receives an iterator (ideally a generator) of chunks of
3234 The method receives an iterator (ideally a generator) of chunks of
3235 bytes to be compressed. It returns an iterator (ideally a generator)
3235 bytes to be compressed. It returns an iterator (ideally a generator)
3236 of bytes of chunks representing the compressed output.
3236 of bytes of chunks representing the compressed output.
3237
3237
3238 Optionally accepts an argument defining how to perform compression.
3238 Optionally accepts an argument defining how to perform compression.
3239 Each engine treats this argument differently.
3239 Each engine treats this argument differently.
3240 """
3240 """
3241 raise NotImplementedError()
3241 raise NotImplementedError()
3242
3242
3243 def decompressorreader(self, fh):
3243 def decompressorreader(self, fh):
3244 """Perform decompression on a file object.
3244 """Perform decompression on a file object.
3245
3245
3246 Argument is an object with a ``read(size)`` method that returns
3246 Argument is an object with a ``read(size)`` method that returns
3247 compressed data. Return value is an object with a ``read(size)`` that
3247 compressed data. Return value is an object with a ``read(size)`` that
3248 returns uncompressed data.
3248 returns uncompressed data.
3249 """
3249 """
3250 raise NotImplementedError()
3250 raise NotImplementedError()
3251
3251
3252 def revlogcompressor(self, opts=None):
3252 def revlogcompressor(self, opts=None):
3253 """Obtain an object that can be used to compress revlog entries.
3253 """Obtain an object that can be used to compress revlog entries.
3254
3254
3255 The object has a ``compress(data)`` method that compresses binary
3255 The object has a ``compress(data)`` method that compresses binary
3256 data. This method returns compressed binary data or ``None`` if
3256 data. This method returns compressed binary data or ``None`` if
3257 the data could not be compressed (too small, not compressible, etc).
3257 the data could not be compressed (too small, not compressible, etc).
3258 The returned data should have a header uniquely identifying this
3258 The returned data should have a header uniquely identifying this
3259 compression format so decompression can be routed to this engine.
3259 compression format so decompression can be routed to this engine.
3260 This header should be identified by the ``revlogheader()`` return
3260 This header should be identified by the ``revlogheader()`` return
3261 value.
3261 value.
3262
3262
3263 The object has a ``decompress(data)`` method that decompresses
3263 The object has a ``decompress(data)`` method that decompresses
3264 data. The method will only be called if ``data`` begins with
3264 data. The method will only be called if ``data`` begins with
3265 ``revlogheader()``. The method should return the raw, uncompressed
3265 ``revlogheader()``. The method should return the raw, uncompressed
3266 data or raise a ``RevlogError``.
3266 data or raise a ``RevlogError``.
3267
3267
3268 The object is reusable but is not thread safe.
3268 The object is reusable but is not thread safe.
3269 """
3269 """
3270 raise NotImplementedError()
3270 raise NotImplementedError()
3271
3271
3272 class _zlibengine(compressionengine):
3272 class _zlibengine(compressionengine):
3273 def name(self):
3273 def name(self):
3274 return 'zlib'
3274 return 'zlib'
3275
3275
3276 def bundletype(self):
3276 def bundletype(self):
3277 return 'gzip', 'GZ'
3277 return 'gzip', 'GZ'
3278
3278
3279 def wireprotosupport(self):
3279 def wireprotosupport(self):
3280 return compewireprotosupport('zlib', 20, 20)
3280 return compewireprotosupport('zlib', 20, 20)
3281
3281
3282 def revlogheader(self):
3282 def revlogheader(self):
3283 return 'x'
3283 return 'x'
3284
3284
3285 def compressstream(self, it, opts=None):
3285 def compressstream(self, it, opts=None):
3286 opts = opts or {}
3286 opts = opts or {}
3287
3287
3288 z = zlib.compressobj(opts.get('level', -1))
3288 z = zlib.compressobj(opts.get('level', -1))
3289 for chunk in it:
3289 for chunk in it:
3290 data = z.compress(chunk)
3290 data = z.compress(chunk)
3291 # Not all calls to compress emit data. It is cheaper to inspect
3291 # Not all calls to compress emit data. It is cheaper to inspect
3292 # here than to feed empty chunks through generator.
3292 # here than to feed empty chunks through generator.
3293 if data:
3293 if data:
3294 yield data
3294 yield data
3295
3295
3296 yield z.flush()
3296 yield z.flush()
3297
3297
3298 def decompressorreader(self, fh):
3298 def decompressorreader(self, fh):
3299 def gen():
3299 def gen():
3300 d = zlib.decompressobj()
3300 d = zlib.decompressobj()
3301 for chunk in filechunkiter(fh):
3301 for chunk in filechunkiter(fh):
3302 while chunk:
3302 while chunk:
3303 # Limit output size to limit memory.
3303 # Limit output size to limit memory.
3304 yield d.decompress(chunk, 2 ** 18)
3304 yield d.decompress(chunk, 2 ** 18)
3305 chunk = d.unconsumed_tail
3305 chunk = d.unconsumed_tail
3306
3306
3307 return chunkbuffer(gen())
3307 return chunkbuffer(gen())
3308
3308
3309 class zlibrevlogcompressor(object):
3309 class zlibrevlogcompressor(object):
3310 def compress(self, data):
3310 def compress(self, data):
3311 insize = len(data)
3311 insize = len(data)
3312 # Caller handles empty input case.
3312 # Caller handles empty input case.
3313 assert insize > 0
3313 assert insize > 0
3314
3314
3315 if insize < 44:
3315 if insize < 44:
3316 return None
3316 return None
3317
3317
3318 elif insize <= 1000000:
3318 elif insize <= 1000000:
3319 compressed = zlib.compress(data)
3319 compressed = zlib.compress(data)
3320 if len(compressed) < insize:
3320 if len(compressed) < insize:
3321 return compressed
3321 return compressed
3322 return None
3322 return None
3323
3323
3324 # zlib makes an internal copy of the input buffer, doubling
3324 # zlib makes an internal copy of the input buffer, doubling
3325 # memory usage for large inputs. So do streaming compression
3325 # memory usage for large inputs. So do streaming compression
3326 # on large inputs.
3326 # on large inputs.
3327 else:
3327 else:
3328 z = zlib.compressobj()
3328 z = zlib.compressobj()
3329 parts = []
3329 parts = []
3330 pos = 0
3330 pos = 0
3331 while pos < insize:
3331 while pos < insize:
3332 pos2 = pos + 2**20
3332 pos2 = pos + 2**20
3333 parts.append(z.compress(data[pos:pos2]))
3333 parts.append(z.compress(data[pos:pos2]))
3334 pos = pos2
3334 pos = pos2
3335 parts.append(z.flush())
3335 parts.append(z.flush())
3336
3336
3337 if sum(map(len, parts)) < insize:
3337 if sum(map(len, parts)) < insize:
3338 return ''.join(parts)
3338 return ''.join(parts)
3339 return None
3339 return None
3340
3340
3341 def decompress(self, data):
3341 def decompress(self, data):
3342 try:
3342 try:
3343 return zlib.decompress(data)
3343 return zlib.decompress(data)
3344 except zlib.error as e:
3344 except zlib.error as e:
3345 raise error.RevlogError(_('revlog decompress error: %s') %
3345 raise error.RevlogError(_('revlog decompress error: %s') %
3346 str(e))
3346 str(e))
3347
3347
3348 def revlogcompressor(self, opts=None):
3348 def revlogcompressor(self, opts=None):
3349 return self.zlibrevlogcompressor()
3349 return self.zlibrevlogcompressor()
3350
3350
3351 compengines.register(_zlibengine())
3351 compengines.register(_zlibengine())
3352
3352
3353 class _bz2engine(compressionengine):
3353 class _bz2engine(compressionengine):
3354 def name(self):
3354 def name(self):
3355 return 'bz2'
3355 return 'bz2'
3356
3356
3357 def bundletype(self):
3357 def bundletype(self):
3358 return 'bzip2', 'BZ'
3358 return 'bzip2', 'BZ'
3359
3359
3360 # We declare a protocol name but don't advertise by default because
3360 # We declare a protocol name but don't advertise by default because
3361 # it is slow.
3361 # it is slow.
3362 def wireprotosupport(self):
3362 def wireprotosupport(self):
3363 return compewireprotosupport('bzip2', 0, 0)
3363 return compewireprotosupport('bzip2', 0, 0)
3364
3364
3365 def compressstream(self, it, opts=None):
3365 def compressstream(self, it, opts=None):
3366 opts = opts or {}
3366 opts = opts or {}
3367 z = bz2.BZ2Compressor(opts.get('level', 9))
3367 z = bz2.BZ2Compressor(opts.get('level', 9))
3368 for chunk in it:
3368 for chunk in it:
3369 data = z.compress(chunk)
3369 data = z.compress(chunk)
3370 if data:
3370 if data:
3371 yield data
3371 yield data
3372
3372
3373 yield z.flush()
3373 yield z.flush()
3374
3374
3375 def decompressorreader(self, fh):
3375 def decompressorreader(self, fh):
3376 def gen():
3376 def gen():
3377 d = bz2.BZ2Decompressor()
3377 d = bz2.BZ2Decompressor()
3378 for chunk in filechunkiter(fh):
3378 for chunk in filechunkiter(fh):
3379 yield d.decompress(chunk)
3379 yield d.decompress(chunk)
3380
3380
3381 return chunkbuffer(gen())
3381 return chunkbuffer(gen())
3382
3382
3383 compengines.register(_bz2engine())
3383 compengines.register(_bz2engine())
3384
3384
3385 class _truncatedbz2engine(compressionengine):
3385 class _truncatedbz2engine(compressionengine):
3386 def name(self):
3386 def name(self):
3387 return 'bz2truncated'
3387 return 'bz2truncated'
3388
3388
3389 def bundletype(self):
3389 def bundletype(self):
3390 return None, '_truncatedBZ'
3390 return None, '_truncatedBZ'
3391
3391
3392 # We don't implement compressstream because it is hackily handled elsewhere.
3392 # We don't implement compressstream because it is hackily handled elsewhere.
3393
3393
3394 def decompressorreader(self, fh):
3394 def decompressorreader(self, fh):
3395 def gen():
3395 def gen():
3396 # The input stream doesn't have the 'BZ' header. So add it back.
3396 # The input stream doesn't have the 'BZ' header. So add it back.
3397 d = bz2.BZ2Decompressor()
3397 d = bz2.BZ2Decompressor()
3398 d.decompress('BZ')
3398 d.decompress('BZ')
3399 for chunk in filechunkiter(fh):
3399 for chunk in filechunkiter(fh):
3400 yield d.decompress(chunk)
3400 yield d.decompress(chunk)
3401
3401
3402 return chunkbuffer(gen())
3402 return chunkbuffer(gen())
3403
3403
3404 compengines.register(_truncatedbz2engine())
3404 compengines.register(_truncatedbz2engine())
3405
3405
3406 class _noopengine(compressionengine):
3406 class _noopengine(compressionengine):
3407 def name(self):
3407 def name(self):
3408 return 'none'
3408 return 'none'
3409
3409
3410 def bundletype(self):
3410 def bundletype(self):
3411 return 'none', 'UN'
3411 return 'none', 'UN'
3412
3412
3413 # Clients always support uncompressed payloads. Servers don't because
3413 # Clients always support uncompressed payloads. Servers don't because
3414 # unless you are on a fast network, uncompressed payloads can easily
3414 # unless you are on a fast network, uncompressed payloads can easily
3415 # saturate your network pipe.
3415 # saturate your network pipe.
3416 def wireprotosupport(self):
3416 def wireprotosupport(self):
3417 return compewireprotosupport('none', 0, 10)
3417 return compewireprotosupport('none', 0, 10)
3418
3418
3419 # We don't implement revlogheader because it is handled specially
3419 # We don't implement revlogheader because it is handled specially
3420 # in the revlog class.
3420 # in the revlog class.
3421
3421
3422 def compressstream(self, it, opts=None):
3422 def compressstream(self, it, opts=None):
3423 return it
3423 return it
3424
3424
3425 def decompressorreader(self, fh):
3425 def decompressorreader(self, fh):
3426 return fh
3426 return fh
3427
3427
3428 class nooprevlogcompressor(object):
3428 class nooprevlogcompressor(object):
3429 def compress(self, data):
3429 def compress(self, data):
3430 return None
3430 return None
3431
3431
3432 def revlogcompressor(self, opts=None):
3432 def revlogcompressor(self, opts=None):
3433 return self.nooprevlogcompressor()
3433 return self.nooprevlogcompressor()
3434
3434
3435 compengines.register(_noopengine())
3435 compengines.register(_noopengine())
3436
3436
3437 class _zstdengine(compressionengine):
3437 class _zstdengine(compressionengine):
3438 def name(self):
3438 def name(self):
3439 return 'zstd'
3439 return 'zstd'
3440
3440
3441 @propertycache
3441 @propertycache
3442 def _module(self):
3442 def _module(self):
3443 # Not all installs have the zstd module available. So defer importing
3443 # Not all installs have the zstd module available. So defer importing
3444 # until first access.
3444 # until first access.
3445 try:
3445 try:
3446 from . import zstd
3446 from . import zstd
3447 # Force delayed import.
3447 # Force delayed import.
3448 zstd.__version__
3448 zstd.__version__
3449 return zstd
3449 return zstd
3450 except ImportError:
3450 except ImportError:
3451 return None
3451 return None
3452
3452
3453 def available(self):
3453 def available(self):
3454 return bool(self._module)
3454 return bool(self._module)
3455
3455
3456 def bundletype(self):
3456 def bundletype(self):
3457 return 'zstd', 'ZS'
3457 return 'zstd', 'ZS'
3458
3458
3459 def wireprotosupport(self):
3459 def wireprotosupport(self):
3460 return compewireprotosupport('zstd', 50, 50)
3460 return compewireprotosupport('zstd', 50, 50)
3461
3461
3462 def revlogheader(self):
3462 def revlogheader(self):
3463 return '\x28'
3463 return '\x28'
3464
3464
3465 def compressstream(self, it, opts=None):
3465 def compressstream(self, it, opts=None):
3466 opts = opts or {}
3466 opts = opts or {}
3467 # zstd level 3 is almost always significantly faster than zlib
3467 # zstd level 3 is almost always significantly faster than zlib
3468 # while providing no worse compression. It strikes a good balance
3468 # while providing no worse compression. It strikes a good balance
3469 # between speed and compression.
3469 # between speed and compression.
3470 level = opts.get('level', 3)
3470 level = opts.get('level', 3)
3471
3471
3472 zstd = self._module
3472 zstd = self._module
3473 z = zstd.ZstdCompressor(level=level).compressobj()
3473 z = zstd.ZstdCompressor(level=level).compressobj()
3474 for chunk in it:
3474 for chunk in it:
3475 data = z.compress(chunk)
3475 data = z.compress(chunk)
3476 if data:
3476 if data:
3477 yield data
3477 yield data
3478
3478
3479 yield z.flush()
3479 yield z.flush()
3480
3480
3481 def decompressorreader(self, fh):
3481 def decompressorreader(self, fh):
3482 zstd = self._module
3482 zstd = self._module
3483 dctx = zstd.ZstdDecompressor()
3483 dctx = zstd.ZstdDecompressor()
3484 return chunkbuffer(dctx.read_from(fh))
3484 return chunkbuffer(dctx.read_from(fh))
3485
3485
3486 class zstdrevlogcompressor(object):
3486 class zstdrevlogcompressor(object):
3487 def __init__(self, zstd, level=3):
3487 def __init__(self, zstd, level=3):
3488 # Writing the content size adds a few bytes to the output. However,
3488 # Writing the content size adds a few bytes to the output. However,
3489 # it allows decompression to be more optimal since we can
3489 # it allows decompression to be more optimal since we can
3490 # pre-allocate a buffer to hold the result.
3490 # pre-allocate a buffer to hold the result.
3491 self._cctx = zstd.ZstdCompressor(level=level,
3491 self._cctx = zstd.ZstdCompressor(level=level,
3492 write_content_size=True)
3492 write_content_size=True)
3493 self._dctx = zstd.ZstdDecompressor()
3493 self._dctx = zstd.ZstdDecompressor()
3494 self._compinsize = zstd.COMPRESSION_RECOMMENDED_INPUT_SIZE
3494 self._compinsize = zstd.COMPRESSION_RECOMMENDED_INPUT_SIZE
3495 self._decompinsize = zstd.DECOMPRESSION_RECOMMENDED_INPUT_SIZE
3495 self._decompinsize = zstd.DECOMPRESSION_RECOMMENDED_INPUT_SIZE
3496
3496
3497 def compress(self, data):
3497 def compress(self, data):
3498 insize = len(data)
3498 insize = len(data)
3499 # Caller handles empty input case.
3499 # Caller handles empty input case.
3500 assert insize > 0
3500 assert insize > 0
3501
3501
3502 if insize < 50:
3502 if insize < 50:
3503 return None
3503 return None
3504
3504
3505 elif insize <= 1000000:
3505 elif insize <= 1000000:
3506 compressed = self._cctx.compress(data)
3506 compressed = self._cctx.compress(data)
3507 if len(compressed) < insize:
3507 if len(compressed) < insize:
3508 return compressed
3508 return compressed
3509 return None
3509 return None
3510 else:
3510 else:
3511 z = self._cctx.compressobj()
3511 z = self._cctx.compressobj()
3512 chunks = []
3512 chunks = []
3513 pos = 0
3513 pos = 0
3514 while pos < insize:
3514 while pos < insize:
3515 pos2 = pos + self._compinsize
3515 pos2 = pos + self._compinsize
3516 chunk = z.compress(data[pos:pos2])
3516 chunk = z.compress(data[pos:pos2])
3517 if chunk:
3517 if chunk:
3518 chunks.append(chunk)
3518 chunks.append(chunk)
3519 pos = pos2
3519 pos = pos2
3520 chunks.append(z.flush())
3520 chunks.append(z.flush())
3521
3521
3522 if sum(map(len, chunks)) < insize:
3522 if sum(map(len, chunks)) < insize:
3523 return ''.join(chunks)
3523 return ''.join(chunks)
3524 return None
3524 return None
3525
3525
3526 def decompress(self, data):
3526 def decompress(self, data):
3527 insize = len(data)
3527 insize = len(data)
3528
3528
3529 try:
3529 try:
3530 # This was measured to be faster than other streaming
3530 # This was measured to be faster than other streaming
3531 # decompressors.
3531 # decompressors.
3532 dobj = self._dctx.decompressobj()
3532 dobj = self._dctx.decompressobj()
3533 chunks = []
3533 chunks = []
3534 pos = 0
3534 pos = 0
3535 while pos < insize:
3535 while pos < insize:
3536 pos2 = pos + self._decompinsize
3536 pos2 = pos + self._decompinsize
3537 chunk = dobj.decompress(data[pos:pos2])
3537 chunk = dobj.decompress(data[pos:pos2])
3538 if chunk:
3538 if chunk:
3539 chunks.append(chunk)
3539 chunks.append(chunk)
3540 pos = pos2
3540 pos = pos2
3541 # Frame should be exhausted, so no finish() API.
3541 # Frame should be exhausted, so no finish() API.
3542
3542
3543 return ''.join(chunks)
3543 return ''.join(chunks)
3544 except Exception as e:
3544 except Exception as e:
3545 raise error.RevlogError(_('revlog decompress error: %s') %
3545 raise error.RevlogError(_('revlog decompress error: %s') %
3546 str(e))
3546 str(e))
3547
3547
3548 def revlogcompressor(self, opts=None):
3548 def revlogcompressor(self, opts=None):
3549 opts = opts or {}
3549 opts = opts or {}
3550 return self.zstdrevlogcompressor(self._module,
3550 return self.zstdrevlogcompressor(self._module,
3551 level=opts.get('level', 3))
3551 level=opts.get('level', 3))
3552
3552
3553 compengines.register(_zstdengine())
3553 compengines.register(_zstdengine())
3554
3554
3555 # convenient shortcut
3555 # convenient shortcut
3556 dst = debugstacktrace
3556 dst = debugstacktrace
General Comments 0
You need to be logged in to leave comments. Login now