1
1
#!/usr/bin/env python
2
2
# -*- coding: utf-8 -*-
3
-
4
3
# copyright 2013, y-p @ github
5
-
6
- from __future__ import print_function
7
- from pandas .compat import range , lrange , map , string_types , text_type
8
-
9
- """Search the git history for all commits touching a named method
4
+ """
5
+ Search the git history for all commits touching a named method
10
6
11
7
You need the sh module to run this
12
- WARNING: this script uses git clean -f, running it on a repo with untracked files
13
- will probably erase them.
8
+ WARNING: this script uses git clean -f, running it on a repo with untracked
9
+ files will probably erase them.
10
+
11
+ Usage::
12
+ $ ./find_commits_touching_func.py (see arguments below)
14
13
"""
14
+ from __future__ import print_function
15
15
import logging
16
16
import re
17
17
import os
18
+ import argparse
18
19
from collections import namedtuple
19
- from pandas .compat import parse_date
20
-
20
+ from pandas .compat import lrange , map , string_types , text_type , parse_date
21
21
try :
22
22
import sh
23
23
except ImportError :
24
- raise ImportError ("The 'sh' package is required in order to run this script. " )
24
+ raise ImportError ("The 'sh' package is required to run this script." )
25
25
26
- import argparse
27
26
28
27
desc = """
29
28
Find all commits touching a specified function across the codebase.
30
29
""" .strip ()
31
30
argparser = argparse .ArgumentParser (description = desc )
32
31
argparser .add_argument ('funcname' , metavar = 'FUNCNAME' ,
33
- help = 'Name of function/method to search for changes on. ' )
32
+ help = 'Name of function/method to search for changes on' )
34
33
argparser .add_argument ('-f' , '--file-masks' , metavar = 'f_re(,f_re)*' ,
35
34
default = ["\.py.?$" ],
36
- help = 'comma separated list of regexes to match filenames against \n ' +
37
- 'defaults all .py? files' )
35
+ help = 'comma separated list of regexes to match '
36
+ 'filenames against \n defaults all .py? files' )
38
37
argparser .add_argument ('-d' , '--dir-masks' , metavar = 'd_re(,d_re)*' ,
39
38
default = [],
40
- help = 'comma separated list of regexes to match base path against' )
39
+ help = 'comma separated list of regexes to match base '
40
+ 'path against' )
41
41
argparser .add_argument ('-p' , '--path-masks' , metavar = 'p_re(,p_re)*' ,
42
42
default = [],
43
- help = 'comma separated list of regexes to match full file path against' )
43
+ help = 'comma separated list of regexes to match full '
44
+ 'file path against' )
44
45
argparser .add_argument ('-y' , '--saw-the-warning' ,
45
- action = 'store_true' ,default = False ,
46
- help = 'must specify this to run, acknowledge you realize this will erase untracked files' )
46
+ action = 'store_true' , default = False ,
47
+ help = 'must specify this to run, acknowledge you '
48
+ 'realize this will erase untracked files' )
47
49
argparser .add_argument ('--debug-level' ,
48
50
default = "CRITICAL" ,
49
- help = 'debug level of messages (DEBUG,INFO,etc...)' )
50
-
51
+ help = 'debug level of messages (DEBUG, INFO, etc...)' )
51
52
args = argparser .parse_args ()
52
53
53
54
54
55
lfmt = logging .Formatter (fmt = '%(levelname)-8s %(message)s' ,
55
- datefmt = '%m-%d %H:%M:%S'
56
- )
57
-
56
+ datefmt = '%m-%d %H:%M:%S' )
58
57
shh = logging .StreamHandler ()
59
58
shh .setFormatter (lfmt )
60
-
61
- logger = logging .getLogger ("findit" )
59
+ logger = logging .getLogger ("findit" )
62
60
logger .addHandler (shh )
63
61
62
+ Hit = namedtuple ("Hit" , "commit path" )
63
+ HASH_LEN = 8
64
64
65
- Hit = namedtuple ("Hit" ,"commit path" )
66
- HASH_LEN = 8
67
65
68
66
def clean_checkout (comm ):
69
- h ,s , d = get_commit_vitals (comm )
67
+ h , s , d = get_commit_vitals (comm )
70
68
if len (s ) > 60 :
71
69
s = s [:60 ] + "..."
72
- s = s .split ("\n " )[0 ]
73
- logger .info ("CO: %s %s" % (comm ,s ))
70
+ s = s .split ("\n " )[0 ]
71
+ logger .info ("CO: %s %s" % (comm , s ))
74
72
75
- sh .git ('checkout' , comm , _tty_out = False )
73
+ sh .git ('checkout' , comm , _tty_out = False )
76
74
sh .git ('clean' , '-f' )
77
75
78
- def get_hits (defname ,files = ()):
79
- cs = set ()
76
+
77
+ def get_hits (defname , files = ()):
78
+ cs = set ()
80
79
for f in files :
81
80
try :
82
- r = sh .git ('blame' , '-L' , '/def\s*{start}/,/def/' .format (start = defname ),f ,_tty_out = False )
81
+ r = sh .git ('blame' ,
82
+ '-L' ,
83
+ '/def\s*{start}/,/def/' .format (start = defname ),
84
+ f ,
85
+ _tty_out = False )
83
86
except sh .ErrorReturnCode_128 :
84
87
logger .debug ("no matches in %s" % f )
85
88
continue
86
89
87
90
lines = r .strip ().splitlines ()[:- 1 ]
88
91
# remove comment lines
89
- lines = [x for x in lines if not re .search ("^\w+\s*\(.+\)\s*#" ,x )]
90
- hits = set (map (lambda x : x .split (" " )[0 ],lines ))
91
- cs .update (set (Hit (commit = c ,path = f ) for c in hits ))
92
+ lines = [x for x in lines if not re .search ("^\w+\s*\(.+\)\s*#" , x )]
93
+ hits = set (map (lambda x : x .split (" " )[0 ], lines ))
94
+ cs .update (set (Hit (commit = c , path = f ) for c in hits ))
92
95
93
96
return cs
94
97
95
- def get_commit_info (c ,fmt ,sep = '\t ' ):
96
- r = sh .git ('log' , "--format={}" .format (fmt ), '{}^..{}' .format (c ,c ),"-n" ,"1" ,_tty_out = False )
98
+
99
+ def get_commit_info (c , fmt , sep = '\t ' ):
100
+ r = sh .git ('log' ,
101
+ "--format={}" .format (fmt ),
102
+ '{}^..{}' .format (c , c ),
103
+ "-n" ,
104
+ "1" ,
105
+ _tty_out = False )
97
106
return text_type (r ).split (sep )
98
107
99
- def get_commit_vitals (c ,hlen = HASH_LEN ):
100
- h ,s ,d = get_commit_info (c ,'%H\t %s\t %ci' ,"\t " )
101
- return h [:hlen ],s ,parse_date (d )
102
108
103
- def file_filter (state ,dirname ,fnames ):
104
- if args .dir_masks and not any (re .search (x ,dirname ) for x in args .dir_masks ):
109
+ def get_commit_vitals (c , hlen = HASH_LEN ):
110
+ h , s , d = get_commit_info (c , '%H\t %s\t %ci' , "\t " )
111
+ return h [:hlen ], s , parse_date (d )
112
+
113
+
114
+ def file_filter (state , dirname , fnames ):
115
+ if (args .dir_masks and
116
+ not any (re .search (x , dirname ) for x in args .dir_masks )):
105
117
return
106
118
for f in fnames :
107
- p = os .path .abspath (os .path .join (os .path .realpath (dirname ),f ))
108
- if any (re .search (x ,f ) for x in args .file_masks )\
109
- or any (re .search (x ,p ) for x in args .path_masks ):
119
+ p = os .path .abspath (os .path .join (os .path .realpath (dirname ), f ))
120
+ if ( any (re .search (x , f ) for x in args .file_masks ) or
121
+ any (re .search (x , p ) for x in args .path_masks ) ):
110
122
if os .path .isfile (p ):
111
123
state ['files' ].append (p )
112
124
113
- def search (defname ,head_commit = "HEAD" ):
114
- HEAD ,s = get_commit_vitals ("HEAD" )[:2 ]
115
- logger .info ("HEAD at %s: %s" % (HEAD ,s ))
125
+
126
+ def search (defname , head_commit = "HEAD" ):
127
+ HEAD , s = get_commit_vitals ("HEAD" )[:2 ]
128
+ logger .info ("HEAD at %s: %s" % (HEAD , s ))
116
129
done_commits = set ()
117
130
# allhits = set()
118
131
files = []
119
132
state = dict (files = files )
120
- os .path . walk ('.' ,file_filter ,state )
133
+ os .walk ('.' , file_filter , state )
121
134
# files now holds a list of paths to files
122
135
123
136
# seed with hits from q
124
- allhits = set (get_hits (defname , files = files ))
137
+ allhits = set (get_hits (defname , files = files ))
125
138
q = set ([HEAD ])
126
139
try :
127
140
while q :
128
- h = q .pop ()
141
+ h = q .pop ()
129
142
clean_checkout (h )
130
- hits = get_hits (defname , files = files )
143
+ hits = get_hits (defname , files = files )
131
144
for x in hits :
132
- prevc = get_commit_vitals (x .commit + "^" )[0 ]
145
+ prevc = get_commit_vitals (x .commit + "^" )[0 ]
133
146
if prevc not in done_commits :
134
147
q .add (prevc )
135
148
allhits .update (hits )
@@ -141,43 +154,46 @@ def search(defname,head_commit="HEAD"):
141
154
clean_checkout (HEAD )
142
155
return allhits
143
156
157
+
144
158
def pprint_hits (hits ):
145
- SUBJ_LEN = 50
159
+ SUBJ_LEN = 50
146
160
PATH_LEN = 20
147
- hits = list (hits )
161
+ hits = list (hits )
148
162
max_p = 0
149
163
for hit in hits :
150
- p = hit .path .split (os .path .realpath (os .curdir )+ os .path .sep )[- 1 ]
151
- max_p = max (max_p ,len (p ))
164
+ p = hit .path .split (os .path .realpath (os .curdir ) + os .path .sep )[- 1 ]
165
+ max_p = max (max_p , len (p ))
152
166
153
167
if max_p < PATH_LEN :
154
168
SUBJ_LEN += PATH_LEN - max_p
155
169
PATH_LEN = max_p
156
170
157
171
def sorter (i ):
158
- h ,s , d = get_commit_vitals (hits [i ].commit )
159
- return hits [i ].path ,d
172
+ h , s , d = get_commit_vitals (hits [i ].commit )
173
+ return hits [i ].path , d
160
174
161
- print (" \n These commits touched the %s method in these files on these dates: \n " \
162
- % args .funcname )
163
- for i in sorted (lrange (len (hits )),key = sorter ):
175
+ print (( ' \n These commits touched the %s method in these files '
176
+ 'on these dates: \n ' ) % args .funcname )
177
+ for i in sorted (lrange (len (hits )), key = sorter ):
164
178
hit = hits [i ]
165
- h ,s , d = get_commit_vitals (hit .commit )
166
- p = hit .path .split (os .path .realpath (os .curdir )+ os .path .sep )[- 1 ]
179
+ h , s , d = get_commit_vitals (hit .commit )
180
+ p = hit .path .split (os .path .realpath (os .curdir ) + os .path .sep )[- 1 ]
167
181
168
182
fmt = "{:%d} {:10} {:<%d} {:<%d}" % (HASH_LEN , SUBJ_LEN , PATH_LEN )
169
183
if len (s ) > SUBJ_LEN :
170
- s = s [:SUBJ_LEN - 5 ] + " ..."
171
- print (fmt .format (h [:HASH_LEN ],d .isoformat ()[:10 ],s , p [- 20 :]) )
184
+ s = s [:SUBJ_LEN - 5 ] + " ..."
185
+ print (fmt .format (h [:HASH_LEN ], d .isoformat ()[:10 ], s , p [- 20 :]))
172
186
173
187
print ("\n " )
174
188
189
+
175
190
def main ():
176
191
if not args .saw_the_warning :
177
192
argparser .print_help ()
178
193
print ("""
179
194
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
180
- WARNING: this script uses git clean -f, running it on a repo with untracked files.
195
+ WARNING:
196
+ this script uses git clean -f, running it on a repo with untracked files.
181
197
It's recommended that you make a fresh clone and run from its root directory.
182
198
You must specify the -y argument to ignore this warning.
183
199
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@@ -190,12 +206,11 @@ def main():
190
206
if isinstance (args .dir_masks , string_types ):
191
207
args .dir_masks = args .dir_masks .split (',' )
192
208
193
- logger .setLevel (getattr (logging ,args .debug_level ))
209
+ logger .setLevel (getattr (logging , args .debug_level ))
194
210
195
- hits = search (args .funcname )
211
+ hits = search (args .funcname )
196
212
pprint_hits (hits )
197
213
198
- pass
199
214
200
215
if __name__ == "__main__" :
201
216
import sys
0 commit comments