11#!/usr/bin/env python
22# -*- coding: utf-8 -*-
3-
43# 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
106
117You 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)
1413"""
14+ from __future__ import print_function
1515import logging
1616import re
1717import os
18+ import argparse
1819from collections import namedtuple
19- from pandas .compat import parse_date
20-
20+ from pandas .compat import lrange , map , string_types , text_type , parse_date
2121try :
2222 import sh
2323except 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." )
2525
26- import argparse
2726
2827desc = """
2928Find all commits touching a specified function across the codebase.
3029""" .strip ()
3130argparser = argparse .ArgumentParser (description = desc )
3231argparser .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' )
3433argparser .add_argument ('-f' , '--file-masks' , metavar = 'f_re(,f_re)*' ,
3534 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' )
3837argparser .add_argument ('-d' , '--dir-masks' , metavar = 'd_re(,d_re)*' ,
3938 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' )
4141argparser .add_argument ('-p' , '--path-masks' , metavar = 'p_re(,p_re)*' ,
4242 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' )
4445argparser .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' )
4749argparser .add_argument ('--debug-level' ,
4850 default = "CRITICAL" ,
49- help = 'debug level of messages (DEBUG,INFO,etc...)' )
50-
51+ help = 'debug level of messages (DEBUG, INFO, etc...)' )
5152args = argparser .parse_args ()
5253
5354
5455lfmt = logging .Formatter (fmt = '%(levelname)-8s %(message)s' ,
55- datefmt = '%m-%d %H:%M:%S'
56- )
57-
56+ datefmt = '%m-%d %H:%M:%S' )
5857shh = logging .StreamHandler ()
5958shh .setFormatter (lfmt )
60-
61- logger = logging .getLogger ("findit" )
59+ logger = logging .getLogger ("findit" )
6260logger .addHandler (shh )
6361
62+ Hit = namedtuple ("Hit" , "commit path" )
63+ HASH_LEN = 8
6464
65- Hit = namedtuple ("Hit" ,"commit path" )
66- HASH_LEN = 8
6765
6866def clean_checkout (comm ):
69- h ,s , d = get_commit_vitals (comm )
67+ h , s , d = get_commit_vitals (comm )
7068 if len (s ) > 60 :
7169 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 ))
7472
75- sh .git ('checkout' , comm , _tty_out = False )
73+ sh .git ('checkout' , comm , _tty_out = False )
7674 sh .git ('clean' , '-f' )
7775
78- def get_hits (defname ,files = ()):
79- cs = set ()
76+
77+ def get_hits (defname , files = ()):
78+ cs = set ()
8079 for f in files :
8180 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 )
8386 except sh .ErrorReturnCode_128 :
8487 logger .debug ("no matches in %s" % f )
8588 continue
8689
8790 lines = r .strip ().splitlines ()[:- 1 ]
8891 # 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 ))
9295
9396 return cs
9497
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 )
97106 return text_type (r ).split (sep )
98107
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 )
102108
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 )):
105117 return
106118 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 ) ):
110122 if os .path .isfile (p ):
111123 state ['files' ].append (p )
112124
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 ))
116129 done_commits = set ()
117130 # allhits = set()
118131 files = []
119132 state = dict (files = files )
120- os .path . walk ('.' ,file_filter ,state )
133+ os .walk ('.' , file_filter , state )
121134 # files now holds a list of paths to files
122135
123136 # seed with hits from q
124- allhits = set (get_hits (defname , files = files ))
137+ allhits = set (get_hits (defname , files = files ))
125138 q = set ([HEAD ])
126139 try :
127140 while q :
128- h = q .pop ()
141+ h = q .pop ()
129142 clean_checkout (h )
130- hits = get_hits (defname , files = files )
143+ hits = get_hits (defname , files = files )
131144 for x in hits :
132- prevc = get_commit_vitals (x .commit + "^" )[0 ]
145+ prevc = get_commit_vitals (x .commit + "^" )[0 ]
133146 if prevc not in done_commits :
134147 q .add (prevc )
135148 allhits .update (hits )
@@ -141,43 +154,46 @@ def search(defname,head_commit="HEAD"):
141154 clean_checkout (HEAD )
142155 return allhits
143156
157+
144158def pprint_hits (hits ):
145- SUBJ_LEN = 50
159+ SUBJ_LEN = 50
146160 PATH_LEN = 20
147- hits = list (hits )
161+ hits = list (hits )
148162 max_p = 0
149163 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 ))
152166
153167 if max_p < PATH_LEN :
154168 SUBJ_LEN += PATH_LEN - max_p
155169 PATH_LEN = max_p
156170
157171 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
160174
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 ):
164178 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 ]
167181
168182 fmt = "{:%d} {:10} {:<%d} {:<%d}" % (HASH_LEN , SUBJ_LEN , PATH_LEN )
169183 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 :]))
172186
173187 print ("\n " )
174188
189+
175190def main ():
176191 if not args .saw_the_warning :
177192 argparser .print_help ()
178193 print ("""
179194!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
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.
181197It's recommended that you make a fresh clone and run from its root directory.
182198You must specify the -y argument to ignore this warning.
183199!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
@@ -190,12 +206,11 @@ def main():
190206 if isinstance (args .dir_masks , string_types ):
191207 args .dir_masks = args .dir_masks .split (',' )
192208
193- logger .setLevel (getattr (logging ,args .debug_level ))
209+ logger .setLevel (getattr (logging , args .debug_level ))
194210
195- hits = search (args .funcname )
211+ hits = search (args .funcname )
196212 pprint_hits (hits )
197213
198- pass
199214
200215if __name__ == "__main__" :
201216 import sys
0 commit comments