7 from optparse
import OptionParser
, OptionValueError
10 from git_stats
import parse
12 GIT_STATS_PRETTY_PRINT
= os
.environ
.get("GIT_STATS_PRETTY_PRINT", "full")
14 def _logContains(log
, regexp
):
15 """Traverses the specified log and searches for the specified regexp.
17 If the first line of the log starts with 'diff' only
18 lines that start with '+' or '-' will be examined.
21 log: The log to search through.
22 regexp: The regexp to match for.
25 matcher
= re
.compile(regexp
)
27 isDiff
= log
[0].startswith("diff")
30 # Skip anything that is not an addition or a removal
31 if isDiff
and not line
.startswith("+") and not line
.startswith("-"):
34 # If this line matches the regexp, accept
35 if matcher
.search(line
):
38 # None of the lines matched, reject
41 def prettyPrint(commits
):
42 """Pretty prints the specified commits with git log
44 Respects the GIT_STATS_PRETTY_PRINT environment variable.
45 Supported are 'full' and 'oneline', defaults to 'full'.
50 if GIT_STATS_PRETTY_PRINT
== "full":
51 opts
= ["-1", "--name-only"]
52 # Enable raw output for the trailing newline, which is desired in this case
53 kwargs
= { "with_raw_output" : True }
54 elif GIT_STATS_PRETTY_PRINT
== "oneline":
55 opts
= ["-1", "--pretty=oneline"]
58 for commit
in commits
:
59 # Copy the opts so that we don't have to kick off the commit later on
61 thisopts
.append(commit
)
62 result
= git
.log(*thisopts
, **kwargs
)
66 def _makePathsRelative(paths
):
67 """Helper function that takes a list of paths and makes it relative
70 paths: The paths to make relative.
72 Returns: A new lists in which the paths are relative to the working dir.
79 # Get our 'distance' to the top
80 prefix
= git
.rev_parse("--show-cdup")
86 result
.append(os
.path
.join(prefix
, path
))
90 def pathsTouchedBy(commit
, ignore_added_files
=True):
91 """Returns a list of paths touched by a specific commit.
94 commit: A commit identifier as accepted by git-log.
95 ignore_added_files: When True newly added files are ignored.
98 A list of paths touched by the specified commit.
103 result
= git
.diff_tree("--name-status", "--no-commit-id", "-r", commit
)
104 log
= result
.split('\n')
110 if len(line
.lstrip()) == 0:
113 # Split the status part and the file name
114 splitline
= line
.split('\t')
115 if splitline
[0] == 'A' and ignore_added_files
:
118 paths
.append(splitline
[1])
122 def commitsThatTouched(paths
, relative
=False, touched_files
={}):
123 """Returns a list of commits that touch the specified paths.
126 paths: A list of changed path relative to the current working dir.
127 relative: Whether the paths are relative to the current directory.
128 touched_files: A dictionary of files and the commits that touched them.
131 A list of 40-character SHA's of the touched commits.
135 raise ValueError("No changed paths specified")
139 if not paths
in touched_files
:
142 result
= git
.rev_list("HEAD", "--", with_keep_cwd
=relative
, *paths
)
143 touched
= result
.split('\n')
144 touched_files
[paths
] = touched
146 touched
= touched_files
[paths
]
150 def commitTouched(commit
):
151 """Shows what commit touched the same files as the specified commit
154 touched
= pathsTouchedBy(commit
, True)
155 touched
= commitsThatTouched(touched
)
159 def commitmsgMatches(commit
, regexp
):
160 """Returns whether the specified commit matches the specified regexp.
162 The commit message for the specified commit is searched,
163 returns true iff amatching line is found.
166 commit: The commit whose commit msg is to be searched.
167 regexp: The regexp to match against.
169 Returns: Whether the commit msg matches the specified regexp.
174 result
= git
.cat_file("-p", commit
)
175 log
= result
.split('\n')
177 # The commit msg begin after the first empty line
182 return _logContains(log
, regexp
)
184 def getDiff(commit
, ignore_whitespace
=True, noContext
=False):
185 """Returns the commit diff for the specified commit
188 commit: The commit to get the diff for.
190 Returns: The commit diff.
197 if ignore_whitespace
:
203 args
.append(commit
+ "^")
207 # Don't look before we leap, just try with 'hash^'
208 result
= git
.diff_tree(with_exceptions
=False, *args
)
210 # We missed, retry with '--root' instead
213 result
= git
.diff_tree(*args
)
218 def commitdiffMatches(commit
, regexp
, raw_diffs
={}):
219 """Returns whether the commit diff matches the specified regexp.
222 commit: The commit whose commit diff is to be searched.
223 regexp: The regexp to match against.
224 raw_diffs: A dictionary with commits and their diffs.
226 Returns: Whether the commit diff matches the specified regexp.
229 if not commit
in raw_diffs
:
230 result
= getDiff(commit
, noContext
=True)
231 diff
= result
.split('\n')
232 raw_diffs
[commit
] = diff
234 diff
= raw_diffs
[commit
]
236 return _logContains(diff
, regexp
)
238 def commitList(log_filter
=None, diff_filter
=None):
239 """Returns a list of all commits that match the specified filters
242 log_filter: If specified, only commits whose log match are included.
243 diff_filter: If specified, only commits whose diff match are included.
248 result
= git
.rev_list("HEAD")
249 commits
= result
.split('\n')
251 if not log_filter
and not diff_filter
:
256 for commit
in commits
:
260 if not commitmsgMatches(commit
, log_filter
):
264 if not commitdiffMatches(commit
, diff_filter
):
268 result
.append(commit
)
272 def _checkOptions(parser
, options
):
273 """Checks the specified options and uses the parser to indicate errors
276 parser: The parser to use to signal when the options are bad.
277 options: The options to check.
280 opts
= [options
.touched
, options
.log_contains
,
281 options
.diff_contains
, options
.all
]
283 if not parse
.isUnique(opts
, at_least_one
=True):
284 parser
.error("Please choose exactly one mode")
286 if not options
.touched
:
288 parser
.error("-r makes no sense without -t")
290 # Check if the specified file is valid
292 parse
.checkFile(value
=options
.touched
, relative
=options
.relative
)
293 except OptionValueError
, e
:
297 """Dispatches commit related commands
300 # Make the help show 'progname commit' instead of just 'progname'
301 progname
= os
.path
.basename(sys
.argv
[0]) + " commit"
303 parser
= OptionParser(option_class
=parse
.GitOption
, prog
=progname
)
308 help="show only commits that modify the specified file")
310 # Toy option, since we have git log for that
314 help="lists all commits on HEAD")
317 "-l", "--log-contains",
318 help="show only commits of which the log contains the specified regexp")
321 "-d", "--diff-contains",
322 help="show only commits of which the diff contains the specified regexp")
327 help="paths are relative to the current directory")
329 parser
.set_default("relative", False)
331 (options
, args
) = parser
.parse_args(list(args
))
333 _checkOptions(parser
, options
)
336 result
= commitsThatTouched([options
.touched
], options
.relative
)
338 if options
.diff_contains
or options
.log_contains
or options
.all
:
339 result
= commitList(diff_filter
=options
.diff_contains
,
340 log_filter
=options
.log_contains
)