automv.py
128 lines
| 4.1 KiB
| text/x-python
|
PythonLexer
/ hgext / automv.py
Martijn Pieters
|
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. | ||||
Jun Wu
|
r31599 | """check for unrecorded moves at commit time (EXPERIMENTAL) | ||
Martijn Pieters
|
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 | ||||
Martijn Pieters
|
r28152 | ``automv.similarity`` config option. This option takes a percentage between 0 | ||
Martijn Pieters
|
r28183 | (disabled) and 100 (files must be identical), the default is 95. | ||
Martijn Pieters
|
r28129 | |||
""" | ||||
Martijn Pieters
|
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. | ||||
Matt Harbison
|
r52756 | from __future__ import annotations | ||
Martijn Pieters
|
r28129 | |||
Yuya Nishihara
|
r29205 | from mercurial.i18n import _ | ||
Martijn Pieters
|
r28129 | from mercurial import ( | ||
commands, | ||||
copies, | ||||
Martijn Pieters
|
r28183 | error, | ||
Martijn Pieters
|
r28129 | extensions, | ||
Pulkit Goyal
|
r34972 | pycompat, | ||
r33185 | registrar, | |||
Martijn Pieters
|
r28129 | scmutil, | ||
Augie Fackler
|
r43346 | similar, | ||
Martijn Pieters
|
r28129 | ) | ||
r33185 | configtable = {} | |||
configitem = registrar.configitem(configtable) | ||||
Augie Fackler
|
r43346 | configitem( | ||
Augie Fackler
|
r46554 | b'automv', | ||
b'similarity', | ||||
default=95, | ||||
r33185 | ) | |||
Augie Fackler
|
r43346 | |||
Martijn Pieters
|
r28129 | def extsetup(ui): | ||
Augie Fackler
|
r43347 | entry = extensions.wrapcommand(commands.table, b'commit', mvcheck) | ||
Martijn Pieters
|
r28129 | entry[1].append( | ||
Augie Fackler
|
r43347 | (b'', b'no-automv', None, _(b'disable automatic file move detection')) | ||
Augie Fackler
|
r43346 | ) | ||
Martijn Pieters
|
r28129 | |||
def mvcheck(orig, ui, repo, *pats, **opts): | ||||
Martijn Pieters
|
r28149 | """Hook to check for moves at commit time""" | ||
Martijn Pieters
|
r28151 | renames = None | ||
Matt Harbison
|
r51763 | disabled = opts.pop('no_automv', False) | ||
r51019 | with repo.wlock(): | |||
if not disabled: | ||||
threshold = ui.configint(b'automv', b'similarity') | ||||
if not 0 <= threshold <= 100: | ||||
raise error.Abort( | ||||
_(b'automv.similarity must be between 0 and 100') | ||||
) | ||||
if threshold > 0: | ||||
Matt Harbison
|
r51763 | match = scmutil.match( | ||
repo[None], pats, pycompat.byteskwargs(opts) | ||||
) | ||||
r51019 | added, removed = _interestingfiles(repo, match) | |||
uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True) | ||||
renames = _findrenames( | ||||
repo, uipathfn, added, removed, threshold / 100.0 | ||||
) | ||||
Martijn Pieters
|
r28151 | |||
if renames is not None: | ||||
r50938 | with repo.dirstate.changing_files(repo): | |||
# XXX this should be wider and integrated with the commit | ||||
# transaction. At the same time as we do the `addremove` logic | ||||
# for commit. However we can't really do better with the | ||||
# current extension structure, and this is not worse than what | ||||
# happened before. | ||||
scmutil._markchanges(repo, (), (), renames) | ||||
Matt Harbison
|
r51763 | return orig(ui, repo, *pats, **opts) | ||
Martijn Pieters
|
r28129 | |||
Augie Fackler
|
r43346 | |||
Martijn Pieters
|
r28129 | def _interestingfiles(repo, matcher): | ||
Martijn Pieters
|
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. | ||||
""" | ||||
Martijn Pieters
|
r28146 | stat = repo.status(match=matcher) | ||
Martin von Zweigbergk
|
r42747 | added = stat.added | ||
removed = stat.removed | ||||
Martijn Pieters
|
r28129 | |||
Augie Fackler
|
r43347 | copy = copies.pathcopies(repo[b'.'], repo[None], matcher) | ||
Martijn Pieters
|
r28129 | # 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 | ||||
Augie Fackler
|
r43346 | |||
Martin von Zweigbergk
|
r41809 | def _findrenames(repo, uipathfn, added, removed, similarity): | ||
Martijn Pieters
|
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. | ||||
""" | ||||
Martijn Pieters
|
r28129 | renames = {} | ||
if similarity > 0: | ||||
for src, dst, score in similar.findrenames( | ||||
Augie Fackler
|
r43346 | repo, added, removed, similarity | ||
): | ||||
Martijn Pieters
|
r28129 | if repo.ui.verbose: | ||
repo.ui.status( | ||||
Augie Fackler
|
r43347 | _(b'detected move of %s as %s (%d%% similar)\n') | ||
Augie Fackler
|
r43346 | % (uipathfn(src), uipathfn(dst), score * 100) | ||
) | ||||
Martijn Pieters
|
r28129 | renames[dst] = src | ||
if renames: | ||||
Augie Fackler
|
r43347 | repo.ui.status(_(b'detected move of %d files\n') % len(renames)) | ||
Martijn Pieters
|
r28129 | return renames | ||