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.
18 log: The log to search through.
19 regexp: The regexp to match for.
22 matcher
= re
.compile(regexp
)
24 isDiff
= log
[0].startswith("diff")
27 # Skip anything that is not an addition or a removal
28 if isDiff
and not line
.startswith("+") and not line
.startswith("-"):
31 # If this line matches the regexp, accept
32 if matcher
.search(line
):
35 # None of the lines matched, reject
38 def prettyPrint(commits
):
39 """Pretty prints the specified commits with git log.
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"]
51 for commit
in commits
:
52 # Enable raw output for the trailing newline, which is desired in this case
54 thisopts
.append(commit
)
55 result
= git
.log(*thisopts
, **kwargs
)
59 def _makePathsRelative(paths
):
60 """Helper function that takes a list of paths and makes it relative
63 paths: The paths to make relative.
65 Returns: A new lists in which the paths are relative to the working dir.
72 # Get our 'distance' to the top
73 prefix
= git
.rev_parse("--show-cdup")
79 result
.append(os
.path
.join(prefix
, path
))
83 def pathsTouchedBy(commit
, ignoreAddedFiles
=True):
84 """Returns a list of paths touched by a specific commit.
87 commit: A commit identifier as accepted by git-log.
88 ignoreAddedFiles: When True newly added files are ignored.
91 A list of paths touched by the specified commit.
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')
104 if len(line
.lstrip()) == 0:
107 splitline
= line
.split('\t')
108 if splitline
[0] == 'A' and ignoreAddedFiles
:
111 paths
.append(splitline
[1])
115 def commitsThatTouched(paths
, relative
=False, touched_files
={}):
116 """Returns a list of commits that touch the specified paths.
119 paths: A list of changed path relative to the current working dir.
120 relative: Whether the paths are relative to the current directory.
121 touched_files: A dictionary of files and the commits that touched them.
124 A list of 40-character SHA's of the touched commits.
128 raise ValueError("No changed paths specified")
132 if not touched_files
.has_key(paths
):
135 result
= git
.rev_list("HEAD", "--", with_keep_cwd
=relative
, *paths
)
136 touched
= result
.split('\n')
137 touched_files
[paths
] = touched
139 touched
= touched_files
[paths
]
143 def commitTouched(commit
):
144 """Shows what commit touched the same files as the specified commit.
147 touched
= pathsTouchedBy(commit
, True)
149 touched
= commitsThatTouched(touched
)
153 def commitmsgMatches(commit
, regexp
):
154 """Returns whether the specified commit matches the specified regexp.
156 The commit message for the specified commit is searched, if a
157 matching line is found, True is returned. Otherwise False is returned.
160 commit: The commit whose commit msg is to be searched.
161 regexp: The regexp to match against.
163 Returns: Whether the commit msg matches the specified regexp.
168 result
= git
.cat_file("-p", commit
)
170 log
= result
.split('\n')
172 # Skip the first lines, the commit msg doesn't begin till after these
173 return _logContains(log
[4:], regexp
)
175 def hasParent(commit
):
176 """Checks if the specified commit has a parent
179 commit: The commitish to check
181 Returns: False if the commitish doesn't have a parent,
182 or if the committish is not a commit at all.
187 hash = git
.rev_parse("--verify", commit
, with_exceptions
=False)
194 parent
= git
.rev_parse("--verify", hash, with_exceptions
=False)
201 def getDiff(commit
, ignoreWhitespace
=True, noContext
=False):
202 """Returns the commit diff for the specified commit
205 commit: The commit to get the diff for.
207 Returns: The commit diff.
220 args
.append(commit
+ "^")
224 result
= git
.diff_tree(with_exceptions
=False, *args
)
228 result
= git
.diff_tree(*args
)
233 def commitdiffMatches(commit
, regexp
, raw_diffs
={}):
234 """Returns whether the commit diff matches the specified regexp.
237 commit: The commit whose commit diff is to be searched.
238 regexp: The regexp to match against.
239 raw_diffs: A dictionary with commits and their diffs.
241 Returns: Whether the commit diff matches the specified regexp.
244 if not raw_diffs
.has_key(commit
):
245 result
= getDiff(commit
, noContext
=True)
246 diff
= result
.split('\n')
247 raw_diffs
[commit
] = diff
249 diff
= raw_diffs
[commit
]
251 return _logContains(diff
, regexp
)
253 def commitList(logFilter
=None, diffFilter
=None):
254 """Returns a list of all commits that match the specified filters
257 logFilter: If specified, only commits whose log match are included.
258 diffFilter: If specified, only commits whose diff match are included.
263 result
= git
.rev_list("HEAD")
264 commits
= result
.split('\n')
266 if not logFilter
and not diffFilter
:
271 for commit
in commits
:
275 if not commitmsgMatches(commit
, logFilter
):
279 if not commitdiffMatches(commit
, diffFilter
):
283 result
.append(commit
)
288 def _checkOptions(parser
, options
):
289 """Checks the specified options and uses the parser to indicate errors
292 parser: The parser to use to signal when the options are bad.
293 options: The options to check.
294 args: The arguments to check, if used.
297 opts
= [options
.touched
, options
.log_contains
,
298 options
.diff_contains
, options
.all
]
300 if not parse
.isUnique(opts
, True):
301 parser
.error("Please choose exactly one mode")
303 if not options
.touched
:
305 parser
.error("-r makes no sense without -t")
308 parse
.check_file(value
=options
.touched
, relative
=options
.relative
)
309 except OptionValueError
, e
:
313 """Dispatches commit related commands
316 progname
= os
.path
.basename(sys
.argv
[0]) + " commit"
318 parser
= OptionParser(option_class
=parse
.GitOption
, prog
=progname
)
323 help="show only commits that modify the specified file")
325 # Toy option, since we have git log for that
329 help="lists all commits on HEAD")
332 "-l", "--log-contains",
333 help="show only commits of which the log contains the specified regexp")
336 "-d", "--diff-contains",
337 help="show only commits of which the diff contains the specified regexp")
342 help="paths are relative to the current directory")
344 parser
.set_default("relative", False)
346 (options
, args
) = parser
.parse_args(list(args
))
348 _checkOptions(parser
, options
)
351 result
= commitsThatTouched([options
.touched
], options
.relative
)
353 if options
.diff_contains
or options
.log_contains
or options
.all
:
354 result
= commitList(diffFilter
=options
.diff_contains
,
355 logFilter
=options
.log_contains
)