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):
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.
123 A list of 40-character SHA's of the touched commits.
127 raise ValueError("No changed paths specified")
131 result
= git
.rev_list("HEAD", "--", with_keep_cwd
=relative
, *paths
)
133 touched
= result
.split('\n')
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
)
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.
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.
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
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.
181 hash = git
.rev_parse("--verify", commit
, with_exceptions
=False)
188 parent
= git
.rev_parse("--verify", hash, with_exceptions
=False)
195 def getDiff(commit
, ignoreWhitespace
=True, noContext
=False):
196 """Returns the commit diff for the specified commit
199 commit: The commit to get the diff for.
201 Returns: The commit diff.
214 if hasParent(commit
):
215 args
.append(commit
+ "^")
217 args
.append("--root")
221 result
= git
.diff_tree(*args
)
226 def commitdiffMatches(commit
, regexp
):
227 """Returns whether the commit diff matches the specified regexp.
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
245 logFilter: If specified, only commits whose log match are included.
246 diffFilter: If specified, only commits whose diff match are included.
251 result
= git
.rev_list("HEAD")
252 commits
= result
.split('\n')
254 if not logFilter
and not diffFilter
:
259 for commit
in commits
:
263 if not commitmsgMatches(commit
, logFilter
):
267 if not commitdiffMatches(commit
, diffFilter
):
271 result
.append(commit
)
276 def _checkOptions(parser
, options
):
277 """Checks the specified options and uses the parser to indicate errors
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
:
293 parser
.error("-r makes no sense without -t")
296 parse
.check_file(value
=options
.touched
, relative
=options
.relative
)
297 except OptionValueError
, e
:
301 """Dispatches commit related commands
304 progname
= os
.path
.basename(sys
.argv
[0]) + " commit"
306 parser
= OptionParser(option_class
=parse
.GitOption
, prog
=progname
)
311 help="show only commits that modify the specified file")
313 # Toy option, since we have git log for that
317 help="lists all commits on HEAD")
320 "-l", "--log-contains",
321 help="show only commits of which the log contains the specified regexp")
324 "-d", "--diff-contains",
325 help="show only commits of which the diff contains the specified regexp")
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
)
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
)