##// END OF EJS Templates
dirstate: use `dirstate.change_files` to scope the change in `automv`...
marmoute -
r50938:5cfc4835 default
parent child Browse files
Show More
@@ -1,118 +1,124
1 # automv.py
1 # automv.py
2 #
2 #
3 # Copyright 2013-2016 Facebook, Inc.
3 # Copyright 2013-2016 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 """check for unrecorded moves at commit time (EXPERIMENTAL)
7 """check for unrecorded moves at commit time (EXPERIMENTAL)
8
8
9 This extension checks at commit/amend time if any of the committed files
9 This extension checks at commit/amend time if any of the committed files
10 comes from an unrecorded mv.
10 comes from an unrecorded mv.
11
11
12 The threshold at which a file is considered a move can be set with the
12 The threshold at which a file is considered a move can be set with the
13 ``automv.similarity`` config option. This option takes a percentage between 0
13 ``automv.similarity`` config option. This option takes a percentage between 0
14 (disabled) and 100 (files must be identical), the default is 95.
14 (disabled) and 100 (files must be identical), the default is 95.
15
15
16 """
16 """
17
17
18 # Using 95 as a default similarity is based on an analysis of the mercurial
18 # Using 95 as a default similarity is based on an analysis of the mercurial
19 # repositories of the cpython, mozilla-central & mercurial repositories, as
19 # repositories of the cpython, mozilla-central & mercurial repositories, as
20 # well as 2 very large facebook repositories. At 95 50% of all potential
20 # well as 2 very large facebook repositories. At 95 50% of all potential
21 # missed moves would be caught, as well as correspond with 87% of all
21 # missed moves would be caught, as well as correspond with 87% of all
22 # explicitly marked moves. Together, 80% of moved files are 95% similar or
22 # explicitly marked moves. Together, 80% of moved files are 95% similar or
23 # more.
23 # more.
24 #
24 #
25 # See http://markmail.org/thread/5pxnljesvufvom57 for context.
25 # See http://markmail.org/thread/5pxnljesvufvom57 for context.
26
26
27
27
28 from mercurial.i18n import _
28 from mercurial.i18n import _
29 from mercurial import (
29 from mercurial import (
30 commands,
30 commands,
31 copies,
31 copies,
32 error,
32 error,
33 extensions,
33 extensions,
34 pycompat,
34 pycompat,
35 registrar,
35 registrar,
36 scmutil,
36 scmutil,
37 similar,
37 similar,
38 )
38 )
39
39
40 configtable = {}
40 configtable = {}
41 configitem = registrar.configitem(configtable)
41 configitem = registrar.configitem(configtable)
42
42
43 configitem(
43 configitem(
44 b'automv',
44 b'automv',
45 b'similarity',
45 b'similarity',
46 default=95,
46 default=95,
47 )
47 )
48
48
49
49
50 def extsetup(ui):
50 def extsetup(ui):
51 entry = extensions.wrapcommand(commands.table, b'commit', mvcheck)
51 entry = extensions.wrapcommand(commands.table, b'commit', mvcheck)
52 entry[1].append(
52 entry[1].append(
53 (b'', b'no-automv', None, _(b'disable automatic file move detection'))
53 (b'', b'no-automv', None, _(b'disable automatic file move detection'))
54 )
54 )
55
55
56
56
57 def mvcheck(orig, ui, repo, *pats, **opts):
57 def mvcheck(orig, ui, repo, *pats, **opts):
58 """Hook to check for moves at commit time"""
58 """Hook to check for moves at commit time"""
59 opts = pycompat.byteskwargs(opts)
59 opts = pycompat.byteskwargs(opts)
60 renames = None
60 renames = None
61 disabled = opts.pop(b'no_automv', False)
61 disabled = opts.pop(b'no_automv', False)
62 if not disabled:
62 if not disabled:
63 threshold = ui.configint(b'automv', b'similarity')
63 threshold = ui.configint(b'automv', b'similarity')
64 if not 0 <= threshold <= 100:
64 if not 0 <= threshold <= 100:
65 raise error.Abort(_(b'automv.similarity must be between 0 and 100'))
65 raise error.Abort(_(b'automv.similarity must be between 0 and 100'))
66 if threshold > 0:
66 if threshold > 0:
67 match = scmutil.match(repo[None], pats, opts)
67 match = scmutil.match(repo[None], pats, opts)
68 added, removed = _interestingfiles(repo, match)
68 added, removed = _interestingfiles(repo, match)
69 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
69 uipathfn = scmutil.getuipathfn(repo, legacyrelativevalue=True)
70 renames = _findrenames(
70 renames = _findrenames(
71 repo, uipathfn, added, removed, threshold / 100.0
71 repo, uipathfn, added, removed, threshold / 100.0
72 )
72 )
73
73
74 with repo.wlock():
74 with repo.wlock():
75 if renames is not None:
75 if renames is not None:
76 scmutil._markchanges(repo, (), (), renames)
76 with repo.dirstate.changing_files(repo):
77 # XXX this should be wider and integrated with the commit
78 # transaction. At the same time as we do the `addremove` logic
79 # for commit. However we can't really do better with the
80 # current extension structure, and this is not worse than what
81 # happened before.
82 scmutil._markchanges(repo, (), (), renames)
77 return orig(ui, repo, *pats, **pycompat.strkwargs(opts))
83 return orig(ui, repo, *pats, **pycompat.strkwargs(opts))
78
84
79
85
80 def _interestingfiles(repo, matcher):
86 def _interestingfiles(repo, matcher):
81 """Find what files were added or removed in this commit.
87 """Find what files were added or removed in this commit.
82
88
83 Returns a tuple of two lists: (added, removed). Only files not *already*
89 Returns a tuple of two lists: (added, removed). Only files not *already*
84 marked as moved are included in the added list.
90 marked as moved are included in the added list.
85
91
86 """
92 """
87 stat = repo.status(match=matcher)
93 stat = repo.status(match=matcher)
88 added = stat.added
94 added = stat.added
89 removed = stat.removed
95 removed = stat.removed
90
96
91 copy = copies.pathcopies(repo[b'.'], repo[None], matcher)
97 copy = copies.pathcopies(repo[b'.'], repo[None], matcher)
92 # remove the copy files for which we already have copy info
98 # remove the copy files for which we already have copy info
93 added = [f for f in added if f not in copy]
99 added = [f for f in added if f not in copy]
94
100
95 return added, removed
101 return added, removed
96
102
97
103
98 def _findrenames(repo, uipathfn, added, removed, similarity):
104 def _findrenames(repo, uipathfn, added, removed, similarity):
99 """Find what files in added are really moved files.
105 """Find what files in added are really moved files.
100
106
101 Any file named in removed that is at least similarity% similar to a file
107 Any file named in removed that is at least similarity% similar to a file
102 in added is seen as a rename.
108 in added is seen as a rename.
103
109
104 """
110 """
105 renames = {}
111 renames = {}
106 if similarity > 0:
112 if similarity > 0:
107 for src, dst, score in similar.findrenames(
113 for src, dst, score in similar.findrenames(
108 repo, added, removed, similarity
114 repo, added, removed, similarity
109 ):
115 ):
110 if repo.ui.verbose:
116 if repo.ui.verbose:
111 repo.ui.status(
117 repo.ui.status(
112 _(b'detected move of %s as %s (%d%% similar)\n')
118 _(b'detected move of %s as %s (%d%% similar)\n')
113 % (uipathfn(src), uipathfn(dst), score * 100)
119 % (uipathfn(src), uipathfn(dst), score * 100)
114 )
120 )
115 renames[dst] = src
121 renames[dst] = src
116 if renames:
122 if renames:
117 repo.ui.status(_(b'detected move of %d files\n') % len(renames))
123 repo.ui.status(_(b'detected move of %d files\n') % len(renames))
118 return renames
124 return renames
General Comments 0
You need to be logged in to leave comments. Login now