Show More
The requested changes are too big and content was truncated. Show full diff
@@ -1,65 +1,65 b'' | |||
|
1 | 1 | # debugshell extension |
|
2 | 2 | """a python shell with repo, changelog & manifest objects""" |
|
3 | 3 | |
|
4 | 4 | from __future__ import absolute_import |
|
5 | 5 | import code |
|
6 | 6 | import mercurial |
|
7 | 7 | import sys |
|
8 | 8 | from mercurial import ( |
|
9 | 9 | demandimport, |
|
10 | 10 | pycompat, |
|
11 | 11 | registrar, |
|
12 | 12 | ) |
|
13 | 13 | |
|
14 | 14 | cmdtable = {} |
|
15 | 15 | command = registrar.command(cmdtable) |
|
16 | 16 | |
|
17 | 17 | |
|
18 | 18 | def pdb(ui, repo, msg, **opts): |
|
19 | 19 | objects = { |
|
20 | 20 | 'mercurial': mercurial, |
|
21 | 21 | 'repo': repo, |
|
22 | 22 | 'cl': repo.changelog, |
|
23 | 23 | 'mf': repo.manifestlog, |
|
24 | 24 | } |
|
25 | 25 | |
|
26 | 26 | code.interact(msg, local=objects) |
|
27 | 27 | |
|
28 | 28 | |
|
29 | 29 | def ipdb(ui, repo, msg, **opts): |
|
30 | 30 | import IPython |
|
31 | 31 | |
|
32 | 32 | cl = repo.changelog |
|
33 | 33 | mf = repo.manifestlog |
|
34 | 34 | cl, mf # use variables to appease pyflakes |
|
35 | 35 | |
|
36 | 36 | IPython.embed() |
|
37 | 37 | |
|
38 | 38 | |
|
39 | 39 | @command(b'debugshell|dbsh', []) |
|
40 | 40 | def debugshell(ui, repo, **opts): |
|
41 | 41 | bannermsg = "loaded repo : %s\n" "using source: %s" % ( |
|
42 | 42 | pycompat.sysstr(repo.root), |
|
43 | 43 | mercurial.__path__[0], |
|
44 | 44 | ) |
|
45 | 45 | |
|
46 | 46 | pdbmap = {'pdb': 'code', 'ipdb': 'IPython'} |
|
47 | 47 | |
|
48 | 48 | debugger = ui.config(b"ui", b"debugger") |
|
49 | 49 | if not debugger: |
|
50 | 50 | debugger = 'pdb' |
|
51 | 51 | else: |
|
52 | 52 | debugger = pycompat.sysstr(debugger) |
|
53 | 53 | |
|
54 | 54 | # if IPython doesn't exist, fallback to code.interact |
|
55 | 55 | try: |
|
56 | 56 | with demandimport.deactivated(): |
|
57 | 57 | __import__(pdbmap[debugger]) |
|
58 | 58 | except ImportError: |
|
59 | ui.warn( | |
|
59 | ui.warnnoi18n( | |
|
60 | 60 | b"%s debugger specified but %s module was not found\n" |
|
61 | 61 | % (debugger, pdbmap[debugger]) |
|
62 | 62 | ) |
|
63 | 63 | debugger = b'pdb' |
|
64 | 64 | |
|
65 | 65 | getattr(sys.modules[__name__], debugger)(ui, repo, bannermsg, **opts) |
@@ -1,3744 +1,3744 b'' | |||
|
1 | 1 | # perf.py - performance test routines |
|
2 | 2 | '''helper extension to measure performance |
|
3 | 3 | |
|
4 | 4 | Configurations |
|
5 | 5 | ============== |
|
6 | 6 | |
|
7 | 7 | ``perf`` |
|
8 | 8 | -------- |
|
9 | 9 | |
|
10 | 10 | ``all-timing`` |
|
11 | 11 | When set, additional statistics will be reported for each benchmark: best, |
|
12 | 12 | worst, median average. If not set only the best timing is reported |
|
13 | 13 | (default: off). |
|
14 | 14 | |
|
15 | 15 | ``presleep`` |
|
16 | 16 | number of second to wait before any group of runs (default: 1) |
|
17 | 17 | |
|
18 | 18 | ``pre-run`` |
|
19 | 19 | number of run to perform before starting measurement. |
|
20 | 20 | |
|
21 | 21 | ``profile-benchmark`` |
|
22 | 22 | Enable profiling for the benchmarked section. |
|
23 | 23 | (The first iteration is benchmarked) |
|
24 | 24 | |
|
25 | 25 | ``run-limits`` |
|
26 | 26 | Control the number of runs each benchmark will perform. The option value |
|
27 | 27 | should be a list of `<time>-<numberofrun>` pairs. After each run the |
|
28 | 28 | conditions are considered in order with the following logic: |
|
29 | 29 | |
|
30 | 30 | If benchmark has been running for <time> seconds, and we have performed |
|
31 | 31 | <numberofrun> iterations, stop the benchmark, |
|
32 | 32 | |
|
33 | 33 | The default value is: `3.0-100, 10.0-3` |
|
34 | 34 | |
|
35 | 35 | ``stub`` |
|
36 | 36 | When set, benchmarks will only be run once, useful for testing |
|
37 | 37 | (default: off) |
|
38 | 38 | ''' |
|
39 | 39 | |
|
40 | 40 | # "historical portability" policy of perf.py: |
|
41 | 41 | # |
|
42 | 42 | # We have to do: |
|
43 | 43 | # - make perf.py "loadable" with as wide Mercurial version as possible |
|
44 | 44 | # This doesn't mean that perf commands work correctly with that Mercurial. |
|
45 | 45 | # BTW, perf.py itself has been available since 1.1 (or eb240755386d). |
|
46 | 46 | # - make historical perf command work correctly with as wide Mercurial |
|
47 | 47 | # version as possible |
|
48 | 48 | # |
|
49 | 49 | # We have to do, if possible with reasonable cost: |
|
50 | 50 | # - make recent perf command for historical feature work correctly |
|
51 | 51 | # with early Mercurial |
|
52 | 52 | # |
|
53 | 53 | # We don't have to do: |
|
54 | 54 | # - make perf command for recent feature work correctly with early |
|
55 | 55 | # Mercurial |
|
56 | 56 | |
|
57 | 57 | from __future__ import absolute_import |
|
58 | 58 | import contextlib |
|
59 | 59 | import functools |
|
60 | 60 | import gc |
|
61 | 61 | import os |
|
62 | 62 | import random |
|
63 | 63 | import shutil |
|
64 | 64 | import struct |
|
65 | 65 | import sys |
|
66 | 66 | import tempfile |
|
67 | 67 | import threading |
|
68 | 68 | import time |
|
69 | 69 | from mercurial import ( |
|
70 | 70 | changegroup, |
|
71 | 71 | cmdutil, |
|
72 | 72 | commands, |
|
73 | 73 | copies, |
|
74 | 74 | error, |
|
75 | 75 | extensions, |
|
76 | 76 | hg, |
|
77 | 77 | mdiff, |
|
78 | 78 | merge, |
|
79 | 79 | revlog, |
|
80 | 80 | util, |
|
81 | 81 | ) |
|
82 | 82 | |
|
83 | 83 | # for "historical portability": |
|
84 | 84 | # try to import modules separately (in dict order), and ignore |
|
85 | 85 | # failure, because these aren't available with early Mercurial |
|
86 | 86 | try: |
|
87 | 87 | from mercurial import branchmap # since 2.5 (or bcee63733aad) |
|
88 | 88 | except ImportError: |
|
89 | 89 | pass |
|
90 | 90 | try: |
|
91 | 91 | from mercurial import obsolete # since 2.3 (or ad0d6c2b3279) |
|
92 | 92 | except ImportError: |
|
93 | 93 | pass |
|
94 | 94 | try: |
|
95 | 95 | from mercurial import registrar # since 3.7 (or 37d50250b696) |
|
96 | 96 | |
|
97 | 97 | dir(registrar) # forcibly load it |
|
98 | 98 | except ImportError: |
|
99 | 99 | registrar = None |
|
100 | 100 | try: |
|
101 | 101 | from mercurial import repoview # since 2.5 (or 3a6ddacb7198) |
|
102 | 102 | except ImportError: |
|
103 | 103 | pass |
|
104 | 104 | try: |
|
105 | 105 | from mercurial.utils import repoviewutil # since 5.0 |
|
106 | 106 | except ImportError: |
|
107 | 107 | repoviewutil = None |
|
108 | 108 | try: |
|
109 | 109 | from mercurial import scmutil # since 1.9 (or 8b252e826c68) |
|
110 | 110 | except ImportError: |
|
111 | 111 | pass |
|
112 | 112 | try: |
|
113 | 113 | from mercurial import setdiscovery # since 1.9 (or cb98fed52495) |
|
114 | 114 | except ImportError: |
|
115 | 115 | pass |
|
116 | 116 | |
|
117 | 117 | try: |
|
118 | 118 | from mercurial import profiling |
|
119 | 119 | except ImportError: |
|
120 | 120 | profiling = None |
|
121 | 121 | |
|
122 | 122 | |
|
123 | 123 | def identity(a): |
|
124 | 124 | return a |
|
125 | 125 | |
|
126 | 126 | |
|
127 | 127 | try: |
|
128 | 128 | from mercurial import pycompat |
|
129 | 129 | |
|
130 | 130 | getargspec = pycompat.getargspec # added to module after 4.5 |
|
131 | 131 | _byteskwargs = pycompat.byteskwargs # since 4.1 (or fbc3f73dc802) |
|
132 | 132 | _sysstr = pycompat.sysstr # since 4.0 (or 2219f4f82ede) |
|
133 | 133 | _bytestr = pycompat.bytestr # since 4.2 (or b70407bd84d5) |
|
134 | 134 | _xrange = pycompat.xrange # since 4.8 (or 7eba8f83129b) |
|
135 | 135 | fsencode = pycompat.fsencode # since 3.9 (or f4a5e0e86a7e) |
|
136 | 136 | if pycompat.ispy3: |
|
137 | 137 | _maxint = sys.maxsize # per py3 docs for replacing maxint |
|
138 | 138 | else: |
|
139 | 139 | _maxint = sys.maxint |
|
140 | 140 | except (NameError, ImportError, AttributeError): |
|
141 | 141 | import inspect |
|
142 | 142 | |
|
143 | 143 | getargspec = inspect.getargspec |
|
144 | 144 | _byteskwargs = identity |
|
145 | 145 | _bytestr = str |
|
146 | 146 | fsencode = identity # no py3 support |
|
147 | 147 | _maxint = sys.maxint # no py3 support |
|
148 | 148 | _sysstr = lambda x: x # no py3 support |
|
149 | 149 | _xrange = xrange |
|
150 | 150 | |
|
151 | 151 | try: |
|
152 | 152 | # 4.7+ |
|
153 | 153 | queue = pycompat.queue.Queue |
|
154 | 154 | except (NameError, AttributeError, ImportError): |
|
155 | 155 | # <4.7. |
|
156 | 156 | try: |
|
157 | 157 | queue = pycompat.queue |
|
158 | 158 | except (NameError, AttributeError, ImportError): |
|
159 | 159 | import Queue as queue |
|
160 | 160 | |
|
161 | 161 | try: |
|
162 | 162 | from mercurial import logcmdutil |
|
163 | 163 | |
|
164 | 164 | makelogtemplater = logcmdutil.maketemplater |
|
165 | 165 | except (AttributeError, ImportError): |
|
166 | 166 | try: |
|
167 | 167 | makelogtemplater = cmdutil.makelogtemplater |
|
168 | 168 | except (AttributeError, ImportError): |
|
169 | 169 | makelogtemplater = None |
|
170 | 170 | |
|
171 | 171 | # for "historical portability": |
|
172 | 172 | # define util.safehasattr forcibly, because util.safehasattr has been |
|
173 | 173 | # available since 1.9.3 (or 94b200a11cf7) |
|
174 | 174 | _undefined = object() |
|
175 | 175 | |
|
176 | 176 | |
|
177 | 177 | def safehasattr(thing, attr): |
|
178 | 178 | return getattr(thing, _sysstr(attr), _undefined) is not _undefined |
|
179 | 179 | |
|
180 | 180 | |
|
181 | 181 | setattr(util, 'safehasattr', safehasattr) |
|
182 | 182 | |
|
183 | 183 | # for "historical portability": |
|
184 | 184 | # define util.timer forcibly, because util.timer has been available |
|
185 | 185 | # since ae5d60bb70c9 |
|
186 | 186 | if safehasattr(time, 'perf_counter'): |
|
187 | 187 | util.timer = time.perf_counter |
|
188 | 188 | elif os.name == b'nt': |
|
189 | 189 | util.timer = time.clock |
|
190 | 190 | else: |
|
191 | 191 | util.timer = time.time |
|
192 | 192 | |
|
193 | 193 | # for "historical portability": |
|
194 | 194 | # use locally defined empty option list, if formatteropts isn't |
|
195 | 195 | # available, because commands.formatteropts has been available since |
|
196 | 196 | # 3.2 (or 7a7eed5176a4), even though formatting itself has been |
|
197 | 197 | # available since 2.2 (or ae5f92e154d3) |
|
198 | 198 | formatteropts = getattr( |
|
199 | 199 | cmdutil, "formatteropts", getattr(commands, "formatteropts", []) |
|
200 | 200 | ) |
|
201 | 201 | |
|
202 | 202 | # for "historical portability": |
|
203 | 203 | # use locally defined option list, if debugrevlogopts isn't available, |
|
204 | 204 | # because commands.debugrevlogopts has been available since 3.7 (or |
|
205 | 205 | # 5606f7d0d063), even though cmdutil.openrevlog() has been available |
|
206 | 206 | # since 1.9 (or a79fea6b3e77). |
|
207 | 207 | revlogopts = getattr( |
|
208 | 208 | cmdutil, |
|
209 | 209 | "debugrevlogopts", |
|
210 | 210 | getattr( |
|
211 | 211 | commands, |
|
212 | 212 | "debugrevlogopts", |
|
213 | 213 | [ |
|
214 | 214 | (b'c', b'changelog', False, b'open changelog'), |
|
215 | 215 | (b'm', b'manifest', False, b'open manifest'), |
|
216 | 216 | (b'', b'dir', False, b'open directory manifest'), |
|
217 | 217 | ], |
|
218 | 218 | ), |
|
219 | 219 | ) |
|
220 | 220 | |
|
221 | 221 | cmdtable = {} |
|
222 | 222 | |
|
223 | 223 | # for "historical portability": |
|
224 | 224 | # define parsealiases locally, because cmdutil.parsealiases has been |
|
225 | 225 | # available since 1.5 (or 6252852b4332) |
|
226 | 226 | def parsealiases(cmd): |
|
227 | 227 | return cmd.split(b"|") |
|
228 | 228 | |
|
229 | 229 | |
|
230 | 230 | if safehasattr(registrar, 'command'): |
|
231 | 231 | command = registrar.command(cmdtable) |
|
232 | 232 | elif safehasattr(cmdutil, 'command'): |
|
233 | 233 | command = cmdutil.command(cmdtable) |
|
234 | 234 | if b'norepo' not in getargspec(command).args: |
|
235 | 235 | # for "historical portability": |
|
236 | 236 | # wrap original cmdutil.command, because "norepo" option has |
|
237 | 237 | # been available since 3.1 (or 75a96326cecb) |
|
238 | 238 | _command = command |
|
239 | 239 | |
|
240 | 240 | def command(name, options=(), synopsis=None, norepo=False): |
|
241 | 241 | if norepo: |
|
242 | 242 | commands.norepo += b' %s' % b' '.join(parsealiases(name)) |
|
243 | 243 | return _command(name, list(options), synopsis) |
|
244 | 244 | |
|
245 | 245 | |
|
246 | 246 | else: |
|
247 | 247 | # for "historical portability": |
|
248 | 248 | # define "@command" annotation locally, because cmdutil.command |
|
249 | 249 | # has been available since 1.9 (or 2daa5179e73f) |
|
250 | 250 | def command(name, options=(), synopsis=None, norepo=False): |
|
251 | 251 | def decorator(func): |
|
252 | 252 | if synopsis: |
|
253 | 253 | cmdtable[name] = func, list(options), synopsis |
|
254 | 254 | else: |
|
255 | 255 | cmdtable[name] = func, list(options) |
|
256 | 256 | if norepo: |
|
257 | 257 | commands.norepo += b' %s' % b' '.join(parsealiases(name)) |
|
258 | 258 | return func |
|
259 | 259 | |
|
260 | 260 | return decorator |
|
261 | 261 | |
|
262 | 262 | |
|
263 | 263 | try: |
|
264 | 264 | import mercurial.registrar |
|
265 | 265 | import mercurial.configitems |
|
266 | 266 | |
|
267 | 267 | configtable = {} |
|
268 | 268 | configitem = mercurial.registrar.configitem(configtable) |
|
269 | 269 | configitem( |
|
270 | 270 | b'perf', |
|
271 | 271 | b'presleep', |
|
272 | 272 | default=mercurial.configitems.dynamicdefault, |
|
273 | 273 | experimental=True, |
|
274 | 274 | ) |
|
275 | 275 | configitem( |
|
276 | 276 | b'perf', |
|
277 | 277 | b'stub', |
|
278 | 278 | default=mercurial.configitems.dynamicdefault, |
|
279 | 279 | experimental=True, |
|
280 | 280 | ) |
|
281 | 281 | configitem( |
|
282 | 282 | b'perf', |
|
283 | 283 | b'parentscount', |
|
284 | 284 | default=mercurial.configitems.dynamicdefault, |
|
285 | 285 | experimental=True, |
|
286 | 286 | ) |
|
287 | 287 | configitem( |
|
288 | 288 | b'perf', |
|
289 | 289 | b'all-timing', |
|
290 | 290 | default=mercurial.configitems.dynamicdefault, |
|
291 | 291 | experimental=True, |
|
292 | 292 | ) |
|
293 | 293 | configitem( |
|
294 | 294 | b'perf', b'pre-run', default=mercurial.configitems.dynamicdefault, |
|
295 | 295 | ) |
|
296 | 296 | configitem( |
|
297 | 297 | b'perf', |
|
298 | 298 | b'profile-benchmark', |
|
299 | 299 | default=mercurial.configitems.dynamicdefault, |
|
300 | 300 | ) |
|
301 | 301 | configitem( |
|
302 | 302 | b'perf', |
|
303 | 303 | b'run-limits', |
|
304 | 304 | default=mercurial.configitems.dynamicdefault, |
|
305 | 305 | experimental=True, |
|
306 | 306 | ) |
|
307 | 307 | except (ImportError, AttributeError): |
|
308 | 308 | pass |
|
309 | 309 | except TypeError: |
|
310 | 310 | # compatibility fix for a11fd395e83f |
|
311 | 311 | # hg version: 5.2 |
|
312 | 312 | configitem( |
|
313 | 313 | b'perf', b'presleep', default=mercurial.configitems.dynamicdefault, |
|
314 | 314 | ) |
|
315 | 315 | configitem( |
|
316 | 316 | b'perf', b'stub', default=mercurial.configitems.dynamicdefault, |
|
317 | 317 | ) |
|
318 | 318 | configitem( |
|
319 | 319 | b'perf', b'parentscount', default=mercurial.configitems.dynamicdefault, |
|
320 | 320 | ) |
|
321 | 321 | configitem( |
|
322 | 322 | b'perf', b'all-timing', default=mercurial.configitems.dynamicdefault, |
|
323 | 323 | ) |
|
324 | 324 | configitem( |
|
325 | 325 | b'perf', b'pre-run', default=mercurial.configitems.dynamicdefault, |
|
326 | 326 | ) |
|
327 | 327 | configitem( |
|
328 | 328 | b'perf', |
|
329 | 329 | b'profile-benchmark', |
|
330 | 330 | default=mercurial.configitems.dynamicdefault, |
|
331 | 331 | ) |
|
332 | 332 | configitem( |
|
333 | 333 | b'perf', b'run-limits', default=mercurial.configitems.dynamicdefault, |
|
334 | 334 | ) |
|
335 | 335 | |
|
336 | 336 | |
|
337 | 337 | def getlen(ui): |
|
338 | 338 | if ui.configbool(b"perf", b"stub", False): |
|
339 | 339 | return lambda x: 1 |
|
340 | 340 | return len |
|
341 | 341 | |
|
342 | 342 | |
|
343 | 343 | class noop(object): |
|
344 | 344 | """dummy context manager""" |
|
345 | 345 | |
|
346 | 346 | def __enter__(self): |
|
347 | 347 | pass |
|
348 | 348 | |
|
349 | 349 | def __exit__(self, *args): |
|
350 | 350 | pass |
|
351 | 351 | |
|
352 | 352 | |
|
353 | 353 | NOOPCTX = noop() |
|
354 | 354 | |
|
355 | 355 | |
|
356 | 356 | def gettimer(ui, opts=None): |
|
357 | 357 | """return a timer function and formatter: (timer, formatter) |
|
358 | 358 | |
|
359 | 359 | This function exists to gather the creation of formatter in a single |
|
360 | 360 | place instead of duplicating it in all performance commands.""" |
|
361 | 361 | |
|
362 | 362 | # enforce an idle period before execution to counteract power management |
|
363 | 363 | # experimental config: perf.presleep |
|
364 | 364 | time.sleep(getint(ui, b"perf", b"presleep", 1)) |
|
365 | 365 | |
|
366 | 366 | if opts is None: |
|
367 | 367 | opts = {} |
|
368 | 368 | # redirect all to stderr unless buffer api is in use |
|
369 | 369 | if not ui._buffers: |
|
370 | 370 | ui = ui.copy() |
|
371 | 371 | uifout = safeattrsetter(ui, b'fout', ignoremissing=True) |
|
372 | 372 | if uifout: |
|
373 | 373 | # for "historical portability": |
|
374 | 374 | # ui.fout/ferr have been available since 1.9 (or 4e1ccd4c2b6d) |
|
375 | 375 | uifout.set(ui.ferr) |
|
376 | 376 | |
|
377 | 377 | # get a formatter |
|
378 | 378 | uiformatter = getattr(ui, 'formatter', None) |
|
379 | 379 | if uiformatter: |
|
380 | 380 | fm = uiformatter(b'perf', opts) |
|
381 | 381 | else: |
|
382 | 382 | # for "historical portability": |
|
383 | 383 | # define formatter locally, because ui.formatter has been |
|
384 | 384 | # available since 2.2 (or ae5f92e154d3) |
|
385 | 385 | from mercurial import node |
|
386 | 386 | |
|
387 | 387 | class defaultformatter(object): |
|
388 | 388 | """Minimized composition of baseformatter and plainformatter |
|
389 | 389 | """ |
|
390 | 390 | |
|
391 | 391 | def __init__(self, ui, topic, opts): |
|
392 | 392 | self._ui = ui |
|
393 | 393 | if ui.debugflag: |
|
394 | 394 | self.hexfunc = node.hex |
|
395 | 395 | else: |
|
396 | 396 | self.hexfunc = node.short |
|
397 | 397 | |
|
398 | 398 | def __nonzero__(self): |
|
399 | 399 | return False |
|
400 | 400 | |
|
401 | 401 | __bool__ = __nonzero__ |
|
402 | 402 | |
|
403 | 403 | def startitem(self): |
|
404 | 404 | pass |
|
405 | 405 | |
|
406 | 406 | def data(self, **data): |
|
407 | 407 | pass |
|
408 | 408 | |
|
409 | 409 | def write(self, fields, deftext, *fielddata, **opts): |
|
410 | 410 | self._ui.write(deftext % fielddata, **opts) |
|
411 | 411 | |
|
412 | 412 | def condwrite(self, cond, fields, deftext, *fielddata, **opts): |
|
413 | 413 | if cond: |
|
414 | 414 | self._ui.write(deftext % fielddata, **opts) |
|
415 | 415 | |
|
416 | 416 | def plain(self, text, **opts): |
|
417 | 417 | self._ui.write(text, **opts) |
|
418 | 418 | |
|
419 | 419 | def end(self): |
|
420 | 420 | pass |
|
421 | 421 | |
|
422 | 422 | fm = defaultformatter(ui, b'perf', opts) |
|
423 | 423 | |
|
424 | 424 | # stub function, runs code only once instead of in a loop |
|
425 | 425 | # experimental config: perf.stub |
|
426 | 426 | if ui.configbool(b"perf", b"stub", False): |
|
427 | 427 | return functools.partial(stub_timer, fm), fm |
|
428 | 428 | |
|
429 | 429 | # experimental config: perf.all-timing |
|
430 | 430 | displayall = ui.configbool(b"perf", b"all-timing", False) |
|
431 | 431 | |
|
432 | 432 | # experimental config: perf.run-limits |
|
433 | 433 | limitspec = ui.configlist(b"perf", b"run-limits", []) |
|
434 | 434 | limits = [] |
|
435 | 435 | for item in limitspec: |
|
436 | 436 | parts = item.split(b'-', 1) |
|
437 | 437 | if len(parts) < 2: |
|
438 | 438 | ui.warn((b'malformatted run limit entry, missing "-": %s\n' % item)) |
|
439 | 439 | continue |
|
440 | 440 | try: |
|
441 | 441 | time_limit = float(_sysstr(parts[0])) |
|
442 | 442 | except ValueError as e: |
|
443 | 443 | ui.warn( |
|
444 | 444 | ( |
|
445 | 445 | b'malformatted run limit entry, %s: %s\n' |
|
446 | 446 | % (_bytestr(e), item) |
|
447 | 447 | ) |
|
448 | 448 | ) |
|
449 | 449 | continue |
|
450 | 450 | try: |
|
451 | 451 | run_limit = int(_sysstr(parts[1])) |
|
452 | 452 | except ValueError as e: |
|
453 | 453 | ui.warn( |
|
454 | 454 | ( |
|
455 | 455 | b'malformatted run limit entry, %s: %s\n' |
|
456 | 456 | % (_bytestr(e), item) |
|
457 | 457 | ) |
|
458 | 458 | ) |
|
459 | 459 | continue |
|
460 | 460 | limits.append((time_limit, run_limit)) |
|
461 | 461 | if not limits: |
|
462 | 462 | limits = DEFAULTLIMITS |
|
463 | 463 | |
|
464 | 464 | profiler = None |
|
465 | 465 | if profiling is not None: |
|
466 | 466 | if ui.configbool(b"perf", b"profile-benchmark", False): |
|
467 | 467 | profiler = profiling.profile(ui) |
|
468 | 468 | |
|
469 | 469 | prerun = getint(ui, b"perf", b"pre-run", 0) |
|
470 | 470 | t = functools.partial( |
|
471 | 471 | _timer, |
|
472 | 472 | fm, |
|
473 | 473 | displayall=displayall, |
|
474 | 474 | limits=limits, |
|
475 | 475 | prerun=prerun, |
|
476 | 476 | profiler=profiler, |
|
477 | 477 | ) |
|
478 | 478 | return t, fm |
|
479 | 479 | |
|
480 | 480 | |
|
481 | 481 | def stub_timer(fm, func, setup=None, title=None): |
|
482 | 482 | if setup is not None: |
|
483 | 483 | setup() |
|
484 | 484 | func() |
|
485 | 485 | |
|
486 | 486 | |
|
487 | 487 | @contextlib.contextmanager |
|
488 | 488 | def timeone(): |
|
489 | 489 | r = [] |
|
490 | 490 | ostart = os.times() |
|
491 | 491 | cstart = util.timer() |
|
492 | 492 | yield r |
|
493 | 493 | cstop = util.timer() |
|
494 | 494 | ostop = os.times() |
|
495 | 495 | a, b = ostart, ostop |
|
496 | 496 | r.append((cstop - cstart, b[0] - a[0], b[1] - a[1])) |
|
497 | 497 | |
|
498 | 498 | |
|
499 | 499 | # list of stop condition (elapsed time, minimal run count) |
|
500 | 500 | DEFAULTLIMITS = ( |
|
501 | 501 | (3.0, 100), |
|
502 | 502 | (10.0, 3), |
|
503 | 503 | ) |
|
504 | 504 | |
|
505 | 505 | |
|
506 | 506 | def _timer( |
|
507 | 507 | fm, |
|
508 | 508 | func, |
|
509 | 509 | setup=None, |
|
510 | 510 | title=None, |
|
511 | 511 | displayall=False, |
|
512 | 512 | limits=DEFAULTLIMITS, |
|
513 | 513 | prerun=0, |
|
514 | 514 | profiler=None, |
|
515 | 515 | ): |
|
516 | 516 | gc.collect() |
|
517 | 517 | results = [] |
|
518 | 518 | begin = util.timer() |
|
519 | 519 | count = 0 |
|
520 | 520 | if profiler is None: |
|
521 | 521 | profiler = NOOPCTX |
|
522 | 522 | for i in range(prerun): |
|
523 | 523 | if setup is not None: |
|
524 | 524 | setup() |
|
525 | 525 | func() |
|
526 | 526 | keepgoing = True |
|
527 | 527 | while keepgoing: |
|
528 | 528 | if setup is not None: |
|
529 | 529 | setup() |
|
530 | 530 | with profiler: |
|
531 | 531 | with timeone() as item: |
|
532 | 532 | r = func() |
|
533 | 533 | profiler = NOOPCTX |
|
534 | 534 | count += 1 |
|
535 | 535 | results.append(item[0]) |
|
536 | 536 | cstop = util.timer() |
|
537 | 537 | # Look for a stop condition. |
|
538 | 538 | elapsed = cstop - begin |
|
539 | 539 | for t, mincount in limits: |
|
540 | 540 | if elapsed >= t and count >= mincount: |
|
541 | 541 | keepgoing = False |
|
542 | 542 | break |
|
543 | 543 | |
|
544 | 544 | formatone(fm, results, title=title, result=r, displayall=displayall) |
|
545 | 545 | |
|
546 | 546 | |
|
547 | 547 | def formatone(fm, timings, title=None, result=None, displayall=False): |
|
548 | 548 | |
|
549 | 549 | count = len(timings) |
|
550 | 550 | |
|
551 | 551 | fm.startitem() |
|
552 | 552 | |
|
553 | 553 | if title: |
|
554 | 554 | fm.write(b'title', b'! %s\n', title) |
|
555 | 555 | if result: |
|
556 | 556 | fm.write(b'result', b'! result: %s\n', result) |
|
557 | 557 | |
|
558 | 558 | def display(role, entry): |
|
559 | 559 | prefix = b'' |
|
560 | 560 | if role != b'best': |
|
561 | 561 | prefix = b'%s.' % role |
|
562 | 562 | fm.plain(b'!') |
|
563 | 563 | fm.write(prefix + b'wall', b' wall %f', entry[0]) |
|
564 | 564 | fm.write(prefix + b'comb', b' comb %f', entry[1] + entry[2]) |
|
565 | 565 | fm.write(prefix + b'user', b' user %f', entry[1]) |
|
566 | 566 | fm.write(prefix + b'sys', b' sys %f', entry[2]) |
|
567 | 567 | fm.write(prefix + b'count', b' (%s of %%d)' % role, count) |
|
568 | 568 | fm.plain(b'\n') |
|
569 | 569 | |
|
570 | 570 | timings.sort() |
|
571 | 571 | min_val = timings[0] |
|
572 | 572 | display(b'best', min_val) |
|
573 | 573 | if displayall: |
|
574 | 574 | max_val = timings[-1] |
|
575 | 575 | display(b'max', max_val) |
|
576 | 576 | avg = tuple([sum(x) / count for x in zip(*timings)]) |
|
577 | 577 | display(b'avg', avg) |
|
578 | 578 | median = timings[len(timings) // 2] |
|
579 | 579 | display(b'median', median) |
|
580 | 580 | |
|
581 | 581 | |
|
582 | 582 | # utilities for historical portability |
|
583 | 583 | |
|
584 | 584 | |
|
585 | 585 | def getint(ui, section, name, default): |
|
586 | 586 | # for "historical portability": |
|
587 | 587 | # ui.configint has been available since 1.9 (or fa2b596db182) |
|
588 | 588 | v = ui.config(section, name, None) |
|
589 | 589 | if v is None: |
|
590 | 590 | return default |
|
591 | 591 | try: |
|
592 | 592 | return int(v) |
|
593 | 593 | except ValueError: |
|
594 | 594 | raise error.ConfigError( |
|
595 | 595 | b"%s.%s is not an integer ('%s')" % (section, name, v) |
|
596 | 596 | ) |
|
597 | 597 | |
|
598 | 598 | |
|
599 | 599 | def safeattrsetter(obj, name, ignoremissing=False): |
|
600 | 600 | """Ensure that 'obj' has 'name' attribute before subsequent setattr |
|
601 | 601 | |
|
602 | 602 | This function is aborted, if 'obj' doesn't have 'name' attribute |
|
603 | 603 | at runtime. This avoids overlooking removal of an attribute, which |
|
604 | 604 | breaks assumption of performance measurement, in the future. |
|
605 | 605 | |
|
606 | 606 | This function returns the object to (1) assign a new value, and |
|
607 | 607 | (2) restore an original value to the attribute. |
|
608 | 608 | |
|
609 | 609 | If 'ignoremissing' is true, missing 'name' attribute doesn't cause |
|
610 | 610 | abortion, and this function returns None. This is useful to |
|
611 | 611 | examine an attribute, which isn't ensured in all Mercurial |
|
612 | 612 | versions. |
|
613 | 613 | """ |
|
614 | 614 | if not util.safehasattr(obj, name): |
|
615 | 615 | if ignoremissing: |
|
616 | 616 | return None |
|
617 | 617 | raise error.Abort( |
|
618 | 618 | ( |
|
619 | 619 | b"missing attribute %s of %s might break assumption" |
|
620 | 620 | b" of performance measurement" |
|
621 | 621 | ) |
|
622 | 622 | % (name, obj) |
|
623 | 623 | ) |
|
624 | 624 | |
|
625 | 625 | origvalue = getattr(obj, _sysstr(name)) |
|
626 | 626 | |
|
627 | 627 | class attrutil(object): |
|
628 | 628 | def set(self, newvalue): |
|
629 | 629 | setattr(obj, _sysstr(name), newvalue) |
|
630 | 630 | |
|
631 | 631 | def restore(self): |
|
632 | 632 | setattr(obj, _sysstr(name), origvalue) |
|
633 | 633 | |
|
634 | 634 | return attrutil() |
|
635 | 635 | |
|
636 | 636 | |
|
637 | 637 | # utilities to examine each internal API changes |
|
638 | 638 | |
|
639 | 639 | |
|
640 | 640 | def getbranchmapsubsettable(): |
|
641 | 641 | # for "historical portability": |
|
642 | 642 | # subsettable is defined in: |
|
643 | 643 | # - branchmap since 2.9 (or 175c6fd8cacc) |
|
644 | 644 | # - repoview since 2.5 (or 59a9f18d4587) |
|
645 | 645 | # - repoviewutil since 5.0 |
|
646 | 646 | for mod in (branchmap, repoview, repoviewutil): |
|
647 | 647 | subsettable = getattr(mod, 'subsettable', None) |
|
648 | 648 | if subsettable: |
|
649 | 649 | return subsettable |
|
650 | 650 | |
|
651 | 651 | # bisecting in bcee63733aad::59a9f18d4587 can reach here (both |
|
652 | 652 | # branchmap and repoview modules exist, but subsettable attribute |
|
653 | 653 | # doesn't) |
|
654 | 654 | raise error.Abort( |
|
655 | 655 | b"perfbranchmap not available with this Mercurial", |
|
656 | 656 | hint=b"use 2.5 or later", |
|
657 | 657 | ) |
|
658 | 658 | |
|
659 | 659 | |
|
660 | 660 | def getsvfs(repo): |
|
661 | 661 | """Return appropriate object to access files under .hg/store |
|
662 | 662 | """ |
|
663 | 663 | # for "historical portability": |
|
664 | 664 | # repo.svfs has been available since 2.3 (or 7034365089bf) |
|
665 | 665 | svfs = getattr(repo, 'svfs', None) |
|
666 | 666 | if svfs: |
|
667 | 667 | return svfs |
|
668 | 668 | else: |
|
669 | 669 | return getattr(repo, 'sopener') |
|
670 | 670 | |
|
671 | 671 | |
|
672 | 672 | def getvfs(repo): |
|
673 | 673 | """Return appropriate object to access files under .hg |
|
674 | 674 | """ |
|
675 | 675 | # for "historical portability": |
|
676 | 676 | # repo.vfs has been available since 2.3 (or 7034365089bf) |
|
677 | 677 | vfs = getattr(repo, 'vfs', None) |
|
678 | 678 | if vfs: |
|
679 | 679 | return vfs |
|
680 | 680 | else: |
|
681 | 681 | return getattr(repo, 'opener') |
|
682 | 682 | |
|
683 | 683 | |
|
684 | 684 | def repocleartagscachefunc(repo): |
|
685 | 685 | """Return the function to clear tags cache according to repo internal API |
|
686 | 686 | """ |
|
687 | 687 | if util.safehasattr(repo, b'_tagscache'): # since 2.0 (or 9dca7653b525) |
|
688 | 688 | # in this case, setattr(repo, '_tagscache', None) or so isn't |
|
689 | 689 | # correct way to clear tags cache, because existing code paths |
|
690 | 690 | # expect _tagscache to be a structured object. |
|
691 | 691 | def clearcache(): |
|
692 | 692 | # _tagscache has been filteredpropertycache since 2.5 (or |
|
693 | 693 | # 98c867ac1330), and delattr() can't work in such case |
|
694 | 694 | if b'_tagscache' in vars(repo): |
|
695 | 695 | del repo.__dict__[b'_tagscache'] |
|
696 | 696 | |
|
697 | 697 | return clearcache |
|
698 | 698 | |
|
699 | 699 | repotags = safeattrsetter(repo, b'_tags', ignoremissing=True) |
|
700 | 700 | if repotags: # since 1.4 (or 5614a628d173) |
|
701 | 701 | return lambda: repotags.set(None) |
|
702 | 702 | |
|
703 | 703 | repotagscache = safeattrsetter(repo, b'tagscache', ignoremissing=True) |
|
704 | 704 | if repotagscache: # since 0.6 (or d7df759d0e97) |
|
705 | 705 | return lambda: repotagscache.set(None) |
|
706 | 706 | |
|
707 | 707 | # Mercurial earlier than 0.6 (or d7df759d0e97) logically reaches |
|
708 | 708 | # this point, but it isn't so problematic, because: |
|
709 | 709 | # - repo.tags of such Mercurial isn't "callable", and repo.tags() |
|
710 | 710 | # in perftags() causes failure soon |
|
711 | 711 | # - perf.py itself has been available since 1.1 (or eb240755386d) |
|
712 | 712 | raise error.Abort(b"tags API of this hg command is unknown") |
|
713 | 713 | |
|
714 | 714 | |
|
715 | 715 | # utilities to clear cache |
|
716 | 716 | |
|
717 | 717 | |
|
718 | 718 | def clearfilecache(obj, attrname): |
|
719 | 719 | unfiltered = getattr(obj, 'unfiltered', None) |
|
720 | 720 | if unfiltered is not None: |
|
721 | 721 | obj = obj.unfiltered() |
|
722 | 722 | if attrname in vars(obj): |
|
723 | 723 | delattr(obj, attrname) |
|
724 | 724 | obj._filecache.pop(attrname, None) |
|
725 | 725 | |
|
726 | 726 | |
|
727 | 727 | def clearchangelog(repo): |
|
728 | 728 | if repo is not repo.unfiltered(): |
|
729 | 729 | object.__setattr__(repo, r'_clcachekey', None) |
|
730 | 730 | object.__setattr__(repo, r'_clcache', None) |
|
731 | 731 | clearfilecache(repo.unfiltered(), 'changelog') |
|
732 | 732 | |
|
733 | 733 | |
|
734 | 734 | # perf commands |
|
735 | 735 | |
|
736 | 736 | |
|
737 | 737 | @command(b'perfwalk', formatteropts) |
|
738 | 738 | def perfwalk(ui, repo, *pats, **opts): |
|
739 | 739 | opts = _byteskwargs(opts) |
|
740 | 740 | timer, fm = gettimer(ui, opts) |
|
741 | 741 | m = scmutil.match(repo[None], pats, {}) |
|
742 | 742 | timer( |
|
743 | 743 | lambda: len( |
|
744 | 744 | list( |
|
745 | 745 | repo.dirstate.walk(m, subrepos=[], unknown=True, ignored=False) |
|
746 | 746 | ) |
|
747 | 747 | ) |
|
748 | 748 | ) |
|
749 | 749 | fm.end() |
|
750 | 750 | |
|
751 | 751 | |
|
752 | 752 | @command(b'perfannotate', formatteropts) |
|
753 | 753 | def perfannotate(ui, repo, f, **opts): |
|
754 | 754 | opts = _byteskwargs(opts) |
|
755 | 755 | timer, fm = gettimer(ui, opts) |
|
756 | 756 | fc = repo[b'.'][f] |
|
757 | 757 | timer(lambda: len(fc.annotate(True))) |
|
758 | 758 | fm.end() |
|
759 | 759 | |
|
760 | 760 | |
|
761 | 761 | @command( |
|
762 | 762 | b'perfstatus', |
|
763 | 763 | [(b'u', b'unknown', False, b'ask status to look for unknown files')] |
|
764 | 764 | + formatteropts, |
|
765 | 765 | ) |
|
766 | 766 | def perfstatus(ui, repo, **opts): |
|
767 | 767 | opts = _byteskwargs(opts) |
|
768 | 768 | # m = match.always(repo.root, repo.getcwd()) |
|
769 | 769 | # timer(lambda: sum(map(len, repo.dirstate.status(m, [], False, False, |
|
770 | 770 | # False)))) |
|
771 | 771 | timer, fm = gettimer(ui, opts) |
|
772 | 772 | timer(lambda: sum(map(len, repo.status(unknown=opts[b'unknown'])))) |
|
773 | 773 | fm.end() |
|
774 | 774 | |
|
775 | 775 | |
|
776 | 776 | @command(b'perfaddremove', formatteropts) |
|
777 | 777 | def perfaddremove(ui, repo, **opts): |
|
778 | 778 | opts = _byteskwargs(opts) |
|
779 | 779 | timer, fm = gettimer(ui, opts) |
|
780 | 780 | try: |
|
781 | 781 | oldquiet = repo.ui.quiet |
|
782 | 782 | repo.ui.quiet = True |
|
783 | 783 | matcher = scmutil.match(repo[None]) |
|
784 | 784 | opts[b'dry_run'] = True |
|
785 | 785 | if b'uipathfn' in getargspec(scmutil.addremove).args: |
|
786 | 786 | uipathfn = scmutil.getuipathfn(repo) |
|
787 | 787 | timer(lambda: scmutil.addremove(repo, matcher, b"", uipathfn, opts)) |
|
788 | 788 | else: |
|
789 | 789 | timer(lambda: scmutil.addremove(repo, matcher, b"", opts)) |
|
790 | 790 | finally: |
|
791 | 791 | repo.ui.quiet = oldquiet |
|
792 | 792 | fm.end() |
|
793 | 793 | |
|
794 | 794 | |
|
795 | 795 | def clearcaches(cl): |
|
796 | 796 | # behave somewhat consistently across internal API changes |
|
797 | 797 | if util.safehasattr(cl, b'clearcaches'): |
|
798 | 798 | cl.clearcaches() |
|
799 | 799 | elif util.safehasattr(cl, b'_nodecache'): |
|
800 | 800 | from mercurial.node import nullid, nullrev |
|
801 | 801 | |
|
802 | 802 | cl._nodecache = {nullid: nullrev} |
|
803 | 803 | cl._nodepos = None |
|
804 | 804 | |
|
805 | 805 | |
|
806 | 806 | @command(b'perfheads', formatteropts) |
|
807 | 807 | def perfheads(ui, repo, **opts): |
|
808 | 808 | """benchmark the computation of a changelog heads""" |
|
809 | 809 | opts = _byteskwargs(opts) |
|
810 | 810 | timer, fm = gettimer(ui, opts) |
|
811 | 811 | cl = repo.changelog |
|
812 | 812 | |
|
813 | 813 | def s(): |
|
814 | 814 | clearcaches(cl) |
|
815 | 815 | |
|
816 | 816 | def d(): |
|
817 | 817 | len(cl.headrevs()) |
|
818 | 818 | |
|
819 | 819 | timer(d, setup=s) |
|
820 | 820 | fm.end() |
|
821 | 821 | |
|
822 | 822 | |
|
823 | 823 | @command( |
|
824 | 824 | b'perftags', |
|
825 | 825 | formatteropts |
|
826 | 826 | + [(b'', b'clear-revlogs', False, b'refresh changelog and manifest'),], |
|
827 | 827 | ) |
|
828 | 828 | def perftags(ui, repo, **opts): |
|
829 | 829 | opts = _byteskwargs(opts) |
|
830 | 830 | timer, fm = gettimer(ui, opts) |
|
831 | 831 | repocleartagscache = repocleartagscachefunc(repo) |
|
832 | 832 | clearrevlogs = opts[b'clear_revlogs'] |
|
833 | 833 | |
|
834 | 834 | def s(): |
|
835 | 835 | if clearrevlogs: |
|
836 | 836 | clearchangelog(repo) |
|
837 | 837 | clearfilecache(repo.unfiltered(), 'manifest') |
|
838 | 838 | repocleartagscache() |
|
839 | 839 | |
|
840 | 840 | def t(): |
|
841 | 841 | return len(repo.tags()) |
|
842 | 842 | |
|
843 | 843 | timer(t, setup=s) |
|
844 | 844 | fm.end() |
|
845 | 845 | |
|
846 | 846 | |
|
847 | 847 | @command(b'perfancestors', formatteropts) |
|
848 | 848 | def perfancestors(ui, repo, **opts): |
|
849 | 849 | opts = _byteskwargs(opts) |
|
850 | 850 | timer, fm = gettimer(ui, opts) |
|
851 | 851 | heads = repo.changelog.headrevs() |
|
852 | 852 | |
|
853 | 853 | def d(): |
|
854 | 854 | for a in repo.changelog.ancestors(heads): |
|
855 | 855 | pass |
|
856 | 856 | |
|
857 | 857 | timer(d) |
|
858 | 858 | fm.end() |
|
859 | 859 | |
|
860 | 860 | |
|
861 | 861 | @command(b'perfancestorset', formatteropts) |
|
862 | 862 | def perfancestorset(ui, repo, revset, **opts): |
|
863 | 863 | opts = _byteskwargs(opts) |
|
864 | 864 | timer, fm = gettimer(ui, opts) |
|
865 | 865 | revs = repo.revs(revset) |
|
866 | 866 | heads = repo.changelog.headrevs() |
|
867 | 867 | |
|
868 | 868 | def d(): |
|
869 | 869 | s = repo.changelog.ancestors(heads) |
|
870 | 870 | for rev in revs: |
|
871 | 871 | rev in s |
|
872 | 872 | |
|
873 | 873 | timer(d) |
|
874 | 874 | fm.end() |
|
875 | 875 | |
|
876 | 876 | |
|
877 | 877 | @command(b'perfdiscovery', formatteropts, b'PATH') |
|
878 | 878 | def perfdiscovery(ui, repo, path, **opts): |
|
879 | 879 | """benchmark discovery between local repo and the peer at given path |
|
880 | 880 | """ |
|
881 | 881 | repos = [repo, None] |
|
882 | 882 | timer, fm = gettimer(ui, opts) |
|
883 | 883 | path = ui.expandpath(path) |
|
884 | 884 | |
|
885 | 885 | def s(): |
|
886 | 886 | repos[1] = hg.peer(ui, opts, path) |
|
887 | 887 | |
|
888 | 888 | def d(): |
|
889 | 889 | setdiscovery.findcommonheads(ui, *repos) |
|
890 | 890 | |
|
891 | 891 | timer(d, setup=s) |
|
892 | 892 | fm.end() |
|
893 | 893 | |
|
894 | 894 | |
|
895 | 895 | @command( |
|
896 | 896 | b'perfbookmarks', |
|
897 | 897 | formatteropts |
|
898 | 898 | + [(b'', b'clear-revlogs', False, b'refresh changelog and manifest'),], |
|
899 | 899 | ) |
|
900 | 900 | def perfbookmarks(ui, repo, **opts): |
|
901 | 901 | """benchmark parsing bookmarks from disk to memory""" |
|
902 | 902 | opts = _byteskwargs(opts) |
|
903 | 903 | timer, fm = gettimer(ui, opts) |
|
904 | 904 | |
|
905 | 905 | clearrevlogs = opts[b'clear_revlogs'] |
|
906 | 906 | |
|
907 | 907 | def s(): |
|
908 | 908 | if clearrevlogs: |
|
909 | 909 | clearchangelog(repo) |
|
910 | 910 | clearfilecache(repo, b'_bookmarks') |
|
911 | 911 | |
|
912 | 912 | def d(): |
|
913 | 913 | repo._bookmarks |
|
914 | 914 | |
|
915 | 915 | timer(d, setup=s) |
|
916 | 916 | fm.end() |
|
917 | 917 | |
|
918 | 918 | |
|
919 | 919 | @command(b'perfbundleread', formatteropts, b'BUNDLE') |
|
920 | 920 | def perfbundleread(ui, repo, bundlepath, **opts): |
|
921 | 921 | """Benchmark reading of bundle files. |
|
922 | 922 | |
|
923 | 923 | This command is meant to isolate the I/O part of bundle reading as |
|
924 | 924 | much as possible. |
|
925 | 925 | """ |
|
926 | 926 | from mercurial import ( |
|
927 | 927 | bundle2, |
|
928 | 928 | exchange, |
|
929 | 929 | streamclone, |
|
930 | 930 | ) |
|
931 | 931 | |
|
932 | 932 | opts = _byteskwargs(opts) |
|
933 | 933 | |
|
934 | 934 | def makebench(fn): |
|
935 | 935 | def run(): |
|
936 | 936 | with open(bundlepath, b'rb') as fh: |
|
937 | 937 | bundle = exchange.readbundle(ui, fh, bundlepath) |
|
938 | 938 | fn(bundle) |
|
939 | 939 | |
|
940 | 940 | return run |
|
941 | 941 | |
|
942 | 942 | def makereadnbytes(size): |
|
943 | 943 | def run(): |
|
944 | 944 | with open(bundlepath, b'rb') as fh: |
|
945 | 945 | bundle = exchange.readbundle(ui, fh, bundlepath) |
|
946 | 946 | while bundle.read(size): |
|
947 | 947 | pass |
|
948 | 948 | |
|
949 | 949 | return run |
|
950 | 950 | |
|
951 | 951 | def makestdioread(size): |
|
952 | 952 | def run(): |
|
953 | 953 | with open(bundlepath, b'rb') as fh: |
|
954 | 954 | while fh.read(size): |
|
955 | 955 | pass |
|
956 | 956 | |
|
957 | 957 | return run |
|
958 | 958 | |
|
959 | 959 | # bundle1 |
|
960 | 960 | |
|
961 | 961 | def deltaiter(bundle): |
|
962 | 962 | for delta in bundle.deltaiter(): |
|
963 | 963 | pass |
|
964 | 964 | |
|
965 | 965 | def iterchunks(bundle): |
|
966 | 966 | for chunk in bundle.getchunks(): |
|
967 | 967 | pass |
|
968 | 968 | |
|
969 | 969 | # bundle2 |
|
970 | 970 | |
|
971 | 971 | def forwardchunks(bundle): |
|
972 | 972 | for chunk in bundle._forwardchunks(): |
|
973 | 973 | pass |
|
974 | 974 | |
|
975 | 975 | def iterparts(bundle): |
|
976 | 976 | for part in bundle.iterparts(): |
|
977 | 977 | pass |
|
978 | 978 | |
|
979 | 979 | def iterpartsseekable(bundle): |
|
980 | 980 | for part in bundle.iterparts(seekable=True): |
|
981 | 981 | pass |
|
982 | 982 | |
|
983 | 983 | def seek(bundle): |
|
984 | 984 | for part in bundle.iterparts(seekable=True): |
|
985 | 985 | part.seek(0, os.SEEK_END) |
|
986 | 986 | |
|
987 | 987 | def makepartreadnbytes(size): |
|
988 | 988 | def run(): |
|
989 | 989 | with open(bundlepath, b'rb') as fh: |
|
990 | 990 | bundle = exchange.readbundle(ui, fh, bundlepath) |
|
991 | 991 | for part in bundle.iterparts(): |
|
992 | 992 | while part.read(size): |
|
993 | 993 | pass |
|
994 | 994 | |
|
995 | 995 | return run |
|
996 | 996 | |
|
997 | 997 | benches = [ |
|
998 | 998 | (makestdioread(8192), b'read(8k)'), |
|
999 | 999 | (makestdioread(16384), b'read(16k)'), |
|
1000 | 1000 | (makestdioread(32768), b'read(32k)'), |
|
1001 | 1001 | (makestdioread(131072), b'read(128k)'), |
|
1002 | 1002 | ] |
|
1003 | 1003 | |
|
1004 | 1004 | with open(bundlepath, b'rb') as fh: |
|
1005 | 1005 | bundle = exchange.readbundle(ui, fh, bundlepath) |
|
1006 | 1006 | |
|
1007 | 1007 | if isinstance(bundle, changegroup.cg1unpacker): |
|
1008 | 1008 | benches.extend( |
|
1009 | 1009 | [ |
|
1010 | 1010 | (makebench(deltaiter), b'cg1 deltaiter()'), |
|
1011 | 1011 | (makebench(iterchunks), b'cg1 getchunks()'), |
|
1012 | 1012 | (makereadnbytes(8192), b'cg1 read(8k)'), |
|
1013 | 1013 | (makereadnbytes(16384), b'cg1 read(16k)'), |
|
1014 | 1014 | (makereadnbytes(32768), b'cg1 read(32k)'), |
|
1015 | 1015 | (makereadnbytes(131072), b'cg1 read(128k)'), |
|
1016 | 1016 | ] |
|
1017 | 1017 | ) |
|
1018 | 1018 | elif isinstance(bundle, bundle2.unbundle20): |
|
1019 | 1019 | benches.extend( |
|
1020 | 1020 | [ |
|
1021 | 1021 | (makebench(forwardchunks), b'bundle2 forwardchunks()'), |
|
1022 | 1022 | (makebench(iterparts), b'bundle2 iterparts()'), |
|
1023 | 1023 | ( |
|
1024 | 1024 | makebench(iterpartsseekable), |
|
1025 | 1025 | b'bundle2 iterparts() seekable', |
|
1026 | 1026 | ), |
|
1027 | 1027 | (makebench(seek), b'bundle2 part seek()'), |
|
1028 | 1028 | (makepartreadnbytes(8192), b'bundle2 part read(8k)'), |
|
1029 | 1029 | (makepartreadnbytes(16384), b'bundle2 part read(16k)'), |
|
1030 | 1030 | (makepartreadnbytes(32768), b'bundle2 part read(32k)'), |
|
1031 | 1031 | (makepartreadnbytes(131072), b'bundle2 part read(128k)'), |
|
1032 | 1032 | ] |
|
1033 | 1033 | ) |
|
1034 | 1034 | elif isinstance(bundle, streamclone.streamcloneapplier): |
|
1035 | 1035 | raise error.Abort(b'stream clone bundles not supported') |
|
1036 | 1036 | else: |
|
1037 | 1037 | raise error.Abort(b'unhandled bundle type: %s' % type(bundle)) |
|
1038 | 1038 | |
|
1039 | 1039 | for fn, title in benches: |
|
1040 | 1040 | timer, fm = gettimer(ui, opts) |
|
1041 | 1041 | timer(fn, title=title) |
|
1042 | 1042 | fm.end() |
|
1043 | 1043 | |
|
1044 | 1044 | |
|
1045 | 1045 | @command( |
|
1046 | 1046 | b'perfchangegroupchangelog', |
|
1047 | 1047 | formatteropts |
|
1048 | 1048 | + [ |
|
1049 | 1049 | (b'', b'cgversion', b'02', b'changegroup version'), |
|
1050 | 1050 | (b'r', b'rev', b'', b'revisions to add to changegroup'), |
|
1051 | 1051 | ], |
|
1052 | 1052 | ) |
|
1053 | 1053 | def perfchangegroupchangelog(ui, repo, cgversion=b'02', rev=None, **opts): |
|
1054 | 1054 | """Benchmark producing a changelog group for a changegroup. |
|
1055 | 1055 | |
|
1056 | 1056 | This measures the time spent processing the changelog during a |
|
1057 | 1057 | bundle operation. This occurs during `hg bundle` and on a server |
|
1058 | 1058 | processing a `getbundle` wire protocol request (handles clones |
|
1059 | 1059 | and pull requests). |
|
1060 | 1060 | |
|
1061 | 1061 | By default, all revisions are added to the changegroup. |
|
1062 | 1062 | """ |
|
1063 | 1063 | opts = _byteskwargs(opts) |
|
1064 | 1064 | cl = repo.changelog |
|
1065 | 1065 | nodes = [cl.lookup(r) for r in repo.revs(rev or b'all()')] |
|
1066 | 1066 | bundler = changegroup.getbundler(cgversion, repo) |
|
1067 | 1067 | |
|
1068 | 1068 | def d(): |
|
1069 | 1069 | state, chunks = bundler._generatechangelog(cl, nodes) |
|
1070 | 1070 | for chunk in chunks: |
|
1071 | 1071 | pass |
|
1072 | 1072 | |
|
1073 | 1073 | timer, fm = gettimer(ui, opts) |
|
1074 | 1074 | |
|
1075 | 1075 | # Terminal printing can interfere with timing. So disable it. |
|
1076 | 1076 | with ui.configoverride({(b'progress', b'disable'): True}): |
|
1077 | 1077 | timer(d) |
|
1078 | 1078 | |
|
1079 | 1079 | fm.end() |
|
1080 | 1080 | |
|
1081 | 1081 | |
|
1082 | 1082 | @command(b'perfdirs', formatteropts) |
|
1083 | 1083 | def perfdirs(ui, repo, **opts): |
|
1084 | 1084 | opts = _byteskwargs(opts) |
|
1085 | 1085 | timer, fm = gettimer(ui, opts) |
|
1086 | 1086 | dirstate = repo.dirstate |
|
1087 | 1087 | b'a' in dirstate |
|
1088 | 1088 | |
|
1089 | 1089 | def d(): |
|
1090 | 1090 | dirstate.hasdir(b'a') |
|
1091 | 1091 | del dirstate._map._dirs |
|
1092 | 1092 | |
|
1093 | 1093 | timer(d) |
|
1094 | 1094 | fm.end() |
|
1095 | 1095 | |
|
1096 | 1096 | |
|
1097 | 1097 | @command(b'perfdirstate', formatteropts) |
|
1098 | 1098 | def perfdirstate(ui, repo, **opts): |
|
1099 | 1099 | opts = _byteskwargs(opts) |
|
1100 | 1100 | timer, fm = gettimer(ui, opts) |
|
1101 | 1101 | b"a" in repo.dirstate |
|
1102 | 1102 | |
|
1103 | 1103 | def d(): |
|
1104 | 1104 | repo.dirstate.invalidate() |
|
1105 | 1105 | b"a" in repo.dirstate |
|
1106 | 1106 | |
|
1107 | 1107 | timer(d) |
|
1108 | 1108 | fm.end() |
|
1109 | 1109 | |
|
1110 | 1110 | |
|
1111 | 1111 | @command(b'perfdirstatedirs', formatteropts) |
|
1112 | 1112 | def perfdirstatedirs(ui, repo, **opts): |
|
1113 | 1113 | opts = _byteskwargs(opts) |
|
1114 | 1114 | timer, fm = gettimer(ui, opts) |
|
1115 | 1115 | b"a" in repo.dirstate |
|
1116 | 1116 | |
|
1117 | 1117 | def d(): |
|
1118 | 1118 | repo.dirstate.hasdir(b"a") |
|
1119 | 1119 | del repo.dirstate._map._dirs |
|
1120 | 1120 | |
|
1121 | 1121 | timer(d) |
|
1122 | 1122 | fm.end() |
|
1123 | 1123 | |
|
1124 | 1124 | |
|
1125 | 1125 | @command(b'perfdirstatefoldmap', formatteropts) |
|
1126 | 1126 | def perfdirstatefoldmap(ui, repo, **opts): |
|
1127 | 1127 | opts = _byteskwargs(opts) |
|
1128 | 1128 | timer, fm = gettimer(ui, opts) |
|
1129 | 1129 | dirstate = repo.dirstate |
|
1130 | 1130 | b'a' in dirstate |
|
1131 | 1131 | |
|
1132 | 1132 | def d(): |
|
1133 | 1133 | dirstate._map.filefoldmap.get(b'a') |
|
1134 | 1134 | del dirstate._map.filefoldmap |
|
1135 | 1135 | |
|
1136 | 1136 | timer(d) |
|
1137 | 1137 | fm.end() |
|
1138 | 1138 | |
|
1139 | 1139 | |
|
1140 | 1140 | @command(b'perfdirfoldmap', formatteropts) |
|
1141 | 1141 | def perfdirfoldmap(ui, repo, **opts): |
|
1142 | 1142 | opts = _byteskwargs(opts) |
|
1143 | 1143 | timer, fm = gettimer(ui, opts) |
|
1144 | 1144 | dirstate = repo.dirstate |
|
1145 | 1145 | b'a' in dirstate |
|
1146 | 1146 | |
|
1147 | 1147 | def d(): |
|
1148 | 1148 | dirstate._map.dirfoldmap.get(b'a') |
|
1149 | 1149 | del dirstate._map.dirfoldmap |
|
1150 | 1150 | del dirstate._map._dirs |
|
1151 | 1151 | |
|
1152 | 1152 | timer(d) |
|
1153 | 1153 | fm.end() |
|
1154 | 1154 | |
|
1155 | 1155 | |
|
1156 | 1156 | @command(b'perfdirstatewrite', formatteropts) |
|
1157 | 1157 | def perfdirstatewrite(ui, repo, **opts): |
|
1158 | 1158 | opts = _byteskwargs(opts) |
|
1159 | 1159 | timer, fm = gettimer(ui, opts) |
|
1160 | 1160 | ds = repo.dirstate |
|
1161 | 1161 | b"a" in ds |
|
1162 | 1162 | |
|
1163 | 1163 | def d(): |
|
1164 | 1164 | ds._dirty = True |
|
1165 | 1165 | ds.write(repo.currenttransaction()) |
|
1166 | 1166 | |
|
1167 | 1167 | timer(d) |
|
1168 | 1168 | fm.end() |
|
1169 | 1169 | |
|
1170 | 1170 | |
|
1171 | 1171 | def _getmergerevs(repo, opts): |
|
1172 | 1172 | """parse command argument to return rev involved in merge |
|
1173 | 1173 | |
|
1174 | 1174 | input: options dictionnary with `rev`, `from` and `bse` |
|
1175 | 1175 | output: (localctx, otherctx, basectx) |
|
1176 | 1176 | """ |
|
1177 | 1177 | if opts[b'from']: |
|
1178 | 1178 | fromrev = scmutil.revsingle(repo, opts[b'from']) |
|
1179 | 1179 | wctx = repo[fromrev] |
|
1180 | 1180 | else: |
|
1181 | 1181 | wctx = repo[None] |
|
1182 | 1182 | # we don't want working dir files to be stat'd in the benchmark, so |
|
1183 | 1183 | # prime that cache |
|
1184 | 1184 | wctx.dirty() |
|
1185 | 1185 | rctx = scmutil.revsingle(repo, opts[b'rev'], opts[b'rev']) |
|
1186 | 1186 | if opts[b'base']: |
|
1187 | 1187 | fromrev = scmutil.revsingle(repo, opts[b'base']) |
|
1188 | 1188 | ancestor = repo[fromrev] |
|
1189 | 1189 | else: |
|
1190 | 1190 | ancestor = wctx.ancestor(rctx) |
|
1191 | 1191 | return (wctx, rctx, ancestor) |
|
1192 | 1192 | |
|
1193 | 1193 | |
|
1194 | 1194 | @command( |
|
1195 | 1195 | b'perfmergecalculate', |
|
1196 | 1196 | [ |
|
1197 | 1197 | (b'r', b'rev', b'.', b'rev to merge against'), |
|
1198 | 1198 | (b'', b'from', b'', b'rev to merge from'), |
|
1199 | 1199 | (b'', b'base', b'', b'the revision to use as base'), |
|
1200 | 1200 | ] |
|
1201 | 1201 | + formatteropts, |
|
1202 | 1202 | ) |
|
1203 | 1203 | def perfmergecalculate(ui, repo, **opts): |
|
1204 | 1204 | opts = _byteskwargs(opts) |
|
1205 | 1205 | timer, fm = gettimer(ui, opts) |
|
1206 | 1206 | |
|
1207 | 1207 | wctx, rctx, ancestor = _getmergerevs(repo, opts) |
|
1208 | 1208 | |
|
1209 | 1209 | def d(): |
|
1210 | 1210 | # acceptremote is True because we don't want prompts in the middle of |
|
1211 | 1211 | # our benchmark |
|
1212 | 1212 | merge.calculateupdates( |
|
1213 | 1213 | repo, |
|
1214 | 1214 | wctx, |
|
1215 | 1215 | rctx, |
|
1216 | 1216 | [ancestor], |
|
1217 | 1217 | branchmerge=False, |
|
1218 | 1218 | force=False, |
|
1219 | 1219 | acceptremote=True, |
|
1220 | 1220 | followcopies=True, |
|
1221 | 1221 | ) |
|
1222 | 1222 | |
|
1223 | 1223 | timer(d) |
|
1224 | 1224 | fm.end() |
|
1225 | 1225 | |
|
1226 | 1226 | |
|
1227 | 1227 | @command( |
|
1228 | 1228 | b'perfmergecopies', |
|
1229 | 1229 | [ |
|
1230 | 1230 | (b'r', b'rev', b'.', b'rev to merge against'), |
|
1231 | 1231 | (b'', b'from', b'', b'rev to merge from'), |
|
1232 | 1232 | (b'', b'base', b'', b'the revision to use as base'), |
|
1233 | 1233 | ] |
|
1234 | 1234 | + formatteropts, |
|
1235 | 1235 | ) |
|
1236 | 1236 | def perfmergecopies(ui, repo, **opts): |
|
1237 | 1237 | """measure runtime of `copies.mergecopies`""" |
|
1238 | 1238 | opts = _byteskwargs(opts) |
|
1239 | 1239 | timer, fm = gettimer(ui, opts) |
|
1240 | 1240 | wctx, rctx, ancestor = _getmergerevs(repo, opts) |
|
1241 | 1241 | |
|
1242 | 1242 | def d(): |
|
1243 | 1243 | # acceptremote is True because we don't want prompts in the middle of |
|
1244 | 1244 | # our benchmark |
|
1245 | 1245 | copies.mergecopies(repo, wctx, rctx, ancestor) |
|
1246 | 1246 | |
|
1247 | 1247 | timer(d) |
|
1248 | 1248 | fm.end() |
|
1249 | 1249 | |
|
1250 | 1250 | |
|
1251 | 1251 | @command(b'perfpathcopies', [], b"REV REV") |
|
1252 | 1252 | def perfpathcopies(ui, repo, rev1, rev2, **opts): |
|
1253 | 1253 | """benchmark the copy tracing logic""" |
|
1254 | 1254 | opts = _byteskwargs(opts) |
|
1255 | 1255 | timer, fm = gettimer(ui, opts) |
|
1256 | 1256 | ctx1 = scmutil.revsingle(repo, rev1, rev1) |
|
1257 | 1257 | ctx2 = scmutil.revsingle(repo, rev2, rev2) |
|
1258 | 1258 | |
|
1259 | 1259 | def d(): |
|
1260 | 1260 | copies.pathcopies(ctx1, ctx2) |
|
1261 | 1261 | |
|
1262 | 1262 | timer(d) |
|
1263 | 1263 | fm.end() |
|
1264 | 1264 | |
|
1265 | 1265 | |
|
1266 | 1266 | @command( |
|
1267 | 1267 | b'perfphases', |
|
1268 | 1268 | [(b'', b'full', False, b'include file reading time too'),], |
|
1269 | 1269 | b"", |
|
1270 | 1270 | ) |
|
1271 | 1271 | def perfphases(ui, repo, **opts): |
|
1272 | 1272 | """benchmark phasesets computation""" |
|
1273 | 1273 | opts = _byteskwargs(opts) |
|
1274 | 1274 | timer, fm = gettimer(ui, opts) |
|
1275 | 1275 | _phases = repo._phasecache |
|
1276 | 1276 | full = opts.get(b'full') |
|
1277 | 1277 | |
|
1278 | 1278 | def d(): |
|
1279 | 1279 | phases = _phases |
|
1280 | 1280 | if full: |
|
1281 | 1281 | clearfilecache(repo, b'_phasecache') |
|
1282 | 1282 | phases = repo._phasecache |
|
1283 | 1283 | phases.invalidate() |
|
1284 | 1284 | phases.loadphaserevs(repo) |
|
1285 | 1285 | |
|
1286 | 1286 | timer(d) |
|
1287 | 1287 | fm.end() |
|
1288 | 1288 | |
|
1289 | 1289 | |
|
1290 | 1290 | @command(b'perfphasesremote', [], b"[DEST]") |
|
1291 | 1291 | def perfphasesremote(ui, repo, dest=None, **opts): |
|
1292 | 1292 | """benchmark time needed to analyse phases of the remote server""" |
|
1293 | 1293 | from mercurial.node import bin |
|
1294 | 1294 | from mercurial import ( |
|
1295 | 1295 | exchange, |
|
1296 | 1296 | hg, |
|
1297 | 1297 | phases, |
|
1298 | 1298 | ) |
|
1299 | 1299 | |
|
1300 | 1300 | opts = _byteskwargs(opts) |
|
1301 | 1301 | timer, fm = gettimer(ui, opts) |
|
1302 | 1302 | |
|
1303 | 1303 | path = ui.paths.getpath(dest, default=(b'default-push', b'default')) |
|
1304 | 1304 | if not path: |
|
1305 | 1305 | raise error.Abort( |
|
1306 | 1306 | b'default repository not configured!', |
|
1307 | 1307 | hint=b"see 'hg help config.paths'", |
|
1308 | 1308 | ) |
|
1309 | 1309 | dest = path.pushloc or path.loc |
|
1310 | ui.status(b'analysing phase of %s\n' % util.hidepassword(dest)) | |
|
1310 | ui.statusnoi18n(b'analysing phase of %s\n' % util.hidepassword(dest)) | |
|
1311 | 1311 | other = hg.peer(repo, opts, dest) |
|
1312 | 1312 | |
|
1313 | 1313 | # easier to perform discovery through the operation |
|
1314 | 1314 | op = exchange.pushoperation(repo, other) |
|
1315 | 1315 | exchange._pushdiscoverychangeset(op) |
|
1316 | 1316 | |
|
1317 | 1317 | remotesubset = op.fallbackheads |
|
1318 | 1318 | |
|
1319 | 1319 | with other.commandexecutor() as e: |
|
1320 | 1320 | remotephases = e.callcommand( |
|
1321 | 1321 | b'listkeys', {b'namespace': b'phases'} |
|
1322 | 1322 | ).result() |
|
1323 | 1323 | del other |
|
1324 | 1324 | publishing = remotephases.get(b'publishing', False) |
|
1325 | 1325 | if publishing: |
|
1326 | ui.status(b'publishing: yes\n') | |
|
1326 | ui.statusnoi18n(b'publishing: yes\n') | |
|
1327 | 1327 | else: |
|
1328 | ui.status(b'publishing: no\n') | |
|
1328 | ui.statusnoi18n(b'publishing: no\n') | |
|
1329 | 1329 | |
|
1330 | 1330 | nodemap = repo.changelog.nodemap |
|
1331 | 1331 | nonpublishroots = 0 |
|
1332 | 1332 | for nhex, phase in remotephases.iteritems(): |
|
1333 | 1333 | if nhex == b'publishing': # ignore data related to publish option |
|
1334 | 1334 | continue |
|
1335 | 1335 | node = bin(nhex) |
|
1336 | 1336 | if node in nodemap and int(phase): |
|
1337 | 1337 | nonpublishroots += 1 |
|
1338 | ui.status(b'number of roots: %d\n' % len(remotephases)) | |
|
1339 | ui.status(b'number of known non public roots: %d\n' % nonpublishroots) | |
|
1338 | ui.statusnoi18n(b'number of roots: %d\n' % len(remotephases)) | |
|
1339 | ui.statusnoi18n(b'number of known non public roots: %d\n' % nonpublishroots) | |
|
1340 | 1340 | |
|
1341 | 1341 | def d(): |
|
1342 | 1342 | phases.remotephasessummary(repo, remotesubset, remotephases) |
|
1343 | 1343 | |
|
1344 | 1344 | timer(d) |
|
1345 | 1345 | fm.end() |
|
1346 | 1346 | |
|
1347 | 1347 | |
|
1348 | 1348 | @command( |
|
1349 | 1349 | b'perfmanifest', |
|
1350 | 1350 | [ |
|
1351 | 1351 | (b'm', b'manifest-rev', False, b'Look up a manifest node revision'), |
|
1352 | 1352 | (b'', b'clear-disk', False, b'clear on-disk caches too'), |
|
1353 | 1353 | ] |
|
1354 | 1354 | + formatteropts, |
|
1355 | 1355 | b'REV|NODE', |
|
1356 | 1356 | ) |
|
1357 | 1357 | def perfmanifest(ui, repo, rev, manifest_rev=False, clear_disk=False, **opts): |
|
1358 | 1358 | """benchmark the time to read a manifest from disk and return a usable |
|
1359 | 1359 | dict-like object |
|
1360 | 1360 | |
|
1361 | 1361 | Manifest caches are cleared before retrieval.""" |
|
1362 | 1362 | opts = _byteskwargs(opts) |
|
1363 | 1363 | timer, fm = gettimer(ui, opts) |
|
1364 | 1364 | if not manifest_rev: |
|
1365 | 1365 | ctx = scmutil.revsingle(repo, rev, rev) |
|
1366 | 1366 | t = ctx.manifestnode() |
|
1367 | 1367 | else: |
|
1368 | 1368 | from mercurial.node import bin |
|
1369 | 1369 | |
|
1370 | 1370 | if len(rev) == 40: |
|
1371 | 1371 | t = bin(rev) |
|
1372 | 1372 | else: |
|
1373 | 1373 | try: |
|
1374 | 1374 | rev = int(rev) |
|
1375 | 1375 | |
|
1376 | 1376 | if util.safehasattr(repo.manifestlog, b'getstorage'): |
|
1377 | 1377 | t = repo.manifestlog.getstorage(b'').node(rev) |
|
1378 | 1378 | else: |
|
1379 | 1379 | t = repo.manifestlog._revlog.lookup(rev) |
|
1380 | 1380 | except ValueError: |
|
1381 | 1381 | raise error.Abort( |
|
1382 | 1382 | b'manifest revision must be integer or full ' b'node' |
|
1383 | 1383 | ) |
|
1384 | 1384 | |
|
1385 | 1385 | def d(): |
|
1386 | 1386 | repo.manifestlog.clearcaches(clear_persisted_data=clear_disk) |
|
1387 | 1387 | repo.manifestlog[t].read() |
|
1388 | 1388 | |
|
1389 | 1389 | timer(d) |
|
1390 | 1390 | fm.end() |
|
1391 | 1391 | |
|
1392 | 1392 | |
|
1393 | 1393 | @command(b'perfchangeset', formatteropts) |
|
1394 | 1394 | def perfchangeset(ui, repo, rev, **opts): |
|
1395 | 1395 | opts = _byteskwargs(opts) |
|
1396 | 1396 | timer, fm = gettimer(ui, opts) |
|
1397 | 1397 | n = scmutil.revsingle(repo, rev).node() |
|
1398 | 1398 | |
|
1399 | 1399 | def d(): |
|
1400 | 1400 | repo.changelog.read(n) |
|
1401 | 1401 | # repo.changelog._cache = None |
|
1402 | 1402 | |
|
1403 | 1403 | timer(d) |
|
1404 | 1404 | fm.end() |
|
1405 | 1405 | |
|
1406 | 1406 | |
|
1407 | 1407 | @command(b'perfignore', formatteropts) |
|
1408 | 1408 | def perfignore(ui, repo, **opts): |
|
1409 | 1409 | """benchmark operation related to computing ignore""" |
|
1410 | 1410 | opts = _byteskwargs(opts) |
|
1411 | 1411 | timer, fm = gettimer(ui, opts) |
|
1412 | 1412 | dirstate = repo.dirstate |
|
1413 | 1413 | |
|
1414 | 1414 | def setupone(): |
|
1415 | 1415 | dirstate.invalidate() |
|
1416 | 1416 | clearfilecache(dirstate, b'_ignore') |
|
1417 | 1417 | |
|
1418 | 1418 | def runone(): |
|
1419 | 1419 | dirstate._ignore |
|
1420 | 1420 | |
|
1421 | 1421 | timer(runone, setup=setupone, title=b"load") |
|
1422 | 1422 | fm.end() |
|
1423 | 1423 | |
|
1424 | 1424 | |
|
1425 | 1425 | @command( |
|
1426 | 1426 | b'perfindex', |
|
1427 | 1427 | [ |
|
1428 | 1428 | (b'', b'rev', [], b'revision to be looked up (default tip)'), |
|
1429 | 1429 | (b'', b'no-lookup', None, b'do not revision lookup post creation'), |
|
1430 | 1430 | ] |
|
1431 | 1431 | + formatteropts, |
|
1432 | 1432 | ) |
|
1433 | 1433 | def perfindex(ui, repo, **opts): |
|
1434 | 1434 | """benchmark index creation time followed by a lookup |
|
1435 | 1435 | |
|
1436 | 1436 | The default is to look `tip` up. Depending on the index implementation, |
|
1437 | 1437 | the revision looked up can matters. For example, an implementation |
|
1438 | 1438 | scanning the index will have a faster lookup time for `--rev tip` than for |
|
1439 | 1439 | `--rev 0`. The number of looked up revisions and their order can also |
|
1440 | 1440 | matters. |
|
1441 | 1441 | |
|
1442 | 1442 | Example of useful set to test: |
|
1443 | 1443 | * tip |
|
1444 | 1444 | * 0 |
|
1445 | 1445 | * -10: |
|
1446 | 1446 | * :10 |
|
1447 | 1447 | * -10: + :10 |
|
1448 | 1448 | * :10: + -10: |
|
1449 | 1449 | * -10000: |
|
1450 | 1450 | * -10000: + 0 |
|
1451 | 1451 | |
|
1452 | 1452 | It is not currently possible to check for lookup of a missing node. For |
|
1453 | 1453 | deeper lookup benchmarking, checkout the `perfnodemap` command.""" |
|
1454 | 1454 | import mercurial.revlog |
|
1455 | 1455 | |
|
1456 | 1456 | opts = _byteskwargs(opts) |
|
1457 | 1457 | timer, fm = gettimer(ui, opts) |
|
1458 | 1458 | mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg |
|
1459 | 1459 | if opts[b'no_lookup']: |
|
1460 | 1460 | if opts['rev']: |
|
1461 | 1461 | raise error.Abort('--no-lookup and --rev are mutually exclusive') |
|
1462 | 1462 | nodes = [] |
|
1463 | 1463 | elif not opts[b'rev']: |
|
1464 | 1464 | nodes = [repo[b"tip"].node()] |
|
1465 | 1465 | else: |
|
1466 | 1466 | revs = scmutil.revrange(repo, opts[b'rev']) |
|
1467 | 1467 | cl = repo.changelog |
|
1468 | 1468 | nodes = [cl.node(r) for r in revs] |
|
1469 | 1469 | |
|
1470 | 1470 | unfi = repo.unfiltered() |
|
1471 | 1471 | # find the filecache func directly |
|
1472 | 1472 | # This avoid polluting the benchmark with the filecache logic |
|
1473 | 1473 | makecl = unfi.__class__.changelog.func |
|
1474 | 1474 | |
|
1475 | 1475 | def setup(): |
|
1476 | 1476 | # probably not necessary, but for good measure |
|
1477 | 1477 | clearchangelog(unfi) |
|
1478 | 1478 | |
|
1479 | 1479 | def d(): |
|
1480 | 1480 | cl = makecl(unfi) |
|
1481 | 1481 | for n in nodes: |
|
1482 | 1482 | cl.rev(n) |
|
1483 | 1483 | |
|
1484 | 1484 | timer(d, setup=setup) |
|
1485 | 1485 | fm.end() |
|
1486 | 1486 | |
|
1487 | 1487 | |
|
1488 | 1488 | @command( |
|
1489 | 1489 | b'perfnodemap', |
|
1490 | 1490 | [ |
|
1491 | 1491 | (b'', b'rev', [], b'revision to be looked up (default tip)'), |
|
1492 | 1492 | (b'', b'clear-caches', True, b'clear revlog cache between calls'), |
|
1493 | 1493 | ] |
|
1494 | 1494 | + formatteropts, |
|
1495 | 1495 | ) |
|
1496 | 1496 | def perfnodemap(ui, repo, **opts): |
|
1497 | 1497 | """benchmark the time necessary to look up revision from a cold nodemap |
|
1498 | 1498 | |
|
1499 | 1499 | Depending on the implementation, the amount and order of revision we look |
|
1500 | 1500 | up can varies. Example of useful set to test: |
|
1501 | 1501 | * tip |
|
1502 | 1502 | * 0 |
|
1503 | 1503 | * -10: |
|
1504 | 1504 | * :10 |
|
1505 | 1505 | * -10: + :10 |
|
1506 | 1506 | * :10: + -10: |
|
1507 | 1507 | * -10000: |
|
1508 | 1508 | * -10000: + 0 |
|
1509 | 1509 | |
|
1510 | 1510 | The command currently focus on valid binary lookup. Benchmarking for |
|
1511 | 1511 | hexlookup, prefix lookup and missing lookup would also be valuable. |
|
1512 | 1512 | """ |
|
1513 | 1513 | import mercurial.revlog |
|
1514 | 1514 | |
|
1515 | 1515 | opts = _byteskwargs(opts) |
|
1516 | 1516 | timer, fm = gettimer(ui, opts) |
|
1517 | 1517 | mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg |
|
1518 | 1518 | |
|
1519 | 1519 | unfi = repo.unfiltered() |
|
1520 | 1520 | clearcaches = opts['clear_caches'] |
|
1521 | 1521 | # find the filecache func directly |
|
1522 | 1522 | # This avoid polluting the benchmark with the filecache logic |
|
1523 | 1523 | makecl = unfi.__class__.changelog.func |
|
1524 | 1524 | if not opts[b'rev']: |
|
1525 | 1525 | raise error.Abort('use --rev to specify revisions to look up') |
|
1526 | 1526 | revs = scmutil.revrange(repo, opts[b'rev']) |
|
1527 | 1527 | cl = repo.changelog |
|
1528 | 1528 | nodes = [cl.node(r) for r in revs] |
|
1529 | 1529 | |
|
1530 | 1530 | # use a list to pass reference to a nodemap from one closure to the next |
|
1531 | 1531 | nodeget = [None] |
|
1532 | 1532 | |
|
1533 | 1533 | def setnodeget(): |
|
1534 | 1534 | # probably not necessary, but for good measure |
|
1535 | 1535 | clearchangelog(unfi) |
|
1536 | 1536 | nodeget[0] = makecl(unfi).nodemap.get |
|
1537 | 1537 | |
|
1538 | 1538 | def d(): |
|
1539 | 1539 | get = nodeget[0] |
|
1540 | 1540 | for n in nodes: |
|
1541 | 1541 | get(n) |
|
1542 | 1542 | |
|
1543 | 1543 | setup = None |
|
1544 | 1544 | if clearcaches: |
|
1545 | 1545 | |
|
1546 | 1546 | def setup(): |
|
1547 | 1547 | setnodeget() |
|
1548 | 1548 | |
|
1549 | 1549 | else: |
|
1550 | 1550 | setnodeget() |
|
1551 | 1551 | d() # prewarm the data structure |
|
1552 | 1552 | timer(d, setup=setup) |
|
1553 | 1553 | fm.end() |
|
1554 | 1554 | |
|
1555 | 1555 | |
|
1556 | 1556 | @command(b'perfstartup', formatteropts) |
|
1557 | 1557 | def perfstartup(ui, repo, **opts): |
|
1558 | 1558 | opts = _byteskwargs(opts) |
|
1559 | 1559 | timer, fm = gettimer(ui, opts) |
|
1560 | 1560 | |
|
1561 | 1561 | def d(): |
|
1562 | 1562 | if os.name != r'nt': |
|
1563 | 1563 | os.system( |
|
1564 | 1564 | b"HGRCPATH= %s version -q > /dev/null" % fsencode(sys.argv[0]) |
|
1565 | 1565 | ) |
|
1566 | 1566 | else: |
|
1567 | 1567 | os.environ[r'HGRCPATH'] = r' ' |
|
1568 | 1568 | os.system(r"%s version -q > NUL" % sys.argv[0]) |
|
1569 | 1569 | |
|
1570 | 1570 | timer(d) |
|
1571 | 1571 | fm.end() |
|
1572 | 1572 | |
|
1573 | 1573 | |
|
1574 | 1574 | @command(b'perfparents', formatteropts) |
|
1575 | 1575 | def perfparents(ui, repo, **opts): |
|
1576 | 1576 | """benchmark the time necessary to fetch one changeset's parents. |
|
1577 | 1577 | |
|
1578 | 1578 | The fetch is done using the `node identifier`, traversing all object layers |
|
1579 | 1579 | from the repository object. The first N revisions will be used for this |
|
1580 | 1580 | benchmark. N is controlled by the ``perf.parentscount`` config option |
|
1581 | 1581 | (default: 1000). |
|
1582 | 1582 | """ |
|
1583 | 1583 | opts = _byteskwargs(opts) |
|
1584 | 1584 | timer, fm = gettimer(ui, opts) |
|
1585 | 1585 | # control the number of commits perfparents iterates over |
|
1586 | 1586 | # experimental config: perf.parentscount |
|
1587 | 1587 | count = getint(ui, b"perf", b"parentscount", 1000) |
|
1588 | 1588 | if len(repo.changelog) < count: |
|
1589 | 1589 | raise error.Abort(b"repo needs %d commits for this test" % count) |
|
1590 | 1590 | repo = repo.unfiltered() |
|
1591 | 1591 | nl = [repo.changelog.node(i) for i in _xrange(count)] |
|
1592 | 1592 | |
|
1593 | 1593 | def d(): |
|
1594 | 1594 | for n in nl: |
|
1595 | 1595 | repo.changelog.parents(n) |
|
1596 | 1596 | |
|
1597 | 1597 | timer(d) |
|
1598 | 1598 | fm.end() |
|
1599 | 1599 | |
|
1600 | 1600 | |
|
1601 | 1601 | @command(b'perfctxfiles', formatteropts) |
|
1602 | 1602 | def perfctxfiles(ui, repo, x, **opts): |
|
1603 | 1603 | opts = _byteskwargs(opts) |
|
1604 | 1604 | x = int(x) |
|
1605 | 1605 | timer, fm = gettimer(ui, opts) |
|
1606 | 1606 | |
|
1607 | 1607 | def d(): |
|
1608 | 1608 | len(repo[x].files()) |
|
1609 | 1609 | |
|
1610 | 1610 | timer(d) |
|
1611 | 1611 | fm.end() |
|
1612 | 1612 | |
|
1613 | 1613 | |
|
1614 | 1614 | @command(b'perfrawfiles', formatteropts) |
|
1615 | 1615 | def perfrawfiles(ui, repo, x, **opts): |
|
1616 | 1616 | opts = _byteskwargs(opts) |
|
1617 | 1617 | x = int(x) |
|
1618 | 1618 | timer, fm = gettimer(ui, opts) |
|
1619 | 1619 | cl = repo.changelog |
|
1620 | 1620 | |
|
1621 | 1621 | def d(): |
|
1622 | 1622 | len(cl.read(x)[3]) |
|
1623 | 1623 | |
|
1624 | 1624 | timer(d) |
|
1625 | 1625 | fm.end() |
|
1626 | 1626 | |
|
1627 | 1627 | |
|
1628 | 1628 | @command(b'perflookup', formatteropts) |
|
1629 | 1629 | def perflookup(ui, repo, rev, **opts): |
|
1630 | 1630 | opts = _byteskwargs(opts) |
|
1631 | 1631 | timer, fm = gettimer(ui, opts) |
|
1632 | 1632 | timer(lambda: len(repo.lookup(rev))) |
|
1633 | 1633 | fm.end() |
|
1634 | 1634 | |
|
1635 | 1635 | |
|
1636 | 1636 | @command( |
|
1637 | 1637 | b'perflinelogedits', |
|
1638 | 1638 | [ |
|
1639 | 1639 | (b'n', b'edits', 10000, b'number of edits'), |
|
1640 | 1640 | (b'', b'max-hunk-lines', 10, b'max lines in a hunk'), |
|
1641 | 1641 | ], |
|
1642 | 1642 | norepo=True, |
|
1643 | 1643 | ) |
|
1644 | 1644 | def perflinelogedits(ui, **opts): |
|
1645 | 1645 | from mercurial import linelog |
|
1646 | 1646 | |
|
1647 | 1647 | opts = _byteskwargs(opts) |
|
1648 | 1648 | |
|
1649 | 1649 | edits = opts[b'edits'] |
|
1650 | 1650 | maxhunklines = opts[b'max_hunk_lines'] |
|
1651 | 1651 | |
|
1652 | 1652 | maxb1 = 100000 |
|
1653 | 1653 | random.seed(0) |
|
1654 | 1654 | randint = random.randint |
|
1655 | 1655 | currentlines = 0 |
|
1656 | 1656 | arglist = [] |
|
1657 | 1657 | for rev in _xrange(edits): |
|
1658 | 1658 | a1 = randint(0, currentlines) |
|
1659 | 1659 | a2 = randint(a1, min(currentlines, a1 + maxhunklines)) |
|
1660 | 1660 | b1 = randint(0, maxb1) |
|
1661 | 1661 | b2 = randint(b1, b1 + maxhunklines) |
|
1662 | 1662 | currentlines += (b2 - b1) - (a2 - a1) |
|
1663 | 1663 | arglist.append((rev, a1, a2, b1, b2)) |
|
1664 | 1664 | |
|
1665 | 1665 | def d(): |
|
1666 | 1666 | ll = linelog.linelog() |
|
1667 | 1667 | for args in arglist: |
|
1668 | 1668 | ll.replacelines(*args) |
|
1669 | 1669 | |
|
1670 | 1670 | timer, fm = gettimer(ui, opts) |
|
1671 | 1671 | timer(d) |
|
1672 | 1672 | fm.end() |
|
1673 | 1673 | |
|
1674 | 1674 | |
|
1675 | 1675 | @command(b'perfrevrange', formatteropts) |
|
1676 | 1676 | def perfrevrange(ui, repo, *specs, **opts): |
|
1677 | 1677 | opts = _byteskwargs(opts) |
|
1678 | 1678 | timer, fm = gettimer(ui, opts) |
|
1679 | 1679 | revrange = scmutil.revrange |
|
1680 | 1680 | timer(lambda: len(revrange(repo, specs))) |
|
1681 | 1681 | fm.end() |
|
1682 | 1682 | |
|
1683 | 1683 | |
|
1684 | 1684 | @command(b'perfnodelookup', formatteropts) |
|
1685 | 1685 | def perfnodelookup(ui, repo, rev, **opts): |
|
1686 | 1686 | opts = _byteskwargs(opts) |
|
1687 | 1687 | timer, fm = gettimer(ui, opts) |
|
1688 | 1688 | import mercurial.revlog |
|
1689 | 1689 | |
|
1690 | 1690 | mercurial.revlog._prereadsize = 2 ** 24 # disable lazy parser in old hg |
|
1691 | 1691 | n = scmutil.revsingle(repo, rev).node() |
|
1692 | 1692 | cl = mercurial.revlog.revlog(getsvfs(repo), b"00changelog.i") |
|
1693 | 1693 | |
|
1694 | 1694 | def d(): |
|
1695 | 1695 | cl.rev(n) |
|
1696 | 1696 | clearcaches(cl) |
|
1697 | 1697 | |
|
1698 | 1698 | timer(d) |
|
1699 | 1699 | fm.end() |
|
1700 | 1700 | |
|
1701 | 1701 | |
|
1702 | 1702 | @command( |
|
1703 | 1703 | b'perflog', |
|
1704 | 1704 | [(b'', b'rename', False, b'ask log to follow renames')] + formatteropts, |
|
1705 | 1705 | ) |
|
1706 | 1706 | def perflog(ui, repo, rev=None, **opts): |
|
1707 | 1707 | opts = _byteskwargs(opts) |
|
1708 | 1708 | if rev is None: |
|
1709 | 1709 | rev = [] |
|
1710 | 1710 | timer, fm = gettimer(ui, opts) |
|
1711 | 1711 | ui.pushbuffer() |
|
1712 | 1712 | timer( |
|
1713 | 1713 | lambda: commands.log( |
|
1714 | 1714 | ui, repo, rev=rev, date=b'', user=b'', copies=opts.get(b'rename') |
|
1715 | 1715 | ) |
|
1716 | 1716 | ) |
|
1717 | 1717 | ui.popbuffer() |
|
1718 | 1718 | fm.end() |
|
1719 | 1719 | |
|
1720 | 1720 | |
|
1721 | 1721 | @command(b'perfmoonwalk', formatteropts) |
|
1722 | 1722 | def perfmoonwalk(ui, repo, **opts): |
|
1723 | 1723 | """benchmark walking the changelog backwards |
|
1724 | 1724 | |
|
1725 | 1725 | This also loads the changelog data for each revision in the changelog. |
|
1726 | 1726 | """ |
|
1727 | 1727 | opts = _byteskwargs(opts) |
|
1728 | 1728 | timer, fm = gettimer(ui, opts) |
|
1729 | 1729 | |
|
1730 | 1730 | def moonwalk(): |
|
1731 | 1731 | for i in repo.changelog.revs(start=(len(repo) - 1), stop=-1): |
|
1732 | 1732 | ctx = repo[i] |
|
1733 | 1733 | ctx.branch() # read changelog data (in addition to the index) |
|
1734 | 1734 | |
|
1735 | 1735 | timer(moonwalk) |
|
1736 | 1736 | fm.end() |
|
1737 | 1737 | |
|
1738 | 1738 | |
|
1739 | 1739 | @command( |
|
1740 | 1740 | b'perftemplating', |
|
1741 | 1741 | [(b'r', b'rev', [], b'revisions to run the template on'),] + formatteropts, |
|
1742 | 1742 | ) |
|
1743 | 1743 | def perftemplating(ui, repo, testedtemplate=None, **opts): |
|
1744 | 1744 | """test the rendering time of a given template""" |
|
1745 | 1745 | if makelogtemplater is None: |
|
1746 | 1746 | raise error.Abort( |
|
1747 | 1747 | b"perftemplating not available with this Mercurial", |
|
1748 | 1748 | hint=b"use 4.3 or later", |
|
1749 | 1749 | ) |
|
1750 | 1750 | |
|
1751 | 1751 | opts = _byteskwargs(opts) |
|
1752 | 1752 | |
|
1753 | 1753 | nullui = ui.copy() |
|
1754 | 1754 | nullui.fout = open(os.devnull, r'wb') |
|
1755 | 1755 | nullui.disablepager() |
|
1756 | 1756 | revs = opts.get(b'rev') |
|
1757 | 1757 | if not revs: |
|
1758 | 1758 | revs = [b'all()'] |
|
1759 | 1759 | revs = list(scmutil.revrange(repo, revs)) |
|
1760 | 1760 | |
|
1761 | 1761 | defaulttemplate = ( |
|
1762 | 1762 | b'{date|shortdate} [{rev}:{node|short}]' |
|
1763 | 1763 | b' {author|person}: {desc|firstline}\n' |
|
1764 | 1764 | ) |
|
1765 | 1765 | if testedtemplate is None: |
|
1766 | 1766 | testedtemplate = defaulttemplate |
|
1767 | 1767 | displayer = makelogtemplater(nullui, repo, testedtemplate) |
|
1768 | 1768 | |
|
1769 | 1769 | def format(): |
|
1770 | 1770 | for r in revs: |
|
1771 | 1771 | ctx = repo[r] |
|
1772 | 1772 | displayer.show(ctx) |
|
1773 | 1773 | displayer.flush(ctx) |
|
1774 | 1774 | |
|
1775 | 1775 | timer, fm = gettimer(ui, opts) |
|
1776 | 1776 | timer(format) |
|
1777 | 1777 | fm.end() |
|
1778 | 1778 | |
|
1779 | 1779 | |
|
1780 | 1780 | def _displaystats(ui, opts, entries, data): |
|
1781 | 1781 | pass |
|
1782 | 1782 | # use a second formatter because the data are quite different, not sure |
|
1783 | 1783 | # how it flies with the templater. |
|
1784 | 1784 | fm = ui.formatter(b'perf-stats', opts) |
|
1785 | 1785 | for key, title in entries: |
|
1786 | 1786 | values = data[key] |
|
1787 | 1787 | nbvalues = len(data) |
|
1788 | 1788 | values.sort() |
|
1789 | 1789 | stats = { |
|
1790 | 1790 | 'key': key, |
|
1791 | 1791 | 'title': title, |
|
1792 | 1792 | 'nbitems': len(values), |
|
1793 | 1793 | 'min': values[0][0], |
|
1794 | 1794 | '10%': values[(nbvalues * 10) // 100][0], |
|
1795 | 1795 | '25%': values[(nbvalues * 25) // 100][0], |
|
1796 | 1796 | '50%': values[(nbvalues * 50) // 100][0], |
|
1797 | 1797 | '75%': values[(nbvalues * 75) // 100][0], |
|
1798 | 1798 | '80%': values[(nbvalues * 80) // 100][0], |
|
1799 | 1799 | '85%': values[(nbvalues * 85) // 100][0], |
|
1800 | 1800 | '90%': values[(nbvalues * 90) // 100][0], |
|
1801 | 1801 | '95%': values[(nbvalues * 95) // 100][0], |
|
1802 | 1802 | '99%': values[(nbvalues * 99) // 100][0], |
|
1803 | 1803 | 'max': values[-1][0], |
|
1804 | 1804 | } |
|
1805 | 1805 | fm.startitem() |
|
1806 | 1806 | fm.data(**stats) |
|
1807 | 1807 | # make node pretty for the human output |
|
1808 | 1808 | fm.plain('### %s (%d items)\n' % (title, len(values))) |
|
1809 | 1809 | lines = [ |
|
1810 | 1810 | 'min', |
|
1811 | 1811 | '10%', |
|
1812 | 1812 | '25%', |
|
1813 | 1813 | '50%', |
|
1814 | 1814 | '75%', |
|
1815 | 1815 | '80%', |
|
1816 | 1816 | '85%', |
|
1817 | 1817 | '90%', |
|
1818 | 1818 | '95%', |
|
1819 | 1819 | '99%', |
|
1820 | 1820 | 'max', |
|
1821 | 1821 | ] |
|
1822 | 1822 | for l in lines: |
|
1823 | 1823 | fm.plain('%s: %s\n' % (l, stats[l])) |
|
1824 | 1824 | fm.end() |
|
1825 | 1825 | |
|
1826 | 1826 | |
|
1827 | 1827 | @command( |
|
1828 | 1828 | b'perfhelper-mergecopies', |
|
1829 | 1829 | formatteropts |
|
1830 | 1830 | + [ |
|
1831 | 1831 | (b'r', b'revs', [], b'restrict search to these revisions'), |
|
1832 | 1832 | (b'', b'timing', False, b'provides extra data (costly)'), |
|
1833 | 1833 | (b'', b'stats', False, b'provides statistic about the measured data'), |
|
1834 | 1834 | ], |
|
1835 | 1835 | ) |
|
1836 | 1836 | def perfhelpermergecopies(ui, repo, revs=[], **opts): |
|
1837 | 1837 | """find statistics about potential parameters for `perfmergecopies` |
|
1838 | 1838 | |
|
1839 | 1839 | This command find (base, p1, p2) triplet relevant for copytracing |
|
1840 | 1840 | benchmarking in the context of a merge. It reports values for some of the |
|
1841 | 1841 | parameters that impact merge copy tracing time during merge. |
|
1842 | 1842 | |
|
1843 | 1843 | If `--timing` is set, rename detection is run and the associated timing |
|
1844 | 1844 | will be reported. The extra details come at the cost of slower command |
|
1845 | 1845 | execution. |
|
1846 | 1846 | |
|
1847 | 1847 | Since rename detection is only run once, other factors might easily |
|
1848 | 1848 | affect the precision of the timing. However it should give a good |
|
1849 | 1849 | approximation of which revision triplets are very costly. |
|
1850 | 1850 | """ |
|
1851 | 1851 | opts = _byteskwargs(opts) |
|
1852 | 1852 | fm = ui.formatter(b'perf', opts) |
|
1853 | 1853 | dotiming = opts[b'timing'] |
|
1854 | 1854 | dostats = opts[b'stats'] |
|
1855 | 1855 | |
|
1856 | 1856 | output_template = [ |
|
1857 | 1857 | ("base", "%(base)12s"), |
|
1858 | 1858 | ("p1", "%(p1.node)12s"), |
|
1859 | 1859 | ("p2", "%(p2.node)12s"), |
|
1860 | 1860 | ("p1.nb-revs", "%(p1.nbrevs)12d"), |
|
1861 | 1861 | ("p1.nb-files", "%(p1.nbmissingfiles)12d"), |
|
1862 | 1862 | ("p1.renames", "%(p1.renamedfiles)12d"), |
|
1863 | 1863 | ("p1.time", "%(p1.time)12.3f"), |
|
1864 | 1864 | ("p2.nb-revs", "%(p2.nbrevs)12d"), |
|
1865 | 1865 | ("p2.nb-files", "%(p2.nbmissingfiles)12d"), |
|
1866 | 1866 | ("p2.renames", "%(p2.renamedfiles)12d"), |
|
1867 | 1867 | ("p2.time", "%(p2.time)12.3f"), |
|
1868 | 1868 | ("renames", "%(nbrenamedfiles)12d"), |
|
1869 | 1869 | ("total.time", "%(time)12.3f"), |
|
1870 | 1870 | ] |
|
1871 | 1871 | if not dotiming: |
|
1872 | 1872 | output_template = [ |
|
1873 | 1873 | i |
|
1874 | 1874 | for i in output_template |
|
1875 | 1875 | if not ('time' in i[0] or 'renames' in i[0]) |
|
1876 | 1876 | ] |
|
1877 | 1877 | header_names = [h for (h, v) in output_template] |
|
1878 | 1878 | output = ' '.join([v for (h, v) in output_template]) + '\n' |
|
1879 | 1879 | header = ' '.join(['%12s'] * len(header_names)) + '\n' |
|
1880 | 1880 | fm.plain(header % tuple(header_names)) |
|
1881 | 1881 | |
|
1882 | 1882 | if not revs: |
|
1883 | 1883 | revs = ['all()'] |
|
1884 | 1884 | revs = scmutil.revrange(repo, revs) |
|
1885 | 1885 | |
|
1886 | 1886 | if dostats: |
|
1887 | 1887 | alldata = { |
|
1888 | 1888 | 'nbrevs': [], |
|
1889 | 1889 | 'nbmissingfiles': [], |
|
1890 | 1890 | } |
|
1891 | 1891 | if dotiming: |
|
1892 | 1892 | alldata['parentnbrenames'] = [] |
|
1893 | 1893 | alldata['totalnbrenames'] = [] |
|
1894 | 1894 | alldata['parenttime'] = [] |
|
1895 | 1895 | alldata['totaltime'] = [] |
|
1896 | 1896 | |
|
1897 | 1897 | roi = repo.revs('merge() and %ld', revs) |
|
1898 | 1898 | for r in roi: |
|
1899 | 1899 | ctx = repo[r] |
|
1900 | 1900 | p1 = ctx.p1() |
|
1901 | 1901 | p2 = ctx.p2() |
|
1902 | 1902 | bases = repo.changelog._commonancestorsheads(p1.rev(), p2.rev()) |
|
1903 | 1903 | for b in bases: |
|
1904 | 1904 | b = repo[b] |
|
1905 | 1905 | p1missing = copies._computeforwardmissing(b, p1) |
|
1906 | 1906 | p2missing = copies._computeforwardmissing(b, p2) |
|
1907 | 1907 | data = { |
|
1908 | 1908 | b'base': b.hex(), |
|
1909 | 1909 | b'p1.node': p1.hex(), |
|
1910 | 1910 | b'p1.nbrevs': len(repo.revs('%d::%d', b.rev(), p1.rev())), |
|
1911 | 1911 | b'p1.nbmissingfiles': len(p1missing), |
|
1912 | 1912 | b'p2.node': p2.hex(), |
|
1913 | 1913 | b'p2.nbrevs': len(repo.revs('%d::%d', b.rev(), p2.rev())), |
|
1914 | 1914 | b'p2.nbmissingfiles': len(p2missing), |
|
1915 | 1915 | } |
|
1916 | 1916 | if dostats: |
|
1917 | 1917 | if p1missing: |
|
1918 | 1918 | alldata['nbrevs'].append( |
|
1919 | 1919 | (data['p1.nbrevs'], b.hex(), p1.hex()) |
|
1920 | 1920 | ) |
|
1921 | 1921 | alldata['nbmissingfiles'].append( |
|
1922 | 1922 | (data['p1.nbmissingfiles'], b.hex(), p1.hex()) |
|
1923 | 1923 | ) |
|
1924 | 1924 | if p2missing: |
|
1925 | 1925 | alldata['nbrevs'].append( |
|
1926 | 1926 | (data['p2.nbrevs'], b.hex(), p2.hex()) |
|
1927 | 1927 | ) |
|
1928 | 1928 | alldata['nbmissingfiles'].append( |
|
1929 | 1929 | (data['p2.nbmissingfiles'], b.hex(), p2.hex()) |
|
1930 | 1930 | ) |
|
1931 | 1931 | if dotiming: |
|
1932 | 1932 | begin = util.timer() |
|
1933 | 1933 | mergedata = copies.mergecopies(repo, p1, p2, b) |
|
1934 | 1934 | end = util.timer() |
|
1935 | 1935 | # not very stable timing since we did only one run |
|
1936 | 1936 | data['time'] = end - begin |
|
1937 | 1937 | # mergedata contains five dicts: "copy", "movewithdir", |
|
1938 | 1938 | # "diverge", "renamedelete" and "dirmove". |
|
1939 | 1939 | # The first 4 are about renamed file so lets count that. |
|
1940 | 1940 | renames = len(mergedata[0]) |
|
1941 | 1941 | renames += len(mergedata[1]) |
|
1942 | 1942 | renames += len(mergedata[2]) |
|
1943 | 1943 | renames += len(mergedata[3]) |
|
1944 | 1944 | data['nbrenamedfiles'] = renames |
|
1945 | 1945 | begin = util.timer() |
|
1946 | 1946 | p1renames = copies.pathcopies(b, p1) |
|
1947 | 1947 | end = util.timer() |
|
1948 | 1948 | data['p1.time'] = end - begin |
|
1949 | 1949 | begin = util.timer() |
|
1950 | 1950 | p2renames = copies.pathcopies(b, p2) |
|
1951 | 1951 | data['p2.time'] = end - begin |
|
1952 | 1952 | end = util.timer() |
|
1953 | 1953 | data['p1.renamedfiles'] = len(p1renames) |
|
1954 | 1954 | data['p2.renamedfiles'] = len(p2renames) |
|
1955 | 1955 | |
|
1956 | 1956 | if dostats: |
|
1957 | 1957 | if p1missing: |
|
1958 | 1958 | alldata['parentnbrenames'].append( |
|
1959 | 1959 | (data['p1.renamedfiles'], b.hex(), p1.hex()) |
|
1960 | 1960 | ) |
|
1961 | 1961 | alldata['parenttime'].append( |
|
1962 | 1962 | (data['p1.time'], b.hex(), p1.hex()) |
|
1963 | 1963 | ) |
|
1964 | 1964 | if p2missing: |
|
1965 | 1965 | alldata['parentnbrenames'].append( |
|
1966 | 1966 | (data['p2.renamedfiles'], b.hex(), p2.hex()) |
|
1967 | 1967 | ) |
|
1968 | 1968 | alldata['parenttime'].append( |
|
1969 | 1969 | (data['p2.time'], b.hex(), p2.hex()) |
|
1970 | 1970 | ) |
|
1971 | 1971 | if p1missing or p2missing: |
|
1972 | 1972 | alldata['totalnbrenames'].append( |
|
1973 | 1973 | ( |
|
1974 | 1974 | data['nbrenamedfiles'], |
|
1975 | 1975 | b.hex(), |
|
1976 | 1976 | p1.hex(), |
|
1977 | 1977 | p2.hex(), |
|
1978 | 1978 | ) |
|
1979 | 1979 | ) |
|
1980 | 1980 | alldata['totaltime'].append( |
|
1981 | 1981 | (data['time'], b.hex(), p1.hex(), p2.hex()) |
|
1982 | 1982 | ) |
|
1983 | 1983 | fm.startitem() |
|
1984 | 1984 | fm.data(**data) |
|
1985 | 1985 | # make node pretty for the human output |
|
1986 | 1986 | out = data.copy() |
|
1987 | 1987 | out['base'] = fm.hexfunc(b.node()) |
|
1988 | 1988 | out['p1.node'] = fm.hexfunc(p1.node()) |
|
1989 | 1989 | out['p2.node'] = fm.hexfunc(p2.node()) |
|
1990 | 1990 | fm.plain(output % out) |
|
1991 | 1991 | |
|
1992 | 1992 | fm.end() |
|
1993 | 1993 | if dostats: |
|
1994 | 1994 | # use a second formatter because the data are quite different, not sure |
|
1995 | 1995 | # how it flies with the templater. |
|
1996 | 1996 | entries = [ |
|
1997 | 1997 | ('nbrevs', 'number of revision covered'), |
|
1998 | 1998 | ('nbmissingfiles', 'number of missing files at head'), |
|
1999 | 1999 | ] |
|
2000 | 2000 | if dotiming: |
|
2001 | 2001 | entries.append( |
|
2002 | 2002 | ('parentnbrenames', 'rename from one parent to base') |
|
2003 | 2003 | ) |
|
2004 | 2004 | entries.append(('totalnbrenames', 'total number of renames')) |
|
2005 | 2005 | entries.append(('parenttime', 'time for one parent')) |
|
2006 | 2006 | entries.append(('totaltime', 'time for both parents')) |
|
2007 | 2007 | _displaystats(ui, opts, entries, alldata) |
|
2008 | 2008 | |
|
2009 | 2009 | |
|
2010 | 2010 | @command( |
|
2011 | 2011 | b'perfhelper-pathcopies', |
|
2012 | 2012 | formatteropts |
|
2013 | 2013 | + [ |
|
2014 | 2014 | (b'r', b'revs', [], b'restrict search to these revisions'), |
|
2015 | 2015 | (b'', b'timing', False, b'provides extra data (costly)'), |
|
2016 | 2016 | (b'', b'stats', False, b'provides statistic about the measured data'), |
|
2017 | 2017 | ], |
|
2018 | 2018 | ) |
|
2019 | 2019 | def perfhelperpathcopies(ui, repo, revs=[], **opts): |
|
2020 | 2020 | """find statistic about potential parameters for the `perftracecopies` |
|
2021 | 2021 | |
|
2022 | 2022 | This command find source-destination pair relevant for copytracing testing. |
|
2023 | 2023 | It report value for some of the parameters that impact copy tracing time. |
|
2024 | 2024 | |
|
2025 | 2025 | If `--timing` is set, rename detection is run and the associated timing |
|
2026 | 2026 | will be reported. The extra details comes at the cost of a slower command |
|
2027 | 2027 | execution. |
|
2028 | 2028 | |
|
2029 | 2029 | Since the rename detection is only run once, other factors might easily |
|
2030 | 2030 | affect the precision of the timing. However it should give a good |
|
2031 | 2031 | approximation of which revision pairs are very costly. |
|
2032 | 2032 | """ |
|
2033 | 2033 | opts = _byteskwargs(opts) |
|
2034 | 2034 | fm = ui.formatter(b'perf', opts) |
|
2035 | 2035 | dotiming = opts[b'timing'] |
|
2036 | 2036 | dostats = opts[b'stats'] |
|
2037 | 2037 | |
|
2038 | 2038 | if dotiming: |
|
2039 | 2039 | header = '%12s %12s %12s %12s %12s %12s\n' |
|
2040 | 2040 | output = ( |
|
2041 | 2041 | "%(source)12s %(destination)12s " |
|
2042 | 2042 | "%(nbrevs)12d %(nbmissingfiles)12d " |
|
2043 | 2043 | "%(nbrenamedfiles)12d %(time)18.5f\n" |
|
2044 | 2044 | ) |
|
2045 | 2045 | header_names = ( |
|
2046 | 2046 | "source", |
|
2047 | 2047 | "destination", |
|
2048 | 2048 | "nb-revs", |
|
2049 | 2049 | "nb-files", |
|
2050 | 2050 | "nb-renames", |
|
2051 | 2051 | "time", |
|
2052 | 2052 | ) |
|
2053 | 2053 | fm.plain(header % header_names) |
|
2054 | 2054 | else: |
|
2055 | 2055 | header = '%12s %12s %12s %12s\n' |
|
2056 | 2056 | output = ( |
|
2057 | 2057 | "%(source)12s %(destination)12s " |
|
2058 | 2058 | "%(nbrevs)12d %(nbmissingfiles)12d\n" |
|
2059 | 2059 | ) |
|
2060 | 2060 | fm.plain(header % ("source", "destination", "nb-revs", "nb-files")) |
|
2061 | 2061 | |
|
2062 | 2062 | if not revs: |
|
2063 | 2063 | revs = ['all()'] |
|
2064 | 2064 | revs = scmutil.revrange(repo, revs) |
|
2065 | 2065 | |
|
2066 | 2066 | if dostats: |
|
2067 | 2067 | alldata = { |
|
2068 | 2068 | 'nbrevs': [], |
|
2069 | 2069 | 'nbmissingfiles': [], |
|
2070 | 2070 | } |
|
2071 | 2071 | if dotiming: |
|
2072 | 2072 | alldata['nbrenames'] = [] |
|
2073 | 2073 | alldata['time'] = [] |
|
2074 | 2074 | |
|
2075 | 2075 | roi = repo.revs('merge() and %ld', revs) |
|
2076 | 2076 | for r in roi: |
|
2077 | 2077 | ctx = repo[r] |
|
2078 | 2078 | p1 = ctx.p1().rev() |
|
2079 | 2079 | p2 = ctx.p2().rev() |
|
2080 | 2080 | bases = repo.changelog._commonancestorsheads(p1, p2) |
|
2081 | 2081 | for p in (p1, p2): |
|
2082 | 2082 | for b in bases: |
|
2083 | 2083 | base = repo[b] |
|
2084 | 2084 | parent = repo[p] |
|
2085 | 2085 | missing = copies._computeforwardmissing(base, parent) |
|
2086 | 2086 | if not missing: |
|
2087 | 2087 | continue |
|
2088 | 2088 | data = { |
|
2089 | 2089 | b'source': base.hex(), |
|
2090 | 2090 | b'destination': parent.hex(), |
|
2091 | 2091 | b'nbrevs': len(repo.revs('%d::%d', b, p)), |
|
2092 | 2092 | b'nbmissingfiles': len(missing), |
|
2093 | 2093 | } |
|
2094 | 2094 | if dostats: |
|
2095 | 2095 | alldata['nbrevs'].append( |
|
2096 | 2096 | (data['nbrevs'], base.hex(), parent.hex(),) |
|
2097 | 2097 | ) |
|
2098 | 2098 | alldata['nbmissingfiles'].append( |
|
2099 | 2099 | (data['nbmissingfiles'], base.hex(), parent.hex(),) |
|
2100 | 2100 | ) |
|
2101 | 2101 | if dotiming: |
|
2102 | 2102 | begin = util.timer() |
|
2103 | 2103 | renames = copies.pathcopies(base, parent) |
|
2104 | 2104 | end = util.timer() |
|
2105 | 2105 | # not very stable timing since we did only one run |
|
2106 | 2106 | data['time'] = end - begin |
|
2107 | 2107 | data['nbrenamedfiles'] = len(renames) |
|
2108 | 2108 | if dostats: |
|
2109 | 2109 | alldata['time'].append( |
|
2110 | 2110 | (data['time'], base.hex(), parent.hex(),) |
|
2111 | 2111 | ) |
|
2112 | 2112 | alldata['nbrenames'].append( |
|
2113 | 2113 | (data['nbrenamedfiles'], base.hex(), parent.hex(),) |
|
2114 | 2114 | ) |
|
2115 | 2115 | fm.startitem() |
|
2116 | 2116 | fm.data(**data) |
|
2117 | 2117 | out = data.copy() |
|
2118 | 2118 | out['source'] = fm.hexfunc(base.node()) |
|
2119 | 2119 | out['destination'] = fm.hexfunc(parent.node()) |
|
2120 | 2120 | fm.plain(output % out) |
|
2121 | 2121 | |
|
2122 | 2122 | fm.end() |
|
2123 | 2123 | if dostats: |
|
2124 | 2124 | # use a second formatter because the data are quite different, not sure |
|
2125 | 2125 | # how it flies with the templater. |
|
2126 | 2126 | fm = ui.formatter(b'perf', opts) |
|
2127 | 2127 | entries = [ |
|
2128 | 2128 | ('nbrevs', 'number of revision covered'), |
|
2129 | 2129 | ('nbmissingfiles', 'number of missing files at head'), |
|
2130 | 2130 | ] |
|
2131 | 2131 | if dotiming: |
|
2132 | 2132 | entries.append(('nbrenames', 'renamed files')) |
|
2133 | 2133 | entries.append(('time', 'time')) |
|
2134 | 2134 | _displaystats(ui, opts, entries, alldata) |
|
2135 | 2135 | |
|
2136 | 2136 | |
|
2137 | 2137 | @command(b'perfcca', formatteropts) |
|
2138 | 2138 | def perfcca(ui, repo, **opts): |
|
2139 | 2139 | opts = _byteskwargs(opts) |
|
2140 | 2140 | timer, fm = gettimer(ui, opts) |
|
2141 | 2141 | timer(lambda: scmutil.casecollisionauditor(ui, False, repo.dirstate)) |
|
2142 | 2142 | fm.end() |
|
2143 | 2143 | |
|
2144 | 2144 | |
|
2145 | 2145 | @command(b'perffncacheload', formatteropts) |
|
2146 | 2146 | def perffncacheload(ui, repo, **opts): |
|
2147 | 2147 | opts = _byteskwargs(opts) |
|
2148 | 2148 | timer, fm = gettimer(ui, opts) |
|
2149 | 2149 | s = repo.store |
|
2150 | 2150 | |
|
2151 | 2151 | def d(): |
|
2152 | 2152 | s.fncache._load() |
|
2153 | 2153 | |
|
2154 | 2154 | timer(d) |
|
2155 | 2155 | fm.end() |
|
2156 | 2156 | |
|
2157 | 2157 | |
|
2158 | 2158 | @command(b'perffncachewrite', formatteropts) |
|
2159 | 2159 | def perffncachewrite(ui, repo, **opts): |
|
2160 | 2160 | opts = _byteskwargs(opts) |
|
2161 | 2161 | timer, fm = gettimer(ui, opts) |
|
2162 | 2162 | s = repo.store |
|
2163 | 2163 | lock = repo.lock() |
|
2164 | 2164 | s.fncache._load() |
|
2165 | 2165 | tr = repo.transaction(b'perffncachewrite') |
|
2166 | 2166 | tr.addbackup(b'fncache') |
|
2167 | 2167 | |
|
2168 | 2168 | def d(): |
|
2169 | 2169 | s.fncache._dirty = True |
|
2170 | 2170 | s.fncache.write(tr) |
|
2171 | 2171 | |
|
2172 | 2172 | timer(d) |
|
2173 | 2173 | tr.close() |
|
2174 | 2174 | lock.release() |
|
2175 | 2175 | fm.end() |
|
2176 | 2176 | |
|
2177 | 2177 | |
|
2178 | 2178 | @command(b'perffncacheencode', formatteropts) |
|
2179 | 2179 | def perffncacheencode(ui, repo, **opts): |
|
2180 | 2180 | opts = _byteskwargs(opts) |
|
2181 | 2181 | timer, fm = gettimer(ui, opts) |
|
2182 | 2182 | s = repo.store |
|
2183 | 2183 | s.fncache._load() |
|
2184 | 2184 | |
|
2185 | 2185 | def d(): |
|
2186 | 2186 | for p in s.fncache.entries: |
|
2187 | 2187 | s.encode(p) |
|
2188 | 2188 | |
|
2189 | 2189 | timer(d) |
|
2190 | 2190 | fm.end() |
|
2191 | 2191 | |
|
2192 | 2192 | |
|
2193 | 2193 | def _bdiffworker(q, blocks, xdiff, ready, done): |
|
2194 | 2194 | while not done.is_set(): |
|
2195 | 2195 | pair = q.get() |
|
2196 | 2196 | while pair is not None: |
|
2197 | 2197 | if xdiff: |
|
2198 | 2198 | mdiff.bdiff.xdiffblocks(*pair) |
|
2199 | 2199 | elif blocks: |
|
2200 | 2200 | mdiff.bdiff.blocks(*pair) |
|
2201 | 2201 | else: |
|
2202 | 2202 | mdiff.textdiff(*pair) |
|
2203 | 2203 | q.task_done() |
|
2204 | 2204 | pair = q.get() |
|
2205 | 2205 | q.task_done() # for the None one |
|
2206 | 2206 | with ready: |
|
2207 | 2207 | ready.wait() |
|
2208 | 2208 | |
|
2209 | 2209 | |
|
2210 | 2210 | def _manifestrevision(repo, mnode): |
|
2211 | 2211 | ml = repo.manifestlog |
|
2212 | 2212 | |
|
2213 | 2213 | if util.safehasattr(ml, b'getstorage'): |
|
2214 | 2214 | store = ml.getstorage(b'') |
|
2215 | 2215 | else: |
|
2216 | 2216 | store = ml._revlog |
|
2217 | 2217 | |
|
2218 | 2218 | return store.revision(mnode) |
|
2219 | 2219 | |
|
2220 | 2220 | |
|
2221 | 2221 | @command( |
|
2222 | 2222 | b'perfbdiff', |
|
2223 | 2223 | revlogopts |
|
2224 | 2224 | + formatteropts |
|
2225 | 2225 | + [ |
|
2226 | 2226 | ( |
|
2227 | 2227 | b'', |
|
2228 | 2228 | b'count', |
|
2229 | 2229 | 1, |
|
2230 | 2230 | b'number of revisions to test (when using --startrev)', |
|
2231 | 2231 | ), |
|
2232 | 2232 | (b'', b'alldata', False, b'test bdiffs for all associated revisions'), |
|
2233 | 2233 | (b'', b'threads', 0, b'number of thread to use (disable with 0)'), |
|
2234 | 2234 | (b'', b'blocks', False, b'test computing diffs into blocks'), |
|
2235 | 2235 | (b'', b'xdiff', False, b'use xdiff algorithm'), |
|
2236 | 2236 | ], |
|
2237 | 2237 | b'-c|-m|FILE REV', |
|
2238 | 2238 | ) |
|
2239 | 2239 | def perfbdiff(ui, repo, file_, rev=None, count=None, threads=0, **opts): |
|
2240 | 2240 | """benchmark a bdiff between revisions |
|
2241 | 2241 | |
|
2242 | 2242 | By default, benchmark a bdiff between its delta parent and itself. |
|
2243 | 2243 | |
|
2244 | 2244 | With ``--count``, benchmark bdiffs between delta parents and self for N |
|
2245 | 2245 | revisions starting at the specified revision. |
|
2246 | 2246 | |
|
2247 | 2247 | With ``--alldata``, assume the requested revision is a changeset and |
|
2248 | 2248 | measure bdiffs for all changes related to that changeset (manifest |
|
2249 | 2249 | and filelogs). |
|
2250 | 2250 | """ |
|
2251 | 2251 | opts = _byteskwargs(opts) |
|
2252 | 2252 | |
|
2253 | 2253 | if opts[b'xdiff'] and not opts[b'blocks']: |
|
2254 | 2254 | raise error.CommandError(b'perfbdiff', b'--xdiff requires --blocks') |
|
2255 | 2255 | |
|
2256 | 2256 | if opts[b'alldata']: |
|
2257 | 2257 | opts[b'changelog'] = True |
|
2258 | 2258 | |
|
2259 | 2259 | if opts.get(b'changelog') or opts.get(b'manifest'): |
|
2260 | 2260 | file_, rev = None, file_ |
|
2261 | 2261 | elif rev is None: |
|
2262 | 2262 | raise error.CommandError(b'perfbdiff', b'invalid arguments') |
|
2263 | 2263 | |
|
2264 | 2264 | blocks = opts[b'blocks'] |
|
2265 | 2265 | xdiff = opts[b'xdiff'] |
|
2266 | 2266 | textpairs = [] |
|
2267 | 2267 | |
|
2268 | 2268 | r = cmdutil.openrevlog(repo, b'perfbdiff', file_, opts) |
|
2269 | 2269 | |
|
2270 | 2270 | startrev = r.rev(r.lookup(rev)) |
|
2271 | 2271 | for rev in range(startrev, min(startrev + count, len(r) - 1)): |
|
2272 | 2272 | if opts[b'alldata']: |
|
2273 | 2273 | # Load revisions associated with changeset. |
|
2274 | 2274 | ctx = repo[rev] |
|
2275 | 2275 | mtext = _manifestrevision(repo, ctx.manifestnode()) |
|
2276 | 2276 | for pctx in ctx.parents(): |
|
2277 | 2277 | pman = _manifestrevision(repo, pctx.manifestnode()) |
|
2278 | 2278 | textpairs.append((pman, mtext)) |
|
2279 | 2279 | |
|
2280 | 2280 | # Load filelog revisions by iterating manifest delta. |
|
2281 | 2281 | man = ctx.manifest() |
|
2282 | 2282 | pman = ctx.p1().manifest() |
|
2283 | 2283 | for filename, change in pman.diff(man).items(): |
|
2284 | 2284 | fctx = repo.file(filename) |
|
2285 | 2285 | f1 = fctx.revision(change[0][0] or -1) |
|
2286 | 2286 | f2 = fctx.revision(change[1][0] or -1) |
|
2287 | 2287 | textpairs.append((f1, f2)) |
|
2288 | 2288 | else: |
|
2289 | 2289 | dp = r.deltaparent(rev) |
|
2290 | 2290 | textpairs.append((r.revision(dp), r.revision(rev))) |
|
2291 | 2291 | |
|
2292 | 2292 | withthreads = threads > 0 |
|
2293 | 2293 | if not withthreads: |
|
2294 | 2294 | |
|
2295 | 2295 | def d(): |
|
2296 | 2296 | for pair in textpairs: |
|
2297 | 2297 | if xdiff: |
|
2298 | 2298 | mdiff.bdiff.xdiffblocks(*pair) |
|
2299 | 2299 | elif blocks: |
|
2300 | 2300 | mdiff.bdiff.blocks(*pair) |
|
2301 | 2301 | else: |
|
2302 | 2302 | mdiff.textdiff(*pair) |
|
2303 | 2303 | |
|
2304 | 2304 | else: |
|
2305 | 2305 | q = queue() |
|
2306 | 2306 | for i in _xrange(threads): |
|
2307 | 2307 | q.put(None) |
|
2308 | 2308 | ready = threading.Condition() |
|
2309 | 2309 | done = threading.Event() |
|
2310 | 2310 | for i in _xrange(threads): |
|
2311 | 2311 | threading.Thread( |
|
2312 | 2312 | target=_bdiffworker, args=(q, blocks, xdiff, ready, done) |
|
2313 | 2313 | ).start() |
|
2314 | 2314 | q.join() |
|
2315 | 2315 | |
|
2316 | 2316 | def d(): |
|
2317 | 2317 | for pair in textpairs: |
|
2318 | 2318 | q.put(pair) |
|
2319 | 2319 | for i in _xrange(threads): |
|
2320 | 2320 | q.put(None) |
|
2321 | 2321 | with ready: |
|
2322 | 2322 | ready.notify_all() |
|
2323 | 2323 | q.join() |
|
2324 | 2324 | |
|
2325 | 2325 | timer, fm = gettimer(ui, opts) |
|
2326 | 2326 | timer(d) |
|
2327 | 2327 | fm.end() |
|
2328 | 2328 | |
|
2329 | 2329 | if withthreads: |
|
2330 | 2330 | done.set() |
|
2331 | 2331 | for i in _xrange(threads): |
|
2332 | 2332 | q.put(None) |
|
2333 | 2333 | with ready: |
|
2334 | 2334 | ready.notify_all() |
|
2335 | 2335 | |
|
2336 | 2336 | |
|
2337 | 2337 | @command( |
|
2338 | 2338 | b'perfunidiff', |
|
2339 | 2339 | revlogopts |
|
2340 | 2340 | + formatteropts |
|
2341 | 2341 | + [ |
|
2342 | 2342 | ( |
|
2343 | 2343 | b'', |
|
2344 | 2344 | b'count', |
|
2345 | 2345 | 1, |
|
2346 | 2346 | b'number of revisions to test (when using --startrev)', |
|
2347 | 2347 | ), |
|
2348 | 2348 | (b'', b'alldata', False, b'test unidiffs for all associated revisions'), |
|
2349 | 2349 | ], |
|
2350 | 2350 | b'-c|-m|FILE REV', |
|
2351 | 2351 | ) |
|
2352 | 2352 | def perfunidiff(ui, repo, file_, rev=None, count=None, **opts): |
|
2353 | 2353 | """benchmark a unified diff between revisions |
|
2354 | 2354 | |
|
2355 | 2355 | This doesn't include any copy tracing - it's just a unified diff |
|
2356 | 2356 | of the texts. |
|
2357 | 2357 | |
|
2358 | 2358 | By default, benchmark a diff between its delta parent and itself. |
|
2359 | 2359 | |
|
2360 | 2360 | With ``--count``, benchmark diffs between delta parents and self for N |
|
2361 | 2361 | revisions starting at the specified revision. |
|
2362 | 2362 | |
|
2363 | 2363 | With ``--alldata``, assume the requested revision is a changeset and |
|
2364 | 2364 | measure diffs for all changes related to that changeset (manifest |
|
2365 | 2365 | and filelogs). |
|
2366 | 2366 | """ |
|
2367 | 2367 | opts = _byteskwargs(opts) |
|
2368 | 2368 | if opts[b'alldata']: |
|
2369 | 2369 | opts[b'changelog'] = True |
|
2370 | 2370 | |
|
2371 | 2371 | if opts.get(b'changelog') or opts.get(b'manifest'): |
|
2372 | 2372 | file_, rev = None, file_ |
|
2373 | 2373 | elif rev is None: |
|
2374 | 2374 | raise error.CommandError(b'perfunidiff', b'invalid arguments') |
|
2375 | 2375 | |
|
2376 | 2376 | textpairs = [] |
|
2377 | 2377 | |
|
2378 | 2378 | r = cmdutil.openrevlog(repo, b'perfunidiff', file_, opts) |
|
2379 | 2379 | |
|
2380 | 2380 | startrev = r.rev(r.lookup(rev)) |
|
2381 | 2381 | for rev in range(startrev, min(startrev + count, len(r) - 1)): |
|
2382 | 2382 | if opts[b'alldata']: |
|
2383 | 2383 | # Load revisions associated with changeset. |
|
2384 | 2384 | ctx = repo[rev] |
|
2385 | 2385 | mtext = _manifestrevision(repo, ctx.manifestnode()) |
|
2386 | 2386 | for pctx in ctx.parents(): |
|
2387 | 2387 | pman = _manifestrevision(repo, pctx.manifestnode()) |
|
2388 | 2388 | textpairs.append((pman, mtext)) |
|
2389 | 2389 | |
|
2390 | 2390 | # Load filelog revisions by iterating manifest delta. |
|
2391 | 2391 | man = ctx.manifest() |
|
2392 | 2392 | pman = ctx.p1().manifest() |
|
2393 | 2393 | for filename, change in pman.diff(man).items(): |
|
2394 | 2394 | fctx = repo.file(filename) |
|
2395 | 2395 | f1 = fctx.revision(change[0][0] or -1) |
|
2396 | 2396 | f2 = fctx.revision(change[1][0] or -1) |
|
2397 | 2397 | textpairs.append((f1, f2)) |
|
2398 | 2398 | else: |
|
2399 | 2399 | dp = r.deltaparent(rev) |
|
2400 | 2400 | textpairs.append((r.revision(dp), r.revision(rev))) |
|
2401 | 2401 | |
|
2402 | 2402 | def d(): |
|
2403 | 2403 | for left, right in textpairs: |
|
2404 | 2404 | # The date strings don't matter, so we pass empty strings. |
|
2405 | 2405 | headerlines, hunks = mdiff.unidiff( |
|
2406 | 2406 | left, b'', right, b'', b'left', b'right', binary=False |
|
2407 | 2407 | ) |
|
2408 | 2408 | # consume iterators in roughly the way patch.py does |
|
2409 | 2409 | b'\n'.join(headerlines) |
|
2410 | 2410 | b''.join(sum((list(hlines) for hrange, hlines in hunks), [])) |
|
2411 | 2411 | |
|
2412 | 2412 | timer, fm = gettimer(ui, opts) |
|
2413 | 2413 | timer(d) |
|
2414 | 2414 | fm.end() |
|
2415 | 2415 | |
|
2416 | 2416 | |
|
2417 | 2417 | @command(b'perfdiffwd', formatteropts) |
|
2418 | 2418 | def perfdiffwd(ui, repo, **opts): |
|
2419 | 2419 | """Profile diff of working directory changes""" |
|
2420 | 2420 | opts = _byteskwargs(opts) |
|
2421 | 2421 | timer, fm = gettimer(ui, opts) |
|
2422 | 2422 | options = { |
|
2423 | 2423 | 'w': 'ignore_all_space', |
|
2424 | 2424 | 'b': 'ignore_space_change', |
|
2425 | 2425 | 'B': 'ignore_blank_lines', |
|
2426 | 2426 | } |
|
2427 | 2427 | |
|
2428 | 2428 | for diffopt in ('', 'w', 'b', 'B', 'wB'): |
|
2429 | 2429 | opts = dict((options[c], b'1') for c in diffopt) |
|
2430 | 2430 | |
|
2431 | 2431 | def d(): |
|
2432 | 2432 | ui.pushbuffer() |
|
2433 | 2433 | commands.diff(ui, repo, **opts) |
|
2434 | 2434 | ui.popbuffer() |
|
2435 | 2435 | |
|
2436 | 2436 | diffopt = diffopt.encode('ascii') |
|
2437 | 2437 | title = b'diffopts: %s' % (diffopt and (b'-' + diffopt) or b'none') |
|
2438 | 2438 | timer(d, title=title) |
|
2439 | 2439 | fm.end() |
|
2440 | 2440 | |
|
2441 | 2441 | |
|
2442 | 2442 | @command(b'perfrevlogindex', revlogopts + formatteropts, b'-c|-m|FILE') |
|
2443 | 2443 | def perfrevlogindex(ui, repo, file_=None, **opts): |
|
2444 | 2444 | """Benchmark operations against a revlog index. |
|
2445 | 2445 | |
|
2446 | 2446 | This tests constructing a revlog instance, reading index data, |
|
2447 | 2447 | parsing index data, and performing various operations related to |
|
2448 | 2448 | index data. |
|
2449 | 2449 | """ |
|
2450 | 2450 | |
|
2451 | 2451 | opts = _byteskwargs(opts) |
|
2452 | 2452 | |
|
2453 | 2453 | rl = cmdutil.openrevlog(repo, b'perfrevlogindex', file_, opts) |
|
2454 | 2454 | |
|
2455 | 2455 | opener = getattr(rl, 'opener') # trick linter |
|
2456 | 2456 | indexfile = rl.indexfile |
|
2457 | 2457 | data = opener.read(indexfile) |
|
2458 | 2458 | |
|
2459 | 2459 | header = struct.unpack(b'>I', data[0:4])[0] |
|
2460 | 2460 | version = header & 0xFFFF |
|
2461 | 2461 | if version == 1: |
|
2462 | 2462 | revlogio = revlog.revlogio() |
|
2463 | 2463 | inline = header & (1 << 16) |
|
2464 | 2464 | else: |
|
2465 | 2465 | raise error.Abort(b'unsupported revlog version: %d' % version) |
|
2466 | 2466 | |
|
2467 | 2467 | rllen = len(rl) |
|
2468 | 2468 | |
|
2469 | 2469 | node0 = rl.node(0) |
|
2470 | 2470 | node25 = rl.node(rllen // 4) |
|
2471 | 2471 | node50 = rl.node(rllen // 2) |
|
2472 | 2472 | node75 = rl.node(rllen // 4 * 3) |
|
2473 | 2473 | node100 = rl.node(rllen - 1) |
|
2474 | 2474 | |
|
2475 | 2475 | allrevs = range(rllen) |
|
2476 | 2476 | allrevsrev = list(reversed(allrevs)) |
|
2477 | 2477 | allnodes = [rl.node(rev) for rev in range(rllen)] |
|
2478 | 2478 | allnodesrev = list(reversed(allnodes)) |
|
2479 | 2479 | |
|
2480 | 2480 | def constructor(): |
|
2481 | 2481 | revlog.revlog(opener, indexfile) |
|
2482 | 2482 | |
|
2483 | 2483 | def read(): |
|
2484 | 2484 | with opener(indexfile) as fh: |
|
2485 | 2485 | fh.read() |
|
2486 | 2486 | |
|
2487 | 2487 | def parseindex(): |
|
2488 | 2488 | revlogio.parseindex(data, inline) |
|
2489 | 2489 | |
|
2490 | 2490 | def getentry(revornode): |
|
2491 | 2491 | index = revlogio.parseindex(data, inline)[0] |
|
2492 | 2492 | index[revornode] |
|
2493 | 2493 | |
|
2494 | 2494 | def getentries(revs, count=1): |
|
2495 | 2495 | index = revlogio.parseindex(data, inline)[0] |
|
2496 | 2496 | |
|
2497 | 2497 | for i in range(count): |
|
2498 | 2498 | for rev in revs: |
|
2499 | 2499 | index[rev] |
|
2500 | 2500 | |
|
2501 | 2501 | def resolvenode(node): |
|
2502 | 2502 | nodemap = revlogio.parseindex(data, inline)[1] |
|
2503 | 2503 | # This only works for the C code. |
|
2504 | 2504 | if nodemap is None: |
|
2505 | 2505 | return |
|
2506 | 2506 | |
|
2507 | 2507 | try: |
|
2508 | 2508 | nodemap[node] |
|
2509 | 2509 | except error.RevlogError: |
|
2510 | 2510 | pass |
|
2511 | 2511 | |
|
2512 | 2512 | def resolvenodes(nodes, count=1): |
|
2513 | 2513 | nodemap = revlogio.parseindex(data, inline)[1] |
|
2514 | 2514 | if nodemap is None: |
|
2515 | 2515 | return |
|
2516 | 2516 | |
|
2517 | 2517 | for i in range(count): |
|
2518 | 2518 | for node in nodes: |
|
2519 | 2519 | try: |
|
2520 | 2520 | nodemap[node] |
|
2521 | 2521 | except error.RevlogError: |
|
2522 | 2522 | pass |
|
2523 | 2523 | |
|
2524 | 2524 | benches = [ |
|
2525 | 2525 | (constructor, b'revlog constructor'), |
|
2526 | 2526 | (read, b'read'), |
|
2527 | 2527 | (parseindex, b'create index object'), |
|
2528 | 2528 | (lambda: getentry(0), b'retrieve index entry for rev 0'), |
|
2529 | 2529 | (lambda: resolvenode(b'a' * 20), b'look up missing node'), |
|
2530 | 2530 | (lambda: resolvenode(node0), b'look up node at rev 0'), |
|
2531 | 2531 | (lambda: resolvenode(node25), b'look up node at 1/4 len'), |
|
2532 | 2532 | (lambda: resolvenode(node50), b'look up node at 1/2 len'), |
|
2533 | 2533 | (lambda: resolvenode(node75), b'look up node at 3/4 len'), |
|
2534 | 2534 | (lambda: resolvenode(node100), b'look up node at tip'), |
|
2535 | 2535 | # 2x variation is to measure caching impact. |
|
2536 | 2536 | (lambda: resolvenodes(allnodes), b'look up all nodes (forward)'), |
|
2537 | 2537 | (lambda: resolvenodes(allnodes, 2), b'look up all nodes 2x (forward)'), |
|
2538 | 2538 | (lambda: resolvenodes(allnodesrev), b'look up all nodes (reverse)'), |
|
2539 | 2539 | ( |
|
2540 | 2540 | lambda: resolvenodes(allnodesrev, 2), |
|
2541 | 2541 | b'look up all nodes 2x (reverse)', |
|
2542 | 2542 | ), |
|
2543 | 2543 | (lambda: getentries(allrevs), b'retrieve all index entries (forward)'), |
|
2544 | 2544 | ( |
|
2545 | 2545 | lambda: getentries(allrevs, 2), |
|
2546 | 2546 | b'retrieve all index entries 2x (forward)', |
|
2547 | 2547 | ), |
|
2548 | 2548 | ( |
|
2549 | 2549 | lambda: getentries(allrevsrev), |
|
2550 | 2550 | b'retrieve all index entries (reverse)', |
|
2551 | 2551 | ), |
|
2552 | 2552 | ( |
|
2553 | 2553 | lambda: getentries(allrevsrev, 2), |
|
2554 | 2554 | b'retrieve all index entries 2x (reverse)', |
|
2555 | 2555 | ), |
|
2556 | 2556 | ] |
|
2557 | 2557 | |
|
2558 | 2558 | for fn, title in benches: |
|
2559 | 2559 | timer, fm = gettimer(ui, opts) |
|
2560 | 2560 | timer(fn, title=title) |
|
2561 | 2561 | fm.end() |
|
2562 | 2562 | |
|
2563 | 2563 | |
|
2564 | 2564 | @command( |
|
2565 | 2565 | b'perfrevlogrevisions', |
|
2566 | 2566 | revlogopts |
|
2567 | 2567 | + formatteropts |
|
2568 | 2568 | + [ |
|
2569 | 2569 | (b'd', b'dist', 100, b'distance between the revisions'), |
|
2570 | 2570 | (b's', b'startrev', 0, b'revision to start reading at'), |
|
2571 | 2571 | (b'', b'reverse', False, b'read in reverse'), |
|
2572 | 2572 | ], |
|
2573 | 2573 | b'-c|-m|FILE', |
|
2574 | 2574 | ) |
|
2575 | 2575 | def perfrevlogrevisions( |
|
2576 | 2576 | ui, repo, file_=None, startrev=0, reverse=False, **opts |
|
2577 | 2577 | ): |
|
2578 | 2578 | """Benchmark reading a series of revisions from a revlog. |
|
2579 | 2579 | |
|
2580 | 2580 | By default, we read every ``-d/--dist`` revision from 0 to tip of |
|
2581 | 2581 | the specified revlog. |
|
2582 | 2582 | |
|
2583 | 2583 | The start revision can be defined via ``-s/--startrev``. |
|
2584 | 2584 | """ |
|
2585 | 2585 | opts = _byteskwargs(opts) |
|
2586 | 2586 | |
|
2587 | 2587 | rl = cmdutil.openrevlog(repo, b'perfrevlogrevisions', file_, opts) |
|
2588 | 2588 | rllen = getlen(ui)(rl) |
|
2589 | 2589 | |
|
2590 | 2590 | if startrev < 0: |
|
2591 | 2591 | startrev = rllen + startrev |
|
2592 | 2592 | |
|
2593 | 2593 | def d(): |
|
2594 | 2594 | rl.clearcaches() |
|
2595 | 2595 | |
|
2596 | 2596 | beginrev = startrev |
|
2597 | 2597 | endrev = rllen |
|
2598 | 2598 | dist = opts[b'dist'] |
|
2599 | 2599 | |
|
2600 | 2600 | if reverse: |
|
2601 | 2601 | beginrev, endrev = endrev - 1, beginrev - 1 |
|
2602 | 2602 | dist = -1 * dist |
|
2603 | 2603 | |
|
2604 | 2604 | for x in _xrange(beginrev, endrev, dist): |
|
2605 | 2605 | # Old revisions don't support passing int. |
|
2606 | 2606 | n = rl.node(x) |
|
2607 | 2607 | rl.revision(n) |
|
2608 | 2608 | |
|
2609 | 2609 | timer, fm = gettimer(ui, opts) |
|
2610 | 2610 | timer(d) |
|
2611 | 2611 | fm.end() |
|
2612 | 2612 | |
|
2613 | 2613 | |
|
2614 | 2614 | @command( |
|
2615 | 2615 | b'perfrevlogwrite', |
|
2616 | 2616 | revlogopts |
|
2617 | 2617 | + formatteropts |
|
2618 | 2618 | + [ |
|
2619 | 2619 | (b's', b'startrev', 1000, b'revision to start writing at'), |
|
2620 | 2620 | (b'', b'stoprev', -1, b'last revision to write'), |
|
2621 | 2621 | (b'', b'count', 3, b'number of passes to perform'), |
|
2622 | 2622 | (b'', b'details', False, b'print timing for every revisions tested'), |
|
2623 | 2623 | (b'', b'source', b'full', b'the kind of data feed in the revlog'), |
|
2624 | 2624 | (b'', b'lazydeltabase', True, b'try the provided delta first'), |
|
2625 | 2625 | (b'', b'clear-caches', True, b'clear revlog cache between calls'), |
|
2626 | 2626 | ], |
|
2627 | 2627 | b'-c|-m|FILE', |
|
2628 | 2628 | ) |
|
2629 | 2629 | def perfrevlogwrite(ui, repo, file_=None, startrev=1000, stoprev=-1, **opts): |
|
2630 | 2630 | """Benchmark writing a series of revisions to a revlog. |
|
2631 | 2631 | |
|
2632 | 2632 | Possible source values are: |
|
2633 | 2633 | * `full`: add from a full text (default). |
|
2634 | 2634 | * `parent-1`: add from a delta to the first parent |
|
2635 | 2635 | * `parent-2`: add from a delta to the second parent if it exists |
|
2636 | 2636 | (use a delta from the first parent otherwise) |
|
2637 | 2637 | * `parent-smallest`: add from the smallest delta (either p1 or p2) |
|
2638 | 2638 | * `storage`: add from the existing precomputed deltas |
|
2639 | 2639 | |
|
2640 | 2640 | Note: This performance command measures performance in a custom way. As a |
|
2641 | 2641 | result some of the global configuration of the 'perf' command does not |
|
2642 | 2642 | apply to it: |
|
2643 | 2643 | |
|
2644 | 2644 | * ``pre-run``: disabled |
|
2645 | 2645 | |
|
2646 | 2646 | * ``profile-benchmark``: disabled |
|
2647 | 2647 | |
|
2648 | 2648 | * ``run-limits``: disabled use --count instead |
|
2649 | 2649 | """ |
|
2650 | 2650 | opts = _byteskwargs(opts) |
|
2651 | 2651 | |
|
2652 | 2652 | rl = cmdutil.openrevlog(repo, b'perfrevlogwrite', file_, opts) |
|
2653 | 2653 | rllen = getlen(ui)(rl) |
|
2654 | 2654 | if startrev < 0: |
|
2655 | 2655 | startrev = rllen + startrev |
|
2656 | 2656 | if stoprev < 0: |
|
2657 | 2657 | stoprev = rllen + stoprev |
|
2658 | 2658 | |
|
2659 | 2659 | lazydeltabase = opts['lazydeltabase'] |
|
2660 | 2660 | source = opts['source'] |
|
2661 | 2661 | clearcaches = opts['clear_caches'] |
|
2662 | 2662 | validsource = ( |
|
2663 | 2663 | b'full', |
|
2664 | 2664 | b'parent-1', |
|
2665 | 2665 | b'parent-2', |
|
2666 | 2666 | b'parent-smallest', |
|
2667 | 2667 | b'storage', |
|
2668 | 2668 | ) |
|
2669 | 2669 | if source not in validsource: |
|
2670 | 2670 | raise error.Abort('invalid source type: %s' % source) |
|
2671 | 2671 | |
|
2672 | 2672 | ### actually gather results |
|
2673 | 2673 | count = opts['count'] |
|
2674 | 2674 | if count <= 0: |
|
2675 | 2675 | raise error.Abort('invalide run count: %d' % count) |
|
2676 | 2676 | allresults = [] |
|
2677 | 2677 | for c in range(count): |
|
2678 | 2678 | timing = _timeonewrite( |
|
2679 | 2679 | ui, |
|
2680 | 2680 | rl, |
|
2681 | 2681 | source, |
|
2682 | 2682 | startrev, |
|
2683 | 2683 | stoprev, |
|
2684 | 2684 | c + 1, |
|
2685 | 2685 | lazydeltabase=lazydeltabase, |
|
2686 | 2686 | clearcaches=clearcaches, |
|
2687 | 2687 | ) |
|
2688 | 2688 | allresults.append(timing) |
|
2689 | 2689 | |
|
2690 | 2690 | ### consolidate the results in a single list |
|
2691 | 2691 | results = [] |
|
2692 | 2692 | for idx, (rev, t) in enumerate(allresults[0]): |
|
2693 | 2693 | ts = [t] |
|
2694 | 2694 | for other in allresults[1:]: |
|
2695 | 2695 | orev, ot = other[idx] |
|
2696 | 2696 | assert orev == rev |
|
2697 | 2697 | ts.append(ot) |
|
2698 | 2698 | results.append((rev, ts)) |
|
2699 | 2699 | resultcount = len(results) |
|
2700 | 2700 | |
|
2701 | 2701 | ### Compute and display relevant statistics |
|
2702 | 2702 | |
|
2703 | 2703 | # get a formatter |
|
2704 | 2704 | fm = ui.formatter(b'perf', opts) |
|
2705 | 2705 | displayall = ui.configbool(b"perf", b"all-timing", False) |
|
2706 | 2706 | |
|
2707 | 2707 | # print individual details if requested |
|
2708 | 2708 | if opts['details']: |
|
2709 | 2709 | for idx, item in enumerate(results, 1): |
|
2710 | 2710 | rev, data = item |
|
2711 | 2711 | title = 'revisions #%d of %d, rev %d' % (idx, resultcount, rev) |
|
2712 | 2712 | formatone(fm, data, title=title, displayall=displayall) |
|
2713 | 2713 | |
|
2714 | 2714 | # sorts results by median time |
|
2715 | 2715 | results.sort(key=lambda x: sorted(x[1])[len(x[1]) // 2]) |
|
2716 | 2716 | # list of (name, index) to display) |
|
2717 | 2717 | relevants = [ |
|
2718 | 2718 | ("min", 0), |
|
2719 | 2719 | ("10%", resultcount * 10 // 100), |
|
2720 | 2720 | ("25%", resultcount * 25 // 100), |
|
2721 | 2721 | ("50%", resultcount * 70 // 100), |
|
2722 | 2722 | ("75%", resultcount * 75 // 100), |
|
2723 | 2723 | ("90%", resultcount * 90 // 100), |
|
2724 | 2724 | ("95%", resultcount * 95 // 100), |
|
2725 | 2725 | ("99%", resultcount * 99 // 100), |
|
2726 | 2726 | ("99.9%", resultcount * 999 // 1000), |
|
2727 | 2727 | ("99.99%", resultcount * 9999 // 10000), |
|
2728 | 2728 | ("99.999%", resultcount * 99999 // 100000), |
|
2729 | 2729 | ("max", -1), |
|
2730 | 2730 | ] |
|
2731 | 2731 | if not ui.quiet: |
|
2732 | 2732 | for name, idx in relevants: |
|
2733 | 2733 | data = results[idx] |
|
2734 | 2734 | title = '%s of %d, rev %d' % (name, resultcount, data[0]) |
|
2735 | 2735 | formatone(fm, data[1], title=title, displayall=displayall) |
|
2736 | 2736 | |
|
2737 | 2737 | # XXX summing that many float will not be very precise, we ignore this fact |
|
2738 | 2738 | # for now |
|
2739 | 2739 | totaltime = [] |
|
2740 | 2740 | for item in allresults: |
|
2741 | 2741 | totaltime.append( |
|
2742 | 2742 | ( |
|
2743 | 2743 | sum(x[1][0] for x in item), |
|
2744 | 2744 | sum(x[1][1] for x in item), |
|
2745 | 2745 | sum(x[1][2] for x in item), |
|
2746 | 2746 | ) |
|
2747 | 2747 | ) |
|
2748 | 2748 | formatone( |
|
2749 | 2749 | fm, |
|
2750 | 2750 | totaltime, |
|
2751 | 2751 | title="total time (%d revs)" % resultcount, |
|
2752 | 2752 | displayall=displayall, |
|
2753 | 2753 | ) |
|
2754 | 2754 | fm.end() |
|
2755 | 2755 | |
|
2756 | 2756 | |
|
2757 | 2757 | class _faketr(object): |
|
2758 | 2758 | def add(s, x, y, z=None): |
|
2759 | 2759 | return None |
|
2760 | 2760 | |
|
2761 | 2761 | |
|
2762 | 2762 | def _timeonewrite( |
|
2763 | 2763 | ui, |
|
2764 | 2764 | orig, |
|
2765 | 2765 | source, |
|
2766 | 2766 | startrev, |
|
2767 | 2767 | stoprev, |
|
2768 | 2768 | runidx=None, |
|
2769 | 2769 | lazydeltabase=True, |
|
2770 | 2770 | clearcaches=True, |
|
2771 | 2771 | ): |
|
2772 | 2772 | timings = [] |
|
2773 | 2773 | tr = _faketr() |
|
2774 | 2774 | with _temprevlog(ui, orig, startrev) as dest: |
|
2775 | 2775 | dest._lazydeltabase = lazydeltabase |
|
2776 | 2776 | revs = list(orig.revs(startrev, stoprev)) |
|
2777 | 2777 | total = len(revs) |
|
2778 | 2778 | topic = 'adding' |
|
2779 | 2779 | if runidx is not None: |
|
2780 | 2780 | topic += ' (run #%d)' % runidx |
|
2781 | 2781 | # Support both old and new progress API |
|
2782 | 2782 | if util.safehasattr(ui, 'makeprogress'): |
|
2783 | 2783 | progress = ui.makeprogress(topic, unit='revs', total=total) |
|
2784 | 2784 | |
|
2785 | 2785 | def updateprogress(pos): |
|
2786 | 2786 | progress.update(pos) |
|
2787 | 2787 | |
|
2788 | 2788 | def completeprogress(): |
|
2789 | 2789 | progress.complete() |
|
2790 | 2790 | |
|
2791 | 2791 | else: |
|
2792 | 2792 | |
|
2793 | 2793 | def updateprogress(pos): |
|
2794 | 2794 | ui.progress(topic, pos, unit='revs', total=total) |
|
2795 | 2795 | |
|
2796 | 2796 | def completeprogress(): |
|
2797 | 2797 | ui.progress(topic, None, unit='revs', total=total) |
|
2798 | 2798 | |
|
2799 | 2799 | for idx, rev in enumerate(revs): |
|
2800 | 2800 | updateprogress(idx) |
|
2801 | 2801 | addargs, addkwargs = _getrevisionseed(orig, rev, tr, source) |
|
2802 | 2802 | if clearcaches: |
|
2803 | 2803 | dest.index.clearcaches() |
|
2804 | 2804 | dest.clearcaches() |
|
2805 | 2805 | with timeone() as r: |
|
2806 | 2806 | dest.addrawrevision(*addargs, **addkwargs) |
|
2807 | 2807 | timings.append((rev, r[0])) |
|
2808 | 2808 | updateprogress(total) |
|
2809 | 2809 | completeprogress() |
|
2810 | 2810 | return timings |
|
2811 | 2811 | |
|
2812 | 2812 | |
|
2813 | 2813 | def _getrevisionseed(orig, rev, tr, source): |
|
2814 | 2814 | from mercurial.node import nullid |
|
2815 | 2815 | |
|
2816 | 2816 | linkrev = orig.linkrev(rev) |
|
2817 | 2817 | node = orig.node(rev) |
|
2818 | 2818 | p1, p2 = orig.parents(node) |
|
2819 | 2819 | flags = orig.flags(rev) |
|
2820 | 2820 | cachedelta = None |
|
2821 | 2821 | text = None |
|
2822 | 2822 | |
|
2823 | 2823 | if source == b'full': |
|
2824 | 2824 | text = orig.revision(rev) |
|
2825 | 2825 | elif source == b'parent-1': |
|
2826 | 2826 | baserev = orig.rev(p1) |
|
2827 | 2827 | cachedelta = (baserev, orig.revdiff(p1, rev)) |
|
2828 | 2828 | elif source == b'parent-2': |
|
2829 | 2829 | parent = p2 |
|
2830 | 2830 | if p2 == nullid: |
|
2831 | 2831 | parent = p1 |
|
2832 | 2832 | baserev = orig.rev(parent) |
|
2833 | 2833 | cachedelta = (baserev, orig.revdiff(parent, rev)) |
|
2834 | 2834 | elif source == b'parent-smallest': |
|
2835 | 2835 | p1diff = orig.revdiff(p1, rev) |
|
2836 | 2836 | parent = p1 |
|
2837 | 2837 | diff = p1diff |
|
2838 | 2838 | if p2 != nullid: |
|
2839 | 2839 | p2diff = orig.revdiff(p2, rev) |
|
2840 | 2840 | if len(p1diff) > len(p2diff): |
|
2841 | 2841 | parent = p2 |
|
2842 | 2842 | diff = p2diff |
|
2843 | 2843 | baserev = orig.rev(parent) |
|
2844 | 2844 | cachedelta = (baserev, diff) |
|
2845 | 2845 | elif source == b'storage': |
|
2846 | 2846 | baserev = orig.deltaparent(rev) |
|
2847 | 2847 | cachedelta = (baserev, orig.revdiff(orig.node(baserev), rev)) |
|
2848 | 2848 | |
|
2849 | 2849 | return ( |
|
2850 | 2850 | (text, tr, linkrev, p1, p2), |
|
2851 | 2851 | {'node': node, 'flags': flags, 'cachedelta': cachedelta}, |
|
2852 | 2852 | ) |
|
2853 | 2853 | |
|
2854 | 2854 | |
|
2855 | 2855 | @contextlib.contextmanager |
|
2856 | 2856 | def _temprevlog(ui, orig, truncaterev): |
|
2857 | 2857 | from mercurial import vfs as vfsmod |
|
2858 | 2858 | |
|
2859 | 2859 | if orig._inline: |
|
2860 | 2860 | raise error.Abort('not supporting inline revlog (yet)') |
|
2861 | 2861 | revlogkwargs = {} |
|
2862 | 2862 | k = 'upperboundcomp' |
|
2863 | 2863 | if util.safehasattr(orig, k): |
|
2864 | 2864 | revlogkwargs[k] = getattr(orig, k) |
|
2865 | 2865 | |
|
2866 | 2866 | origindexpath = orig.opener.join(orig.indexfile) |
|
2867 | 2867 | origdatapath = orig.opener.join(orig.datafile) |
|
2868 | 2868 | indexname = 'revlog.i' |
|
2869 | 2869 | dataname = 'revlog.d' |
|
2870 | 2870 | |
|
2871 | 2871 | tmpdir = tempfile.mkdtemp(prefix='tmp-hgperf-') |
|
2872 | 2872 | try: |
|
2873 | 2873 | # copy the data file in a temporary directory |
|
2874 | 2874 | ui.debug('copying data in %s\n' % tmpdir) |
|
2875 | 2875 | destindexpath = os.path.join(tmpdir, 'revlog.i') |
|
2876 | 2876 | destdatapath = os.path.join(tmpdir, 'revlog.d') |
|
2877 | 2877 | shutil.copyfile(origindexpath, destindexpath) |
|
2878 | 2878 | shutil.copyfile(origdatapath, destdatapath) |
|
2879 | 2879 | |
|
2880 | 2880 | # remove the data we want to add again |
|
2881 | 2881 | ui.debug('truncating data to be rewritten\n') |
|
2882 | 2882 | with open(destindexpath, 'ab') as index: |
|
2883 | 2883 | index.seek(0) |
|
2884 | 2884 | index.truncate(truncaterev * orig._io.size) |
|
2885 | 2885 | with open(destdatapath, 'ab') as data: |
|
2886 | 2886 | data.seek(0) |
|
2887 | 2887 | data.truncate(orig.start(truncaterev)) |
|
2888 | 2888 | |
|
2889 | 2889 | # instantiate a new revlog from the temporary copy |
|
2890 | 2890 | ui.debug('truncating adding to be rewritten\n') |
|
2891 | 2891 | vfs = vfsmod.vfs(tmpdir) |
|
2892 | 2892 | vfs.options = getattr(orig.opener, 'options', None) |
|
2893 | 2893 | |
|
2894 | 2894 | dest = revlog.revlog( |
|
2895 | 2895 | vfs, indexfile=indexname, datafile=dataname, **revlogkwargs |
|
2896 | 2896 | ) |
|
2897 | 2897 | if dest._inline: |
|
2898 | 2898 | raise error.Abort('not supporting inline revlog (yet)') |
|
2899 | 2899 | # make sure internals are initialized |
|
2900 | 2900 | dest.revision(len(dest) - 1) |
|
2901 | 2901 | yield dest |
|
2902 | 2902 | del dest, vfs |
|
2903 | 2903 | finally: |
|
2904 | 2904 | shutil.rmtree(tmpdir, True) |
|
2905 | 2905 | |
|
2906 | 2906 | |
|
2907 | 2907 | @command( |
|
2908 | 2908 | b'perfrevlogchunks', |
|
2909 | 2909 | revlogopts |
|
2910 | 2910 | + formatteropts |
|
2911 | 2911 | + [ |
|
2912 | 2912 | (b'e', b'engines', b'', b'compression engines to use'), |
|
2913 | 2913 | (b's', b'startrev', 0, b'revision to start at'), |
|
2914 | 2914 | ], |
|
2915 | 2915 | b'-c|-m|FILE', |
|
2916 | 2916 | ) |
|
2917 | 2917 | def perfrevlogchunks(ui, repo, file_=None, engines=None, startrev=0, **opts): |
|
2918 | 2918 | """Benchmark operations on revlog chunks. |
|
2919 | 2919 | |
|
2920 | 2920 | Logically, each revlog is a collection of fulltext revisions. However, |
|
2921 | 2921 | stored within each revlog are "chunks" of possibly compressed data. This |
|
2922 | 2922 | data needs to be read and decompressed or compressed and written. |
|
2923 | 2923 | |
|
2924 | 2924 | This command measures the time it takes to read+decompress and recompress |
|
2925 | 2925 | chunks in a revlog. It effectively isolates I/O and compression performance. |
|
2926 | 2926 | For measurements of higher-level operations like resolving revisions, |
|
2927 | 2927 | see ``perfrevlogrevisions`` and ``perfrevlogrevision``. |
|
2928 | 2928 | """ |
|
2929 | 2929 | opts = _byteskwargs(opts) |
|
2930 | 2930 | |
|
2931 | 2931 | rl = cmdutil.openrevlog(repo, b'perfrevlogchunks', file_, opts) |
|
2932 | 2932 | |
|
2933 | 2933 | # _chunkraw was renamed to _getsegmentforrevs. |
|
2934 | 2934 | try: |
|
2935 | 2935 | segmentforrevs = rl._getsegmentforrevs |
|
2936 | 2936 | except AttributeError: |
|
2937 | 2937 | segmentforrevs = rl._chunkraw |
|
2938 | 2938 | |
|
2939 | 2939 | # Verify engines argument. |
|
2940 | 2940 | if engines: |
|
2941 | 2941 | engines = set(e.strip() for e in engines.split(b',')) |
|
2942 | 2942 | for engine in engines: |
|
2943 | 2943 | try: |
|
2944 | 2944 | util.compressionengines[engine] |
|
2945 | 2945 | except KeyError: |
|
2946 | 2946 | raise error.Abort(b'unknown compression engine: %s' % engine) |
|
2947 | 2947 | else: |
|
2948 | 2948 | engines = [] |
|
2949 | 2949 | for e in util.compengines: |
|
2950 | 2950 | engine = util.compengines[e] |
|
2951 | 2951 | try: |
|
2952 | 2952 | if engine.available(): |
|
2953 | 2953 | engine.revlogcompressor().compress(b'dummy') |
|
2954 | 2954 | engines.append(e) |
|
2955 | 2955 | except NotImplementedError: |
|
2956 | 2956 | pass |
|
2957 | 2957 | |
|
2958 | 2958 | revs = list(rl.revs(startrev, len(rl) - 1)) |
|
2959 | 2959 | |
|
2960 | 2960 | def rlfh(rl): |
|
2961 | 2961 | if rl._inline: |
|
2962 | 2962 | return getsvfs(repo)(rl.indexfile) |
|
2963 | 2963 | else: |
|
2964 | 2964 | return getsvfs(repo)(rl.datafile) |
|
2965 | 2965 | |
|
2966 | 2966 | def doread(): |
|
2967 | 2967 | rl.clearcaches() |
|
2968 | 2968 | for rev in revs: |
|
2969 | 2969 | segmentforrevs(rev, rev) |
|
2970 | 2970 | |
|
2971 | 2971 | def doreadcachedfh(): |
|
2972 | 2972 | rl.clearcaches() |
|
2973 | 2973 | fh = rlfh(rl) |
|
2974 | 2974 | for rev in revs: |
|
2975 | 2975 | segmentforrevs(rev, rev, df=fh) |
|
2976 | 2976 | |
|
2977 | 2977 | def doreadbatch(): |
|
2978 | 2978 | rl.clearcaches() |
|
2979 | 2979 | segmentforrevs(revs[0], revs[-1]) |
|
2980 | 2980 | |
|
2981 | 2981 | def doreadbatchcachedfh(): |
|
2982 | 2982 | rl.clearcaches() |
|
2983 | 2983 | fh = rlfh(rl) |
|
2984 | 2984 | segmentforrevs(revs[0], revs[-1], df=fh) |
|
2985 | 2985 | |
|
2986 | 2986 | def dochunk(): |
|
2987 | 2987 | rl.clearcaches() |
|
2988 | 2988 | fh = rlfh(rl) |
|
2989 | 2989 | for rev in revs: |
|
2990 | 2990 | rl._chunk(rev, df=fh) |
|
2991 | 2991 | |
|
2992 | 2992 | chunks = [None] |
|
2993 | 2993 | |
|
2994 | 2994 | def dochunkbatch(): |
|
2995 | 2995 | rl.clearcaches() |
|
2996 | 2996 | fh = rlfh(rl) |
|
2997 | 2997 | # Save chunks as a side-effect. |
|
2998 | 2998 | chunks[0] = rl._chunks(revs, df=fh) |
|
2999 | 2999 | |
|
3000 | 3000 | def docompress(compressor): |
|
3001 | 3001 | rl.clearcaches() |
|
3002 | 3002 | |
|
3003 | 3003 | try: |
|
3004 | 3004 | # Swap in the requested compression engine. |
|
3005 | 3005 | oldcompressor = rl._compressor |
|
3006 | 3006 | rl._compressor = compressor |
|
3007 | 3007 | for chunk in chunks[0]: |
|
3008 | 3008 | rl.compress(chunk) |
|
3009 | 3009 | finally: |
|
3010 | 3010 | rl._compressor = oldcompressor |
|
3011 | 3011 | |
|
3012 | 3012 | benches = [ |
|
3013 | 3013 | (lambda: doread(), b'read'), |
|
3014 | 3014 | (lambda: doreadcachedfh(), b'read w/ reused fd'), |
|
3015 | 3015 | (lambda: doreadbatch(), b'read batch'), |
|
3016 | 3016 | (lambda: doreadbatchcachedfh(), b'read batch w/ reused fd'), |
|
3017 | 3017 | (lambda: dochunk(), b'chunk'), |
|
3018 | 3018 | (lambda: dochunkbatch(), b'chunk batch'), |
|
3019 | 3019 | ] |
|
3020 | 3020 | |
|
3021 | 3021 | for engine in sorted(engines): |
|
3022 | 3022 | compressor = util.compengines[engine].revlogcompressor() |
|
3023 | 3023 | benches.append( |
|
3024 | 3024 | ( |
|
3025 | 3025 | functools.partial(docompress, compressor), |
|
3026 | 3026 | b'compress w/ %s' % engine, |
|
3027 | 3027 | ) |
|
3028 | 3028 | ) |
|
3029 | 3029 | |
|
3030 | 3030 | for fn, title in benches: |
|
3031 | 3031 | timer, fm = gettimer(ui, opts) |
|
3032 | 3032 | timer(fn, title=title) |
|
3033 | 3033 | fm.end() |
|
3034 | 3034 | |
|
3035 | 3035 | |
|
3036 | 3036 | @command( |
|
3037 | 3037 | b'perfrevlogrevision', |
|
3038 | 3038 | revlogopts |
|
3039 | 3039 | + formatteropts |
|
3040 | 3040 | + [(b'', b'cache', False, b'use caches instead of clearing')], |
|
3041 | 3041 | b'-c|-m|FILE REV', |
|
3042 | 3042 | ) |
|
3043 | 3043 | def perfrevlogrevision(ui, repo, file_, rev=None, cache=None, **opts): |
|
3044 | 3044 | """Benchmark obtaining a revlog revision. |
|
3045 | 3045 | |
|
3046 | 3046 | Obtaining a revlog revision consists of roughly the following steps: |
|
3047 | 3047 | |
|
3048 | 3048 | 1. Compute the delta chain |
|
3049 | 3049 | 2. Slice the delta chain if applicable |
|
3050 | 3050 | 3. Obtain the raw chunks for that delta chain |
|
3051 | 3051 | 4. Decompress each raw chunk |
|
3052 | 3052 | 5. Apply binary patches to obtain fulltext |
|
3053 | 3053 | 6. Verify hash of fulltext |
|
3054 | 3054 | |
|
3055 | 3055 | This command measures the time spent in each of these phases. |
|
3056 | 3056 | """ |
|
3057 | 3057 | opts = _byteskwargs(opts) |
|
3058 | 3058 | |
|
3059 | 3059 | if opts.get(b'changelog') or opts.get(b'manifest'): |
|
3060 | 3060 | file_, rev = None, file_ |
|
3061 | 3061 | elif rev is None: |
|
3062 | 3062 | raise error.CommandError(b'perfrevlogrevision', b'invalid arguments') |
|
3063 | 3063 | |
|
3064 | 3064 | r = cmdutil.openrevlog(repo, b'perfrevlogrevision', file_, opts) |
|
3065 | 3065 | |
|
3066 | 3066 | # _chunkraw was renamed to _getsegmentforrevs. |
|
3067 | 3067 | try: |
|
3068 | 3068 | segmentforrevs = r._getsegmentforrevs |
|
3069 | 3069 | except AttributeError: |
|
3070 | 3070 | segmentforrevs = r._chunkraw |
|
3071 | 3071 | |
|
3072 | 3072 | node = r.lookup(rev) |
|
3073 | 3073 | rev = r.rev(node) |
|
3074 | 3074 | |
|
3075 | 3075 | def getrawchunks(data, chain): |
|
3076 | 3076 | start = r.start |
|
3077 | 3077 | length = r.length |
|
3078 | 3078 | inline = r._inline |
|
3079 | 3079 | iosize = r._io.size |
|
3080 | 3080 | buffer = util.buffer |
|
3081 | 3081 | |
|
3082 | 3082 | chunks = [] |
|
3083 | 3083 | ladd = chunks.append |
|
3084 | 3084 | for idx, item in enumerate(chain): |
|
3085 | 3085 | offset = start(item[0]) |
|
3086 | 3086 | bits = data[idx] |
|
3087 | 3087 | for rev in item: |
|
3088 | 3088 | chunkstart = start(rev) |
|
3089 | 3089 | if inline: |
|
3090 | 3090 | chunkstart += (rev + 1) * iosize |
|
3091 | 3091 | chunklength = length(rev) |
|
3092 | 3092 | ladd(buffer(bits, chunkstart - offset, chunklength)) |
|
3093 | 3093 | |
|
3094 | 3094 | return chunks |
|
3095 | 3095 | |
|
3096 | 3096 | def dodeltachain(rev): |
|
3097 | 3097 | if not cache: |
|
3098 | 3098 | r.clearcaches() |
|
3099 | 3099 | r._deltachain(rev) |
|
3100 | 3100 | |
|
3101 | 3101 | def doread(chain): |
|
3102 | 3102 | if not cache: |
|
3103 | 3103 | r.clearcaches() |
|
3104 | 3104 | for item in slicedchain: |
|
3105 | 3105 | segmentforrevs(item[0], item[-1]) |
|
3106 | 3106 | |
|
3107 | 3107 | def doslice(r, chain, size): |
|
3108 | 3108 | for s in slicechunk(r, chain, targetsize=size): |
|
3109 | 3109 | pass |
|
3110 | 3110 | |
|
3111 | 3111 | def dorawchunks(data, chain): |
|
3112 | 3112 | if not cache: |
|
3113 | 3113 | r.clearcaches() |
|
3114 | 3114 | getrawchunks(data, chain) |
|
3115 | 3115 | |
|
3116 | 3116 | def dodecompress(chunks): |
|
3117 | 3117 | decomp = r.decompress |
|
3118 | 3118 | for chunk in chunks: |
|
3119 | 3119 | decomp(chunk) |
|
3120 | 3120 | |
|
3121 | 3121 | def dopatch(text, bins): |
|
3122 | 3122 | if not cache: |
|
3123 | 3123 | r.clearcaches() |
|
3124 | 3124 | mdiff.patches(text, bins) |
|
3125 | 3125 | |
|
3126 | 3126 | def dohash(text): |
|
3127 | 3127 | if not cache: |
|
3128 | 3128 | r.clearcaches() |
|
3129 | 3129 | r.checkhash(text, node, rev=rev) |
|
3130 | 3130 | |
|
3131 | 3131 | def dorevision(): |
|
3132 | 3132 | if not cache: |
|
3133 | 3133 | r.clearcaches() |
|
3134 | 3134 | r.revision(node) |
|
3135 | 3135 | |
|
3136 | 3136 | try: |
|
3137 | 3137 | from mercurial.revlogutils.deltas import slicechunk |
|
3138 | 3138 | except ImportError: |
|
3139 | 3139 | slicechunk = getattr(revlog, '_slicechunk', None) |
|
3140 | 3140 | |
|
3141 | 3141 | size = r.length(rev) |
|
3142 | 3142 | chain = r._deltachain(rev)[0] |
|
3143 | 3143 | if not getattr(r, '_withsparseread', False): |
|
3144 | 3144 | slicedchain = (chain,) |
|
3145 | 3145 | else: |
|
3146 | 3146 | slicedchain = tuple(slicechunk(r, chain, targetsize=size)) |
|
3147 | 3147 | data = [segmentforrevs(seg[0], seg[-1])[1] for seg in slicedchain] |
|
3148 | 3148 | rawchunks = getrawchunks(data, slicedchain) |
|
3149 | 3149 | bins = r._chunks(chain) |
|
3150 | 3150 | text = bytes(bins[0]) |
|
3151 | 3151 | bins = bins[1:] |
|
3152 | 3152 | text = mdiff.patches(text, bins) |
|
3153 | 3153 | |
|
3154 | 3154 | benches = [ |
|
3155 | 3155 | (lambda: dorevision(), b'full'), |
|
3156 | 3156 | (lambda: dodeltachain(rev), b'deltachain'), |
|
3157 | 3157 | (lambda: doread(chain), b'read'), |
|
3158 | 3158 | ] |
|
3159 | 3159 | |
|
3160 | 3160 | if getattr(r, '_withsparseread', False): |
|
3161 | 3161 | slicing = (lambda: doslice(r, chain, size), b'slice-sparse-chain') |
|
3162 | 3162 | benches.append(slicing) |
|
3163 | 3163 | |
|
3164 | 3164 | benches.extend( |
|
3165 | 3165 | [ |
|
3166 | 3166 | (lambda: dorawchunks(data, slicedchain), b'rawchunks'), |
|
3167 | 3167 | (lambda: dodecompress(rawchunks), b'decompress'), |
|
3168 | 3168 | (lambda: dopatch(text, bins), b'patch'), |
|
3169 | 3169 | (lambda: dohash(text), b'hash'), |
|
3170 | 3170 | ] |
|
3171 | 3171 | ) |
|
3172 | 3172 | |
|
3173 | 3173 | timer, fm = gettimer(ui, opts) |
|
3174 | 3174 | for fn, title in benches: |
|
3175 | 3175 | timer(fn, title=title) |
|
3176 | 3176 | fm.end() |
|
3177 | 3177 | |
|
3178 | 3178 | |
|
3179 | 3179 | @command( |
|
3180 | 3180 | b'perfrevset', |
|
3181 | 3181 | [ |
|
3182 | 3182 | (b'C', b'clear', False, b'clear volatile cache between each call.'), |
|
3183 | 3183 | (b'', b'contexts', False, b'obtain changectx for each revision'), |
|
3184 | 3184 | ] |
|
3185 | 3185 | + formatteropts, |
|
3186 | 3186 | b"REVSET", |
|
3187 | 3187 | ) |
|
3188 | 3188 | def perfrevset(ui, repo, expr, clear=False, contexts=False, **opts): |
|
3189 | 3189 | """benchmark the execution time of a revset |
|
3190 | 3190 | |
|
3191 | 3191 | Use the --clean option if need to evaluate the impact of build volatile |
|
3192 | 3192 | revisions set cache on the revset execution. Volatile cache hold filtered |
|
3193 | 3193 | and obsolete related cache.""" |
|
3194 | 3194 | opts = _byteskwargs(opts) |
|
3195 | 3195 | |
|
3196 | 3196 | timer, fm = gettimer(ui, opts) |
|
3197 | 3197 | |
|
3198 | 3198 | def d(): |
|
3199 | 3199 | if clear: |
|
3200 | 3200 | repo.invalidatevolatilesets() |
|
3201 | 3201 | if contexts: |
|
3202 | 3202 | for ctx in repo.set(expr): |
|
3203 | 3203 | pass |
|
3204 | 3204 | else: |
|
3205 | 3205 | for r in repo.revs(expr): |
|
3206 | 3206 | pass |
|
3207 | 3207 | |
|
3208 | 3208 | timer(d) |
|
3209 | 3209 | fm.end() |
|
3210 | 3210 | |
|
3211 | 3211 | |
|
3212 | 3212 | @command( |
|
3213 | 3213 | b'perfvolatilesets', |
|
3214 | 3214 | [(b'', b'clear-obsstore', False, b'drop obsstore between each call.'),] |
|
3215 | 3215 | + formatteropts, |
|
3216 | 3216 | ) |
|
3217 | 3217 | def perfvolatilesets(ui, repo, *names, **opts): |
|
3218 | 3218 | """benchmark the computation of various volatile set |
|
3219 | 3219 | |
|
3220 | 3220 | Volatile set computes element related to filtering and obsolescence.""" |
|
3221 | 3221 | opts = _byteskwargs(opts) |
|
3222 | 3222 | timer, fm = gettimer(ui, opts) |
|
3223 | 3223 | repo = repo.unfiltered() |
|
3224 | 3224 | |
|
3225 | 3225 | def getobs(name): |
|
3226 | 3226 | def d(): |
|
3227 | 3227 | repo.invalidatevolatilesets() |
|
3228 | 3228 | if opts[b'clear_obsstore']: |
|
3229 | 3229 | clearfilecache(repo, b'obsstore') |
|
3230 | 3230 | obsolete.getrevs(repo, name) |
|
3231 | 3231 | |
|
3232 | 3232 | return d |
|
3233 | 3233 | |
|
3234 | 3234 | allobs = sorted(obsolete.cachefuncs) |
|
3235 | 3235 | if names: |
|
3236 | 3236 | allobs = [n for n in allobs if n in names] |
|
3237 | 3237 | |
|
3238 | 3238 | for name in allobs: |
|
3239 | 3239 | timer(getobs(name), title=name) |
|
3240 | 3240 | |
|
3241 | 3241 | def getfiltered(name): |
|
3242 | 3242 | def d(): |
|
3243 | 3243 | repo.invalidatevolatilesets() |
|
3244 | 3244 | if opts[b'clear_obsstore']: |
|
3245 | 3245 | clearfilecache(repo, b'obsstore') |
|
3246 | 3246 | repoview.filterrevs(repo, name) |
|
3247 | 3247 | |
|
3248 | 3248 | return d |
|
3249 | 3249 | |
|
3250 | 3250 | allfilter = sorted(repoview.filtertable) |
|
3251 | 3251 | if names: |
|
3252 | 3252 | allfilter = [n for n in allfilter if n in names] |
|
3253 | 3253 | |
|
3254 | 3254 | for name in allfilter: |
|
3255 | 3255 | timer(getfiltered(name), title=name) |
|
3256 | 3256 | fm.end() |
|
3257 | 3257 | |
|
3258 | 3258 | |
|
3259 | 3259 | @command( |
|
3260 | 3260 | b'perfbranchmap', |
|
3261 | 3261 | [ |
|
3262 | 3262 | (b'f', b'full', False, b'Includes build time of subset'), |
|
3263 | 3263 | ( |
|
3264 | 3264 | b'', |
|
3265 | 3265 | b'clear-revbranch', |
|
3266 | 3266 | False, |
|
3267 | 3267 | b'purge the revbranch cache between computation', |
|
3268 | 3268 | ), |
|
3269 | 3269 | ] |
|
3270 | 3270 | + formatteropts, |
|
3271 | 3271 | ) |
|
3272 | 3272 | def perfbranchmap(ui, repo, *filternames, **opts): |
|
3273 | 3273 | """benchmark the update of a branchmap |
|
3274 | 3274 | |
|
3275 | 3275 | This benchmarks the full repo.branchmap() call with read and write disabled |
|
3276 | 3276 | """ |
|
3277 | 3277 | opts = _byteskwargs(opts) |
|
3278 | 3278 | full = opts.get(b"full", False) |
|
3279 | 3279 | clear_revbranch = opts.get(b"clear_revbranch", False) |
|
3280 | 3280 | timer, fm = gettimer(ui, opts) |
|
3281 | 3281 | |
|
3282 | 3282 | def getbranchmap(filtername): |
|
3283 | 3283 | """generate a benchmark function for the filtername""" |
|
3284 | 3284 | if filtername is None: |
|
3285 | 3285 | view = repo |
|
3286 | 3286 | else: |
|
3287 | 3287 | view = repo.filtered(filtername) |
|
3288 | 3288 | if util.safehasattr(view._branchcaches, '_per_filter'): |
|
3289 | 3289 | filtered = view._branchcaches._per_filter |
|
3290 | 3290 | else: |
|
3291 | 3291 | # older versions |
|
3292 | 3292 | filtered = view._branchcaches |
|
3293 | 3293 | |
|
3294 | 3294 | def d(): |
|
3295 | 3295 | if clear_revbranch: |
|
3296 | 3296 | repo.revbranchcache()._clear() |
|
3297 | 3297 | if full: |
|
3298 | 3298 | view._branchcaches.clear() |
|
3299 | 3299 | else: |
|
3300 | 3300 | filtered.pop(filtername, None) |
|
3301 | 3301 | view.branchmap() |
|
3302 | 3302 | |
|
3303 | 3303 | return d |
|
3304 | 3304 | |
|
3305 | 3305 | # add filter in smaller subset to bigger subset |
|
3306 | 3306 | possiblefilters = set(repoview.filtertable) |
|
3307 | 3307 | if filternames: |
|
3308 | 3308 | possiblefilters &= set(filternames) |
|
3309 | 3309 | subsettable = getbranchmapsubsettable() |
|
3310 | 3310 | allfilters = [] |
|
3311 | 3311 | while possiblefilters: |
|
3312 | 3312 | for name in possiblefilters: |
|
3313 | 3313 | subset = subsettable.get(name) |
|
3314 | 3314 | if subset not in possiblefilters: |
|
3315 | 3315 | break |
|
3316 | 3316 | else: |
|
3317 | 3317 | assert False, b'subset cycle %s!' % possiblefilters |
|
3318 | 3318 | allfilters.append(name) |
|
3319 | 3319 | possiblefilters.remove(name) |
|
3320 | 3320 | |
|
3321 | 3321 | # warm the cache |
|
3322 | 3322 | if not full: |
|
3323 | 3323 | for name in allfilters: |
|
3324 | 3324 | repo.filtered(name).branchmap() |
|
3325 | 3325 | if not filternames or b'unfiltered' in filternames: |
|
3326 | 3326 | # add unfiltered |
|
3327 | 3327 | allfilters.append(None) |
|
3328 | 3328 | |
|
3329 | 3329 | if util.safehasattr(branchmap.branchcache, 'fromfile'): |
|
3330 | 3330 | branchcacheread = safeattrsetter(branchmap.branchcache, b'fromfile') |
|
3331 | 3331 | branchcacheread.set(classmethod(lambda *args: None)) |
|
3332 | 3332 | else: |
|
3333 | 3333 | # older versions |
|
3334 | 3334 | branchcacheread = safeattrsetter(branchmap, b'read') |
|
3335 | 3335 | branchcacheread.set(lambda *args: None) |
|
3336 | 3336 | branchcachewrite = safeattrsetter(branchmap.branchcache, b'write') |
|
3337 | 3337 | branchcachewrite.set(lambda *args: None) |
|
3338 | 3338 | try: |
|
3339 | 3339 | for name in allfilters: |
|
3340 | 3340 | printname = name |
|
3341 | 3341 | if name is None: |
|
3342 | 3342 | printname = b'unfiltered' |
|
3343 | 3343 | timer(getbranchmap(name), title=str(printname)) |
|
3344 | 3344 | finally: |
|
3345 | 3345 | branchcacheread.restore() |
|
3346 | 3346 | branchcachewrite.restore() |
|
3347 | 3347 | fm.end() |
|
3348 | 3348 | |
|
3349 | 3349 | |
|
3350 | 3350 | @command( |
|
3351 | 3351 | b'perfbranchmapupdate', |
|
3352 | 3352 | [ |
|
3353 | 3353 | (b'', b'base', [], b'subset of revision to start from'), |
|
3354 | 3354 | (b'', b'target', [], b'subset of revision to end with'), |
|
3355 | 3355 | (b'', b'clear-caches', False, b'clear cache between each runs'), |
|
3356 | 3356 | ] |
|
3357 | 3357 | + formatteropts, |
|
3358 | 3358 | ) |
|
3359 | 3359 | def perfbranchmapupdate(ui, repo, base=(), target=(), **opts): |
|
3360 | 3360 | """benchmark branchmap update from for <base> revs to <target> revs |
|
3361 | 3361 | |
|
3362 | 3362 | If `--clear-caches` is passed, the following items will be reset before |
|
3363 | 3363 | each update: |
|
3364 | 3364 | * the changelog instance and associated indexes |
|
3365 | 3365 | * the rev-branch-cache instance |
|
3366 | 3366 | |
|
3367 | 3367 | Examples: |
|
3368 | 3368 | |
|
3369 | 3369 | # update for the one last revision |
|
3370 | 3370 | $ hg perfbranchmapupdate --base 'not tip' --target 'tip' |
|
3371 | 3371 | |
|
3372 | 3372 | $ update for change coming with a new branch |
|
3373 | 3373 | $ hg perfbranchmapupdate --base 'stable' --target 'default' |
|
3374 | 3374 | """ |
|
3375 | 3375 | from mercurial import branchmap |
|
3376 | 3376 | from mercurial import repoview |
|
3377 | 3377 | |
|
3378 | 3378 | opts = _byteskwargs(opts) |
|
3379 | 3379 | timer, fm = gettimer(ui, opts) |
|
3380 | 3380 | clearcaches = opts[b'clear_caches'] |
|
3381 | 3381 | unfi = repo.unfiltered() |
|
3382 | 3382 | x = [None] # used to pass data between closure |
|
3383 | 3383 | |
|
3384 | 3384 | # we use a `list` here to avoid possible side effect from smartset |
|
3385 | 3385 | baserevs = list(scmutil.revrange(repo, base)) |
|
3386 | 3386 | targetrevs = list(scmutil.revrange(repo, target)) |
|
3387 | 3387 | if not baserevs: |
|
3388 | 3388 | raise error.Abort(b'no revisions selected for --base') |
|
3389 | 3389 | if not targetrevs: |
|
3390 | 3390 | raise error.Abort(b'no revisions selected for --target') |
|
3391 | 3391 | |
|
3392 | 3392 | # make sure the target branchmap also contains the one in the base |
|
3393 | 3393 | targetrevs = list(set(baserevs) | set(targetrevs)) |
|
3394 | 3394 | targetrevs.sort() |
|
3395 | 3395 | |
|
3396 | 3396 | cl = repo.changelog |
|
3397 | 3397 | allbaserevs = list(cl.ancestors(baserevs, inclusive=True)) |
|
3398 | 3398 | allbaserevs.sort() |
|
3399 | 3399 | alltargetrevs = frozenset(cl.ancestors(targetrevs, inclusive=True)) |
|
3400 | 3400 | |
|
3401 | 3401 | newrevs = list(alltargetrevs.difference(allbaserevs)) |
|
3402 | 3402 | newrevs.sort() |
|
3403 | 3403 | |
|
3404 | 3404 | allrevs = frozenset(unfi.changelog.revs()) |
|
3405 | 3405 | basefilterrevs = frozenset(allrevs.difference(allbaserevs)) |
|
3406 | 3406 | targetfilterrevs = frozenset(allrevs.difference(alltargetrevs)) |
|
3407 | 3407 | |
|
3408 | 3408 | def basefilter(repo, visibilityexceptions=None): |
|
3409 | 3409 | return basefilterrevs |
|
3410 | 3410 | |
|
3411 | 3411 | def targetfilter(repo, visibilityexceptions=None): |
|
3412 | 3412 | return targetfilterrevs |
|
3413 | 3413 | |
|
3414 | 3414 | msg = b'benchmark of branchmap with %d revisions with %d new ones\n' |
|
3415 | 3415 | ui.status(msg % (len(allbaserevs), len(newrevs))) |
|
3416 | 3416 | if targetfilterrevs: |
|
3417 | 3417 | msg = b'(%d revisions still filtered)\n' |
|
3418 | 3418 | ui.status(msg % len(targetfilterrevs)) |
|
3419 | 3419 | |
|
3420 | 3420 | try: |
|
3421 | 3421 | repoview.filtertable[b'__perf_branchmap_update_base'] = basefilter |
|
3422 | 3422 | repoview.filtertable[b'__perf_branchmap_update_target'] = targetfilter |
|
3423 | 3423 | |
|
3424 | 3424 | baserepo = repo.filtered(b'__perf_branchmap_update_base') |
|
3425 | 3425 | targetrepo = repo.filtered(b'__perf_branchmap_update_target') |
|
3426 | 3426 | |
|
3427 | 3427 | # try to find an existing branchmap to reuse |
|
3428 | 3428 | subsettable = getbranchmapsubsettable() |
|
3429 | 3429 | candidatefilter = subsettable.get(None) |
|
3430 | 3430 | while candidatefilter is not None: |
|
3431 | 3431 | candidatebm = repo.filtered(candidatefilter).branchmap() |
|
3432 | 3432 | if candidatebm.validfor(baserepo): |
|
3433 | 3433 | filtered = repoview.filterrevs(repo, candidatefilter) |
|
3434 | 3434 | missing = [r for r in allbaserevs if r in filtered] |
|
3435 | 3435 | base = candidatebm.copy() |
|
3436 | 3436 | base.update(baserepo, missing) |
|
3437 | 3437 | break |
|
3438 | 3438 | candidatefilter = subsettable.get(candidatefilter) |
|
3439 | 3439 | else: |
|
3440 | 3440 | # no suitable subset where found |
|
3441 | 3441 | base = branchmap.branchcache() |
|
3442 | 3442 | base.update(baserepo, allbaserevs) |
|
3443 | 3443 | |
|
3444 | 3444 | def setup(): |
|
3445 | 3445 | x[0] = base.copy() |
|
3446 | 3446 | if clearcaches: |
|
3447 | 3447 | unfi._revbranchcache = None |
|
3448 | 3448 | clearchangelog(repo) |
|
3449 | 3449 | |
|
3450 | 3450 | def bench(): |
|
3451 | 3451 | x[0].update(targetrepo, newrevs) |
|
3452 | 3452 | |
|
3453 | 3453 | timer(bench, setup=setup) |
|
3454 | 3454 | fm.end() |
|
3455 | 3455 | finally: |
|
3456 | 3456 | repoview.filtertable.pop(b'__perf_branchmap_update_base', None) |
|
3457 | 3457 | repoview.filtertable.pop(b'__perf_branchmap_update_target', None) |
|
3458 | 3458 | |
|
3459 | 3459 | |
|
3460 | 3460 | @command( |
|
3461 | 3461 | b'perfbranchmapload', |
|
3462 | 3462 | [ |
|
3463 | 3463 | (b'f', b'filter', b'', b'Specify repoview filter'), |
|
3464 | 3464 | (b'', b'list', False, b'List brachmap filter caches'), |
|
3465 | 3465 | (b'', b'clear-revlogs', False, b'refresh changelog and manifest'), |
|
3466 | 3466 | ] |
|
3467 | 3467 | + formatteropts, |
|
3468 | 3468 | ) |
|
3469 | 3469 | def perfbranchmapload(ui, repo, filter=b'', list=False, **opts): |
|
3470 | 3470 | """benchmark reading the branchmap""" |
|
3471 | 3471 | opts = _byteskwargs(opts) |
|
3472 | 3472 | clearrevlogs = opts[b'clear_revlogs'] |
|
3473 | 3473 | |
|
3474 | 3474 | if list: |
|
3475 | 3475 | for name, kind, st in repo.cachevfs.readdir(stat=True): |
|
3476 | 3476 | if name.startswith(b'branch2'): |
|
3477 | 3477 | filtername = name.partition(b'-')[2] or b'unfiltered' |
|
3478 | 3478 | ui.status( |
|
3479 | 3479 | b'%s - %s\n' % (filtername, util.bytecount(st.st_size)) |
|
3480 | 3480 | ) |
|
3481 | 3481 | return |
|
3482 | 3482 | if not filter: |
|
3483 | 3483 | filter = None |
|
3484 | 3484 | subsettable = getbranchmapsubsettable() |
|
3485 | 3485 | if filter is None: |
|
3486 | 3486 | repo = repo.unfiltered() |
|
3487 | 3487 | else: |
|
3488 | 3488 | repo = repoview.repoview(repo, filter) |
|
3489 | 3489 | |
|
3490 | 3490 | repo.branchmap() # make sure we have a relevant, up to date branchmap |
|
3491 | 3491 | |
|
3492 | 3492 | try: |
|
3493 | 3493 | fromfile = branchmap.branchcache.fromfile |
|
3494 | 3494 | except AttributeError: |
|
3495 | 3495 | # older versions |
|
3496 | 3496 | fromfile = branchmap.read |
|
3497 | 3497 | |
|
3498 | 3498 | currentfilter = filter |
|
3499 | 3499 | # try once without timer, the filter may not be cached |
|
3500 | 3500 | while fromfile(repo) is None: |
|
3501 | 3501 | currentfilter = subsettable.get(currentfilter) |
|
3502 | 3502 | if currentfilter is None: |
|
3503 | 3503 | raise error.Abort( |
|
3504 | 3504 | b'No branchmap cached for %s repo' % (filter or b'unfiltered') |
|
3505 | 3505 | ) |
|
3506 | 3506 | repo = repo.filtered(currentfilter) |
|
3507 | 3507 | timer, fm = gettimer(ui, opts) |
|
3508 | 3508 | |
|
3509 | 3509 | def setup(): |
|
3510 | 3510 | if clearrevlogs: |
|
3511 | 3511 | clearchangelog(repo) |
|
3512 | 3512 | |
|
3513 | 3513 | def bench(): |
|
3514 | 3514 | fromfile(repo) |
|
3515 | 3515 | |
|
3516 | 3516 | timer(bench, setup=setup) |
|
3517 | 3517 | fm.end() |
|
3518 | 3518 | |
|
3519 | 3519 | |
|
3520 | 3520 | @command(b'perfloadmarkers') |
|
3521 | 3521 | def perfloadmarkers(ui, repo): |
|
3522 | 3522 | """benchmark the time to parse the on-disk markers for a repo |
|
3523 | 3523 | |
|
3524 | 3524 | Result is the number of markers in the repo.""" |
|
3525 | 3525 | timer, fm = gettimer(ui) |
|
3526 | 3526 | svfs = getsvfs(repo) |
|
3527 | 3527 | timer(lambda: len(obsolete.obsstore(svfs))) |
|
3528 | 3528 | fm.end() |
|
3529 | 3529 | |
|
3530 | 3530 | |
|
3531 | 3531 | @command( |
|
3532 | 3532 | b'perflrucachedict', |
|
3533 | 3533 | formatteropts |
|
3534 | 3534 | + [ |
|
3535 | 3535 | (b'', b'costlimit', 0, b'maximum total cost of items in cache'), |
|
3536 | 3536 | (b'', b'mincost', 0, b'smallest cost of items in cache'), |
|
3537 | 3537 | (b'', b'maxcost', 100, b'maximum cost of items in cache'), |
|
3538 | 3538 | (b'', b'size', 4, b'size of cache'), |
|
3539 | 3539 | (b'', b'gets', 10000, b'number of key lookups'), |
|
3540 | 3540 | (b'', b'sets', 10000, b'number of key sets'), |
|
3541 | 3541 | (b'', b'mixed', 10000, b'number of mixed mode operations'), |
|
3542 | 3542 | ( |
|
3543 | 3543 | b'', |
|
3544 | 3544 | b'mixedgetfreq', |
|
3545 | 3545 | 50, |
|
3546 | 3546 | b'frequency of get vs set ops in mixed mode', |
|
3547 | 3547 | ), |
|
3548 | 3548 | ], |
|
3549 | 3549 | norepo=True, |
|
3550 | 3550 | ) |
|
3551 | 3551 | def perflrucache( |
|
3552 | 3552 | ui, |
|
3553 | 3553 | mincost=0, |
|
3554 | 3554 | maxcost=100, |
|
3555 | 3555 | costlimit=0, |
|
3556 | 3556 | size=4, |
|
3557 | 3557 | gets=10000, |
|
3558 | 3558 | sets=10000, |
|
3559 | 3559 | mixed=10000, |
|
3560 | 3560 | mixedgetfreq=50, |
|
3561 | 3561 | **opts |
|
3562 | 3562 | ): |
|
3563 | 3563 | opts = _byteskwargs(opts) |
|
3564 | 3564 | |
|
3565 | 3565 | def doinit(): |
|
3566 | 3566 | for i in _xrange(10000): |
|
3567 | 3567 | util.lrucachedict(size) |
|
3568 | 3568 | |
|
3569 | 3569 | costrange = list(range(mincost, maxcost + 1)) |
|
3570 | 3570 | |
|
3571 | 3571 | values = [] |
|
3572 | 3572 | for i in _xrange(size): |
|
3573 | 3573 | values.append(random.randint(0, _maxint)) |
|
3574 | 3574 | |
|
3575 | 3575 | # Get mode fills the cache and tests raw lookup performance with no |
|
3576 | 3576 | # eviction. |
|
3577 | 3577 | getseq = [] |
|
3578 | 3578 | for i in _xrange(gets): |
|
3579 | 3579 | getseq.append(random.choice(values)) |
|
3580 | 3580 | |
|
3581 | 3581 | def dogets(): |
|
3582 | 3582 | d = util.lrucachedict(size) |
|
3583 | 3583 | for v in values: |
|
3584 | 3584 | d[v] = v |
|
3585 | 3585 | for key in getseq: |
|
3586 | 3586 | value = d[key] |
|
3587 | 3587 | value # silence pyflakes warning |
|
3588 | 3588 | |
|
3589 | 3589 | def dogetscost(): |
|
3590 | 3590 | d = util.lrucachedict(size, maxcost=costlimit) |
|
3591 | 3591 | for i, v in enumerate(values): |
|
3592 | 3592 | d.insert(v, v, cost=costs[i]) |
|
3593 | 3593 | for key in getseq: |
|
3594 | 3594 | try: |
|
3595 | 3595 | value = d[key] |
|
3596 | 3596 | value # silence pyflakes warning |
|
3597 | 3597 | except KeyError: |
|
3598 | 3598 | pass |
|
3599 | 3599 | |
|
3600 | 3600 | # Set mode tests insertion speed with cache eviction. |
|
3601 | 3601 | setseq = [] |
|
3602 | 3602 | costs = [] |
|
3603 | 3603 | for i in _xrange(sets): |
|
3604 | 3604 | setseq.append(random.randint(0, _maxint)) |
|
3605 | 3605 | costs.append(random.choice(costrange)) |
|
3606 | 3606 | |
|
3607 | 3607 | def doinserts(): |
|
3608 | 3608 | d = util.lrucachedict(size) |
|
3609 | 3609 | for v in setseq: |
|
3610 | 3610 | d.insert(v, v) |
|
3611 | 3611 | |
|
3612 | 3612 | def doinsertscost(): |
|
3613 | 3613 | d = util.lrucachedict(size, maxcost=costlimit) |
|
3614 | 3614 | for i, v in enumerate(setseq): |
|
3615 | 3615 | d.insert(v, v, cost=costs[i]) |
|
3616 | 3616 | |
|
3617 | 3617 | def dosets(): |
|
3618 | 3618 | d = util.lrucachedict(size) |
|
3619 | 3619 | for v in setseq: |
|
3620 | 3620 | d[v] = v |
|
3621 | 3621 | |
|
3622 | 3622 | # Mixed mode randomly performs gets and sets with eviction. |
|
3623 | 3623 | mixedops = [] |
|
3624 | 3624 | for i in _xrange(mixed): |
|
3625 | 3625 | r = random.randint(0, 100) |
|
3626 | 3626 | if r < mixedgetfreq: |
|
3627 | 3627 | op = 0 |
|
3628 | 3628 | else: |
|
3629 | 3629 | op = 1 |
|
3630 | 3630 | |
|
3631 | 3631 | mixedops.append( |
|
3632 | 3632 | (op, random.randint(0, size * 2), random.choice(costrange)) |
|
3633 | 3633 | ) |
|
3634 | 3634 | |
|
3635 | 3635 | def domixed(): |
|
3636 | 3636 | d = util.lrucachedict(size) |
|
3637 | 3637 | |
|
3638 | 3638 | for op, v, cost in mixedops: |
|
3639 | 3639 | if op == 0: |
|
3640 | 3640 | try: |
|
3641 | 3641 | d[v] |
|
3642 | 3642 | except KeyError: |
|
3643 | 3643 | pass |
|
3644 | 3644 | else: |
|
3645 | 3645 | d[v] = v |
|
3646 | 3646 | |
|
3647 | 3647 | def domixedcost(): |
|
3648 | 3648 | d = util.lrucachedict(size, maxcost=costlimit) |
|
3649 | 3649 | |
|
3650 | 3650 | for op, v, cost in mixedops: |
|
3651 | 3651 | if op == 0: |
|
3652 | 3652 | try: |
|
3653 | 3653 | d[v] |
|
3654 | 3654 | except KeyError: |
|
3655 | 3655 | pass |
|
3656 | 3656 | else: |
|
3657 | 3657 | d.insert(v, v, cost=cost) |
|
3658 | 3658 | |
|
3659 | 3659 | benches = [ |
|
3660 | 3660 | (doinit, b'init'), |
|
3661 | 3661 | ] |
|
3662 | 3662 | |
|
3663 | 3663 | if costlimit: |
|
3664 | 3664 | benches.extend( |
|
3665 | 3665 | [ |
|
3666 | 3666 | (dogetscost, b'gets w/ cost limit'), |
|
3667 | 3667 | (doinsertscost, b'inserts w/ cost limit'), |
|
3668 | 3668 | (domixedcost, b'mixed w/ cost limit'), |
|
3669 | 3669 | ] |
|
3670 | 3670 | ) |
|
3671 | 3671 | else: |
|
3672 | 3672 | benches.extend( |
|
3673 | 3673 | [ |
|
3674 | 3674 | (dogets, b'gets'), |
|
3675 | 3675 | (doinserts, b'inserts'), |
|
3676 | 3676 | (dosets, b'sets'), |
|
3677 | 3677 | (domixed, b'mixed'), |
|
3678 | 3678 | ] |
|
3679 | 3679 | ) |
|
3680 | 3680 | |
|
3681 | 3681 | for fn, title in benches: |
|
3682 | 3682 | timer, fm = gettimer(ui, opts) |
|
3683 | 3683 | timer(fn, title=title) |
|
3684 | 3684 | fm.end() |
|
3685 | 3685 | |
|
3686 | 3686 | |
|
3687 | 3687 | @command(b'perfwrite', formatteropts) |
|
3688 | 3688 | def perfwrite(ui, repo, **opts): |
|
3689 | 3689 | """microbenchmark ui.write |
|
3690 | 3690 | """ |
|
3691 | 3691 | opts = _byteskwargs(opts) |
|
3692 | 3692 | |
|
3693 | 3693 | timer, fm = gettimer(ui, opts) |
|
3694 | 3694 | |
|
3695 | 3695 | def write(): |
|
3696 | 3696 | for i in range(100000): |
|
3697 | ui.write(b'Testing write performance\n') | |
|
3697 | ui.writenoi18n(b'Testing write performance\n') | |
|
3698 | 3698 | |
|
3699 | 3699 | timer(write) |
|
3700 | 3700 | fm.end() |
|
3701 | 3701 | |
|
3702 | 3702 | |
|
3703 | 3703 | def uisetup(ui): |
|
3704 | 3704 | if util.safehasattr(cmdutil, b'openrevlog') and not util.safehasattr( |
|
3705 | 3705 | commands, b'debugrevlogopts' |
|
3706 | 3706 | ): |
|
3707 | 3707 | # for "historical portability": |
|
3708 | 3708 | # In this case, Mercurial should be 1.9 (or a79fea6b3e77) - |
|
3709 | 3709 | # 3.7 (or 5606f7d0d063). Therefore, '--dir' option for |
|
3710 | 3710 | # openrevlog() should cause failure, because it has been |
|
3711 | 3711 | # available since 3.5 (or 49c583ca48c4). |
|
3712 | 3712 | def openrevlog(orig, repo, cmd, file_, opts): |
|
3713 | 3713 | if opts.get(b'dir') and not util.safehasattr(repo, b'dirlog'): |
|
3714 | 3714 | raise error.Abort( |
|
3715 | 3715 | b"This version doesn't support --dir option", |
|
3716 | 3716 | hint=b"use 3.5 or later", |
|
3717 | 3717 | ) |
|
3718 | 3718 | return orig(repo, cmd, file_, opts) |
|
3719 | 3719 | |
|
3720 | 3720 | extensions.wrapfunction(cmdutil, b'openrevlog', openrevlog) |
|
3721 | 3721 | |
|
3722 | 3722 | |
|
3723 | 3723 | @command( |
|
3724 | 3724 | b'perfprogress', |
|
3725 | 3725 | formatteropts |
|
3726 | 3726 | + [ |
|
3727 | 3727 | (b'', b'topic', b'topic', b'topic for progress messages'), |
|
3728 | 3728 | (b'c', b'total', 1000000, b'total value we are progressing to'), |
|
3729 | 3729 | ], |
|
3730 | 3730 | norepo=True, |
|
3731 | 3731 | ) |
|
3732 | 3732 | def perfprogress(ui, topic=None, total=None, **opts): |
|
3733 | 3733 | """printing of progress bars""" |
|
3734 | 3734 | opts = _byteskwargs(opts) |
|
3735 | 3735 | |
|
3736 | 3736 | timer, fm = gettimer(ui, opts) |
|
3737 | 3737 | |
|
3738 | 3738 | def doprogress(): |
|
3739 | 3739 | with ui.makeprogress(topic, total=total) as progress: |
|
3740 | 3740 | for i in _xrange(total): |
|
3741 | 3741 | progress.increment() |
|
3742 | 3742 | |
|
3743 | 3743 | timer(doprogress) |
|
3744 | 3744 | fm.end() |
@@ -1,227 +1,227 b'' | |||
|
1 | 1 | #!/usr/bin/env python |
|
2 | 2 | # |
|
3 | 3 | # checkseclevel - checking section title levels in each online help document |
|
4 | 4 | |
|
5 | 5 | from __future__ import absolute_import |
|
6 | 6 | |
|
7 | 7 | import optparse |
|
8 | 8 | import os |
|
9 | 9 | import sys |
|
10 | 10 | |
|
11 | 11 | # import from the live mercurial repo |
|
12 | 12 | os.environ['HGMODULEPOLICY'] = 'py' |
|
13 | 13 | sys.path.insert(0, "..") |
|
14 | 14 | from mercurial import demandimport |
|
15 | 15 | |
|
16 | 16 | demandimport.enable() |
|
17 | 17 | from mercurial import ( |
|
18 | 18 | commands, |
|
19 | 19 | extensions, |
|
20 | 20 | help, |
|
21 | 21 | minirst, |
|
22 | 22 | ui as uimod, |
|
23 | 23 | ) |
|
24 | 24 | |
|
25 | 25 | table = commands.table |
|
26 | 26 | helptable = help.helptable |
|
27 | 27 | |
|
28 | 28 | level2mark = [b'"', b'=', b'-', b'.', b'#'] |
|
29 | 29 | reservedmarks = [b'"'] |
|
30 | 30 | |
|
31 | 31 | mark2level = {} |
|
32 | 32 | for m, l in zip(level2mark, range(len(level2mark))): |
|
33 | 33 | if m not in reservedmarks: |
|
34 | 34 | mark2level[m] = l |
|
35 | 35 | |
|
36 | 36 | initlevel_topic = 0 |
|
37 | 37 | initlevel_cmd = 1 |
|
38 | 38 | initlevel_ext = 1 |
|
39 | 39 | initlevel_ext_cmd = 3 |
|
40 | 40 | |
|
41 | 41 | |
|
42 | 42 | def showavailables(ui, initlevel): |
|
43 | 43 | avail = ' available marks and order of them in this help: %s\n' % ( |
|
44 | 44 | ', '.join(['%r' % (m * 4) for m in level2mark[initlevel + 1 :]]) |
|
45 | 45 | ) |
|
46 | 46 | ui.warn(avail.encode('utf-8')) |
|
47 | 47 | |
|
48 | 48 | |
|
49 | 49 | def checkseclevel(ui, doc, name, initlevel): |
|
50 | ui.note('checking "%s"\n' % name) | |
|
50 | ui.notenoi18n('checking "%s"\n' % name) | |
|
51 | 51 | if not isinstance(doc, bytes): |
|
52 | 52 | doc = doc.encode('utf-8') |
|
53 | 53 | blocks, pruned = minirst.parse(doc, 0, ['verbose']) |
|
54 | 54 | errorcnt = 0 |
|
55 | 55 | curlevel = initlevel |
|
56 | 56 | for block in blocks: |
|
57 | 57 | if block[b'type'] != b'section': |
|
58 | 58 | continue |
|
59 | 59 | mark = block[b'underline'] |
|
60 | 60 | title = block[b'lines'][0] |
|
61 | 61 | if (mark not in mark2level) or (mark2level[mark] <= initlevel): |
|
62 | 62 | ui.warn( |
|
63 | 63 | ( |
|
64 | 64 | 'invalid section mark %r for "%s" of %s\n' |
|
65 | 65 | % (mark * 4, title, name) |
|
66 | 66 | ).encode('utf-8') |
|
67 | 67 | ) |
|
68 | 68 | showavailables(ui, initlevel) |
|
69 | 69 | errorcnt += 1 |
|
70 | 70 | continue |
|
71 | 71 | nextlevel = mark2level[mark] |
|
72 | 72 | if curlevel < nextlevel and curlevel + 1 != nextlevel: |
|
73 | ui.warn('gap of section level at "%s" of %s\n' % (title, name)) | |
|
73 | ui.warnnoi18n('gap of section level at "%s" of %s\n' % (title, name)) | |
|
74 | 74 | showavailables(ui, initlevel) |
|
75 | 75 | errorcnt += 1 |
|
76 | 76 | continue |
|
77 | ui.note( | |
|
77 | ui.notenoi18n( | |
|
78 | 78 | 'appropriate section level for "%s %s"\n' |
|
79 | 79 | % (mark * (nextlevel * 2), title) |
|
80 | 80 | ) |
|
81 | 81 | curlevel = nextlevel |
|
82 | 82 | |
|
83 | 83 | return errorcnt |
|
84 | 84 | |
|
85 | 85 | |
|
86 | 86 | def checkcmdtable(ui, cmdtable, namefmt, initlevel): |
|
87 | 87 | errorcnt = 0 |
|
88 | 88 | for k, entry in cmdtable.items(): |
|
89 | 89 | name = k.split(b"|")[0].lstrip(b"^") |
|
90 | 90 | if not entry[0].__doc__: |
|
91 | ui.note('skip checking %s: no help document\n' % (namefmt % name)) | |
|
91 | ui.notenoi18n('skip checking %s: no help document\n' % (namefmt % name)) | |
|
92 | 92 | continue |
|
93 | 93 | errorcnt += checkseclevel( |
|
94 | 94 | ui, entry[0].__doc__, namefmt % name, initlevel |
|
95 | 95 | ) |
|
96 | 96 | return errorcnt |
|
97 | 97 | |
|
98 | 98 | |
|
99 | 99 | def checkhghelps(ui): |
|
100 | 100 | errorcnt = 0 |
|
101 | 101 | for h in helptable: |
|
102 | 102 | names, sec, doc = h[0:3] |
|
103 | 103 | if callable(doc): |
|
104 | 104 | doc = doc(ui) |
|
105 | 105 | errorcnt += checkseclevel( |
|
106 | 106 | ui, doc, '%s help topic' % names[0], initlevel_topic |
|
107 | 107 | ) |
|
108 | 108 | |
|
109 | 109 | errorcnt += checkcmdtable(ui, table, '%s command', initlevel_cmd) |
|
110 | 110 | |
|
111 | 111 | for name in sorted( |
|
112 | 112 | list(extensions.enabled()) + list(extensions.disabled()) |
|
113 | 113 | ): |
|
114 | 114 | mod = extensions.load(ui, name, None) |
|
115 | 115 | if not mod.__doc__: |
|
116 | ui.note('skip checking %s extension: no help document\n' % name) | |
|
116 | ui.notenoi18n('skip checking %s extension: no help document\n' % name) | |
|
117 | 117 | continue |
|
118 | 118 | errorcnt += checkseclevel( |
|
119 | 119 | ui, mod.__doc__, '%s extension' % name, initlevel_ext |
|
120 | 120 | ) |
|
121 | 121 | |
|
122 | 122 | cmdtable = getattr(mod, 'cmdtable', None) |
|
123 | 123 | if cmdtable: |
|
124 | 124 | errorcnt += checkcmdtable( |
|
125 | 125 | ui, |
|
126 | 126 | cmdtable, |
|
127 | 127 | '%%s command of %s extension' % name, |
|
128 | 128 | initlevel_ext_cmd, |
|
129 | 129 | ) |
|
130 | 130 | return errorcnt |
|
131 | 131 | |
|
132 | 132 | |
|
133 | 133 | def checkfile(ui, filename, initlevel): |
|
134 | 134 | if filename == '-': |
|
135 | 135 | filename = 'stdin' |
|
136 | 136 | doc = sys.stdin.read() |
|
137 | 137 | else: |
|
138 | 138 | with open(filename) as fp: |
|
139 | 139 | doc = fp.read() |
|
140 | 140 | |
|
141 | ui.note( | |
|
141 | ui.notenoi18n( | |
|
142 | 142 | 'checking input from %s with initlevel %d\n' % (filename, initlevel) |
|
143 | 143 | ) |
|
144 | 144 | return checkseclevel(ui, doc, 'input from %s' % filename, initlevel) |
|
145 | 145 | |
|
146 | 146 | |
|
147 | 147 | def main(): |
|
148 | 148 | optparser = optparse.OptionParser( |
|
149 | 149 | """%prog [options] |
|
150 | 150 | |
|
151 | 151 | This checks all help documents of Mercurial (topics, commands, |
|
152 | 152 | extensions and commands of them), if no file is specified by --file |
|
153 | 153 | option. |
|
154 | 154 | """ |
|
155 | 155 | ) |
|
156 | 156 | optparser.add_option( |
|
157 | 157 | "-v", "--verbose", help="enable additional output", action="store_true" |
|
158 | 158 | ) |
|
159 | 159 | optparser.add_option( |
|
160 | 160 | "-d", "--debug", help="debug mode", action="store_true" |
|
161 | 161 | ) |
|
162 | 162 | optparser.add_option( |
|
163 | 163 | "-f", |
|
164 | 164 | "--file", |
|
165 | 165 | help="filename to read in (or '-' for stdin)", |
|
166 | 166 | action="store", |
|
167 | 167 | default="", |
|
168 | 168 | ) |
|
169 | 169 | |
|
170 | 170 | optparser.add_option( |
|
171 | 171 | "-t", |
|
172 | 172 | "--topic", |
|
173 | 173 | help="parse file as help topic", |
|
174 | 174 | action="store_const", |
|
175 | 175 | dest="initlevel", |
|
176 | 176 | const=0, |
|
177 | 177 | ) |
|
178 | 178 | optparser.add_option( |
|
179 | 179 | "-c", |
|
180 | 180 | "--command", |
|
181 | 181 | help="parse file as help of core command", |
|
182 | 182 | action="store_const", |
|
183 | 183 | dest="initlevel", |
|
184 | 184 | const=1, |
|
185 | 185 | ) |
|
186 | 186 | optparser.add_option( |
|
187 | 187 | "-e", |
|
188 | 188 | "--extension", |
|
189 | 189 | help="parse file as help of extension", |
|
190 | 190 | action="store_const", |
|
191 | 191 | dest="initlevel", |
|
192 | 192 | const=1, |
|
193 | 193 | ) |
|
194 | 194 | optparser.add_option( |
|
195 | 195 | "-C", |
|
196 | 196 | "--extension-command", |
|
197 | 197 | help="parse file as help of extension command", |
|
198 | 198 | action="store_const", |
|
199 | 199 | dest="initlevel", |
|
200 | 200 | const=3, |
|
201 | 201 | ) |
|
202 | 202 | |
|
203 | 203 | optparser.add_option( |
|
204 | 204 | "-l", |
|
205 | 205 | "--initlevel", |
|
206 | 206 | help="set initial section level manually", |
|
207 | 207 | action="store", |
|
208 | 208 | type="int", |
|
209 | 209 | default=0, |
|
210 | 210 | ) |
|
211 | 211 | |
|
212 | 212 | (options, args) = optparser.parse_args() |
|
213 | 213 | |
|
214 | 214 | ui = uimod.ui.load() |
|
215 | 215 | ui.setconfig(b'ui', b'verbose', options.verbose, b'--verbose') |
|
216 | 216 | ui.setconfig(b'ui', b'debug', options.debug, b'--debug') |
|
217 | 217 | |
|
218 | 218 | if options.file: |
|
219 | 219 | if checkfile(ui, options.file, options.initlevel): |
|
220 | 220 | sys.exit(1) |
|
221 | 221 | else: |
|
222 | 222 | if checkhghelps(ui): |
|
223 | 223 | sys.exit(1) |
|
224 | 224 | |
|
225 | 225 | |
|
226 | 226 | if __name__ == "__main__": |
|
227 | 227 | main() |
@@ -1,1073 +1,1073 b'' | |||
|
1 | 1 | # Mercurial built-in replacement for cvsps. |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2008, Frank Kingswood <frank@kingswood-consulting.co.uk> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | from __future__ import absolute_import |
|
8 | 8 | |
|
9 | 9 | import functools |
|
10 | 10 | import os |
|
11 | 11 | import re |
|
12 | 12 | |
|
13 | 13 | from mercurial.i18n import _ |
|
14 | 14 | from mercurial import ( |
|
15 | 15 | encoding, |
|
16 | 16 | error, |
|
17 | 17 | hook, |
|
18 | 18 | pycompat, |
|
19 | 19 | util, |
|
20 | 20 | ) |
|
21 | 21 | from mercurial.utils import ( |
|
22 | 22 | dateutil, |
|
23 | 23 | procutil, |
|
24 | 24 | stringutil, |
|
25 | 25 | ) |
|
26 | 26 | |
|
27 | 27 | pickle = util.pickle |
|
28 | 28 | |
|
29 | 29 | |
|
30 | 30 | class logentry(object): |
|
31 | 31 | '''Class logentry has the following attributes: |
|
32 | 32 | .author - author name as CVS knows it |
|
33 | 33 | .branch - name of branch this revision is on |
|
34 | 34 | .branches - revision tuple of branches starting at this revision |
|
35 | 35 | .comment - commit message |
|
36 | 36 | .commitid - CVS commitid or None |
|
37 | 37 | .date - the commit date as a (time, tz) tuple |
|
38 | 38 | .dead - true if file revision is dead |
|
39 | 39 | .file - Name of file |
|
40 | 40 | .lines - a tuple (+lines, -lines) or None |
|
41 | 41 | .parent - Previous revision of this entry |
|
42 | 42 | .rcs - name of file as returned from CVS |
|
43 | 43 | .revision - revision number as tuple |
|
44 | 44 | .tags - list of tags on the file |
|
45 | 45 | .synthetic - is this a synthetic "file ... added on ..." revision? |
|
46 | 46 | .mergepoint - the branch that has been merged from (if present in |
|
47 | 47 | rlog output) or None |
|
48 | 48 | .branchpoints - the branches that start at the current entry or empty |
|
49 | 49 | ''' |
|
50 | 50 | |
|
51 | 51 | def __init__(self, **entries): |
|
52 | 52 | self.synthetic = False |
|
53 | 53 | self.__dict__.update(entries) |
|
54 | 54 | |
|
55 | 55 | def __repr__(self): |
|
56 | 56 | items = ( |
|
57 | 57 | r"%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__) |
|
58 | 58 | ) |
|
59 | 59 | return r"%s(%s)" % (type(self).__name__, r", ".join(items)) |
|
60 | 60 | |
|
61 | 61 | |
|
62 | 62 | class logerror(Exception): |
|
63 | 63 | pass |
|
64 | 64 | |
|
65 | 65 | |
|
66 | 66 | def getrepopath(cvspath): |
|
67 | 67 | """Return the repository path from a CVS path. |
|
68 | 68 | |
|
69 | 69 | >>> getrepopath(b'/foo/bar') |
|
70 | 70 | '/foo/bar' |
|
71 | 71 | >>> getrepopath(b'c:/foo/bar') |
|
72 | 72 | '/foo/bar' |
|
73 | 73 | >>> getrepopath(b':pserver:10/foo/bar') |
|
74 | 74 | '/foo/bar' |
|
75 | 75 | >>> getrepopath(b':pserver:10c:/foo/bar') |
|
76 | 76 | '/foo/bar' |
|
77 | 77 | >>> getrepopath(b':pserver:/foo/bar') |
|
78 | 78 | '/foo/bar' |
|
79 | 79 | >>> getrepopath(b':pserver:c:/foo/bar') |
|
80 | 80 | '/foo/bar' |
|
81 | 81 | >>> getrepopath(b':pserver:truc@foo.bar:/foo/bar') |
|
82 | 82 | '/foo/bar' |
|
83 | 83 | >>> getrepopath(b':pserver:truc@foo.bar:c:/foo/bar') |
|
84 | 84 | '/foo/bar' |
|
85 | 85 | >>> getrepopath(b'user@server/path/to/repository') |
|
86 | 86 | '/path/to/repository' |
|
87 | 87 | """ |
|
88 | 88 | # According to CVS manual, CVS paths are expressed like: |
|
89 | 89 | # [:method:][[user][:password]@]hostname[:[port]]/path/to/repository |
|
90 | 90 | # |
|
91 | 91 | # CVSpath is splitted into parts and then position of the first occurrence |
|
92 | 92 | # of the '/' char after the '@' is located. The solution is the rest of the |
|
93 | 93 | # string after that '/' sign including it |
|
94 | 94 | |
|
95 | 95 | parts = cvspath.split(b':') |
|
96 | 96 | atposition = parts[-1].find(b'@') |
|
97 | 97 | start = 0 |
|
98 | 98 | |
|
99 | 99 | if atposition != -1: |
|
100 | 100 | start = atposition |
|
101 | 101 | |
|
102 | 102 | repopath = parts[-1][parts[-1].find(b'/', start) :] |
|
103 | 103 | return repopath |
|
104 | 104 | |
|
105 | 105 | |
|
106 | 106 | def createlog(ui, directory=None, root=b"", rlog=True, cache=None): |
|
107 | 107 | '''Collect the CVS rlog''' |
|
108 | 108 | |
|
109 | 109 | # Because we store many duplicate commit log messages, reusing strings |
|
110 | 110 | # saves a lot of memory and pickle storage space. |
|
111 | 111 | _scache = {} |
|
112 | 112 | |
|
113 | 113 | def scache(s): |
|
114 | 114 | b"return a shared version of a string" |
|
115 | 115 | return _scache.setdefault(s, s) |
|
116 | 116 | |
|
117 | 117 | ui.status(_(b'collecting CVS rlog\n')) |
|
118 | 118 | |
|
119 | 119 | log = [] # list of logentry objects containing the CVS state |
|
120 | 120 | |
|
121 | 121 | # patterns to match in CVS (r)log output, by state of use |
|
122 | 122 | re_00 = re.compile(b'RCS file: (.+)$') |
|
123 | 123 | re_01 = re.compile(b'cvs \\[r?log aborted\\]: (.+)$') |
|
124 | 124 | re_02 = re.compile(b'cvs (r?log|server): (.+)\n$') |
|
125 | 125 | re_03 = re.compile( |
|
126 | 126 | b"(Cannot access.+CVSROOT)|" b"(can't create temporary directory.+)$" |
|
127 | 127 | ) |
|
128 | 128 | re_10 = re.compile(b'Working file: (.+)$') |
|
129 | 129 | re_20 = re.compile(b'symbolic names:') |
|
130 | 130 | re_30 = re.compile(b'\t(.+): ([\\d.]+)$') |
|
131 | 131 | re_31 = re.compile(b'----------------------------$') |
|
132 | 132 | re_32 = re.compile( |
|
133 | 133 | b'=======================================' |
|
134 | 134 | b'======================================$' |
|
135 | 135 | ) |
|
136 | 136 | re_50 = re.compile(br'revision ([\d.]+)(\s+locked by:\s+.+;)?$') |
|
137 | 137 | re_60 = re.compile( |
|
138 | 138 | br'date:\s+(.+);\s+author:\s+(.+);\s+state:\s+(.+?);' |
|
139 | 139 | br'(\s+lines:\s+(\+\d+)?\s+(-\d+)?;)?' |
|
140 | 140 | br'(\s+commitid:\s+([^;]+);)?' |
|
141 | 141 | br'(.*mergepoint:\s+([^;]+);)?' |
|
142 | 142 | ) |
|
143 | 143 | re_70 = re.compile(b'branches: (.+);$') |
|
144 | 144 | |
|
145 | 145 | file_added_re = re.compile(br'file [^/]+ was (initially )?added on branch') |
|
146 | 146 | |
|
147 | 147 | prefix = b'' # leading path to strip of what we get from CVS |
|
148 | 148 | |
|
149 | 149 | if directory is None: |
|
150 | 150 | # Current working directory |
|
151 | 151 | |
|
152 | 152 | # Get the real directory in the repository |
|
153 | 153 | try: |
|
154 | 154 | with open(os.path.join(b'CVS', b'Repository'), b'rb') as f: |
|
155 | 155 | prefix = f.read().strip() |
|
156 | 156 | directory = prefix |
|
157 | 157 | if prefix == b".": |
|
158 | 158 | prefix = b"" |
|
159 | 159 | except IOError: |
|
160 | 160 | raise logerror(_(b'not a CVS sandbox')) |
|
161 | 161 | |
|
162 | 162 | if prefix and not prefix.endswith(pycompat.ossep): |
|
163 | 163 | prefix += pycompat.ossep |
|
164 | 164 | |
|
165 | 165 | # Use the Root file in the sandbox, if it exists |
|
166 | 166 | try: |
|
167 | 167 | root = open(os.path.join(b'CVS', b'Root'), b'rb').read().strip() |
|
168 | 168 | except IOError: |
|
169 | 169 | pass |
|
170 | 170 | |
|
171 | 171 | if not root: |
|
172 | 172 | root = encoding.environ.get(b'CVSROOT', b'') |
|
173 | 173 | |
|
174 | 174 | # read log cache if one exists |
|
175 | 175 | oldlog = [] |
|
176 | 176 | date = None |
|
177 | 177 | |
|
178 | 178 | if cache: |
|
179 | 179 | cachedir = os.path.expanduser(b'~/.hg.cvsps') |
|
180 | 180 | if not os.path.exists(cachedir): |
|
181 | 181 | os.mkdir(cachedir) |
|
182 | 182 | |
|
183 | 183 | # The cvsps cache pickle needs a uniquified name, based on the |
|
184 | 184 | # repository location. The address may have all sort of nasties |
|
185 | 185 | # in it, slashes, colons and such. So here we take just the |
|
186 | 186 | # alphanumeric characters, concatenated in a way that does not |
|
187 | 187 | # mix up the various components, so that |
|
188 | 188 | # :pserver:user@server:/path |
|
189 | 189 | # and |
|
190 | 190 | # /pserver/user/server/path |
|
191 | 191 | # are mapped to different cache file names. |
|
192 | 192 | cachefile = root.split(b":") + [directory, b"cache"] |
|
193 | 193 | cachefile = [b'-'.join(re.findall(br'\w+', s)) for s in cachefile if s] |
|
194 | 194 | cachefile = os.path.join( |
|
195 | 195 | cachedir, b'.'.join([s for s in cachefile if s]) |
|
196 | 196 | ) |
|
197 | 197 | |
|
198 | 198 | if cache == b'update': |
|
199 | 199 | try: |
|
200 | 200 | ui.note(_(b'reading cvs log cache %s\n') % cachefile) |
|
201 | 201 | oldlog = pickle.load(open(cachefile, b'rb')) |
|
202 | 202 | for e in oldlog: |
|
203 | 203 | if not ( |
|
204 | 204 | util.safehasattr(e, b'branchpoints') |
|
205 | 205 | and util.safehasattr(e, b'commitid') |
|
206 | 206 | and util.safehasattr(e, b'mergepoint') |
|
207 | 207 | ): |
|
208 | 208 | ui.status(_(b'ignoring old cache\n')) |
|
209 | 209 | oldlog = [] |
|
210 | 210 | break |
|
211 | 211 | |
|
212 | 212 | ui.note(_(b'cache has %d log entries\n') % len(oldlog)) |
|
213 | 213 | except Exception as e: |
|
214 | 214 | ui.note(_(b'error reading cache: %r\n') % e) |
|
215 | 215 | |
|
216 | 216 | if oldlog: |
|
217 | 217 | date = oldlog[-1].date # last commit date as a (time,tz) tuple |
|
218 | 218 | date = dateutil.datestr(date, b'%Y/%m/%d %H:%M:%S %1%2') |
|
219 | 219 | |
|
220 | 220 | # build the CVS commandline |
|
221 | 221 | cmd = [b'cvs', b'-q'] |
|
222 | 222 | if root: |
|
223 | 223 | cmd.append(b'-d%s' % root) |
|
224 | 224 | p = util.normpath(getrepopath(root)) |
|
225 | 225 | if not p.endswith(b'/'): |
|
226 | 226 | p += b'/' |
|
227 | 227 | if prefix: |
|
228 | 228 | # looks like normpath replaces "" by "." |
|
229 | 229 | prefix = p + util.normpath(prefix) |
|
230 | 230 | else: |
|
231 | 231 | prefix = p |
|
232 | 232 | cmd.append([b'log', b'rlog'][rlog]) |
|
233 | 233 | if date: |
|
234 | 234 | # no space between option and date string |
|
235 | 235 | cmd.append(b'-d>%s' % date) |
|
236 | 236 | cmd.append(directory) |
|
237 | 237 | |
|
238 | 238 | # state machine begins here |
|
239 | 239 | tags = {} # dictionary of revisions on current file with their tags |
|
240 | 240 | branchmap = {} # mapping between branch names and revision numbers |
|
241 | 241 | rcsmap = {} |
|
242 | 242 | state = 0 |
|
243 | 243 | store = False # set when a new record can be appended |
|
244 | 244 | |
|
245 | 245 | cmd = [procutil.shellquote(arg) for arg in cmd] |
|
246 | 246 | ui.note(_(b"running %s\n") % (b' '.join(cmd))) |
|
247 | 247 | ui.debug(b"prefix=%r directory=%r root=%r\n" % (prefix, directory, root)) |
|
248 | 248 | |
|
249 | 249 | pfp = procutil.popen(b' '.join(cmd), b'rb') |
|
250 | 250 | peek = util.fromnativeeol(pfp.readline()) |
|
251 | 251 | while True: |
|
252 | 252 | line = peek |
|
253 | 253 | if line == b'': |
|
254 | 254 | break |
|
255 | 255 | peek = util.fromnativeeol(pfp.readline()) |
|
256 | 256 | if line.endswith(b'\n'): |
|
257 | 257 | line = line[:-1] |
|
258 | 258 | # ui.debug('state=%d line=%r\n' % (state, line)) |
|
259 | 259 | |
|
260 | 260 | if state == 0: |
|
261 | 261 | # initial state, consume input until we see 'RCS file' |
|
262 | 262 | match = re_00.match(line) |
|
263 | 263 | if match: |
|
264 | 264 | rcs = match.group(1) |
|
265 | 265 | tags = {} |
|
266 | 266 | if rlog: |
|
267 | 267 | filename = util.normpath(rcs[:-2]) |
|
268 | 268 | if filename.startswith(prefix): |
|
269 | 269 | filename = filename[len(prefix) :] |
|
270 | 270 | if filename.startswith(b'/'): |
|
271 | 271 | filename = filename[1:] |
|
272 | 272 | if filename.startswith(b'Attic/'): |
|
273 | 273 | filename = filename[6:] |
|
274 | 274 | else: |
|
275 | 275 | filename = filename.replace(b'/Attic/', b'/') |
|
276 | 276 | state = 2 |
|
277 | 277 | continue |
|
278 | 278 | state = 1 |
|
279 | 279 | continue |
|
280 | 280 | match = re_01.match(line) |
|
281 | 281 | if match: |
|
282 | 282 | raise logerror(match.group(1)) |
|
283 | 283 | match = re_02.match(line) |
|
284 | 284 | if match: |
|
285 | 285 | raise logerror(match.group(2)) |
|
286 | 286 | if re_03.match(line): |
|
287 | 287 | raise logerror(line) |
|
288 | 288 | |
|
289 | 289 | elif state == 1: |
|
290 | 290 | # expect 'Working file' (only when using log instead of rlog) |
|
291 | 291 | match = re_10.match(line) |
|
292 | 292 | assert match, _(b'RCS file must be followed by working file') |
|
293 | 293 | filename = util.normpath(match.group(1)) |
|
294 | 294 | state = 2 |
|
295 | 295 | |
|
296 | 296 | elif state == 2: |
|
297 | 297 | # expect 'symbolic names' |
|
298 | 298 | if re_20.match(line): |
|
299 | 299 | branchmap = {} |
|
300 | 300 | state = 3 |
|
301 | 301 | |
|
302 | 302 | elif state == 3: |
|
303 | 303 | # read the symbolic names and store as tags |
|
304 | 304 | match = re_30.match(line) |
|
305 | 305 | if match: |
|
306 | 306 | rev = [int(x) for x in match.group(2).split(b'.')] |
|
307 | 307 | |
|
308 | 308 | # Convert magic branch number to an odd-numbered one |
|
309 | 309 | revn = len(rev) |
|
310 | 310 | if revn > 3 and (revn % 2) == 0 and rev[-2] == 0: |
|
311 | 311 | rev = rev[:-2] + rev[-1:] |
|
312 | 312 | rev = tuple(rev) |
|
313 | 313 | |
|
314 | 314 | if rev not in tags: |
|
315 | 315 | tags[rev] = [] |
|
316 | 316 | tags[rev].append(match.group(1)) |
|
317 | 317 | branchmap[match.group(1)] = match.group(2) |
|
318 | 318 | |
|
319 | 319 | elif re_31.match(line): |
|
320 | 320 | state = 5 |
|
321 | 321 | elif re_32.match(line): |
|
322 | 322 | state = 0 |
|
323 | 323 | |
|
324 | 324 | elif state == 4: |
|
325 | 325 | # expecting '------' separator before first revision |
|
326 | 326 | if re_31.match(line): |
|
327 | 327 | state = 5 |
|
328 | 328 | else: |
|
329 | 329 | assert not re_32.match(line), _( |
|
330 | 330 | b'must have at least ' b'some revisions' |
|
331 | 331 | ) |
|
332 | 332 | |
|
333 | 333 | elif state == 5: |
|
334 | 334 | # expecting revision number and possibly (ignored) lock indication |
|
335 | 335 | # we create the logentry here from values stored in states 0 to 4, |
|
336 | 336 | # as this state is re-entered for subsequent revisions of a file. |
|
337 | 337 | match = re_50.match(line) |
|
338 | 338 | assert match, _(b'expected revision number') |
|
339 | 339 | e = logentry( |
|
340 | 340 | rcs=scache(rcs), |
|
341 | 341 | file=scache(filename), |
|
342 | 342 | revision=tuple([int(x) for x in match.group(1).split(b'.')]), |
|
343 | 343 | branches=[], |
|
344 | 344 | parent=None, |
|
345 | 345 | commitid=None, |
|
346 | 346 | mergepoint=None, |
|
347 | 347 | branchpoints=set(), |
|
348 | 348 | ) |
|
349 | 349 | |
|
350 | 350 | state = 6 |
|
351 | 351 | |
|
352 | 352 | elif state == 6: |
|
353 | 353 | # expecting date, author, state, lines changed |
|
354 | 354 | match = re_60.match(line) |
|
355 | 355 | assert match, _(b'revision must be followed by date line') |
|
356 | 356 | d = match.group(1) |
|
357 | 357 | if d[2] == b'/': |
|
358 | 358 | # Y2K |
|
359 | 359 | d = b'19' + d |
|
360 | 360 | |
|
361 | 361 | if len(d.split()) != 3: |
|
362 | 362 | # cvs log dates always in GMT |
|
363 | 363 | d = d + b' UTC' |
|
364 | 364 | e.date = dateutil.parsedate( |
|
365 | 365 | d, |
|
366 | 366 | [ |
|
367 | 367 | b'%y/%m/%d %H:%M:%S', |
|
368 | 368 | b'%Y/%m/%d %H:%M:%S', |
|
369 | 369 | b'%Y-%m-%d %H:%M:%S', |
|
370 | 370 | ], |
|
371 | 371 | ) |
|
372 | 372 | e.author = scache(match.group(2)) |
|
373 | 373 | e.dead = match.group(3).lower() == b'dead' |
|
374 | 374 | |
|
375 | 375 | if match.group(5): |
|
376 | 376 | if match.group(6): |
|
377 | 377 | e.lines = (int(match.group(5)), int(match.group(6))) |
|
378 | 378 | else: |
|
379 | 379 | e.lines = (int(match.group(5)), 0) |
|
380 | 380 | elif match.group(6): |
|
381 | 381 | e.lines = (0, int(match.group(6))) |
|
382 | 382 | else: |
|
383 | 383 | e.lines = None |
|
384 | 384 | |
|
385 | 385 | if match.group(7): # cvs 1.12 commitid |
|
386 | 386 | e.commitid = match.group(8) |
|
387 | 387 | |
|
388 | 388 | if match.group(9): # cvsnt mergepoint |
|
389 | 389 | myrev = match.group(10).split(b'.') |
|
390 | 390 | if len(myrev) == 2: # head |
|
391 | 391 | e.mergepoint = b'HEAD' |
|
392 | 392 | else: |
|
393 | 393 | myrev = b'.'.join(myrev[:-2] + [b'0', myrev[-2]]) |
|
394 | 394 | branches = [b for b in branchmap if branchmap[b] == myrev] |
|
395 | 395 | assert len(branches) == 1, ( |
|
396 | 396 | b'unknown branch: %s' % e.mergepoint |
|
397 | 397 | ) |
|
398 | 398 | e.mergepoint = branches[0] |
|
399 | 399 | |
|
400 | 400 | e.comment = [] |
|
401 | 401 | state = 7 |
|
402 | 402 | |
|
403 | 403 | elif state == 7: |
|
404 | 404 | # read the revision numbers of branches that start at this revision |
|
405 | 405 | # or store the commit log message otherwise |
|
406 | 406 | m = re_70.match(line) |
|
407 | 407 | if m: |
|
408 | 408 | e.branches = [ |
|
409 | 409 | tuple([int(y) for y in x.strip().split(b'.')]) |
|
410 | 410 | for x in m.group(1).split(b';') |
|
411 | 411 | ] |
|
412 | 412 | state = 8 |
|
413 | 413 | elif re_31.match(line) and re_50.match(peek): |
|
414 | 414 | state = 5 |
|
415 | 415 | store = True |
|
416 | 416 | elif re_32.match(line): |
|
417 | 417 | state = 0 |
|
418 | 418 | store = True |
|
419 | 419 | else: |
|
420 | 420 | e.comment.append(line) |
|
421 | 421 | |
|
422 | 422 | elif state == 8: |
|
423 | 423 | # store commit log message |
|
424 | 424 | if re_31.match(line): |
|
425 | 425 | cpeek = peek |
|
426 | 426 | if cpeek.endswith(b'\n'): |
|
427 | 427 | cpeek = cpeek[:-1] |
|
428 | 428 | if re_50.match(cpeek): |
|
429 | 429 | state = 5 |
|
430 | 430 | store = True |
|
431 | 431 | else: |
|
432 | 432 | e.comment.append(line) |
|
433 | 433 | elif re_32.match(line): |
|
434 | 434 | state = 0 |
|
435 | 435 | store = True |
|
436 | 436 | else: |
|
437 | 437 | e.comment.append(line) |
|
438 | 438 | |
|
439 | 439 | # When a file is added on a branch B1, CVS creates a synthetic |
|
440 | 440 | # dead trunk revision 1.1 so that the branch has a root. |
|
441 | 441 | # Likewise, if you merge such a file to a later branch B2 (one |
|
442 | 442 | # that already existed when the file was added on B1), CVS |
|
443 | 443 | # creates a synthetic dead revision 1.1.x.1 on B2. Don't drop |
|
444 | 444 | # these revisions now, but mark them synthetic so |
|
445 | 445 | # createchangeset() can take care of them. |
|
446 | 446 | if ( |
|
447 | 447 | store |
|
448 | 448 | and e.dead |
|
449 | 449 | and e.revision[-1] == 1 |
|
450 | 450 | and len(e.comment) == 1 # 1.1 or 1.1.x.1 |
|
451 | 451 | and file_added_re.match(e.comment[0]) |
|
452 | 452 | ): |
|
453 | 453 | ui.debug( |
|
454 | 454 | b'found synthetic revision in %s: %r\n' % (e.rcs, e.comment[0]) |
|
455 | 455 | ) |
|
456 | 456 | e.synthetic = True |
|
457 | 457 | |
|
458 | 458 | if store: |
|
459 | 459 | # clean up the results and save in the log. |
|
460 | 460 | store = False |
|
461 | 461 | e.tags = sorted([scache(x) for x in tags.get(e.revision, [])]) |
|
462 | 462 | e.comment = scache(b'\n'.join(e.comment)) |
|
463 | 463 | |
|
464 | 464 | revn = len(e.revision) |
|
465 | 465 | if revn > 3 and (revn % 2) == 0: |
|
466 | 466 | e.branch = tags.get(e.revision[:-1], [None])[0] |
|
467 | 467 | else: |
|
468 | 468 | e.branch = None |
|
469 | 469 | |
|
470 | 470 | # find the branches starting from this revision |
|
471 | 471 | branchpoints = set() |
|
472 | 472 | for branch, revision in branchmap.iteritems(): |
|
473 | 473 | revparts = tuple([int(i) for i in revision.split(b'.')]) |
|
474 | 474 | if len(revparts) < 2: # bad tags |
|
475 | 475 | continue |
|
476 | 476 | if revparts[-2] == 0 and revparts[-1] % 2 == 0: |
|
477 | 477 | # normal branch |
|
478 | 478 | if revparts[:-2] == e.revision: |
|
479 | 479 | branchpoints.add(branch) |
|
480 | 480 | elif revparts == (1, 1, 1): # vendor branch |
|
481 | 481 | if revparts in e.branches: |
|
482 | 482 | branchpoints.add(branch) |
|
483 | 483 | e.branchpoints = branchpoints |
|
484 | 484 | |
|
485 | 485 | log.append(e) |
|
486 | 486 | |
|
487 | 487 | rcsmap[e.rcs.replace(b'/Attic/', b'/')] = e.rcs |
|
488 | 488 | |
|
489 | 489 | if len(log) % 100 == 0: |
|
490 | 490 | ui.status( |
|
491 | 491 | stringutil.ellipsis(b'%d %s' % (len(log), e.file), 80) |
|
492 | 492 | + b'\n' |
|
493 | 493 | ) |
|
494 | 494 | |
|
495 | 495 | log.sort(key=lambda x: (x.rcs, x.revision)) |
|
496 | 496 | |
|
497 | 497 | # find parent revisions of individual files |
|
498 | 498 | versions = {} |
|
499 | 499 | for e in sorted(oldlog, key=lambda x: (x.rcs, x.revision)): |
|
500 | 500 | rcs = e.rcs.replace(b'/Attic/', b'/') |
|
501 | 501 | if rcs in rcsmap: |
|
502 | 502 | e.rcs = rcsmap[rcs] |
|
503 | 503 | branch = e.revision[:-1] |
|
504 | 504 | versions[(e.rcs, branch)] = e.revision |
|
505 | 505 | |
|
506 | 506 | for e in log: |
|
507 | 507 | branch = e.revision[:-1] |
|
508 | 508 | p = versions.get((e.rcs, branch), None) |
|
509 | 509 | if p is None: |
|
510 | 510 | p = e.revision[:-2] |
|
511 | 511 | e.parent = p |
|
512 | 512 | versions[(e.rcs, branch)] = e.revision |
|
513 | 513 | |
|
514 | 514 | # update the log cache |
|
515 | 515 | if cache: |
|
516 | 516 | if log: |
|
517 | 517 | # join up the old and new logs |
|
518 | 518 | log.sort(key=lambda x: x.date) |
|
519 | 519 | |
|
520 | 520 | if oldlog and oldlog[-1].date >= log[0].date: |
|
521 | 521 | raise logerror( |
|
522 | 522 | _( |
|
523 | 523 | b'log cache overlaps with new log entries,' |
|
524 | 524 | b' re-run without cache.' |
|
525 | 525 | ) |
|
526 | 526 | ) |
|
527 | 527 | |
|
528 | 528 | log = oldlog + log |
|
529 | 529 | |
|
530 | 530 | # write the new cachefile |
|
531 | 531 | ui.note(_(b'writing cvs log cache %s\n') % cachefile) |
|
532 | 532 | pickle.dump(log, open(cachefile, b'wb')) |
|
533 | 533 | else: |
|
534 | 534 | log = oldlog |
|
535 | 535 | |
|
536 | 536 | ui.status(_(b'%d log entries\n') % len(log)) |
|
537 | 537 | |
|
538 | 538 | encodings = ui.configlist(b'convert', b'cvsps.logencoding') |
|
539 | 539 | if encodings: |
|
540 | 540 | |
|
541 | 541 | def revstr(r): |
|
542 | 542 | # this is needed, because logentry.revision is a tuple of "int" |
|
543 | 543 | # (e.g. (1, 2) for "1.2") |
|
544 | 544 | return b'.'.join(pycompat.maplist(pycompat.bytestr, r)) |
|
545 | 545 | |
|
546 | 546 | for entry in log: |
|
547 | 547 | comment = entry.comment |
|
548 | 548 | for e in encodings: |
|
549 | 549 | try: |
|
550 | 550 | entry.comment = comment.decode(pycompat.sysstr(e)).encode( |
|
551 | 551 | 'utf-8' |
|
552 | 552 | ) |
|
553 | 553 | if ui.debugflag: |
|
554 | 554 | ui.debug( |
|
555 | 555 | b"transcoding by %s: %s of %s\n" |
|
556 | 556 | % (e, revstr(entry.revision), entry.file) |
|
557 | 557 | ) |
|
558 | 558 | break |
|
559 | 559 | except UnicodeDecodeError: |
|
560 | 560 | pass # try next encoding |
|
561 | 561 | except LookupError as inst: # unknown encoding, maybe |
|
562 | 562 | raise error.Abort( |
|
563 | 563 | inst, |
|
564 | 564 | hint=_( |
|
565 | 565 | b'check convert.cvsps.logencoding' b' configuration' |
|
566 | 566 | ), |
|
567 | 567 | ) |
|
568 | 568 | else: |
|
569 | 569 | raise error.Abort( |
|
570 | 570 | _( |
|
571 | 571 | b"no encoding can transcode" |
|
572 | 572 | b" CVS log message for %s of %s" |
|
573 | 573 | ) |
|
574 | 574 | % (revstr(entry.revision), entry.file), |
|
575 | 575 | hint=_( |
|
576 | 576 | b'check convert.cvsps.logencoding' b' configuration' |
|
577 | 577 | ), |
|
578 | 578 | ) |
|
579 | 579 | |
|
580 | 580 | hook.hook(ui, None, b"cvslog", True, log=log) |
|
581 | 581 | |
|
582 | 582 | return log |
|
583 | 583 | |
|
584 | 584 | |
|
585 | 585 | class changeset(object): |
|
586 | 586 | '''Class changeset has the following attributes: |
|
587 | 587 | .id - integer identifying this changeset (list index) |
|
588 | 588 | .author - author name as CVS knows it |
|
589 | 589 | .branch - name of branch this changeset is on, or None |
|
590 | 590 | .comment - commit message |
|
591 | 591 | .commitid - CVS commitid or None |
|
592 | 592 | .date - the commit date as a (time,tz) tuple |
|
593 | 593 | .entries - list of logentry objects in this changeset |
|
594 | 594 | .parents - list of one or two parent changesets |
|
595 | 595 | .tags - list of tags on this changeset |
|
596 | 596 | .synthetic - from synthetic revision "file ... added on branch ..." |
|
597 | 597 | .mergepoint- the branch that has been merged from or None |
|
598 | 598 | .branchpoints- the branches that start at the current entry or empty |
|
599 | 599 | ''' |
|
600 | 600 | |
|
601 | 601 | def __init__(self, **entries): |
|
602 | 602 | self.id = None |
|
603 | 603 | self.synthetic = False |
|
604 | 604 | self.__dict__.update(entries) |
|
605 | 605 | |
|
606 | 606 | def __repr__(self): |
|
607 | 607 | items = ( |
|
608 | 608 | b"%s=%r" % (k, self.__dict__[k]) for k in sorted(self.__dict__) |
|
609 | 609 | ) |
|
610 | 610 | return b"%s(%s)" % (type(self).__name__, b", ".join(items)) |
|
611 | 611 | |
|
612 | 612 | |
|
613 | 613 | def createchangeset(ui, log, fuzz=60, mergefrom=None, mergeto=None): |
|
614 | 614 | '''Convert log into changesets.''' |
|
615 | 615 | |
|
616 | 616 | ui.status(_(b'creating changesets\n')) |
|
617 | 617 | |
|
618 | 618 | # try to order commitids by date |
|
619 | 619 | mindate = {} |
|
620 | 620 | for e in log: |
|
621 | 621 | if e.commitid: |
|
622 | 622 | if e.commitid not in mindate: |
|
623 | 623 | mindate[e.commitid] = e.date |
|
624 | 624 | else: |
|
625 | 625 | mindate[e.commitid] = min(e.date, mindate[e.commitid]) |
|
626 | 626 | |
|
627 | 627 | # Merge changesets |
|
628 | 628 | log.sort( |
|
629 | 629 | key=lambda x: ( |
|
630 | 630 | mindate.get(x.commitid, (-1, 0)), |
|
631 | 631 | x.commitid or b'', |
|
632 | 632 | x.comment, |
|
633 | 633 | x.author, |
|
634 | 634 | x.branch or b'', |
|
635 | 635 | x.date, |
|
636 | 636 | x.branchpoints, |
|
637 | 637 | ) |
|
638 | 638 | ) |
|
639 | 639 | |
|
640 | 640 | changesets = [] |
|
641 | 641 | files = set() |
|
642 | 642 | c = None |
|
643 | 643 | for i, e in enumerate(log): |
|
644 | 644 | |
|
645 | 645 | # Check if log entry belongs to the current changeset or not. |
|
646 | 646 | |
|
647 | 647 | # Since CVS is file-centric, two different file revisions with |
|
648 | 648 | # different branchpoints should be treated as belonging to two |
|
649 | 649 | # different changesets (and the ordering is important and not |
|
650 | 650 | # honoured by cvsps at this point). |
|
651 | 651 | # |
|
652 | 652 | # Consider the following case: |
|
653 | 653 | # foo 1.1 branchpoints: [MYBRANCH] |
|
654 | 654 | # bar 1.1 branchpoints: [MYBRANCH, MYBRANCH2] |
|
655 | 655 | # |
|
656 | 656 | # Here foo is part only of MYBRANCH, but not MYBRANCH2, e.g. a |
|
657 | 657 | # later version of foo may be in MYBRANCH2, so foo should be the |
|
658 | 658 | # first changeset and bar the next and MYBRANCH and MYBRANCH2 |
|
659 | 659 | # should both start off of the bar changeset. No provisions are |
|
660 | 660 | # made to ensure that this is, in fact, what happens. |
|
661 | 661 | if not ( |
|
662 | 662 | c |
|
663 | 663 | and e.branchpoints == c.branchpoints |
|
664 | 664 | and ( # cvs commitids |
|
665 | 665 | (e.commitid is not None and e.commitid == c.commitid) |
|
666 | 666 | or ( # no commitids, use fuzzy commit detection |
|
667 | 667 | (e.commitid is None or c.commitid is None) |
|
668 | 668 | and e.comment == c.comment |
|
669 | 669 | and e.author == c.author |
|
670 | 670 | and e.branch == c.branch |
|
671 | 671 | and ( |
|
672 | 672 | (c.date[0] + c.date[1]) |
|
673 | 673 | <= (e.date[0] + e.date[1]) |
|
674 | 674 | <= (c.date[0] + c.date[1]) + fuzz |
|
675 | 675 | ) |
|
676 | 676 | and e.file not in files |
|
677 | 677 | ) |
|
678 | 678 | ) |
|
679 | 679 | ): |
|
680 | 680 | c = changeset( |
|
681 | 681 | comment=e.comment, |
|
682 | 682 | author=e.author, |
|
683 | 683 | branch=e.branch, |
|
684 | 684 | date=e.date, |
|
685 | 685 | entries=[], |
|
686 | 686 | mergepoint=e.mergepoint, |
|
687 | 687 | branchpoints=e.branchpoints, |
|
688 | 688 | commitid=e.commitid, |
|
689 | 689 | ) |
|
690 | 690 | changesets.append(c) |
|
691 | 691 | |
|
692 | 692 | files = set() |
|
693 | 693 | if len(changesets) % 100 == 0: |
|
694 | 694 | t = b'%d %s' % (len(changesets), repr(e.comment)[1:-1]) |
|
695 | 695 | ui.status(stringutil.ellipsis(t, 80) + b'\n') |
|
696 | 696 | |
|
697 | 697 | c.entries.append(e) |
|
698 | 698 | files.add(e.file) |
|
699 | 699 | c.date = e.date # changeset date is date of latest commit in it |
|
700 | 700 | |
|
701 | 701 | # Mark synthetic changesets |
|
702 | 702 | |
|
703 | 703 | for c in changesets: |
|
704 | 704 | # Synthetic revisions always get their own changeset, because |
|
705 | 705 | # the log message includes the filename. E.g. if you add file3 |
|
706 | 706 | # and file4 on a branch, you get four log entries and three |
|
707 | 707 | # changesets: |
|
708 | 708 | # "File file3 was added on branch ..." (synthetic, 1 entry) |
|
709 | 709 | # "File file4 was added on branch ..." (synthetic, 1 entry) |
|
710 | 710 | # "Add file3 and file4 to fix ..." (real, 2 entries) |
|
711 | 711 | # Hence the check for 1 entry here. |
|
712 | 712 | c.synthetic = len(c.entries) == 1 and c.entries[0].synthetic |
|
713 | 713 | |
|
714 | 714 | # Sort files in each changeset |
|
715 | 715 | |
|
716 | 716 | def entitycompare(l, r): |
|
717 | 717 | b'Mimic cvsps sorting order' |
|
718 | 718 | l = l.file.split(b'/') |
|
719 | 719 | r = r.file.split(b'/') |
|
720 | 720 | nl = len(l) |
|
721 | 721 | nr = len(r) |
|
722 | 722 | n = min(nl, nr) |
|
723 | 723 | for i in range(n): |
|
724 | 724 | if i + 1 == nl and nl < nr: |
|
725 | 725 | return -1 |
|
726 | 726 | elif i + 1 == nr and nl > nr: |
|
727 | 727 | return +1 |
|
728 | 728 | elif l[i] < r[i]: |
|
729 | 729 | return -1 |
|
730 | 730 | elif l[i] > r[i]: |
|
731 | 731 | return +1 |
|
732 | 732 | return 0 |
|
733 | 733 | |
|
734 | 734 | for c in changesets: |
|
735 | 735 | c.entries.sort(key=functools.cmp_to_key(entitycompare)) |
|
736 | 736 | |
|
737 | 737 | # Sort changesets by date |
|
738 | 738 | |
|
739 | 739 | odd = set() |
|
740 | 740 | |
|
741 | 741 | def cscmp(l, r): |
|
742 | 742 | d = sum(l.date) - sum(r.date) |
|
743 | 743 | if d: |
|
744 | 744 | return d |
|
745 | 745 | |
|
746 | 746 | # detect vendor branches and initial commits on a branch |
|
747 | 747 | le = {} |
|
748 | 748 | for e in l.entries: |
|
749 | 749 | le[e.rcs] = e.revision |
|
750 | 750 | re = {} |
|
751 | 751 | for e in r.entries: |
|
752 | 752 | re[e.rcs] = e.revision |
|
753 | 753 | |
|
754 | 754 | d = 0 |
|
755 | 755 | for e in l.entries: |
|
756 | 756 | if re.get(e.rcs, None) == e.parent: |
|
757 | 757 | assert not d |
|
758 | 758 | d = 1 |
|
759 | 759 | break |
|
760 | 760 | |
|
761 | 761 | for e in r.entries: |
|
762 | 762 | if le.get(e.rcs, None) == e.parent: |
|
763 | 763 | if d: |
|
764 | 764 | odd.add((l, r)) |
|
765 | 765 | d = -1 |
|
766 | 766 | break |
|
767 | 767 | # By this point, the changesets are sufficiently compared that |
|
768 | 768 | # we don't really care about ordering. However, this leaves |
|
769 | 769 | # some race conditions in the tests, so we compare on the |
|
770 | 770 | # number of files modified, the files contained in each |
|
771 | 771 | # changeset, and the branchpoints in the change to ensure test |
|
772 | 772 | # output remains stable. |
|
773 | 773 | |
|
774 | 774 | # recommended replacement for cmp from |
|
775 | 775 | # https://docs.python.org/3.0/whatsnew/3.0.html |
|
776 | 776 | c = lambda x, y: (x > y) - (x < y) |
|
777 | 777 | # Sort bigger changes first. |
|
778 | 778 | if not d: |
|
779 | 779 | d = c(len(l.entries), len(r.entries)) |
|
780 | 780 | # Try sorting by filename in the change. |
|
781 | 781 | if not d: |
|
782 | 782 | d = c([e.file for e in l.entries], [e.file for e in r.entries]) |
|
783 | 783 | # Try and put changes without a branch point before ones with |
|
784 | 784 | # a branch point. |
|
785 | 785 | if not d: |
|
786 | 786 | d = c(len(l.branchpoints), len(r.branchpoints)) |
|
787 | 787 | return d |
|
788 | 788 | |
|
789 | 789 | changesets.sort(key=functools.cmp_to_key(cscmp)) |
|
790 | 790 | |
|
791 | 791 | # Collect tags |
|
792 | 792 | |
|
793 | 793 | globaltags = {} |
|
794 | 794 | for c in changesets: |
|
795 | 795 | for e in c.entries: |
|
796 | 796 | for tag in e.tags: |
|
797 | 797 | # remember which is the latest changeset to have this tag |
|
798 | 798 | globaltags[tag] = c |
|
799 | 799 | |
|
800 | 800 | for c in changesets: |
|
801 | 801 | tags = set() |
|
802 | 802 | for e in c.entries: |
|
803 | 803 | tags.update(e.tags) |
|
804 | 804 | # remember tags only if this is the latest changeset to have it |
|
805 | 805 | c.tags = sorted(tag for tag in tags if globaltags[tag] is c) |
|
806 | 806 | |
|
807 | 807 | # Find parent changesets, handle {{mergetobranch BRANCHNAME}} |
|
808 | 808 | # by inserting dummy changesets with two parents, and handle |
|
809 | 809 | # {{mergefrombranch BRANCHNAME}} by setting two parents. |
|
810 | 810 | |
|
811 | 811 | if mergeto is None: |
|
812 | 812 | mergeto = br'{{mergetobranch ([-\w]+)}}' |
|
813 | 813 | if mergeto: |
|
814 | 814 | mergeto = re.compile(mergeto) |
|
815 | 815 | |
|
816 | 816 | if mergefrom is None: |
|
817 | 817 | mergefrom = br'{{mergefrombranch ([-\w]+)}}' |
|
818 | 818 | if mergefrom: |
|
819 | 819 | mergefrom = re.compile(mergefrom) |
|
820 | 820 | |
|
821 | 821 | versions = {} # changeset index where we saw any particular file version |
|
822 | 822 | branches = {} # changeset index where we saw a branch |
|
823 | 823 | n = len(changesets) |
|
824 | 824 | i = 0 |
|
825 | 825 | while i < n: |
|
826 | 826 | c = changesets[i] |
|
827 | 827 | |
|
828 | 828 | for f in c.entries: |
|
829 | 829 | versions[(f.rcs, f.revision)] = i |
|
830 | 830 | |
|
831 | 831 | p = None |
|
832 | 832 | if c.branch in branches: |
|
833 | 833 | p = branches[c.branch] |
|
834 | 834 | else: |
|
835 | 835 | # first changeset on a new branch |
|
836 | 836 | # the parent is a changeset with the branch in its |
|
837 | 837 | # branchpoints such that it is the latest possible |
|
838 | 838 | # commit without any intervening, unrelated commits. |
|
839 | 839 | |
|
840 | 840 | for candidate in pycompat.xrange(i): |
|
841 | 841 | if c.branch not in changesets[candidate].branchpoints: |
|
842 | 842 | if p is not None: |
|
843 | 843 | break |
|
844 | 844 | continue |
|
845 | 845 | p = candidate |
|
846 | 846 | |
|
847 | 847 | c.parents = [] |
|
848 | 848 | if p is not None: |
|
849 | 849 | p = changesets[p] |
|
850 | 850 | |
|
851 | 851 | # Ensure no changeset has a synthetic changeset as a parent. |
|
852 | 852 | while p.synthetic: |
|
853 | 853 | assert len(p.parents) <= 1, _( |
|
854 | 854 | b'synthetic changeset cannot have multiple parents' |
|
855 | 855 | ) |
|
856 | 856 | if p.parents: |
|
857 | 857 | p = p.parents[0] |
|
858 | 858 | else: |
|
859 | 859 | p = None |
|
860 | 860 | break |
|
861 | 861 | |
|
862 | 862 | if p is not None: |
|
863 | 863 | c.parents.append(p) |
|
864 | 864 | |
|
865 | 865 | if c.mergepoint: |
|
866 | 866 | if c.mergepoint == b'HEAD': |
|
867 | 867 | c.mergepoint = None |
|
868 | 868 | c.parents.append(changesets[branches[c.mergepoint]]) |
|
869 | 869 | |
|
870 | 870 | if mergefrom: |
|
871 | 871 | m = mergefrom.search(c.comment) |
|
872 | 872 | if m: |
|
873 | 873 | m = m.group(1) |
|
874 | 874 | if m == b'HEAD': |
|
875 | 875 | m = None |
|
876 | 876 | try: |
|
877 | 877 | candidate = changesets[branches[m]] |
|
878 | 878 | except KeyError: |
|
879 | 879 | ui.warn( |
|
880 | 880 | _( |
|
881 | 881 | b"warning: CVS commit message references " |
|
882 | 882 | b"non-existent branch %r:\n%s\n" |
|
883 | 883 | ) |
|
884 | 884 | % (pycompat.bytestr(m), c.comment) |
|
885 | 885 | ) |
|
886 | 886 | if m in branches and c.branch != m and not candidate.synthetic: |
|
887 | 887 | c.parents.append(candidate) |
|
888 | 888 | |
|
889 | 889 | if mergeto: |
|
890 | 890 | m = mergeto.search(c.comment) |
|
891 | 891 | if m: |
|
892 | 892 | if m.groups(): |
|
893 | 893 | m = m.group(1) |
|
894 | 894 | if m == b'HEAD': |
|
895 | 895 | m = None |
|
896 | 896 | else: |
|
897 | 897 | m = None # if no group found then merge to HEAD |
|
898 | 898 | if m in branches and c.branch != m: |
|
899 | 899 | # insert empty changeset for merge |
|
900 | 900 | cc = changeset( |
|
901 | 901 | author=c.author, |
|
902 | 902 | branch=m, |
|
903 | 903 | date=c.date, |
|
904 | 904 | comment=b'convert-repo: CVS merge from branch %s' |
|
905 | 905 | % c.branch, |
|
906 | 906 | entries=[], |
|
907 | 907 | tags=[], |
|
908 | 908 | parents=[changesets[branches[m]], c], |
|
909 | 909 | ) |
|
910 | 910 | changesets.insert(i + 1, cc) |
|
911 | 911 | branches[m] = i + 1 |
|
912 | 912 | |
|
913 | 913 | # adjust our loop counters now we have inserted a new entry |
|
914 | 914 | n += 1 |
|
915 | 915 | i += 2 |
|
916 | 916 | continue |
|
917 | 917 | |
|
918 | 918 | branches[c.branch] = i |
|
919 | 919 | i += 1 |
|
920 | 920 | |
|
921 | 921 | # Drop synthetic changesets (safe now that we have ensured no other |
|
922 | 922 | # changesets can have them as parents). |
|
923 | 923 | i = 0 |
|
924 | 924 | while i < len(changesets): |
|
925 | 925 | if changesets[i].synthetic: |
|
926 | 926 | del changesets[i] |
|
927 | 927 | else: |
|
928 | 928 | i += 1 |
|
929 | 929 | |
|
930 | 930 | # Number changesets |
|
931 | 931 | |
|
932 | 932 | for i, c in enumerate(changesets): |
|
933 | 933 | c.id = i + 1 |
|
934 | 934 | |
|
935 | 935 | if odd: |
|
936 | 936 | for l, r in odd: |
|
937 | 937 | if l.id is not None and r.id is not None: |
|
938 | 938 | ui.warn( |
|
939 | 939 | _(b'changeset %d is both before and after %d\n') |
|
940 | 940 | % (l.id, r.id) |
|
941 | 941 | ) |
|
942 | 942 | |
|
943 | 943 | ui.status(_(b'%d changeset entries\n') % len(changesets)) |
|
944 | 944 | |
|
945 | 945 | hook.hook(ui, None, b"cvschangesets", True, changesets=changesets) |
|
946 | 946 | |
|
947 | 947 | return changesets |
|
948 | 948 | |
|
949 | 949 | |
|
950 | 950 | def debugcvsps(ui, *args, **opts): |
|
951 | 951 | '''Read CVS rlog for current directory or named path in |
|
952 | 952 | repository, and convert the log to changesets based on matching |
|
953 | 953 | commit log entries and dates. |
|
954 | 954 | ''' |
|
955 | 955 | opts = pycompat.byteskwargs(opts) |
|
956 | 956 | if opts[b"new_cache"]: |
|
957 | 957 | cache = b"write" |
|
958 | 958 | elif opts[b"update_cache"]: |
|
959 | 959 | cache = b"update" |
|
960 | 960 | else: |
|
961 | 961 | cache = None |
|
962 | 962 | |
|
963 | 963 | revisions = opts[b"revisions"] |
|
964 | 964 | |
|
965 | 965 | try: |
|
966 | 966 | if args: |
|
967 | 967 | log = [] |
|
968 | 968 | for d in args: |
|
969 | 969 | log += createlog(ui, d, root=opts[b"root"], cache=cache) |
|
970 | 970 | else: |
|
971 | 971 | log = createlog(ui, root=opts[b"root"], cache=cache) |
|
972 | 972 | except logerror as e: |
|
973 | 973 | ui.write(b"%r\n" % e) |
|
974 | 974 | return |
|
975 | 975 | |
|
976 | 976 | changesets = createchangeset(ui, log, opts[b"fuzz"]) |
|
977 | 977 | del log |
|
978 | 978 | |
|
979 | 979 | # Print changesets (optionally filtered) |
|
980 | 980 | |
|
981 | 981 | off = len(revisions) |
|
982 | 982 | branches = {} # latest version number in each branch |
|
983 | 983 | ancestors = {} # parent branch |
|
984 | 984 | for cs in changesets: |
|
985 | 985 | |
|
986 | 986 | if opts[b"ancestors"]: |
|
987 | 987 | if cs.branch not in branches and cs.parents and cs.parents[0].id: |
|
988 | 988 | ancestors[cs.branch] = ( |
|
989 | 989 | changesets[cs.parents[0].id - 1].branch, |
|
990 | 990 | cs.parents[0].id, |
|
991 | 991 | ) |
|
992 | 992 | branches[cs.branch] = cs.id |
|
993 | 993 | |
|
994 | 994 | # limit by branches |
|
995 | 995 | if ( |
|
996 | 996 | opts[b"branches"] |
|
997 | 997 | and (cs.branch or b'HEAD') not in opts[b"branches"] |
|
998 | 998 | ): |
|
999 | 999 | continue |
|
1000 | 1000 | |
|
1001 | 1001 | if not off: |
|
1002 | 1002 | # Note: trailing spaces on several lines here are needed to have |
|
1003 | 1003 | # bug-for-bug compatibility with cvsps. |
|
1004 | 1004 | ui.write(b'---------------------\n') |
|
1005 | 1005 | ui.write((b'PatchSet %d \n' % cs.id)) |
|
1006 | 1006 | ui.write( |
|
1007 | 1007 | ( |
|
1008 | 1008 | b'Date: %s\n' |
|
1009 | 1009 | % dateutil.datestr(cs.date, b'%Y/%m/%d %H:%M:%S %1%2') |
|
1010 | 1010 | ) |
|
1011 | 1011 | ) |
|
1012 | 1012 | ui.write((b'Author: %s\n' % cs.author)) |
|
1013 | 1013 | ui.write((b'Branch: %s\n' % (cs.branch or b'HEAD'))) |
|
1014 | 1014 | ui.write( |
|
1015 | 1015 | ( |
|
1016 | 1016 | b'Tag%s: %s \n' |
|
1017 | 1017 | % ( |
|
1018 | 1018 | [b'', b's'][len(cs.tags) > 1], |
|
1019 | 1019 | b','.join(cs.tags) or b'(none)', |
|
1020 | 1020 | ) |
|
1021 | 1021 | ) |
|
1022 | 1022 | ) |
|
1023 | 1023 | if cs.branchpoints: |
|
1024 | ui.write( | |
|
1024 | ui.writenoi18n( | |
|
1025 | 1025 | b'Branchpoints: %s \n' % b', '.join(sorted(cs.branchpoints)) |
|
1026 | 1026 | ) |
|
1027 | 1027 | if opts[b"parents"] and cs.parents: |
|
1028 | 1028 | if len(cs.parents) > 1: |
|
1029 | 1029 | ui.write( |
|
1030 | 1030 | ( |
|
1031 | 1031 | b'Parents: %s\n' |
|
1032 | 1032 | % (b','.join([(b"%d" % p.id) for p in cs.parents])) |
|
1033 | 1033 | ) |
|
1034 | 1034 | ) |
|
1035 | 1035 | else: |
|
1036 | 1036 | ui.write((b'Parent: %d\n' % cs.parents[0].id)) |
|
1037 | 1037 | |
|
1038 | 1038 | if opts[b"ancestors"]: |
|
1039 | 1039 | b = cs.branch |
|
1040 | 1040 | r = [] |
|
1041 | 1041 | while b: |
|
1042 | 1042 | b, c = ancestors[b] |
|
1043 | 1043 | r.append(b'%s:%d:%d' % (b or b"HEAD", c, branches[b])) |
|
1044 | 1044 | if r: |
|
1045 | 1045 | ui.write((b'Ancestors: %s\n' % (b','.join(r)))) |
|
1046 | 1046 | |
|
1047 | ui.write(b'Log:\n') | |
|
1047 | ui.writenoi18n(b'Log:\n') | |
|
1048 | 1048 | ui.write(b'%s\n\n' % cs.comment) |
|
1049 | ui.write(b'Members: \n') | |
|
1049 | ui.writenoi18n(b'Members: \n') | |
|
1050 | 1050 | for f in cs.entries: |
|
1051 | 1051 | fn = f.file |
|
1052 | 1052 | if fn.startswith(opts[b"prefix"]): |
|
1053 | 1053 | fn = fn[len(opts[b"prefix"]) :] |
|
1054 | 1054 | ui.write( |
|
1055 | 1055 | b'\t%s:%s->%s%s \n' |
|
1056 | 1056 | % ( |
|
1057 | 1057 | fn, |
|
1058 | 1058 | b'.'.join([b"%d" % x for x in f.parent]) or b'INITIAL', |
|
1059 | 1059 | b'.'.join([(b"%d" % x) for x in f.revision]), |
|
1060 | 1060 | [b'', b'(DEAD)'][f.dead], |
|
1061 | 1061 | ) |
|
1062 | 1062 | ) |
|
1063 | 1063 | ui.write(b'\n') |
|
1064 | 1064 | |
|
1065 | 1065 | # have we seen the start tag? |
|
1066 | 1066 | if revisions and off: |
|
1067 | 1067 | if revisions[0] == (b"%d" % cs.id) or revisions[0] in cs.tags: |
|
1068 | 1068 | off = False |
|
1069 | 1069 | |
|
1070 | 1070 | # see if we reached the end tag |
|
1071 | 1071 | if len(revisions) > 1 and not off: |
|
1072 | 1072 | if revisions[1] == (b"%d" % cs.id) or revisions[1] in cs.tags: |
|
1073 | 1073 | break |
@@ -1,385 +1,385 b'' | |||
|
1 | 1 | # Minimal support for git commands on an hg repository |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2005, 2006 Chris Mason <mason@suse.com> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | |
|
8 | 8 | '''browse the repository in a graphical way |
|
9 | 9 | |
|
10 | 10 | The hgk extension allows browsing the history of a repository in a |
|
11 | 11 | graphical way. It requires Tcl/Tk version 8.4 or later. (Tcl/Tk is not |
|
12 | 12 | distributed with Mercurial.) |
|
13 | 13 | |
|
14 | 14 | hgk consists of two parts: a Tcl script that does the displaying and |
|
15 | 15 | querying of information, and an extension to Mercurial named hgk.py, |
|
16 | 16 | which provides hooks for hgk to get information. hgk can be found in |
|
17 | 17 | the contrib directory, and the extension is shipped in the hgext |
|
18 | 18 | repository, and needs to be enabled. |
|
19 | 19 | |
|
20 | 20 | The :hg:`view` command will launch the hgk Tcl script. For this command |
|
21 | 21 | to work, hgk must be in your search path. Alternately, you can specify |
|
22 | 22 | the path to hgk in your configuration file:: |
|
23 | 23 | |
|
24 | 24 | [hgk] |
|
25 | 25 | path = /location/of/hgk |
|
26 | 26 | |
|
27 | 27 | hgk can make use of the extdiff extension to visualize revisions. |
|
28 | 28 | Assuming you had already configured extdiff vdiff command, just add:: |
|
29 | 29 | |
|
30 | 30 | [hgk] |
|
31 | 31 | vdiff=vdiff |
|
32 | 32 | |
|
33 | 33 | Revisions context menu will now display additional entries to fire |
|
34 | 34 | vdiff on hovered and selected revisions. |
|
35 | 35 | ''' |
|
36 | 36 | |
|
37 | 37 | from __future__ import absolute_import |
|
38 | 38 | |
|
39 | 39 | import os |
|
40 | 40 | |
|
41 | 41 | from mercurial.i18n import _ |
|
42 | 42 | from mercurial.node import ( |
|
43 | 43 | nullid, |
|
44 | 44 | nullrev, |
|
45 | 45 | short, |
|
46 | 46 | ) |
|
47 | 47 | from mercurial import ( |
|
48 | 48 | commands, |
|
49 | 49 | obsolete, |
|
50 | 50 | patch, |
|
51 | 51 | pycompat, |
|
52 | 52 | registrar, |
|
53 | 53 | scmutil, |
|
54 | 54 | ) |
|
55 | 55 | |
|
56 | 56 | cmdtable = {} |
|
57 | 57 | command = registrar.command(cmdtable) |
|
58 | 58 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
|
59 | 59 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
|
60 | 60 | # be specifying the version(s) of Mercurial they are tested with, or |
|
61 | 61 | # leave the attribute unspecified. |
|
62 | 62 | testedwith = b'ships-with-hg-core' |
|
63 | 63 | |
|
64 | 64 | configtable = {} |
|
65 | 65 | configitem = registrar.configitem(configtable) |
|
66 | 66 | |
|
67 | 67 | configitem( |
|
68 | 68 | b'hgk', b'path', default=b'hgk', |
|
69 | 69 | ) |
|
70 | 70 | |
|
71 | 71 | |
|
72 | 72 | @command( |
|
73 | 73 | b'debug-diff-tree', |
|
74 | 74 | [ |
|
75 | 75 | (b'p', b'patch', None, _(b'generate patch')), |
|
76 | 76 | (b'r', b'recursive', None, _(b'recursive')), |
|
77 | 77 | (b'P', b'pretty', None, _(b'pretty')), |
|
78 | 78 | (b's', b'stdin', None, _(b'stdin')), |
|
79 | 79 | (b'C', b'copy', None, _(b'detect copies')), |
|
80 | 80 | (b'S', b'search', b"", _(b'search')), |
|
81 | 81 | ], |
|
82 | 82 | b'[OPTION]... NODE1 NODE2 [FILE]...', |
|
83 | 83 | inferrepo=True, |
|
84 | 84 | ) |
|
85 | 85 | def difftree(ui, repo, node1=None, node2=None, *files, **opts): |
|
86 | 86 | """diff trees from two commits""" |
|
87 | 87 | |
|
88 | 88 | def __difftree(repo, node1, node2, files=None): |
|
89 | 89 | assert node2 is not None |
|
90 | 90 | if files is None: |
|
91 | 91 | files = [] |
|
92 | 92 | mmap = repo[node1].manifest() |
|
93 | 93 | mmap2 = repo[node2].manifest() |
|
94 | 94 | m = scmutil.match(repo[node1], files) |
|
95 | 95 | modified, added, removed = repo.status(node1, node2, m)[:3] |
|
96 | 96 | empty = short(nullid) |
|
97 | 97 | |
|
98 | 98 | for f in modified: |
|
99 | 99 | # TODO get file permissions |
|
100 | ui.write( | |
|
100 | ui.writenoi18n( | |
|
101 | 101 | b":100664 100664 %s %s M\t%s\t%s\n" |
|
102 | 102 | % (short(mmap[f]), short(mmap2[f]), f, f) |
|
103 | 103 | ) |
|
104 | 104 | for f in added: |
|
105 | ui.write( | |
|
105 | ui.writenoi18n( | |
|
106 | 106 | b":000000 100664 %s %s N\t%s\t%s\n" |
|
107 | 107 | % (empty, short(mmap2[f]), f, f) |
|
108 | 108 | ) |
|
109 | 109 | for f in removed: |
|
110 | ui.write( | |
|
110 | ui.writenoi18n( | |
|
111 | 111 | b":100664 000000 %s %s D\t%s\t%s\n" |
|
112 | 112 | % (short(mmap[f]), empty, f, f) |
|
113 | 113 | ) |
|
114 | 114 | |
|
115 | 115 | ## |
|
116 | 116 | |
|
117 | 117 | while True: |
|
118 | 118 | if opts[r'stdin']: |
|
119 | 119 | line = ui.fin.readline() |
|
120 | 120 | if not line: |
|
121 | 121 | break |
|
122 | 122 | line = line.rstrip(pycompat.oslinesep).split(b' ') |
|
123 | 123 | node1 = line[0] |
|
124 | 124 | if len(line) > 1: |
|
125 | 125 | node2 = line[1] |
|
126 | 126 | else: |
|
127 | 127 | node2 = None |
|
128 | 128 | node1 = repo.lookup(node1) |
|
129 | 129 | if node2: |
|
130 | 130 | node2 = repo.lookup(node2) |
|
131 | 131 | else: |
|
132 | 132 | node2 = node1 |
|
133 | 133 | node1 = repo.changelog.parents(node1)[0] |
|
134 | 134 | if opts[r'patch']: |
|
135 | 135 | if opts[r'pretty']: |
|
136 | 136 | catcommit(ui, repo, node2, b"") |
|
137 | 137 | m = scmutil.match(repo[node1], files) |
|
138 | 138 | diffopts = patch.difffeatureopts(ui) |
|
139 | 139 | diffopts.git = True |
|
140 | 140 | chunks = patch.diff(repo, node1, node2, match=m, opts=diffopts) |
|
141 | 141 | for chunk in chunks: |
|
142 | 142 | ui.write(chunk) |
|
143 | 143 | else: |
|
144 | 144 | __difftree(repo, node1, node2, files=files) |
|
145 | 145 | if not opts[r'stdin']: |
|
146 | 146 | break |
|
147 | 147 | |
|
148 | 148 | |
|
149 | 149 | def catcommit(ui, repo, n, prefix, ctx=None): |
|
150 | 150 | nlprefix = b'\n' + prefix |
|
151 | 151 | if ctx is None: |
|
152 | 152 | ctx = repo[n] |
|
153 | 153 | # use ctx.node() instead ?? |
|
154 | 154 | ui.write((b"tree %s\n" % short(ctx.changeset()[0]))) |
|
155 | 155 | for p in ctx.parents(): |
|
156 | 156 | ui.write((b"parent %s\n" % p)) |
|
157 | 157 | |
|
158 | 158 | date = ctx.date() |
|
159 | 159 | description = ctx.description().replace(b"\0", b"") |
|
160 | 160 | ui.write((b"author %s %d %d\n" % (ctx.user(), int(date[0]), date[1]))) |
|
161 | 161 | |
|
162 | 162 | if b'committer' in ctx.extra(): |
|
163 | 163 | ui.write((b"committer %s\n" % ctx.extra()[b'committer'])) |
|
164 | 164 | |
|
165 | 165 | ui.write((b"revision %d\n" % ctx.rev())) |
|
166 | 166 | ui.write((b"branch %s\n" % ctx.branch())) |
|
167 | 167 | if obsolete.isenabled(repo, obsolete.createmarkersopt): |
|
168 | 168 | if ctx.obsolete(): |
|
169 | ui.write(b"obsolete\n") | |
|
169 | ui.writenoi18n(b"obsolete\n") | |
|
170 | 170 | ui.write((b"phase %s\n\n" % ctx.phasestr())) |
|
171 | 171 | |
|
172 | 172 | if prefix != b"": |
|
173 | 173 | ui.write( |
|
174 | 174 | b"%s%s\n" % (prefix, description.replace(b'\n', nlprefix).strip()) |
|
175 | 175 | ) |
|
176 | 176 | else: |
|
177 | 177 | ui.write(description + b"\n") |
|
178 | 178 | if prefix: |
|
179 | 179 | ui.write(b'\0') |
|
180 | 180 | |
|
181 | 181 | |
|
182 | 182 | @command(b'debug-merge-base', [], _(b'REV REV')) |
|
183 | 183 | def base(ui, repo, node1, node2): |
|
184 | 184 | """output common ancestor information""" |
|
185 | 185 | node1 = repo.lookup(node1) |
|
186 | 186 | node2 = repo.lookup(node2) |
|
187 | 187 | n = repo.changelog.ancestor(node1, node2) |
|
188 | 188 | ui.write(short(n) + b"\n") |
|
189 | 189 | |
|
190 | 190 | |
|
191 | 191 | @command( |
|
192 | 192 | b'debug-cat-file', |
|
193 | 193 | [(b's', b'stdin', None, _(b'stdin'))], |
|
194 | 194 | _(b'[OPTION]... TYPE FILE'), |
|
195 | 195 | inferrepo=True, |
|
196 | 196 | ) |
|
197 | 197 | def catfile(ui, repo, type=None, r=None, **opts): |
|
198 | 198 | """cat a specific revision""" |
|
199 | 199 | # in stdin mode, every line except the commit is prefixed with two |
|
200 | 200 | # spaces. This way the our caller can find the commit without magic |
|
201 | 201 | # strings |
|
202 | 202 | # |
|
203 | 203 | prefix = b"" |
|
204 | 204 | if opts[r'stdin']: |
|
205 | 205 | line = ui.fin.readline() |
|
206 | 206 | if not line: |
|
207 | 207 | return |
|
208 | 208 | (type, r) = line.rstrip(pycompat.oslinesep).split(b' ') |
|
209 | 209 | prefix = b" " |
|
210 | 210 | else: |
|
211 | 211 | if not type or not r: |
|
212 | 212 | ui.warn(_(b"cat-file: type or revision not supplied\n")) |
|
213 | 213 | commands.help_(ui, b'cat-file') |
|
214 | 214 | |
|
215 | 215 | while r: |
|
216 | 216 | if type != b"commit": |
|
217 | 217 | ui.warn(_(b"aborting hg cat-file only understands commits\n")) |
|
218 | 218 | return 1 |
|
219 | 219 | n = repo.lookup(r) |
|
220 | 220 | catcommit(ui, repo, n, prefix) |
|
221 | 221 | if opts[r'stdin']: |
|
222 | 222 | line = ui.fin.readline() |
|
223 | 223 | if not line: |
|
224 | 224 | break |
|
225 | 225 | (type, r) = line.rstrip(pycompat.oslinesep).split(b' ') |
|
226 | 226 | else: |
|
227 | 227 | break |
|
228 | 228 | |
|
229 | 229 | |
|
230 | 230 | # git rev-tree is a confusing thing. You can supply a number of |
|
231 | 231 | # commit sha1s on the command line, and it walks the commit history |
|
232 | 232 | # telling you which commits are reachable from the supplied ones via |
|
233 | 233 | # a bitmask based on arg position. |
|
234 | 234 | # you can specify a commit to stop at by starting the sha1 with ^ |
|
235 | 235 | def revtree(ui, args, repo, full=b"tree", maxnr=0, parents=False): |
|
236 | 236 | def chlogwalk(): |
|
237 | 237 | count = len(repo) |
|
238 | 238 | i = count |
|
239 | 239 | l = [0] * 100 |
|
240 | 240 | chunk = 100 |
|
241 | 241 | while True: |
|
242 | 242 | if chunk > i: |
|
243 | 243 | chunk = i |
|
244 | 244 | i = 0 |
|
245 | 245 | else: |
|
246 | 246 | i -= chunk |
|
247 | 247 | |
|
248 | 248 | for x in pycompat.xrange(chunk): |
|
249 | 249 | if i + x >= count: |
|
250 | 250 | l[chunk - x :] = [0] * (chunk - x) |
|
251 | 251 | break |
|
252 | 252 | if full is not None: |
|
253 | 253 | if (i + x) in repo: |
|
254 | 254 | l[x] = repo[i + x] |
|
255 | 255 | l[x].changeset() # force reading |
|
256 | 256 | else: |
|
257 | 257 | if (i + x) in repo: |
|
258 | 258 | l[x] = 1 |
|
259 | 259 | for x in pycompat.xrange(chunk - 1, -1, -1): |
|
260 | 260 | if l[x] != 0: |
|
261 | 261 | yield (i + x, full is not None and l[x] or None) |
|
262 | 262 | if i == 0: |
|
263 | 263 | break |
|
264 | 264 | |
|
265 | 265 | # calculate and return the reachability bitmask for sha |
|
266 | 266 | def is_reachable(ar, reachable, sha): |
|
267 | 267 | if len(ar) == 0: |
|
268 | 268 | return 1 |
|
269 | 269 | mask = 0 |
|
270 | 270 | for i in pycompat.xrange(len(ar)): |
|
271 | 271 | if sha in reachable[i]: |
|
272 | 272 | mask |= 1 << i |
|
273 | 273 | |
|
274 | 274 | return mask |
|
275 | 275 | |
|
276 | 276 | reachable = [] |
|
277 | 277 | stop_sha1 = [] |
|
278 | 278 | want_sha1 = [] |
|
279 | 279 | count = 0 |
|
280 | 280 | |
|
281 | 281 | # figure out which commits they are asking for and which ones they |
|
282 | 282 | # want us to stop on |
|
283 | 283 | for i, arg in enumerate(args): |
|
284 | 284 | if arg.startswith(b'^'): |
|
285 | 285 | s = repo.lookup(arg[1:]) |
|
286 | 286 | stop_sha1.append(s) |
|
287 | 287 | want_sha1.append(s) |
|
288 | 288 | elif arg != b'HEAD': |
|
289 | 289 | want_sha1.append(repo.lookup(arg)) |
|
290 | 290 | |
|
291 | 291 | # calculate the graph for the supplied commits |
|
292 | 292 | for i, n in enumerate(want_sha1): |
|
293 | 293 | reachable.append(set()) |
|
294 | 294 | visit = [n] |
|
295 | 295 | reachable[i].add(n) |
|
296 | 296 | while visit: |
|
297 | 297 | n = visit.pop(0) |
|
298 | 298 | if n in stop_sha1: |
|
299 | 299 | continue |
|
300 | 300 | for p in repo.changelog.parents(n): |
|
301 | 301 | if p not in reachable[i]: |
|
302 | 302 | reachable[i].add(p) |
|
303 | 303 | visit.append(p) |
|
304 | 304 | if p in stop_sha1: |
|
305 | 305 | continue |
|
306 | 306 | |
|
307 | 307 | # walk the repository looking for commits that are in our |
|
308 | 308 | # reachability graph |
|
309 | 309 | for i, ctx in chlogwalk(): |
|
310 | 310 | if i not in repo: |
|
311 | 311 | continue |
|
312 | 312 | n = repo.changelog.node(i) |
|
313 | 313 | mask = is_reachable(want_sha1, reachable, n) |
|
314 | 314 | if mask: |
|
315 | 315 | parentstr = b"" |
|
316 | 316 | if parents: |
|
317 | 317 | pp = repo.changelog.parents(n) |
|
318 | 318 | if pp[0] != nullid: |
|
319 | 319 | parentstr += b" " + short(pp[0]) |
|
320 | 320 | if pp[1] != nullid: |
|
321 | 321 | parentstr += b" " + short(pp[1]) |
|
322 | 322 | if not full: |
|
323 | 323 | ui.write(b"%s%s\n" % (short(n), parentstr)) |
|
324 | 324 | elif full == b"commit": |
|
325 | 325 | ui.write(b"%s%s\n" % (short(n), parentstr)) |
|
326 | 326 | catcommit(ui, repo, n, b' ', ctx) |
|
327 | 327 | else: |
|
328 | 328 | (p1, p2) = repo.changelog.parents(n) |
|
329 | 329 | (h, h1, h2) = map(short, (n, p1, p2)) |
|
330 | 330 | (i1, i2) = map(repo.changelog.rev, (p1, p2)) |
|
331 | 331 | |
|
332 | 332 | date = ctx.date()[0] |
|
333 | 333 | ui.write(b"%s %s:%s" % (date, h, mask)) |
|
334 | 334 | mask = is_reachable(want_sha1, reachable, p1) |
|
335 | 335 | if i1 != nullrev and mask > 0: |
|
336 | 336 | ui.write(b"%s:%s " % (h1, mask)), |
|
337 | 337 | mask = is_reachable(want_sha1, reachable, p2) |
|
338 | 338 | if i2 != nullrev and mask > 0: |
|
339 | 339 | ui.write(b"%s:%s " % (h2, mask)) |
|
340 | 340 | ui.write(b"\n") |
|
341 | 341 | if maxnr and count >= maxnr: |
|
342 | 342 | break |
|
343 | 343 | count += 1 |
|
344 | 344 | |
|
345 | 345 | |
|
346 | 346 | # git rev-list tries to order things by date, and has the ability to stop |
|
347 | 347 | # at a given commit without walking the whole repo. TODO add the stop |
|
348 | 348 | # parameter |
|
349 | 349 | @command( |
|
350 | 350 | b'debug-rev-list', |
|
351 | 351 | [ |
|
352 | 352 | (b'H', b'header', None, _(b'header')), |
|
353 | 353 | (b't', b'topo-order', None, _(b'topo-order')), |
|
354 | 354 | (b'p', b'parents', None, _(b'parents')), |
|
355 | 355 | (b'n', b'max-count', 0, _(b'max-count')), |
|
356 | 356 | ], |
|
357 | 357 | b'[OPTION]... REV...', |
|
358 | 358 | ) |
|
359 | 359 | def revlist(ui, repo, *revs, **opts): |
|
360 | 360 | """print revisions""" |
|
361 | 361 | if opts[b'header']: |
|
362 | 362 | full = b"commit" |
|
363 | 363 | else: |
|
364 | 364 | full = None |
|
365 | 365 | copy = [x for x in revs] |
|
366 | 366 | revtree(ui, copy, repo, full, opts[r'max_count'], opts[r'parents']) |
|
367 | 367 | |
|
368 | 368 | |
|
369 | 369 | @command( |
|
370 | 370 | b'view', |
|
371 | 371 | [(b'l', b'limit', b'', _(b'limit number of changes displayed'), _(b'NUM'))], |
|
372 | 372 | _(b'[-l LIMIT] [REVRANGE]'), |
|
373 | 373 | helpcategory=command.CATEGORY_CHANGE_NAVIGATION, |
|
374 | 374 | ) |
|
375 | 375 | def view(ui, repo, *etc, **opts): |
|
376 | 376 | b"start interactive history viewer" |
|
377 | 377 | opts = pycompat.byteskwargs(opts) |
|
378 | 378 | os.chdir(repo.root) |
|
379 | 379 | optstr = b' '.join([b'--%s %s' % (k, v) for k, v in opts.iteritems() if v]) |
|
380 | 380 | if repo.filtername is None: |
|
381 | 381 | optstr += b'--hidden' |
|
382 | 382 | |
|
383 | 383 | cmd = ui.config(b"hgk", b"path") + b" %s %s" % (optstr, b" ".join(etc)) |
|
384 | 384 | ui.debug(b"running %s\n" % cmd) |
|
385 | 385 | ui.system(cmd, blockedtag=b'hgk_view') |
@@ -1,886 +1,886 b'' | |||
|
1 | 1 | # keyword.py - $Keyword$ expansion for Mercurial |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2007-2015 Christian Ebert <blacktrash@gmx.net> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | # |
|
8 | 8 | # $Id$ |
|
9 | 9 | # |
|
10 | 10 | # Keyword expansion hack against the grain of a Distributed SCM |
|
11 | 11 | # |
|
12 | 12 | # There are many good reasons why this is not needed in a distributed |
|
13 | 13 | # SCM, still it may be useful in very small projects based on single |
|
14 | 14 | # files (like LaTeX packages), that are mostly addressed to an |
|
15 | 15 | # audience not running a version control system. |
|
16 | 16 | # |
|
17 | 17 | # For in-depth discussion refer to |
|
18 | 18 | # <https://mercurial-scm.org/wiki/KeywordPlan>. |
|
19 | 19 | # |
|
20 | 20 | # Keyword expansion is based on Mercurial's changeset template mappings. |
|
21 | 21 | # |
|
22 | 22 | # Binary files are not touched. |
|
23 | 23 | # |
|
24 | 24 | # Files to act upon/ignore are specified in the [keyword] section. |
|
25 | 25 | # Customized keyword template mappings in the [keywordmaps] section. |
|
26 | 26 | # |
|
27 | 27 | # Run 'hg help keyword' and 'hg kwdemo' to get info on configuration. |
|
28 | 28 | |
|
29 | 29 | '''expand keywords in tracked files |
|
30 | 30 | |
|
31 | 31 | This extension expands RCS/CVS-like or self-customized $Keywords$ in |
|
32 | 32 | tracked text files selected by your configuration. |
|
33 | 33 | |
|
34 | 34 | Keywords are only expanded in local repositories and not stored in the |
|
35 | 35 | change history. The mechanism can be regarded as a convenience for the |
|
36 | 36 | current user or for archive distribution. |
|
37 | 37 | |
|
38 | 38 | Keywords expand to the changeset data pertaining to the latest change |
|
39 | 39 | relative to the working directory parent of each file. |
|
40 | 40 | |
|
41 | 41 | Configuration is done in the [keyword], [keywordset] and [keywordmaps] |
|
42 | 42 | sections of hgrc files. |
|
43 | 43 | |
|
44 | 44 | Example:: |
|
45 | 45 | |
|
46 | 46 | [keyword] |
|
47 | 47 | # expand keywords in every python file except those matching "x*" |
|
48 | 48 | **.py = |
|
49 | 49 | x* = ignore |
|
50 | 50 | |
|
51 | 51 | [keywordset] |
|
52 | 52 | # prefer svn- over cvs-like default keywordmaps |
|
53 | 53 | svn = True |
|
54 | 54 | |
|
55 | 55 | .. note:: |
|
56 | 56 | |
|
57 | 57 | The more specific you are in your filename patterns the less you |
|
58 | 58 | lose speed in huge repositories. |
|
59 | 59 | |
|
60 | 60 | For [keywordmaps] template mapping and expansion demonstration and |
|
61 | 61 | control run :hg:`kwdemo`. See :hg:`help templates` for a list of |
|
62 | 62 | available templates and filters. |
|
63 | 63 | |
|
64 | 64 | Three additional date template filters are provided: |
|
65 | 65 | |
|
66 | 66 | :``utcdate``: "2006/09/18 15:13:13" |
|
67 | 67 | :``svnutcdate``: "2006-09-18 15:13:13Z" |
|
68 | 68 | :``svnisodate``: "2006-09-18 08:13:13 -700 (Mon, 18 Sep 2006)" |
|
69 | 69 | |
|
70 | 70 | The default template mappings (view with :hg:`kwdemo -d`) can be |
|
71 | 71 | replaced with customized keywords and templates. Again, run |
|
72 | 72 | :hg:`kwdemo` to control the results of your configuration changes. |
|
73 | 73 | |
|
74 | 74 | Before changing/disabling active keywords, you must run :hg:`kwshrink` |
|
75 | 75 | to avoid storing expanded keywords in the change history. |
|
76 | 76 | |
|
77 | 77 | To force expansion after enabling it, or a configuration change, run |
|
78 | 78 | :hg:`kwexpand`. |
|
79 | 79 | |
|
80 | 80 | Expansions spanning more than one line and incremental expansions, |
|
81 | 81 | like CVS' $Log$, are not supported. A keyword template map "Log = |
|
82 | 82 | {desc}" expands to the first line of the changeset description. |
|
83 | 83 | ''' |
|
84 | 84 | |
|
85 | 85 | |
|
86 | 86 | from __future__ import absolute_import |
|
87 | 87 | |
|
88 | 88 | import os |
|
89 | 89 | import re |
|
90 | 90 | import weakref |
|
91 | 91 | |
|
92 | 92 | from mercurial.i18n import _ |
|
93 | 93 | from mercurial.hgweb import webcommands |
|
94 | 94 | |
|
95 | 95 | from mercurial import ( |
|
96 | 96 | cmdutil, |
|
97 | 97 | context, |
|
98 | 98 | dispatch, |
|
99 | 99 | error, |
|
100 | 100 | extensions, |
|
101 | 101 | filelog, |
|
102 | 102 | localrepo, |
|
103 | 103 | logcmdutil, |
|
104 | 104 | match, |
|
105 | 105 | patch, |
|
106 | 106 | pathutil, |
|
107 | 107 | pycompat, |
|
108 | 108 | registrar, |
|
109 | 109 | scmutil, |
|
110 | 110 | templatefilters, |
|
111 | 111 | templateutil, |
|
112 | 112 | util, |
|
113 | 113 | ) |
|
114 | 114 | from mercurial.utils import ( |
|
115 | 115 | dateutil, |
|
116 | 116 | stringutil, |
|
117 | 117 | ) |
|
118 | 118 | |
|
119 | 119 | cmdtable = {} |
|
120 | 120 | command = registrar.command(cmdtable) |
|
121 | 121 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
|
122 | 122 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
|
123 | 123 | # be specifying the version(s) of Mercurial they are tested with, or |
|
124 | 124 | # leave the attribute unspecified. |
|
125 | 125 | testedwith = b'ships-with-hg-core' |
|
126 | 126 | |
|
127 | 127 | # hg commands that do not act on keywords |
|
128 | 128 | nokwcommands = ( |
|
129 | 129 | b'add addremove annotate bundle export grep incoming init log' |
|
130 | 130 | b' outgoing push tip verify convert email glog' |
|
131 | 131 | ) |
|
132 | 132 | |
|
133 | 133 | # webcommands that do not act on keywords |
|
134 | 134 | nokwwebcommands = b'annotate changeset rev filediff diff comparison' |
|
135 | 135 | |
|
136 | 136 | # hg commands that trigger expansion only when writing to working dir, |
|
137 | 137 | # not when reading filelog, and unexpand when reading from working dir |
|
138 | 138 | restricted = ( |
|
139 | 139 | b'merge kwexpand kwshrink record qrecord resolve transplant' |
|
140 | 140 | b' unshelve rebase graft backout histedit fetch' |
|
141 | 141 | ) |
|
142 | 142 | |
|
143 | 143 | # names of extensions using dorecord |
|
144 | 144 | recordextensions = b'record' |
|
145 | 145 | |
|
146 | 146 | colortable = { |
|
147 | 147 | b'kwfiles.enabled': b'green bold', |
|
148 | 148 | b'kwfiles.deleted': b'cyan bold underline', |
|
149 | 149 | b'kwfiles.enabledunknown': b'green', |
|
150 | 150 | b'kwfiles.ignored': b'bold', |
|
151 | 151 | b'kwfiles.ignoredunknown': b'none', |
|
152 | 152 | } |
|
153 | 153 | |
|
154 | 154 | templatefilter = registrar.templatefilter() |
|
155 | 155 | |
|
156 | 156 | configtable = {} |
|
157 | 157 | configitem = registrar.configitem(configtable) |
|
158 | 158 | |
|
159 | 159 | configitem( |
|
160 | 160 | b'keywordset', b'svn', default=False, |
|
161 | 161 | ) |
|
162 | 162 | # date like in cvs' $Date |
|
163 | 163 | @templatefilter(b'utcdate', intype=templateutil.date) |
|
164 | 164 | def utcdate(date): |
|
165 | 165 | '''Date. Returns a UTC-date in this format: "2009/08/18 11:00:13". |
|
166 | 166 | ''' |
|
167 | 167 | dateformat = b'%Y/%m/%d %H:%M:%S' |
|
168 | 168 | return dateutil.datestr((date[0], 0), dateformat) |
|
169 | 169 | |
|
170 | 170 | |
|
171 | 171 | # date like in svn's $Date |
|
172 | 172 | @templatefilter(b'svnisodate', intype=templateutil.date) |
|
173 | 173 | def svnisodate(date): |
|
174 | 174 | '''Date. Returns a date in this format: "2009-08-18 13:00:13 |
|
175 | 175 | +0200 (Tue, 18 Aug 2009)". |
|
176 | 176 | ''' |
|
177 | 177 | return dateutil.datestr(date, b'%Y-%m-%d %H:%M:%S %1%2 (%a, %d %b %Y)') |
|
178 | 178 | |
|
179 | 179 | |
|
180 | 180 | # date like in svn's $Id |
|
181 | 181 | @templatefilter(b'svnutcdate', intype=templateutil.date) |
|
182 | 182 | def svnutcdate(date): |
|
183 | 183 | '''Date. Returns a UTC-date in this format: "2009-08-18 |
|
184 | 184 | 11:00:13Z". |
|
185 | 185 | ''' |
|
186 | 186 | dateformat = b'%Y-%m-%d %H:%M:%SZ' |
|
187 | 187 | return dateutil.datestr((date[0], 0), dateformat) |
|
188 | 188 | |
|
189 | 189 | |
|
190 | 190 | # make keyword tools accessible |
|
191 | 191 | kwtools = {b'hgcmd': b''} |
|
192 | 192 | |
|
193 | 193 | |
|
194 | 194 | def _defaultkwmaps(ui): |
|
195 | 195 | '''Returns default keywordmaps according to keywordset configuration.''' |
|
196 | 196 | templates = { |
|
197 | 197 | b'Revision': b'{node|short}', |
|
198 | 198 | b'Author': b'{author|user}', |
|
199 | 199 | } |
|
200 | 200 | kwsets = ( |
|
201 | 201 | { |
|
202 | 202 | b'Date': b'{date|utcdate}', |
|
203 | 203 | b'RCSfile': b'{file|basename},v', |
|
204 | 204 | b'RCSFile': b'{file|basename},v', # kept for backwards compatibility |
|
205 | 205 | # with hg-keyword |
|
206 | 206 | b'Source': b'{root}/{file},v', |
|
207 | 207 | b'Id': b'{file|basename},v {node|short} {date|utcdate} {author|user}', |
|
208 | 208 | b'Header': b'{root}/{file},v {node|short} {date|utcdate} {author|user}', |
|
209 | 209 | }, |
|
210 | 210 | { |
|
211 | 211 | b'Date': b'{date|svnisodate}', |
|
212 | 212 | b'Id': b'{file|basename},v {node|short} {date|svnutcdate} {author|user}', |
|
213 | 213 | b'LastChangedRevision': b'{node|short}', |
|
214 | 214 | b'LastChangedBy': b'{author|user}', |
|
215 | 215 | b'LastChangedDate': b'{date|svnisodate}', |
|
216 | 216 | }, |
|
217 | 217 | ) |
|
218 | 218 | templates.update(kwsets[ui.configbool(b'keywordset', b'svn')]) |
|
219 | 219 | return templates |
|
220 | 220 | |
|
221 | 221 | |
|
222 | 222 | def _shrinktext(text, subfunc): |
|
223 | 223 | '''Helper for keyword expansion removal in text. |
|
224 | 224 | Depending on subfunc also returns number of substitutions.''' |
|
225 | 225 | return subfunc(br'$\1$', text) |
|
226 | 226 | |
|
227 | 227 | |
|
228 | 228 | def _preselect(wstatus, changed): |
|
229 | 229 | '''Retrieves modified and added files from a working directory state |
|
230 | 230 | and returns the subset of each contained in given changed files |
|
231 | 231 | retrieved from a change context.''' |
|
232 | 232 | modified = [f for f in wstatus.modified if f in changed] |
|
233 | 233 | added = [f for f in wstatus.added if f in changed] |
|
234 | 234 | return modified, added |
|
235 | 235 | |
|
236 | 236 | |
|
237 | 237 | class kwtemplater(object): |
|
238 | 238 | ''' |
|
239 | 239 | Sets up keyword templates, corresponding keyword regex, and |
|
240 | 240 | provides keyword substitution functions. |
|
241 | 241 | ''' |
|
242 | 242 | |
|
243 | 243 | def __init__(self, ui, repo, inc, exc): |
|
244 | 244 | self.ui = ui |
|
245 | 245 | self._repo = weakref.ref(repo) |
|
246 | 246 | self.match = match.match(repo.root, b'', [], inc, exc) |
|
247 | 247 | self.restrict = kwtools[b'hgcmd'] in restricted.split() |
|
248 | 248 | self.postcommit = False |
|
249 | 249 | |
|
250 | 250 | kwmaps = self.ui.configitems(b'keywordmaps') |
|
251 | 251 | if kwmaps: # override default templates |
|
252 | 252 | self.templates = dict(kwmaps) |
|
253 | 253 | else: |
|
254 | 254 | self.templates = _defaultkwmaps(self.ui) |
|
255 | 255 | |
|
256 | 256 | @property |
|
257 | 257 | def repo(self): |
|
258 | 258 | return self._repo() |
|
259 | 259 | |
|
260 | 260 | @util.propertycache |
|
261 | 261 | def escape(self): |
|
262 | 262 | '''Returns bar-separated and escaped keywords.''' |
|
263 | 263 | return b'|'.join(map(stringutil.reescape, self.templates.keys())) |
|
264 | 264 | |
|
265 | 265 | @util.propertycache |
|
266 | 266 | def rekw(self): |
|
267 | 267 | '''Returns regex for unexpanded keywords.''' |
|
268 | 268 | return re.compile(br'\$(%s)\$' % self.escape) |
|
269 | 269 | |
|
270 | 270 | @util.propertycache |
|
271 | 271 | def rekwexp(self): |
|
272 | 272 | '''Returns regex for expanded keywords.''' |
|
273 | 273 | return re.compile(br'\$(%s): [^$\n\r]*? \$' % self.escape) |
|
274 | 274 | |
|
275 | 275 | def substitute(self, data, path, ctx, subfunc): |
|
276 | 276 | '''Replaces keywords in data with expanded template.''' |
|
277 | 277 | |
|
278 | 278 | def kwsub(mobj): |
|
279 | 279 | kw = mobj.group(1) |
|
280 | 280 | ct = logcmdutil.maketemplater( |
|
281 | 281 | self.ui, self.repo, self.templates[kw] |
|
282 | 282 | ) |
|
283 | 283 | self.ui.pushbuffer() |
|
284 | 284 | ct.show(ctx, root=self.repo.root, file=path) |
|
285 | 285 | ekw = templatefilters.firstline(self.ui.popbuffer()) |
|
286 | 286 | return b'$%s: %s $' % (kw, ekw) |
|
287 | 287 | |
|
288 | 288 | return subfunc(kwsub, data) |
|
289 | 289 | |
|
290 | 290 | def linkctx(self, path, fileid): |
|
291 | 291 | '''Similar to filelog.linkrev, but returns a changectx.''' |
|
292 | 292 | return self.repo.filectx(path, fileid=fileid).changectx() |
|
293 | 293 | |
|
294 | 294 | def expand(self, path, node, data): |
|
295 | 295 | '''Returns data with keywords expanded.''' |
|
296 | 296 | if ( |
|
297 | 297 | not self.restrict |
|
298 | 298 | and self.match(path) |
|
299 | 299 | and not stringutil.binary(data) |
|
300 | 300 | ): |
|
301 | 301 | ctx = self.linkctx(path, node) |
|
302 | 302 | return self.substitute(data, path, ctx, self.rekw.sub) |
|
303 | 303 | return data |
|
304 | 304 | |
|
305 | 305 | def iskwfile(self, cand, ctx): |
|
306 | 306 | '''Returns subset of candidates which are configured for keyword |
|
307 | 307 | expansion but are not symbolic links.''' |
|
308 | 308 | return [f for f in cand if self.match(f) and b'l' not in ctx.flags(f)] |
|
309 | 309 | |
|
310 | 310 | def overwrite(self, ctx, candidates, lookup, expand, rekw=False): |
|
311 | 311 | '''Overwrites selected files expanding/shrinking keywords.''' |
|
312 | 312 | if self.restrict or lookup or self.postcommit: # exclude kw_copy |
|
313 | 313 | candidates = self.iskwfile(candidates, ctx) |
|
314 | 314 | if not candidates: |
|
315 | 315 | return |
|
316 | 316 | kwcmd = self.restrict and lookup # kwexpand/kwshrink |
|
317 | 317 | if self.restrict or expand and lookup: |
|
318 | 318 | mf = ctx.manifest() |
|
319 | 319 | if self.restrict or rekw: |
|
320 | 320 | re_kw = self.rekw |
|
321 | 321 | else: |
|
322 | 322 | re_kw = self.rekwexp |
|
323 | 323 | if expand: |
|
324 | 324 | msg = _(b'overwriting %s expanding keywords\n') |
|
325 | 325 | else: |
|
326 | 326 | msg = _(b'overwriting %s shrinking keywords\n') |
|
327 | 327 | for f in candidates: |
|
328 | 328 | if self.restrict: |
|
329 | 329 | data = self.repo.file(f).read(mf[f]) |
|
330 | 330 | else: |
|
331 | 331 | data = self.repo.wread(f) |
|
332 | 332 | if stringutil.binary(data): |
|
333 | 333 | continue |
|
334 | 334 | if expand: |
|
335 | 335 | parents = ctx.parents() |
|
336 | 336 | if lookup: |
|
337 | 337 | ctx = self.linkctx(f, mf[f]) |
|
338 | 338 | elif self.restrict and len(parents) > 1: |
|
339 | 339 | # merge commit |
|
340 | 340 | # in case of conflict f is in modified state during |
|
341 | 341 | # merge, even if f does not differ from f in parent |
|
342 | 342 | for p in parents: |
|
343 | 343 | if f in p and not p[f].cmp(ctx[f]): |
|
344 | 344 | ctx = p[f].changectx() |
|
345 | 345 | break |
|
346 | 346 | data, found = self.substitute(data, f, ctx, re_kw.subn) |
|
347 | 347 | elif self.restrict: |
|
348 | 348 | found = re_kw.search(data) |
|
349 | 349 | else: |
|
350 | 350 | data, found = _shrinktext(data, re_kw.subn) |
|
351 | 351 | if found: |
|
352 | 352 | self.ui.note(msg % f) |
|
353 | 353 | fp = self.repo.wvfs(f, b"wb", atomictemp=True) |
|
354 | 354 | fp.write(data) |
|
355 | 355 | fp.close() |
|
356 | 356 | if kwcmd: |
|
357 | 357 | self.repo.dirstate.normal(f) |
|
358 | 358 | elif self.postcommit: |
|
359 | 359 | self.repo.dirstate.normallookup(f) |
|
360 | 360 | |
|
361 | 361 | def shrink(self, fname, text): |
|
362 | 362 | '''Returns text with all keyword substitutions removed.''' |
|
363 | 363 | if self.match(fname) and not stringutil.binary(text): |
|
364 | 364 | return _shrinktext(text, self.rekwexp.sub) |
|
365 | 365 | return text |
|
366 | 366 | |
|
367 | 367 | def shrinklines(self, fname, lines): |
|
368 | 368 | '''Returns lines with keyword substitutions removed.''' |
|
369 | 369 | if self.match(fname): |
|
370 | 370 | text = b''.join(lines) |
|
371 | 371 | if not stringutil.binary(text): |
|
372 | 372 | return _shrinktext(text, self.rekwexp.sub).splitlines(True) |
|
373 | 373 | return lines |
|
374 | 374 | |
|
375 | 375 | def wread(self, fname, data): |
|
376 | 376 | '''If in restricted mode returns data read from wdir with |
|
377 | 377 | keyword substitutions removed.''' |
|
378 | 378 | if self.restrict: |
|
379 | 379 | return self.shrink(fname, data) |
|
380 | 380 | return data |
|
381 | 381 | |
|
382 | 382 | |
|
383 | 383 | class kwfilelog(filelog.filelog): |
|
384 | 384 | ''' |
|
385 | 385 | Subclass of filelog to hook into its read, add, cmp methods. |
|
386 | 386 | Keywords are "stored" unexpanded, and processed on reading. |
|
387 | 387 | ''' |
|
388 | 388 | |
|
389 | 389 | def __init__(self, opener, kwt, path): |
|
390 | 390 | super(kwfilelog, self).__init__(opener, path) |
|
391 | 391 | self.kwt = kwt |
|
392 | 392 | self.path = path |
|
393 | 393 | |
|
394 | 394 | def read(self, node): |
|
395 | 395 | '''Expands keywords when reading filelog.''' |
|
396 | 396 | data = super(kwfilelog, self).read(node) |
|
397 | 397 | if self.renamed(node): |
|
398 | 398 | return data |
|
399 | 399 | return self.kwt.expand(self.path, node, data) |
|
400 | 400 | |
|
401 | 401 | def add(self, text, meta, tr, link, p1=None, p2=None): |
|
402 | 402 | '''Removes keyword substitutions when adding to filelog.''' |
|
403 | 403 | text = self.kwt.shrink(self.path, text) |
|
404 | 404 | return super(kwfilelog, self).add(text, meta, tr, link, p1, p2) |
|
405 | 405 | |
|
406 | 406 | def cmp(self, node, text): |
|
407 | 407 | '''Removes keyword substitutions for comparison.''' |
|
408 | 408 | text = self.kwt.shrink(self.path, text) |
|
409 | 409 | return super(kwfilelog, self).cmp(node, text) |
|
410 | 410 | |
|
411 | 411 | |
|
412 | 412 | def _status(ui, repo, wctx, kwt, *pats, **opts): |
|
413 | 413 | '''Bails out if [keyword] configuration is not active. |
|
414 | 414 | Returns status of working directory.''' |
|
415 | 415 | if kwt: |
|
416 | 416 | opts = pycompat.byteskwargs(opts) |
|
417 | 417 | return repo.status( |
|
418 | 418 | match=scmutil.match(wctx, pats, opts), |
|
419 | 419 | clean=True, |
|
420 | 420 | unknown=opts.get(b'unknown') or opts.get(b'all'), |
|
421 | 421 | ) |
|
422 | 422 | if ui.configitems(b'keyword'): |
|
423 | 423 | raise error.Abort(_(b'[keyword] patterns cannot match')) |
|
424 | 424 | raise error.Abort(_(b'no [keyword] patterns configured')) |
|
425 | 425 | |
|
426 | 426 | |
|
427 | 427 | def _kwfwrite(ui, repo, expand, *pats, **opts): |
|
428 | 428 | '''Selects files and passes them to kwtemplater.overwrite.''' |
|
429 | 429 | wctx = repo[None] |
|
430 | 430 | if len(wctx.parents()) > 1: |
|
431 | 431 | raise error.Abort(_(b'outstanding uncommitted merge')) |
|
432 | 432 | kwt = getattr(repo, '_keywordkwt', None) |
|
433 | 433 | with repo.wlock(): |
|
434 | 434 | status = _status(ui, repo, wctx, kwt, *pats, **opts) |
|
435 | 435 | if status.modified or status.added or status.removed or status.deleted: |
|
436 | 436 | raise error.Abort(_(b'outstanding uncommitted changes')) |
|
437 | 437 | kwt.overwrite(wctx, status.clean, True, expand) |
|
438 | 438 | |
|
439 | 439 | |
|
440 | 440 | @command( |
|
441 | 441 | b'kwdemo', |
|
442 | 442 | [ |
|
443 | 443 | (b'd', b'default', None, _(b'show default keyword template maps')), |
|
444 | 444 | (b'f', b'rcfile', b'', _(b'read maps from rcfile'), _(b'FILE')), |
|
445 | 445 | ], |
|
446 | 446 | _(b'hg kwdemo [-d] [-f RCFILE] [TEMPLATEMAP]...'), |
|
447 | 447 | optionalrepo=True, |
|
448 | 448 | ) |
|
449 | 449 | def demo(ui, repo, *args, **opts): |
|
450 | 450 | '''print [keywordmaps] configuration and an expansion example |
|
451 | 451 | |
|
452 | 452 | Show current, custom, or default keyword template maps and their |
|
453 | 453 | expansions. |
|
454 | 454 | |
|
455 | 455 | Extend the current configuration by specifying maps as arguments |
|
456 | 456 | and using -f/--rcfile to source an external hgrc file. |
|
457 | 457 | |
|
458 | 458 | Use -d/--default to disable current configuration. |
|
459 | 459 | |
|
460 | 460 | See :hg:`help templates` for information on templates and filters. |
|
461 | 461 | ''' |
|
462 | 462 | |
|
463 | 463 | def demoitems(section, items): |
|
464 | 464 | ui.write(b'[%s]\n' % section) |
|
465 | 465 | for k, v in sorted(items): |
|
466 | 466 | if isinstance(v, bool): |
|
467 | 467 | v = stringutil.pprint(v) |
|
468 | 468 | ui.write(b'%s = %s\n' % (k, v)) |
|
469 | 469 | |
|
470 | 470 | fn = b'demo.txt' |
|
471 | 471 | tmpdir = pycompat.mkdtemp(b'', b'kwdemo.') |
|
472 | 472 | ui.note(_(b'creating temporary repository at %s\n') % tmpdir) |
|
473 | 473 | if repo is None: |
|
474 | 474 | baseui = ui |
|
475 | 475 | else: |
|
476 | 476 | baseui = repo.baseui |
|
477 | 477 | repo = localrepo.instance(baseui, tmpdir, create=True) |
|
478 | 478 | ui.setconfig(b'keyword', fn, b'', b'keyword') |
|
479 | 479 | svn = ui.configbool(b'keywordset', b'svn') |
|
480 | 480 | # explicitly set keywordset for demo output |
|
481 | 481 | ui.setconfig(b'keywordset', b'svn', svn, b'keyword') |
|
482 | 482 | |
|
483 | 483 | uikwmaps = ui.configitems(b'keywordmaps') |
|
484 | 484 | if args or opts.get(r'rcfile'): |
|
485 | 485 | ui.status(_(b'\n\tconfiguration using custom keyword template maps\n')) |
|
486 | 486 | if uikwmaps: |
|
487 | 487 | ui.status(_(b'\textending current template maps\n')) |
|
488 | 488 | if opts.get(r'default') or not uikwmaps: |
|
489 | 489 | if svn: |
|
490 | 490 | ui.status(_(b'\toverriding default svn keywordset\n')) |
|
491 | 491 | else: |
|
492 | 492 | ui.status(_(b'\toverriding default cvs keywordset\n')) |
|
493 | 493 | if opts.get(r'rcfile'): |
|
494 | 494 | ui.readconfig(opts.get(b'rcfile')) |
|
495 | 495 | if args: |
|
496 | 496 | # simulate hgrc parsing |
|
497 | 497 | rcmaps = b'[keywordmaps]\n%s\n' % b'\n'.join(args) |
|
498 | 498 | repo.vfs.write(b'hgrc', rcmaps) |
|
499 | 499 | ui.readconfig(repo.vfs.join(b'hgrc')) |
|
500 | 500 | kwmaps = dict(ui.configitems(b'keywordmaps')) |
|
501 | 501 | elif opts.get(r'default'): |
|
502 | 502 | if svn: |
|
503 | 503 | ui.status(_(b'\n\tconfiguration using default svn keywordset\n')) |
|
504 | 504 | else: |
|
505 | 505 | ui.status(_(b'\n\tconfiguration using default cvs keywordset\n')) |
|
506 | 506 | kwmaps = _defaultkwmaps(ui) |
|
507 | 507 | if uikwmaps: |
|
508 | 508 | ui.status(_(b'\tdisabling current template maps\n')) |
|
509 | 509 | for k, v in kwmaps.iteritems(): |
|
510 | 510 | ui.setconfig(b'keywordmaps', k, v, b'keyword') |
|
511 | 511 | else: |
|
512 | 512 | ui.status(_(b'\n\tconfiguration using current keyword template maps\n')) |
|
513 | 513 | if uikwmaps: |
|
514 | 514 | kwmaps = dict(uikwmaps) |
|
515 | 515 | else: |
|
516 | 516 | kwmaps = _defaultkwmaps(ui) |
|
517 | 517 | |
|
518 | 518 | uisetup(ui) |
|
519 | 519 | reposetup(ui, repo) |
|
520 | ui.write(b'[extensions]\nkeyword =\n') | |
|
520 | ui.writenoi18n(b'[extensions]\nkeyword =\n') | |
|
521 | 521 | demoitems(b'keyword', ui.configitems(b'keyword')) |
|
522 | 522 | demoitems(b'keywordset', ui.configitems(b'keywordset')) |
|
523 | 523 | demoitems(b'keywordmaps', kwmaps.iteritems()) |
|
524 | 524 | keywords = b'$' + b'$\n$'.join(sorted(kwmaps.keys())) + b'$\n' |
|
525 | 525 | repo.wvfs.write(fn, keywords) |
|
526 | 526 | repo[None].add([fn]) |
|
527 | 527 | ui.note(_(b'\nkeywords written to %s:\n') % fn) |
|
528 | 528 | ui.note(keywords) |
|
529 | 529 | with repo.wlock(): |
|
530 | 530 | repo.dirstate.setbranch(b'demobranch') |
|
531 | 531 | for name, cmd in ui.configitems(b'hooks'): |
|
532 | 532 | if name.split(b'.', 1)[0].find(b'commit') > -1: |
|
533 | 533 | repo.ui.setconfig(b'hooks', name, b'', b'keyword') |
|
534 | 534 | msg = _(b'hg keyword configuration and expansion example') |
|
535 | 535 | ui.note((b"hg ci -m '%s'\n" % msg)) |
|
536 | 536 | repo.commit(text=msg) |
|
537 | 537 | ui.status(_(b'\n\tkeywords expanded\n')) |
|
538 | 538 | ui.write(repo.wread(fn)) |
|
539 | 539 | repo.wvfs.rmtree(repo.root) |
|
540 | 540 | |
|
541 | 541 | |
|
542 | 542 | @command( |
|
543 | 543 | b'kwexpand', |
|
544 | 544 | cmdutil.walkopts, |
|
545 | 545 | _(b'hg kwexpand [OPTION]... [FILE]...'), |
|
546 | 546 | inferrepo=True, |
|
547 | 547 | ) |
|
548 | 548 | def expand(ui, repo, *pats, **opts): |
|
549 | 549 | '''expand keywords in the working directory |
|
550 | 550 | |
|
551 | 551 | Run after (re)enabling keyword expansion. |
|
552 | 552 | |
|
553 | 553 | kwexpand refuses to run if given files contain local changes. |
|
554 | 554 | ''' |
|
555 | 555 | # 3rd argument sets expansion to True |
|
556 | 556 | _kwfwrite(ui, repo, True, *pats, **opts) |
|
557 | 557 | |
|
558 | 558 | |
|
559 | 559 | @command( |
|
560 | 560 | b'kwfiles', |
|
561 | 561 | [ |
|
562 | 562 | (b'A', b'all', None, _(b'show keyword status flags of all files')), |
|
563 | 563 | (b'i', b'ignore', None, _(b'show files excluded from expansion')), |
|
564 | 564 | (b'u', b'unknown', None, _(b'only show unknown (not tracked) files')), |
|
565 | 565 | ] |
|
566 | 566 | + cmdutil.walkopts, |
|
567 | 567 | _(b'hg kwfiles [OPTION]... [FILE]...'), |
|
568 | 568 | inferrepo=True, |
|
569 | 569 | ) |
|
570 | 570 | def files(ui, repo, *pats, **opts): |
|
571 | 571 | '''show files configured for keyword expansion |
|
572 | 572 | |
|
573 | 573 | List which files in the working directory are matched by the |
|
574 | 574 | [keyword] configuration patterns. |
|
575 | 575 | |
|
576 | 576 | Useful to prevent inadvertent keyword expansion and to speed up |
|
577 | 577 | execution by including only files that are actual candidates for |
|
578 | 578 | expansion. |
|
579 | 579 | |
|
580 | 580 | See :hg:`help keyword` on how to construct patterns both for |
|
581 | 581 | inclusion and exclusion of files. |
|
582 | 582 | |
|
583 | 583 | With -A/--all and -v/--verbose the codes used to show the status |
|
584 | 584 | of files are:: |
|
585 | 585 | |
|
586 | 586 | K = keyword expansion candidate |
|
587 | 587 | k = keyword expansion candidate (not tracked) |
|
588 | 588 | I = ignored |
|
589 | 589 | i = ignored (not tracked) |
|
590 | 590 | ''' |
|
591 | 591 | kwt = getattr(repo, '_keywordkwt', None) |
|
592 | 592 | wctx = repo[None] |
|
593 | 593 | status = _status(ui, repo, wctx, kwt, *pats, **opts) |
|
594 | 594 | if pats: |
|
595 | 595 | cwd = repo.getcwd() |
|
596 | 596 | else: |
|
597 | 597 | cwd = b'' |
|
598 | 598 | files = [] |
|
599 | 599 | opts = pycompat.byteskwargs(opts) |
|
600 | 600 | if not opts.get(b'unknown') or opts.get(b'all'): |
|
601 | 601 | files = sorted(status.modified + status.added + status.clean) |
|
602 | 602 | kwfiles = kwt.iskwfile(files, wctx) |
|
603 | 603 | kwdeleted = kwt.iskwfile(status.deleted, wctx) |
|
604 | 604 | kwunknown = kwt.iskwfile(status.unknown, wctx) |
|
605 | 605 | if not opts.get(b'ignore') or opts.get(b'all'): |
|
606 | 606 | showfiles = kwfiles, kwdeleted, kwunknown |
|
607 | 607 | else: |
|
608 | 608 | showfiles = [], [], [] |
|
609 | 609 | if opts.get(b'all') or opts.get(b'ignore'): |
|
610 | 610 | showfiles += ( |
|
611 | 611 | [f for f in files if f not in kwfiles], |
|
612 | 612 | [f for f in status.unknown if f not in kwunknown], |
|
613 | 613 | ) |
|
614 | 614 | kwlabels = b'enabled deleted enabledunknown ignored ignoredunknown'.split() |
|
615 | 615 | kwstates = zip(kwlabels, pycompat.bytestr(b'K!kIi'), showfiles) |
|
616 | 616 | fm = ui.formatter(b'kwfiles', opts) |
|
617 | 617 | fmt = b'%.0s%s\n' |
|
618 | 618 | if opts.get(b'all') or ui.verbose: |
|
619 | 619 | fmt = b'%s %s\n' |
|
620 | 620 | for kwstate, char, filenames in kwstates: |
|
621 | 621 | label = b'kwfiles.' + kwstate |
|
622 | 622 | for f in filenames: |
|
623 | 623 | fm.startitem() |
|
624 | 624 | fm.data(kwstatus=char, path=f) |
|
625 | 625 | fm.plain(fmt % (char, repo.pathto(f, cwd)), label=label) |
|
626 | 626 | fm.end() |
|
627 | 627 | |
|
628 | 628 | |
|
629 | 629 | @command( |
|
630 | 630 | b'kwshrink', |
|
631 | 631 | cmdutil.walkopts, |
|
632 | 632 | _(b'hg kwshrink [OPTION]... [FILE]...'), |
|
633 | 633 | inferrepo=True, |
|
634 | 634 | ) |
|
635 | 635 | def shrink(ui, repo, *pats, **opts): |
|
636 | 636 | '''revert expanded keywords in the working directory |
|
637 | 637 | |
|
638 | 638 | Must be run before changing/disabling active keywords. |
|
639 | 639 | |
|
640 | 640 | kwshrink refuses to run if given files contain local changes. |
|
641 | 641 | ''' |
|
642 | 642 | # 3rd argument sets expansion to False |
|
643 | 643 | _kwfwrite(ui, repo, False, *pats, **opts) |
|
644 | 644 | |
|
645 | 645 | |
|
646 | 646 | # monkeypatches |
|
647 | 647 | |
|
648 | 648 | |
|
649 | 649 | def kwpatchfile_init(orig, self, ui, gp, backend, store, eolmode=None): |
|
650 | 650 | '''Monkeypatch/wrap patch.patchfile.__init__ to avoid |
|
651 | 651 | rejects or conflicts due to expanded keywords in working dir.''' |
|
652 | 652 | orig(self, ui, gp, backend, store, eolmode) |
|
653 | 653 | kwt = getattr(getattr(backend, 'repo', None), '_keywordkwt', None) |
|
654 | 654 | if kwt: |
|
655 | 655 | # shrink keywords read from working dir |
|
656 | 656 | self.lines = kwt.shrinklines(self.fname, self.lines) |
|
657 | 657 | |
|
658 | 658 | |
|
659 | 659 | def kwdiff(orig, repo, *args, **kwargs): |
|
660 | 660 | '''Monkeypatch patch.diff to avoid expansion.''' |
|
661 | 661 | kwt = getattr(repo, '_keywordkwt', None) |
|
662 | 662 | if kwt: |
|
663 | 663 | restrict = kwt.restrict |
|
664 | 664 | kwt.restrict = True |
|
665 | 665 | try: |
|
666 | 666 | for chunk in orig(repo, *args, **kwargs): |
|
667 | 667 | yield chunk |
|
668 | 668 | finally: |
|
669 | 669 | if kwt: |
|
670 | 670 | kwt.restrict = restrict |
|
671 | 671 | |
|
672 | 672 | |
|
673 | 673 | def kwweb_skip(orig, web): |
|
674 | 674 | '''Wraps webcommands.x turning off keyword expansion.''' |
|
675 | 675 | kwt = getattr(web.repo, '_keywordkwt', None) |
|
676 | 676 | if kwt: |
|
677 | 677 | origmatch = kwt.match |
|
678 | 678 | kwt.match = util.never |
|
679 | 679 | try: |
|
680 | 680 | for chunk in orig(web): |
|
681 | 681 | yield chunk |
|
682 | 682 | finally: |
|
683 | 683 | if kwt: |
|
684 | 684 | kwt.match = origmatch |
|
685 | 685 | |
|
686 | 686 | |
|
687 | 687 | def kw_amend(orig, ui, repo, old, extra, pats, opts): |
|
688 | 688 | '''Wraps cmdutil.amend expanding keywords after amend.''' |
|
689 | 689 | kwt = getattr(repo, '_keywordkwt', None) |
|
690 | 690 | if kwt is None: |
|
691 | 691 | return orig(ui, repo, old, extra, pats, opts) |
|
692 | 692 | with repo.wlock(): |
|
693 | 693 | kwt.postcommit = True |
|
694 | 694 | newid = orig(ui, repo, old, extra, pats, opts) |
|
695 | 695 | if newid != old.node(): |
|
696 | 696 | ctx = repo[newid] |
|
697 | 697 | kwt.restrict = True |
|
698 | 698 | kwt.overwrite(ctx, ctx.files(), False, True) |
|
699 | 699 | kwt.restrict = False |
|
700 | 700 | return newid |
|
701 | 701 | |
|
702 | 702 | |
|
703 | 703 | def kw_copy(orig, ui, repo, pats, opts, rename=False): |
|
704 | 704 | '''Wraps cmdutil.copy so that copy/rename destinations do not |
|
705 | 705 | contain expanded keywords. |
|
706 | 706 | Note that the source of a regular file destination may also be a |
|
707 | 707 | symlink: |
|
708 | 708 | hg cp sym x -> x is symlink |
|
709 | 709 | cp sym x; hg cp -A sym x -> x is file (maybe expanded keywords) |
|
710 | 710 | For the latter we have to follow the symlink to find out whether its |
|
711 | 711 | target is configured for expansion and we therefore must unexpand the |
|
712 | 712 | keywords in the destination.''' |
|
713 | 713 | kwt = getattr(repo, '_keywordkwt', None) |
|
714 | 714 | if kwt is None: |
|
715 | 715 | return orig(ui, repo, pats, opts, rename) |
|
716 | 716 | with repo.wlock(): |
|
717 | 717 | orig(ui, repo, pats, opts, rename) |
|
718 | 718 | if opts.get(b'dry_run'): |
|
719 | 719 | return |
|
720 | 720 | wctx = repo[None] |
|
721 | 721 | cwd = repo.getcwd() |
|
722 | 722 | |
|
723 | 723 | def haskwsource(dest): |
|
724 | 724 | '''Returns true if dest is a regular file and configured for |
|
725 | 725 | expansion or a symlink which points to a file configured for |
|
726 | 726 | expansion. ''' |
|
727 | 727 | source = repo.dirstate.copied(dest) |
|
728 | 728 | if b'l' in wctx.flags(source): |
|
729 | 729 | source = pathutil.canonpath( |
|
730 | 730 | repo.root, cwd, os.path.realpath(source) |
|
731 | 731 | ) |
|
732 | 732 | return kwt.match(source) |
|
733 | 733 | |
|
734 | 734 | candidates = [ |
|
735 | 735 | f |
|
736 | 736 | for f in repo.dirstate.copies() |
|
737 | 737 | if b'l' not in wctx.flags(f) and haskwsource(f) |
|
738 | 738 | ] |
|
739 | 739 | kwt.overwrite(wctx, candidates, False, False) |
|
740 | 740 | |
|
741 | 741 | |
|
742 | 742 | def kw_dorecord(orig, ui, repo, commitfunc, *pats, **opts): |
|
743 | 743 | '''Wraps record.dorecord expanding keywords after recording.''' |
|
744 | 744 | kwt = getattr(repo, '_keywordkwt', None) |
|
745 | 745 | if kwt is None: |
|
746 | 746 | return orig(ui, repo, commitfunc, *pats, **opts) |
|
747 | 747 | with repo.wlock(): |
|
748 | 748 | # record returns 0 even when nothing has changed |
|
749 | 749 | # therefore compare nodes before and after |
|
750 | 750 | kwt.postcommit = True |
|
751 | 751 | ctx = repo[b'.'] |
|
752 | 752 | wstatus = ctx.status() |
|
753 | 753 | ret = orig(ui, repo, commitfunc, *pats, **opts) |
|
754 | 754 | recctx = repo[b'.'] |
|
755 | 755 | if ctx != recctx: |
|
756 | 756 | modified, added = _preselect(wstatus, recctx.files()) |
|
757 | 757 | kwt.restrict = False |
|
758 | 758 | kwt.overwrite(recctx, modified, False, True) |
|
759 | 759 | kwt.overwrite(recctx, added, False, True, True) |
|
760 | 760 | kwt.restrict = True |
|
761 | 761 | return ret |
|
762 | 762 | |
|
763 | 763 | |
|
764 | 764 | def kwfilectx_cmp(orig, self, fctx): |
|
765 | 765 | if fctx._customcmp: |
|
766 | 766 | return fctx.cmp(self) |
|
767 | 767 | kwt = getattr(self._repo, '_keywordkwt', None) |
|
768 | 768 | if kwt is None: |
|
769 | 769 | return orig(self, fctx) |
|
770 | 770 | # keyword affects data size, comparing wdir and filelog size does |
|
771 | 771 | # not make sense |
|
772 | 772 | if ( |
|
773 | 773 | fctx._filenode is None |
|
774 | 774 | and ( |
|
775 | 775 | self._repo._encodefilterpats |
|
776 | 776 | or kwt.match(fctx.path()) |
|
777 | 777 | and b'l' not in fctx.flags() |
|
778 | 778 | or self.size() - 4 == fctx.size() |
|
779 | 779 | ) |
|
780 | 780 | or self.size() == fctx.size() |
|
781 | 781 | ): |
|
782 | 782 | return self._filelog.cmp(self._filenode, fctx.data()) |
|
783 | 783 | return True |
|
784 | 784 | |
|
785 | 785 | |
|
786 | 786 | def uisetup(ui): |
|
787 | 787 | ''' Monkeypatches dispatch._parse to retrieve user command. |
|
788 | 788 | Overrides file method to return kwfilelog instead of filelog |
|
789 | 789 | if file matches user configuration. |
|
790 | 790 | Wraps commit to overwrite configured files with updated |
|
791 | 791 | keyword substitutions. |
|
792 | 792 | Monkeypatches patch and webcommands.''' |
|
793 | 793 | |
|
794 | 794 | def kwdispatch_parse(orig, ui, args): |
|
795 | 795 | '''Monkeypatch dispatch._parse to obtain running hg command.''' |
|
796 | 796 | cmd, func, args, options, cmdoptions = orig(ui, args) |
|
797 | 797 | kwtools[b'hgcmd'] = cmd |
|
798 | 798 | return cmd, func, args, options, cmdoptions |
|
799 | 799 | |
|
800 | 800 | extensions.wrapfunction(dispatch, b'_parse', kwdispatch_parse) |
|
801 | 801 | |
|
802 | 802 | extensions.wrapfunction(context.filectx, b'cmp', kwfilectx_cmp) |
|
803 | 803 | extensions.wrapfunction(patch.patchfile, b'__init__', kwpatchfile_init) |
|
804 | 804 | extensions.wrapfunction(patch, b'diff', kwdiff) |
|
805 | 805 | extensions.wrapfunction(cmdutil, b'amend', kw_amend) |
|
806 | 806 | extensions.wrapfunction(cmdutil, b'copy', kw_copy) |
|
807 | 807 | extensions.wrapfunction(cmdutil, b'dorecord', kw_dorecord) |
|
808 | 808 | for c in nokwwebcommands.split(): |
|
809 | 809 | extensions.wrapfunction(webcommands, c, kwweb_skip) |
|
810 | 810 | |
|
811 | 811 | |
|
812 | 812 | def reposetup(ui, repo): |
|
813 | 813 | '''Sets up repo as kwrepo for keyword substitution.''' |
|
814 | 814 | |
|
815 | 815 | try: |
|
816 | 816 | if ( |
|
817 | 817 | not repo.local() |
|
818 | 818 | or kwtools[b'hgcmd'] in nokwcommands.split() |
|
819 | 819 | or b'.hg' in util.splitpath(repo.root) |
|
820 | 820 | or repo._url.startswith(b'bundle:') |
|
821 | 821 | ): |
|
822 | 822 | return |
|
823 | 823 | except AttributeError: |
|
824 | 824 | pass |
|
825 | 825 | |
|
826 | 826 | inc, exc = [], [b'.hg*'] |
|
827 | 827 | for pat, opt in ui.configitems(b'keyword'): |
|
828 | 828 | if opt != b'ignore': |
|
829 | 829 | inc.append(pat) |
|
830 | 830 | else: |
|
831 | 831 | exc.append(pat) |
|
832 | 832 | if not inc: |
|
833 | 833 | return |
|
834 | 834 | |
|
835 | 835 | kwt = kwtemplater(ui, repo, inc, exc) |
|
836 | 836 | |
|
837 | 837 | class kwrepo(repo.__class__): |
|
838 | 838 | def file(self, f): |
|
839 | 839 | if f[0] == b'/': |
|
840 | 840 | f = f[1:] |
|
841 | 841 | return kwfilelog(self.svfs, kwt, f) |
|
842 | 842 | |
|
843 | 843 | def wread(self, filename): |
|
844 | 844 | data = super(kwrepo, self).wread(filename) |
|
845 | 845 | return kwt.wread(filename, data) |
|
846 | 846 | |
|
847 | 847 | def commit(self, *args, **opts): |
|
848 | 848 | # use custom commitctx for user commands |
|
849 | 849 | # other extensions can still wrap repo.commitctx directly |
|
850 | 850 | self.commitctx = self.kwcommitctx |
|
851 | 851 | try: |
|
852 | 852 | return super(kwrepo, self).commit(*args, **opts) |
|
853 | 853 | finally: |
|
854 | 854 | del self.commitctx |
|
855 | 855 | |
|
856 | 856 | def kwcommitctx(self, ctx, error=False, origctx=None): |
|
857 | 857 | n = super(kwrepo, self).commitctx(ctx, error, origctx) |
|
858 | 858 | # no lock needed, only called from repo.commit() which already locks |
|
859 | 859 | if not kwt.postcommit: |
|
860 | 860 | restrict = kwt.restrict |
|
861 | 861 | kwt.restrict = True |
|
862 | 862 | kwt.overwrite( |
|
863 | 863 | self[n], sorted(ctx.added() + ctx.modified()), False, True |
|
864 | 864 | ) |
|
865 | 865 | kwt.restrict = restrict |
|
866 | 866 | return n |
|
867 | 867 | |
|
868 | 868 | def rollback(self, dryrun=False, force=False): |
|
869 | 869 | with self.wlock(): |
|
870 | 870 | origrestrict = kwt.restrict |
|
871 | 871 | try: |
|
872 | 872 | if not dryrun: |
|
873 | 873 | changed = self[b'.'].files() |
|
874 | 874 | ret = super(kwrepo, self).rollback(dryrun, force) |
|
875 | 875 | if not dryrun: |
|
876 | 876 | ctx = self[b'.'] |
|
877 | 877 | modified, added = _preselect(ctx.status(), changed) |
|
878 | 878 | kwt.restrict = False |
|
879 | 879 | kwt.overwrite(ctx, modified, True, True) |
|
880 | 880 | kwt.overwrite(ctx, added, True, False) |
|
881 | 881 | return ret |
|
882 | 882 | finally: |
|
883 | 883 | kwt.restrict = origrestrict |
|
884 | 884 | |
|
885 | 885 | repo.__class__ = kwrepo |
|
886 | 886 | repo._keywordkwt = kwt |
@@ -1,1256 +1,1256 b'' | |||
|
1 | 1 | # phabricator.py - simple Phabricator integration |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2017 Facebook, Inc. |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | """simple Phabricator integration (EXPERIMENTAL) |
|
8 | 8 | |
|
9 | 9 | This extension provides a ``phabsend`` command which sends a stack of |
|
10 | 10 | changesets to Phabricator, and a ``phabread`` command which prints a stack of |
|
11 | 11 | revisions in a format suitable for :hg:`import`, and a ``phabupdate`` command |
|
12 | 12 | to update statuses in batch. |
|
13 | 13 | |
|
14 | 14 | By default, Phabricator requires ``Test Plan`` which might prevent some |
|
15 | 15 | changeset from being sent. The requirement could be disabled by changing |
|
16 | 16 | ``differential.require-test-plan-field`` config server side. |
|
17 | 17 | |
|
18 | 18 | Config:: |
|
19 | 19 | |
|
20 | 20 | [phabricator] |
|
21 | 21 | # Phabricator URL |
|
22 | 22 | url = https://phab.example.com/ |
|
23 | 23 | |
|
24 | 24 | # Repo callsign. If a repo has a URL https://$HOST/diffusion/FOO, then its |
|
25 | 25 | # callsign is "FOO". |
|
26 | 26 | callsign = FOO |
|
27 | 27 | |
|
28 | 28 | # curl command to use. If not set (default), use builtin HTTP library to |
|
29 | 29 | # communicate. If set, use the specified curl command. This could be useful |
|
30 | 30 | # if you need to specify advanced options that is not easily supported by |
|
31 | 31 | # the internal library. |
|
32 | 32 | curlcmd = curl --connect-timeout 2 --retry 3 --silent |
|
33 | 33 | |
|
34 | 34 | [auth] |
|
35 | 35 | example.schemes = https |
|
36 | 36 | example.prefix = phab.example.com |
|
37 | 37 | |
|
38 | 38 | # API token. Get it from https://$HOST/conduit/login/ |
|
39 | 39 | example.phabtoken = cli-xxxxxxxxxxxxxxxxxxxxxxxxxxxx |
|
40 | 40 | """ |
|
41 | 41 | |
|
42 | 42 | from __future__ import absolute_import |
|
43 | 43 | |
|
44 | 44 | import contextlib |
|
45 | 45 | import itertools |
|
46 | 46 | import json |
|
47 | 47 | import operator |
|
48 | 48 | import re |
|
49 | 49 | |
|
50 | 50 | from mercurial.node import bin, nullid |
|
51 | 51 | from mercurial.i18n import _ |
|
52 | 52 | from mercurial import ( |
|
53 | 53 | cmdutil, |
|
54 | 54 | context, |
|
55 | 55 | encoding, |
|
56 | 56 | error, |
|
57 | 57 | exthelper, |
|
58 | 58 | httpconnection as httpconnectionmod, |
|
59 | 59 | mdiff, |
|
60 | 60 | obsutil, |
|
61 | 61 | parser, |
|
62 | 62 | patch, |
|
63 | 63 | phases, |
|
64 | 64 | pycompat, |
|
65 | 65 | scmutil, |
|
66 | 66 | smartset, |
|
67 | 67 | tags, |
|
68 | 68 | templatefilters, |
|
69 | 69 | templateutil, |
|
70 | 70 | url as urlmod, |
|
71 | 71 | util, |
|
72 | 72 | ) |
|
73 | 73 | from mercurial.utils import ( |
|
74 | 74 | procutil, |
|
75 | 75 | stringutil, |
|
76 | 76 | ) |
|
77 | 77 | |
|
78 | 78 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
|
79 | 79 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
|
80 | 80 | # be specifying the version(s) of Mercurial they are tested with, or |
|
81 | 81 | # leave the attribute unspecified. |
|
82 | 82 | testedwith = b'ships-with-hg-core' |
|
83 | 83 | |
|
84 | 84 | eh = exthelper.exthelper() |
|
85 | 85 | |
|
86 | 86 | cmdtable = eh.cmdtable |
|
87 | 87 | command = eh.command |
|
88 | 88 | configtable = eh.configtable |
|
89 | 89 | templatekeyword = eh.templatekeyword |
|
90 | 90 | |
|
91 | 91 | # developer config: phabricator.batchsize |
|
92 | 92 | eh.configitem( |
|
93 | 93 | b'phabricator', b'batchsize', default=12, |
|
94 | 94 | ) |
|
95 | 95 | eh.configitem( |
|
96 | 96 | b'phabricator', b'callsign', default=None, |
|
97 | 97 | ) |
|
98 | 98 | eh.configitem( |
|
99 | 99 | b'phabricator', b'curlcmd', default=None, |
|
100 | 100 | ) |
|
101 | 101 | # developer config: phabricator.repophid |
|
102 | 102 | eh.configitem( |
|
103 | 103 | b'phabricator', b'repophid', default=None, |
|
104 | 104 | ) |
|
105 | 105 | eh.configitem( |
|
106 | 106 | b'phabricator', b'url', default=None, |
|
107 | 107 | ) |
|
108 | 108 | eh.configitem( |
|
109 | 109 | b'phabsend', b'confirm', default=False, |
|
110 | 110 | ) |
|
111 | 111 | |
|
112 | 112 | colortable = { |
|
113 | 113 | b'phabricator.action.created': b'green', |
|
114 | 114 | b'phabricator.action.skipped': b'magenta', |
|
115 | 115 | b'phabricator.action.updated': b'magenta', |
|
116 | 116 | b'phabricator.desc': b'', |
|
117 | 117 | b'phabricator.drev': b'bold', |
|
118 | 118 | b'phabricator.node': b'', |
|
119 | 119 | } |
|
120 | 120 | |
|
121 | 121 | _VCR_FLAGS = [ |
|
122 | 122 | ( |
|
123 | 123 | b'', |
|
124 | 124 | b'test-vcr', |
|
125 | 125 | b'', |
|
126 | 126 | _( |
|
127 | 127 | b'Path to a vcr file. If nonexistent, will record a new vcr transcript' |
|
128 | 128 | b', otherwise will mock all http requests using the specified vcr file.' |
|
129 | 129 | b' (ADVANCED)' |
|
130 | 130 | ), |
|
131 | 131 | ), |
|
132 | 132 | ] |
|
133 | 133 | |
|
134 | 134 | |
|
135 | 135 | def vcrcommand(name, flags, spec, helpcategory=None, optionalrepo=False): |
|
136 | 136 | fullflags = flags + _VCR_FLAGS |
|
137 | 137 | |
|
138 | 138 | def hgmatcher(r1, r2): |
|
139 | 139 | if r1.uri != r2.uri or r1.method != r2.method: |
|
140 | 140 | return False |
|
141 | 141 | r1params = r1.body.split(b'&') |
|
142 | 142 | r2params = r2.body.split(b'&') |
|
143 | 143 | return set(r1params) == set(r2params) |
|
144 | 144 | |
|
145 | 145 | def sanitiserequest(request): |
|
146 | 146 | request.body = re.sub( |
|
147 | 147 | br'cli-[a-z0-9]+', br'cli-hahayouwish', request.body |
|
148 | 148 | ) |
|
149 | 149 | return request |
|
150 | 150 | |
|
151 | 151 | def sanitiseresponse(response): |
|
152 | 152 | if r'set-cookie' in response[r'headers']: |
|
153 | 153 | del response[r'headers'][r'set-cookie'] |
|
154 | 154 | return response |
|
155 | 155 | |
|
156 | 156 | def decorate(fn): |
|
157 | 157 | def inner(*args, **kwargs): |
|
158 | 158 | cassette = pycompat.fsdecode(kwargs.pop(r'test_vcr', None)) |
|
159 | 159 | if cassette: |
|
160 | 160 | import hgdemandimport |
|
161 | 161 | |
|
162 | 162 | with hgdemandimport.deactivated(): |
|
163 | 163 | import vcr as vcrmod |
|
164 | 164 | import vcr.stubs as stubs |
|
165 | 165 | |
|
166 | 166 | vcr = vcrmod.VCR( |
|
167 | 167 | serializer=r'json', |
|
168 | 168 | before_record_request=sanitiserequest, |
|
169 | 169 | before_record_response=sanitiseresponse, |
|
170 | 170 | custom_patches=[ |
|
171 | 171 | ( |
|
172 | 172 | urlmod, |
|
173 | 173 | r'httpconnection', |
|
174 | 174 | stubs.VCRHTTPConnection, |
|
175 | 175 | ), |
|
176 | 176 | ( |
|
177 | 177 | urlmod, |
|
178 | 178 | r'httpsconnection', |
|
179 | 179 | stubs.VCRHTTPSConnection, |
|
180 | 180 | ), |
|
181 | 181 | ], |
|
182 | 182 | ) |
|
183 | 183 | vcr.register_matcher(r'hgmatcher', hgmatcher) |
|
184 | 184 | with vcr.use_cassette(cassette, match_on=[r'hgmatcher']): |
|
185 | 185 | return fn(*args, **kwargs) |
|
186 | 186 | return fn(*args, **kwargs) |
|
187 | 187 | |
|
188 | 188 | inner.__name__ = fn.__name__ |
|
189 | 189 | inner.__doc__ = fn.__doc__ |
|
190 | 190 | return command( |
|
191 | 191 | name, |
|
192 | 192 | fullflags, |
|
193 | 193 | spec, |
|
194 | 194 | helpcategory=helpcategory, |
|
195 | 195 | optionalrepo=optionalrepo, |
|
196 | 196 | )(inner) |
|
197 | 197 | |
|
198 | 198 | return decorate |
|
199 | 199 | |
|
200 | 200 | |
|
201 | 201 | def urlencodenested(params): |
|
202 | 202 | """like urlencode, but works with nested parameters. |
|
203 | 203 | |
|
204 | 204 | For example, if params is {'a': ['b', 'c'], 'd': {'e': 'f'}}, it will be |
|
205 | 205 | flattened to {'a[0]': 'b', 'a[1]': 'c', 'd[e]': 'f'} and then passed to |
|
206 | 206 | urlencode. Note: the encoding is consistent with PHP's http_build_query. |
|
207 | 207 | """ |
|
208 | 208 | flatparams = util.sortdict() |
|
209 | 209 | |
|
210 | 210 | def process(prefix, obj): |
|
211 | 211 | if isinstance(obj, bool): |
|
212 | 212 | obj = {True: b'true', False: b'false'}[obj] # Python -> PHP form |
|
213 | 213 | lister = lambda l: [(b'%d' % k, v) for k, v in enumerate(l)] |
|
214 | 214 | items = {list: lister, dict: lambda x: x.items()}.get(type(obj)) |
|
215 | 215 | if items is None: |
|
216 | 216 | flatparams[prefix] = obj |
|
217 | 217 | else: |
|
218 | 218 | for k, v in items(obj): |
|
219 | 219 | if prefix: |
|
220 | 220 | process(b'%s[%s]' % (prefix, k), v) |
|
221 | 221 | else: |
|
222 | 222 | process(k, v) |
|
223 | 223 | |
|
224 | 224 | process(b'', params) |
|
225 | 225 | return util.urlreq.urlencode(flatparams) |
|
226 | 226 | |
|
227 | 227 | |
|
228 | 228 | def readurltoken(ui): |
|
229 | 229 | """return conduit url, token and make sure they exist |
|
230 | 230 | |
|
231 | 231 | Currently read from [auth] config section. In the future, it might |
|
232 | 232 | make sense to read from .arcconfig and .arcrc as well. |
|
233 | 233 | """ |
|
234 | 234 | url = ui.config(b'phabricator', b'url') |
|
235 | 235 | if not url: |
|
236 | 236 | raise error.Abort( |
|
237 | 237 | _(b'config %s.%s is required') % (b'phabricator', b'url') |
|
238 | 238 | ) |
|
239 | 239 | |
|
240 | 240 | res = httpconnectionmod.readauthforuri(ui, url, util.url(url).user) |
|
241 | 241 | token = None |
|
242 | 242 | |
|
243 | 243 | if res: |
|
244 | 244 | group, auth = res |
|
245 | 245 | |
|
246 | 246 | ui.debug(b"using auth.%s.* for authentication\n" % group) |
|
247 | 247 | |
|
248 | 248 | token = auth.get(b'phabtoken') |
|
249 | 249 | |
|
250 | 250 | if not token: |
|
251 | 251 | raise error.Abort( |
|
252 | 252 | _(b'Can\'t find conduit token associated to %s') % (url,) |
|
253 | 253 | ) |
|
254 | 254 | |
|
255 | 255 | return url, token |
|
256 | 256 | |
|
257 | 257 | |
|
258 | 258 | def callconduit(ui, name, params): |
|
259 | 259 | """call Conduit API, params is a dict. return json.loads result, or None""" |
|
260 | 260 | host, token = readurltoken(ui) |
|
261 | 261 | url, authinfo = util.url(b'/'.join([host, b'api', name])).authinfo() |
|
262 | 262 | ui.debug(b'Conduit Call: %s %s\n' % (url, pycompat.byterepr(params))) |
|
263 | 263 | params = params.copy() |
|
264 | 264 | params[b'api.token'] = token |
|
265 | 265 | data = urlencodenested(params) |
|
266 | 266 | curlcmd = ui.config(b'phabricator', b'curlcmd') |
|
267 | 267 | if curlcmd: |
|
268 | 268 | sin, sout = procutil.popen2( |
|
269 | 269 | b'%s -d @- %s' % (curlcmd, procutil.shellquote(url)) |
|
270 | 270 | ) |
|
271 | 271 | sin.write(data) |
|
272 | 272 | sin.close() |
|
273 | 273 | body = sout.read() |
|
274 | 274 | else: |
|
275 | 275 | urlopener = urlmod.opener(ui, authinfo) |
|
276 | 276 | request = util.urlreq.request(pycompat.strurl(url), data=data) |
|
277 | 277 | with contextlib.closing(urlopener.open(request)) as rsp: |
|
278 | 278 | body = rsp.read() |
|
279 | 279 | ui.debug(b'Conduit Response: %s\n' % body) |
|
280 | 280 | parsed = pycompat.rapply( |
|
281 | 281 | lambda x: encoding.unitolocal(x) |
|
282 | 282 | if isinstance(x, pycompat.unicode) |
|
283 | 283 | else x, |
|
284 | 284 | # json.loads only accepts bytes from py3.6+ |
|
285 | 285 | json.loads(encoding.unifromlocal(body)), |
|
286 | 286 | ) |
|
287 | 287 | if parsed.get(b'error_code'): |
|
288 | 288 | msg = _(b'Conduit Error (%s): %s') % ( |
|
289 | 289 | parsed[b'error_code'], |
|
290 | 290 | parsed[b'error_info'], |
|
291 | 291 | ) |
|
292 | 292 | raise error.Abort(msg) |
|
293 | 293 | return parsed[b'result'] |
|
294 | 294 | |
|
295 | 295 | |
|
296 | 296 | @vcrcommand(b'debugcallconduit', [], _(b'METHOD'), optionalrepo=True) |
|
297 | 297 | def debugcallconduit(ui, repo, name): |
|
298 | 298 | """call Conduit API |
|
299 | 299 | |
|
300 | 300 | Call parameters are read from stdin as a JSON blob. Result will be written |
|
301 | 301 | to stdout as a JSON blob. |
|
302 | 302 | """ |
|
303 | 303 | # json.loads only accepts bytes from 3.6+ |
|
304 | 304 | rawparams = encoding.unifromlocal(ui.fin.read()) |
|
305 | 305 | # json.loads only returns unicode strings |
|
306 | 306 | params = pycompat.rapply( |
|
307 | 307 | lambda x: encoding.unitolocal(x) |
|
308 | 308 | if isinstance(x, pycompat.unicode) |
|
309 | 309 | else x, |
|
310 | 310 | json.loads(rawparams), |
|
311 | 311 | ) |
|
312 | 312 | # json.dumps only accepts unicode strings |
|
313 | 313 | result = pycompat.rapply( |
|
314 | 314 | lambda x: encoding.unifromlocal(x) if isinstance(x, bytes) else x, |
|
315 | 315 | callconduit(ui, name, params), |
|
316 | 316 | ) |
|
317 | 317 | s = json.dumps(result, sort_keys=True, indent=2, separators=(u',', u': ')) |
|
318 | 318 | ui.write(b'%s\n' % encoding.unitolocal(s)) |
|
319 | 319 | |
|
320 | 320 | |
|
321 | 321 | def getrepophid(repo): |
|
322 | 322 | """given callsign, return repository PHID or None""" |
|
323 | 323 | # developer config: phabricator.repophid |
|
324 | 324 | repophid = repo.ui.config(b'phabricator', b'repophid') |
|
325 | 325 | if repophid: |
|
326 | 326 | return repophid |
|
327 | 327 | callsign = repo.ui.config(b'phabricator', b'callsign') |
|
328 | 328 | if not callsign: |
|
329 | 329 | return None |
|
330 | 330 | query = callconduit( |
|
331 | 331 | repo.ui, |
|
332 | 332 | b'diffusion.repository.search', |
|
333 | 333 | {b'constraints': {b'callsigns': [callsign]}}, |
|
334 | 334 | ) |
|
335 | 335 | if len(query[b'data']) == 0: |
|
336 | 336 | return None |
|
337 | 337 | repophid = query[b'data'][0][b'phid'] |
|
338 | 338 | repo.ui.setconfig(b'phabricator', b'repophid', repophid) |
|
339 | 339 | return repophid |
|
340 | 340 | |
|
341 | 341 | |
|
342 | 342 | _differentialrevisiontagre = re.compile(br'\AD([1-9][0-9]*)\Z') |
|
343 | 343 | _differentialrevisiondescre = re.compile( |
|
344 | 344 | br'^Differential Revision:\s*(?P<url>(?:.*)D(?P<id>[1-9][0-9]*))$', re.M |
|
345 | 345 | ) |
|
346 | 346 | |
|
347 | 347 | |
|
348 | 348 | def getoldnodedrevmap(repo, nodelist): |
|
349 | 349 | """find previous nodes that has been sent to Phabricator |
|
350 | 350 | |
|
351 | 351 | return {node: (oldnode, Differential diff, Differential Revision ID)} |
|
352 | 352 | for node in nodelist with known previous sent versions, or associated |
|
353 | 353 | Differential Revision IDs. ``oldnode`` and ``Differential diff`` could |
|
354 | 354 | be ``None``. |
|
355 | 355 | |
|
356 | 356 | Examines commit messages like "Differential Revision:" to get the |
|
357 | 357 | association information. |
|
358 | 358 | |
|
359 | 359 | If such commit message line is not found, examines all precursors and their |
|
360 | 360 | tags. Tags with format like "D1234" are considered a match and the node |
|
361 | 361 | with that tag, and the number after "D" (ex. 1234) will be returned. |
|
362 | 362 | |
|
363 | 363 | The ``old node``, if not None, is guaranteed to be the last diff of |
|
364 | 364 | corresponding Differential Revision, and exist in the repo. |
|
365 | 365 | """ |
|
366 | 366 | unfi = repo.unfiltered() |
|
367 | 367 | nodemap = unfi.changelog.nodemap |
|
368 | 368 | |
|
369 | 369 | result = {} # {node: (oldnode?, lastdiff?, drev)} |
|
370 | 370 | toconfirm = {} # {node: (force, {precnode}, drev)} |
|
371 | 371 | for node in nodelist: |
|
372 | 372 | ctx = unfi[node] |
|
373 | 373 | # For tags like "D123", put them into "toconfirm" to verify later |
|
374 | 374 | precnodes = list(obsutil.allpredecessors(unfi.obsstore, [node])) |
|
375 | 375 | for n in precnodes: |
|
376 | 376 | if n in nodemap: |
|
377 | 377 | for tag in unfi.nodetags(n): |
|
378 | 378 | m = _differentialrevisiontagre.match(tag) |
|
379 | 379 | if m: |
|
380 | 380 | toconfirm[node] = (0, set(precnodes), int(m.group(1))) |
|
381 | 381 | continue |
|
382 | 382 | |
|
383 | 383 | # Check commit message |
|
384 | 384 | m = _differentialrevisiondescre.search(ctx.description()) |
|
385 | 385 | if m: |
|
386 | 386 | toconfirm[node] = (1, set(precnodes), int(m.group(r'id'))) |
|
387 | 387 | |
|
388 | 388 | # Double check if tags are genuine by collecting all old nodes from |
|
389 | 389 | # Phabricator, and expect precursors overlap with it. |
|
390 | 390 | if toconfirm: |
|
391 | 391 | drevs = [drev for force, precs, drev in toconfirm.values()] |
|
392 | 392 | alldiffs = callconduit( |
|
393 | 393 | unfi.ui, b'differential.querydiffs', {b'revisionIDs': drevs} |
|
394 | 394 | ) |
|
395 | 395 | getnode = lambda d: bin(getdiffmeta(d).get(b'node', b'')) or None |
|
396 | 396 | for newnode, (force, precset, drev) in toconfirm.items(): |
|
397 | 397 | diffs = [ |
|
398 | 398 | d for d in alldiffs.values() if int(d[b'revisionID']) == drev |
|
399 | 399 | ] |
|
400 | 400 | |
|
401 | 401 | # "precursors" as known by Phabricator |
|
402 | 402 | phprecset = set(getnode(d) for d in diffs) |
|
403 | 403 | |
|
404 | 404 | # Ignore if precursors (Phabricator and local repo) do not overlap, |
|
405 | 405 | # and force is not set (when commit message says nothing) |
|
406 | 406 | if not force and not bool(phprecset & precset): |
|
407 | 407 | tagname = b'D%d' % drev |
|
408 | 408 | tags.tag( |
|
409 | 409 | repo, |
|
410 | 410 | tagname, |
|
411 | 411 | nullid, |
|
412 | 412 | message=None, |
|
413 | 413 | user=None, |
|
414 | 414 | date=None, |
|
415 | 415 | local=True, |
|
416 | 416 | ) |
|
417 | 417 | unfi.ui.warn( |
|
418 | 418 | _( |
|
419 | 419 | b'D%s: local tag removed - does not match ' |
|
420 | 420 | b'Differential history\n' |
|
421 | 421 | ) |
|
422 | 422 | % drev |
|
423 | 423 | ) |
|
424 | 424 | continue |
|
425 | 425 | |
|
426 | 426 | # Find the last node using Phabricator metadata, and make sure it |
|
427 | 427 | # exists in the repo |
|
428 | 428 | oldnode = lastdiff = None |
|
429 | 429 | if diffs: |
|
430 | 430 | lastdiff = max(diffs, key=lambda d: int(d[b'id'])) |
|
431 | 431 | oldnode = getnode(lastdiff) |
|
432 | 432 | if oldnode and oldnode not in nodemap: |
|
433 | 433 | oldnode = None |
|
434 | 434 | |
|
435 | 435 | result[newnode] = (oldnode, lastdiff, drev) |
|
436 | 436 | |
|
437 | 437 | return result |
|
438 | 438 | |
|
439 | 439 | |
|
440 | 440 | def getdiff(ctx, diffopts): |
|
441 | 441 | """plain-text diff without header (user, commit message, etc)""" |
|
442 | 442 | output = util.stringio() |
|
443 | 443 | for chunk, _label in patch.diffui( |
|
444 | 444 | ctx.repo(), ctx.p1().node(), ctx.node(), None, opts=diffopts |
|
445 | 445 | ): |
|
446 | 446 | output.write(chunk) |
|
447 | 447 | return output.getvalue() |
|
448 | 448 | |
|
449 | 449 | |
|
450 | 450 | def creatediff(ctx): |
|
451 | 451 | """create a Differential Diff""" |
|
452 | 452 | repo = ctx.repo() |
|
453 | 453 | repophid = getrepophid(repo) |
|
454 | 454 | # Create a "Differential Diff" via "differential.createrawdiff" API |
|
455 | 455 | params = {b'diff': getdiff(ctx, mdiff.diffopts(git=True, context=32767))} |
|
456 | 456 | if repophid: |
|
457 | 457 | params[b'repositoryPHID'] = repophid |
|
458 | 458 | diff = callconduit(repo.ui, b'differential.createrawdiff', params) |
|
459 | 459 | if not diff: |
|
460 | 460 | raise error.Abort(_(b'cannot create diff for %s') % ctx) |
|
461 | 461 | return diff |
|
462 | 462 | |
|
463 | 463 | |
|
464 | 464 | def writediffproperties(ctx, diff): |
|
465 | 465 | """write metadata to diff so patches could be applied losslessly""" |
|
466 | 466 | params = { |
|
467 | 467 | b'diff_id': diff[b'id'], |
|
468 | 468 | b'name': b'hg:meta', |
|
469 | 469 | b'data': templatefilters.json( |
|
470 | 470 | { |
|
471 | 471 | b'user': ctx.user(), |
|
472 | 472 | b'date': b'%d %d' % ctx.date(), |
|
473 | 473 | b'branch': ctx.branch(), |
|
474 | 474 | b'node': ctx.hex(), |
|
475 | 475 | b'parent': ctx.p1().hex(), |
|
476 | 476 | } |
|
477 | 477 | ), |
|
478 | 478 | } |
|
479 | 479 | callconduit(ctx.repo().ui, b'differential.setdiffproperty', params) |
|
480 | 480 | |
|
481 | 481 | params = { |
|
482 | 482 | b'diff_id': diff[b'id'], |
|
483 | 483 | b'name': b'local:commits', |
|
484 | 484 | b'data': templatefilters.json( |
|
485 | 485 | { |
|
486 | 486 | ctx.hex(): { |
|
487 | 487 | b'author': stringutil.person(ctx.user()), |
|
488 | 488 | b'authorEmail': stringutil.email(ctx.user()), |
|
489 | 489 | b'time': int(ctx.date()[0]), |
|
490 | 490 | b'commit': ctx.hex(), |
|
491 | 491 | b'parents': [ctx.p1().hex()], |
|
492 | 492 | b'branch': ctx.branch(), |
|
493 | 493 | }, |
|
494 | 494 | } |
|
495 | 495 | ), |
|
496 | 496 | } |
|
497 | 497 | callconduit(ctx.repo().ui, b'differential.setdiffproperty', params) |
|
498 | 498 | |
|
499 | 499 | |
|
500 | 500 | def createdifferentialrevision( |
|
501 | 501 | ctx, |
|
502 | 502 | revid=None, |
|
503 | 503 | parentrevphid=None, |
|
504 | 504 | oldnode=None, |
|
505 | 505 | olddiff=None, |
|
506 | 506 | actions=None, |
|
507 | 507 | comment=None, |
|
508 | 508 | ): |
|
509 | 509 | """create or update a Differential Revision |
|
510 | 510 | |
|
511 | 511 | If revid is None, create a new Differential Revision, otherwise update |
|
512 | 512 | revid. If parentrevphid is not None, set it as a dependency. |
|
513 | 513 | |
|
514 | 514 | If oldnode is not None, check if the patch content (without commit message |
|
515 | 515 | and metadata) has changed before creating another diff. |
|
516 | 516 | |
|
517 | 517 | If actions is not None, they will be appended to the transaction. |
|
518 | 518 | """ |
|
519 | 519 | repo = ctx.repo() |
|
520 | 520 | if oldnode: |
|
521 | 521 | diffopts = mdiff.diffopts(git=True, context=32767) |
|
522 | 522 | oldctx = repo.unfiltered()[oldnode] |
|
523 | 523 | neednewdiff = getdiff(ctx, diffopts) != getdiff(oldctx, diffopts) |
|
524 | 524 | else: |
|
525 | 525 | neednewdiff = True |
|
526 | 526 | |
|
527 | 527 | transactions = [] |
|
528 | 528 | if neednewdiff: |
|
529 | 529 | diff = creatediff(ctx) |
|
530 | 530 | transactions.append({b'type': b'update', b'value': diff[b'phid']}) |
|
531 | 531 | if comment: |
|
532 | 532 | transactions.append({b'type': b'comment', b'value': comment}) |
|
533 | 533 | else: |
|
534 | 534 | # Even if we don't need to upload a new diff because the patch content |
|
535 | 535 | # does not change. We might still need to update its metadata so |
|
536 | 536 | # pushers could know the correct node metadata. |
|
537 | 537 | assert olddiff |
|
538 | 538 | diff = olddiff |
|
539 | 539 | writediffproperties(ctx, diff) |
|
540 | 540 | |
|
541 | 541 | # Set the parent Revision every time, so commit re-ordering is picked-up |
|
542 | 542 | if parentrevphid: |
|
543 | 543 | transactions.append( |
|
544 | 544 | {b'type': b'parents.set', b'value': [parentrevphid]} |
|
545 | 545 | ) |
|
546 | 546 | |
|
547 | 547 | if actions: |
|
548 | 548 | transactions += actions |
|
549 | 549 | |
|
550 | 550 | # Parse commit message and update related fields. |
|
551 | 551 | desc = ctx.description() |
|
552 | 552 | info = callconduit( |
|
553 | 553 | repo.ui, b'differential.parsecommitmessage', {b'corpus': desc} |
|
554 | 554 | ) |
|
555 | 555 | for k, v in info[b'fields'].items(): |
|
556 | 556 | if k in [b'title', b'summary', b'testPlan']: |
|
557 | 557 | transactions.append({b'type': k, b'value': v}) |
|
558 | 558 | |
|
559 | 559 | params = {b'transactions': transactions} |
|
560 | 560 | if revid is not None: |
|
561 | 561 | # Update an existing Differential Revision |
|
562 | 562 | params[b'objectIdentifier'] = revid |
|
563 | 563 | |
|
564 | 564 | revision = callconduit(repo.ui, b'differential.revision.edit', params) |
|
565 | 565 | if not revision: |
|
566 | 566 | raise error.Abort(_(b'cannot create revision for %s') % ctx) |
|
567 | 567 | |
|
568 | 568 | return revision, diff |
|
569 | 569 | |
|
570 | 570 | |
|
571 | 571 | def userphids(repo, names): |
|
572 | 572 | """convert user names to PHIDs""" |
|
573 | 573 | names = [name.lower() for name in names] |
|
574 | 574 | query = {b'constraints': {b'usernames': names}} |
|
575 | 575 | result = callconduit(repo.ui, b'user.search', query) |
|
576 | 576 | # username not found is not an error of the API. So check if we have missed |
|
577 | 577 | # some names here. |
|
578 | 578 | data = result[b'data'] |
|
579 | 579 | resolved = set(entry[b'fields'][b'username'].lower() for entry in data) |
|
580 | 580 | unresolved = set(names) - resolved |
|
581 | 581 | if unresolved: |
|
582 | 582 | raise error.Abort( |
|
583 | 583 | _(b'unknown username: %s') % b' '.join(sorted(unresolved)) |
|
584 | 584 | ) |
|
585 | 585 | return [entry[b'phid'] for entry in data] |
|
586 | 586 | |
|
587 | 587 | |
|
588 | 588 | @vcrcommand( |
|
589 | 589 | b'phabsend', |
|
590 | 590 | [ |
|
591 | 591 | (b'r', b'rev', [], _(b'revisions to send'), _(b'REV')), |
|
592 | 592 | (b'', b'amend', True, _(b'update commit messages')), |
|
593 | 593 | (b'', b'reviewer', [], _(b'specify reviewers')), |
|
594 | 594 | (b'', b'blocker', [], _(b'specify blocking reviewers')), |
|
595 | 595 | ( |
|
596 | 596 | b'm', |
|
597 | 597 | b'comment', |
|
598 | 598 | b'', |
|
599 | 599 | _(b'add a comment to Revisions with new/updated Diffs'), |
|
600 | 600 | ), |
|
601 | 601 | (b'', b'confirm', None, _(b'ask for confirmation before sending')), |
|
602 | 602 | ], |
|
603 | 603 | _(b'REV [OPTIONS]'), |
|
604 | 604 | helpcategory=command.CATEGORY_IMPORT_EXPORT, |
|
605 | 605 | ) |
|
606 | 606 | def phabsend(ui, repo, *revs, **opts): |
|
607 | 607 | """upload changesets to Phabricator |
|
608 | 608 | |
|
609 | 609 | If there are multiple revisions specified, they will be send as a stack |
|
610 | 610 | with a linear dependencies relationship using the order specified by the |
|
611 | 611 | revset. |
|
612 | 612 | |
|
613 | 613 | For the first time uploading changesets, local tags will be created to |
|
614 | 614 | maintain the association. After the first time, phabsend will check |
|
615 | 615 | obsstore and tags information so it can figure out whether to update an |
|
616 | 616 | existing Differential Revision, or create a new one. |
|
617 | 617 | |
|
618 | 618 | If --amend is set, update commit messages so they have the |
|
619 | 619 | ``Differential Revision`` URL, remove related tags. This is similar to what |
|
620 | 620 | arcanist will do, and is more desired in author-push workflows. Otherwise, |
|
621 | 621 | use local tags to record the ``Differential Revision`` association. |
|
622 | 622 | |
|
623 | 623 | The --confirm option lets you confirm changesets before sending them. You |
|
624 | 624 | can also add following to your configuration file to make it default |
|
625 | 625 | behaviour:: |
|
626 | 626 | |
|
627 | 627 | [phabsend] |
|
628 | 628 | confirm = true |
|
629 | 629 | |
|
630 | 630 | phabsend will check obsstore and the above association to decide whether to |
|
631 | 631 | update an existing Differential Revision, or create a new one. |
|
632 | 632 | """ |
|
633 | 633 | opts = pycompat.byteskwargs(opts) |
|
634 | 634 | revs = list(revs) + opts.get(b'rev', []) |
|
635 | 635 | revs = scmutil.revrange(repo, revs) |
|
636 | 636 | |
|
637 | 637 | if not revs: |
|
638 | 638 | raise error.Abort(_(b'phabsend requires at least one changeset')) |
|
639 | 639 | if opts.get(b'amend'): |
|
640 | 640 | cmdutil.checkunfinished(repo) |
|
641 | 641 | |
|
642 | 642 | # {newnode: (oldnode, olddiff, olddrev} |
|
643 | 643 | oldmap = getoldnodedrevmap(repo, [repo[r].node() for r in revs]) |
|
644 | 644 | |
|
645 | 645 | confirm = ui.configbool(b'phabsend', b'confirm') |
|
646 | 646 | confirm |= bool(opts.get(b'confirm')) |
|
647 | 647 | if confirm: |
|
648 | 648 | confirmed = _confirmbeforesend(repo, revs, oldmap) |
|
649 | 649 | if not confirmed: |
|
650 | 650 | raise error.Abort(_(b'phabsend cancelled')) |
|
651 | 651 | |
|
652 | 652 | actions = [] |
|
653 | 653 | reviewers = opts.get(b'reviewer', []) |
|
654 | 654 | blockers = opts.get(b'blocker', []) |
|
655 | 655 | phids = [] |
|
656 | 656 | if reviewers: |
|
657 | 657 | phids.extend(userphids(repo, reviewers)) |
|
658 | 658 | if blockers: |
|
659 | 659 | phids.extend( |
|
660 | 660 | map(lambda phid: b'blocking(%s)' % phid, userphids(repo, blockers)) |
|
661 | 661 | ) |
|
662 | 662 | if phids: |
|
663 | 663 | actions.append({b'type': b'reviewers.add', b'value': phids}) |
|
664 | 664 | |
|
665 | 665 | drevids = [] # [int] |
|
666 | 666 | diffmap = {} # {newnode: diff} |
|
667 | 667 | |
|
668 | 668 | # Send patches one by one so we know their Differential Revision PHIDs and |
|
669 | 669 | # can provide dependency relationship |
|
670 | 670 | lastrevphid = None |
|
671 | 671 | for rev in revs: |
|
672 | 672 | ui.debug(b'sending rev %d\n' % rev) |
|
673 | 673 | ctx = repo[rev] |
|
674 | 674 | |
|
675 | 675 | # Get Differential Revision ID |
|
676 | 676 | oldnode, olddiff, revid = oldmap.get(ctx.node(), (None, None, None)) |
|
677 | 677 | if oldnode != ctx.node() or opts.get(b'amend'): |
|
678 | 678 | # Create or update Differential Revision |
|
679 | 679 | revision, diff = createdifferentialrevision( |
|
680 | 680 | ctx, |
|
681 | 681 | revid, |
|
682 | 682 | lastrevphid, |
|
683 | 683 | oldnode, |
|
684 | 684 | olddiff, |
|
685 | 685 | actions, |
|
686 | 686 | opts.get(b'comment'), |
|
687 | 687 | ) |
|
688 | 688 | diffmap[ctx.node()] = diff |
|
689 | 689 | newrevid = int(revision[b'object'][b'id']) |
|
690 | 690 | newrevphid = revision[b'object'][b'phid'] |
|
691 | 691 | if revid: |
|
692 | 692 | action = b'updated' |
|
693 | 693 | else: |
|
694 | 694 | action = b'created' |
|
695 | 695 | |
|
696 | 696 | # Create a local tag to note the association, if commit message |
|
697 | 697 | # does not have it already |
|
698 | 698 | m = _differentialrevisiondescre.search(ctx.description()) |
|
699 | 699 | if not m or int(m.group(r'id')) != newrevid: |
|
700 | 700 | tagname = b'D%d' % newrevid |
|
701 | 701 | tags.tag( |
|
702 | 702 | repo, |
|
703 | 703 | tagname, |
|
704 | 704 | ctx.node(), |
|
705 | 705 | message=None, |
|
706 | 706 | user=None, |
|
707 | 707 | date=None, |
|
708 | 708 | local=True, |
|
709 | 709 | ) |
|
710 | 710 | else: |
|
711 | 711 | # Nothing changed. But still set "newrevphid" so the next revision |
|
712 | 712 | # could depend on this one and "newrevid" for the summary line. |
|
713 | 713 | newrevphid = querydrev(repo, b'%d' % revid)[0][b'phid'] |
|
714 | 714 | newrevid = revid |
|
715 | 715 | action = b'skipped' |
|
716 | 716 | |
|
717 | 717 | actiondesc = ui.label( |
|
718 | 718 | { |
|
719 | 719 | b'created': _(b'created'), |
|
720 | 720 | b'skipped': _(b'skipped'), |
|
721 | 721 | b'updated': _(b'updated'), |
|
722 | 722 | }[action], |
|
723 | 723 | b'phabricator.action.%s' % action, |
|
724 | 724 | ) |
|
725 | 725 | drevdesc = ui.label(b'D%d' % newrevid, b'phabricator.drev') |
|
726 | 726 | nodedesc = ui.label(bytes(ctx), b'phabricator.node') |
|
727 | 727 | desc = ui.label(ctx.description().split(b'\n')[0], b'phabricator.desc') |
|
728 | 728 | ui.write( |
|
729 | 729 | _(b'%s - %s - %s: %s\n') % (drevdesc, actiondesc, nodedesc, desc) |
|
730 | 730 | ) |
|
731 | 731 | drevids.append(newrevid) |
|
732 | 732 | lastrevphid = newrevphid |
|
733 | 733 | |
|
734 | 734 | # Update commit messages and remove tags |
|
735 | 735 | if opts.get(b'amend'): |
|
736 | 736 | unfi = repo.unfiltered() |
|
737 | 737 | drevs = callconduit(ui, b'differential.query', {b'ids': drevids}) |
|
738 | 738 | with repo.wlock(), repo.lock(), repo.transaction(b'phabsend'): |
|
739 | 739 | wnode = unfi[b'.'].node() |
|
740 | 740 | mapping = {} # {oldnode: [newnode]} |
|
741 | 741 | for i, rev in enumerate(revs): |
|
742 | 742 | old = unfi[rev] |
|
743 | 743 | drevid = drevids[i] |
|
744 | 744 | drev = [d for d in drevs if int(d[b'id']) == drevid][0] |
|
745 | 745 | newdesc = getdescfromdrev(drev) |
|
746 | 746 | # Make sure commit message contain "Differential Revision" |
|
747 | 747 | if old.description() != newdesc: |
|
748 | 748 | if old.phase() == phases.public: |
|
749 | 749 | ui.warn( |
|
750 | 750 | _(b"warning: not updating public commit %s\n") |
|
751 | 751 | % scmutil.formatchangeid(old) |
|
752 | 752 | ) |
|
753 | 753 | continue |
|
754 | 754 | parents = [ |
|
755 | 755 | mapping.get(old.p1().node(), (old.p1(),))[0], |
|
756 | 756 | mapping.get(old.p2().node(), (old.p2(),))[0], |
|
757 | 757 | ] |
|
758 | 758 | new = context.metadataonlyctx( |
|
759 | 759 | repo, |
|
760 | 760 | old, |
|
761 | 761 | parents=parents, |
|
762 | 762 | text=newdesc, |
|
763 | 763 | user=old.user(), |
|
764 | 764 | date=old.date(), |
|
765 | 765 | extra=old.extra(), |
|
766 | 766 | ) |
|
767 | 767 | |
|
768 | 768 | newnode = new.commit() |
|
769 | 769 | |
|
770 | 770 | mapping[old.node()] = [newnode] |
|
771 | 771 | # Update diff property |
|
772 | 772 | # If it fails just warn and keep going, otherwise the DREV |
|
773 | 773 | # associations will be lost |
|
774 | 774 | try: |
|
775 | 775 | writediffproperties(unfi[newnode], diffmap[old.node()]) |
|
776 | 776 | except util.urlerr.urlerror: |
|
777 | ui.warn(b'Failed to update metadata for D%s\n' % drevid) | |
|
777 | ui.warnnoi18n(b'Failed to update metadata for D%s\n' % drevid) | |
|
778 | 778 | # Remove local tags since it's no longer necessary |
|
779 | 779 | tagname = b'D%d' % drevid |
|
780 | 780 | if tagname in repo.tags(): |
|
781 | 781 | tags.tag( |
|
782 | 782 | repo, |
|
783 | 783 | tagname, |
|
784 | 784 | nullid, |
|
785 | 785 | message=None, |
|
786 | 786 | user=None, |
|
787 | 787 | date=None, |
|
788 | 788 | local=True, |
|
789 | 789 | ) |
|
790 | 790 | scmutil.cleanupnodes(repo, mapping, b'phabsend', fixphase=True) |
|
791 | 791 | if wnode in mapping: |
|
792 | 792 | unfi.setparents(mapping[wnode][0]) |
|
793 | 793 | |
|
794 | 794 | |
|
795 | 795 | # Map from "hg:meta" keys to header understood by "hg import". The order is |
|
796 | 796 | # consistent with "hg export" output. |
|
797 | 797 | _metanamemap = util.sortdict( |
|
798 | 798 | [ |
|
799 | 799 | (b'user', b'User'), |
|
800 | 800 | (b'date', b'Date'), |
|
801 | 801 | (b'branch', b'Branch'), |
|
802 | 802 | (b'node', b'Node ID'), |
|
803 | 803 | (b'parent', b'Parent '), |
|
804 | 804 | ] |
|
805 | 805 | ) |
|
806 | 806 | |
|
807 | 807 | |
|
808 | 808 | def _confirmbeforesend(repo, revs, oldmap): |
|
809 | 809 | url, token = readurltoken(repo.ui) |
|
810 | 810 | ui = repo.ui |
|
811 | 811 | for rev in revs: |
|
812 | 812 | ctx = repo[rev] |
|
813 | 813 | desc = ctx.description().splitlines()[0] |
|
814 | 814 | oldnode, olddiff, drevid = oldmap.get(ctx.node(), (None, None, None)) |
|
815 | 815 | if drevid: |
|
816 | 816 | drevdesc = ui.label(b'D%s' % drevid, b'phabricator.drev') |
|
817 | 817 | else: |
|
818 | 818 | drevdesc = ui.label(_(b'NEW'), b'phabricator.drev') |
|
819 | 819 | |
|
820 | 820 | ui.write( |
|
821 | 821 | _(b'%s - %s: %s\n') |
|
822 | 822 | % ( |
|
823 | 823 | drevdesc, |
|
824 | 824 | ui.label(bytes(ctx), b'phabricator.node'), |
|
825 | 825 | ui.label(desc, b'phabricator.desc'), |
|
826 | 826 | ) |
|
827 | 827 | ) |
|
828 | 828 | |
|
829 | 829 | if ui.promptchoice( |
|
830 | 830 | _(b'Send the above changes to %s (yn)?' b'$$ &Yes $$ &No') % url |
|
831 | 831 | ): |
|
832 | 832 | return False |
|
833 | 833 | |
|
834 | 834 | return True |
|
835 | 835 | |
|
836 | 836 | |
|
837 | 837 | _knownstatusnames = { |
|
838 | 838 | b'accepted', |
|
839 | 839 | b'needsreview', |
|
840 | 840 | b'needsrevision', |
|
841 | 841 | b'closed', |
|
842 | 842 | b'abandoned', |
|
843 | 843 | } |
|
844 | 844 | |
|
845 | 845 | |
|
846 | 846 | def _getstatusname(drev): |
|
847 | 847 | """get normalized status name from a Differential Revision""" |
|
848 | 848 | return drev[b'statusName'].replace(b' ', b'').lower() |
|
849 | 849 | |
|
850 | 850 | |
|
851 | 851 | # Small language to specify differential revisions. Support symbols: (), :X, |
|
852 | 852 | # +, and -. |
|
853 | 853 | |
|
854 | 854 | _elements = { |
|
855 | 855 | # token-type: binding-strength, primary, prefix, infix, suffix |
|
856 | 856 | b'(': (12, None, (b'group', 1, b')'), None, None), |
|
857 | 857 | b':': (8, None, (b'ancestors', 8), None, None), |
|
858 | 858 | b'&': (5, None, None, (b'and_', 5), None), |
|
859 | 859 | b'+': (4, None, None, (b'add', 4), None), |
|
860 | 860 | b'-': (4, None, None, (b'sub', 4), None), |
|
861 | 861 | b')': (0, None, None, None, None), |
|
862 | 862 | b'symbol': (0, b'symbol', None, None, None), |
|
863 | 863 | b'end': (0, None, None, None, None), |
|
864 | 864 | } |
|
865 | 865 | |
|
866 | 866 | |
|
867 | 867 | def _tokenize(text): |
|
868 | 868 | view = memoryview(text) # zero-copy slice |
|
869 | 869 | special = b'():+-& ' |
|
870 | 870 | pos = 0 |
|
871 | 871 | length = len(text) |
|
872 | 872 | while pos < length: |
|
873 | 873 | symbol = b''.join( |
|
874 | 874 | itertools.takewhile( |
|
875 | 875 | lambda ch: ch not in special, pycompat.iterbytestr(view[pos:]) |
|
876 | 876 | ) |
|
877 | 877 | ) |
|
878 | 878 | if symbol: |
|
879 | 879 | yield (b'symbol', symbol, pos) |
|
880 | 880 | pos += len(symbol) |
|
881 | 881 | else: # special char, ignore space |
|
882 | 882 | if text[pos] != b' ': |
|
883 | 883 | yield (text[pos], None, pos) |
|
884 | 884 | pos += 1 |
|
885 | 885 | yield (b'end', None, pos) |
|
886 | 886 | |
|
887 | 887 | |
|
888 | 888 | def _parse(text): |
|
889 | 889 | tree, pos = parser.parser(_elements).parse(_tokenize(text)) |
|
890 | 890 | if pos != len(text): |
|
891 | 891 | raise error.ParseError(b'invalid token', pos) |
|
892 | 892 | return tree |
|
893 | 893 | |
|
894 | 894 | |
|
895 | 895 | def _parsedrev(symbol): |
|
896 | 896 | """str -> int or None, ex. 'D45' -> 45; '12' -> 12; 'x' -> None""" |
|
897 | 897 | if symbol.startswith(b'D') and symbol[1:].isdigit(): |
|
898 | 898 | return int(symbol[1:]) |
|
899 | 899 | if symbol.isdigit(): |
|
900 | 900 | return int(symbol) |
|
901 | 901 | |
|
902 | 902 | |
|
903 | 903 | def _prefetchdrevs(tree): |
|
904 | 904 | """return ({single-drev-id}, {ancestor-drev-id}) to prefetch""" |
|
905 | 905 | drevs = set() |
|
906 | 906 | ancestordrevs = set() |
|
907 | 907 | op = tree[0] |
|
908 | 908 | if op == b'symbol': |
|
909 | 909 | r = _parsedrev(tree[1]) |
|
910 | 910 | if r: |
|
911 | 911 | drevs.add(r) |
|
912 | 912 | elif op == b'ancestors': |
|
913 | 913 | r, a = _prefetchdrevs(tree[1]) |
|
914 | 914 | drevs.update(r) |
|
915 | 915 | ancestordrevs.update(r) |
|
916 | 916 | ancestordrevs.update(a) |
|
917 | 917 | else: |
|
918 | 918 | for t in tree[1:]: |
|
919 | 919 | r, a = _prefetchdrevs(t) |
|
920 | 920 | drevs.update(r) |
|
921 | 921 | ancestordrevs.update(a) |
|
922 | 922 | return drevs, ancestordrevs |
|
923 | 923 | |
|
924 | 924 | |
|
925 | 925 | def querydrev(repo, spec): |
|
926 | 926 | """return a list of "Differential Revision" dicts |
|
927 | 927 | |
|
928 | 928 | spec is a string using a simple query language, see docstring in phabread |
|
929 | 929 | for details. |
|
930 | 930 | |
|
931 | 931 | A "Differential Revision dict" looks like: |
|
932 | 932 | |
|
933 | 933 | { |
|
934 | 934 | "id": "2", |
|
935 | 935 | "phid": "PHID-DREV-672qvysjcczopag46qty", |
|
936 | 936 | "title": "example", |
|
937 | 937 | "uri": "https://phab.example.com/D2", |
|
938 | 938 | "dateCreated": "1499181406", |
|
939 | 939 | "dateModified": "1499182103", |
|
940 | 940 | "authorPHID": "PHID-USER-tv3ohwc4v4jeu34otlye", |
|
941 | 941 | "status": "0", |
|
942 | 942 | "statusName": "Needs Review", |
|
943 | 943 | "properties": [], |
|
944 | 944 | "branch": null, |
|
945 | 945 | "summary": "", |
|
946 | 946 | "testPlan": "", |
|
947 | 947 | "lineCount": "2", |
|
948 | 948 | "activeDiffPHID": "PHID-DIFF-xoqnjkobbm6k4dk6hi72", |
|
949 | 949 | "diffs": [ |
|
950 | 950 | "3", |
|
951 | 951 | "4", |
|
952 | 952 | ], |
|
953 | 953 | "commits": [], |
|
954 | 954 | "reviewers": [], |
|
955 | 955 | "ccs": [], |
|
956 | 956 | "hashes": [], |
|
957 | 957 | "auxiliary": { |
|
958 | 958 | "phabricator:projects": [], |
|
959 | 959 | "phabricator:depends-on": [ |
|
960 | 960 | "PHID-DREV-gbapp366kutjebt7agcd" |
|
961 | 961 | ] |
|
962 | 962 | }, |
|
963 | 963 | "repositoryPHID": "PHID-REPO-hub2hx62ieuqeheznasv", |
|
964 | 964 | "sourcePath": null |
|
965 | 965 | } |
|
966 | 966 | """ |
|
967 | 967 | |
|
968 | 968 | def fetch(params): |
|
969 | 969 | """params -> single drev or None""" |
|
970 | 970 | key = (params.get(b'ids') or params.get(b'phids') or [None])[0] |
|
971 | 971 | if key in prefetched: |
|
972 | 972 | return prefetched[key] |
|
973 | 973 | drevs = callconduit(repo.ui, b'differential.query', params) |
|
974 | 974 | # Fill prefetched with the result |
|
975 | 975 | for drev in drevs: |
|
976 | 976 | prefetched[drev[b'phid']] = drev |
|
977 | 977 | prefetched[int(drev[b'id'])] = drev |
|
978 | 978 | if key not in prefetched: |
|
979 | 979 | raise error.Abort( |
|
980 | 980 | _(b'cannot get Differential Revision %r') % params |
|
981 | 981 | ) |
|
982 | 982 | return prefetched[key] |
|
983 | 983 | |
|
984 | 984 | def getstack(topdrevids): |
|
985 | 985 | """given a top, get a stack from the bottom, [id] -> [id]""" |
|
986 | 986 | visited = set() |
|
987 | 987 | result = [] |
|
988 | 988 | queue = [{b'ids': [i]} for i in topdrevids] |
|
989 | 989 | while queue: |
|
990 | 990 | params = queue.pop() |
|
991 | 991 | drev = fetch(params) |
|
992 | 992 | if drev[b'id'] in visited: |
|
993 | 993 | continue |
|
994 | 994 | visited.add(drev[b'id']) |
|
995 | 995 | result.append(int(drev[b'id'])) |
|
996 | 996 | auxiliary = drev.get(b'auxiliary', {}) |
|
997 | 997 | depends = auxiliary.get(b'phabricator:depends-on', []) |
|
998 | 998 | for phid in depends: |
|
999 | 999 | queue.append({b'phids': [phid]}) |
|
1000 | 1000 | result.reverse() |
|
1001 | 1001 | return smartset.baseset(result) |
|
1002 | 1002 | |
|
1003 | 1003 | # Initialize prefetch cache |
|
1004 | 1004 | prefetched = {} # {id or phid: drev} |
|
1005 | 1005 | |
|
1006 | 1006 | tree = _parse(spec) |
|
1007 | 1007 | drevs, ancestordrevs = _prefetchdrevs(tree) |
|
1008 | 1008 | |
|
1009 | 1009 | # developer config: phabricator.batchsize |
|
1010 | 1010 | batchsize = repo.ui.configint(b'phabricator', b'batchsize') |
|
1011 | 1011 | |
|
1012 | 1012 | # Prefetch Differential Revisions in batch |
|
1013 | 1013 | tofetch = set(drevs) |
|
1014 | 1014 | for r in ancestordrevs: |
|
1015 | 1015 | tofetch.update(range(max(1, r - batchsize), r + 1)) |
|
1016 | 1016 | if drevs: |
|
1017 | 1017 | fetch({b'ids': list(tofetch)}) |
|
1018 | 1018 | validids = sorted(set(getstack(list(ancestordrevs))) | set(drevs)) |
|
1019 | 1019 | |
|
1020 | 1020 | # Walk through the tree, return smartsets |
|
1021 | 1021 | def walk(tree): |
|
1022 | 1022 | op = tree[0] |
|
1023 | 1023 | if op == b'symbol': |
|
1024 | 1024 | drev = _parsedrev(tree[1]) |
|
1025 | 1025 | if drev: |
|
1026 | 1026 | return smartset.baseset([drev]) |
|
1027 | 1027 | elif tree[1] in _knownstatusnames: |
|
1028 | 1028 | drevs = [ |
|
1029 | 1029 | r |
|
1030 | 1030 | for r in validids |
|
1031 | 1031 | if _getstatusname(prefetched[r]) == tree[1] |
|
1032 | 1032 | ] |
|
1033 | 1033 | return smartset.baseset(drevs) |
|
1034 | 1034 | else: |
|
1035 | 1035 | raise error.Abort(_(b'unknown symbol: %s') % tree[1]) |
|
1036 | 1036 | elif op in {b'and_', b'add', b'sub'}: |
|
1037 | 1037 | assert len(tree) == 3 |
|
1038 | 1038 | return getattr(operator, op)(walk(tree[1]), walk(tree[2])) |
|
1039 | 1039 | elif op == b'group': |
|
1040 | 1040 | return walk(tree[1]) |
|
1041 | 1041 | elif op == b'ancestors': |
|
1042 | 1042 | return getstack(walk(tree[1])) |
|
1043 | 1043 | else: |
|
1044 | 1044 | raise error.ProgrammingError(b'illegal tree: %r' % tree) |
|
1045 | 1045 | |
|
1046 | 1046 | return [prefetched[r] for r in walk(tree)] |
|
1047 | 1047 | |
|
1048 | 1048 | |
|
1049 | 1049 | def getdescfromdrev(drev): |
|
1050 | 1050 | """get description (commit message) from "Differential Revision" |
|
1051 | 1051 | |
|
1052 | 1052 | This is similar to differential.getcommitmessage API. But we only care |
|
1053 | 1053 | about limited fields: title, summary, test plan, and URL. |
|
1054 | 1054 | """ |
|
1055 | 1055 | title = drev[b'title'] |
|
1056 | 1056 | summary = drev[b'summary'].rstrip() |
|
1057 | 1057 | testplan = drev[b'testPlan'].rstrip() |
|
1058 | 1058 | if testplan: |
|
1059 | 1059 | testplan = b'Test Plan:\n%s' % testplan |
|
1060 | 1060 | uri = b'Differential Revision: %s' % drev[b'uri'] |
|
1061 | 1061 | return b'\n\n'.join(filter(None, [title, summary, testplan, uri])) |
|
1062 | 1062 | |
|
1063 | 1063 | |
|
1064 | 1064 | def getdiffmeta(diff): |
|
1065 | 1065 | """get commit metadata (date, node, user, p1) from a diff object |
|
1066 | 1066 | |
|
1067 | 1067 | The metadata could be "hg:meta", sent by phabsend, like: |
|
1068 | 1068 | |
|
1069 | 1069 | "properties": { |
|
1070 | 1070 | "hg:meta": { |
|
1071 | 1071 | "date": "1499571514 25200", |
|
1072 | 1072 | "node": "98c08acae292b2faf60a279b4189beb6cff1414d", |
|
1073 | 1073 | "user": "Foo Bar <foo@example.com>", |
|
1074 | 1074 | "parent": "6d0abad76b30e4724a37ab8721d630394070fe16" |
|
1075 | 1075 | } |
|
1076 | 1076 | } |
|
1077 | 1077 | |
|
1078 | 1078 | Or converted from "local:commits", sent by "arc", like: |
|
1079 | 1079 | |
|
1080 | 1080 | "properties": { |
|
1081 | 1081 | "local:commits": { |
|
1082 | 1082 | "98c08acae292b2faf60a279b4189beb6cff1414d": { |
|
1083 | 1083 | "author": "Foo Bar", |
|
1084 | 1084 | "time": 1499546314, |
|
1085 | 1085 | "branch": "default", |
|
1086 | 1086 | "tag": "", |
|
1087 | 1087 | "commit": "98c08acae292b2faf60a279b4189beb6cff1414d", |
|
1088 | 1088 | "rev": "98c08acae292b2faf60a279b4189beb6cff1414d", |
|
1089 | 1089 | "local": "1000", |
|
1090 | 1090 | "parents": ["6d0abad76b30e4724a37ab8721d630394070fe16"], |
|
1091 | 1091 | "summary": "...", |
|
1092 | 1092 | "message": "...", |
|
1093 | 1093 | "authorEmail": "foo@example.com" |
|
1094 | 1094 | } |
|
1095 | 1095 | } |
|
1096 | 1096 | } |
|
1097 | 1097 | |
|
1098 | 1098 | Note: metadata extracted from "local:commits" will lose time zone |
|
1099 | 1099 | information. |
|
1100 | 1100 | """ |
|
1101 | 1101 | props = diff.get(b'properties') or {} |
|
1102 | 1102 | meta = props.get(b'hg:meta') |
|
1103 | 1103 | if not meta: |
|
1104 | 1104 | if props.get(b'local:commits'): |
|
1105 | 1105 | commit = sorted(props[b'local:commits'].values())[0] |
|
1106 | 1106 | meta = {} |
|
1107 | 1107 | if b'author' in commit and b'authorEmail' in commit: |
|
1108 | 1108 | meta[b'user'] = b'%s <%s>' % ( |
|
1109 | 1109 | commit[b'author'], |
|
1110 | 1110 | commit[b'authorEmail'], |
|
1111 | 1111 | ) |
|
1112 | 1112 | if b'time' in commit: |
|
1113 | 1113 | meta[b'date'] = b'%d 0' % int(commit[b'time']) |
|
1114 | 1114 | if b'branch' in commit: |
|
1115 | 1115 | meta[b'branch'] = commit[b'branch'] |
|
1116 | 1116 | node = commit.get(b'commit', commit.get(b'rev')) |
|
1117 | 1117 | if node: |
|
1118 | 1118 | meta[b'node'] = node |
|
1119 | 1119 | if len(commit.get(b'parents', ())) >= 1: |
|
1120 | 1120 | meta[b'parent'] = commit[b'parents'][0] |
|
1121 | 1121 | else: |
|
1122 | 1122 | meta = {} |
|
1123 | 1123 | if b'date' not in meta and b'dateCreated' in diff: |
|
1124 | 1124 | meta[b'date'] = b'%s 0' % diff[b'dateCreated'] |
|
1125 | 1125 | if b'branch' not in meta and diff.get(b'branch'): |
|
1126 | 1126 | meta[b'branch'] = diff[b'branch'] |
|
1127 | 1127 | if b'parent' not in meta and diff.get(b'sourceControlBaseRevision'): |
|
1128 | 1128 | meta[b'parent'] = diff[b'sourceControlBaseRevision'] |
|
1129 | 1129 | return meta |
|
1130 | 1130 | |
|
1131 | 1131 | |
|
1132 | 1132 | def readpatch(repo, drevs, write): |
|
1133 | 1133 | """generate plain-text patch readable by 'hg import' |
|
1134 | 1134 | |
|
1135 | 1135 | write is usually ui.write. drevs is what "querydrev" returns, results of |
|
1136 | 1136 | "differential.query". |
|
1137 | 1137 | """ |
|
1138 | 1138 | # Prefetch hg:meta property for all diffs |
|
1139 | 1139 | diffids = sorted(set(max(int(v) for v in drev[b'diffs']) for drev in drevs)) |
|
1140 | 1140 | diffs = callconduit(repo.ui, b'differential.querydiffs', {b'ids': diffids}) |
|
1141 | 1141 | |
|
1142 | 1142 | # Generate patch for each drev |
|
1143 | 1143 | for drev in drevs: |
|
1144 | 1144 | repo.ui.note(_(b'reading D%s\n') % drev[b'id']) |
|
1145 | 1145 | |
|
1146 | 1146 | diffid = max(int(v) for v in drev[b'diffs']) |
|
1147 | 1147 | body = callconduit( |
|
1148 | 1148 | repo.ui, b'differential.getrawdiff', {b'diffID': diffid} |
|
1149 | 1149 | ) |
|
1150 | 1150 | desc = getdescfromdrev(drev) |
|
1151 | 1151 | header = b'# HG changeset patch\n' |
|
1152 | 1152 | |
|
1153 | 1153 | # Try to preserve metadata from hg:meta property. Write hg patch |
|
1154 | 1154 | # headers that can be read by the "import" command. See patchheadermap |
|
1155 | 1155 | # and extract in mercurial/patch.py for supported headers. |
|
1156 | 1156 | meta = getdiffmeta(diffs[b'%d' % diffid]) |
|
1157 | 1157 | for k in _metanamemap.keys(): |
|
1158 | 1158 | if k in meta: |
|
1159 | 1159 | header += b'# %s %s\n' % (_metanamemap[k], meta[k]) |
|
1160 | 1160 | |
|
1161 | 1161 | content = b'%s%s\n%s' % (header, desc, body) |
|
1162 | 1162 | write(content) |
|
1163 | 1163 | |
|
1164 | 1164 | |
|
1165 | 1165 | @vcrcommand( |
|
1166 | 1166 | b'phabread', |
|
1167 | 1167 | [(b'', b'stack', False, _(b'read dependencies'))], |
|
1168 | 1168 | _(b'DREVSPEC [OPTIONS]'), |
|
1169 | 1169 | helpcategory=command.CATEGORY_IMPORT_EXPORT, |
|
1170 | 1170 | ) |
|
1171 | 1171 | def phabread(ui, repo, spec, **opts): |
|
1172 | 1172 | """print patches from Phabricator suitable for importing |
|
1173 | 1173 | |
|
1174 | 1174 | DREVSPEC could be a Differential Revision identity, like ``D123``, or just |
|
1175 | 1175 | the number ``123``. It could also have common operators like ``+``, ``-``, |
|
1176 | 1176 | ``&``, ``(``, ``)`` for complex queries. Prefix ``:`` could be used to |
|
1177 | 1177 | select a stack. |
|
1178 | 1178 | |
|
1179 | 1179 | ``abandoned``, ``accepted``, ``closed``, ``needsreview``, ``needsrevision`` |
|
1180 | 1180 | could be used to filter patches by status. For performance reason, they |
|
1181 | 1181 | only represent a subset of non-status selections and cannot be used alone. |
|
1182 | 1182 | |
|
1183 | 1183 | For example, ``:D6+8-(2+D4)`` selects a stack up to D6, plus D8 and exclude |
|
1184 | 1184 | D2 and D4. ``:D9 & needsreview`` selects "Needs Review" revisions in a |
|
1185 | 1185 | stack up to D9. |
|
1186 | 1186 | |
|
1187 | 1187 | If --stack is given, follow dependencies information and read all patches. |
|
1188 | 1188 | It is equivalent to the ``:`` operator. |
|
1189 | 1189 | """ |
|
1190 | 1190 | opts = pycompat.byteskwargs(opts) |
|
1191 | 1191 | if opts.get(b'stack'): |
|
1192 | 1192 | spec = b':(%s)' % spec |
|
1193 | 1193 | drevs = querydrev(repo, spec) |
|
1194 | 1194 | readpatch(repo, drevs, ui.write) |
|
1195 | 1195 | |
|
1196 | 1196 | |
|
1197 | 1197 | @vcrcommand( |
|
1198 | 1198 | b'phabupdate', |
|
1199 | 1199 | [ |
|
1200 | 1200 | (b'', b'accept', False, _(b'accept revisions')), |
|
1201 | 1201 | (b'', b'reject', False, _(b'reject revisions')), |
|
1202 | 1202 | (b'', b'abandon', False, _(b'abandon revisions')), |
|
1203 | 1203 | (b'', b'reclaim', False, _(b'reclaim revisions')), |
|
1204 | 1204 | (b'm', b'comment', b'', _(b'comment on the last revision')), |
|
1205 | 1205 | ], |
|
1206 | 1206 | _(b'DREVSPEC [OPTIONS]'), |
|
1207 | 1207 | helpcategory=command.CATEGORY_IMPORT_EXPORT, |
|
1208 | 1208 | ) |
|
1209 | 1209 | def phabupdate(ui, repo, spec, **opts): |
|
1210 | 1210 | """update Differential Revision in batch |
|
1211 | 1211 | |
|
1212 | 1212 | DREVSPEC selects revisions. See :hg:`help phabread` for its usage. |
|
1213 | 1213 | """ |
|
1214 | 1214 | opts = pycompat.byteskwargs(opts) |
|
1215 | 1215 | flags = [n for n in b'accept reject abandon reclaim'.split() if opts.get(n)] |
|
1216 | 1216 | if len(flags) > 1: |
|
1217 | 1217 | raise error.Abort(_(b'%s cannot be used together') % b', '.join(flags)) |
|
1218 | 1218 | |
|
1219 | 1219 | actions = [] |
|
1220 | 1220 | for f in flags: |
|
1221 | 1221 | actions.append({b'type': f, b'value': b'true'}) |
|
1222 | 1222 | |
|
1223 | 1223 | drevs = querydrev(repo, spec) |
|
1224 | 1224 | for i, drev in enumerate(drevs): |
|
1225 | 1225 | if i + 1 == len(drevs) and opts.get(b'comment'): |
|
1226 | 1226 | actions.append({b'type': b'comment', b'value': opts[b'comment']}) |
|
1227 | 1227 | if actions: |
|
1228 | 1228 | params = { |
|
1229 | 1229 | b'objectIdentifier': drev[b'phid'], |
|
1230 | 1230 | b'transactions': actions, |
|
1231 | 1231 | } |
|
1232 | 1232 | callconduit(ui, b'differential.revision.edit', params) |
|
1233 | 1233 | |
|
1234 | 1234 | |
|
1235 | 1235 | @eh.templatekeyword(b'phabreview', requires={b'ctx'}) |
|
1236 | 1236 | def template_review(context, mapping): |
|
1237 | 1237 | """:phabreview: Object describing the review for this changeset. |
|
1238 | 1238 | Has attributes `url` and `id`. |
|
1239 | 1239 | """ |
|
1240 | 1240 | ctx = context.resource(mapping, b'ctx') |
|
1241 | 1241 | m = _differentialrevisiondescre.search(ctx.description()) |
|
1242 | 1242 | if m: |
|
1243 | 1243 | return templateutil.hybriddict( |
|
1244 | 1244 | {b'url': m.group(r'url'), b'id': b"D%s" % m.group(r'id'),} |
|
1245 | 1245 | ) |
|
1246 | 1246 | else: |
|
1247 | 1247 | tags = ctx.repo().nodetags(ctx.node()) |
|
1248 | 1248 | for t in tags: |
|
1249 | 1249 | if _differentialrevisiontagre.match(t): |
|
1250 | 1250 | url = ctx.repo().ui.config(b'phabricator', b'url') |
|
1251 | 1251 | if not url.endswith(b'/'): |
|
1252 | 1252 | url += b'/' |
|
1253 | 1253 | url += t |
|
1254 | 1254 | |
|
1255 | 1255 | return templateutil.hybriddict({b'url': url, b'id': t,}) |
|
1256 | 1256 | return None |
@@ -1,476 +1,476 b'' | |||
|
1 | 1 | # debugcommands.py - debug logic for remotefilelog |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2013 Facebook, Inc. |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | from __future__ import absolute_import |
|
8 | 8 | |
|
9 | 9 | import hashlib |
|
10 | 10 | import os |
|
11 | 11 | import zlib |
|
12 | 12 | |
|
13 | 13 | from mercurial.node import bin, hex, nullid, short |
|
14 | 14 | from mercurial.i18n import _ |
|
15 | 15 | from mercurial import ( |
|
16 | 16 | error, |
|
17 | 17 | filelog, |
|
18 | 18 | lock as lockmod, |
|
19 | 19 | node as nodemod, |
|
20 | 20 | pycompat, |
|
21 | 21 | revlog, |
|
22 | 22 | ) |
|
23 | 23 | from . import ( |
|
24 | 24 | constants, |
|
25 | 25 | datapack, |
|
26 | 26 | fileserverclient, |
|
27 | 27 | historypack, |
|
28 | 28 | repack, |
|
29 | 29 | shallowutil, |
|
30 | 30 | ) |
|
31 | 31 | |
|
32 | 32 | |
|
33 | 33 | def debugremotefilelog(ui, path, **opts): |
|
34 | 34 | decompress = opts.get(r'decompress') |
|
35 | 35 | |
|
36 | 36 | size, firstnode, mapping = parsefileblob(path, decompress) |
|
37 | 37 | |
|
38 | 38 | ui.status(_(b"size: %d bytes\n") % size) |
|
39 | 39 | ui.status(_(b"path: %s \n") % path) |
|
40 | 40 | ui.status(_(b"key: %s \n") % (short(firstnode))) |
|
41 | 41 | ui.status(_(b"\n")) |
|
42 | 42 | ui.status( |
|
43 | 43 | _(b"%12s => %12s %13s %13s %12s\n") |
|
44 | 44 | % (b"node", b"p1", b"p2", b"linknode", b"copyfrom") |
|
45 | 45 | ) |
|
46 | 46 | |
|
47 | 47 | queue = [firstnode] |
|
48 | 48 | while queue: |
|
49 | 49 | node = queue.pop(0) |
|
50 | 50 | p1, p2, linknode, copyfrom = mapping[node] |
|
51 | 51 | ui.status( |
|
52 | 52 | _(b"%s => %s %s %s %s\n") |
|
53 | 53 | % (short(node), short(p1), short(p2), short(linknode), copyfrom) |
|
54 | 54 | ) |
|
55 | 55 | if p1 != nullid: |
|
56 | 56 | queue.append(p1) |
|
57 | 57 | if p2 != nullid: |
|
58 | 58 | queue.append(p2) |
|
59 | 59 | |
|
60 | 60 | |
|
61 | 61 | def buildtemprevlog(repo, file): |
|
62 | 62 | # get filename key |
|
63 | 63 | filekey = nodemod.hex(hashlib.sha1(file).digest()) |
|
64 | 64 | filedir = os.path.join(repo.path, b'store/data', filekey) |
|
65 | 65 | |
|
66 | 66 | # sort all entries based on linkrev |
|
67 | 67 | fctxs = [] |
|
68 | 68 | for filenode in os.listdir(filedir): |
|
69 | 69 | if b'_old' not in filenode: |
|
70 | 70 | fctxs.append(repo.filectx(file, fileid=bin(filenode))) |
|
71 | 71 | |
|
72 | 72 | fctxs = sorted(fctxs, key=lambda x: x.linkrev()) |
|
73 | 73 | |
|
74 | 74 | # add to revlog |
|
75 | 75 | temppath = repo.sjoin(b'data/temprevlog.i') |
|
76 | 76 | if os.path.exists(temppath): |
|
77 | 77 | os.remove(temppath) |
|
78 | 78 | r = filelog.filelog(repo.svfs, b'temprevlog') |
|
79 | 79 | |
|
80 | 80 | class faket(object): |
|
81 | 81 | def add(self, a, b, c): |
|
82 | 82 | pass |
|
83 | 83 | |
|
84 | 84 | t = faket() |
|
85 | 85 | for fctx in fctxs: |
|
86 | 86 | if fctx.node() not in repo: |
|
87 | 87 | continue |
|
88 | 88 | |
|
89 | 89 | p = fctx.filelog().parents(fctx.filenode()) |
|
90 | 90 | meta = {} |
|
91 | 91 | if fctx.renamed(): |
|
92 | 92 | meta[b'copy'] = fctx.renamed()[0] |
|
93 | 93 | meta[b'copyrev'] = hex(fctx.renamed()[1]) |
|
94 | 94 | |
|
95 | 95 | r.add(fctx.data(), meta, t, fctx.linkrev(), p[0], p[1]) |
|
96 | 96 | |
|
97 | 97 | return r |
|
98 | 98 | |
|
99 | 99 | |
|
100 | 100 | def debugindex(orig, ui, repo, file_=None, **opts): |
|
101 | 101 | """dump the contents of an index file""" |
|
102 | 102 | if ( |
|
103 | 103 | opts.get(r'changelog') |
|
104 | 104 | or opts.get(r'manifest') |
|
105 | 105 | or opts.get(r'dir') |
|
106 | 106 | or not shallowutil.isenabled(repo) |
|
107 | 107 | or not repo.shallowmatch(file_) |
|
108 | 108 | ): |
|
109 | 109 | return orig(ui, repo, file_, **opts) |
|
110 | 110 | |
|
111 | 111 | r = buildtemprevlog(repo, file_) |
|
112 | 112 | |
|
113 | 113 | # debugindex like normal |
|
114 | 114 | format = opts.get(b'format', 0) |
|
115 | 115 | if format not in (0, 1): |
|
116 | 116 | raise error.Abort(_(b"unknown format %d") % format) |
|
117 | 117 | |
|
118 | 118 | generaldelta = r.version & revlog.FLAG_GENERALDELTA |
|
119 | 119 | if generaldelta: |
|
120 | 120 | basehdr = b' delta' |
|
121 | 121 | else: |
|
122 | 122 | basehdr = b' base' |
|
123 | 123 | |
|
124 | 124 | if format == 0: |
|
125 | 125 | ui.write( |
|
126 | 126 | ( |
|
127 | 127 | b" rev offset length " + basehdr + b" linkrev" |
|
128 | 128 | b" nodeid p1 p2\n" |
|
129 | 129 | ) |
|
130 | 130 | ) |
|
131 | 131 | elif format == 1: |
|
132 | 132 | ui.write( |
|
133 | 133 | ( |
|
134 | 134 | b" rev flag offset length" |
|
135 | 135 | b" size " + basehdr + b" link p1 p2" |
|
136 | 136 | b" nodeid\n" |
|
137 | 137 | ) |
|
138 | 138 | ) |
|
139 | 139 | |
|
140 | 140 | for i in r: |
|
141 | 141 | node = r.node(i) |
|
142 | 142 | if generaldelta: |
|
143 | 143 | base = r.deltaparent(i) |
|
144 | 144 | else: |
|
145 | 145 | base = r.chainbase(i) |
|
146 | 146 | if format == 0: |
|
147 | 147 | try: |
|
148 | 148 | pp = r.parents(node) |
|
149 | 149 | except Exception: |
|
150 | 150 | pp = [nullid, nullid] |
|
151 | 151 | ui.write( |
|
152 | 152 | b"% 6d % 9d % 7d % 6d % 7d %s %s %s\n" |
|
153 | 153 | % ( |
|
154 | 154 | i, |
|
155 | 155 | r.start(i), |
|
156 | 156 | r.length(i), |
|
157 | 157 | base, |
|
158 | 158 | r.linkrev(i), |
|
159 | 159 | short(node), |
|
160 | 160 | short(pp[0]), |
|
161 | 161 | short(pp[1]), |
|
162 | 162 | ) |
|
163 | 163 | ) |
|
164 | 164 | elif format == 1: |
|
165 | 165 | pr = r.parentrevs(i) |
|
166 | 166 | ui.write( |
|
167 | 167 | b"% 6d %04x % 8d % 8d % 8d % 6d % 6d % 6d % 6d %s\n" |
|
168 | 168 | % ( |
|
169 | 169 | i, |
|
170 | 170 | r.flags(i), |
|
171 | 171 | r.start(i), |
|
172 | 172 | r.length(i), |
|
173 | 173 | r.rawsize(i), |
|
174 | 174 | base, |
|
175 | 175 | r.linkrev(i), |
|
176 | 176 | pr[0], |
|
177 | 177 | pr[1], |
|
178 | 178 | short(node), |
|
179 | 179 | ) |
|
180 | 180 | ) |
|
181 | 181 | |
|
182 | 182 | |
|
183 | 183 | def debugindexdot(orig, ui, repo, file_): |
|
184 | 184 | """dump an index DAG as a graphviz dot file""" |
|
185 | 185 | if not shallowutil.isenabled(repo): |
|
186 | 186 | return orig(ui, repo, file_) |
|
187 | 187 | |
|
188 | 188 | r = buildtemprevlog(repo, os.path.basename(file_)[:-2]) |
|
189 | 189 | |
|
190 | ui.write(b"digraph G {\n") | |
|
190 | ui.writenoi18n(b"digraph G {\n") | |
|
191 | 191 | for i in r: |
|
192 | 192 | node = r.node(i) |
|
193 | 193 | pp = r.parents(node) |
|
194 | 194 | ui.write(b"\t%d -> %d\n" % (r.rev(pp[0]), i)) |
|
195 | 195 | if pp[1] != nullid: |
|
196 | 196 | ui.write(b"\t%d -> %d\n" % (r.rev(pp[1]), i)) |
|
197 | 197 | ui.write(b"}\n") |
|
198 | 198 | |
|
199 | 199 | |
|
200 | 200 | def verifyremotefilelog(ui, path, **opts): |
|
201 | 201 | decompress = opts.get(r'decompress') |
|
202 | 202 | |
|
203 | 203 | for root, dirs, files in os.walk(path): |
|
204 | 204 | for file in files: |
|
205 | 205 | if file == b"repos": |
|
206 | 206 | continue |
|
207 | 207 | filepath = os.path.join(root, file) |
|
208 | 208 | size, firstnode, mapping = parsefileblob(filepath, decompress) |
|
209 | 209 | for p1, p2, linknode, copyfrom in mapping.itervalues(): |
|
210 | 210 | if linknode == nullid: |
|
211 | 211 | actualpath = os.path.relpath(root, path) |
|
212 | 212 | key = fileserverclient.getcachekey( |
|
213 | 213 | b"reponame", actualpath, file |
|
214 | 214 | ) |
|
215 | 215 | ui.status( |
|
216 | 216 | b"%s %s\n" % (key, os.path.relpath(filepath, path)) |
|
217 | 217 | ) |
|
218 | 218 | |
|
219 | 219 | |
|
220 | 220 | def _decompressblob(raw): |
|
221 | 221 | return zlib.decompress(raw) |
|
222 | 222 | |
|
223 | 223 | |
|
224 | 224 | def parsefileblob(path, decompress): |
|
225 | 225 | f = open(path, b"rb") |
|
226 | 226 | try: |
|
227 | 227 | raw = f.read() |
|
228 | 228 | finally: |
|
229 | 229 | f.close() |
|
230 | 230 | |
|
231 | 231 | if decompress: |
|
232 | 232 | raw = _decompressblob(raw) |
|
233 | 233 | |
|
234 | 234 | offset, size, flags = shallowutil.parsesizeflags(raw) |
|
235 | 235 | start = offset + size |
|
236 | 236 | |
|
237 | 237 | firstnode = None |
|
238 | 238 | |
|
239 | 239 | mapping = {} |
|
240 | 240 | while start < len(raw): |
|
241 | 241 | divider = raw.index(b'\0', start + 80) |
|
242 | 242 | |
|
243 | 243 | currentnode = raw[start : (start + 20)] |
|
244 | 244 | if not firstnode: |
|
245 | 245 | firstnode = currentnode |
|
246 | 246 | |
|
247 | 247 | p1 = raw[(start + 20) : (start + 40)] |
|
248 | 248 | p2 = raw[(start + 40) : (start + 60)] |
|
249 | 249 | linknode = raw[(start + 60) : (start + 80)] |
|
250 | 250 | copyfrom = raw[(start + 80) : divider] |
|
251 | 251 | |
|
252 | 252 | mapping[currentnode] = (p1, p2, linknode, copyfrom) |
|
253 | 253 | start = divider + 1 |
|
254 | 254 | |
|
255 | 255 | return size, firstnode, mapping |
|
256 | 256 | |
|
257 | 257 | |
|
258 | 258 | def debugdatapack(ui, *paths, **opts): |
|
259 | 259 | for path in paths: |
|
260 | 260 | if b'.data' in path: |
|
261 | 261 | path = path[: path.index(b'.data')] |
|
262 | 262 | ui.write(b"%s:\n" % path) |
|
263 | 263 | dpack = datapack.datapack(path) |
|
264 | 264 | node = opts.get(r'node') |
|
265 | 265 | if node: |
|
266 | 266 | deltachain = dpack.getdeltachain(b'', bin(node)) |
|
267 | 267 | dumpdeltachain(ui, deltachain, **opts) |
|
268 | 268 | return |
|
269 | 269 | |
|
270 | 270 | if opts.get(r'long'): |
|
271 | 271 | hashformatter = hex |
|
272 | 272 | hashlen = 42 |
|
273 | 273 | else: |
|
274 | 274 | hashformatter = short |
|
275 | 275 | hashlen = 14 |
|
276 | 276 | |
|
277 | 277 | lastfilename = None |
|
278 | 278 | totaldeltasize = 0 |
|
279 | 279 | totalblobsize = 0 |
|
280 | 280 | |
|
281 | 281 | def printtotals(): |
|
282 | 282 | if lastfilename is not None: |
|
283 | 283 | ui.write(b"\n") |
|
284 | 284 | if not totaldeltasize or not totalblobsize: |
|
285 | 285 | return |
|
286 | 286 | difference = totalblobsize - totaldeltasize |
|
287 | 287 | deltastr = b"%0.1f%% %s" % ( |
|
288 | 288 | (100.0 * abs(difference) / totalblobsize), |
|
289 | 289 | (b"smaller" if difference > 0 else b"bigger"), |
|
290 | 290 | ) |
|
291 | 291 | |
|
292 | ui.write( | |
|
292 | ui.writenoi18n( | |
|
293 | 293 | b"Total:%s%s %s (%s)\n" |
|
294 | 294 | % ( |
|
295 | 295 | b"".ljust(2 * hashlen - len(b"Total:")), |
|
296 | 296 | (b'%d' % totaldeltasize).ljust(12), |
|
297 | 297 | (b'%d' % totalblobsize).ljust(9), |
|
298 | 298 | deltastr, |
|
299 | 299 | ) |
|
300 | 300 | ) |
|
301 | 301 | |
|
302 | 302 | bases = {} |
|
303 | 303 | nodes = set() |
|
304 | 304 | failures = 0 |
|
305 | 305 | for filename, node, deltabase, deltalen in dpack.iterentries(): |
|
306 | 306 | bases[node] = deltabase |
|
307 | 307 | if node in nodes: |
|
308 | 308 | ui.write((b"Bad entry: %s appears twice\n" % short(node))) |
|
309 | 309 | failures += 1 |
|
310 | 310 | nodes.add(node) |
|
311 | 311 | if filename != lastfilename: |
|
312 | 312 | printtotals() |
|
313 | 313 | name = b'(empty name)' if filename == b'' else filename |
|
314 | 314 | ui.write(b"%s:\n" % name) |
|
315 | 315 | ui.write( |
|
316 | 316 | b"%s%s%s%s\n" |
|
317 | 317 | % ( |
|
318 | 318 | b"Node".ljust(hashlen), |
|
319 | 319 | b"Delta Base".ljust(hashlen), |
|
320 | 320 | b"Delta Length".ljust(14), |
|
321 | 321 | b"Blob Size".ljust(9), |
|
322 | 322 | ) |
|
323 | 323 | ) |
|
324 | 324 | lastfilename = filename |
|
325 | 325 | totalblobsize = 0 |
|
326 | 326 | totaldeltasize = 0 |
|
327 | 327 | |
|
328 | 328 | # Metadata could be missing, in which case it will be an empty dict. |
|
329 | 329 | meta = dpack.getmeta(filename, node) |
|
330 | 330 | if constants.METAKEYSIZE in meta: |
|
331 | 331 | blobsize = meta[constants.METAKEYSIZE] |
|
332 | 332 | totaldeltasize += deltalen |
|
333 | 333 | totalblobsize += blobsize |
|
334 | 334 | else: |
|
335 | 335 | blobsize = b"(missing)" |
|
336 | 336 | ui.write( |
|
337 | 337 | b"%s %s %s%s\n" |
|
338 | 338 | % ( |
|
339 | 339 | hashformatter(node), |
|
340 | 340 | hashformatter(deltabase), |
|
341 | 341 | (b'%d' % deltalen).ljust(14), |
|
342 | 342 | pycompat.bytestr(blobsize), |
|
343 | 343 | ) |
|
344 | 344 | ) |
|
345 | 345 | |
|
346 | 346 | if filename is not None: |
|
347 | 347 | printtotals() |
|
348 | 348 | |
|
349 | 349 | failures += _sanitycheck(ui, set(nodes), bases) |
|
350 | 350 | if failures > 1: |
|
351 | 351 | ui.warn((b"%d failures\n" % failures)) |
|
352 | 352 | return 1 |
|
353 | 353 | |
|
354 | 354 | |
|
355 | 355 | def _sanitycheck(ui, nodes, bases): |
|
356 | 356 | """ |
|
357 | 357 | Does some basic sanity checking on a packfiles with ``nodes`` ``bases`` (a |
|
358 | 358 | mapping of node->base): |
|
359 | 359 | |
|
360 | 360 | - Each deltabase must itself be a node elsewhere in the pack |
|
361 | 361 | - There must be no cycles |
|
362 | 362 | """ |
|
363 | 363 | failures = 0 |
|
364 | 364 | for node in nodes: |
|
365 | 365 | seen = set() |
|
366 | 366 | current = node |
|
367 | 367 | deltabase = bases[current] |
|
368 | 368 | |
|
369 | 369 | while deltabase != nullid: |
|
370 | 370 | if deltabase not in nodes: |
|
371 | 371 | ui.warn( |
|
372 | 372 | ( |
|
373 | 373 | b"Bad entry: %s has an unknown deltabase (%s)\n" |
|
374 | 374 | % (short(node), short(deltabase)) |
|
375 | 375 | ) |
|
376 | 376 | ) |
|
377 | 377 | failures += 1 |
|
378 | 378 | break |
|
379 | 379 | |
|
380 | 380 | if deltabase in seen: |
|
381 | 381 | ui.warn( |
|
382 | 382 | ( |
|
383 | 383 | b"Bad entry: %s has a cycle (at %s)\n" |
|
384 | 384 | % (short(node), short(deltabase)) |
|
385 | 385 | ) |
|
386 | 386 | ) |
|
387 | 387 | failures += 1 |
|
388 | 388 | break |
|
389 | 389 | |
|
390 | 390 | current = deltabase |
|
391 | 391 | seen.add(current) |
|
392 | 392 | deltabase = bases[current] |
|
393 | 393 | # Since ``node`` begins a valid chain, reset/memoize its base to nullid |
|
394 | 394 | # so we don't traverse it again. |
|
395 | 395 | bases[node] = nullid |
|
396 | 396 | return failures |
|
397 | 397 | |
|
398 | 398 | |
|
399 | 399 | def dumpdeltachain(ui, deltachain, **opts): |
|
400 | 400 | hashformatter = hex |
|
401 | 401 | hashlen = 40 |
|
402 | 402 | |
|
403 | 403 | lastfilename = None |
|
404 | 404 | for filename, node, filename, deltabasenode, delta in deltachain: |
|
405 | 405 | if filename != lastfilename: |
|
406 | 406 | ui.write(b"\n%s\n" % filename) |
|
407 | 407 | lastfilename = filename |
|
408 | 408 | ui.write( |
|
409 | 409 | b"%s %s %s %s\n" |
|
410 | 410 | % ( |
|
411 | 411 | b"Node".ljust(hashlen), |
|
412 | 412 | b"Delta Base".ljust(hashlen), |
|
413 | 413 | b"Delta SHA1".ljust(hashlen), |
|
414 | 414 | b"Delta Length".ljust(6), |
|
415 | 415 | ) |
|
416 | 416 | ) |
|
417 | 417 | |
|
418 | 418 | ui.write( |
|
419 | 419 | b"%s %s %s %d\n" |
|
420 | 420 | % ( |
|
421 | 421 | hashformatter(node), |
|
422 | 422 | hashformatter(deltabasenode), |
|
423 | 423 | nodemod.hex(hashlib.sha1(delta).digest()), |
|
424 | 424 | len(delta), |
|
425 | 425 | ) |
|
426 | 426 | ) |
|
427 | 427 | |
|
428 | 428 | |
|
429 | 429 | def debughistorypack(ui, path): |
|
430 | 430 | if b'.hist' in path: |
|
431 | 431 | path = path[: path.index(b'.hist')] |
|
432 | 432 | hpack = historypack.historypack(path) |
|
433 | 433 | |
|
434 | 434 | lastfilename = None |
|
435 | 435 | for entry in hpack.iterentries(): |
|
436 | 436 | filename, node, p1node, p2node, linknode, copyfrom = entry |
|
437 | 437 | if filename != lastfilename: |
|
438 | 438 | ui.write(b"\n%s\n" % filename) |
|
439 | 439 | ui.write( |
|
440 | 440 | b"%s%s%s%s%s\n" |
|
441 | 441 | % ( |
|
442 | 442 | b"Node".ljust(14), |
|
443 | 443 | b"P1 Node".ljust(14), |
|
444 | 444 | b"P2 Node".ljust(14), |
|
445 | 445 | b"Link Node".ljust(14), |
|
446 | 446 | b"Copy From", |
|
447 | 447 | ) |
|
448 | 448 | ) |
|
449 | 449 | lastfilename = filename |
|
450 | 450 | ui.write( |
|
451 | 451 | b"%s %s %s %s %s\n" |
|
452 | 452 | % ( |
|
453 | 453 | short(node), |
|
454 | 454 | short(p1node), |
|
455 | 455 | short(p2node), |
|
456 | 456 | short(linknode), |
|
457 | 457 | copyfrom, |
|
458 | 458 | ) |
|
459 | 459 | ) |
|
460 | 460 | |
|
461 | 461 | |
|
462 | 462 | def debugwaitonrepack(repo): |
|
463 | 463 | with lockmod.lock(repack.repacklockvfs(repo), b"repacklock", timeout=-1): |
|
464 | 464 | return |
|
465 | 465 | |
|
466 | 466 | |
|
467 | 467 | def debugwaitonprefetch(repo): |
|
468 | 468 | with repo._lock( |
|
469 | 469 | repo.svfs, |
|
470 | 470 | b"prefetchlock", |
|
471 | 471 | True, |
|
472 | 472 | None, |
|
473 | 473 | None, |
|
474 | 474 | _(b'prefetching in %s') % repo.origroot, |
|
475 | 475 | ): |
|
476 | 476 | pass |
@@ -1,530 +1,530 b'' | |||
|
1 | 1 | # show.py - Extension implementing `hg show` |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2017 Gregory Szorc <gregory.szorc@gmail.com> |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | |
|
8 | 8 | """unified command to show various repository information (EXPERIMENTAL) |
|
9 | 9 | |
|
10 | 10 | This extension provides the :hg:`show` command, which provides a central |
|
11 | 11 | command for displaying commonly-accessed repository data and views of that |
|
12 | 12 | data. |
|
13 | 13 | |
|
14 | 14 | The following config options can influence operation. |
|
15 | 15 | |
|
16 | 16 | ``commands`` |
|
17 | 17 | ------------ |
|
18 | 18 | |
|
19 | 19 | ``show.aliasprefix`` |
|
20 | 20 | List of strings that will register aliases for views. e.g. ``s`` will |
|
21 | 21 | effectively set config options ``alias.s<view> = show <view>`` for all |
|
22 | 22 | views. i.e. `hg swork` would execute `hg show work`. |
|
23 | 23 | |
|
24 | 24 | Aliases that would conflict with existing registrations will not be |
|
25 | 25 | performed. |
|
26 | 26 | """ |
|
27 | 27 | |
|
28 | 28 | from __future__ import absolute_import |
|
29 | 29 | |
|
30 | 30 | from mercurial.i18n import _ |
|
31 | 31 | from mercurial.node import nullrev |
|
32 | 32 | from mercurial import ( |
|
33 | 33 | cmdutil, |
|
34 | 34 | commands, |
|
35 | 35 | destutil, |
|
36 | 36 | error, |
|
37 | 37 | formatter, |
|
38 | 38 | graphmod, |
|
39 | 39 | logcmdutil, |
|
40 | 40 | phases, |
|
41 | 41 | pycompat, |
|
42 | 42 | registrar, |
|
43 | 43 | revset, |
|
44 | 44 | revsetlang, |
|
45 | 45 | scmutil, |
|
46 | 46 | ) |
|
47 | 47 | |
|
48 | 48 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
|
49 | 49 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
|
50 | 50 | # be specifying the version(s) of Mercurial they are tested with, or |
|
51 | 51 | # leave the attribute unspecified. |
|
52 | 52 | testedwith = b'ships-with-hg-core' |
|
53 | 53 | |
|
54 | 54 | cmdtable = {} |
|
55 | 55 | command = registrar.command(cmdtable) |
|
56 | 56 | |
|
57 | 57 | revsetpredicate = registrar.revsetpredicate() |
|
58 | 58 | |
|
59 | 59 | |
|
60 | 60 | class showcmdfunc(registrar._funcregistrarbase): |
|
61 | 61 | """Register a function to be invoked for an `hg show <thing>`.""" |
|
62 | 62 | |
|
63 | 63 | # Used by _formatdoc(). |
|
64 | 64 | _docformat = b'%s -- %s' |
|
65 | 65 | |
|
66 | 66 | def _extrasetup(self, name, func, fmtopic=None, csettopic=None): |
|
67 | 67 | """Called with decorator arguments to register a show view. |
|
68 | 68 | |
|
69 | 69 | ``name`` is the sub-command name. |
|
70 | 70 | |
|
71 | 71 | ``func`` is the function being decorated. |
|
72 | 72 | |
|
73 | 73 | ``fmtopic`` is the topic in the style that will be rendered for |
|
74 | 74 | this view. |
|
75 | 75 | |
|
76 | 76 | ``csettopic`` is the topic in the style to be used for a changeset |
|
77 | 77 | printer. |
|
78 | 78 | |
|
79 | 79 | If ``fmtopic`` is specified, the view function will receive a |
|
80 | 80 | formatter instance. If ``csettopic`` is specified, the view |
|
81 | 81 | function will receive a changeset printer. |
|
82 | 82 | """ |
|
83 | 83 | func._fmtopic = fmtopic |
|
84 | 84 | func._csettopic = csettopic |
|
85 | 85 | |
|
86 | 86 | |
|
87 | 87 | showview = showcmdfunc() |
|
88 | 88 | |
|
89 | 89 | |
|
90 | 90 | @command( |
|
91 | 91 | b'show', |
|
92 | 92 | [ |
|
93 | 93 | # TODO: Switch this template flag to use cmdutil.formatteropts if |
|
94 | 94 | # 'hg show' becomes stable before --template/-T is stable. For now, |
|
95 | 95 | # we are putting it here without the '(EXPERIMENTAL)' flag because it |
|
96 | 96 | # is an important part of the 'hg show' user experience and the entire |
|
97 | 97 | # 'hg show' experience is experimental. |
|
98 | 98 | (b'T', b'template', b'', b'display with template', _(b'TEMPLATE')), |
|
99 | 99 | ], |
|
100 | 100 | _(b'VIEW'), |
|
101 | 101 | helpcategory=command.CATEGORY_CHANGE_NAVIGATION, |
|
102 | 102 | ) |
|
103 | 103 | def show(ui, repo, view=None, template=None): |
|
104 | 104 | """show various repository information |
|
105 | 105 | |
|
106 | 106 | A requested view of repository data is displayed. |
|
107 | 107 | |
|
108 | 108 | If no view is requested, the list of available views is shown and the |
|
109 | 109 | command aborts. |
|
110 | 110 | |
|
111 | 111 | .. note:: |
|
112 | 112 | |
|
113 | 113 | There are no backwards compatibility guarantees for the output of this |
|
114 | 114 | command. Output may change in any future Mercurial release. |
|
115 | 115 | |
|
116 | 116 | Consumers wanting stable command output should specify a template via |
|
117 | 117 | ``-T/--template``. |
|
118 | 118 | |
|
119 | 119 | List of available views: |
|
120 | 120 | """ |
|
121 | 121 | if ui.plain() and not template: |
|
122 | 122 | hint = _(b'invoke with -T/--template to control output format') |
|
123 | 123 | raise error.Abort( |
|
124 | 124 | _(b'must specify a template in plain mode'), hint=hint |
|
125 | 125 | ) |
|
126 | 126 | |
|
127 | 127 | views = showview._table |
|
128 | 128 | |
|
129 | 129 | if not view: |
|
130 | 130 | ui.pager(b'show') |
|
131 | 131 | # TODO consider using formatter here so available views can be |
|
132 | 132 | # rendered to custom format. |
|
133 | 133 | ui.write(_(b'available views:\n')) |
|
134 | 134 | ui.write(b'\n') |
|
135 | 135 | |
|
136 | 136 | for name, func in sorted(views.items()): |
|
137 | 137 | ui.write(b'%s\n' % pycompat.sysbytes(func.__doc__)) |
|
138 | 138 | |
|
139 | 139 | ui.write(b'\n') |
|
140 | 140 | raise error.Abort( |
|
141 | 141 | _(b'no view requested'), |
|
142 | 142 | hint=_(b'use "hg show VIEW" to choose a view'), |
|
143 | 143 | ) |
|
144 | 144 | |
|
145 | 145 | # TODO use same logic as dispatch to perform prefix matching. |
|
146 | 146 | if view not in views: |
|
147 | 147 | raise error.Abort( |
|
148 | 148 | _(b'unknown view: %s') % view, |
|
149 | 149 | hint=_(b'run "hg show" to see available views'), |
|
150 | 150 | ) |
|
151 | 151 | |
|
152 | 152 | template = template or b'show' |
|
153 | 153 | |
|
154 | 154 | fn = views[view] |
|
155 | 155 | ui.pager(b'show') |
|
156 | 156 | |
|
157 | 157 | if fn._fmtopic: |
|
158 | 158 | fmtopic = b'show%s' % fn._fmtopic |
|
159 | 159 | with ui.formatter(fmtopic, {b'template': template}) as fm: |
|
160 | 160 | return fn(ui, repo, fm) |
|
161 | 161 | elif fn._csettopic: |
|
162 | 162 | ref = b'show%s' % fn._csettopic |
|
163 | 163 | spec = formatter.lookuptemplate(ui, ref, template) |
|
164 | 164 | displayer = logcmdutil.changesettemplater(ui, repo, spec, buffered=True) |
|
165 | 165 | return fn(ui, repo, displayer) |
|
166 | 166 | else: |
|
167 | 167 | return fn(ui, repo) |
|
168 | 168 | |
|
169 | 169 | |
|
170 | 170 | @showview(b'bookmarks', fmtopic=b'bookmarks') |
|
171 | 171 | def showbookmarks(ui, repo, fm): |
|
172 | 172 | """bookmarks and their associated changeset""" |
|
173 | 173 | marks = repo._bookmarks |
|
174 | 174 | if not len(marks): |
|
175 | 175 | # This is a bit hacky. Ideally, templates would have a way to |
|
176 | 176 | # specify an empty output, but we shouldn't corrupt JSON while |
|
177 | 177 | # waiting for this functionality. |
|
178 | 178 | if not isinstance(fm, formatter.jsonformatter): |
|
179 | 179 | ui.write(_(b'(no bookmarks set)\n')) |
|
180 | 180 | return |
|
181 | 181 | |
|
182 | 182 | revs = [repo[node].rev() for node in marks.values()] |
|
183 | 183 | active = repo._activebookmark |
|
184 | 184 | longestname = max(len(b) for b in marks) |
|
185 | 185 | nodelen = longestshortest(repo, revs) |
|
186 | 186 | |
|
187 | 187 | for bm, node in sorted(marks.items()): |
|
188 | 188 | fm.startitem() |
|
189 | 189 | fm.context(ctx=repo[node]) |
|
190 | 190 | fm.write(b'bookmark', b'%s', bm) |
|
191 | 191 | fm.write(b'node', fm.hexfunc(node), fm.hexfunc(node)) |
|
192 | 192 | fm.data( |
|
193 | 193 | active=bm == active, longestbookmarklen=longestname, nodelen=nodelen |
|
194 | 194 | ) |
|
195 | 195 | |
|
196 | 196 | |
|
197 | 197 | @showview(b'stack', csettopic=b'stack') |
|
198 | 198 | def showstack(ui, repo, displayer): |
|
199 | 199 | """current line of work""" |
|
200 | 200 | wdirctx = repo[b'.'] |
|
201 | 201 | if wdirctx.rev() == nullrev: |
|
202 | 202 | raise error.Abort( |
|
203 | 203 | _( |
|
204 | 204 | b'stack view only available when there is a ' |
|
205 | 205 | b'working directory' |
|
206 | 206 | ) |
|
207 | 207 | ) |
|
208 | 208 | |
|
209 | 209 | if wdirctx.phase() == phases.public: |
|
210 | 210 | ui.write( |
|
211 | 211 | _( |
|
212 | 212 | b'(empty stack; working directory parent is a published ' |
|
213 | 213 | b'changeset)\n' |
|
214 | 214 | ) |
|
215 | 215 | ) |
|
216 | 216 | return |
|
217 | 217 | |
|
218 | 218 | # TODO extract "find stack" into a function to facilitate |
|
219 | 219 | # customization and reuse. |
|
220 | 220 | |
|
221 | 221 | baserev = destutil.stackbase(ui, repo) |
|
222 | 222 | basectx = None |
|
223 | 223 | |
|
224 | 224 | if baserev is None: |
|
225 | 225 | baserev = wdirctx.rev() |
|
226 | 226 | stackrevs = {wdirctx.rev()} |
|
227 | 227 | else: |
|
228 | 228 | stackrevs = set(repo.revs(b'%d::.', baserev)) |
|
229 | 229 | |
|
230 | 230 | ctx = repo[baserev] |
|
231 | 231 | if ctx.p1().rev() != nullrev: |
|
232 | 232 | basectx = ctx.p1() |
|
233 | 233 | |
|
234 | 234 | # And relevant descendants. |
|
235 | 235 | branchpointattip = False |
|
236 | 236 | cl = repo.changelog |
|
237 | 237 | |
|
238 | 238 | for rev in cl.descendants([wdirctx.rev()]): |
|
239 | 239 | ctx = repo[rev] |
|
240 | 240 | |
|
241 | 241 | # Will only happen if . is public. |
|
242 | 242 | if ctx.phase() == phases.public: |
|
243 | 243 | break |
|
244 | 244 | |
|
245 | 245 | stackrevs.add(ctx.rev()) |
|
246 | 246 | |
|
247 | 247 | # ctx.children() within a function iterating on descandants |
|
248 | 248 | # potentially has severe performance concerns because revlog.children() |
|
249 | 249 | # iterates over all revisions after ctx's node. However, the number of |
|
250 | 250 | # draft changesets should be a reasonably small number. So even if |
|
251 | 251 | # this is quadratic, the perf impact should be minimal. |
|
252 | 252 | if len(ctx.children()) > 1: |
|
253 | 253 | branchpointattip = True |
|
254 | 254 | break |
|
255 | 255 | |
|
256 | 256 | stackrevs = list(sorted(stackrevs, reverse=True)) |
|
257 | 257 | |
|
258 | 258 | # Find likely target heads for the current stack. These are likely |
|
259 | 259 | # merge or rebase targets. |
|
260 | 260 | if basectx: |
|
261 | 261 | # TODO make this customizable? |
|
262 | 262 | newheads = set( |
|
263 | 263 | repo.revs( |
|
264 | 264 | b'heads(%d::) - %ld - not public()', basectx.rev(), stackrevs |
|
265 | 265 | ) |
|
266 | 266 | ) |
|
267 | 267 | else: |
|
268 | 268 | newheads = set() |
|
269 | 269 | |
|
270 | 270 | allrevs = set(stackrevs) | newheads | {baserev} |
|
271 | 271 | nodelen = longestshortest(repo, allrevs) |
|
272 | 272 | |
|
273 | 273 | try: |
|
274 | 274 | cmdutil.findcmd(b'rebase', commands.table) |
|
275 | 275 | haverebase = True |
|
276 | 276 | except (error.AmbiguousCommand, error.UnknownCommand): |
|
277 | 277 | haverebase = False |
|
278 | 278 | |
|
279 | 279 | # TODO use templating. |
|
280 | 280 | # TODO consider using graphmod. But it may not be necessary given |
|
281 | 281 | # our simplicity and the customizations required. |
|
282 | 282 | # TODO use proper graph symbols from graphmod |
|
283 | 283 | |
|
284 | 284 | tres = formatter.templateresources(ui, repo) |
|
285 | 285 | shortesttmpl = formatter.maketemplater( |
|
286 | 286 | ui, b'{shortest(node, %d)}' % nodelen, resources=tres |
|
287 | 287 | ) |
|
288 | 288 | |
|
289 | 289 | def shortest(ctx): |
|
290 | 290 | return shortesttmpl.renderdefault({b'ctx': ctx, b'node': ctx.hex()}) |
|
291 | 291 | |
|
292 | 292 | # We write out new heads to aid in DAG awareness and to help with decision |
|
293 | 293 | # making on how the stack should be reconciled with commits made since the |
|
294 | 294 | # branch point. |
|
295 | 295 | if newheads: |
|
296 | 296 | # Calculate distance from base so we can render the count and so we can |
|
297 | 297 | # sort display order by commit distance. |
|
298 | 298 | revdistance = {} |
|
299 | 299 | for head in newheads: |
|
300 | 300 | # There is some redundancy in DAG traversal here and therefore |
|
301 | 301 | # room to optimize. |
|
302 | 302 | ancestors = cl.ancestors([head], stoprev=basectx.rev()) |
|
303 | 303 | revdistance[head] = len(list(ancestors)) |
|
304 | 304 | |
|
305 | 305 | sourcectx = repo[stackrevs[-1]] |
|
306 | 306 | |
|
307 | 307 | sortedheads = sorted( |
|
308 | 308 | newheads, key=lambda x: revdistance[x], reverse=True |
|
309 | 309 | ) |
|
310 | 310 | |
|
311 | 311 | for i, rev in enumerate(sortedheads): |
|
312 | 312 | ctx = repo[rev] |
|
313 | 313 | |
|
314 | 314 | if i: |
|
315 | 315 | ui.write(b': ') |
|
316 | 316 | else: |
|
317 | 317 | ui.write(b' ') |
|
318 | 318 | |
|
319 | ui.write(b'o ') | |
|
319 | ui.writenoi18n(b'o ') | |
|
320 | 320 | displayer.show(ctx, nodelen=nodelen) |
|
321 | 321 | displayer.flush(ctx) |
|
322 | 322 | ui.write(b'\n') |
|
323 | 323 | |
|
324 | 324 | if i: |
|
325 | 325 | ui.write(b':/') |
|
326 | 326 | else: |
|
327 | 327 | ui.write(b' /') |
|
328 | 328 | |
|
329 | 329 | ui.write(b' (') |
|
330 | 330 | ui.write( |
|
331 | 331 | _(b'%d commits ahead') % revdistance[rev], |
|
332 | 332 | label=b'stack.commitdistance', |
|
333 | 333 | ) |
|
334 | 334 | |
|
335 | 335 | if haverebase: |
|
336 | 336 | # TODO may be able to omit --source in some scenarios |
|
337 | 337 | ui.write(b'; ') |
|
338 | 338 | ui.write( |
|
339 | 339 | ( |
|
340 | 340 | b'hg rebase --source %s --dest %s' |
|
341 | 341 | % (shortest(sourcectx), shortest(ctx)) |
|
342 | 342 | ), |
|
343 | 343 | label=b'stack.rebasehint', |
|
344 | 344 | ) |
|
345 | 345 | |
|
346 | 346 | ui.write(b')\n') |
|
347 | 347 | |
|
348 | 348 | ui.write(b':\n: ') |
|
349 | 349 | ui.write(_(b'(stack head)\n'), label=b'stack.label') |
|
350 | 350 | |
|
351 | 351 | if branchpointattip: |
|
352 | 352 | ui.write(b' \\ / ') |
|
353 | 353 | ui.write(_(b'(multiple children)\n'), label=b'stack.label') |
|
354 | 354 | ui.write(b' |\n') |
|
355 | 355 | |
|
356 | 356 | for rev in stackrevs: |
|
357 | 357 | ctx = repo[rev] |
|
358 | 358 | symbol = b'@' if rev == wdirctx.rev() else b'o' |
|
359 | 359 | |
|
360 | 360 | if newheads: |
|
361 | 361 | ui.write(b': ') |
|
362 | 362 | else: |
|
363 | 363 | ui.write(b' ') |
|
364 | 364 | |
|
365 | 365 | ui.write(symbol, b' ') |
|
366 | 366 | displayer.show(ctx, nodelen=nodelen) |
|
367 | 367 | displayer.flush(ctx) |
|
368 | 368 | ui.write(b'\n') |
|
369 | 369 | |
|
370 | 370 | # TODO display histedit hint? |
|
371 | 371 | |
|
372 | 372 | if basectx: |
|
373 | 373 | # Vertically and horizontally separate stack base from parent |
|
374 | 374 | # to reinforce stack boundary. |
|
375 | 375 | if newheads: |
|
376 | 376 | ui.write(b':/ ') |
|
377 | 377 | else: |
|
378 | 378 | ui.write(b' / ') |
|
379 | 379 | |
|
380 | 380 | ui.write(_(b'(stack base)'), b'\n', label=b'stack.label') |
|
381 | ui.write(b'o ') | |
|
381 | ui.writenoi18n(b'o ') | |
|
382 | 382 | |
|
383 | 383 | displayer.show(basectx, nodelen=nodelen) |
|
384 | 384 | displayer.flush(basectx) |
|
385 | 385 | ui.write(b'\n') |
|
386 | 386 | |
|
387 | 387 | |
|
388 | 388 | @revsetpredicate(b'_underway([commitage[, headage]])') |
|
389 | 389 | def underwayrevset(repo, subset, x): |
|
390 | 390 | args = revset.getargsdict(x, b'underway', b'commitage headage') |
|
391 | 391 | if b'commitage' not in args: |
|
392 | 392 | args[b'commitage'] = None |
|
393 | 393 | if b'headage' not in args: |
|
394 | 394 | args[b'headage'] = None |
|
395 | 395 | |
|
396 | 396 | # We assume callers of this revset add a topographical sort on the |
|
397 | 397 | # result. This means there is no benefit to making the revset lazy |
|
398 | 398 | # since the topographical sort needs to consume all revs. |
|
399 | 399 | # |
|
400 | 400 | # With this in mind, we build up the set manually instead of constructing |
|
401 | 401 | # a complex revset. This enables faster execution. |
|
402 | 402 | |
|
403 | 403 | # Mutable changesets (non-public) are the most important changesets |
|
404 | 404 | # to return. ``not public()`` will also pull in obsolete changesets if |
|
405 | 405 | # there is a non-obsolete changeset with obsolete ancestors. This is |
|
406 | 406 | # why we exclude obsolete changesets from this query. |
|
407 | 407 | rs = b'not public() and not obsolete()' |
|
408 | 408 | rsargs = [] |
|
409 | 409 | if args[b'commitage']: |
|
410 | 410 | rs += b' and date(%s)' |
|
411 | 411 | rsargs.append( |
|
412 | 412 | revsetlang.getstring( |
|
413 | 413 | args[b'commitage'], _(b'commitage requires a string') |
|
414 | 414 | ) |
|
415 | 415 | ) |
|
416 | 416 | |
|
417 | 417 | mutable = repo.revs(rs, *rsargs) |
|
418 | 418 | relevant = revset.baseset(mutable) |
|
419 | 419 | |
|
420 | 420 | # Add parents of mutable changesets to provide context. |
|
421 | 421 | relevant += repo.revs(b'parents(%ld)', mutable) |
|
422 | 422 | |
|
423 | 423 | # We also pull in (public) heads if they a) aren't closing a branch |
|
424 | 424 | # b) are recent. |
|
425 | 425 | rs = b'head() and not closed()' |
|
426 | 426 | rsargs = [] |
|
427 | 427 | if args[b'headage']: |
|
428 | 428 | rs += b' and date(%s)' |
|
429 | 429 | rsargs.append( |
|
430 | 430 | revsetlang.getstring( |
|
431 | 431 | args[b'headage'], _(b'headage requires a string') |
|
432 | 432 | ) |
|
433 | 433 | ) |
|
434 | 434 | |
|
435 | 435 | relevant += repo.revs(rs, *rsargs) |
|
436 | 436 | |
|
437 | 437 | # Add working directory parent. |
|
438 | 438 | wdirrev = repo[b'.'].rev() |
|
439 | 439 | if wdirrev != nullrev: |
|
440 | 440 | relevant += revset.baseset({wdirrev}) |
|
441 | 441 | |
|
442 | 442 | return subset & relevant |
|
443 | 443 | |
|
444 | 444 | |
|
445 | 445 | @showview(b'work', csettopic=b'work') |
|
446 | 446 | def showwork(ui, repo, displayer): |
|
447 | 447 | """changesets that aren't finished""" |
|
448 | 448 | # TODO support date-based limiting when calling revset. |
|
449 | 449 | revs = repo.revs(b'sort(_underway(), topo)') |
|
450 | 450 | nodelen = longestshortest(repo, revs) |
|
451 | 451 | |
|
452 | 452 | revdag = graphmod.dagwalker(repo, revs) |
|
453 | 453 | |
|
454 | 454 | ui.setconfig(b'experimental', b'graphshorten', True) |
|
455 | 455 | logcmdutil.displaygraph( |
|
456 | 456 | ui, |
|
457 | 457 | repo, |
|
458 | 458 | revdag, |
|
459 | 459 | displayer, |
|
460 | 460 | graphmod.asciiedges, |
|
461 | 461 | props={b'nodelen': nodelen}, |
|
462 | 462 | ) |
|
463 | 463 | |
|
464 | 464 | |
|
465 | 465 | def extsetup(ui): |
|
466 | 466 | # Alias `hg <prefix><view>` to `hg show <view>`. |
|
467 | 467 | for prefix in ui.configlist(b'commands', b'show.aliasprefix'): |
|
468 | 468 | for view in showview._table: |
|
469 | 469 | name = b'%s%s' % (prefix, view) |
|
470 | 470 | |
|
471 | 471 | choice, allcommands = cmdutil.findpossible( |
|
472 | 472 | name, commands.table, strict=True |
|
473 | 473 | ) |
|
474 | 474 | |
|
475 | 475 | # This alias is already a command name. Don't set it. |
|
476 | 476 | if name in choice: |
|
477 | 477 | continue |
|
478 | 478 | |
|
479 | 479 | # Same for aliases. |
|
480 | 480 | if ui.config(b'alias', name, None): |
|
481 | 481 | continue |
|
482 | 482 | |
|
483 | 483 | ui.setconfig(b'alias', name, b'show %s' % view, source=b'show') |
|
484 | 484 | |
|
485 | 485 | |
|
486 | 486 | def longestshortest(repo, revs, minlen=4): |
|
487 | 487 | """Return the length of the longest shortest node to identify revisions. |
|
488 | 488 | |
|
489 | 489 | The result of this function can be used with the ``shortest()`` template |
|
490 | 490 | function to ensure that a value is unique and unambiguous for a given |
|
491 | 491 | set of nodes. |
|
492 | 492 | |
|
493 | 493 | The number of revisions in the repo is taken into account to prevent |
|
494 | 494 | a numeric node prefix from conflicting with an integer revision number. |
|
495 | 495 | If we fail to do this, a value of e.g. ``10023`` could mean either |
|
496 | 496 | revision 10023 or node ``10023abc...``. |
|
497 | 497 | """ |
|
498 | 498 | if not revs: |
|
499 | 499 | return minlen |
|
500 | 500 | cl = repo.changelog |
|
501 | 501 | return max( |
|
502 | 502 | len(scmutil.shortesthexnodeidprefix(repo, cl.node(r), minlen)) |
|
503 | 503 | for r in revs |
|
504 | 504 | ) |
|
505 | 505 | |
|
506 | 506 | |
|
507 | 507 | # Adjust the docstring of the show command so it shows all registered views. |
|
508 | 508 | # This is a bit hacky because it runs at the end of module load. When moved |
|
509 | 509 | # into core or when another extension wants to provide a view, we'll need |
|
510 | 510 | # to do this more robustly. |
|
511 | 511 | # TODO make this more robust. |
|
512 | 512 | def _updatedocstring(): |
|
513 | 513 | longest = max(map(len, showview._table.keys())) |
|
514 | 514 | entries = [] |
|
515 | 515 | for key in sorted(showview._table.keys()): |
|
516 | 516 | entries.append( |
|
517 | 517 | r' %s %s' |
|
518 | 518 | % ( |
|
519 | 519 | pycompat.sysstr(key.ljust(longest)), |
|
520 | 520 | showview._table[key]._origdoc, |
|
521 | 521 | ) |
|
522 | 522 | ) |
|
523 | 523 | |
|
524 | 524 | cmdtable[b'show'][0].__doc__ = pycompat.sysstr(b'%s\n\n%s\n ') % ( |
|
525 | 525 | cmdtable[b'show'][0].__doc__.rstrip(), |
|
526 | 526 | pycompat.sysstr(b'\n\n').join(entries), |
|
527 | 527 | ) |
|
528 | 528 | |
|
529 | 529 | |
|
530 | 530 | _updatedocstring() |
@@ -1,218 +1,218 b'' | |||
|
1 | 1 | # win32mbcs.py -- MBCS filename support for Mercurial |
|
2 | 2 | # |
|
3 | 3 | # Copyright (c) 2008 Shun-ichi Goto <shunichi.goto@gmail.com> |
|
4 | 4 | # |
|
5 | 5 | # Version: 0.3 |
|
6 | 6 | # Author: Shun-ichi Goto <shunichi.goto@gmail.com> |
|
7 | 7 | # |
|
8 | 8 | # This software may be used and distributed according to the terms of the |
|
9 | 9 | # GNU General Public License version 2 or any later version. |
|
10 | 10 | # |
|
11 | 11 | |
|
12 | 12 | '''allow the use of MBCS paths with problematic encodings |
|
13 | 13 | |
|
14 | 14 | Some MBCS encodings are not good for some path operations (i.e. |
|
15 | 15 | splitting path, case conversion, etc.) with its encoded bytes. We call |
|
16 | 16 | such a encoding (i.e. shift_jis and big5) as "problematic encoding". |
|
17 | 17 | This extension can be used to fix the issue with those encodings by |
|
18 | 18 | wrapping some functions to convert to Unicode string before path |
|
19 | 19 | operation. |
|
20 | 20 | |
|
21 | 21 | This extension is useful for: |
|
22 | 22 | |
|
23 | 23 | - Japanese Windows users using shift_jis encoding. |
|
24 | 24 | - Chinese Windows users using big5 encoding. |
|
25 | 25 | - All users who use a repository with one of problematic encodings on |
|
26 | 26 | case-insensitive file system. |
|
27 | 27 | |
|
28 | 28 | This extension is not needed for: |
|
29 | 29 | |
|
30 | 30 | - Any user who use only ASCII chars in path. |
|
31 | 31 | - Any user who do not use any of problematic encodings. |
|
32 | 32 | |
|
33 | 33 | Note that there are some limitations on using this extension: |
|
34 | 34 | |
|
35 | 35 | - You should use single encoding in one repository. |
|
36 | 36 | - If the repository path ends with 0x5c, .hg/hgrc cannot be read. |
|
37 | 37 | - win32mbcs is not compatible with fixutf8 extension. |
|
38 | 38 | |
|
39 | 39 | By default, win32mbcs uses encoding.encoding decided by Mercurial. |
|
40 | 40 | You can specify the encoding by config option:: |
|
41 | 41 | |
|
42 | 42 | [win32mbcs] |
|
43 | 43 | encoding = sjis |
|
44 | 44 | |
|
45 | 45 | It is useful for the users who want to commit with UTF-8 log message. |
|
46 | 46 | ''' |
|
47 | 47 | from __future__ import absolute_import |
|
48 | 48 | |
|
49 | 49 | import os |
|
50 | 50 | import sys |
|
51 | 51 | |
|
52 | 52 | from mercurial.i18n import _ |
|
53 | 53 | from mercurial import ( |
|
54 | 54 | encoding, |
|
55 | 55 | error, |
|
56 | 56 | pycompat, |
|
57 | 57 | registrar, |
|
58 | 58 | ) |
|
59 | 59 | |
|
60 | 60 | # Note for extension authors: ONLY specify testedwith = 'ships-with-hg-core' for |
|
61 | 61 | # extensions which SHIP WITH MERCURIAL. Non-mainline extensions should |
|
62 | 62 | # be specifying the version(s) of Mercurial they are tested with, or |
|
63 | 63 | # leave the attribute unspecified. |
|
64 | 64 | testedwith = b'ships-with-hg-core' |
|
65 | 65 | |
|
66 | 66 | configtable = {} |
|
67 | 67 | configitem = registrar.configitem(configtable) |
|
68 | 68 | |
|
69 | 69 | # Encoding.encoding may be updated by --encoding option. |
|
70 | 70 | # Use a lambda do delay the resolution. |
|
71 | 71 | configitem( |
|
72 | 72 | b'win32mbcs', b'encoding', default=lambda: encoding.encoding, |
|
73 | 73 | ) |
|
74 | 74 | |
|
75 | 75 | _encoding = None # see extsetup |
|
76 | 76 | |
|
77 | 77 | |
|
78 | 78 | def decode(arg): |
|
79 | 79 | if isinstance(arg, str): |
|
80 | 80 | uarg = arg.decode(_encoding) |
|
81 | 81 | if arg == uarg.encode(_encoding): |
|
82 | 82 | return uarg |
|
83 | 83 | raise UnicodeError(b"Not local encoding") |
|
84 | 84 | elif isinstance(arg, tuple): |
|
85 | 85 | return tuple(map(decode, arg)) |
|
86 | 86 | elif isinstance(arg, list): |
|
87 | 87 | return map(decode, arg) |
|
88 | 88 | elif isinstance(arg, dict): |
|
89 | 89 | for k, v in arg.items(): |
|
90 | 90 | arg[k] = decode(v) |
|
91 | 91 | return arg |
|
92 | 92 | |
|
93 | 93 | |
|
94 | 94 | def encode(arg): |
|
95 | 95 | if isinstance(arg, pycompat.unicode): |
|
96 | 96 | return arg.encode(_encoding) |
|
97 | 97 | elif isinstance(arg, tuple): |
|
98 | 98 | return tuple(map(encode, arg)) |
|
99 | 99 | elif isinstance(arg, list): |
|
100 | 100 | return map(encode, arg) |
|
101 | 101 | elif isinstance(arg, dict): |
|
102 | 102 | for k, v in arg.items(): |
|
103 | 103 | arg[k] = encode(v) |
|
104 | 104 | return arg |
|
105 | 105 | |
|
106 | 106 | |
|
107 | 107 | def appendsep(s): |
|
108 | 108 | # ensure the path ends with os.sep, appending it if necessary. |
|
109 | 109 | try: |
|
110 | 110 | us = decode(s) |
|
111 | 111 | except UnicodeError: |
|
112 | 112 | us = s |
|
113 | 113 | if us and us[-1] not in b':/\\': |
|
114 | 114 | s += pycompat.ossep |
|
115 | 115 | return s |
|
116 | 116 | |
|
117 | 117 | |
|
118 | 118 | def basewrapper(func, argtype, enc, dec, args, kwds): |
|
119 | 119 | # check check already converted, then call original |
|
120 | 120 | for arg in args: |
|
121 | 121 | if isinstance(arg, argtype): |
|
122 | 122 | return func(*args, **kwds) |
|
123 | 123 | |
|
124 | 124 | try: |
|
125 | 125 | # convert string arguments, call func, then convert back the |
|
126 | 126 | # return value. |
|
127 | 127 | return enc(func(*dec(args), **dec(kwds))) |
|
128 | 128 | except UnicodeError: |
|
129 | 129 | raise error.Abort( |
|
130 | 130 | _(b"[win32mbcs] filename conversion failed with" b" %s encoding\n") |
|
131 | 131 | % _encoding |
|
132 | 132 | ) |
|
133 | 133 | |
|
134 | 134 | |
|
135 | 135 | def wrapper(func, args, kwds): |
|
136 | 136 | return basewrapper(func, pycompat.unicode, encode, decode, args, kwds) |
|
137 | 137 | |
|
138 | 138 | |
|
139 | 139 | def reversewrapper(func, args, kwds): |
|
140 | 140 | return basewrapper(func, str, decode, encode, args, kwds) |
|
141 | 141 | |
|
142 | 142 | |
|
143 | 143 | def wrapperforlistdir(func, args, kwds): |
|
144 | 144 | # Ensure 'path' argument ends with os.sep to avoids |
|
145 | 145 | # misinterpreting last 0x5c of MBCS 2nd byte as path separator. |
|
146 | 146 | if args: |
|
147 | 147 | args = list(args) |
|
148 | 148 | args[0] = appendsep(args[0]) |
|
149 | 149 | if b'path' in kwds: |
|
150 | 150 | kwds[b'path'] = appendsep(kwds[b'path']) |
|
151 | 151 | return func(*args, **kwds) |
|
152 | 152 | |
|
153 | 153 | |
|
154 | 154 | def wrapname(name, wrapper): |
|
155 | 155 | module, name = name.rsplit(b'.', 1) |
|
156 | 156 | module = sys.modules[module] |
|
157 | 157 | func = getattr(module, name) |
|
158 | 158 | |
|
159 | 159 | def f(*args, **kwds): |
|
160 | 160 | return wrapper(func, args, kwds) |
|
161 | 161 | |
|
162 | 162 | f.__name__ = func.__name__ |
|
163 | 163 | setattr(module, name, f) |
|
164 | 164 | |
|
165 | 165 | |
|
166 | 166 | # List of functions to be wrapped. |
|
167 | 167 | # NOTE: os.path.dirname() and os.path.basename() are safe because |
|
168 | 168 | # they use result of os.path.split() |
|
169 | 169 | funcs = b'''os.path.join os.path.split os.path.splitext |
|
170 | 170 | os.path.normpath os.makedirs mercurial.util.endswithsep |
|
171 | 171 | mercurial.util.splitpath mercurial.util.fscasesensitive |
|
172 | 172 | mercurial.util.fspath mercurial.util.pconvert mercurial.util.normpath |
|
173 | 173 | mercurial.util.checkwinfilename mercurial.util.checkosfilename |
|
174 | 174 | mercurial.util.split''' |
|
175 | 175 | |
|
176 | 176 | # These functions are required to be called with local encoded string |
|
177 | 177 | # because they expects argument is local encoded string and cause |
|
178 | 178 | # problem with unicode string. |
|
179 | 179 | rfuncs = b'''mercurial.encoding.upper mercurial.encoding.lower |
|
180 | 180 | mercurial.util._filenamebytestr''' |
|
181 | 181 | |
|
182 | 182 | # List of Windows specific functions to be wrapped. |
|
183 | 183 | winfuncs = b'''os.path.splitunc''' |
|
184 | 184 | |
|
185 | 185 | # codec and alias names of sjis and big5 to be faked. |
|
186 | 186 | problematic_encodings = b'''big5 big5-tw csbig5 big5hkscs big5-hkscs |
|
187 | 187 | hkscs cp932 932 ms932 mskanji ms-kanji shift_jis csshiftjis shiftjis |
|
188 | 188 | sjis s_jis shift_jis_2004 shiftjis2004 sjis_2004 sjis2004 |
|
189 | 189 | shift_jisx0213 shiftjisx0213 sjisx0213 s_jisx0213 950 cp950 ms950 ''' |
|
190 | 190 | |
|
191 | 191 | |
|
192 | 192 | def extsetup(ui): |
|
193 | 193 | # TODO: decide use of config section for this extension |
|
194 | 194 | if (not os.path.supports_unicode_filenames) and ( |
|
195 | 195 | pycompat.sysplatform != b'cygwin' |
|
196 | 196 | ): |
|
197 | 197 | ui.warn(_(b"[win32mbcs] cannot activate on this platform.\n")) |
|
198 | 198 | return |
|
199 | 199 | # determine encoding for filename |
|
200 | 200 | global _encoding |
|
201 | 201 | _encoding = ui.config(b'win32mbcs', b'encoding') |
|
202 | 202 | # fake is only for relevant environment. |
|
203 | 203 | if _encoding.lower() in problematic_encodings.split(): |
|
204 | 204 | for f in funcs.split(): |
|
205 | 205 | wrapname(f, wrapper) |
|
206 | 206 | if pycompat.iswindows: |
|
207 | 207 | for f in winfuncs.split(): |
|
208 | 208 | wrapname(f, wrapper) |
|
209 | 209 | wrapname(b"mercurial.util.listdir", wrapperforlistdir) |
|
210 | 210 | wrapname(b"mercurial.windows.listdir", wrapperforlistdir) |
|
211 | 211 | # wrap functions to be called with local byte string arguments |
|
212 | 212 | for f in rfuncs.split(): |
|
213 | 213 | wrapname(f, reversewrapper) |
|
214 | 214 | # Check sys.args manually instead of using ui.debug() because |
|
215 | 215 | # command line options is not yet applied when |
|
216 | 216 | # extensions.loadall() is called. |
|
217 | 217 | if b'--debug' in sys.argv: |
|
218 | ui.write(b"[win32mbcs] activated with encoding: %s\n" % _encoding) | |
|
218 | ui.writenoi18n(b"[win32mbcs] activated with encoding: %s\n" % _encoding) |
@@ -1,2555 +1,2555 b'' | |||
|
1 | 1 | # bundle2.py - generic container format to transmit arbitrary data. |
|
2 | 2 | # |
|
3 | 3 | # Copyright 2013 Facebook, Inc. |
|
4 | 4 | # |
|
5 | 5 | # This software may be used and distributed according to the terms of the |
|
6 | 6 | # GNU General Public License version 2 or any later version. |
|
7 | 7 | """Handling of the new bundle2 format |
|
8 | 8 | |
|
9 | 9 | The goal of bundle2 is to act as an atomically packet to transmit a set of |
|
10 | 10 | payloads in an application agnostic way. It consist in a sequence of "parts" |
|
11 | 11 | that will be handed to and processed by the application layer. |
|
12 | 12 | |
|
13 | 13 | |
|
14 | 14 | General format architecture |
|
15 | 15 | =========================== |
|
16 | 16 | |
|
17 | 17 | The format is architectured as follow |
|
18 | 18 | |
|
19 | 19 | - magic string |
|
20 | 20 | - stream level parameters |
|
21 | 21 | - payload parts (any number) |
|
22 | 22 | - end of stream marker. |
|
23 | 23 | |
|
24 | 24 | the Binary format |
|
25 | 25 | ============================ |
|
26 | 26 | |
|
27 | 27 | All numbers are unsigned and big-endian. |
|
28 | 28 | |
|
29 | 29 | stream level parameters |
|
30 | 30 | ------------------------ |
|
31 | 31 | |
|
32 | 32 | Binary format is as follow |
|
33 | 33 | |
|
34 | 34 | :params size: int32 |
|
35 | 35 | |
|
36 | 36 | The total number of Bytes used by the parameters |
|
37 | 37 | |
|
38 | 38 | :params value: arbitrary number of Bytes |
|
39 | 39 | |
|
40 | 40 | A blob of `params size` containing the serialized version of all stream level |
|
41 | 41 | parameters. |
|
42 | 42 | |
|
43 | 43 | The blob contains a space separated list of parameters. Parameters with value |
|
44 | 44 | are stored in the form `<name>=<value>`. Both name and value are urlquoted. |
|
45 | 45 | |
|
46 | 46 | Empty name are obviously forbidden. |
|
47 | 47 | |
|
48 | 48 | Name MUST start with a letter. If this first letter is lower case, the |
|
49 | 49 | parameter is advisory and can be safely ignored. However when the first |
|
50 | 50 | letter is capital, the parameter is mandatory and the bundling process MUST |
|
51 | 51 | stop if he is not able to proceed it. |
|
52 | 52 | |
|
53 | 53 | Stream parameters use a simple textual format for two main reasons: |
|
54 | 54 | |
|
55 | 55 | - Stream level parameters should remain simple and we want to discourage any |
|
56 | 56 | crazy usage. |
|
57 | 57 | - Textual data allow easy human inspection of a bundle2 header in case of |
|
58 | 58 | troubles. |
|
59 | 59 | |
|
60 | 60 | Any Applicative level options MUST go into a bundle2 part instead. |
|
61 | 61 | |
|
62 | 62 | Payload part |
|
63 | 63 | ------------------------ |
|
64 | 64 | |
|
65 | 65 | Binary format is as follow |
|
66 | 66 | |
|
67 | 67 | :header size: int32 |
|
68 | 68 | |
|
69 | 69 | The total number of Bytes used by the part header. When the header is empty |
|
70 | 70 | (size = 0) this is interpreted as the end of stream marker. |
|
71 | 71 | |
|
72 | 72 | :header: |
|
73 | 73 | |
|
74 | 74 | The header defines how to interpret the part. It contains two piece of |
|
75 | 75 | data: the part type, and the part parameters. |
|
76 | 76 | |
|
77 | 77 | The part type is used to route an application level handler, that can |
|
78 | 78 | interpret payload. |
|
79 | 79 | |
|
80 | 80 | Part parameters are passed to the application level handler. They are |
|
81 | 81 | meant to convey information that will help the application level object to |
|
82 | 82 | interpret the part payload. |
|
83 | 83 | |
|
84 | 84 | The binary format of the header is has follow |
|
85 | 85 | |
|
86 | 86 | :typesize: (one byte) |
|
87 | 87 | |
|
88 | 88 | :parttype: alphanumerical part name (restricted to [a-zA-Z0-9_:-]*) |
|
89 | 89 | |
|
90 | 90 | :partid: A 32bits integer (unique in the bundle) that can be used to refer |
|
91 | 91 | to this part. |
|
92 | 92 | |
|
93 | 93 | :parameters: |
|
94 | 94 | |
|
95 | 95 | Part's parameter may have arbitrary content, the binary structure is:: |
|
96 | 96 | |
|
97 | 97 | <mandatory-count><advisory-count><param-sizes><param-data> |
|
98 | 98 | |
|
99 | 99 | :mandatory-count: 1 byte, number of mandatory parameters |
|
100 | 100 | |
|
101 | 101 | :advisory-count: 1 byte, number of advisory parameters |
|
102 | 102 | |
|
103 | 103 | :param-sizes: |
|
104 | 104 | |
|
105 | 105 | N couple of bytes, where N is the total number of parameters. Each |
|
106 | 106 | couple contains (<size-of-key>, <size-of-value) for one parameter. |
|
107 | 107 | |
|
108 | 108 | :param-data: |
|
109 | 109 | |
|
110 | 110 | A blob of bytes from which each parameter key and value can be |
|
111 | 111 | retrieved using the list of size couples stored in the previous |
|
112 | 112 | field. |
|
113 | 113 | |
|
114 | 114 | Mandatory parameters comes first, then the advisory ones. |
|
115 | 115 | |
|
116 | 116 | Each parameter's key MUST be unique within the part. |
|
117 | 117 | |
|
118 | 118 | :payload: |
|
119 | 119 | |
|
120 | 120 | payload is a series of `<chunksize><chunkdata>`. |
|
121 | 121 | |
|
122 | 122 | `chunksize` is an int32, `chunkdata` are plain bytes (as much as |
|
123 | 123 | `chunksize` says)` The payload part is concluded by a zero size chunk. |
|
124 | 124 | |
|
125 | 125 | The current implementation always produces either zero or one chunk. |
|
126 | 126 | This is an implementation limitation that will ultimately be lifted. |
|
127 | 127 | |
|
128 | 128 | `chunksize` can be negative to trigger special case processing. No such |
|
129 | 129 | processing is in place yet. |
|
130 | 130 | |
|
131 | 131 | Bundle processing |
|
132 | 132 | ============================ |
|
133 | 133 | |
|
134 | 134 | Each part is processed in order using a "part handler". Handler are registered |
|
135 | 135 | for a certain part type. |
|
136 | 136 | |
|
137 | 137 | The matching of a part to its handler is case insensitive. The case of the |
|
138 | 138 | part type is used to know if a part is mandatory or advisory. If the Part type |
|
139 | 139 | contains any uppercase char it is considered mandatory. When no handler is |
|
140 | 140 | known for a Mandatory part, the process is aborted and an exception is raised. |
|
141 | 141 | If the part is advisory and no handler is known, the part is ignored. When the |
|
142 | 142 | process is aborted, the full bundle is still read from the stream to keep the |
|
143 | 143 | channel usable. But none of the part read from an abort are processed. In the |
|
144 | 144 | future, dropping the stream may become an option for channel we do not care to |
|
145 | 145 | preserve. |
|
146 | 146 | """ |
|
147 | 147 | |
|
148 | 148 | from __future__ import absolute_import, division |
|
149 | 149 | |
|
150 | 150 | import collections |
|
151 | 151 | import errno |
|
152 | 152 | import os |
|
153 | 153 | import re |
|
154 | 154 | import string |
|
155 | 155 | import struct |
|
156 | 156 | import sys |
|
157 | 157 | |
|
158 | 158 | from .i18n import _ |
|
159 | 159 | from . import ( |
|
160 | 160 | bookmarks, |
|
161 | 161 | changegroup, |
|
162 | 162 | encoding, |
|
163 | 163 | error, |
|
164 | 164 | node as nodemod, |
|
165 | 165 | obsolete, |
|
166 | 166 | phases, |
|
167 | 167 | pushkey, |
|
168 | 168 | pycompat, |
|
169 | 169 | streamclone, |
|
170 | 170 | tags, |
|
171 | 171 | url, |
|
172 | 172 | util, |
|
173 | 173 | ) |
|
174 | 174 | from .utils import stringutil |
|
175 | 175 | |
|
176 | 176 | urlerr = util.urlerr |
|
177 | 177 | urlreq = util.urlreq |
|
178 | 178 | |
|
179 | 179 | _pack = struct.pack |
|
180 | 180 | _unpack = struct.unpack |
|
181 | 181 | |
|
182 | 182 | _fstreamparamsize = b'>i' |
|
183 | 183 | _fpartheadersize = b'>i' |
|
184 | 184 | _fparttypesize = b'>B' |
|
185 | 185 | _fpartid = b'>I' |
|
186 | 186 | _fpayloadsize = b'>i' |
|
187 | 187 | _fpartparamcount = b'>BB' |
|
188 | 188 | |
|
189 | 189 | preferedchunksize = 32768 |
|
190 | 190 | |
|
191 | 191 | _parttypeforbidden = re.compile(b'[^a-zA-Z0-9_:-]') |
|
192 | 192 | |
|
193 | 193 | |
|
194 | 194 | def outdebug(ui, message): |
|
195 | 195 | """debug regarding output stream (bundling)""" |
|
196 | 196 | if ui.configbool(b'devel', b'bundle2.debug'): |
|
197 | 197 | ui.debug(b'bundle2-output: %s\n' % message) |
|
198 | 198 | |
|
199 | 199 | |
|
200 | 200 | def indebug(ui, message): |
|
201 | 201 | """debug on input stream (unbundling)""" |
|
202 | 202 | if ui.configbool(b'devel', b'bundle2.debug'): |
|
203 | 203 | ui.debug(b'bundle2-input: %s\n' % message) |
|
204 | 204 | |
|
205 | 205 | |
|
206 | 206 | def validateparttype(parttype): |
|
207 | 207 | """raise ValueError if a parttype contains invalid character""" |
|
208 | 208 | if _parttypeforbidden.search(parttype): |
|
209 | 209 | raise ValueError(parttype) |
|
210 | 210 | |
|
211 | 211 | |
|
212 | 212 | def _makefpartparamsizes(nbparams): |
|
213 | 213 | """return a struct format to read part parameter sizes |
|
214 | 214 | |
|
215 | 215 | The number parameters is variable so we need to build that format |
|
216 | 216 | dynamically. |
|
217 | 217 | """ |
|
218 | 218 | return b'>' + (b'BB' * nbparams) |
|
219 | 219 | |
|
220 | 220 | |
|
221 | 221 | parthandlermapping = {} |
|
222 | 222 | |
|
223 | 223 | |
|
224 | 224 | def parthandler(parttype, params=()): |
|
225 | 225 | """decorator that register a function as a bundle2 part handler |
|
226 | 226 | |
|
227 | 227 | eg:: |
|
228 | 228 | |
|
229 | 229 | @parthandler('myparttype', ('mandatory', 'param', 'handled')) |
|
230 | 230 | def myparttypehandler(...): |
|
231 | 231 | '''process a part of type "my part".''' |
|
232 | 232 | ... |
|
233 | 233 | """ |
|
234 | 234 | validateparttype(parttype) |
|
235 | 235 | |
|
236 | 236 | def _decorator(func): |
|
237 | 237 | lparttype = parttype.lower() # enforce lower case matching. |
|
238 | 238 | assert lparttype not in parthandlermapping |
|
239 | 239 | parthandlermapping[lparttype] = func |
|
240 | 240 | func.params = frozenset(params) |
|
241 | 241 | return func |
|
242 | 242 | |
|
243 | 243 | return _decorator |
|
244 | 244 | |
|
245 | 245 | |
|
246 | 246 | class unbundlerecords(object): |
|
247 | 247 | """keep record of what happens during and unbundle |
|
248 | 248 | |
|
249 | 249 | New records are added using `records.add('cat', obj)`. Where 'cat' is a |
|
250 | 250 | category of record and obj is an arbitrary object. |
|
251 | 251 | |
|
252 | 252 | `records['cat']` will return all entries of this category 'cat'. |
|
253 | 253 | |
|
254 | 254 | Iterating on the object itself will yield `('category', obj)` tuples |
|
255 | 255 | for all entries. |
|
256 | 256 | |
|
257 | 257 | All iterations happens in chronological order. |
|
258 | 258 | """ |
|
259 | 259 | |
|
260 | 260 | def __init__(self): |
|
261 | 261 | self._categories = {} |
|
262 | 262 | self._sequences = [] |
|
263 | 263 | self._replies = {} |
|
264 | 264 | |
|
265 | 265 | def add(self, category, entry, inreplyto=None): |
|
266 | 266 | """add a new record of a given category. |
|
267 | 267 | |
|
268 | 268 | The entry can then be retrieved in the list returned by |
|
269 | 269 | self['category'].""" |
|
270 | 270 | self._categories.setdefault(category, []).append(entry) |
|
271 | 271 | self._sequences.append((category, entry)) |
|
272 | 272 | if inreplyto is not None: |
|
273 | 273 | self.getreplies(inreplyto).add(category, entry) |
|
274 | 274 | |
|
275 | 275 | def getreplies(self, partid): |
|
276 | 276 | """get the records that are replies to a specific part""" |
|
277 | 277 | return self._replies.setdefault(partid, unbundlerecords()) |
|
278 | 278 | |
|
279 | 279 | def __getitem__(self, cat): |
|
280 | 280 | return tuple(self._categories.get(cat, ())) |
|
281 | 281 | |
|
282 | 282 | def __iter__(self): |
|
283 | 283 | return iter(self._sequences) |
|
284 | 284 | |
|
285 | 285 | def __len__(self): |
|
286 | 286 | return len(self._sequences) |
|
287 | 287 | |
|
288 | 288 | def __nonzero__(self): |
|
289 | 289 | return bool(self._sequences) |
|
290 | 290 | |
|
291 | 291 | __bool__ = __nonzero__ |
|
292 | 292 | |
|
293 | 293 | |
|
294 | 294 | class bundleoperation(object): |
|
295 | 295 | """an object that represents a single bundling process |
|
296 | 296 | |
|
297 | 297 | Its purpose is to carry unbundle-related objects and states. |
|
298 | 298 | |
|
299 | 299 | A new object should be created at the beginning of each bundle processing. |
|
300 | 300 | The object is to be returned by the processing function. |
|
301 | 301 | |
|
302 | 302 | The object has very little content now it will ultimately contain: |
|
303 | 303 | * an access to the repo the bundle is applied to, |
|
304 | 304 | * a ui object, |
|
305 | 305 | * a way to retrieve a transaction to add changes to the repo, |
|
306 | 306 | * a way to record the result of processing each part, |
|
307 | 307 | * a way to construct a bundle response when applicable. |
|
308 | 308 | """ |
|
309 | 309 | |
|
310 | 310 | def __init__(self, repo, transactiongetter, captureoutput=True, source=b''): |
|
311 | 311 | self.repo = repo |
|
312 | 312 | self.ui = repo.ui |
|
313 | 313 | self.records = unbundlerecords() |
|
314 | 314 | self.reply = None |
|
315 | 315 | self.captureoutput = captureoutput |
|
316 | 316 | self.hookargs = {} |
|
317 | 317 | self._gettransaction = transactiongetter |
|
318 | 318 | # carries value that can modify part behavior |
|
319 | 319 | self.modes = {} |
|
320 | 320 | self.source = source |
|
321 | 321 | |
|
322 | 322 | def gettransaction(self): |
|
323 | 323 | transaction = self._gettransaction() |
|
324 | 324 | |
|
325 | 325 | if self.hookargs: |
|
326 | 326 | # the ones added to the transaction supercede those added |
|
327 | 327 | # to the operation. |
|
328 | 328 | self.hookargs.update(transaction.hookargs) |
|
329 | 329 | transaction.hookargs = self.hookargs |
|
330 | 330 | |
|
331 | 331 | # mark the hookargs as flushed. further attempts to add to |
|
332 | 332 | # hookargs will result in an abort. |
|
333 | 333 | self.hookargs = None |
|
334 | 334 | |
|
335 | 335 | return transaction |
|
336 | 336 | |
|
337 | 337 | def addhookargs(self, hookargs): |
|
338 | 338 | if self.hookargs is None: |
|
339 | 339 | raise error.ProgrammingError( |
|
340 | 340 | b'attempted to add hookargs to ' |
|
341 | 341 | b'operation after transaction started' |
|
342 | 342 | ) |
|
343 | 343 | self.hookargs.update(hookargs) |
|
344 | 344 | |
|
345 | 345 | |
|
346 | 346 | class TransactionUnavailable(RuntimeError): |
|
347 | 347 | pass |
|
348 | 348 | |
|
349 | 349 | |
|
350 | 350 | def _notransaction(): |
|
351 | 351 | """default method to get a transaction while processing a bundle |
|
352 | 352 | |
|
353 | 353 | Raise an exception to highlight the fact that no transaction was expected |
|
354 | 354 | to be created""" |
|
355 | 355 | raise TransactionUnavailable() |
|
356 | 356 | |
|
357 | 357 | |
|
358 | 358 | def applybundle(repo, unbundler, tr, source, url=None, **kwargs): |
|
359 | 359 | # transform me into unbundler.apply() as soon as the freeze is lifted |
|
360 | 360 | if isinstance(unbundler, unbundle20): |
|
361 | 361 | tr.hookargs[b'bundle2'] = b'1' |
|
362 | 362 | if source is not None and b'source' not in tr.hookargs: |
|
363 | 363 | tr.hookargs[b'source'] = source |
|
364 | 364 | if url is not None and b'url' not in tr.hookargs: |
|
365 | 365 | tr.hookargs[b'url'] = url |
|
366 | 366 | return processbundle(repo, unbundler, lambda: tr, source=source) |
|
367 | 367 | else: |
|
368 | 368 | # the transactiongetter won't be used, but we might as well set it |
|
369 | 369 | op = bundleoperation(repo, lambda: tr, source=source) |
|
370 | 370 | _processchangegroup(op, unbundler, tr, source, url, **kwargs) |
|
371 | 371 | return op |
|
372 | 372 | |
|
373 | 373 | |
|
374 | 374 | class partiterator(object): |
|
375 | 375 | def __init__(self, repo, op, unbundler): |
|
376 | 376 | self.repo = repo |
|
377 | 377 | self.op = op |
|
378 | 378 | self.unbundler = unbundler |
|
379 | 379 | self.iterator = None |
|
380 | 380 | self.count = 0 |
|
381 | 381 | self.current = None |
|
382 | 382 | |
|
383 | 383 | def __enter__(self): |
|
384 | 384 | def func(): |
|
385 | 385 | itr = enumerate(self.unbundler.iterparts(), 1) |
|
386 | 386 | for count, p in itr: |
|
387 | 387 | self.count = count |
|
388 | 388 | self.current = p |
|
389 | 389 | yield p |
|
390 | 390 | p.consume() |
|
391 | 391 | self.current = None |
|
392 | 392 | |
|
393 | 393 | self.iterator = func() |
|
394 | 394 | return self.iterator |
|
395 | 395 | |
|
396 | 396 | def __exit__(self, type, exc, tb): |
|
397 | 397 | if not self.iterator: |
|
398 | 398 | return |
|
399 | 399 | |
|
400 | 400 | # Only gracefully abort in a normal exception situation. User aborts |
|
401 | 401 | # like Ctrl+C throw a KeyboardInterrupt which is not a base Exception, |
|
402 | 402 | # and should not gracefully cleanup. |
|
403 | 403 | if isinstance(exc, Exception): |
|
404 | 404 | # Any exceptions seeking to the end of the bundle at this point are |
|
405 | 405 | # almost certainly related to the underlying stream being bad. |
|
406 | 406 | # And, chances are that the exception we're handling is related to |
|
407 | 407 | # getting in that bad state. So, we swallow the seeking error and |
|
408 | 408 | # re-raise the original error. |
|
409 | 409 | seekerror = False |
|
410 | 410 | try: |
|
411 | 411 | if self.current: |
|
412 | 412 | # consume the part content to not corrupt the stream. |
|
413 | 413 | self.current.consume() |
|
414 | 414 | |
|
415 | 415 | for part in self.iterator: |
|
416 | 416 | # consume the bundle content |
|
417 | 417 | part.consume() |
|
418 | 418 | except Exception: |
|
419 | 419 | seekerror = True |
|
420 | 420 | |
|
421 | 421 | # Small hack to let caller code distinguish exceptions from bundle2 |
|
422 | 422 | # processing from processing the old format. This is mostly needed |
|
423 | 423 | # to handle different return codes to unbundle according to the type |
|
424 | 424 | # of bundle. We should probably clean up or drop this return code |
|
425 | 425 | # craziness in a future version. |
|
426 | 426 | exc.duringunbundle2 = True |
|
427 | 427 | salvaged = [] |
|
428 | 428 | replycaps = None |
|
429 | 429 | if self.op.reply is not None: |
|
430 | 430 | salvaged = self.op.reply.salvageoutput() |
|
431 | 431 | replycaps = self.op.reply.capabilities |
|
432 | 432 | exc._replycaps = replycaps |
|
433 | 433 | exc._bundle2salvagedoutput = salvaged |
|
434 | 434 | |
|
435 | 435 | # Re-raising from a variable loses the original stack. So only use |
|
436 | 436 | # that form if we need to. |
|
437 | 437 | if seekerror: |
|
438 | 438 | raise exc |
|
439 | 439 | |
|
440 | 440 | self.repo.ui.debug( |
|
441 | 441 | b'bundle2-input-bundle: %i parts total\n' % self.count |
|
442 | 442 | ) |
|
443 | 443 | |
|
444 | 444 | |
|
445 | 445 | def processbundle(repo, unbundler, transactiongetter=None, op=None, source=b''): |
|
446 | 446 | """This function process a bundle, apply effect to/from a repo |
|
447 | 447 | |
|
448 | 448 | It iterates over each part then searches for and uses the proper handling |
|
449 | 449 | code to process the part. Parts are processed in order. |
|
450 | 450 | |
|
451 | 451 | Unknown Mandatory part will abort the process. |
|
452 | 452 | |
|
453 | 453 | It is temporarily possible to provide a prebuilt bundleoperation to the |
|
454 | 454 | function. This is used to ensure output is properly propagated in case of |
|
455 | 455 | an error during the unbundling. This output capturing part will likely be |
|
456 | 456 | reworked and this ability will probably go away in the process. |
|
457 | 457 | """ |
|
458 | 458 | if op is None: |
|
459 | 459 | if transactiongetter is None: |
|
460 | 460 | transactiongetter = _notransaction |
|
461 | 461 | op = bundleoperation(repo, transactiongetter, source=source) |
|
462 | 462 | # todo: |
|
463 | 463 | # - replace this is a init function soon. |
|
464 | 464 | # - exception catching |
|
465 | 465 | unbundler.params |
|
466 | 466 | if repo.ui.debugflag: |
|
467 | 467 | msg = [b'bundle2-input-bundle:'] |
|
468 | 468 | if unbundler.params: |
|
469 | 469 | msg.append(b' %i params' % len(unbundler.params)) |
|
470 | 470 | if op._gettransaction is None or op._gettransaction is _notransaction: |
|
471 | 471 | msg.append(b' no-transaction') |
|
472 | 472 | else: |
|
473 | 473 | msg.append(b' with-transaction') |
|
474 | 474 | msg.append(b'\n') |
|
475 | 475 | repo.ui.debug(b''.join(msg)) |
|
476 | 476 | |
|
477 | 477 | processparts(repo, op, unbundler) |
|
478 | 478 | |
|
479 | 479 | return op |
|
480 | 480 | |
|
481 | 481 | |
|
482 | 482 | def processparts(repo, op, unbundler): |
|
483 | 483 | with partiterator(repo, op, unbundler) as parts: |
|
484 | 484 | for part in parts: |
|
485 | 485 | _processpart(op, part) |
|
486 | 486 | |
|
487 | 487 | |
|
488 | 488 | def _processchangegroup(op, cg, tr, source, url, **kwargs): |
|
489 | 489 | ret = cg.apply(op.repo, tr, source, url, **kwargs) |
|
490 | 490 | op.records.add(b'changegroup', {b'return': ret,}) |
|
491 | 491 | return ret |
|
492 | 492 | |
|
493 | 493 | |
|
494 | 494 | def _gethandler(op, part): |
|
495 | 495 | status = b'unknown' # used by debug output |
|
496 | 496 | try: |
|
497 | 497 | handler = parthandlermapping.get(part.type) |
|
498 | 498 | if handler is None: |
|
499 | 499 | status = b'unsupported-type' |
|
500 | 500 | raise error.BundleUnknownFeatureError(parttype=part.type) |
|
501 | 501 | indebug(op.ui, b'found a handler for part %s' % part.type) |
|
502 | 502 | unknownparams = part.mandatorykeys - handler.params |
|
503 | 503 | if unknownparams: |
|
504 | 504 | unknownparams = list(unknownparams) |
|
505 | 505 | unknownparams.sort() |
|
506 | 506 | status = b'unsupported-params (%s)' % b', '.join(unknownparams) |
|
507 | 507 | raise error.BundleUnknownFeatureError( |
|
508 | 508 | parttype=part.type, params=unknownparams |
|
509 | 509 | ) |
|
510 | 510 | status = b'supported' |
|
511 | 511 | except error.BundleUnknownFeatureError as exc: |
|
512 | 512 | if part.mandatory: # mandatory parts |
|
513 | 513 | raise |
|
514 | 514 | indebug(op.ui, b'ignoring unsupported advisory part %s' % exc) |
|
515 | 515 | return # skip to part processing |
|
516 | 516 | finally: |
|
517 | 517 | if op.ui.debugflag: |
|
518 | 518 | msg = [b'bundle2-input-part: "%s"' % part.type] |
|
519 | 519 | if not part.mandatory: |
|
520 | 520 | msg.append(b' (advisory)') |
|
521 | 521 | nbmp = len(part.mandatorykeys) |
|
522 | 522 | nbap = len(part.params) - nbmp |
|
523 | 523 | if nbmp or nbap: |
|
524 | 524 | msg.append(b' (params:') |
|
525 | 525 | if nbmp: |
|
526 | 526 | msg.append(b' %i mandatory' % nbmp) |
|
527 | 527 | if nbap: |
|
528 | 528 | msg.append(b' %i advisory' % nbmp) |
|
529 | 529 | msg.append(b')') |
|
530 | 530 | msg.append(b' %s\n' % status) |
|
531 | 531 | op.ui.debug(b''.join(msg)) |
|
532 | 532 | |
|
533 | 533 | return handler |
|
534 | 534 | |
|
535 | 535 | |
|
536 | 536 | def _processpart(op, part): |
|
537 | 537 | """process a single part from a bundle |
|
538 | 538 | |
|
539 | 539 | The part is guaranteed to have been fully consumed when the function exits |
|
540 | 540 | (even if an exception is raised).""" |
|
541 | 541 | handler = _gethandler(op, part) |
|
542 | 542 | if handler is None: |
|
543 | 543 | return |
|
544 | 544 | |
|
545 | 545 | # handler is called outside the above try block so that we don't |
|
546 | 546 | # risk catching KeyErrors from anything other than the |
|
547 | 547 | # parthandlermapping lookup (any KeyError raised by handler() |
|
548 | 548 | # itself represents a defect of a different variety). |
|
549 | 549 | output = None |
|
550 | 550 | if op.captureoutput and op.reply is not None: |
|
551 | 551 | op.ui.pushbuffer(error=True, subproc=True) |
|
552 | 552 | output = b'' |
|
553 | 553 | try: |
|
554 | 554 | handler(op, part) |
|
555 | 555 | finally: |
|
556 | 556 | if output is not None: |
|
557 | 557 | output = op.ui.popbuffer() |
|
558 | 558 | if output: |
|
559 | 559 | outpart = op.reply.newpart(b'output', data=output, mandatory=False) |
|
560 | 560 | outpart.addparam( |
|
561 | 561 | b'in-reply-to', pycompat.bytestr(part.id), mandatory=False |
|
562 | 562 | ) |
|
563 | 563 | |
|
564 | 564 | |
|
565 | 565 | def decodecaps(blob): |
|
566 | 566 | """decode a bundle2 caps bytes blob into a dictionary |
|
567 | 567 | |
|
568 | 568 | The blob is a list of capabilities (one per line) |
|
569 | 569 | Capabilities may have values using a line of the form:: |
|
570 | 570 | |
|
571 | 571 | capability=value1,value2,value3 |
|
572 | 572 | |
|
573 | 573 | The values are always a list.""" |
|
574 | 574 | caps = {} |
|
575 | 575 | for line in blob.splitlines(): |
|
576 | 576 | if not line: |
|
577 | 577 | continue |
|
578 | 578 | if b'=' not in line: |
|
579 | 579 | key, vals = line, () |
|
580 | 580 | else: |
|
581 | 581 | key, vals = line.split(b'=', 1) |
|
582 | 582 | vals = vals.split(b',') |
|
583 | 583 | key = urlreq.unquote(key) |
|
584 | 584 | vals = [urlreq.unquote(v) for v in vals] |
|
585 | 585 | caps[key] = vals |
|
586 | 586 | return caps |
|
587 | 587 | |
|
588 | 588 | |
|
589 | 589 | def encodecaps(caps): |
|
590 | 590 | """encode a bundle2 caps dictionary into a bytes blob""" |
|
591 | 591 | chunks = [] |
|
592 | 592 | for ca in sorted(caps): |
|
593 | 593 | vals = caps[ca] |
|
594 | 594 | ca = urlreq.quote(ca) |
|
595 | 595 | vals = [urlreq.quote(v) for v in vals] |
|
596 | 596 | if vals: |
|
597 | 597 | ca = b"%s=%s" % (ca, b','.join(vals)) |
|
598 | 598 | chunks.append(ca) |
|
599 | 599 | return b'\n'.join(chunks) |
|
600 | 600 | |
|
601 | 601 | |
|
602 | 602 | bundletypes = { |
|
603 | 603 | b"": (b"", b'UN'), # only when using unbundle on ssh and old http servers |
|
604 | 604 | # since the unification ssh accepts a header but there |
|
605 | 605 | # is no capability signaling it. |
|
606 | 606 | b"HG20": (), # special-cased below |
|
607 | 607 | b"HG10UN": (b"HG10UN", b'UN'), |
|
608 | 608 | b"HG10BZ": (b"HG10", b'BZ'), |
|
609 | 609 | b"HG10GZ": (b"HG10GZ", b'GZ'), |
|
610 | 610 | } |
|
611 | 611 | |
|
612 | 612 | # hgweb uses this list to communicate its preferred type |
|
613 | 613 | bundlepriority = [b'HG10GZ', b'HG10BZ', b'HG10UN'] |
|
614 | 614 | |
|
615 | 615 | |
|
616 | 616 | class bundle20(object): |
|
617 | 617 | """represent an outgoing bundle2 container |
|
618 | 618 | |
|
619 | 619 | Use the `addparam` method to add stream level parameter. and `newpart` to |
|
620 | 620 | populate it. Then call `getchunks` to retrieve all the binary chunks of |
|
621 | 621 | data that compose the bundle2 container.""" |
|
622 | 622 | |
|
623 | 623 | _magicstring = b'HG20' |
|
624 | 624 | |
|
625 | 625 | def __init__(self, ui, capabilities=()): |
|
626 | 626 | self.ui = ui |
|
627 | 627 | self._params = [] |
|
628 | 628 | self._parts = [] |
|
629 | 629 | self.capabilities = dict(capabilities) |
|
630 | 630 | self._compengine = util.compengines.forbundletype(b'UN') |
|
631 | 631 | self._compopts = None |
|
632 | 632 | # If compression is being handled by a consumer of the raw |
|
633 | 633 | # data (e.g. the wire protocol), unsetting this flag tells |
|
634 | 634 | # consumers that the bundle is best left uncompressed. |
|
635 | 635 | self.prefercompressed = True |
|
636 | 636 | |
|
637 | 637 | def setcompression(self, alg, compopts=None): |
|
638 | 638 | """setup core part compression to <alg>""" |
|
639 | 639 | if alg in (None, b'UN'): |
|
640 | 640 | return |
|
641 | 641 | assert not any(n.lower() == b'compression' for n, v in self._params) |
|
642 | 642 | self.addparam(b'Compression', alg) |
|
643 | 643 | self._compengine = util.compengines.forbundletype(alg) |
|
644 | 644 | self._compopts = compopts |
|
645 | 645 | |
|
646 | 646 | @property |
|
647 | 647 | def nbparts(self): |
|
648 | 648 | """total number of parts added to the bundler""" |
|
649 | 649 | return len(self._parts) |
|
650 | 650 | |
|
651 | 651 | # methods used to defines the bundle2 content |
|
652 | 652 | def addparam(self, name, value=None): |
|
653 | 653 | """add a stream level parameter""" |
|
654 | 654 | if not name: |
|
655 | 655 | raise error.ProgrammingError(b'empty parameter name') |
|
656 | 656 | if name[0:1] not in pycompat.bytestr(string.ascii_letters): |
|
657 | 657 | raise error.ProgrammingError( |
|
658 | 658 | b'non letter first character: %s' % name |
|
659 | 659 | ) |
|
660 | 660 | self._params.append((name, value)) |
|
661 | 661 | |
|
662 | 662 | def addpart(self, part): |
|
663 | 663 | """add a new part to the bundle2 container |
|
664 | 664 | |
|
665 | 665 | Parts contains the actual applicative payload.""" |
|
666 | 666 | assert part.id is None |
|
667 | 667 | part.id = len(self._parts) # very cheap counter |
|
668 | 668 | self._parts.append(part) |
|
669 | 669 | |
|
670 | 670 | def newpart(self, typeid, *args, **kwargs): |
|
671 | 671 | """create a new part and add it to the containers |
|
672 | 672 | |
|
673 | 673 | As the part is directly added to the containers. For now, this means |
|
674 | 674 | that any failure to properly initialize the part after calling |
|
675 | 675 | ``newpart`` should result in a failure of the whole bundling process. |
|
676 | 676 | |
|
677 | 677 | You can still fall back to manually create and add if you need better |
|
678 | 678 | control.""" |
|
679 | 679 | part = bundlepart(typeid, *args, **kwargs) |
|
680 | 680 | self.addpart(part) |
|
681 | 681 | return part |
|
682 | 682 | |
|
683 | 683 | # methods used to generate the bundle2 stream |
|
684 | 684 | def getchunks(self): |
|
685 | 685 | if self.ui.debugflag: |
|
686 | 686 | msg = [b'bundle2-output-bundle: "%s",' % self._magicstring] |
|
687 | 687 | if self._params: |
|
688 | 688 | msg.append(b' (%i params)' % len(self._params)) |
|
689 | 689 | msg.append(b' %i parts total\n' % len(self._parts)) |
|
690 | 690 | self.ui.debug(b''.join(msg)) |
|
691 | 691 | outdebug(self.ui, b'start emission of %s stream' % self._magicstring) |
|
692 | 692 | yield self._magicstring |
|
693 | 693 | param = self._paramchunk() |
|
694 | 694 | outdebug(self.ui, b'bundle parameter: %s' % param) |
|
695 | 695 | yield _pack(_fstreamparamsize, len(param)) |
|
696 | 696 | if param: |
|
697 | 697 | yield param |
|
698 | 698 | for chunk in self._compengine.compressstream( |
|
699 | 699 | self._getcorechunk(), self._compopts |
|
700 | 700 | ): |
|
701 | 701 | yield chunk |
|
702 | 702 | |
|
703 | 703 | def _paramchunk(self): |
|
704 | 704 | """return a encoded version of all stream parameters""" |
|
705 | 705 | blocks = [] |
|
706 | 706 | for par, value in self._params: |
|
707 | 707 | par = urlreq.quote(par) |
|
708 | 708 | if value is not None: |
|
709 | 709 | value = urlreq.quote(value) |
|
710 | 710 | par = b'%s=%s' % (par, value) |
|
711 | 711 | blocks.append(par) |
|
712 | 712 | return b' '.join(blocks) |
|
713 | 713 | |
|
714 | 714 | def _getcorechunk(self): |
|
715 | 715 | """yield chunk for the core part of the bundle |
|
716 | 716 | |
|
717 | 717 | (all but headers and parameters)""" |
|
718 | 718 | outdebug(self.ui, b'start of parts') |
|
719 | 719 | for part in self._parts: |
|
720 | 720 | outdebug(self.ui, b'bundle part: "%s"' % part.type) |
|
721 | 721 | for chunk in part.getchunks(ui=self.ui): |
|
722 | 722 | yield chunk |
|
723 | 723 | outdebug(self.ui, b'end of bundle') |
|
724 | 724 | yield _pack(_fpartheadersize, 0) |
|
725 | 725 | |
|
726 | 726 | def salvageoutput(self): |
|
727 | 727 | """return a list with a copy of all output parts in the bundle |
|
728 | 728 | |
|
729 | 729 | This is meant to be used during error handling to make sure we preserve |
|
730 | 730 | server output""" |
|
731 | 731 | salvaged = [] |
|
732 | 732 | for part in self._parts: |
|
733 | 733 | if part.type.startswith(b'output'): |
|
734 | 734 | salvaged.append(part.copy()) |
|
735 | 735 | return salvaged |
|
736 | 736 | |
|
737 | 737 | |
|
738 | 738 | class unpackermixin(object): |
|
739 | 739 | """A mixin to extract bytes and struct data from a stream""" |
|
740 | 740 | |
|
741 | 741 | def __init__(self, fp): |
|
742 | 742 | self._fp = fp |
|
743 | 743 | |
|
744 | 744 | def _unpack(self, format): |
|
745 | 745 | """unpack this struct format from the stream |
|
746 | 746 | |
|
747 | 747 | This method is meant for internal usage by the bundle2 protocol only. |
|
748 | 748 | They directly manipulate the low level stream including bundle2 level |
|
749 | 749 | instruction. |
|
750 | 750 | |
|
751 | 751 | Do not use it to implement higher-level logic or methods.""" |
|
752 | 752 | data = self._readexact(struct.calcsize(format)) |
|
753 | 753 | return _unpack(format, data) |
|
754 | 754 | |
|
755 | 755 | def _readexact(self, size): |
|
756 | 756 | """read exactly <size> bytes from the stream |
|
757 | 757 | |
|
758 | 758 | This method is meant for internal usage by the bundle2 protocol only. |
|
759 | 759 | They directly manipulate the low level stream including bundle2 level |
|
760 | 760 | instruction. |
|
761 | 761 | |
|
762 | 762 | Do not use it to implement higher-level logic or methods.""" |
|
763 | 763 | return changegroup.readexactly(self._fp, size) |
|
764 | 764 | |
|
765 | 765 | |
|
766 | 766 | def getunbundler(ui, fp, magicstring=None): |
|
767 | 767 | """return a valid unbundler object for a given magicstring""" |
|
768 | 768 | if magicstring is None: |
|
769 | 769 | magicstring = changegroup.readexactly(fp, 4) |
|
770 | 770 | magic, version = magicstring[0:2], magicstring[2:4] |
|
771 | 771 | if magic != b'HG': |
|
772 | 772 | ui.debug( |
|
773 | 773 | b"error: invalid magic: %r (version %r), should be 'HG'\n" |
|
774 | 774 | % (magic, version) |
|
775 | 775 | ) |
|
776 | 776 | raise error.Abort(_(b'not a Mercurial bundle')) |
|
777 | 777 | unbundlerclass = formatmap.get(version) |
|
778 | 778 | if unbundlerclass is None: |
|
779 | 779 | raise error.Abort(_(b'unknown bundle version %s') % version) |
|
780 | 780 | unbundler = unbundlerclass(ui, fp) |
|
781 | 781 | indebug(ui, b'start processing of %s stream' % magicstring) |
|
782 | 782 | return unbundler |
|
783 | 783 | |
|
784 | 784 | |
|
785 | 785 | class unbundle20(unpackermixin): |
|
786 | 786 | """interpret a bundle2 stream |
|
787 | 787 | |
|
788 | 788 | This class is fed with a binary stream and yields parts through its |
|
789 | 789 | `iterparts` methods.""" |
|
790 | 790 | |
|
791 | 791 | _magicstring = b'HG20' |
|
792 | 792 | |
|
793 | 793 | def __init__(self, ui, fp): |
|
794 | 794 | """If header is specified, we do not read it out of the stream.""" |
|
795 | 795 | self.ui = ui |
|
796 | 796 | self._compengine = util.compengines.forbundletype(b'UN') |
|
797 | 797 | self._compressed = None |
|
798 | 798 | super(unbundle20, self).__init__(fp) |
|
799 | 799 | |
|
800 | 800 | @util.propertycache |
|
801 | 801 | def params(self): |
|
802 | 802 | """dictionary of stream level parameters""" |
|
803 | 803 | indebug(self.ui, b'reading bundle2 stream parameters') |
|
804 | 804 | params = {} |
|
805 | 805 | paramssize = self._unpack(_fstreamparamsize)[0] |
|
806 | 806 | if paramssize < 0: |
|
807 | 807 | raise error.BundleValueError( |
|
808 | 808 | b'negative bundle param size: %i' % paramssize |
|
809 | 809 | ) |
|
810 | 810 | if paramssize: |
|
811 | 811 | params = self._readexact(paramssize) |
|
812 | 812 | params = self._processallparams(params) |
|
813 | 813 | return params |
|
814 | 814 | |
|
815 | 815 | def _processallparams(self, paramsblock): |
|
816 | 816 | """""" |
|
817 | 817 | params = util.sortdict() |
|
818 | 818 | for p in paramsblock.split(b' '): |
|
819 | 819 | p = p.split(b'=', 1) |
|
820 | 820 | p = [urlreq.unquote(i) for i in p] |
|
821 | 821 | if len(p) < 2: |
|
822 | 822 | p.append(None) |
|
823 | 823 | self._processparam(*p) |
|
824 | 824 | params[p[0]] = p[1] |
|
825 | 825 | return params |
|
826 | 826 | |
|
827 | 827 | def _processparam(self, name, value): |
|
828 | 828 | """process a parameter, applying its effect if needed |
|
829 | 829 | |
|
830 | 830 | Parameter starting with a lower case letter are advisory and will be |
|
831 | 831 | ignored when unknown. Those starting with an upper case letter are |
|
832 | 832 | mandatory and will this function will raise a KeyError when unknown. |
|
833 | 833 | |
|
834 | 834 | Note: no option are currently supported. Any input will be either |
|
835 | 835 | ignored or failing. |
|
836 | 836 | """ |
|
837 | 837 | if not name: |
|
838 | 838 | raise ValueError(r'empty parameter name') |
|
839 | 839 | if name[0:1] not in pycompat.bytestr(string.ascii_letters): |
|
840 | 840 | raise ValueError(r'non letter first character: %s' % name) |
|
841 | 841 | try: |
|
842 | 842 | handler = b2streamparamsmap[name.lower()] |
|
843 | 843 | except KeyError: |
|
844 | 844 | if name[0:1].islower(): |
|
845 | 845 | indebug(self.ui, b"ignoring unknown parameter %s" % name) |
|
846 | 846 | else: |
|
847 | 847 | raise error.BundleUnknownFeatureError(params=(name,)) |
|
848 | 848 | else: |
|
849 | 849 | handler(self, name, value) |
|
850 | 850 | |
|
851 | 851 | def _forwardchunks(self): |
|
852 | 852 | """utility to transfer a bundle2 as binary |
|
853 | 853 | |
|
854 | 854 | This is made necessary by the fact the 'getbundle' command over 'ssh' |
|
855 | 855 | have no way to know then the reply end, relying on the bundle to be |
|
856 | 856 | interpreted to know its end. This is terrible and we are sorry, but we |
|
857 | 857 | needed to move forward to get general delta enabled. |
|
858 | 858 | """ |
|
859 | 859 | yield self._magicstring |
|
860 | 860 | assert b'params' not in vars(self) |
|
861 | 861 | paramssize = self._unpack(_fstreamparamsize)[0] |
|
862 | 862 | if paramssize < 0: |
|
863 | 863 | raise error.BundleValueError( |
|
864 | 864 | b'negative bundle param size: %i' % paramssize |
|
865 | 865 | ) |
|
866 | 866 | if paramssize: |
|
867 | 867 | params = self._readexact(paramssize) |
|
868 | 868 | self._processallparams(params) |
|
869 | 869 | # The payload itself is decompressed below, so drop |
|
870 | 870 | # the compression parameter passed down to compensate. |
|
871 | 871 | outparams = [] |
|
872 | 872 | for p in params.split(b' '): |
|
873 | 873 | k, v = p.split(b'=', 1) |
|
874 | 874 | if k.lower() != b'compression': |
|
875 | 875 | outparams.append(p) |
|
876 | 876 | outparams = b' '.join(outparams) |
|
877 | 877 | yield _pack(_fstreamparamsize, len(outparams)) |
|
878 | 878 | yield outparams |
|
879 | 879 | else: |
|
880 | 880 | yield _pack(_fstreamparamsize, paramssize) |
|
881 | 881 | # From there, payload might need to be decompressed |
|
882 | 882 | self._fp = self._compengine.decompressorreader(self._fp) |
|
883 | 883 | emptycount = 0 |
|
884 | 884 | while emptycount < 2: |
|
885 | 885 | # so we can brainlessly loop |
|
886 | 886 | assert _fpartheadersize == _fpayloadsize |
|
887 | 887 | size = self._unpack(_fpartheadersize)[0] |
|
888 | 888 | yield _pack(_fpartheadersize, size) |
|
889 | 889 | if size: |
|
890 | 890 | emptycount = 0 |
|
891 | 891 | else: |
|
892 | 892 | emptycount += 1 |
|
893 | 893 | continue |
|
894 | 894 | if size == flaginterrupt: |
|
895 | 895 | continue |
|
896 | 896 | elif size < 0: |
|
897 | 897 | raise error.BundleValueError(b'negative chunk size: %i') |
|
898 | 898 | yield self._readexact(size) |
|
899 | 899 | |
|
900 | 900 | def iterparts(self, seekable=False): |
|
901 | 901 | """yield all parts contained in the stream""" |
|
902 | 902 | cls = seekableunbundlepart if seekable else unbundlepart |
|
903 | 903 | # make sure param have been loaded |
|
904 | 904 | self.params |
|
905 | 905 | # From there, payload need to be decompressed |
|
906 | 906 | self._fp = self._compengine.decompressorreader(self._fp) |
|
907 | 907 | indebug(self.ui, b'start extraction of bundle2 parts') |
|
908 | 908 | headerblock = self._readpartheader() |
|
909 | 909 | while headerblock is not None: |
|
910 | 910 | part = cls(self.ui, headerblock, self._fp) |
|
911 | 911 | yield part |
|
912 | 912 | # Ensure part is fully consumed so we can start reading the next |
|
913 | 913 | # part. |
|
914 | 914 | part.consume() |
|
915 | 915 | |
|
916 | 916 | headerblock = self._readpartheader() |
|
917 | 917 | indebug(self.ui, b'end of bundle2 stream') |
|
918 | 918 | |
|
919 | 919 | def _readpartheader(self): |
|
920 | 920 | """reads a part header size and return the bytes blob |
|
921 | 921 | |
|
922 | 922 | returns None if empty""" |
|
923 | 923 | headersize = self._unpack(_fpartheadersize)[0] |
|
924 | 924 | if headersize < 0: |
|
925 | 925 | raise error.BundleValueError( |
|
926 | 926 | b'negative part header size: %i' % headersize |
|
927 | 927 | ) |
|
928 | 928 | indebug(self.ui, b'part header size: %i' % headersize) |
|
929 | 929 | if headersize: |
|
930 | 930 | return self._readexact(headersize) |
|
931 | 931 | return None |
|
932 | 932 | |
|
933 | 933 | def compressed(self): |
|
934 | 934 | self.params # load params |
|
935 | 935 | return self._compressed |
|
936 | 936 | |
|
937 | 937 | def close(self): |
|
938 | 938 | """close underlying file""" |
|
939 | 939 | if util.safehasattr(self._fp, b'close'): |
|
940 | 940 | return self._fp.close() |
|
941 | 941 | |
|
942 | 942 | |
|
943 | 943 | formatmap = {b'20': unbundle20} |
|
944 | 944 | |
|
945 | 945 | b2streamparamsmap = {} |
|
946 | 946 | |
|
947 | 947 | |
|
948 | 948 | def b2streamparamhandler(name): |
|
949 | 949 | """register a handler for a stream level parameter""" |
|
950 | 950 | |
|
951 | 951 | def decorator(func): |
|
952 | 952 | assert name not in formatmap |
|
953 | 953 | b2streamparamsmap[name] = func |
|
954 | 954 | return func |
|
955 | 955 | |
|
956 | 956 | return decorator |
|
957 | 957 | |
|
958 | 958 | |
|
959 | 959 | @b2streamparamhandler(b'compression') |
|
960 | 960 | def processcompression(unbundler, param, value): |
|
961 | 961 | """read compression parameter and install payload decompression""" |
|
962 | 962 | if value not in util.compengines.supportedbundletypes: |
|
963 | 963 | raise error.BundleUnknownFeatureError(params=(param,), values=(value,)) |
|
964 | 964 | unbundler._compengine = util.compengines.forbundletype(value) |
|
965 | 965 | if value is not None: |
|
966 | 966 | unbundler._compressed = True |
|
967 | 967 | |
|
968 | 968 | |
|
969 | 969 | class bundlepart(object): |
|
970 | 970 | """A bundle2 part contains application level payload |
|
971 | 971 | |
|
972 | 972 | The part `type` is used to route the part to the application level |
|
973 | 973 | handler. |
|
974 | 974 | |
|
975 | 975 | The part payload is contained in ``part.data``. It could be raw bytes or a |
|
976 | 976 | generator of byte chunks. |
|
977 | 977 | |
|
978 | 978 | You can add parameters to the part using the ``addparam`` method. |
|
979 | 979 | Parameters can be either mandatory (default) or advisory. Remote side |
|
980 | 980 | should be able to safely ignore the advisory ones. |
|
981 | 981 | |
|
982 | 982 | Both data and parameters cannot be modified after the generation has begun. |
|
983 | 983 | """ |
|
984 | 984 | |
|
985 | 985 | def __init__( |
|
986 | 986 | self, |
|
987 | 987 | parttype, |
|
988 | 988 | mandatoryparams=(), |
|
989 | 989 | advisoryparams=(), |
|
990 | 990 | data=b'', |
|
991 | 991 | mandatory=True, |
|
992 | 992 | ): |
|
993 | 993 | validateparttype(parttype) |
|
994 | 994 | self.id = None |
|
995 | 995 | self.type = parttype |
|
996 | 996 | self._data = data |
|
997 | 997 | self._mandatoryparams = list(mandatoryparams) |
|
998 | 998 | self._advisoryparams = list(advisoryparams) |
|
999 | 999 | # checking for duplicated entries |
|
1000 | 1000 | self._seenparams = set() |
|
1001 | 1001 | for pname, __ in self._mandatoryparams + self._advisoryparams: |
|
1002 | 1002 | if pname in self._seenparams: |
|
1003 | 1003 | raise error.ProgrammingError(b'duplicated params: %s' % pname) |
|
1004 | 1004 | self._seenparams.add(pname) |
|
1005 | 1005 | # status of the part's generation: |
|
1006 | 1006 | # - None: not started, |
|
1007 | 1007 | # - False: currently generated, |
|
1008 | 1008 | # - True: generation done. |
|
1009 | 1009 | self._generated = None |
|
1010 | 1010 | self.mandatory = mandatory |
|
1011 | 1011 | |
|
1012 | 1012 | def __repr__(self): |
|
1013 | 1013 | cls = b"%s.%s" % (self.__class__.__module__, self.__class__.__name__) |
|
1014 | 1014 | return b'<%s object at %x; id: %s; type: %s; mandatory: %s>' % ( |
|
1015 | 1015 | cls, |
|
1016 | 1016 | id(self), |
|
1017 | 1017 | self.id, |
|
1018 | 1018 | self.type, |
|
1019 | 1019 | self.mandatory, |
|
1020 | 1020 | ) |
|
1021 | 1021 | |
|
1022 | 1022 | def copy(self): |
|
1023 | 1023 | """return a copy of the part |
|
1024 | 1024 | |
|
1025 | 1025 | The new part have the very same content but no partid assigned yet. |
|
1026 | 1026 | Parts with generated data cannot be copied.""" |
|
1027 | 1027 | assert not util.safehasattr(self.data, b'next') |
|
1028 | 1028 | return self.__class__( |
|
1029 | 1029 | self.type, |
|
1030 | 1030 | self._mandatoryparams, |
|
1031 | 1031 | self._advisoryparams, |
|
1032 | 1032 | self._data, |
|
1033 | 1033 | self.mandatory, |
|
1034 | 1034 | ) |
|
1035 | 1035 | |
|
1036 | 1036 | # methods used to defines the part content |
|
1037 | 1037 | @property |
|
1038 | 1038 | def data(self): |
|
1039 | 1039 | return self._data |
|
1040 | 1040 | |
|
1041 | 1041 | @data.setter |
|
1042 | 1042 | def data(self, data): |
|
1043 | 1043 | if self._generated is not None: |
|
1044 | 1044 | raise error.ReadOnlyPartError(b'part is being generated') |
|
1045 | 1045 | self._data = data |
|
1046 | 1046 | |
|
1047 | 1047 | @property |
|
1048 | 1048 | def mandatoryparams(self): |
|
1049 | 1049 | # make it an immutable tuple to force people through ``addparam`` |
|
1050 | 1050 | return tuple(self._mandatoryparams) |
|
1051 | 1051 | |
|
1052 | 1052 | @property |
|
1053 | 1053 | def advisoryparams(self): |
|
1054 | 1054 | # make it an immutable tuple to force people through ``addparam`` |
|
1055 | 1055 | return tuple(self._advisoryparams) |
|
1056 | 1056 | |
|
1057 | 1057 | def addparam(self, name, value=b'', mandatory=True): |
|
1058 | 1058 | """add a parameter to the part |
|
1059 | 1059 | |
|
1060 | 1060 | If 'mandatory' is set to True, the remote handler must claim support |
|
1061 | 1061 | for this parameter or the unbundling will be aborted. |
|
1062 | 1062 | |
|
1063 | 1063 | The 'name' and 'value' cannot exceed 255 bytes each. |
|
1064 | 1064 | """ |
|
1065 | 1065 | if self._generated is not None: |
|
1066 | 1066 | raise error.ReadOnlyPartError(b'part is being generated') |
|
1067 | 1067 | if name in self._seenparams: |
|
1068 | 1068 | raise ValueError(b'duplicated params: %s' % name) |
|
1069 | 1069 | self._seenparams.add(name) |
|
1070 | 1070 | params = self._advisoryparams |
|
1071 | 1071 | if mandatory: |
|
1072 | 1072 | params = self._mandatoryparams |
|
1073 | 1073 | params.append((name, value)) |
|
1074 | 1074 | |
|
1075 | 1075 | # methods used to generates the bundle2 stream |
|
1076 | 1076 | def getchunks(self, ui): |
|
1077 | 1077 | if self._generated is not None: |
|
1078 | 1078 | raise error.ProgrammingError(b'part can only be consumed once') |
|
1079 | 1079 | self._generated = False |
|
1080 | 1080 | |
|
1081 | 1081 | if ui.debugflag: |
|
1082 | 1082 | msg = [b'bundle2-output-part: "%s"' % self.type] |
|
1083 | 1083 | if not self.mandatory: |
|
1084 | 1084 | msg.append(b' (advisory)') |
|
1085 | 1085 | nbmp = len(self.mandatoryparams) |
|
1086 | 1086 | nbap = len(self.advisoryparams) |
|
1087 | 1087 | if nbmp or nbap: |
|
1088 | 1088 | msg.append(b' (params:') |
|
1089 | 1089 | if nbmp: |
|
1090 | 1090 | msg.append(b' %i mandatory' % nbmp) |
|
1091 | 1091 | if nbap: |
|
1092 | 1092 | msg.append(b' %i advisory' % nbmp) |
|
1093 | 1093 | msg.append(b')') |
|
1094 | 1094 | if not self.data: |
|
1095 | 1095 | msg.append(b' empty payload') |
|
1096 | 1096 | elif util.safehasattr(self.data, b'next') or util.safehasattr( |
|
1097 | 1097 | self.data, b'__next__' |
|
1098 | 1098 | ): |
|
1099 | 1099 | msg.append(b' streamed payload') |
|
1100 | 1100 | else: |
|
1101 | 1101 | msg.append(b' %i bytes payload' % len(self.data)) |
|
1102 | 1102 | msg.append(b'\n') |
|
1103 | 1103 | ui.debug(b''.join(msg)) |
|
1104 | 1104 | |
|
1105 | 1105 | #### header |
|
1106 | 1106 | if self.mandatory: |
|
1107 | 1107 | parttype = self.type.upper() |
|
1108 | 1108 | else: |
|
1109 | 1109 | parttype = self.type.lower() |
|
1110 | 1110 | outdebug(ui, b'part %s: "%s"' % (pycompat.bytestr(self.id), parttype)) |
|
1111 | 1111 | ## parttype |
|
1112 | 1112 | header = [ |
|
1113 | 1113 | _pack(_fparttypesize, len(parttype)), |
|
1114 | 1114 | parttype, |
|
1115 | 1115 | _pack(_fpartid, self.id), |
|
1116 | 1116 | ] |
|
1117 | 1117 | ## parameters |
|
1118 | 1118 | # count |
|
1119 | 1119 | manpar = self.mandatoryparams |
|
1120 | 1120 | advpar = self.advisoryparams |
|
1121 | 1121 | header.append(_pack(_fpartparamcount, len(manpar), len(advpar))) |
|
1122 | 1122 | # size |
|
1123 | 1123 | parsizes = [] |
|
1124 | 1124 | for key, value in manpar: |
|
1125 | 1125 | parsizes.append(len(key)) |
|
1126 | 1126 | parsizes.append(len(value)) |
|
1127 | 1127 | for key, value in advpar: |
|
1128 | 1128 | parsizes.append(len(key)) |
|
1129 | 1129 | parsizes.append(len(value)) |
|
1130 | 1130 | paramsizes = _pack(_makefpartparamsizes(len(parsizes) // 2), *parsizes) |
|
1131 | 1131 | header.append(paramsizes) |
|
1132 | 1132 | # key, value |
|
1133 | 1133 | for key, value in manpar: |
|
1134 | 1134 | header.append(key) |
|
1135 | 1135 | header.append(value) |
|
1136 | 1136 | for key, value in advpar: |
|
1137 | 1137 | header.append(key) |
|
1138 | 1138 | header.append(value) |
|
1139 | 1139 | ## finalize header |
|
1140 | 1140 | try: |
|
1141 | 1141 | headerchunk = b''.join(header) |
|
1142 | 1142 | except TypeError: |
|
1143 | 1143 | raise TypeError( |
|
1144 | 1144 | r'Found a non-bytes trying to ' |
|
1145 | 1145 | r'build bundle part header: %r' % header |
|
1146 | 1146 | ) |
|
1147 | 1147 | outdebug(ui, b'header chunk size: %i' % len(headerchunk)) |
|
1148 | 1148 | yield _pack(_fpartheadersize, len(headerchunk)) |
|
1149 | 1149 | yield headerchunk |
|
1150 | 1150 | ## payload |
|
1151 | 1151 | try: |
|
1152 | 1152 | for chunk in self._payloadchunks(): |
|
1153 | 1153 | outdebug(ui, b'payload chunk size: %i' % len(chunk)) |
|
1154 | 1154 | yield _pack(_fpayloadsize, len(chunk)) |
|
1155 | 1155 | yield chunk |
|
1156 | 1156 | except GeneratorExit: |
|
1157 | 1157 | # GeneratorExit means that nobody is listening for our |
|
1158 | 1158 | # results anyway, so just bail quickly rather than trying |
|
1159 | 1159 | # to produce an error part. |
|
1160 | 1160 | ui.debug(b'bundle2-generatorexit\n') |
|
1161 | 1161 | raise |
|
1162 | 1162 | except BaseException as exc: |
|
1163 | 1163 | bexc = stringutil.forcebytestr(exc) |
|
1164 | 1164 | # backup exception data for later |
|
1165 | 1165 | ui.debug( |
|
1166 | 1166 | b'bundle2-input-stream-interrupt: encoding exception %s' % bexc |
|
1167 | 1167 | ) |
|
1168 | 1168 | tb = sys.exc_info()[2] |
|
1169 | 1169 | msg = b'unexpected error: %s' % bexc |
|
1170 | 1170 | interpart = bundlepart( |
|
1171 | 1171 | b'error:abort', [(b'message', msg)], mandatory=False |
|
1172 | 1172 | ) |
|
1173 | 1173 | interpart.id = 0 |
|
1174 | 1174 | yield _pack(_fpayloadsize, -1) |
|
1175 | 1175 | for chunk in interpart.getchunks(ui=ui): |
|
1176 | 1176 | yield chunk |
|
1177 | 1177 | outdebug(ui, b'closing payload chunk') |
|
1178 | 1178 | # abort current part payload |
|
1179 | 1179 | yield _pack(_fpayloadsize, 0) |
|
1180 | 1180 | pycompat.raisewithtb(exc, tb) |
|
1181 | 1181 | # end of payload |
|
1182 | 1182 | outdebug(ui, b'closing payload chunk') |
|
1183 | 1183 | yield _pack(_fpayloadsize, 0) |
|
1184 | 1184 | self._generated = True |
|
1185 | 1185 | |
|
1186 | 1186 | def _payloadchunks(self): |
|
1187 | 1187 | """yield chunks of a the part payload |
|
1188 | 1188 | |
|
1189 | 1189 | Exists to handle the different methods to provide data to a part.""" |
|
1190 | 1190 | # we only support fixed size data now. |
|
1191 | 1191 | # This will be improved in the future. |
|
1192 | 1192 | if util.safehasattr(self.data, b'next') or util.safehasattr( |
|
1193 | 1193 | self.data, b'__next__' |
|
1194 | 1194 | ): |
|
1195 | 1195 | buff = util.chunkbuffer(self.data) |
|
1196 | 1196 | chunk = buff.read(preferedchunksize) |
|
1197 | 1197 | while chunk: |
|
1198 | 1198 | yield chunk |
|
1199 | 1199 | chunk = buff.read(preferedchunksize) |
|
1200 | 1200 | elif len(self.data): |
|
1201 | 1201 | yield self.data |
|
1202 | 1202 | |
|
1203 | 1203 | |
|
1204 | 1204 | flaginterrupt = -1 |
|
1205 | 1205 | |
|
1206 | 1206 | |
|
1207 | 1207 | class interrupthandler(unpackermixin): |
|
1208 | 1208 | """read one part and process it with restricted capability |
|
1209 | 1209 | |
|
1210 | 1210 | This allows to transmit exception raised on the producer size during part |
|
1211 | 1211 | iteration while the consumer is reading a part. |
|
1212 | 1212 | |
|
1213 | 1213 | Part processed in this manner only have access to a ui object,""" |
|
1214 | 1214 | |
|
1215 | 1215 | def __init__(self, ui, fp): |
|
1216 | 1216 | super(interrupthandler, self).__init__(fp) |
|
1217 | 1217 | self.ui = ui |
|
1218 | 1218 | |
|
1219 | 1219 | def _readpartheader(self): |
|
1220 | 1220 | """reads a part header size and return the bytes blob |
|
1221 | 1221 | |
|
1222 | 1222 | returns None if empty""" |
|
1223 | 1223 | headersize = self._unpack(_fpartheadersize)[0] |
|
1224 | 1224 | if headersize < 0: |
|
1225 | 1225 | raise error.BundleValueError( |
|
1226 | 1226 | b'negative part header size: %i' % headersize |
|
1227 | 1227 | ) |
|
1228 | 1228 | indebug(self.ui, b'part header size: %i\n' % headersize) |
|
1229 | 1229 | if headersize: |
|
1230 | 1230 | return self._readexact(headersize) |
|
1231 | 1231 | return None |
|
1232 | 1232 | |
|
1233 | 1233 | def __call__(self): |
|
1234 | 1234 | |
|
1235 | 1235 | self.ui.debug( |
|
1236 | 1236 | b'bundle2-input-stream-interrupt:' b' opening out of band context\n' |
|
1237 | 1237 | ) |
|
1238 | 1238 | indebug(self.ui, b'bundle2 stream interruption, looking for a part.') |
|
1239 | 1239 | headerblock = self._readpartheader() |
|
1240 | 1240 | if headerblock is None: |
|
1241 | 1241 | indebug(self.ui, b'no part found during interruption.') |
|
1242 | 1242 | return |
|
1243 | 1243 | part = unbundlepart(self.ui, headerblock, self._fp) |
|
1244 | 1244 | op = interruptoperation(self.ui) |
|
1245 | 1245 | hardabort = False |
|
1246 | 1246 | try: |
|
1247 | 1247 | _processpart(op, part) |
|
1248 | 1248 | except (SystemExit, KeyboardInterrupt): |
|
1249 | 1249 | hardabort = True |
|
1250 | 1250 | raise |
|
1251 | 1251 | finally: |
|
1252 | 1252 | if not hardabort: |
|
1253 | 1253 | part.consume() |
|
1254 | 1254 | self.ui.debug( |
|
1255 | 1255 | b'bundle2-input-stream-interrupt:' b' closing out of band context\n' |
|
1256 | 1256 | ) |
|
1257 | 1257 | |
|
1258 | 1258 | |
|
1259 | 1259 | class interruptoperation(object): |
|
1260 | 1260 | """A limited operation to be use by part handler during interruption |
|
1261 | 1261 | |
|
1262 | 1262 | It only have access to an ui object. |
|
1263 | 1263 | """ |
|
1264 | 1264 | |
|
1265 | 1265 | def __init__(self, ui): |
|
1266 | 1266 | self.ui = ui |
|
1267 | 1267 | self.reply = None |
|
1268 | 1268 | self.captureoutput = False |
|
1269 | 1269 | |
|
1270 | 1270 | @property |
|
1271 | 1271 | def repo(self): |
|
1272 | 1272 | raise error.ProgrammingError(b'no repo access from stream interruption') |
|
1273 | 1273 | |
|
1274 | 1274 | def gettransaction(self): |
|
1275 | 1275 | raise TransactionUnavailable(b'no repo access from stream interruption') |
|
1276 | 1276 | |
|
1277 | 1277 | |
|
1278 | 1278 | def decodepayloadchunks(ui, fh): |
|
1279 | 1279 | """Reads bundle2 part payload data into chunks. |
|
1280 | 1280 | |
|
1281 | 1281 | Part payload data consists of framed chunks. This function takes |
|
1282 | 1282 | a file handle and emits those chunks. |
|
1283 | 1283 | """ |
|
1284 | 1284 | dolog = ui.configbool(b'devel', b'bundle2.debug') |
|
1285 | 1285 | debug = ui.debug |
|
1286 | 1286 | |
|
1287 | 1287 | headerstruct = struct.Struct(_fpayloadsize) |
|
1288 | 1288 | headersize = headerstruct.size |
|
1289 | 1289 | unpack = headerstruct.unpack |
|
1290 | 1290 | |
|
1291 | 1291 | readexactly = changegroup.readexactly |
|
1292 | 1292 | read = fh.read |
|
1293 | 1293 | |
|
1294 | 1294 | chunksize = unpack(readexactly(fh, headersize))[0] |
|
1295 | 1295 | indebug(ui, b'payload chunk size: %i' % chunksize) |
|
1296 | 1296 | |
|
1297 | 1297 | # changegroup.readexactly() is inlined below for performance. |
|
1298 | 1298 | while chunksize: |
|
1299 | 1299 | if chunksize >= 0: |
|
1300 | 1300 | s = read(chunksize) |
|
1301 | 1301 | if len(s) < chunksize: |
|
1302 | 1302 | raise error.Abort( |
|
1303 | 1303 | _( |
|
1304 | 1304 | b'stream ended unexpectedly ' |
|
1305 | 1305 | b' (got %d bytes, expected %d)' |
|
1306 | 1306 | ) |
|
1307 | 1307 | % (len(s), chunksize) |
|
1308 | 1308 | ) |
|
1309 | 1309 | |
|
1310 | 1310 | yield s |
|
1311 | 1311 | elif chunksize == flaginterrupt: |
|
1312 | 1312 | # Interrupt "signal" detected. The regular stream is interrupted |
|
1313 | 1313 | # and a bundle2 part follows. Consume it. |
|
1314 | 1314 | interrupthandler(ui, fh)() |
|
1315 | 1315 | else: |
|
1316 | 1316 | raise error.BundleValueError( |
|
1317 | 1317 | b'negative payload chunk size: %s' % chunksize |
|
1318 | 1318 | ) |
|
1319 | 1319 | |
|
1320 | 1320 | s = read(headersize) |
|
1321 | 1321 | if len(s) < headersize: |
|
1322 | 1322 | raise error.Abort( |
|
1323 | 1323 | _(b'stream ended unexpectedly ' b' (got %d bytes, expected %d)') |
|
1324 | 1324 | % (len(s), chunksize) |
|
1325 | 1325 | ) |
|
1326 | 1326 | |
|
1327 | 1327 | chunksize = unpack(s)[0] |
|
1328 | 1328 | |
|
1329 | 1329 | # indebug() inlined for performance. |
|
1330 | 1330 | if dolog: |
|
1331 | 1331 | debug(b'bundle2-input: payload chunk size: %i\n' % chunksize) |
|
1332 | 1332 | |
|
1333 | 1333 | |
|
1334 | 1334 | class unbundlepart(unpackermixin): |
|
1335 | 1335 | """a bundle part read from a bundle""" |
|
1336 | 1336 | |
|
1337 | 1337 | def __init__(self, ui, header, fp): |
|
1338 | 1338 | super(unbundlepart, self).__init__(fp) |
|
1339 | 1339 | self._seekable = util.safehasattr(fp, b'seek') and util.safehasattr( |
|
1340 | 1340 | fp, b'tell' |
|
1341 | 1341 | ) |
|
1342 | 1342 | self.ui = ui |
|
1343 | 1343 | # unbundle state attr |
|
1344 | 1344 | self._headerdata = header |
|
1345 | 1345 | self._headeroffset = 0 |
|
1346 | 1346 | self._initialized = False |
|
1347 | 1347 | self.consumed = False |
|
1348 | 1348 | # part data |
|
1349 | 1349 | self.id = None |
|
1350 | 1350 | self.type = None |
|
1351 | 1351 | self.mandatoryparams = None |
|
1352 | 1352 | self.advisoryparams = None |
|
1353 | 1353 | self.params = None |
|
1354 | 1354 | self.mandatorykeys = () |
|
1355 | 1355 | self._readheader() |
|
1356 | 1356 | self._mandatory = None |
|
1357 | 1357 | self._pos = 0 |
|
1358 | 1358 | |
|
1359 | 1359 | def _fromheader(self, size): |
|
1360 | 1360 | """return the next <size> byte from the header""" |
|
1361 | 1361 | offset = self._headeroffset |
|
1362 | 1362 | data = self._headerdata[offset : (offset + size)] |
|
1363 | 1363 | self._headeroffset = offset + size |
|
1364 | 1364 | return data |
|
1365 | 1365 | |
|
1366 | 1366 | def _unpackheader(self, format): |
|
1367 | 1367 | """read given format from header |
|
1368 | 1368 | |
|
1369 | 1369 | This automatically compute the size of the format to read.""" |
|
1370 | 1370 | data = self._fromheader(struct.calcsize(format)) |
|
1371 | 1371 | return _unpack(format, data) |
|
1372 | 1372 | |
|
1373 | 1373 | def _initparams(self, mandatoryparams, advisoryparams): |
|
1374 | 1374 | """internal function to setup all logic related parameters""" |
|
1375 | 1375 | # make it read only to prevent people touching it by mistake. |
|
1376 | 1376 | self.mandatoryparams = tuple(mandatoryparams) |
|
1377 | 1377 | self.advisoryparams = tuple(advisoryparams) |
|
1378 | 1378 | # user friendly UI |
|
1379 | 1379 | self.params = util.sortdict(self.mandatoryparams) |
|
1380 | 1380 | self.params.update(self.advisoryparams) |
|
1381 | 1381 | self.mandatorykeys = frozenset(p[0] for p in mandatoryparams) |
|
1382 | 1382 | |
|
1383 | 1383 | def _readheader(self): |
|
1384 | 1384 | """read the header and setup the object""" |
|
1385 | 1385 | typesize = self._unpackheader(_fparttypesize)[0] |
|
1386 | 1386 | self.type = self._fromheader(typesize) |
|
1387 | 1387 | indebug(self.ui, b'part type: "%s"' % self.type) |
|
1388 | 1388 | self.id = self._unpackheader(_fpartid)[0] |
|
1389 | 1389 | indebug(self.ui, b'part id: "%s"' % pycompat.bytestr(self.id)) |
|
1390 | 1390 | # extract mandatory bit from type |
|
1391 | 1391 | self.mandatory = self.type != self.type.lower() |
|
1392 | 1392 | self.type = self.type.lower() |
|
1393 | 1393 | ## reading parameters |
|
1394 | 1394 | # param count |
|
1395 | 1395 | mancount, advcount = self._unpackheader(_fpartparamcount) |
|
1396 | 1396 | indebug(self.ui, b'part parameters: %i' % (mancount + advcount)) |
|
1397 | 1397 | # param size |
|
1398 | 1398 | fparamsizes = _makefpartparamsizes(mancount + advcount) |
|
1399 | 1399 | paramsizes = self._unpackheader(fparamsizes) |
|
1400 | 1400 | # make it a list of couple again |
|
1401 | 1401 | paramsizes = list(zip(paramsizes[::2], paramsizes[1::2])) |
|
1402 | 1402 | # split mandatory from advisory |
|
1403 | 1403 | mansizes = paramsizes[:mancount] |
|
1404 | 1404 | advsizes = paramsizes[mancount:] |
|
1405 | 1405 | # retrieve param value |
|
1406 | 1406 | manparams = [] |
|
1407 | 1407 | for key, value in mansizes: |
|
1408 | 1408 | manparams.append((self._fromheader(key), self._fromheader(value))) |
|
1409 | 1409 | advparams = [] |
|
1410 | 1410 | for key, value in advsizes: |
|
1411 | 1411 | advparams.append((self._fromheader(key), self._fromheader(value))) |
|
1412 | 1412 | self._initparams(manparams, advparams) |
|
1413 | 1413 | ## part payload |
|
1414 | 1414 | self._payloadstream = util.chunkbuffer(self._payloadchunks()) |
|
1415 | 1415 | # we read the data, tell it |
|
1416 | 1416 | self._initialized = True |
|
1417 | 1417 | |
|
1418 | 1418 | def _payloadchunks(self): |
|
1419 | 1419 | """Generator of decoded chunks in the payload.""" |
|
1420 | 1420 | return decodepayloadchunks(self.ui, self._fp) |
|
1421 | 1421 | |
|
1422 | 1422 | def consume(self): |
|
1423 | 1423 | """Read the part payload until completion. |
|
1424 | 1424 | |
|
1425 | 1425 | By consuming the part data, the underlying stream read offset will |
|
1426 | 1426 | be advanced to the next part (or end of stream). |
|
1427 | 1427 | """ |
|
1428 | 1428 | if self.consumed: |
|
1429 | 1429 | return |
|
1430 | 1430 | |
|
1431 | 1431 | chunk = self.read(32768) |
|
1432 | 1432 | while chunk: |
|
1433 | 1433 | self._pos += len(chunk) |
|
1434 | 1434 | chunk = self.read(32768) |
|
1435 | 1435 | |
|
1436 | 1436 | def read(self, size=None): |
|
1437 | 1437 | """read payload data""" |
|
1438 | 1438 | if not self._initialized: |
|
1439 | 1439 | self._readheader() |
|
1440 | 1440 | if size is None: |
|
1441 | 1441 | data = self._payloadstream.read() |
|
1442 | 1442 | else: |
|
1443 | 1443 | data = self._payloadstream.read(size) |
|
1444 | 1444 | self._pos += len(data) |
|
1445 | 1445 | if size is None or len(data) < size: |
|
1446 | 1446 | if not self.consumed and self._pos: |
|
1447 | 1447 | self.ui.debug( |
|
1448 | 1448 | b'bundle2-input-part: total payload size %i\n' % self._pos |
|
1449 | 1449 | ) |
|
1450 | 1450 | self.consumed = True |
|
1451 | 1451 | return data |
|
1452 | 1452 | |
|
1453 | 1453 | |
|
1454 | 1454 | class seekableunbundlepart(unbundlepart): |
|
1455 | 1455 | """A bundle2 part in a bundle that is seekable. |
|
1456 | 1456 | |
|
1457 | 1457 | Regular ``unbundlepart`` instances can only be read once. This class |
|
1458 | 1458 | extends ``unbundlepart`` to enable bi-directional seeking within the |
|
1459 | 1459 | part. |
|
1460 | 1460 | |
|
1461 | 1461 | Bundle2 part data consists of framed chunks. Offsets when seeking |
|
1462 | 1462 | refer to the decoded data, not the offsets in the underlying bundle2 |
|
1463 | 1463 | stream. |
|
1464 | 1464 | |
|
1465 | 1465 | To facilitate quickly seeking within the decoded data, instances of this |
|
1466 | 1466 | class maintain a mapping between offsets in the underlying stream and |
|
1467 | 1467 | the decoded payload. This mapping will consume memory in proportion |
|
1468 | 1468 | to the number of chunks within the payload (which almost certainly |
|
1469 | 1469 | increases in proportion with the size of the part). |
|
1470 | 1470 | """ |
|
1471 | 1471 | |
|
1472 | 1472 | def __init__(self, ui, header, fp): |
|
1473 | 1473 | # (payload, file) offsets for chunk starts. |
|
1474 | 1474 | self._chunkindex = [] |
|
1475 | 1475 | |
|
1476 | 1476 | super(seekableunbundlepart, self).__init__(ui, header, fp) |
|
1477 | 1477 | |
|
1478 | 1478 | def _payloadchunks(self, chunknum=0): |
|
1479 | 1479 | '''seek to specified chunk and start yielding data''' |
|
1480 | 1480 | if len(self._chunkindex) == 0: |
|
1481 | 1481 | assert chunknum == 0, b'Must start with chunk 0' |
|
1482 | 1482 | self._chunkindex.append((0, self._tellfp())) |
|
1483 | 1483 | else: |
|
1484 | 1484 | assert chunknum < len(self._chunkindex), ( |
|
1485 | 1485 | b'Unknown chunk %d' % chunknum |
|
1486 | 1486 | ) |
|
1487 | 1487 | self._seekfp(self._chunkindex[chunknum][1]) |
|
1488 | 1488 | |
|
1489 | 1489 | pos = self._chunkindex[chunknum][0] |
|
1490 | 1490 | |
|
1491 | 1491 | for chunk in decodepayloadchunks(self.ui, self._fp): |
|
1492 | 1492 | chunknum += 1 |
|
1493 | 1493 | pos += len(chunk) |
|
1494 | 1494 | if chunknum == len(self._chunkindex): |
|
1495 | 1495 | self._chunkindex.append((pos, self._tellfp())) |
|
1496 | 1496 | |
|
1497 | 1497 | yield chunk |
|
1498 | 1498 | |
|
1499 | 1499 | def _findchunk(self, pos): |
|
1500 | 1500 | '''for a given payload position, return a chunk number and offset''' |
|
1501 | 1501 | for chunk, (ppos, fpos) in enumerate(self._chunkindex): |
|
1502 | 1502 | if ppos == pos: |
|
1503 | 1503 | return chunk, 0 |
|
1504 | 1504 | elif ppos > pos: |
|
1505 | 1505 | return chunk - 1, pos - self._chunkindex[chunk - 1][0] |
|
1506 | 1506 | raise ValueError(b'Unknown chunk') |
|
1507 | 1507 | |
|
1508 | 1508 | def tell(self): |
|
1509 | 1509 | return self._pos |
|
1510 | 1510 | |
|
1511 | 1511 | def seek(self, offset, whence=os.SEEK_SET): |
|
1512 | 1512 | if whence == os.SEEK_SET: |
|
1513 | 1513 | newpos = offset |
|
1514 | 1514 | elif whence == os.SEEK_CUR: |
|
1515 | 1515 | newpos = self._pos + offset |
|
1516 | 1516 | elif whence == os.SEEK_END: |
|
1517 | 1517 | if not self.consumed: |
|
1518 | 1518 | # Can't use self.consume() here because it advances self._pos. |
|
1519 | 1519 | chunk = self.read(32768) |
|
1520 | 1520 | while chunk: |
|
1521 | 1521 | chunk = self.read(32768) |
|
1522 | 1522 | newpos = self._chunkindex[-1][0] - offset |
|
1523 | 1523 | else: |
|
1524 | 1524 | raise ValueError(b'Unknown whence value: %r' % (whence,)) |
|
1525 | 1525 | |
|
1526 | 1526 | if newpos > self._chunkindex[-1][0] and not self.consumed: |
|
1527 | 1527 | # Can't use self.consume() here because it advances self._pos. |
|
1528 | 1528 | chunk = self.read(32768) |
|
1529 | 1529 | while chunk: |
|
1530 | 1530 | chunk = self.read(32668) |
|
1531 | 1531 | |
|
1532 | 1532 | if not 0 <= newpos <= self._chunkindex[-1][0]: |
|
1533 | 1533 | raise ValueError(b'Offset out of range') |
|
1534 | 1534 | |
|
1535 | 1535 | if self._pos != newpos: |
|
1536 | 1536 | chunk, internaloffset = self._findchunk(newpos) |
|
1537 | 1537 | self._payloadstream = util.chunkbuffer(self._payloadchunks(chunk)) |
|
1538 | 1538 | adjust = self.read(internaloffset) |
|
1539 | 1539 | if len(adjust) != internaloffset: |
|
1540 | 1540 | raise error.Abort(_(b'Seek failed\n')) |
|
1541 | 1541 | self._pos = newpos |
|
1542 | 1542 | |
|
1543 | 1543 | def _seekfp(self, offset, whence=0): |
|
1544 | 1544 | """move the underlying file pointer |
|
1545 | 1545 | |
|
1546 | 1546 | This method is meant for internal usage by the bundle2 protocol only. |
|
1547 | 1547 | They directly manipulate the low level stream including bundle2 level |
|
1548 | 1548 | instruction. |
|
1549 | 1549 | |
|
1550 | 1550 | Do not use it to implement higher-level logic or methods.""" |
|
1551 | 1551 | if self._seekable: |
|
1552 | 1552 | return self._fp.seek(offset, whence) |
|
1553 | 1553 | else: |
|
1554 | 1554 | raise NotImplementedError(_(b'File pointer is not seekable')) |
|
1555 | 1555 | |
|
1556 | 1556 | def _tellfp(self): |
|
1557 | 1557 | """return the file offset, or None if file is not seekable |
|
1558 | 1558 | |
|
1559 | 1559 | This method is meant for internal usage by the bundle2 protocol only. |
|
1560 | 1560 | They directly manipulate the low level stream including bundle2 level |
|
1561 | 1561 | instruction. |
|
1562 | 1562 | |
|
1563 | 1563 | Do not use it to implement higher-level logic or methods.""" |
|
1564 | 1564 | if self._seekable: |
|
1565 | 1565 | try: |
|
1566 | 1566 | return self._fp.tell() |
|
1567 | 1567 | except IOError as e: |
|
1568 | 1568 | if e.errno == errno.ESPIPE: |
|
1569 | 1569 | self._seekable = False |
|
1570 | 1570 | else: |
|
1571 | 1571 | raise |
|
1572 | 1572 | return None |
|
1573 | 1573 | |
|
1574 | 1574 | |
|
1575 | 1575 | # These are only the static capabilities. |
|
1576 | 1576 | # Check the 'getrepocaps' function for the rest. |
|
1577 | 1577 | capabilities = { |
|
1578 | 1578 | b'HG20': (), |
|
1579 | 1579 | b'bookmarks': (), |
|
1580 | 1580 | b'error': (b'abort', b'unsupportedcontent', b'pushraced', b'pushkey'), |
|
1581 | 1581 | b'listkeys': (), |
|
1582 | 1582 | b'pushkey': (), |
|
1583 | 1583 | b'digests': tuple(sorted(util.DIGESTS.keys())), |
|
1584 | 1584 | b'remote-changegroup': (b'http', b'https'), |
|
1585 | 1585 | b'hgtagsfnodes': (), |
|
1586 | 1586 | b'rev-branch-cache': (), |
|
1587 | 1587 | b'phases': (b'heads',), |
|
1588 | 1588 | b'stream': (b'v2',), |
|
1589 | 1589 | } |
|
1590 | 1590 | |
|
1591 | 1591 | |
|
1592 | 1592 | def getrepocaps(repo, allowpushback=False, role=None): |
|
1593 | 1593 | """return the bundle2 capabilities for a given repo |
|
1594 | 1594 | |
|
1595 | 1595 | Exists to allow extensions (like evolution) to mutate the capabilities. |
|
1596 | 1596 | |
|
1597 | 1597 | The returned value is used for servers advertising their capabilities as |
|
1598 | 1598 | well as clients advertising their capabilities to servers as part of |
|
1599 | 1599 | bundle2 requests. The ``role`` argument specifies which is which. |
|
1600 | 1600 | """ |
|
1601 | 1601 | if role not in (b'client', b'server'): |
|
1602 | 1602 | raise error.ProgrammingError(b'role argument must be client or server') |
|
1603 | 1603 | |
|
1604 | 1604 | caps = capabilities.copy() |
|
1605 | 1605 | caps[b'changegroup'] = tuple( |
|
1606 | 1606 | sorted(changegroup.supportedincomingversions(repo)) |
|
1607 | 1607 | ) |
|
1608 | 1608 | if obsolete.isenabled(repo, obsolete.exchangeopt): |
|
1609 | 1609 | supportedformat = tuple(b'V%i' % v for v in obsolete.formats) |
|
1610 | 1610 | caps[b'obsmarkers'] = supportedformat |
|
1611 | 1611 | if allowpushback: |
|
1612 | 1612 | caps[b'pushback'] = () |
|
1613 | 1613 | cpmode = repo.ui.config(b'server', b'concurrent-push-mode') |
|
1614 | 1614 | if cpmode == b'check-related': |
|
1615 | 1615 | caps[b'checkheads'] = (b'related',) |
|
1616 | 1616 | if b'phases' in repo.ui.configlist(b'devel', b'legacy.exchange'): |
|
1617 | 1617 | caps.pop(b'phases') |
|
1618 | 1618 | |
|
1619 | 1619 | # Don't advertise stream clone support in server mode if not configured. |
|
1620 | 1620 | if role == b'server': |
|
1621 | 1621 | streamsupported = repo.ui.configbool( |
|
1622 | 1622 | b'server', b'uncompressed', untrusted=True |
|
1623 | 1623 | ) |
|
1624 | 1624 | featuresupported = repo.ui.configbool(b'server', b'bundle2.stream') |
|
1625 | 1625 | |
|
1626 | 1626 | if not streamsupported or not featuresupported: |
|
1627 | 1627 | caps.pop(b'stream') |
|
1628 | 1628 | # Else always advertise support on client, because payload support |
|
1629 | 1629 | # should always be advertised. |
|
1630 | 1630 | |
|
1631 | 1631 | return caps |
|
1632 | 1632 | |
|
1633 | 1633 | |
|
1634 | 1634 | def bundle2caps(remote): |
|
1635 | 1635 | """return the bundle capabilities of a peer as dict""" |
|
1636 | 1636 | raw = remote.capable(b'bundle2') |
|
1637 | 1637 | if not raw and raw != b'': |
|
1638 | 1638 | return {} |
|
1639 | 1639 | capsblob = urlreq.unquote(remote.capable(b'bundle2')) |
|
1640 | 1640 | return decodecaps(capsblob) |
|
1641 | 1641 | |
|
1642 | 1642 | |
|
1643 | 1643 | def obsmarkersversion(caps): |
|
1644 | 1644 | """extract the list of supported obsmarkers versions from a bundle2caps dict |
|
1645 | 1645 | """ |
|
1646 | 1646 | obscaps = caps.get(b'obsmarkers', ()) |
|
1647 | 1647 | return [int(c[1:]) for c in obscaps if c.startswith(b'V')] |
|
1648 | 1648 | |
|
1649 | 1649 | |
|
1650 | 1650 | def writenewbundle( |
|
1651 | 1651 | ui, |
|
1652 | 1652 | repo, |
|
1653 | 1653 | source, |
|
1654 | 1654 | filename, |
|
1655 | 1655 | bundletype, |
|
1656 | 1656 | outgoing, |
|
1657 | 1657 | opts, |
|
1658 | 1658 | vfs=None, |
|
1659 | 1659 | compression=None, |
|
1660 | 1660 | compopts=None, |
|
1661 | 1661 | ): |
|
1662 | 1662 | if bundletype.startswith(b'HG10'): |
|
1663 | 1663 | cg = changegroup.makechangegroup(repo, outgoing, b'01', source) |
|
1664 | 1664 | return writebundle( |
|
1665 | 1665 | ui, |
|
1666 | 1666 | cg, |
|
1667 | 1667 | filename, |
|
1668 | 1668 | bundletype, |
|
1669 | 1669 | vfs=vfs, |
|
1670 | 1670 | compression=compression, |
|
1671 | 1671 | compopts=compopts, |
|
1672 | 1672 | ) |
|
1673 | 1673 | elif not bundletype.startswith(b'HG20'): |
|
1674 | 1674 | raise error.ProgrammingError(b'unknown bundle type: %s' % bundletype) |
|
1675 | 1675 | |
|
1676 | 1676 | caps = {} |
|
1677 | 1677 | if b'obsolescence' in opts: |
|
1678 | 1678 | caps[b'obsmarkers'] = (b'V1',) |
|
1679 | 1679 | bundle = bundle20(ui, caps) |
|
1680 | 1680 | bundle.setcompression(compression, compopts) |
|
1681 | 1681 | _addpartsfromopts(ui, repo, bundle, source, outgoing, opts) |
|
1682 | 1682 | chunkiter = bundle.getchunks() |
|
1683 | 1683 | |
|
1684 | 1684 | return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs) |
|
1685 | 1685 | |
|
1686 | 1686 | |
|
1687 | 1687 | def _addpartsfromopts(ui, repo, bundler, source, outgoing, opts): |
|
1688 | 1688 | # We should eventually reconcile this logic with the one behind |
|
1689 | 1689 | # 'exchange.getbundle2partsgenerator'. |
|
1690 | 1690 | # |
|
1691 | 1691 | # The type of input from 'getbundle' and 'writenewbundle' are a bit |
|
1692 | 1692 | # different right now. So we keep them separated for now for the sake of |
|
1693 | 1693 | # simplicity. |
|
1694 | 1694 | |
|
1695 | 1695 | # we might not always want a changegroup in such bundle, for example in |
|
1696 | 1696 | # stream bundles |
|
1697 | 1697 | if opts.get(b'changegroup', True): |
|
1698 | 1698 | cgversion = opts.get(b'cg.version') |
|
1699 | 1699 | if cgversion is None: |
|
1700 | 1700 | cgversion = changegroup.safeversion(repo) |
|
1701 | 1701 | cg = changegroup.makechangegroup(repo, outgoing, cgversion, source) |
|
1702 | 1702 | part = bundler.newpart(b'changegroup', data=cg.getchunks()) |
|
1703 | 1703 | part.addparam(b'version', cg.version) |
|
1704 | 1704 | if b'clcount' in cg.extras: |
|
1705 | 1705 | part.addparam( |
|
1706 | 1706 | b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False |
|
1707 | 1707 | ) |
|
1708 | 1708 | if opts.get(b'phases') and repo.revs( |
|
1709 | 1709 | b'%ln and secret()', outgoing.missingheads |
|
1710 | 1710 | ): |
|
1711 | 1711 | part.addparam( |
|
1712 | 1712 | b'targetphase', b'%d' % phases.secret, mandatory=False |
|
1713 | 1713 | ) |
|
1714 | 1714 | |
|
1715 | 1715 | if opts.get(b'streamv2', False): |
|
1716 | 1716 | addpartbundlestream2(bundler, repo, stream=True) |
|
1717 | 1717 | |
|
1718 | 1718 | if opts.get(b'tagsfnodescache', True): |
|
1719 | 1719 | addparttagsfnodescache(repo, bundler, outgoing) |
|
1720 | 1720 | |
|
1721 | 1721 | if opts.get(b'revbranchcache', True): |
|
1722 | 1722 | addpartrevbranchcache(repo, bundler, outgoing) |
|
1723 | 1723 | |
|
1724 | 1724 | if opts.get(b'obsolescence', False): |
|
1725 | 1725 | obsmarkers = repo.obsstore.relevantmarkers(outgoing.missing) |
|
1726 | 1726 | buildobsmarkerspart(bundler, obsmarkers) |
|
1727 | 1727 | |
|
1728 | 1728 | if opts.get(b'phases', False): |
|
1729 | 1729 | headsbyphase = phases.subsetphaseheads(repo, outgoing.missing) |
|
1730 | 1730 | phasedata = phases.binaryencode(headsbyphase) |
|
1731 | 1731 | bundler.newpart(b'phase-heads', data=phasedata) |
|
1732 | 1732 | |
|
1733 | 1733 | |
|
1734 | 1734 | def addparttagsfnodescache(repo, bundler, outgoing): |
|
1735 | 1735 | # we include the tags fnode cache for the bundle changeset |
|
1736 | 1736 | # (as an optional parts) |
|
1737 | 1737 | cache = tags.hgtagsfnodescache(repo.unfiltered()) |
|
1738 | 1738 | chunks = [] |
|
1739 | 1739 | |
|
1740 | 1740 | # .hgtags fnodes are only relevant for head changesets. While we could |
|
1741 | 1741 | # transfer values for all known nodes, there will likely be little to |
|
1742 | 1742 | # no benefit. |
|
1743 | 1743 | # |
|
1744 | 1744 | # We don't bother using a generator to produce output data because |
|
1745 | 1745 | # a) we only have 40 bytes per head and even esoteric numbers of heads |
|
1746 | 1746 | # consume little memory (1M heads is 40MB) b) we don't want to send the |
|
1747 | 1747 | # part if we don't have entries and knowing if we have entries requires |
|
1748 | 1748 | # cache lookups. |
|
1749 | 1749 | for node in outgoing.missingheads: |
|
1750 | 1750 | # Don't compute missing, as this may slow down serving. |
|
1751 | 1751 | fnode = cache.getfnode(node, computemissing=False) |
|
1752 | 1752 | if fnode is not None: |
|
1753 | 1753 | chunks.extend([node, fnode]) |
|
1754 | 1754 | |
|
1755 | 1755 | if chunks: |
|
1756 | 1756 | bundler.newpart(b'hgtagsfnodes', data=b''.join(chunks)) |
|
1757 | 1757 | |
|
1758 | 1758 | |
|
1759 | 1759 | def addpartrevbranchcache(repo, bundler, outgoing): |
|
1760 | 1760 | # we include the rev branch cache for the bundle changeset |
|
1761 | 1761 | # (as an optional parts) |
|
1762 | 1762 | cache = repo.revbranchcache() |
|
1763 | 1763 | cl = repo.unfiltered().changelog |
|
1764 | 1764 | branchesdata = collections.defaultdict(lambda: (set(), set())) |
|
1765 | 1765 | for node in outgoing.missing: |
|
1766 | 1766 | branch, close = cache.branchinfo(cl.rev(node)) |
|
1767 | 1767 | branchesdata[branch][close].add(node) |
|
1768 | 1768 | |
|
1769 | 1769 | def generate(): |
|
1770 | 1770 | for branch, (nodes, closed) in sorted(branchesdata.items()): |
|
1771 | 1771 | utf8branch = encoding.fromlocal(branch) |
|
1772 | 1772 | yield rbcstruct.pack(len(utf8branch), len(nodes), len(closed)) |
|
1773 | 1773 | yield utf8branch |
|
1774 | 1774 | for n in sorted(nodes): |
|
1775 | 1775 | yield n |
|
1776 | 1776 | for n in sorted(closed): |
|
1777 | 1777 | yield n |
|
1778 | 1778 | |
|
1779 | 1779 | bundler.newpart(b'cache:rev-branch-cache', data=generate(), mandatory=False) |
|
1780 | 1780 | |
|
1781 | 1781 | |
|
1782 | 1782 | def _formatrequirementsspec(requirements): |
|
1783 | 1783 | requirements = [req for req in requirements if req != b"shared"] |
|
1784 | 1784 | return urlreq.quote(b','.join(sorted(requirements))) |
|
1785 | 1785 | |
|
1786 | 1786 | |
|
1787 | 1787 | def _formatrequirementsparams(requirements): |
|
1788 | 1788 | requirements = _formatrequirementsspec(requirements) |
|
1789 | 1789 | params = b"%s%s" % (urlreq.quote(b"requirements="), requirements) |
|
1790 | 1790 | return params |
|
1791 | 1791 | |
|
1792 | 1792 | |
|
1793 | 1793 | def addpartbundlestream2(bundler, repo, **kwargs): |
|
1794 | 1794 | if not kwargs.get(r'stream', False): |
|
1795 | 1795 | return |
|
1796 | 1796 | |
|
1797 | 1797 | if not streamclone.allowservergeneration(repo): |
|
1798 | 1798 | raise error.Abort( |
|
1799 | 1799 | _( |
|
1800 | 1800 | b'stream data requested but server does not allow ' |
|
1801 | 1801 | b'this feature' |
|
1802 | 1802 | ), |
|
1803 | 1803 | hint=_( |
|
1804 | 1804 | b'well-behaved clients should not be ' |
|
1805 | 1805 | b'requesting stream data from servers not ' |
|
1806 | 1806 | b'advertising it; the client may be buggy' |
|
1807 | 1807 | ), |
|
1808 | 1808 | ) |
|
1809 | 1809 | |
|
1810 | 1810 | # Stream clones don't compress well. And compression undermines a |
|
1811 | 1811 | # goal of stream clones, which is to be fast. Communicate the desire |
|
1812 | 1812 | # to avoid compression to consumers of the bundle. |
|
1813 | 1813 | bundler.prefercompressed = False |
|
1814 | 1814 | |
|
1815 | 1815 | # get the includes and excludes |
|
1816 | 1816 | includepats = kwargs.get(r'includepats') |
|
1817 | 1817 | excludepats = kwargs.get(r'excludepats') |
|
1818 | 1818 | |
|
1819 | 1819 | narrowstream = repo.ui.configbool( |
|
1820 | 1820 | b'experimental', b'server.stream-narrow-clones' |
|
1821 | 1821 | ) |
|
1822 | 1822 | |
|
1823 | 1823 | if (includepats or excludepats) and not narrowstream: |
|
1824 | 1824 | raise error.Abort(_(b'server does not support narrow stream clones')) |
|
1825 | 1825 | |
|
1826 | 1826 | includeobsmarkers = False |
|
1827 | 1827 | if repo.obsstore: |
|
1828 | 1828 | remoteversions = obsmarkersversion(bundler.capabilities) |
|
1829 | 1829 | if not remoteversions: |
|
1830 | 1830 | raise error.Abort( |
|
1831 | 1831 | _( |
|
1832 | 1832 | b'server has obsolescence markers, but client ' |
|
1833 | 1833 | b'cannot receive them via stream clone' |
|
1834 | 1834 | ) |
|
1835 | 1835 | ) |
|
1836 | 1836 | elif repo.obsstore._version in remoteversions: |
|
1837 | 1837 | includeobsmarkers = True |
|
1838 | 1838 | |
|
1839 | 1839 | filecount, bytecount, it = streamclone.generatev2( |
|
1840 | 1840 | repo, includepats, excludepats, includeobsmarkers |
|
1841 | 1841 | ) |
|
1842 | 1842 | requirements = _formatrequirementsspec(repo.requirements) |
|
1843 | 1843 | part = bundler.newpart(b'stream2', data=it) |
|
1844 | 1844 | part.addparam(b'bytecount', b'%d' % bytecount, mandatory=True) |
|
1845 | 1845 | part.addparam(b'filecount', b'%d' % filecount, mandatory=True) |
|
1846 | 1846 | part.addparam(b'requirements', requirements, mandatory=True) |
|
1847 | 1847 | |
|
1848 | 1848 | |
|
1849 | 1849 | def buildobsmarkerspart(bundler, markers): |
|
1850 | 1850 | """add an obsmarker part to the bundler with <markers> |
|
1851 | 1851 | |
|
1852 | 1852 | No part is created if markers is empty. |
|
1853 | 1853 | Raises ValueError if the bundler doesn't support any known obsmarker format. |
|
1854 | 1854 | """ |
|
1855 | 1855 | if not markers: |
|
1856 | 1856 | return None |
|
1857 | 1857 | |
|
1858 | 1858 | remoteversions = obsmarkersversion(bundler.capabilities) |
|
1859 | 1859 | version = obsolete.commonversion(remoteversions) |
|
1860 | 1860 | if version is None: |
|
1861 | 1861 | raise ValueError(b'bundler does not support common obsmarker format') |
|
1862 | 1862 | stream = obsolete.encodemarkers(markers, True, version=version) |
|
1863 | 1863 | return bundler.newpart(b'obsmarkers', data=stream) |
|
1864 | 1864 | |
|
1865 | 1865 | |
|
1866 | 1866 | def writebundle( |
|
1867 | 1867 | ui, cg, filename, bundletype, vfs=None, compression=None, compopts=None |
|
1868 | 1868 | ): |
|
1869 | 1869 | """Write a bundle file and return its filename. |
|
1870 | 1870 | |
|
1871 | 1871 | Existing files will not be overwritten. |
|
1872 | 1872 | If no filename is specified, a temporary file is created. |
|
1873 | 1873 | bz2 compression can be turned off. |
|
1874 | 1874 | The bundle file will be deleted in case of errors. |
|
1875 | 1875 | """ |
|
1876 | 1876 | |
|
1877 | 1877 | if bundletype == b"HG20": |
|
1878 | 1878 | bundle = bundle20(ui) |
|
1879 | 1879 | bundle.setcompression(compression, compopts) |
|
1880 | 1880 | part = bundle.newpart(b'changegroup', data=cg.getchunks()) |
|
1881 | 1881 | part.addparam(b'version', cg.version) |
|
1882 | 1882 | if b'clcount' in cg.extras: |
|
1883 | 1883 | part.addparam( |
|
1884 | 1884 | b'nbchanges', b'%d' % cg.extras[b'clcount'], mandatory=False |
|
1885 | 1885 | ) |
|
1886 | 1886 | chunkiter = bundle.getchunks() |
|
1887 | 1887 | else: |
|
1888 | 1888 | # compression argument is only for the bundle2 case |
|
1889 | 1889 | assert compression is None |
|
1890 | 1890 | if cg.version != b'01': |
|
1891 | 1891 | raise error.Abort( |
|
1892 | 1892 | _(b'old bundle types only supports v1 ' b'changegroups') |
|
1893 | 1893 | ) |
|
1894 | 1894 | header, comp = bundletypes[bundletype] |
|
1895 | 1895 | if comp not in util.compengines.supportedbundletypes: |
|
1896 | 1896 | raise error.Abort(_(b'unknown stream compression type: %s') % comp) |
|
1897 | 1897 | compengine = util.compengines.forbundletype(comp) |
|
1898 | 1898 | |
|
1899 | 1899 | def chunkiter(): |
|
1900 | 1900 | yield header |
|
1901 | 1901 | for chunk in compengine.compressstream(cg.getchunks(), compopts): |
|
1902 | 1902 | yield chunk |
|
1903 | 1903 | |
|
1904 | 1904 | chunkiter = chunkiter() |
|
1905 | 1905 | |
|
1906 | 1906 | # parse the changegroup data, otherwise we will block |
|
1907 | 1907 | # in case of sshrepo because we don't know the end of the stream |
|
1908 | 1908 | return changegroup.writechunks(ui, chunkiter, filename, vfs=vfs) |
|
1909 | 1909 | |
|
1910 | 1910 | |
|
1911 | 1911 | def combinechangegroupresults(op): |
|
1912 | 1912 | """logic to combine 0 or more addchangegroup results into one""" |
|
1913 | 1913 | results = [r.get(b'return', 0) for r in op.records[b'changegroup']] |
|
1914 | 1914 | changedheads = 0 |
|
1915 | 1915 | result = 1 |
|
1916 | 1916 | for ret in results: |
|
1917 | 1917 | # If any changegroup result is 0, return 0 |
|
1918 | 1918 | if ret == 0: |
|
1919 | 1919 | result = 0 |
|
1920 | 1920 | break |
|
1921 | 1921 | if ret < -1: |
|
1922 | 1922 | changedheads += ret + 1 |
|
1923 | 1923 | elif ret > 1: |
|
1924 | 1924 | changedheads += ret - 1 |
|
1925 | 1925 | if changedheads > 0: |
|
1926 | 1926 | result = 1 + changedheads |
|
1927 | 1927 | elif changedheads < 0: |
|
1928 | 1928 | result = -1 + changedheads |
|
1929 | 1929 | return result |
|
1930 | 1930 | |
|
1931 | 1931 | |
|
1932 | 1932 | @parthandler( |
|
1933 | 1933 | b'changegroup', (b'version', b'nbchanges', b'treemanifest', b'targetphase') |
|
1934 | 1934 | ) |
|
1935 | 1935 | def handlechangegroup(op, inpart): |
|
1936 | 1936 | """apply a changegroup part on the repo |
|
1937 | 1937 | |
|
1938 | 1938 | This is a very early implementation that will massive rework before being |
|
1939 | 1939 | inflicted to any end-user. |
|
1940 | 1940 | """ |
|
1941 | 1941 | from . import localrepo |
|
1942 | 1942 | |
|
1943 | 1943 | tr = op.gettransaction() |
|
1944 | 1944 | unpackerversion = inpart.params.get(b'version', b'01') |
|
1945 | 1945 | # We should raise an appropriate exception here |
|
1946 | 1946 | cg = changegroup.getunbundler(unpackerversion, inpart, None) |
|
1947 | 1947 | # the source and url passed here are overwritten by the one contained in |
|
1948 | 1948 | # the transaction.hookargs argument. So 'bundle2' is a placeholder |
|
1949 | 1949 | nbchangesets = None |
|
1950 | 1950 | if b'nbchanges' in inpart.params: |
|
1951 | 1951 | nbchangesets = int(inpart.params.get(b'nbchanges')) |
|
1952 | 1952 | if ( |
|
1953 | 1953 | b'treemanifest' in inpart.params |
|
1954 | 1954 | and b'treemanifest' not in op.repo.requirements |
|
1955 | 1955 | ): |
|
1956 | 1956 | if len(op.repo.changelog) != 0: |
|
1957 | 1957 | raise error.Abort( |
|
1958 | 1958 | _( |
|
1959 | 1959 | b"bundle contains tree manifests, but local repo is " |
|
1960 | 1960 | b"non-empty and does not use tree manifests" |
|
1961 | 1961 | ) |
|
1962 | 1962 | ) |
|
1963 | 1963 | op.repo.requirements.add(b'treemanifest') |
|
1964 | 1964 | op.repo.svfs.options = localrepo.resolvestorevfsoptions( |
|
1965 | 1965 | op.repo.ui, op.repo.requirements, op.repo.features |
|
1966 | 1966 | ) |
|
1967 | 1967 | op.repo._writerequirements() |
|
1968 | 1968 | extrakwargs = {} |
|
1969 | 1969 | targetphase = inpart.params.get(b'targetphase') |
|
1970 | 1970 | if targetphase is not None: |
|
1971 | 1971 | extrakwargs[r'targetphase'] = int(targetphase) |
|
1972 | 1972 | ret = _processchangegroup( |
|
1973 | 1973 | op, |
|
1974 | 1974 | cg, |
|
1975 | 1975 | tr, |
|
1976 | 1976 | b'bundle2', |
|
1977 | 1977 | b'bundle2', |
|
1978 | 1978 | expectedtotal=nbchangesets, |
|
1979 | 1979 | **extrakwargs |
|
1980 | 1980 | ) |
|
1981 | 1981 | if op.reply is not None: |
|
1982 | 1982 | # This is definitely not the final form of this |
|
1983 | 1983 | # return. But one need to start somewhere. |
|
1984 | 1984 | part = op.reply.newpart(b'reply:changegroup', mandatory=False) |
|
1985 | 1985 | part.addparam( |
|
1986 | 1986 | b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False |
|
1987 | 1987 | ) |
|
1988 | 1988 | part.addparam(b'return', b'%i' % ret, mandatory=False) |
|
1989 | 1989 | assert not inpart.read() |
|
1990 | 1990 | |
|
1991 | 1991 | |
|
1992 | 1992 | _remotechangegroupparams = tuple( |
|
1993 | 1993 | [b'url', b'size', b'digests'] |
|
1994 | 1994 | + [b'digest:%s' % k for k in util.DIGESTS.keys()] |
|
1995 | 1995 | ) |
|
1996 | 1996 | |
|
1997 | 1997 | |
|
1998 | 1998 | @parthandler(b'remote-changegroup', _remotechangegroupparams) |
|
1999 | 1999 | def handleremotechangegroup(op, inpart): |
|
2000 | 2000 | """apply a bundle10 on the repo, given an url and validation information |
|
2001 | 2001 | |
|
2002 | 2002 | All the information about the remote bundle to import are given as |
|
2003 | 2003 | parameters. The parameters include: |
|
2004 | 2004 | - url: the url to the bundle10. |
|
2005 | 2005 | - size: the bundle10 file size. It is used to validate what was |
|
2006 | 2006 | retrieved by the client matches the server knowledge about the bundle. |
|
2007 | 2007 | - digests: a space separated list of the digest types provided as |
|
2008 | 2008 | parameters. |
|
2009 | 2009 | - digest:<digest-type>: the hexadecimal representation of the digest with |
|
2010 | 2010 | that name. Like the size, it is used to validate what was retrieved by |
|
2011 | 2011 | the client matches what the server knows about the bundle. |
|
2012 | 2012 | |
|
2013 | 2013 | When multiple digest types are given, all of them are checked. |
|
2014 | 2014 | """ |
|
2015 | 2015 | try: |
|
2016 | 2016 | raw_url = inpart.params[b'url'] |
|
2017 | 2017 | except KeyError: |
|
2018 | 2018 | raise error.Abort(_(b'remote-changegroup: missing "%s" param') % b'url') |
|
2019 | 2019 | parsed_url = util.url(raw_url) |
|
2020 | 2020 | if parsed_url.scheme not in capabilities[b'remote-changegroup']: |
|
2021 | 2021 | raise error.Abort( |
|
2022 | 2022 | _(b'remote-changegroup does not support %s urls') |
|
2023 | 2023 | % parsed_url.scheme |
|
2024 | 2024 | ) |
|
2025 | 2025 | |
|
2026 | 2026 | try: |
|
2027 | 2027 | size = int(inpart.params[b'size']) |
|
2028 | 2028 | except ValueError: |
|
2029 | 2029 | raise error.Abort( |
|
2030 | 2030 | _(b'remote-changegroup: invalid value for param "%s"') % b'size' |
|
2031 | 2031 | ) |
|
2032 | 2032 | except KeyError: |
|
2033 | 2033 | raise error.Abort( |
|
2034 | 2034 | _(b'remote-changegroup: missing "%s" param') % b'size' |
|
2035 | 2035 | ) |
|
2036 | 2036 | |
|
2037 | 2037 | digests = {} |
|
2038 | 2038 | for typ in inpart.params.get(b'digests', b'').split(): |
|
2039 | 2039 | param = b'digest:%s' % typ |
|
2040 | 2040 | try: |
|
2041 | 2041 | value = inpart.params[param] |
|
2042 | 2042 | except KeyError: |
|
2043 | 2043 | raise error.Abort( |
|
2044 | 2044 | _(b'remote-changegroup: missing "%s" param') % param |
|
2045 | 2045 | ) |
|
2046 | 2046 | digests[typ] = value |
|
2047 | 2047 | |
|
2048 | 2048 | real_part = util.digestchecker(url.open(op.ui, raw_url), size, digests) |
|
2049 | 2049 | |
|
2050 | 2050 | tr = op.gettransaction() |
|
2051 | 2051 | from . import exchange |
|
2052 | 2052 | |
|
2053 | 2053 | cg = exchange.readbundle(op.repo.ui, real_part, raw_url) |
|
2054 | 2054 | if not isinstance(cg, changegroup.cg1unpacker): |
|
2055 | 2055 | raise error.Abort( |
|
2056 | 2056 | _(b'%s: not a bundle version 1.0') % util.hidepassword(raw_url) |
|
2057 | 2057 | ) |
|
2058 | 2058 | ret = _processchangegroup(op, cg, tr, b'bundle2', b'bundle2') |
|
2059 | 2059 | if op.reply is not None: |
|
2060 | 2060 | # This is definitely not the final form of this |
|
2061 | 2061 | # return. But one need to start somewhere. |
|
2062 | 2062 | part = op.reply.newpart(b'reply:changegroup') |
|
2063 | 2063 | part.addparam( |
|
2064 | 2064 | b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False |
|
2065 | 2065 | ) |
|
2066 | 2066 | part.addparam(b'return', b'%i' % ret, mandatory=False) |
|
2067 | 2067 | try: |
|
2068 | 2068 | real_part.validate() |
|
2069 | 2069 | except error.Abort as e: |
|
2070 | 2070 | raise error.Abort( |
|
2071 | 2071 | _(b'bundle at %s is corrupted:\n%s') |
|
2072 | 2072 | % (util.hidepassword(raw_url), bytes(e)) |
|
2073 | 2073 | ) |
|
2074 | 2074 | assert not inpart.read() |
|
2075 | 2075 | |
|
2076 | 2076 | |
|
2077 | 2077 | @parthandler(b'reply:changegroup', (b'return', b'in-reply-to')) |
|
2078 | 2078 | def handlereplychangegroup(op, inpart): |
|
2079 | 2079 | ret = int(inpart.params[b'return']) |
|
2080 | 2080 | replyto = int(inpart.params[b'in-reply-to']) |
|
2081 | 2081 | op.records.add(b'changegroup', {b'return': ret}, replyto) |
|
2082 | 2082 | |
|
2083 | 2083 | |
|
2084 | 2084 | @parthandler(b'check:bookmarks') |
|
2085 | 2085 | def handlecheckbookmarks(op, inpart): |
|
2086 | 2086 | """check location of bookmarks |
|
2087 | 2087 | |
|
2088 | 2088 | This part is to be used to detect push race regarding bookmark, it |
|
2089 | 2089 | contains binary encoded (bookmark, node) tuple. If the local state does |
|
2090 | 2090 | not marks the one in the part, a PushRaced exception is raised |
|
2091 | 2091 | """ |
|
2092 | 2092 | bookdata = bookmarks.binarydecode(inpart) |
|
2093 | 2093 | |
|
2094 | 2094 | msgstandard = ( |
|
2095 | 2095 | b'remote repository changed while pushing - please try again ' |
|
2096 | 2096 | b'(bookmark "%s" move from %s to %s)' |
|
2097 | 2097 | ) |
|
2098 | 2098 | msgmissing = ( |
|
2099 | 2099 | b'remote repository changed while pushing - please try again ' |
|
2100 | 2100 | b'(bookmark "%s" is missing, expected %s)' |
|
2101 | 2101 | ) |
|
2102 | 2102 | msgexist = ( |
|
2103 | 2103 | b'remote repository changed while pushing - please try again ' |
|
2104 | 2104 | b'(bookmark "%s" set on %s, expected missing)' |
|
2105 | 2105 | ) |
|
2106 | 2106 | for book, node in bookdata: |
|
2107 | 2107 | currentnode = op.repo._bookmarks.get(book) |
|
2108 | 2108 | if currentnode != node: |
|
2109 | 2109 | if node is None: |
|
2110 | 2110 | finalmsg = msgexist % (book, nodemod.short(currentnode)) |
|
2111 | 2111 | elif currentnode is None: |
|
2112 | 2112 | finalmsg = msgmissing % (book, nodemod.short(node)) |
|
2113 | 2113 | else: |
|
2114 | 2114 | finalmsg = msgstandard % ( |
|
2115 | 2115 | book, |
|
2116 | 2116 | nodemod.short(node), |
|
2117 | 2117 | nodemod.short(currentnode), |
|
2118 | 2118 | ) |
|
2119 | 2119 | raise error.PushRaced(finalmsg) |
|
2120 | 2120 | |
|
2121 | 2121 | |
|
2122 | 2122 | @parthandler(b'check:heads') |
|
2123 | 2123 | def handlecheckheads(op, inpart): |
|
2124 | 2124 | """check that head of the repo did not change |
|
2125 | 2125 | |
|
2126 | 2126 | This is used to detect a push race when using unbundle. |
|
2127 | 2127 | This replaces the "heads" argument of unbundle.""" |
|
2128 | 2128 | h = inpart.read(20) |
|
2129 | 2129 | heads = [] |
|
2130 | 2130 | while len(h) == 20: |
|
2131 | 2131 | heads.append(h) |
|
2132 | 2132 | h = inpart.read(20) |
|
2133 | 2133 | assert not h |
|
2134 | 2134 | # Trigger a transaction so that we are guaranteed to have the lock now. |
|
2135 | 2135 | if op.ui.configbool(b'experimental', b'bundle2lazylocking'): |
|
2136 | 2136 | op.gettransaction() |
|
2137 | 2137 | if sorted(heads) != sorted(op.repo.heads()): |
|
2138 | 2138 | raise error.PushRaced( |
|
2139 | 2139 | b'remote repository changed while pushing - ' b'please try again' |
|
2140 | 2140 | ) |
|
2141 | 2141 | |
|
2142 | 2142 | |
|
2143 | 2143 | @parthandler(b'check:updated-heads') |
|
2144 | 2144 | def handlecheckupdatedheads(op, inpart): |
|
2145 | 2145 | """check for race on the heads touched by a push |
|
2146 | 2146 | |
|
2147 | 2147 | This is similar to 'check:heads' but focus on the heads actually updated |
|
2148 | 2148 | during the push. If other activities happen on unrelated heads, it is |
|
2149 | 2149 | ignored. |
|
2150 | 2150 | |
|
2151 | 2151 | This allow server with high traffic to avoid push contention as long as |
|
2152 | 2152 | unrelated parts of the graph are involved.""" |
|
2153 | 2153 | h = inpart.read(20) |
|
2154 | 2154 | heads = [] |
|
2155 | 2155 | while len(h) == 20: |
|
2156 | 2156 | heads.append(h) |
|
2157 | 2157 | h = inpart.read(20) |
|
2158 | 2158 | assert not h |
|
2159 | 2159 | # trigger a transaction so that we are guaranteed to have the lock now. |
|
2160 | 2160 | if op.ui.configbool(b'experimental', b'bundle2lazylocking'): |
|
2161 | 2161 | op.gettransaction() |
|
2162 | 2162 | |
|
2163 | 2163 | currentheads = set() |
|
2164 | 2164 | for ls in op.repo.branchmap().iterheads(): |
|
2165 | 2165 | currentheads.update(ls) |
|
2166 | 2166 | |
|
2167 | 2167 | for h in heads: |
|
2168 | 2168 | if h not in currentheads: |
|
2169 | 2169 | raise error.PushRaced( |
|
2170 | 2170 | b'remote repository changed while pushing - ' |
|
2171 | 2171 | b'please try again' |
|
2172 | 2172 | ) |
|
2173 | 2173 | |
|
2174 | 2174 | |
|
2175 | 2175 | @parthandler(b'check:phases') |
|
2176 | 2176 | def handlecheckphases(op, inpart): |
|
2177 | 2177 | """check that phase boundaries of the repository did not change |
|
2178 | 2178 | |
|
2179 | 2179 | This is used to detect a push race. |
|
2180 | 2180 | """ |
|
2181 | 2181 | phasetonodes = phases.binarydecode(inpart) |
|
2182 | 2182 | unfi = op.repo.unfiltered() |
|
2183 | 2183 | cl = unfi.changelog |
|
2184 | 2184 | phasecache = unfi._phasecache |
|
2185 | 2185 | msg = ( |
|
2186 | 2186 | b'remote repository changed while pushing - please try again ' |
|
2187 | 2187 | b'(%s is %s expected %s)' |
|
2188 | 2188 | ) |
|
2189 | 2189 | for expectedphase, nodes in enumerate(phasetonodes): |
|
2190 | 2190 | for n in nodes: |
|
2191 | 2191 | actualphase = phasecache.phase(unfi, cl.rev(n)) |
|
2192 | 2192 | if actualphase != expectedphase: |
|
2193 | 2193 | finalmsg = msg % ( |
|
2194 | 2194 | nodemod.short(n), |
|
2195 | 2195 | phases.phasenames[actualphase], |
|
2196 | 2196 | phases.phasenames[expectedphase], |
|
2197 | 2197 | ) |
|
2198 | 2198 | raise error.PushRaced(finalmsg) |
|
2199 | 2199 | |
|
2200 | 2200 | |
|
2201 | 2201 | @parthandler(b'output') |
|
2202 | 2202 | def handleoutput(op, inpart): |
|
2203 | 2203 | """forward output captured on the server to the client""" |
|
2204 | 2204 | for line in inpart.read().splitlines(): |
|
2205 | 2205 | op.ui.status(_(b'remote: %s\n') % line) |
|
2206 | 2206 | |
|
2207 | 2207 | |
|
2208 | 2208 | @parthandler(b'replycaps') |
|
2209 | 2209 | def handlereplycaps(op, inpart): |
|
2210 | 2210 | """Notify that a reply bundle should be created |
|
2211 | 2211 | |
|
2212 | 2212 | The payload contains the capabilities information for the reply""" |
|
2213 | 2213 | caps = decodecaps(inpart.read()) |
|
2214 | 2214 | if op.reply is None: |
|
2215 | 2215 | op.reply = bundle20(op.ui, caps) |
|
2216 | 2216 | |
|
2217 | 2217 | |
|
2218 | 2218 | class AbortFromPart(error.Abort): |
|
2219 | 2219 | """Sub-class of Abort that denotes an error from a bundle2 part.""" |
|
2220 | 2220 | |
|
2221 | 2221 | |
|
2222 | 2222 | @parthandler(b'error:abort', (b'message', b'hint')) |
|
2223 | 2223 | def handleerrorabort(op, inpart): |
|
2224 | 2224 | """Used to transmit abort error over the wire""" |
|
2225 | 2225 | raise AbortFromPart( |
|
2226 | 2226 | inpart.params[b'message'], hint=inpart.params.get(b'hint') |
|
2227 | 2227 | ) |
|
2228 | 2228 | |
|
2229 | 2229 | |
|
2230 | 2230 | @parthandler( |
|
2231 | 2231 | b'error:pushkey', |
|
2232 | 2232 | (b'namespace', b'key', b'new', b'old', b'ret', b'in-reply-to'), |
|
2233 | 2233 | ) |
|
2234 | 2234 | def handleerrorpushkey(op, inpart): |
|
2235 | 2235 | """Used to transmit failure of a mandatory pushkey over the wire""" |
|
2236 | 2236 | kwargs = {} |
|
2237 | 2237 | for name in (b'namespace', b'key', b'new', b'old', b'ret'): |
|
2238 | 2238 | value = inpart.params.get(name) |
|
2239 | 2239 | if value is not None: |
|
2240 | 2240 | kwargs[name] = value |
|
2241 | 2241 | raise error.PushkeyFailed( |
|
2242 | 2242 | inpart.params[b'in-reply-to'], **pycompat.strkwargs(kwargs) |
|
2243 | 2243 | ) |
|
2244 | 2244 | |
|
2245 | 2245 | |
|
2246 | 2246 | @parthandler(b'error:unsupportedcontent', (b'parttype', b'params')) |
|
2247 | 2247 | def handleerrorunsupportedcontent(op, inpart): |
|
2248 | 2248 | """Used to transmit unknown content error over the wire""" |
|
2249 | 2249 | kwargs = {} |
|
2250 | 2250 | parttype = inpart.params.get(b'parttype') |
|
2251 | 2251 | if parttype is not None: |
|
2252 | 2252 | kwargs[b'parttype'] = parttype |
|
2253 | 2253 | params = inpart.params.get(b'params') |
|
2254 | 2254 | if params is not None: |
|
2255 | 2255 | kwargs[b'params'] = params.split(b'\0') |
|
2256 | 2256 | |
|
2257 | 2257 | raise error.BundleUnknownFeatureError(**pycompat.strkwargs(kwargs)) |
|
2258 | 2258 | |
|
2259 | 2259 | |
|
2260 | 2260 | @parthandler(b'error:pushraced', (b'message',)) |
|
2261 | 2261 | def handleerrorpushraced(op, inpart): |
|
2262 | 2262 | """Used to transmit push race error over the wire""" |
|
2263 | 2263 | raise error.ResponseError(_(b'push failed:'), inpart.params[b'message']) |
|
2264 | 2264 | |
|
2265 | 2265 | |
|
2266 | 2266 | @parthandler(b'listkeys', (b'namespace',)) |
|
2267 | 2267 | def handlelistkeys(op, inpart): |
|
2268 | 2268 | """retrieve pushkey namespace content stored in a bundle2""" |
|
2269 | 2269 | namespace = inpart.params[b'namespace'] |
|
2270 | 2270 | r = pushkey.decodekeys(inpart.read()) |
|
2271 | 2271 | op.records.add(b'listkeys', (namespace, r)) |
|
2272 | 2272 | |
|
2273 | 2273 | |
|
2274 | 2274 | @parthandler(b'pushkey', (b'namespace', b'key', b'old', b'new')) |
|
2275 | 2275 | def handlepushkey(op, inpart): |
|
2276 | 2276 | """process a pushkey request""" |
|
2277 | 2277 | dec = pushkey.decode |
|
2278 | 2278 | namespace = dec(inpart.params[b'namespace']) |
|
2279 | 2279 | key = dec(inpart.params[b'key']) |
|
2280 | 2280 | old = dec(inpart.params[b'old']) |
|
2281 | 2281 | new = dec(inpart.params[b'new']) |
|
2282 | 2282 | # Grab the transaction to ensure that we have the lock before performing the |
|
2283 | 2283 | # pushkey. |
|
2284 | 2284 | if op.ui.configbool(b'experimental', b'bundle2lazylocking'): |
|
2285 | 2285 | op.gettransaction() |
|
2286 | 2286 | ret = op.repo.pushkey(namespace, key, old, new) |
|
2287 | 2287 | record = {b'namespace': namespace, b'key': key, b'old': old, b'new': new} |
|
2288 | 2288 | op.records.add(b'pushkey', record) |
|
2289 | 2289 | if op.reply is not None: |
|
2290 | 2290 | rpart = op.reply.newpart(b'reply:pushkey') |
|
2291 | 2291 | rpart.addparam( |
|
2292 | 2292 | b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False |
|
2293 | 2293 | ) |
|
2294 | 2294 | rpart.addparam(b'return', b'%i' % ret, mandatory=False) |
|
2295 | 2295 | if inpart.mandatory and not ret: |
|
2296 | 2296 | kwargs = {} |
|
2297 | 2297 | for key in (b'namespace', b'key', b'new', b'old', b'ret'): |
|
2298 | 2298 | if key in inpart.params: |
|
2299 | 2299 | kwargs[key] = inpart.params[key] |
|
2300 | 2300 | raise error.PushkeyFailed( |
|
2301 | 2301 | partid=b'%d' % inpart.id, **pycompat.strkwargs(kwargs) |
|
2302 | 2302 | ) |
|
2303 | 2303 | |
|
2304 | 2304 | |
|
2305 | 2305 | @parthandler(b'bookmarks') |
|
2306 | 2306 | def handlebookmark(op, inpart): |
|
2307 | 2307 | """transmit bookmark information |
|
2308 | 2308 | |
|
2309 | 2309 | The part contains binary encoded bookmark information. |
|
2310 | 2310 | |
|
2311 | 2311 | The exact behavior of this part can be controlled by the 'bookmarks' mode |
|
2312 | 2312 | on the bundle operation. |
|
2313 | 2313 | |
|
2314 | 2314 | When mode is 'apply' (the default) the bookmark information is applied as |
|
2315 | 2315 | is to the unbundling repository. Make sure a 'check:bookmarks' part is |
|
2316 | 2316 | issued earlier to check for push races in such update. This behavior is |
|
2317 | 2317 | suitable for pushing. |
|
2318 | 2318 | |
|
2319 | 2319 | When mode is 'records', the information is recorded into the 'bookmarks' |
|
2320 | 2320 | records of the bundle operation. This behavior is suitable for pulling. |
|
2321 | 2321 | """ |
|
2322 | 2322 | changes = bookmarks.binarydecode(inpart) |
|
2323 | 2323 | |
|
2324 | 2324 | pushkeycompat = op.repo.ui.configbool( |
|
2325 | 2325 | b'server', b'bookmarks-pushkey-compat' |
|
2326 | 2326 | ) |
|
2327 | 2327 | bookmarksmode = op.modes.get(b'bookmarks', b'apply') |
|
2328 | 2328 | |
|
2329 | 2329 | if bookmarksmode == b'apply': |
|
2330 | 2330 | tr = op.gettransaction() |
|
2331 | 2331 | bookstore = op.repo._bookmarks |
|
2332 | 2332 | if pushkeycompat: |
|
2333 | 2333 | allhooks = [] |
|
2334 | 2334 | for book, node in changes: |
|
2335 | 2335 | hookargs = tr.hookargs.copy() |
|
2336 | 2336 | hookargs[b'pushkeycompat'] = b'1' |
|
2337 | 2337 | hookargs[b'namespace'] = b'bookmarks' |
|
2338 | 2338 | hookargs[b'key'] = book |
|
2339 | 2339 | hookargs[b'old'] = nodemod.hex(bookstore.get(book, b'')) |
|
2340 | 2340 | hookargs[b'new'] = nodemod.hex( |
|
2341 | 2341 | node if node is not None else b'' |
|
2342 | 2342 | ) |
|
2343 | 2343 | allhooks.append(hookargs) |
|
2344 | 2344 | |
|
2345 | 2345 | for hookargs in allhooks: |
|
2346 | 2346 | op.repo.hook( |
|
2347 | 2347 | b'prepushkey', throw=True, **pycompat.strkwargs(hookargs) |
|
2348 | 2348 | ) |
|
2349 | 2349 | |
|
2350 | 2350 | bookstore.applychanges(op.repo, op.gettransaction(), changes) |
|
2351 | 2351 | |
|
2352 | 2352 | if pushkeycompat: |
|
2353 | 2353 | |
|
2354 | 2354 | def runhook(): |
|
2355 | 2355 | for hookargs in allhooks: |
|
2356 | 2356 | op.repo.hook(b'pushkey', **pycompat.strkwargs(hookargs)) |
|
2357 | 2357 | |
|
2358 | 2358 | op.repo._afterlock(runhook) |
|
2359 | 2359 | |
|
2360 | 2360 | elif bookmarksmode == b'records': |
|
2361 | 2361 | for book, node in changes: |
|
2362 | 2362 | record = {b'bookmark': book, b'node': node} |
|
2363 | 2363 | op.records.add(b'bookmarks', record) |
|
2364 | 2364 | else: |
|
2365 | 2365 | raise error.ProgrammingError( |
|
2366 | 2366 | b'unkown bookmark mode: %s' % bookmarksmode |
|
2367 | 2367 | ) |
|
2368 | 2368 | |
|
2369 | 2369 | |
|
2370 | 2370 | @parthandler(b'phase-heads') |
|
2371 | 2371 | def handlephases(op, inpart): |
|
2372 | 2372 | """apply phases from bundle part to repo""" |
|
2373 | 2373 | headsbyphase = phases.binarydecode(inpart) |
|
2374 | 2374 | phases.updatephases(op.repo.unfiltered(), op.gettransaction, headsbyphase) |
|
2375 | 2375 | |
|
2376 | 2376 | |
|
2377 | 2377 | @parthandler(b'reply:pushkey', (b'return', b'in-reply-to')) |
|
2378 | 2378 | def handlepushkeyreply(op, inpart): |
|
2379 | 2379 | """retrieve the result of a pushkey request""" |
|
2380 | 2380 | ret = int(inpart.params[b'return']) |
|
2381 | 2381 | partid = int(inpart.params[b'in-reply-to']) |
|
2382 | 2382 | op.records.add(b'pushkey', {b'return': ret}, partid) |
|
2383 | 2383 | |
|
2384 | 2384 | |
|
2385 | 2385 | @parthandler(b'obsmarkers') |
|
2386 | 2386 | def handleobsmarker(op, inpart): |
|
2387 | 2387 | """add a stream of obsmarkers to the repo""" |
|
2388 | 2388 | tr = op.gettransaction() |
|
2389 | 2389 | markerdata = inpart.read() |
|
2390 | 2390 | if op.ui.config(b'experimental', b'obsmarkers-exchange-debug'): |
|
2391 | op.ui.write( | |
|
2391 | op.ui.writenoi18n( | |
|
2392 | 2392 | b'obsmarker-exchange: %i bytes received\n' % len(markerdata) |
|
2393 | 2393 | ) |
|
2394 | 2394 | # The mergemarkers call will crash if marker creation is not enabled. |
|
2395 | 2395 | # we want to avoid this if the part is advisory. |
|
2396 | 2396 | if not inpart.mandatory and op.repo.obsstore.readonly: |
|
2397 | 2397 | op.repo.ui.debug( |
|
2398 | 2398 | b'ignoring obsolescence markers, feature not enabled\n' |
|
2399 | 2399 | ) |
|
2400 | 2400 | return |
|
2401 | 2401 | new = op.repo.obsstore.mergemarkers(tr, markerdata) |
|
2402 | 2402 | op.repo.invalidatevolatilesets() |
|
2403 | 2403 | op.records.add(b'obsmarkers', {b'new': new}) |
|
2404 | 2404 | if op.reply is not None: |
|
2405 | 2405 | rpart = op.reply.newpart(b'reply:obsmarkers') |
|
2406 | 2406 | rpart.addparam( |
|
2407 | 2407 | b'in-reply-to', pycompat.bytestr(inpart.id), mandatory=False |
|
2408 | 2408 | ) |
|
2409 | 2409 | rpart.addparam(b'new', b'%i' % new, mandatory=False) |
|
2410 | 2410 | |
|
2411 | 2411 | |
|
2412 | 2412 | @parthandler(b'reply:obsmarkers', (b'new', b'in-reply-to')) |
|
2413 | 2413 | def handleobsmarkerreply(op, inpart): |
|
2414 | 2414 | """retrieve the result of a pushkey request""" |
|
2415 | 2415 | ret = int(inpart.params[b'new']) |
|
2416 | 2416 | partid = int(inpart.params[b'in-reply-to']) |
|
2417 | 2417 | op.records.add(b'obsmarkers', {b'new': ret}, partid) |
|
2418 | 2418 | |
|
2419 | 2419 | |
|
2420 | 2420 | @parthandler(b'hgtagsfnodes') |
|
2421 | 2421 | def handlehgtagsfnodes(op, inpart): |
|
2422 | 2422 | """Applies .hgtags fnodes cache entries to the local repo. |
|
2423 | 2423 | |
|
2424 | 2424 | Payload is pairs of 20 byte changeset nodes and filenodes. |
|
2425 | 2425 | """ |
|
2426 | 2426 | # Grab the transaction so we ensure that we have the lock at this point. |
|
2427 | 2427 | if op.ui.configbool(b'experimental', b'bundle2lazylocking'): |
|
2428 | 2428 | op.gettransaction() |
|
2429 | 2429 | cache = tags.hgtagsfnodescache(op.repo.unfiltered()) |
|
2430 | 2430 | |
|
2431 | 2431 | count = 0 |
|
2432 | 2432 | while True: |
|
2433 | 2433 | node = inpart.read(20) |
|
2434 | 2434 | fnode = inpart.read(20) |
|
2435 | 2435 | if len(node) < 20 or len(fnode) < 20: |
|
2436 | 2436 | op.ui.debug(b'ignoring incomplete received .hgtags fnodes data\n') |
|
2437 | 2437 | break |
|
2438 | 2438 | cache.setfnode(node, fnode) |
|
2439 | 2439 | count += 1 |
|
2440 | 2440 | |
|
2441 | 2441 | cache.write() |
|
2442 | 2442 | op.ui.debug(b'applied %i hgtags fnodes cache entries\n' % count) |
|
2443 | 2443 | |
|
2444 | 2444 | |
|
2445 | 2445 | rbcstruct = struct.Struct(b'>III') |
|
2446 | 2446 | |
|
2447 | 2447 | |
|
2448 | 2448 | @parthandler(b'cache:rev-branch-cache') |
|
2449 | 2449 | def handlerbc(op, inpart): |
|
2450 | 2450 | """receive a rev-branch-cache payload and update the local cache |
|
2451 | 2451 | |
|
2452 | 2452 | The payload is a series of data related to each branch |
|
2453 | 2453 | |
|
2454 | 2454 | 1) branch name length |
|
2455 | 2455 | 2) number of open heads |
|
2456 | 2456 | 3) number of closed heads |
|
2457 | 2457 | 4) open heads nodes |
|
2458 | 2458 | 5) closed heads nodes |
|
2459 | 2459 | """ |
|
2460 | 2460 | total = 0 |
|
2461 | 2461 | rawheader = inpart.read(rbcstruct.size) |
|
2462 | 2462 | cache = op.repo.revbranchcache() |
|
2463 | 2463 | cl = op.repo.unfiltered().changelog |
|
2464 | 2464 | while rawheader: |
|
2465 | 2465 | header = rbcstruct.unpack(rawheader) |
|
2466 | 2466 | total += header[1] + header[2] |
|
2467 | 2467 | utf8branch = inpart.read(header[0]) |
|
2468 | 2468 | branch = encoding.tolocal(utf8branch) |
|
2469 | 2469 | for x in pycompat.xrange(header[1]): |
|
2470 | 2470 | node = inpart.read(20) |
|
2471 | 2471 | rev = cl.rev(node) |
|
2472 | 2472 | cache.setdata(branch, rev, node, False) |
|
2473 | 2473 | for x in pycompat.xrange(header[2]): |
|
2474 | 2474 | node = inpart.read(20) |
|
2475 | 2475 | rev = cl.rev(node) |
|
2476 | 2476 | cache.setdata(branch, rev, node, True) |
|
2477 | 2477 | rawheader = inpart.read(rbcstruct.size) |
|
2478 | 2478 | cache.write() |
|
2479 | 2479 | |
|
2480 | 2480 | |
|
2481 | 2481 | @parthandler(b'pushvars') |
|
2482 | 2482 | def bundle2getvars(op, part): |
|
2483 | 2483 | '''unbundle a bundle2 containing shellvars on the server''' |
|
2484 | 2484 | # An option to disable unbundling on server-side for security reasons |
|
2485 | 2485 | if op.ui.configbool(b'push', b'pushvars.server'): |
|
2486 | 2486 | hookargs = {} |
|
2487 | 2487 | for key, value in part.advisoryparams: |
|
2488 | 2488 | key = key.upper() |
|
2489 | 2489 | # We want pushed variables to have USERVAR_ prepended so we know |
|
2490 | 2490 | # they came from the --pushvar flag. |
|
2491 | 2491 | key = b"USERVAR_" + key |
|
2492 | 2492 | hookargs[key] = value |
|
2493 | 2493 | op.addhookargs(hookargs) |
|
2494 | 2494 | |
|
2495 | 2495 | |
|
2496 | 2496 | @parthandler(b'stream2', (b'requirements', b'filecount', b'bytecount')) |
|
2497 | 2497 | def handlestreamv2bundle(op, part): |
|
2498 | 2498 | |
|
2499 | 2499 | requirements = urlreq.unquote(part.params[b'requirements']).split(b',') |
|
2500 | 2500 | filecount = int(part.params[b'filecount']) |
|
2501 | 2501 | bytecount = int(part.params[b'bytecount']) |
|
2502 | 2502 | |
|
2503 | 2503 | repo = op.repo |
|
2504 | 2504 | if len(repo): |
|
2505 | 2505 | msg = _(b'cannot apply stream clone to non empty repository') |
|
2506 | 2506 | raise error.Abort(msg) |
|
2507 | 2507 | |
|
2508 | 2508 | repo.ui.debug(b'applying stream bundle\n') |
|
2509 | 2509 | streamclone.applybundlev2(repo, part, filecount, bytecount, requirements) |
|
2510 | 2510 | |
|
2511 | 2511 | |
|
2512 | 2512 | def widen_bundle( |
|
2513 | 2513 | bundler, repo, oldmatcher, newmatcher, common, known, cgversion, ellipses |
|
2514 | 2514 | ): |
|
2515 | 2515 | """generates bundle2 for widening a narrow clone |
|
2516 | 2516 | |
|
2517 | 2517 | bundler is the bundle to which data should be added |
|
2518 | 2518 | repo is the localrepository instance |
|
2519 | 2519 | oldmatcher matches what the client already has |
|
2520 | 2520 | newmatcher matches what the client needs (including what it already has) |
|
2521 | 2521 | common is set of common heads between server and client |
|
2522 | 2522 | known is a set of revs known on the client side (used in ellipses) |
|
2523 | 2523 | cgversion is the changegroup version to send |
|
2524 | 2524 | ellipses is boolean value telling whether to send ellipses data or not |
|
2525 | 2525 | |
|
2526 | 2526 | returns bundle2 of the data required for extending |
|
2527 | 2527 | """ |
|
2528 | 2528 | commonnodes = set() |
|
2529 | 2529 | cl = repo.changelog |
|
2530 | 2530 | for r in repo.revs(b"::%ln", common): |
|
2531 | 2531 | commonnodes.add(cl.node(r)) |
|
2532 | 2532 | if commonnodes: |
|
2533 | 2533 | # XXX: we should only send the filelogs (and treemanifest). user |
|
2534 | 2534 | # already has the changelog and manifest |
|
2535 | 2535 | packer = changegroup.getbundler( |
|
2536 | 2536 | cgversion, |
|
2537 | 2537 | repo, |
|
2538 | 2538 | oldmatcher=oldmatcher, |
|
2539 | 2539 | matcher=newmatcher, |
|
2540 | 2540 | fullnodes=commonnodes, |
|
2541 | 2541 | ) |
|
2542 | 2542 | cgdata = packer.generate( |
|
2543 | 2543 | {nodemod.nullid}, |
|
2544 | 2544 | list(commonnodes), |
|
2545 | 2545 | False, |
|
2546 | 2546 | b'narrow_widen', |
|
2547 | 2547 | changelog=False, |
|
2548 | 2548 | ) |
|
2549 | 2549 | |
|
2550 | 2550 | part = bundler.newpart(b'changegroup', data=cgdata) |
|
2551 | 2551 | part.addparam(b'version', cgversion) |
|
2552 | 2552 | if b'treemanifest' in repo.requirements: |
|
2553 | 2553 | part.addparam(b'treemanifest', b'1') |
|
2554 | 2554 | |
|
2555 | 2555 | return bundler |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
General Comments 0
You need to be logged in to leave comments.
Login now