automv.py
100 lines
| 3.3 KiB
| text/x-python
|
PythonLexer
/ hgext / automv.py
|
r28129 | # automv.py | ||
# | ||||
# Copyright 2013-2016 Facebook, Inc. | ||||
# | ||||
# This software may be used and distributed according to the terms of the | ||||
# GNU General Public License version 2 or any later version. | ||||
|
r31599 | """check for unrecorded moves at commit time (EXPERIMENTAL) | ||
|
r28129 | |||
This extension checks at commit/amend time if any of the committed files | ||||
comes from an unrecorded mv. | ||||
The threshold at which a file is considered a move can be set with the | ||||
|
r28152 | ``automv.similarity`` config option. This option takes a percentage between 0 | ||
|
r28183 | (disabled) and 100 (files must be identical), the default is 95. | ||
|
r28129 | |||
""" | ||||
|
r28183 | |||
# Using 95 as a default similarity is based on an analysis of the mercurial | ||||
# repositories of the cpython, mozilla-central & mercurial repositories, as | ||||
# well as 2 very large facebook repositories. At 95 50% of all potential | ||||
# missed moves would be caught, as well as correspond with 87% of all | ||||
# explicitly marked moves. Together, 80% of moved files are 95% similar or | ||||
# more. | ||||
# | ||||
# See http://markmail.org/thread/5pxnljesvufvom57 for context. | ||||
|
r28129 | from __future__ import absolute_import | ||
|
r29205 | from mercurial.i18n import _ | ||
|
r28129 | from mercurial import ( | ||
commands, | ||||
copies, | ||||
|
r28183 | error, | ||
|
r28129 | extensions, | ||
scmutil, | ||||
similar | ||||
) | ||||
def extsetup(ui): | ||||
entry = extensions.wrapcommand( | ||||
commands.table, 'commit', mvcheck) | ||||
entry[1].append( | ||||
('', 'no-automv', None, | ||||
_('disable automatic file move detection'))) | ||||
def mvcheck(orig, ui, repo, *pats, **opts): | ||||
|
r28149 | """Hook to check for moves at commit time""" | ||
|
r28151 | renames = None | ||
|
r28129 | disabled = opts.pop('no_automv', False) | ||
if not disabled: | ||||
|
r28183 | threshold = ui.configint('automv', 'similarity', 95) | ||
if not 0 <= threshold <= 100: | ||||
raise error.Abort(_('automv.similarity must be between 0 and 100')) | ||||
|
r28129 | if threshold > 0: | ||
match = scmutil.match(repo[None], pats, opts) | ||||
added, removed = _interestingfiles(repo, match) | ||||
|
r28152 | renames = _findrenames(repo, match, added, removed, | ||
threshold / 100.0) | ||||
|
r28151 | |||
with repo.wlock(): | ||||
if renames is not None: | ||||
|
r28150 | scmutil._markchanges(repo, (), (), renames) | ||
|
r28151 | return orig(ui, repo, *pats, **opts) | ||
|
r28129 | |||
def _interestingfiles(repo, matcher): | ||||
|
r28149 | """Find what files were added or removed in this commit. | ||
Returns a tuple of two lists: (added, removed). Only files not *already* | ||||
marked as moved are included in the added list. | ||||
""" | ||||
|
r28146 | stat = repo.status(match=matcher) | ||
|
r28129 | added = stat[1] | ||
removed = stat[2] | ||||
copy = copies._forwardcopies(repo['.'], repo[None], matcher) | ||||
# remove the copy files for which we already have copy info | ||||
added = [f for f in added if f not in copy] | ||||
return added, removed | ||||
def _findrenames(repo, matcher, added, removed, similarity): | ||||
|
r28149 | """Find what files in added are really moved files. | ||
Any file named in removed that is at least similarity% similar to a file | ||||
in added is seen as a rename. | ||||
""" | ||||
|
r28129 | renames = {} | ||
if similarity > 0: | ||||
for src, dst, score in similar.findrenames( | ||||
repo, added, removed, similarity): | ||||
if repo.ui.verbose: | ||||
repo.ui.status( | ||||
_('detected move of %s as %s (%d%% similar)\n') % ( | ||||
matcher.rel(src), matcher.rel(dst), score * 100)) | ||||
renames[dst] = src | ||||
if renames: | ||||
repo.ui.status(_('detected move of %d files\n') % len(renames)) | ||||
return renames | ||||