Files
forge/services/convert/git_commit.go
zeekayandHanzo Dev aa87442fdd Record upstream authorship centrally in NOTICE instead of in every file header
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the
Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in
NOTICE, alongside LICENSE. The header lint accepts any copyright line above the
SPDX line, so it no longer demands an upstream one.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 17:50:35 -07:00

227 lines
6.4 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// SPDX-License-Identifier: MIT
package convert
import (
"context"
"net/url"
"time"
repo_model "github.com/hanzoai/git/models/repo"
user_model "github.com/hanzoai/git/models/user"
"github.com/hanzoai/git/modules/git"
"github.com/hanzoai/git/modules/gitrepo"
"github.com/hanzoai/git/modules/log"
api "github.com/hanzoai/git/modules/structs"
ctx "github.com/hanzoai/git/services/context"
"github.com/hanzoai/git/services/gitdiff"
)
// ToCommitUser convert a git.Signature to an api.CommitUser
func ToCommitUser(sig *git.Signature) *api.CommitUser {
return &api.CommitUser{
Identity: api.Identity{
Name: sig.Name,
Email: sig.Email,
},
Date: sig.When.UTC().Format(time.RFC3339),
}
}
// ToCommitMeta convert a git.Tag to an api.CommitMeta
func ToCommitMeta(repo *repo_model.Repository, tag *git.Tag) *api.CommitMeta {
return &api.CommitMeta{
SHA: tag.Object.String(),
URL: repo.APIURL() + "/git/commits/" + tag.ID.String(),
Created: tag.Tagger.When,
}
}
// ToPayloadCommit convert a git.Commit to api.PayloadCommit
func ToPayloadCommit(ctx context.Context, repo *repo_model.Repository, c *git.Commit) *api.PayloadCommit {
authorUsername := ""
if author, err := user_model.GetUserByEmail(ctx, c.Author.Email); err == nil {
authorUsername = author.Name
} else if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
}
committerUsername := ""
if committer, err := user_model.GetUserByEmail(ctx, c.Committer.Email); err == nil {
committerUsername = committer.Name
} else if !user_model.IsErrUserNotExist(err) {
log.Error("GetUserByEmail: %v", err)
}
return &api.PayloadCommit{
ID: c.ID.String(),
Message: c.MessageUTF8(),
URL: repo.HTMLURL() + "/commit/" + c.ID.String(),
Author: &api.PayloadUser{
Name: c.Author.Name,
Email: c.Author.Email,
UserName: authorUsername,
},
Committer: &api.PayloadUser{
Name: c.Committer.Name,
Email: c.Committer.Email,
UserName: committerUsername,
},
Timestamp: c.Author.When,
Verification: ToVerification(ctx, c),
}
}
type ToCommitOptions struct {
Stat bool
Verification bool
Files bool
}
func ParseCommitOptions(ctx *ctx.APIContext) ToCommitOptions {
return ToCommitOptions{
Stat: ctx.FormString("stat") == "" || ctx.FormBool("stat"),
Files: ctx.FormString("files") == "" || ctx.FormBool("files"),
Verification: ctx.FormString("verification") == "" || ctx.FormBool("verification"),
}
}
// ToCommit convert a git.Commit to api.Commit
func ToCommit(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, commit *git.Commit, userCache map[string]*user_model.User, opts ToCommitOptions) (*api.Commit, error) {
var apiAuthor, apiCommitter *api.User
// Retrieve author and committer information
var cacheAuthor *user_model.User
var ok bool
if userCache == nil {
cacheAuthor = (*user_model.User)(nil)
ok = false
} else {
cacheAuthor, ok = userCache[commit.Author.Email]
}
if ok {
apiAuthor = ToUser(ctx, cacheAuthor, nil)
} else {
author, err := user_model.GetUserByEmail(ctx, commit.Author.Email)
if err != nil && !user_model.IsErrUserNotExist(err) {
return nil, err
} else if err == nil {
apiAuthor = ToUser(ctx, author, nil)
if userCache != nil {
userCache[commit.Author.Email] = author
}
}
}
var cacheCommitter *user_model.User
if userCache == nil {
cacheCommitter = (*user_model.User)(nil)
ok = false
} else {
cacheCommitter, ok = userCache[commit.Committer.Email]
}
if ok {
apiCommitter = ToUser(ctx, cacheCommitter, nil)
} else {
committer, err := user_model.GetUserByEmail(ctx, commit.Committer.Email)
if err != nil && !user_model.IsErrUserNotExist(err) {
return nil, err
} else if err == nil {
apiCommitter = ToUser(ctx, committer, nil)
if userCache != nil {
userCache[commit.Committer.Email] = committer
}
}
}
// Retrieve parent(s) of the commit
apiParents := make([]*api.CommitMeta, commit.ParentCount())
for i := 0; i < commit.ParentCount(); i++ {
sha, _ := commit.ParentID(i)
apiParents[i] = &api.CommitMeta{
URL: repo.APIURL() + "/git/commits/" + url.PathEscape(sha.String()),
SHA: sha.String(),
}
}
res := &api.Commit{
CommitMeta: &api.CommitMeta{
URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()),
SHA: commit.ID.String(),
Created: commit.Committer.When,
},
HTMLURL: repo.HTMLURL() + "/commit/" + url.PathEscape(commit.ID.String()),
RepoCommit: &api.RepoCommit{
URL: repo.APIURL() + "/git/commits/" + url.PathEscape(commit.ID.String()),
Author: &api.CommitUser{
Identity: api.Identity{
Name: commit.Author.Name,
Email: commit.Author.Email,
},
Date: commit.Author.When.Format(time.RFC3339),
},
Committer: &api.CommitUser{
Identity: api.Identity{
Name: commit.Committer.Name,
Email: commit.Committer.Email,
},
Date: commit.Committer.When.Format(time.RFC3339),
},
Message: commit.MessageUTF8(),
Tree: &api.CommitMeta{
URL: repo.APIURL() + "/git/trees/" + url.PathEscape(commit.ID.String()),
SHA: commit.ID.String(),
Created: commit.Committer.When,
},
},
Author: apiAuthor,
Committer: apiCommitter,
Parents: apiParents,
}
// Retrieve verification for commit
if opts.Verification {
res.RepoCommit.Verification = ToVerification(ctx, commit)
}
// Retrieve files affected by the commit
if opts.Files {
fileStatus, err := gitrepo.GetCommitFileStatus(ctx, repo, commit.ID.String())
if err != nil {
return nil, err
}
affectedFileList := make([]*api.CommitAffectedFiles, 0, len(fileStatus.Added)+len(fileStatus.Removed)+len(fileStatus.Modified))
for filestatus, files := range map[string][]string{"added": fileStatus.Added, "removed": fileStatus.Removed, "modified": fileStatus.Modified} {
for _, filename := range files {
affectedFileList = append(affectedFileList, &api.CommitAffectedFiles{
Filename: filename,
Status: filestatus,
})
}
}
res.Files = affectedFileList
}
// Get diff stats for commit
if opts.Stat {
diffShortStat, err := gitdiff.GetDiffShortStat(ctx, repo, gitRepo, "", commit.ID.String())
if err != nil {
return nil, err
}
res.Stats = &api.CommitStats{
Total: diffShortStat.TotalAddition + diffShortStat.TotalDeletion,
Additions: diffShortStat.TotalAddition,
Deletions: diffShortStat.TotalDeletion,
}
}
return res, nil
}