internal/core/contributors.go (116 lines of code) (raw):
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* 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
*
* https://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 core
import (
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/JetBrains/qodana-cli/internal/cloud"
"github.com/JetBrains/qodana-cli/internal/platform/git"
"github.com/JetBrains/qodana-cli/internal/platform/strutil"
)
// various variables for parsing git log output.
var (
gitFormatSep = "||" // separator for git log format
gitFormat = strings.Join(
[]string{
"%aE", // author mail (respecting .mailmap)
"%aN", // author name (respecting .mailmap)
"%H", // commit hash, in full SHA-256 format
"%ai", // author date, ISO 8601-like format
},
gitFormatSep,
)
)
const qodanaBotEmail = "qodana-support@jetbrains.com"
// author struct represents a git commit author.
type author struct {
Email string `json:"email"`
Username string `json:"username"`
}
// getId() returns the author's email if it is not empty, otherwise it returns the username.
func (a *author) getId() string {
if a.Email != "" {
return a.Email
}
return a.Username
}
// isBot returns true if the author is a bot.
func (a *author) isBot() bool {
return strings.HasSuffix(a.Email, cloud.GitHubBotSuffix) || strutil.Contains(cloud.CommonGitBots, a.Email)
}
// commit struct represents a git commit.
type commit struct {
Author *author `json:"-"` // author of the commit
Date string `json:"date"` // ISO 8601-like format
Sha256 string `json:"sha256"`
}
// contributor struct represents a git repo contributor: a pair of author and number of contributions.
type contributor struct {
Author *author `json:"author"`
Projects []string `json:"projects"`
Count int `json:"count"`
Commits []commit `json:"commits"`
}
// ToJSON returns the JSON representation of the list of contributors.
func ToJSON(contributors []contributor) (string, error) {
output := map[string]interface{}{
"total": len(contributors),
"contributors": contributors,
}
out, err := json.MarshalIndent(output, "", " ")
if err != nil {
return "", fmt.Errorf("failed to marshal json: %w", err)
}
return string(out), nil
}
// parseCommits returns the list of commits for future processing.
func parseCommits(gitLogOutput []string, excludeBots bool) []commit {
var commits []commit
for _, line := range gitLogOutput {
fields := strings.Split(line, gitFormatSep)
if len(fields) != 4 {
continue
}
a := author{
Email: fields[0],
Username: fields[1],
}
if excludeBots && a.isBot() {
continue
}
if a.Email == qodanaBotEmail {
continue
}
commits = append(
commits, commit{
Author: &a,
Date: fields[3],
Sha256: fields[2],
},
)
}
return commits
}
// GetContributors returns the list of contributors of the git repository.
func GetContributors(repoDirs []string, days int, excludeBots bool) []contributor {
contributorMap := make(map[string]*contributor)
for _, repoDir := range repoDirs {
gLog := git.Log(repoDir, gitFormat, days)
for _, c := range parseCommits(gLog, excludeBots) {
authorId := c.Author.getId()
if i, ok := contributorMap[authorId]; ok {
i.Count++
i.Projects = strutil.Append(i.Projects, repoDir)
i.Commits = append(i.Commits, c)
} else {
contributorMap[authorId] = &contributor{
Author: c.Author,
Count: 1,
Projects: []string{repoDir},
Commits: []commit{c},
}
}
}
}
contributors := make([]contributor, 0, len(contributorMap))
for _, c := range contributorMap {
contributors = append(contributors, *c)
}
sort.Slice(
contributors, func(i, j int) bool {
return contributors[i].Count > contributors[j].Count
},
)
return contributors
}