8 from optparse
import OptionParser
, Option
, OptionValueError
11 def _logContains(log
, regexp
):
12 """Traverses the specified log and searches for the specified regexp.
15 log: The log to search through.
16 regexp: The regexp to match for.
19 matcher
= re
.compile(regexp
)
21 isDiff
= log
[0].startswith("diff")
24 # Skip anything that is not an addition or a removal
25 if isDiff
and not line
.startswith("+") and not line
.startswith("-"):
28 # If this line matches the regexp, accept
29 if matcher
.search(line
):
32 # None of the lines matched, reject
35 def check_commit(option
, opt
, value
):
36 """Checks whether value is a valid rev and returns its hash
40 rev
= git
.rev_parse("--verify", value
)
43 raise OptionValueError("Unknown commit '" + value
+ "'")
47 class CommitOption(Option
):
48 """This parser understands a new type, "commit"
51 TYPES
= Option
.TYPES
+ ("commit",)
52 TYPE_CHECKER
= copy
.copy(Option
.TYPE_CHECKER
)
53 TYPE_CHECKER
["commit"] = check_commit
55 def prettyPrint(commits
):
56 """Pretty prints the specified commits with git log.
60 for commit
in commits
:
61 # Enable raw output for the trailing newline, which is desired in this case
62 result
= git
.log("-1", "--name-only", commit
, with_raw_output
=True)
65 def _makePathsRelative(paths
):
66 """Helper function that takes a list of paths and makes it relative
69 paths: The paths to make relative.
71 Returns: A new lists in which the paths are relative to the working dir.
78 # Get our 'distance' to the top
79 prefix
= git
.rev_parse("--show-cdup")
85 result
.append(os
.path
.join(prefix
, path
))
89 def pathsTouchedBy(commit
, ignoreAddedFiles
):
90 """Returns a list of paths touched by a specific commit.
93 commit: A commit identifier as accepted by git-log.
94 ignoreAddedFiles: When True newly added files are ignored.
97 A list of paths touched by the specified commit.
102 # Enable raw output, we use the last empty line as delimiter
103 result
= git
.diff_tree("--name-status", "--no-commit-id", "-r", commit
, with_raw_output
=True)
105 log
= result
.split('\n')
110 if len(line
.lstrip()) == 0:
113 splitline
= line
.split('\t')
114 if splitline
[0] == 'A' and ignoreAddedFiles
:
117 paths
.append(splitline
[1])
121 def commitsThatTouched(paths
, relative
=False):
122 """Returns a list of commits that touch the specified paths.
125 paths: A list of changed path relative to the current working dir.
126 relative: Whether the paths are relative to the current directory.
129 A list of 40-character SHA's of the touched commits.
133 raise ValueError("No changed paths specified")
136 relativePaths
= _makePathsRelative(paths
)
138 relativePaths
= paths
142 result
= git
.rev_list("HEAD", *relativePaths
)
144 touched
= result
.split('\n')
148 def commitTouched(commit
):
149 """Shows what commit touched the same files as the specified commit.
152 touched
= pathsTouchedBy(commit
, True)
154 touched
= commitsThatTouched(touched
)
158 def commitmsgMatches(commit
, regexp
):
159 """Returns whether the specified commit matches the specified regexp.
161 The commit message for the specified commit is searched, if a
162 matching line is found, True is returned. Otherwise False is returned.
165 commit: The commit whose commit msg is to be searched.
166 regexp: The regexp to match against.
168 Returns: Whether the commit msg matches the specified regexp.
173 result
= git
.cat_file("-p", commit
)
175 log
= result
.split('\n')
177 # Skip the first lines, the commit msg doesn't begin till after these
178 return _logContains(log
[4:], regexp
)
180 def hasParent(commit
):
181 """Checks if the specified commit has a parent
184 commit: The commitish to check
186 Returns: False if the commitish doesn't have a parent,
187 or if the committish is not a commit at all.
192 hash = git
.rev_parse("--verify", commit
)
199 parent
= git
.rev_parse("--verify", hash)
206 def commitdiffMatches(commit
, regexp
):
207 """Returns whether the commit diff matches the specified regexp.
210 commit: The commit whose commit diff is to be searched.
211 regexp: The regexp to match against.
213 Returns: Whether the commit diff matches the specified regexp.
218 if hasParent(commit
):
223 result
= git
.diff_tree("-p", "-U0", lhs
, commit
)
225 log
= result
.split('\n')
227 return _logContains(log
, regexp
)
229 def commitList(logFilter
=None, diffFilter
=None):
235 result
= git
.rev_list("HEAD")
236 commits
= result
.split('\n')
238 if not logFilter
and not diffFilter
:
243 for commit
in commits
:
247 if not commitmsgMatches(commit
, logFilter
):
251 if not commitdiffMatches(commit
, diffFilter
):
255 result
.append(commit
)
261 def _isUnique(options
, atLeastOne
=False):
262 """Checks if a list of options is unique
265 options: The list of options to check
266 atLeastOne: If set, when no optiosn are set, return False
271 for option
in options
:
273 # If we already found one, it's not unique for sure
279 # If there is only one, it's unique
283 # None found, so unique only if we don't require at least one
284 return not atLeastOne
286 def _checkOptions(parser
, options
):
287 """Checks the specified options and uses the parser to indicate errors
290 parser: The parser to use to signal when the options are bad.
291 options: The options to check.
294 opts
= [options
.touched
, options
.log_contains
, options
.diff_contains
, options
.all
]
296 if not _isUnique(opts
, True):
297 parser
.error("Please choose exactly one mode")
299 if options
.relative
and not options
.touched
:
300 parser
.error("-r makes no sense without -t")
303 """Dispatches commit related commands
306 progname
= os
.path
.basename(sys
.argv
[0]) + " commit"
308 parser
= OptionParser(prog
=progname
)
314 help="show only commits that modify the specified files")
316 # Toy option, since we have git log for that
320 help="lists all commits on HEAD")
323 "-l", "--log-contains",
324 help="show only commits of which the log contains the specified regexp")
327 "-d", "--diff-contains",
328 help="show only commits of which the diff contains the specified regexp")
333 help="paths are relative to the current directory")
335 parser
.set_default("touched", False)
336 parser
.set_default("relative", False)
338 (options
, args
) = parser
.parse_args(list(args
))
340 _checkOptions(parser
, options
)
343 result
= commitsThatTouched(args
, options
.relative
)
346 if options
.diff_contains
or options
.log_contains
or options
.all
:
347 result
= commitList(diffFilter
=options
.diff_contains
,
348 logFilter
=options
.log_contains
)