feat: Initial commit

This commit is contained in:
2026-07-13 05:45:48 +07:00
commit 1f616240db
12 changed files with 810 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
package scanner
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
)
type Entry struct {
Name string
Email string
Field string // "Author", "Committer", or a trailer name like "Co-authored-by"
Repo string
CommitSHA string
Date string
PushedByLogin string `json:",omitempty"` // GitHub login that pushed this commit, per the public Events API
}
const (
unitSep = "\x1f"
recordSep = "\x1e"
)
var logFormat = "%H" + unitSep + "%an" + unitSep + "%ae" + unitSep + "%cn" + unitSep + "%ce" + unitSep + "%aI" + unitSep + "%B" + recordSep
var trailerPattern = regexp.MustCompile(`(?m)^([A-Za-z][A-Za-z-]*-by):\s*(.+?)\s*<([^>]+)>\s*$`)
func Clone(repoFullName, repoURL, token string) (dir string, cleanup func(), err error) {
tmpDir, err := os.MkdirTemp("", "git-identity-audit-*")
if err != nil {
return "", nil, fmt.Errorf("creating temp dir: %w", err)
}
cleanup = func() { os.RemoveAll(tmpDir) }
authURL := injectToken(repoURL, token)
cmd := exec.Command("git", "clone", "--bare", "--filter=blob:none", "--quiet", authURL, tmpDir)
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
cleanup()
return "", nil, fmt.Errorf("cloning %s: %w: %s", repoFullName, err, stderr.String())
}
return tmpDir, cleanup, nil
}
func CountCommits(gitDir string) (int, error) {
cmd := exec.Command("git", "--git-dir", gitDir, "rev-list", "--all", "--count")
out, err := cmd.Output()
if err != nil {
return 0, fmt.Errorf("counting commits: %w", err)
}
n, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return 0, fmt.Errorf("parsing commit count: %w", err)
}
return n, nil
}
func StreamLog(gitDir, repoFullName string, onCommit func(entries []Entry)) error {
cmd := exec.Command("git", "--git-dir", gitDir, "log", "--all", "--format="+logFormat)
stdout, err := cmd.StdoutPipe()
if err != nil {
return fmt.Errorf("opening log stream: %w", err)
}
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Start(); err != nil {
return fmt.Errorf("starting log stream: %w", err)
}
scanner := bufio.NewScanner(stdout)
scanner.Buffer(make([]byte, 0, 64*1024), 16*1024*1024)
scanner.Split(splitOnRecordSep)
for scanner.Scan() {
record := strings.Trim(scanner.Text(), "\n")
if record == "" {
continue
}
onCommit(parseRecord(record, repoFullName))
}
scanErr := scanner.Err()
waitErr := cmd.Wait()
if waitErr != nil {
return fmt.Errorf("reading log for %s: %w: %s", repoFullName, waitErr, stderr.String())
}
if scanErr != nil {
return fmt.Errorf("reading log for %s: %w", repoFullName, scanErr)
}
return nil
}
func splitOnRecordSep(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, recordSep[0]); i >= 0 {
return i + 1, data[:i], nil
}
if atEOF {
return len(data), data, nil
}
return 0, nil, nil
}
func injectToken(repoURL, token string) string {
const prefix = "https://"
if !strings.HasPrefix(repoURL, prefix) {
return repoURL
}
return prefix + "x-access-token:" + token + "@" + strings.TrimPrefix(repoURL, prefix)
}
func parseRecord(record, repoFullName string) []Entry {
fields := strings.SplitN(record, unitSep, 7)
if len(fields) < 7 {
return nil
}
sha, authorName, authorEmail, committerName, committerEmail, date, body := fields[0], fields[1], fields[2], fields[3], fields[4], fields[5], fields[6]
entries := []Entry{
{Name: authorName, Email: authorEmail, Field: "Author", Repo: repoFullName, CommitSHA: sha, Date: date},
{Name: committerName, Email: committerEmail, Field: "Committer", Repo: repoFullName, CommitSHA: sha, Date: date},
}
for _, match := range trailerPattern.FindAllStringSubmatch(body, -1) {
entries = append(entries, Entry{Name: match[2], Email: match[3], Field: match[1], Repo: repoFullName, CommitSHA: sha, Date: date})
}
return entries
}
+86
View File
@@ -0,0 +1,86 @@
package scanner
import (
"os"
"os/exec"
"path/filepath"
"sort"
"testing"
)
func runGit(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %v failed: %v\n%s", args, err, out)
}
}
func TestScanExtractsAuthorCommitterAndTrailers(t *testing.T) {
dir := t.TempDir()
runGit(t, dir, "init", "-q")
runGit(t, dir, "config", "user.email", "author@example.com")
runGit(t, dir, "config", "user.name", "Author One")
if err := os.WriteFile(filepath.Join(dir, "file.txt"), []byte("hello"), 0644); err != nil {
t.Fatal(err)
}
runGit(t, dir, "add", "file.txt")
runGit(t, dir, "commit", "-q", "-m",
"Initial commit\n\nCo-authored-by: Helper Two <helper@example.com>\nSigned-off-by: Author One <author@example.com>")
runGit(t, dir, "-c", "user.name=Committer Three", "-c", "user.email=committer@example.com",
"commit", "-q", "--allow-empty", "-m", "second commit")
gitDir, cleanup, err := Clone("smoke/test", "file://"+dir, "")
if err != nil {
t.Fatalf("Clone failed: %v", err)
}
defer cleanup()
count, err := CountCommits(gitDir)
if err != nil {
t.Fatalf("CountCommits failed: %v", err)
}
if count != 2 {
t.Errorf("expected 2 commits, got %d", count)
}
var entries []Entry
commitsSeen := 0
if err := StreamLog(gitDir, "smoke/test", func(e []Entry) {
commitsSeen++
entries = append(entries, e...)
}); err != nil {
t.Fatalf("StreamLog failed: %v", err)
}
if commitsSeen != 2 {
t.Errorf("expected onCommit called 2 times, got %d", commitsSeen)
}
got := map[string]bool{}
for _, e := range entries {
got[e.Field+"|"+e.Name+"|"+e.Email] = true
}
want := []string{
"Author|Author One|author@example.com",
"Committer|Author One|author@example.com",
"Co-authored-by|Helper Two|helper@example.com",
"Signed-off-by|Author One|author@example.com",
"Author|Author One|author@example.com",
"Committer|Committer Three|committer@example.com",
}
for _, w := range want {
if !got[w] {
all := make([]string, 0, len(got))
for k := range got {
all = append(all, k)
}
sort.Strings(all)
t.Errorf("expected entry %q not found; got: %v", w, all)
}
}
}