7 from optparse
import OptionParser
, OptionValueError
10 from git_stats
import parse
12 def _logContains(log
, regexp
):
13 """Traverses the specified log and searches for the specified regexp.
16 log: The log to search through.
17 regexp: The regexp to match for.
20 matcher
= re
.compile(regexp
)
22 isDiff
= log
[0].startswith("diff")
25 # Skip anything that is not an addition or a removal
26 if isDiff
and not line
.startswith("+") and not line
.startswith("-"):
29 # If this line matches the regexp, accept
30 if matcher
.search(line
):
33 # None of the lines matched, reject
36 def prettyPrint(commits
):
37 """Pretty prints the specified commits with git log.
41 for commit
in commits
:
42 # Enable raw output for the trailing newline, which is desired in this case
43 result
= git
.log("-1", "--name-only", commit
, with_raw_output
=True)
46 def _makePathsRelative(paths
):
47 """Helper function that takes a list of paths and makes it relative
50 paths: The paths to make relative.
52 Returns: A new lists in which the paths are relative to the working dir.
59 # Get our 'distance' to the top
60 prefix
= git
.rev_parse("--show-cdup")
66 result
.append(os
.path
.join(prefix
, path
))
70 def pathsTouchedBy(commit
, ignoreAddedFiles
):
71 """Returns a list of paths touched by a specific commit.
74 commit: A commit identifier as accepted by git-log.
75 ignoreAddedFiles: When True newly added files are ignored.
78 A list of paths touched by the specified commit.
83 # Enable raw output, we use the last empty line as delimiter
84 result
= git
.diff_tree("--name-status", "--no-commit-id", "-r", commit
, with_raw_output
=True)
86 log
= result
.split('\n')
91 if len(line
.lstrip()) == 0:
94 splitline
= line
.split('\t')
95 if splitline
[0] == 'A' and ignoreAddedFiles
:
98 paths
.append(splitline
[1])
102 def commitsThatTouched(paths
, relative
=False):
103 """Returns a list of commits that touch the specified paths.
106 paths: A list of changed path relative to the current working dir.
107 relative: Whether the paths are relative to the current directory.
110 A list of 40-character SHA's of the touched commits.
114 raise ValueError("No changed paths specified")
118 result
= git
.rev_list("HEAD", with_keep_cwd
=relative
, *paths
)
120 touched
= result
.split('\n')
124 def commitTouched(commit
):
125 """Shows what commit touched the same files as the specified commit.
128 touched
= pathsTouchedBy(commit
, True)
130 touched
= commitsThatTouched(touched
)
134 def commitmsgMatches(commit
, regexp
):
135 """Returns whether the specified commit matches the specified regexp.
137 The commit message for the specified commit is searched, if a
138 matching line is found, True is returned. Otherwise False is returned.
141 commit: The commit whose commit msg is to be searched.
142 regexp: The regexp to match against.
144 Returns: Whether the commit msg matches the specified regexp.
149 result
= git
.cat_file("-p", commit
)
151 log
= result
.split('\n')
153 # Skip the first lines, the commit msg doesn't begin till after these
154 return _logContains(log
[4:], regexp
)
156 def hasParent(commit
):
157 """Checks if the specified commit has a parent
160 commit: The commitish to check
162 Returns: False if the commitish doesn't have a parent,
163 or if the committish is not a commit at all.
168 hash = git
.rev_parse("--verify", commit
, with_exceptions
=False)
175 parent
= git
.rev_parse("--verify", hash, with_exceptions
=False)
182 def commitdiffMatches(commit
, regexp
):
183 """Returns whether the commit diff matches the specified regexp.
186 commit: The commit whose commit diff is to be searched.
187 regexp: The regexp to match against.
189 Returns: Whether the commit diff matches the specified regexp.
194 if hasParent(commit
):
199 result
= git
.diff_tree("-p", "-U0", lhs
, commit
)
201 log
= result
.split('\n')
203 return _logContains(log
, regexp
)
205 def commitList(logFilter
=None, diffFilter
=None):
211 result
= git
.rev_list("HEAD")
212 commits
= result
.split('\n')
214 if not logFilter
and not diffFilter
:
219 for commit
in commits
:
223 if not commitmsgMatches(commit
, logFilter
):
227 if not commitdiffMatches(commit
, diffFilter
):
231 result
.append(commit
)
237 def _isUnique(options
, atLeastOne
=False):
238 """Checks if a list of options is unique
241 options: The list of options to check
242 atLeastOne: If set, when no optiosn are set, return False
247 for option
in options
:
249 # If we already found one, it's not unique for sure
255 # If there is only one, it's unique
259 # None found, so unique only if we don't require at least one
260 return not atLeastOne
262 def _checkOptions(parser
, options
, args
):
263 """Checks the specified options and uses the parser to indicate errors
266 parser: The parser to use to signal when the options are bad.
267 options: The options to check.
268 args: The arguments to check, if used.
271 opts
= [options
.touched
, options
.log_contains
, options
.diff_contains
, options
.all
]
273 if not _isUnique(opts
, True):
274 parser
.error("Please choose exactly one mode")
276 if not options
.touched
:
278 parser
.error("-r makes no sense without -t")
281 raise ValueError("Please specify only one file")
284 parse
.check_file(value
=args
[0], relative
=options
.relative
)
285 except OptionValueError
, e
:
289 """Dispatches commit related commands
292 progname
= os
.path
.basename(sys
.argv
[0]) + " commit"
294 parser
= OptionParser(prog
=progname
)
300 help="show only commits that modify the specified file")
302 # Toy option, since we have git log for that
306 help="lists all commits on HEAD")
309 "-l", "--log-contains",
310 help="show only commits of which the log contains the specified regexp")
313 "-d", "--diff-contains",
314 help="show only commits of which the diff contains the specified regexp")
319 help="paths are relative to the current directory")
321 parser
.set_default("touched", False)
322 parser
.set_default("relative", False)
324 (options
, args
) = parser
.parse_args(list(args
))
326 _checkOptions(parser
, options
, args
)
329 result
= commitsThatTouched(args
, options
.relative
)
332 if options
.diff_contains
or options
.log_contains
or options
.all
:
333 result
= commitList(diffFilter
=options
.diff_contains
,
334 logFilter
=options
.log_contains
)