| /* |
| Copyright 2013 Google Inc. All Rights Reserved. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| // Package lint provides helper methods for quickly running lint checks |
| // against a proposed change on Gerrit. |
| package lint |
| |
| import ( |
| "errors" |
| "fmt" |
| "log" |
| "strings" |
| |
| "gwt.googlesource.com/buildglue.git/checkstyle" |
| "gwt.googlesource.com/buildglue.git/gerrit" |
| "gwt.googlesource.com/buildglue.git/git" |
| ) |
| |
| // Errors that can be returned by Lint. |
| var ( |
| LintFailed = errors.New("commit failed lint checking") |
| ) |
| |
| // Should be kept in sync with GWT's build.xml files. |
| var checks = []struct { |
| config string |
| includes []string |
| excludes []string |
| }{ |
| { |
| config: "eclipse/settings/code-style/gwt-checkstyle.xml", |
| includes: []string{ |
| "dev/core/src/", |
| "dev/core/super/com/google/gwt/dev/jjs/intrinsic/", |
| "user/src/", |
| "user/super/com/google/gwt/emul/", |
| "user/super/com/google/gwt/junit/translatable/", |
| }, |
| excludes: []string{ |
| "dev/core/src/com/google/gwt/dev/shell/remoteui/RemoteMessageProto.java", |
| "dev/core/src/com/google/gwt/dev/asm/", |
| "dev/core/src/com/google/gwt/dev/js/rhino/", |
| "dev/core/src/org/eclipse/", |
| "dev/core/src/org/apache/", |
| "user/src/javax/validation/", |
| "user/src/org/hibernate/validator/", |
| }, |
| }, |
| { |
| config: "eclipse/settings/code-style/gwt-checkstyle-tests.xml", |
| includes: []string{ |
| "user/test/com/google/", |
| "user/test/test/", |
| }, |
| }, |
| } |
| |
| // Match returns true if file is a ".java" file that starts with a string |
| // from includes but does not start with any strings from excludes. |
| func match(file string, includes, excludes []string) bool { |
| if !strings.HasSuffix(file, ".java") { |
| return false |
| } |
| for _, exclude := range excludes { |
| if strings.HasPrefix(file, exclude) { |
| return false |
| } |
| } |
| for _, include := range includes { |
| if strings.HasPrefix(file, include) { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // Matches returns the sublist of files that match the provided includes |
| // and excludes rules. |
| func matches(files, includes, excludes []string) []string { |
| res := []string{} |
| for _, file := range files { |
| if match(file, includes, excludes) { |
| res = append(res, file) |
| } |
| } |
| return res |
| } |
| |
| type linter struct { |
| rev string |
| comments map[string][]gerrit.Comment |
| failed bool |
| } |
| |
| func newLinter(rev string) *linter { |
| return &linter{ |
| rev: rev, |
| comments: make(map[string][]gerrit.Comment), |
| } |
| } |
| |
| func (l *linter) comment(file string, line int, severity, message string) { |
| comment := gerrit.Comment{Line: line, Message: fmt.Sprintf("[%s] %s", severity, message)} |
| l.comments[file] = append(l.comments[file], comment) |
| if severity == "error" { |
| l.failed = true |
| } |
| } |
| |
| func (l *linter) checkStyle(config string, files []string) error { |
| out, err := checkstyle.Run(config, files) |
| if err != nil { |
| log.Println("running checkstyle failed:", err) |
| return err |
| } |
| for _, file := range out { |
| if len(file.Errors) == 0 { |
| continue |
| } |
| blameable, err := git.Blame(l.rev, file.Name) |
| if err != nil { |
| return err |
| } |
| for _, err := range file.Errors { |
| if blameable[err.Line] { |
| l.comment(file.Name, err.Line, err.Severity, err.Message) |
| } |
| } |
| } |
| return nil |
| } |
| |
| func (l *linter) checkMessage(commitmsg []string) { |
| warn := func(line int, message string) { |
| // Gerrit includes some extra header lines before the commit message. |
| // TODO(mdempsky): Is it always exactly six lines? |
| l.comment("/COMMIT_MSG", line+6, "warning", message) |
| } |
| |
| if len(commitmsg) >= 3 && len(commitmsg[1]) > 0 { |
| warn(2, "Subject line and body should be separated by a blank line.") |
| } |
| for i, line := range commitmsg { |
| if i != 1 && len(line) > 72 { |
| warn(i+1, "Commit message lines should be 72 characters or fewer.") |
| break |
| } |
| } |
| } |
| |
| func (l *linter) review() gerrit.Review { |
| res := gerrit.Review{} |
| if l.failed { |
| res.Message = ("Oops, this change failed the fast style check! " + |
| "Please fix all errors and try again.") |
| res.Labels = &gerrit.Labels{Verified: -1} |
| } else if len(l.comments) != 0 { |
| res.Message = ("This change passed the fast style check, " + |
| "but still triggered some non-error warnings. " + |
| "Please review and fix if appropriate.") |
| } else { |
| res.Message = ("Woo hoo, this change passed the fast style check " + |
| "with ZERO new warnings! You rock!") |
| } |
| if len(l.comments) != 0 { |
| res.Comments = l.comments |
| } |
| return res |
| } |
| |
| // Lint fetches ref from Gerrit, inspects the commit message and runs checkstyle against |
| // any added or modified files, and posts a review to Gerrit with the results. |
| // Returns a LintFailed error if any lint errors were found. |
| func Lint(ref string) error { |
| if err := git.Run("fetch", "https://gwt.googlesource.com/gwt", ref); err != nil { |
| log.Println("fetch failed:", err) |
| return err |
| } |
| rev, err := git.Line("rev-parse", "FETCH_HEAD") |
| if err != nil { |
| return err |
| } |
| if err = git.Run("checkout", rev); err != nil { |
| return err |
| } |
| files, err := git.Lines("diff-tree", "-r", "--name-only", "--diff-filter=AM", rev+"^", rev) |
| if err != nil { |
| return err |
| } |
| |
| log.Println("running lint checks") |
| |
| linter := newLinter(rev) |
| for _, check := range checks { |
| if err := linter.checkStyle(check.config, matches(files, check.includes, check.excludes)); err != nil { |
| log.Println("checkstyle error:", err) |
| return err |
| } |
| } |
| |
| commitmsg, err := git.Lines("show", "-s", "--pretty=format:%B", rev) |
| if err != nil { |
| return err |
| } |
| linter.checkMessage(commitmsg) |
| |
| if err = gerrit.Post(ref, linter.review()); err != nil { |
| return err |
| } |
| |
| if linter.failed { |
| return LintFailed |
| } |
| return nil |
| } |