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 }