577 lines
13 KiB
Go
577 lines
13 KiB
Go
package git
|
|
|
|
import "errors"
|
|
import "io"
|
|
import "strings"
|
|
|
|
import git "github.com/go-git/go-git/v5"
|
|
import "github.com/go-git/go-git/v5/plumbing"
|
|
import "github.com/go-git/go-git/v5/plumbing/filemode"
|
|
import "github.com/go-git/go-git/v5/plumbing/format/diff"
|
|
import "github.com/go-git/go-git/v5/plumbing/object"
|
|
import "github.com/go-git/go-git/v5/plumbing/storer"
|
|
|
|
type Commit struct {
|
|
Hash string `json:"hash"`
|
|
Author string `json:"author"`
|
|
Email string `json:"email"`
|
|
When string `json:"when"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type CommitDetail struct {
|
|
Hash string `json:"hash"`
|
|
Author string `json:"author"`
|
|
Email string `json:"email"`
|
|
When string `json:"when"`
|
|
Message string `json:"message"`
|
|
Files []CommitFile `json:"files"`
|
|
}
|
|
|
|
type CommitFile struct {
|
|
Path string `json:"path"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
type TreeEntry struct {
|
|
Name string `json:"name"`
|
|
Path string `json:"path"`
|
|
Type string `json:"type"`
|
|
}
|
|
|
|
var ErrPathNotFound = errors.New("path not found")
|
|
|
|
func ListBranches(repoPath string) ([]string, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var iter storer.ReferenceIter
|
|
var branches []string
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
iter, err = repo.Branches()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer iter.Close()
|
|
err = iter.ForEach(func(ref *plumbing.Reference) error {
|
|
if ref.Hash().IsZero() {
|
|
return nil
|
|
}
|
|
branches = append(branches, ref.Name().Short())
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return branches, nil
|
|
}
|
|
|
|
func ListCommits(repoPath, ref string, limit, offset int, query string) ([]Commit, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var commit *object.Commit
|
|
var iter object.CommitIter
|
|
var commits []Commit
|
|
var matchIndex int
|
|
var msg string
|
|
var author string
|
|
var email string
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commit, err = resolveCommit(repo, ref)
|
|
if err != nil {
|
|
if isReferenceNotFound(err) {
|
|
return []Commit{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
iter, err = repo.Log(&git.LogOptions{From: commit.Hash})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer iter.Close()
|
|
query = strings.ToLower(strings.TrimSpace(query))
|
|
err = iter.ForEach(func(c *object.Commit) error {
|
|
if query != "" {
|
|
msg = strings.ToLower(c.Message)
|
|
author = strings.ToLower(c.Author.Name)
|
|
email = strings.ToLower(c.Author.Email)
|
|
if !strings.Contains(msg, query) && !strings.Contains(author, query) && !strings.Contains(email, query) {
|
|
return nil
|
|
}
|
|
}
|
|
if matchIndex < offset {
|
|
matchIndex++
|
|
return nil
|
|
}
|
|
commits = append(commits, Commit{
|
|
Hash: c.Hash.String(),
|
|
Author: c.Author.Name,
|
|
Email: c.Author.Email,
|
|
When: c.Author.When.UTC().Format(timeFormat),
|
|
Message: strings.TrimSpace(c.Message),
|
|
})
|
|
matchIndex++
|
|
if limit > 0 && len(commits) >= limit {
|
|
return storer.ErrStop
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil && err != storer.ErrStop {
|
|
return nil, err
|
|
}
|
|
return commits, nil
|
|
}
|
|
|
|
func ListFileHistory(repoPath, ref, path string, limit int) ([]Commit, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var commit *object.Commit
|
|
var iter object.CommitIter
|
|
var commits []Commit
|
|
var count int
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commit, err = resolveCommit(repo, ref)
|
|
if err != nil {
|
|
if isReferenceNotFound(err) {
|
|
return []Commit{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
iter, err = repo.Log(&git.LogOptions{
|
|
From: commit.Hash,
|
|
PathFilter: func(p string) bool {
|
|
return p == path
|
|
},
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer iter.Close()
|
|
count = 0
|
|
err = iter.ForEach(func(c *object.Commit) error {
|
|
commits = append(commits, Commit{
|
|
Hash: c.Hash.String(),
|
|
Author: c.Author.Name,
|
|
Email: c.Author.Email,
|
|
When: c.Author.When.UTC().Format(timeFormat),
|
|
Message: strings.TrimSpace(c.Message),
|
|
})
|
|
count++
|
|
if limit > 0 && count >= limit {
|
|
return storer.ErrStop
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil && err != storer.ErrStop {
|
|
return nil, err
|
|
}
|
|
return commits, nil
|
|
}
|
|
|
|
func DiffFile(repoPath, ref, path string) (string, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var commit *object.Commit
|
|
var parent *object.Commit
|
|
var patch *object.Patch
|
|
var i int
|
|
var filePatches []diff.FilePatch
|
|
var fp diff.FilePatch
|
|
var from diff.File
|
|
var to diff.File
|
|
var header string
|
|
var b strings.Builder
|
|
var chunks []diff.Chunk
|
|
var chunk diff.Chunk
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
commit, err = resolveCommit(repo, ref)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if commit.NumParents() == 0 {
|
|
return "", nil
|
|
}
|
|
parent, err = commit.Parent(0)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
patch, err = parent.Patch(commit)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
filePatches = patch.FilePatches()
|
|
for i = 0; i < len(filePatches); i++ {
|
|
fp = filePatches[i]
|
|
from, to = fp.Files()
|
|
if (from != nil && from.Path() == path) || (to != nil && to.Path() == path) {
|
|
header = "diff --git"
|
|
if from != nil {
|
|
header += " a/" + from.Path()
|
|
} else {
|
|
header += " a/" + path
|
|
}
|
|
if to != nil {
|
|
header += " b/" + to.Path()
|
|
} else {
|
|
header += " b/" + path
|
|
}
|
|
header += "\n"
|
|
b.WriteString(header)
|
|
chunks = fp.Chunks()
|
|
for i = 0; i < len(chunks); i++ {
|
|
chunk = chunks[i]
|
|
b.WriteString(chunk.Content())
|
|
}
|
|
return b.String(), nil
|
|
}
|
|
}
|
|
return "", nil
|
|
}
|
|
|
|
func GetCommitDetail(repoPath, hash string) (CommitDetail, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var commit *object.Commit
|
|
var parent *object.Commit
|
|
var patch *object.Patch
|
|
var files []CommitFile
|
|
var tree *object.Tree
|
|
var iter *object.FileIter
|
|
var file *object.File
|
|
var filePatches []diff.FilePatch
|
|
var fp diff.FilePatch
|
|
var from diff.File
|
|
var to diff.File
|
|
var path string
|
|
var status string
|
|
var seen map[string]string
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return CommitDetail{}, err
|
|
}
|
|
commit, err = repo.CommitObject(plumbing.NewHash(hash))
|
|
if err != nil {
|
|
return CommitDetail{}, err
|
|
}
|
|
if commit.NumParents() == 0 {
|
|
tree, err = commit.Tree()
|
|
if err != nil {
|
|
return CommitDetail{}, err
|
|
}
|
|
iter = tree.Files()
|
|
err = iter.ForEach(func(f *object.File) error {
|
|
file = f
|
|
files = append(files, CommitFile{Path: file.Name, Status: "added"})
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return CommitDetail{}, err
|
|
}
|
|
} else {
|
|
parent, err = commit.Parent(0)
|
|
if err != nil {
|
|
return CommitDetail{}, err
|
|
}
|
|
patch, err = parent.Patch(commit)
|
|
if err != nil {
|
|
return CommitDetail{}, err
|
|
}
|
|
filePatches = patch.FilePatches()
|
|
seen = map[string]string{}
|
|
for _, fp = range filePatches {
|
|
from, to = fp.Files()
|
|
if from == nil && to == nil {
|
|
continue
|
|
}
|
|
if to != nil {
|
|
path = to.Path()
|
|
} else {
|
|
path = from.Path()
|
|
}
|
|
if from == nil {
|
|
status = "added"
|
|
} else if to == nil {
|
|
status = "deleted"
|
|
} else if from.Path() != to.Path() {
|
|
status = "renamed"
|
|
} else {
|
|
status = "modified"
|
|
}
|
|
seen[path] = status
|
|
}
|
|
for path, status = range seen {
|
|
files = append(files, CommitFile{Path: path, Status: status})
|
|
}
|
|
}
|
|
return CommitDetail{
|
|
Hash: commit.Hash.String(),
|
|
Author: commit.Author.Name,
|
|
Email: commit.Author.Email,
|
|
When: commit.Author.When.UTC().Format(timeFormat),
|
|
Message: strings.TrimSpace(commit.Message),
|
|
Files: files,
|
|
}, nil
|
|
}
|
|
|
|
func CommitDiff(repoPath, hash string) (string, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var commit *object.Commit
|
|
var parent *object.Commit
|
|
var patch *object.Patch
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
commit, err = repo.CommitObject(plumbing.NewHash(hash))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if commit.NumParents() == 0 {
|
|
return "", nil
|
|
}
|
|
parent, err = commit.Parent(0)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
patch, err = parent.Patch(commit)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return patch.String(), nil
|
|
}
|
|
|
|
func ListCommitsBetween(repoPath, baseRef, headRef string, limit int) ([]Commit, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var head *object.Commit
|
|
var base *object.Commit
|
|
var iter object.CommitIter
|
|
var commits []Commit
|
|
var count int
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
head, err = resolveCommit(repo, headRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
base, err = resolveCommit(repo, baseRef)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
iter, err = repo.Log(&git.LogOptions{From: head.Hash})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer iter.Close()
|
|
count = 0
|
|
err = iter.ForEach(func(c *object.Commit) error {
|
|
if c.Hash == base.Hash {
|
|
return storer.ErrStop
|
|
}
|
|
commits = append(commits, Commit{
|
|
Hash: c.Hash.String(),
|
|
Author: c.Author.Name,
|
|
Email: c.Author.Email,
|
|
When: c.Author.When.UTC().Format(timeFormat),
|
|
Message: strings.TrimSpace(c.Message),
|
|
})
|
|
count++
|
|
if limit > 0 && count >= limit {
|
|
return storer.ErrStop
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil && err != storer.ErrStop {
|
|
return nil, err
|
|
}
|
|
return commits, nil
|
|
}
|
|
|
|
func ListTree(repoPath, ref, path string) ([]TreeEntry, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var commit *object.Commit
|
|
var tree *object.Tree
|
|
var entries []TreeEntry
|
|
var i int
|
|
var entry object.TreeEntry
|
|
var typeName string
|
|
var entryPath string
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commit, err = resolveCommit(repo, ref)
|
|
if err != nil {
|
|
if isReferenceNotFound(err) {
|
|
return []TreeEntry{}, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
tree, err = commit.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if path != "" {
|
|
tree, err = tree.Tree(path)
|
|
if err != nil {
|
|
if errors.Is(err, object.ErrEntryNotFound) || errors.Is(err, object.ErrDirectoryNotFound) {
|
|
return nil, ErrPathNotFound
|
|
}
|
|
return nil, err
|
|
}
|
|
}
|
|
entries = make([]TreeEntry, 0, len(tree.Entries))
|
|
for i = 0; i < len(tree.Entries); i++ {
|
|
entry = tree.Entries[i]
|
|
typeName = "file"
|
|
if entry.Mode == filemode.Dir {
|
|
typeName = "dir"
|
|
}
|
|
entryPath = entry.Name
|
|
if path != "" {
|
|
entryPath = path + "/" + entry.Name
|
|
}
|
|
entries = append(entries, TreeEntry{
|
|
Name: entry.Name,
|
|
Path: entryPath,
|
|
Type: typeName,
|
|
})
|
|
}
|
|
return entries, nil
|
|
}
|
|
|
|
func ReadBlob(repoPath, ref, path string, maxBytes int64) (string, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var commit *object.Commit
|
|
var tree *object.Tree
|
|
var file *object.File
|
|
var reader io.ReadCloser
|
|
var limited *io.LimitedReader
|
|
var data []byte
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
commit, err = resolveCommit(repo, ref)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tree, err = commit.Tree()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
file, err = tree.File(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
reader, err = file.Reader()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer reader.Close()
|
|
if maxBytes <= 0 {
|
|
maxBytes = 200 * 1024
|
|
}
|
|
limited = &io.LimitedReader{R: reader, N: maxBytes}
|
|
data, err = io.ReadAll(limited)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(data), nil
|
|
}
|
|
|
|
func ReadBlobBytes(repoPath, ref, path string, maxBytes int64) ([]byte, error) {
|
|
var repo *git.Repository
|
|
var err error
|
|
var commit *object.Commit
|
|
var tree *object.Tree
|
|
var file *object.File
|
|
var reader io.ReadCloser
|
|
var limited *io.LimitedReader
|
|
var data []byte
|
|
repo, err = git.PlainOpen(repoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commit, err = resolveCommit(repo, ref)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
tree, err = commit.Tree()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
file, err = tree.File(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader, err = file.Reader()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer reader.Close()
|
|
if maxBytes <= 0 {
|
|
maxBytes = 5 * 1024 * 1024
|
|
}
|
|
limited = &io.LimitedReader{R: reader, N: maxBytes}
|
|
data, err = io.ReadAll(limited)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return data, nil
|
|
}
|
|
|
|
const timeFormat = "2006-01-02 15:04:05Z"
|
|
|
|
func resolveCommit(repo *git.Repository, ref string) (*object.Commit, error) {
|
|
var commit *object.Commit
|
|
var err error
|
|
ref = strings.TrimSpace(ref)
|
|
if ref == "" {
|
|
ref = "HEAD"
|
|
}
|
|
commit, err = resolveCommitByRef(repo, ref)
|
|
if err == nil {
|
|
return commit, nil
|
|
}
|
|
if ref != "HEAD" && !strings.HasPrefix(ref, "refs/") {
|
|
return resolveCommitByRef(repo, "refs/heads/"+ref)
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
func resolveCommitByRef(repo *git.Repository, ref string) (*object.Commit, error) {
|
|
var hash *plumbing.Hash
|
|
var err error
|
|
var commit *object.Commit
|
|
hash, err = repo.ResolveRevision(plumbing.Revision(ref))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
commit, err = repo.CommitObject(*hash)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return commit, nil
|
|
}
|
|
|
|
func isReferenceNotFound(err error) bool {
|
|
return errors.Is(err, plumbing.ErrReferenceNotFound)
|
|
}
|