When specifying kwargs they should be in quotes when not part of a function call.
[git-stats.git] / src / git_stats / commit.py
blobcaa6763595ea2240f683912b0c47a3cc2ad37d7e
1 #!/usr/bin/env python
3 import os
4 import sys
5 import re
7 from optparse import OptionParser, OptionValueError
8 from git import Git
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 """
41 git = Git(".")
43 if GIT_STATS_PRETTY_PRINT == "full":
44 opts = ["-1", "--name-only"]
45 kwargs = { "with_raw_output" : True }
46 elif GIT_STATS_PRETTY_PRINT == "oneline":
47 opts = ["-1", "--pretty=oneline"]
48 kwargs = {}
50 for commit in commits:
51 # Enable raw output for the trailing newline, which is desired in this case
52 thisopts = opts[:]
53 thisopts.append(commit)
54 result = git.log(*thisopts, **kwargs)
56 print(result)
58 def _makePathsRelative(paths):
59 """Helper function that takes a list of paths and makes it relative
61 Args:
62 paths: The paths to make relative.
64 Returns: A new lists in which the paths are relative to the working dir.
65 """
67 result = []
69 git = Git(".")
71 # Get our 'distance' to the top
72 prefix = git.rev_parse("--show-cdup")
74 prefix = prefix
76 # Make it relative
77 for path in paths:
78 result.append(os.path.join(prefix, path))
80 return result
82 def pathsTouchedBy(commit, ignoreAddedFiles):
83 """Returns a list of paths touched by a specific commit.
85 Params:
86 commit: A commit identifier as accepted by git-log.
87 ignoreAddedFiles: When True newly added files are ignored.
89 Returns:
90 A list of paths touched by the specified commit.
91 """
93 git = Git(".")
95 # Enable raw output, we use the last empty line as delimiter
96 result = git.diff_tree("--name-status", "--no-commit-id", "-r", commit, with_raw_output=True)
98 log = result.split('\n')
100 paths = []
102 for line in log:
103 if len(line.lstrip()) == 0:
104 continue
106 splitline = line.split('\t')
107 if splitline[0] == 'A' and ignoreAddedFiles:
108 continue
110 paths.append(splitline[1])
112 return paths
114 def commitsThatTouched(paths, relative=False):
115 """Returns a list of commits that touch the specified paths.
117 Params:
118 paths: A list of changed path relative to the current working dir.
119 relative: Whether the paths are relative to the current directory.
121 Returns:
122 A list of 40-character SHA's of the touched commits.
125 if not paths:
126 raise ValueError("No changed paths specified")
128 git = Git(".")
130 result = git.rev_list("HEAD", with_keep_cwd=relative, *paths)
132 touched = result.split('\n')
134 return touched
136 def commitTouched(commit):
137 """Shows what commit touched the same files as the specified commit.
140 touched = pathsTouchedBy(commit, True)
142 touched = commitsThatTouched(touched)
144 prettyPrint(touched)
146 def commitmsgMatches(commit, regexp):
147 """Returns whether the specified commit matches the specified regexp.
149 The commit message for the specified commit is searched, if a
150 matching line is found, True is returned. Otherwise False is returned.
152 Params:
153 commit: The commit whose commit msg is to be searched.
154 regexp: The regexp to match against.
156 Returns: Whether the commit msg matches the specified regexp.
159 git = Git(".")
161 result = git.cat_file("-p", commit)
163 log = result.split('\n')
165 # Skip the first lines, the commit msg doesn't begin till after these
166 return _logContains(log[4:], regexp)
168 def hasParent(commit):
169 """Checks if the specified commit has a parent
171 Args:
172 commit: The commitish to check
174 Returns: False if the commitish doesn't have a parent,
175 or if the committish is not a commit at all.
178 git = Git(".")
180 hash = git.rev_parse("--verify", commit, with_exceptions=False)
182 if not hash:
183 return False
185 hash = hash + "^"
187 parent = git.rev_parse("--verify", hash, with_exceptions=False)
189 if not parent:
190 return False
192 return True
194 def commitdiffMatches(commit, regexp):
195 """Returns whether the commit diff matches the specified regexp.
197 Params:
198 commit: The commit whose commit diff is to be searched.
199 regexp: The regexp to match against.
201 Returns: Whether the commit diff matches the specified regexp.
204 git = Git(".")
206 if hasParent(commit):
207 lhs = commit + "^"
208 else:
209 lhs = "--root"
211 result = git.diff_tree("-p", "-U0", lhs, commit)
213 log = result.split('\n')
215 return _logContains(log, regexp)
217 def commitList(logFilter=None, diffFilter=None):
221 git = Git(".")
223 result = git.rev_list("HEAD")
224 commits = result.split('\n')
226 if not logFilter and not diffFilter:
227 return commits
229 result = []
231 for commit in commits:
232 addIt = True
234 if logFilter:
235 if not commitmsgMatches(commit, logFilter):
236 addIt = False
238 if diffFilter:
239 if not commitdiffMatches(commit, diffFilter):
240 addIt = False
242 if addIt:
243 result.append(commit)
246 return result
249 def _isUnique(options, atLeastOne=False):
250 """Checks if a list of options is unique
252 Args:
253 options: The list of options to check
254 atLeastOne: If set, when no optiosn are set, return False
257 unique = False
259 for option in options:
260 if option:
261 # If we already found one, it's not unique for sure
262 if unique:
263 return False
264 else:
265 unique = True
267 # If there is only one, it's unique
268 if unique:
269 return True
271 # None found, so unique only if we don't require at least one
272 return not atLeastOne
274 def _checkOptions(parser, options, args):
275 """Checks the specified options and uses the parser to indicate errors
277 Args:
278 parser: The parser to use to signal when the options are bad.
279 options: The options to check.
280 args: The arguments to check, if used.
283 opts = [options.touched, options.log_contains, options.diff_contains, options.all]
285 if not _isUnique(opts, True):
286 parser.error("Please choose exactly one mode")
288 if not options.touched:
289 if options.relative:
290 parser.error("-r makes no sense without -t")
291 else:
292 if len(args) > 1:
293 raise ValueError("Please specify only one file")
295 try:
296 parse.check_file(value=args[0], 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(prog=progname)
308 parser.add_option(
309 "-t", "--touches",
310 dest="touched",
311 action="store_true",
312 help="show only commits that modify the specified file")
314 # Toy option, since we have git log for that
315 parser.add_option(
316 "-a", "--all",
317 action="store_true",
318 help="lists all commits on HEAD")
320 parser.add_option(
321 "-l", "--log-contains",
322 help="show only commits of which the log contains the specified regexp")
324 parser.add_option(
325 "-d", "--diff-contains",
326 help="show only commits of which the diff contains the specified regexp")
328 parser.add_option(
329 "-r", "--relative",
330 action="store_true",
331 help="paths are relative to the current directory")
333 parser.set_default("touched", False)
334 parser.set_default("relative", False)
336 (options, args) = parser.parse_args(list(args))
338 _checkOptions(parser, options, args)
340 if options.touched:
341 result = commitsThatTouched(args, options.relative)
342 prettyPrint(result)
344 if options.diff_contains or options.log_contains or options.all:
345 result = commitList(diffFilter=options.diff_contains,
346 logFilter=options.log_contains)
347 prettyPrint(result)