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.
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"]
50 for commit
in commits
:
51 # Enable raw output for the trailing newline, which is desired in this case
53 thisopts
.append(commit
)
54 result
= git
.log(*thisopts
, **kwargs
)
58 def _makePathsRelative(paths
):
59 """Helper function that takes a list of paths and makes it relative
62 paths: The paths to make relative.
64 Returns: A new lists in which the paths are relative to the working dir.
71 # Get our 'distance' to the top
72 prefix
= git
.rev_parse("--show-cdup")
78 result
.append(os
.path
.join(prefix
, path
))
82 def pathsTouchedBy(commit
, ignoreAddedFiles
):
83 """Returns a list of paths touched by a specific commit.
86 commit: A commit identifier as accepted by git-log.
87 ignoreAddedFiles: When True newly added files are ignored.
90 A list of paths touched by the specified commit.
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')
103 if len(line
.lstrip()) == 0:
106 splitline
= line
.split('\t')
107 if splitline
[0] == 'A' and ignoreAddedFiles
:
110 paths
.append(splitline
[1])
114 def commitsThatTouched(paths
, relative
=False):
115 """Returns a list of commits that touch the specified paths.
118 paths: A list of changed path relative to the current working dir.
119 relative: Whether the paths are relative to the current directory.
122 A list of 40-character SHA's of the touched commits.
126 raise ValueError("No changed paths specified")
130 result
= git
.rev_list("HEAD", with_keep_cwd
=relative
, *paths
)
132 touched
= result
.split('\n')
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
)
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.
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.
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
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.
180 hash = git
.rev_parse("--verify", commit
, with_exceptions
=False)
187 parent
= git
.rev_parse("--verify", hash, with_exceptions
=False)
194 def commitdiffMatches(commit
, regexp
):
195 """Returns whether the commit diff matches the specified regexp.
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.
206 if hasParent(commit
):
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):
223 result
= git
.rev_list("HEAD")
224 commits
= result
.split('\n')
226 if not logFilter
and not diffFilter
:
231 for commit
in commits
:
235 if not commitmsgMatches(commit
, logFilter
):
239 if not commitdiffMatches(commit
, diffFilter
):
243 result
.append(commit
)
249 def _isUnique(options
, atLeastOne
=False):
250 """Checks if a list of options is unique
253 options: The list of options to check
254 atLeastOne: If set, when no optiosn are set, return False
259 for option
in options
:
261 # If we already found one, it's not unique for sure
267 # If there is only one, it's unique
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
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
:
290 parser
.error("-r makes no sense without -t")
293 raise ValueError("Please specify only one file")
296 parse
.check_file(value
=args
[0], relative
=options
.relative
)
297 except OptionValueError
, e
:
301 """Dispatches commit related commands
304 progname
= os
.path
.basename(sys
.argv
[0]) + " commit"
306 parser
= OptionParser(prog
=progname
)
312 help="show only commits that modify the specified file")
314 # Toy option, since we have git log for that
318 help="lists all commits on HEAD")
321 "-l", "--log-contains",
322 help="show only commits of which the log contains the specified regexp")
325 "-d", "--diff-contains",
326 help="show only commits of which the diff contains the specified regexp")
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
)
341 result
= commitsThatTouched(args
, options
.relative
)
344 if options
.diff_contains
or options
.log_contains
or options
.all
:
345 result
= commitList(diffFilter
=options
.diff_contains
,
346 logFilter
=options
.log_contains
)