gitstats: Check for empty diffs
[git-stats.git] / src / git_stats / commit.py
blobd8a301b3779a7e5e2cb3737892dc7dcd6ce68c89
1 #!/usr/bin/env python
3 import os
4 import sys
5 import re
7 from optparse import OptionParser, OptionValueError
8 from git import Repo
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 Params:
18 log: The log to search through.
19 regexp: The regexp to match for.
20 """
22 matcher = re.compile(regexp)
24 isDiff = log[0].startswith("diff")
26 for line in log:
27 # Skip anything that is not an addition or a removal
28 if isDiff and not line.startswith("+") and not line.startswith("-"):
29 continue
31 # If this line matches the regexp, accept
32 if matcher.search(line):
33 return True
35 # None of the lines matched, reject
36 return False
38 def prettyPrint(commits):
39 """Pretty prints the specified commits with git log.
40 """
42 git = Repo(".").git
44 if GIT_STATS_PRETTY_PRINT == "full":
45 opts = ["-1", "--name-only"]
46 kwargs = { "with_raw_output" : True }
47 elif GIT_STATS_PRETTY_PRINT == "oneline":
48 opts = ["-1", "--pretty=oneline"]
49 kwargs = {}
51 for commit in commits:
52 # Enable raw output for the trailing newline, which is desired in this case
53 thisopts = opts[:]
54 thisopts.append(commit)
55 result = git.log(*thisopts, **kwargs)
57 print(result)
59 def _makePathsRelative(paths):
60 """Helper function that takes a list of paths and makes it relative
62 Args:
63 paths: The paths to make relative.
65 Returns: A new lists in which the paths are relative to the working dir.
66 """
68 result = []
70 git = Repo(".").git
72 # Get our 'distance' to the top
73 prefix = git.rev_parse("--show-cdup")
75 prefix = prefix
77 # Make it relative
78 for path in paths:
79 result.append(os.path.join(prefix, path))
81 return result
83 def pathsTouchedBy(commit, ignoreAddedFiles=True):
84 """Returns a list of paths touched by a specific commit.
86 Params:
87 commit: A commit identifier as accepted by git-log.
88 ignoreAddedFiles: When True newly added files are ignored.
90 Returns:
91 A list of paths touched by the specified commit.
92 """
94 git = Repo(".").git
96 # Enable raw output, we use the last empty line as delimiter
97 result = git.diff_tree("--name-status", "--no-commit-id", "-r", commit, with_raw_output=True)
99 log = result.split('\n')
101 paths = []
103 for line in log:
104 if len(line.lstrip()) == 0:
105 continue
107 splitline = line.split('\t')
108 if splitline[0] == 'A' and ignoreAddedFiles:
109 continue
111 paths.append(splitline[1])
113 return paths
115 def commitsThatTouched(paths, relative=False):
116 """Returns a list of commits that touch the specified paths.
118 Params:
119 paths: A list of changed path relative to the current working dir.
120 relative: Whether the paths are relative to the current directory.
122 Returns:
123 A list of 40-character SHA's of the touched commits.
126 if not paths:
127 raise ValueError("No changed paths specified")
129 git = Repo(".").git
131 result = git.rev_list("HEAD", "--", with_keep_cwd=relative, *paths)
133 touched = result.split('\n')
135 return touched
137 def commitTouched(commit):
138 """Shows what commit touched the same files as the specified commit.
141 touched = pathsTouchedBy(commit, True)
143 touched = commitsThatTouched(touched)
145 prettyPrint(touched)
147 def commitmsgMatches(commit, regexp):
148 """Returns whether the specified commit matches the specified regexp.
150 The commit message for the specified commit is searched, if a
151 matching line is found, True is returned. Otherwise False is returned.
153 Params:
154 commit: The commit whose commit msg is to be searched.
155 regexp: The regexp to match against.
157 Returns: Whether the commit msg matches the specified regexp.
160 git = Repo(".").git
162 result = git.cat_file("-p", commit)
164 log = result.split('\n')
166 # Skip the first lines, the commit msg doesn't begin till after these
167 return _logContains(log[4:], regexp)
169 def hasParent(commit):
170 """Checks if the specified commit has a parent
172 Args:
173 commit: The commitish to check
175 Returns: False if the commitish doesn't have a parent,
176 or if the committish is not a commit at all.
179 git = Repo(".").git
181 hash = git.rev_parse("--verify", commit, with_exceptions=False)
183 if not hash:
184 return False
186 hash = hash + "^"
188 parent = git.rev_parse("--verify", hash, with_exceptions=False)
190 if not parent:
191 return False
193 return True
195 def getDiff(commit, ignoreWhitespace=True, noContext=False):
196 """Returns the commit diff for the specified commit
198 Params:
199 commit: The commit to get the diff for.
201 Returns: The commit diff.
204 git = Repo(".").git
206 args = ["-p"]
208 if ignoreWhitespace:
209 args.append("-w")
211 if noContext:
212 args.append("-U0")
214 if hasParent(commit):
215 args.append(commit + "^")
216 else:
217 args.append("--root")
219 args.append(commit)
221 result = git.diff_tree(*args)
223 return result
226 def commitdiffMatches(commit, regexp):
227 """Returns whether the commit diff matches the specified regexp.
229 Params:
230 commit: The commit whose commit diff is to be searched.
231 regexp: The regexp to match against.
233 Returns: Whether the commit diff matches the specified regexp.
236 result = getDiff(commit, noContext=True)
237 log = result.split('\n')
239 return _logContains(log, regexp)
241 def commitList(logFilter=None, diffFilter=None):
242 """Returns a list of all commits that match the specified filters
244 Args:
245 logFilter: If specified, only commits whose log match are included.
246 diffFilter: If specified, only commits whose diff match are included.
249 git = Repo(".").git
251 result = git.rev_list("HEAD")
252 commits = result.split('\n')
254 if not logFilter and not diffFilter:
255 return commits
257 result = []
259 for commit in commits:
260 addIt = True
262 if logFilter:
263 if not commitmsgMatches(commit, logFilter):
264 addIt = False
266 if diffFilter:
267 if not commitdiffMatches(commit, diffFilter):
268 addIt = False
270 if addIt:
271 result.append(commit)
274 return result
276 def _checkOptions(parser, options):
277 """Checks the specified options and uses the parser to indicate errors
279 Args:
280 parser: The parser to use to signal when the options are bad.
281 options: The options to check.
282 args: The arguments to check, if used.
285 opts = [options.touched, options.log_contains,
286 options.diff_contains, options.all]
288 if not parse.isUnique(opts, True):
289 parser.error("Please choose exactly one mode")
291 if not options.touched:
292 if options.relative:
293 parser.error("-r makes no sense without -t")
294 else:
295 try:
296 parse.check_file(value=options.touched, relative=options.relative)
297 except OptionValueError, e:
298 parser.error(e)
300 def dispatch(*args):
301 """Dispatches commit related commands
304 progname = os.path.basename(sys.argv[0]) + " commit"
306 parser = OptionParser(option_class=parse.GitOption, prog=progname)
308 parser.add_option(
309 "-t", "--touches",
310 dest="touched",
311 help="show only commits that modify the specified file")
313 # Toy option, since we have git log for that
314 parser.add_option(
315 "-a", "--all",
316 action="store_true",
317 help="lists all commits on HEAD")
319 parser.add_option(
320 "-l", "--log-contains",
321 help="show only commits of which the log contains the specified regexp")
323 parser.add_option(
324 "-d", "--diff-contains",
325 help="show only commits of which the diff contains the specified regexp")
327 parser.add_option(
328 "-r", "--relative",
329 action="store_true",
330 help="paths are relative to the current directory")
332 parser.set_default("relative", False)
334 (options, args) = parser.parse_args(list(args))
336 _checkOptions(parser, options)
338 if options.touched:
339 result = commitsThatTouched([options.touched], options.relative)
341 if options.diff_contains or options.log_contains or options.all:
342 result = commitList(diffFilter=options.diff_contains,
343 logFilter=options.log_contains)
345 prettyPrint(result)