Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merge/release] Add commit message linting feature #43

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions GET.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,20 @@ maintainers:
features:
- dco_check
- comments
- commit_linting
```

Features:

* `dco_check` - checks that each commit finishes with a "Signed-off-by:" statement
* `comments` - allows `maintainers` to issue commands to Derek to add labels etc
* `commit_linting` - applies linting rules to commit messages

Commit linting ensures:

* subjects start with a capital letter
* subject lines are less than 50 characters
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Less than or equal to 50 characters

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub will allow up to 72 characters before it wraps. I'm wondering whether we should expand the limit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In all areas that the subject is displayed? Is the client similar? The referenced article is 4 years old so gh may have changed in that time.

I liked the idea of that being configurable, even if it was through env-vars.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could move the hard-limit to 72 characters as per GitHub UI

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subject lines contain no more than 72 characters


**Testing**

Create a label of "no-dco" within every project you want Derek to help you with.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ Derek unlock
* [x] Lock thread
* [x] Edit title
* [x] Toggle the DCO-feature
* [x] Add commit message linting feature

Future work:

Expand Down
29 changes: 18 additions & 11 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,8 @@ import (
"github.com/alexellis/derek/types"
)

const dcoCheck = "dco_check"
const comments = "comments"
const deleted = "deleted"

func hmacValidation() bool {
val := os.Getenv("validate_hmac")
return len(val) > 0 && (val == "1" || val == "true")
}

func main() {
bytesIn, _ := ioutil.ReadAll(os.Stdin)

Expand Down Expand Up @@ -53,7 +46,7 @@ func handleEvent(eventType string, bytesIn []byte) error {
case "pull_request":
req := types.PullRequestOuter{}
if err := json.Unmarshal(bytesIn, &req); err != nil {
return fmt.Errorf("Cannot parse input %s", err.Error())
return fmt.Errorf("pull_request handler, cannot parse input: %s", err.Error())
}

customer, err := auth.IsCustomer(req.Repository)
Expand All @@ -67,11 +60,18 @@ func handleEvent(eventType string, bytesIn []byte) error {
if err != nil {
return fmt.Errorf("Unable to access maintainers file at: %s/%s", req.Repository.Owner.Login, req.Repository.Name)
}

if req.Action != closedConstant {
if enabledFeature(dcoCheck, derekConfig) {
handlePullRequest(req)
prFeatures := types.PullRequestFeatures{
CommitLintingFeature: enabledFeature(types.CommitLintingFeature, derekConfig),
DCOCheckFeature: enabledFeature(types.DCOCheckFeature, derekConfig),
}

if prFeatures.Enabled() {
handlePullRequest(req, prFeatures)
}
}

break

case "issue_comment":
Expand All @@ -93,14 +93,21 @@ func handleEvent(eventType string, bytesIn []byte) error {
}

if req.Action != deleted {
if permittedUserFeature(comments, derekConfig, req.Comment.User.Login) {
if permittedUserFeature(types.CommentFeature, derekConfig, req.Comment.User.Login) {
handleComment(req)
}
}

break

default:
return fmt.Errorf("X_Github_Event want: ['pull_request', 'issue_comment'], got: " + eventType)
}

return nil
}

func hmacValidation() bool {
val := os.Getenv("validate_hmac")
return len(val) > 0 && (val == "1" || val == "true")
}
198 changes: 148 additions & 50 deletions pullRequestHandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"github.com/google/go-github/github"
)

func handlePullRequest(req types.PullRequestOuter) {
func handlePullRequest(req types.PullRequestOuter, prFeatures types.PullRequestFeatures) {
ctx := context.Background()

token := os.Getenv("access_token")
Expand All @@ -32,88 +32,98 @@ func handlePullRequest(req types.PullRequestOuter) {

client := auth.MakeClient(ctx, token)

hasUnsignedCommits, err := hasUnsigned(req, client)
commits, commitFetchErr := getCommits(req, client)

if err != nil {
log.Fatal(err)
} else if hasUnsignedCommits {
fmt.Println("May need to apply labels on item.")
if commitFetchErr != nil {
log.Fatal(commitFetchErr)
}

issue, _, labelErr := client.Issues.Get(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number)
issue, _, labelErr := client.Issues.Get(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number)
if labelErr != nil {
log.Fatalln("Unable to fetch labels for PR %s/%s/#%d", req.Repository.Owner, req.Repository.Name, req.PullRequest.Number)
}

if labelErr != nil {
log.Fatalln(labelErr)
if prFeatures.CommitLintingFeature {
lintResult := lintCommits(commits)
applyErr := applyLintingLabel(req, client, issue, lintResult)
if applyErr != nil {
log.Printf("Error applying linting rule: %s", applyErr)
}
fmt.Println("Current labels ", issue.Labels)
}

if hasNoDcoLabel(issue) == false {
fmt.Println("Applying label")
_, res, assignLabelErr := client.Issues.AddLabelsToIssue(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, []string{"no-dco"})
if assignLabelErr != nil {
log.Fatalf("%s limit: %d, remaining: %d", assignLabelErr, res.Limit, res.Remaining)
}
if prFeatures.DCOCheckFeature {
if hasUnsigned(commits) == true {
fmt.Println("May need to apply labels on item.")

link := fmt.Sprintf("https://github.com/%s/%s/blob/master/CONTRIBUTING.md", req.Repository.Owner.Login, req.Repository.Name)
body := `Thank you for your contribution. I've just checked and your commit doesn't appear to be signed-off.
That's something we need before your Pull Request can be merged. Please see our [contributing guide](` + link + `).`
fmt.Println("Current labels ", issue.Labels)

comment := &github.IssueComment{
Body: &body,
}
if hasLabelAssigned("no-dco", issue) == false {
fmt.Println("Applying label")
_, res, assignLabelErr := client.Issues.AddLabelsToIssue(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, []string{"no-dco"})
if assignLabelErr != nil {
log.Fatalf("%s limit: %d, remaining: %d", assignLabelErr, res.Limit, res.Remaining)
}

comment, resp, err := client.Issues.CreateComment(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, comment)
if err != nil {
log.Fatalf("%s limit: %d, remaining: %d", assignLabelErr, resp.Limit, resp.Remaining)
log.Fatal(err)
}
fmt.Println(comment, resp.Rate)
}
} else {
fmt.Println("Things look OK right now.")
issue, res, labelErr := client.Issues.Get(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number)
link := fmt.Sprintf("https://github.com/%s/%s/blob/master/CONTRIBUTING.md", req.Repository.Owner.Login, req.Repository.Name)
body := `Thank you for your contribution. I've just checked and your commit doesn't appear to be signed-off.
That's something we need before your Pull Request can be merged. Please see our [contributing guide](` + link + `).`

if labelErr != nil {
log.Fatalf("%s limit: %d, remaining: %d", labelErr, res.Limit, res.Remaining)
log.Fatalln()
}
comment := &github.IssueComment{
Body: &body,
}

if hasNoDcoLabel(issue) {
fmt.Println("Removing label")
_, removeLabelErr := client.Issues.RemoveLabelForIssue(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, "no-dco")
if removeLabelErr != nil {
log.Fatal(removeLabelErr)
comment, resp, err := client.Issues.CreateComment(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, comment)
if err != nil {
log.Fatalf("%s limit: %d, remaining: %d", assignLabelErr, resp.Limit, resp.Remaining)
log.Fatal(err)
}
fmt.Println(comment, resp.Rate)
}
} else {
fmt.Println("Things look OK right now.")

if hasLabelAssigned("no-dco", issue) {
fmt.Println("Removing label")
_, removeLabelErr := client.Issues.RemoveLabelForIssue(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, "no-dco")
if removeLabelErr != nil {
log.Fatal(removeLabelErr)
}
}
}
}
}

func hasNoDcoLabel(issue *github.Issue) bool {
func hasLabelAssigned(labelName string, issue *github.Issue) bool {
if issue != nil {
for _, label := range issue.Labels {
if label.GetName() == "no-dco" {
if label.GetName() == labelName {
return true
}
}
}

return false
}

func hasUnsigned(req types.PullRequestOuter, client *github.Client) (bool, error) {
hasUnsigned := false
func getCommits(req types.PullRequestOuter, client *github.Client) ([]*github.RepositoryCommit, error) {
ctx := context.Background()

var err error
var responseErr error
listOpts := &github.ListOptions{
Page: 0,
}

commits, resp, err := client.PullRequests.ListCommits(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, listOpts)
if err != nil {
log.Fatalf("Error getting PR %d\n%s", req.PullRequest.Number, err.Error())
return hasUnsigned, err
responseErr = fmt.Errorf("unable to fetch commits for PR: %d, Error: %s", req.PullRequest.Number, err.Error())
}

fmt.Println("Rate limiting", resp.Rate)
log.Printf("Rate limiting remaining: %d", resp.Rate.Remaining)
return commits, responseErr
}

func hasUnsigned(commits []*github.RepositoryCommit) bool {
hasUnsigned := false

for _, commit := range commits {
if commit.Commit != nil && commit.Commit.Message != nil {
Expand All @@ -123,9 +133,97 @@ func hasUnsigned(req types.PullRequestOuter, client *github.Client) (bool, error
}
}

return hasUnsigned, err
return hasUnsigned
}

func isSigned(msg string) bool {
return strings.Contains(msg, "Signed-off-by:")
}

func lintCommits(commits []*github.RepositoryCommit) bool {
for _, commit := range commits {
if commit.Commit != nil && commit.Commit.Message != nil {
if lintCommit(commit.Commit.Message) == false {
return false
}
}
}
return true
}

func lintCommit(message *string) bool {
var valid bool

if message == nil {
return false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this is tested in lintCommits, is this belt and braces or a hangover?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure.. I might have to double-check

}

parts := strings.Split(*message, "\n")

if len(parts) > 0 {
lengthValid := len(parts[0]) <= 50
var startsWithUpper bool

firstCharacter := getFirstCharacter(parts[0])
if firstCharacter != nil {
startsWithUpper = len(*firstCharacter) > 0 && strings.ToUpper(*firstCharacter) == *firstCharacter
}

valid = lengthValid && startsWithUpper
}

return valid
}

func getFirstCharacter(msg string) *string {
var ret *string

for _, runeVal := range msg {
asStr := string(runeVal)
ret = &asStr
break
}

return ret
}

func applyLintingLabel(req types.PullRequestOuter, client *github.Client, issue *github.Issue, lintResult bool) error {
labelCaption := "review/commit-message"
var actionErr error
hasLabel := hasLabelAssigned(labelCaption, issue)

ctx := context.Background()

if hasLabel {
if lintResult == true {
res, assignLabelErr := client.Issues.RemoveLabelForIssue(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, labelCaption)
if assignLabelErr != nil {
actionErr = fmt.Errorf("removeLabel: %s limit: %d, remaining: %d", assignLabelErr, res.Limit, res.Remaining)
}
}
} else {
if lintResult == false {
_, res, assignLabelErr := client.Issues.AddLabelsToIssue(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, []string{labelCaption})
if assignLabelErr != nil {
actionErr = fmt.Errorf("addLabel: %s limit: %d, remaining: %d", assignLabelErr, res.Limit, res.Remaining)
}
link := fmt.Sprintf("https://github.com/%s/%s/blob/master/CONTRIBUTING.md", req.Repository.Owner.Login, req.Repository.Name)

body := `Please check that your commit messages fit within [these guidelines](` + link + `):

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please check that your commit messages fit within [these guidelines](link):

  • Commit subject should not exceed 50 characters
  • Commit subject should start with an uppercase letter

I think there needs to be a quick link here to make it as easy as possible for everyone to edit their commit history. A direct link to this page (as in the contributing guide) would be perfect.

Prior Slack discussion here.

Thoughts?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes a link could be good for re-writing history.

- Commit subject should not exceed 50 characters
- Commit subject should start with an uppercase letter
`

comment := &github.IssueComment{
Body: &body,
}

_, _, err := client.Issues.CreateComment(ctx, req.Repository.Owner.Login, req.Repository.Name, req.PullRequest.Number, comment)
if err != nil {
actionErr = fmt.Errorf("unable to create comment due to linting check: %s", err)
}
}
}

return actionErr
}
Loading