diff --git a/branch_diff_commits.go b/branch_diff_commits.go index 8ff3538..de871c5 100644 --- a/branch_diff_commits.go +++ b/branch_diff_commits.go @@ -2,52 +2,38 @@ package git import ( "fmt" - - "github.com/go-git/go-git/v5/plumbing" + "strings" ) // BranchDiffCommits compares commits from 2 branches and returns of a diff of them. -func (g *Git) BranchDiffCommits(branchA string, branchB string) ([]plumbing.Hash, error) { - branchACommit, err := g.LatestCommitOnBranch(branchA) - - if err != nil { - return nil, fmt.Errorf("Failed getting latest commit for branch %v: %v", branchA, err) - } - - branchBCommit, err := g.LatestCommitOnBranch(branchB) - - if err != nil { - return nil, fmt.Errorf("Failed getting latest commit for branch %v: %v", branchB, err) - } - - branchACommits, err := g.CommitsOnBranch(branchACommit.Hash) - +// Uses git log with exclusion syntax for efficient comparison - finds commits in branchA that are not in branchB. +// This is more efficient than fetching all commits from both branches and comparing them. +func (g *Git) BranchDiffCommits(branchA string, branchB string) ([]Hash, error) { + // git log branchA ^branchB shows all commits reachable from branchA but not from branchB + // This is equivalent to: commits in branchA that are not in branchB + // The ^branchB syntax excludes all commits reachable from branchB + output, err := g.runGitCommand("log", "--format=%H", branchA, "^"+branchB) if err != nil { - return nil, fmt.Errorf("Failed getting commits for branch %v: %v", branchA, err) + return nil, fmt.Errorf("Failed comparing branches %v and %v: %v", branchA, branchB, err) } - branchBCommits, err := g.CommitsOnBranch(branchBCommit.Hash) + var diffCommits []Hash + lines := strings.Split(output, "\n") - if err != nil { - return nil, fmt.Errorf("Failed getting commits for branch %v: %v", branchB, err) - } - - var diffCommits []plumbing.Hash + for _, line := range lines { + line = strings.TrimSpace(line) + if line == "" { + continue + } - for _, commit := range branchACommits { - if !contains(branchBCommits, commit) { - diffCommits = append(diffCommits, commit) + hash, err := NewHash(line) + if err != nil { + // Skip invalid hashes + continue } + + diffCommits = append(diffCommits, hash) } return diffCommits, nil } - -func contains(s []plumbing.Hash, e plumbing.Hash) bool { - for _, a := range s { - if a == e { - return true - } - } - return false -} diff --git a/branch_diff_commits_test.go b/branch_diff_commits_test.go index 5a0783c..654189d 100644 --- a/branch_diff_commits_test.go +++ b/branch_diff_commits_test.go @@ -1,36 +1,36 @@ package git import ( + "os" "testing" - "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" ) func TestBranchDiffCommits(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - testGit := &Git{repo: repo} + createTestHistory(t, testGit) commits, err := testGit.BranchDiffCommits("my-branch", "master") - commit, _ := repo.CommitObject(commits[0]) + commit, _ := testGit.Commit(commits[0]) assert.NoError(t, err) - assert.Equal(t, "third commit on new branch", commit.Message) + assert.Equal(t, "third commit on new branch\n", commit.Message) assert.Equal(t, 3, len(commits)) } func TestBranchDiffCommitsWithMasterMerge(t *testing.T) { - repo, _ := git.PlainOpen("./testdata/commits_on_branch") - testGit := &Git{repo: repo} + testGit, err := OpenGit("./testdata/commits_on_branch") + assert.NoError(t, err) commits, err := testGit.BranchDiffCommits("behind-master", "origin/master") assert.Equal(t, 2, len(commits)) - commit, _ := repo.CommitObject(commits[1]) + commit, _ := testGit.Commit(commits[1]) assert.Equal(t, "chore: commit on branch\n", commit.Message) diff --git a/commit.go b/commit.go index 9840ab6..9a7e90f 100644 --- a/commit.go +++ b/commit.go @@ -1,17 +1,83 @@ package git import ( - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" + "strconv" + "strings" + "time" ) -// Commit find a commit based on commit hash and returns the Commit object -func (g *Git) Commit(hash plumbing.Hash) (*object.Commit, error) { - commitObject, err := g.repo.CommitObject(hash) +// Commit finds a commit based on commit hash and returns the Commit object +func (g *Git) Commit(hash Hash) (*Commit, error) { + hashStr := hash.String() + // Get commit message - use %B to get full body, then normalize + // go-git's Message field returns the raw commit message which always ends with \n + fullMessage, err := g.runGitCommand("log", "-1", "--format=%B", hashStr) + if err != nil { + return nil, err + } + + // Normalize: remove trailing newlines and add exactly one + // This matches go-git's behavior where Message always ends with \n + message := strings.TrimRight(fullMessage, "\n") + if message != "" { + message += "\n" + } + + // Get author info + authorName, err := g.runGitCommand("log", "-1", "--format=%an", hashStr) + if err != nil { + return nil, err + } + + authorEmail, err := g.runGitCommand("log", "-1", "--format=%ae", hashStr) + if err != nil { + return nil, err + } + + authorDateStr, err := g.runGitCommand("log", "-1", "--format=%at", hashStr) + if err != nil { + return nil, err + } + + authorTimestamp, err := strconv.ParseInt(authorDateStr, 10, 64) + if err != nil { + return nil, err + } + + // Get committer info + committerName, err := g.runGitCommand("log", "-1", "--format=%cn", hashStr) + if err != nil { + return nil, err + } + + committerEmail, err := g.runGitCommand("log", "-1", "--format=%ce", hashStr) + if err != nil { + return nil, err + } + + committerDateStr, err := g.runGitCommand("log", "-1", "--format=%ct", hashStr) + if err != nil { + return nil, err + } + + committerTimestamp, err := strconv.ParseInt(committerDateStr, 10, 64) if err != nil { return nil, err } - return commitObject, nil + return &Commit{ + Hash: hash, + Message: message, + Author: Signature{ + Name: authorName, + Email: authorEmail, + When: time.Unix(authorTimestamp, 0), + }, + Committer: Signature{ + Name: committerName, + Email: committerEmail, + When: time.Unix(committerTimestamp, 0), + }, + }, nil } diff --git a/commit_date.go b/commit_date.go index 3f995ee..27875c9 100644 --- a/commit_date.go +++ b/commit_date.go @@ -1,20 +1,23 @@ package git import ( + "strconv" "time" - - "github.com/go-git/go-git/v5/plumbing" ) // commitDate gets the commit at hash and returns the time of the commit -func (g *Git) commitDate(commit plumbing.Hash) (time.Time, error) { - commitObject, err := g.repo.CommitObject(commit) +func (g *Git) commitDate(commit Hash) (time.Time, error) { + hashStr := commit.String() + dateStr, err := g.runGitCommand("log", "-1", "--format=%ct", hashStr) if err != nil { return time.Now(), err } - when := commitObject.Committer.When + timestamp, err := strconv.ParseInt(dateStr, 10, 64) + if err != nil { + return time.Now(), err + } - return when, nil + return time.Unix(timestamp, 0), nil } diff --git a/commit_test.go b/commit_test.go index a3cf8b1..9da26d9 100644 --- a/commit_test.go +++ b/commit_test.go @@ -1,21 +1,22 @@ package git import ( + "os" "testing" "github.com/stretchr/testify/assert" ) func TestCommit(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - testGit := &Git{repo: repo} + createTestHistory(t, testGit) head, _ := testGit.CurrentCommit() commit, err := testGit.Commit(head.Hash) assert.NoError(t, err) - assert.Equal(t, "third commit on new branch", commit.Message) + assert.Equal(t, "third commit on new branch\n", commit.Message) assert.NotEmpty(t, commit.Hash) } diff --git a/commits_between.go b/commits_between.go index adad187..9be8b18 100644 --- a/commits_between.go +++ b/commits_between.go @@ -1,40 +1,68 @@ package git import ( - "errors" - - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" -) - -var ( - errReachedToCommit = errors.New("reached to commit") + "strings" ) // CommitsBetween returns a slice of commit hashes between two commits -func (g *Git) CommitsBetween(from plumbing.Hash, to plumbing.Hash) ([]plumbing.Hash, error) { - cIter, _ := g.repo.Log(&git.LogOptions{From: from}) +func (g *Git) CommitsBetween(from Hash, to Hash) ([]Hash, error) { + // If from and to are equal, return empty slice + if from == to { + return []Hash{}, nil + } - var commits []plumbing.Hash + fromStr := from.String() + toStr := to.String() - err := cIter.ForEach(func(c *object.Commit) error { - // If no previous tag is found then from and to are equal - if from == to { - return nil + // Check if 'to' is an empty hash (all zeros) + var emptyHash Hash + if to == emptyHash { + // If 'to' is empty, return all commits from 'from' + output, err := g.runGitCommand("log", "--format=%H", fromStr) + if err != nil { + return nil, err } - if c.Hash == to { - return errReachedToCommit + lines := strings.Split(output, "\n") + var commits []Hash + for _, line := range lines { + if line == "" { + continue + } + hash, err := NewHash(line) + if err != nil { + continue + } + commits = append(commits, hash) } - commits = append(commits, c.Hash) - return nil - }) - - if err == errReachedToCommit { return commits, nil } + + // Get commits from 'from' to 'to' (excluding 'to') + // Use ^to to exclude the 'to' commit + output, err := g.runGitCommand("log", "--format=%H", fromStr, "^"+toStr) if err != nil { - return commits, err + // If the command fails, it might be because there are no commits between + // Try to check if 'to' is reachable from 'from' + _, err2 := g.runGitCommand("merge-base", "--is-ancestor", toStr, fromStr) + if err2 != nil { + return []Hash{}, nil + } + return nil, err } + + lines := strings.Split(output, "\n") + var commits []Hash + + for _, line := range lines { + if line == "" { + continue + } + hash, err := NewHash(line) + if err != nil { + continue + } + commits = append(commits, hash) + } + return commits, nil } diff --git a/commits_between_test.go b/commits_between_test.go index fbf8d81..5e1739f 100644 --- a/commits_between_test.go +++ b/commits_between_test.go @@ -1,71 +1,69 @@ package git import ( + "os" "testing" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" "github.com/stretchr/testify/assert" ) func TestCommitsBetween(t *testing.T) { - repo, _ := git.PlainOpen("./testdata/git_tags") - testGit := &Git{repo: repo} + testGit, err := OpenGit("./testdata/git_tags") + assert.NoError(t, err) - head, err := repo.Head() + headHashStr, err := testGit.runGitCommand("rev-parse", "HEAD") + assert.NoError(t, err) + headHash, err := NewHash(headHashStr) assert.NoError(t, err) - tag, err := testGit.PreviousTag(head.Hash()) + tag, err := testGit.PreviousTag(headHash) assert.NoError(t, err) - commit, err := repo.CommitObject(tag.Hash) + commit, err := testGit.Commit(tag.Hash) assert.NoError(t, err) assert.Equal(t, "chore: first commit on master\n", commit.Message) - commits, err := testGit.CommitsBetween(head.Hash(), tag.Hash) + commits, err := testGit.CommitsBetween(headHash, tag.Hash) assert.NoError(t, err) assert.Len(t, commits, 3) - middleCommit, _ := repo.CommitObject(commits[1]) + middleCommit, _ := testGit.Commit(commits[1]) assert.Equal(t, "chore: third commit on master\n", middleCommit.Message) } func TestNoToCommit(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - head, _ := repo.Head() + lastHash := createTestHistory(t, testGit) - testGit := &Git{repo: repo} - - commits, err := testGit.CommitsBetween(head.Hash(), plumbing.Hash{}) + var emptyHash Hash + commits, err := testGit.CommitsBetween(lastHash, emptyHash) assert.Equal(t, 4, len(commits)) - commit, commitErr := repo.CommitObject(commits[0]) + commit, commitErr := testGit.Commit(commits[0]) assert.NoError(t, commitErr) - assert.Equal(t, "third commit on new branch", commit.Message) + assert.Equal(t, "third commit on new branch\n", commit.Message) assert.Equal(t, err, nil) - lastCommit, _ := repo.CommitObject(commits[3]) + lastCommit, _ := testGit.Commit(commits[3]) - assert.Equal(t, "test commit on master", lastCommit.Message) + assert.Equal(t, "test commit on master\n", lastCommit.Message) } func TestToFromEqual(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) - - head, _ := repo.Head() + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - testGit := &Git{repo: repo} + lastHash := createTestHistory(t, testGit) - commits, err := testGit.CommitsBetween(head.Hash(), head.Hash()) + commits, err := testGit.CommitsBetween(lastHash, lastHash) assert.Equal(t, 0, len(commits)) assert.NoError(t, err) diff --git a/commits_on_branch.go b/commits_on_branch.go index 33680d5..1ce0d63 100644 --- a/commits_on_branch.go +++ b/commits_on_branch.go @@ -1,39 +1,36 @@ package git import ( - "errors" - "github.com/apex/log" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" + "strings" ) -// ErrCommonCommitFound is used for identifying when the iterator has reached the common commit -var ErrCommonCommitFound = errors.New("common commit found") - // CommitsOnBranch iterates through all references and returns commit hashes on given branch. \n // Important to note is that this will provide all commits from anything the branch is connected to. func (g *Git) CommitsOnBranch( - branchCommit plumbing.Hash, -) ([]plumbing.Hash, error) { - var branchCommits []plumbing.Hash - - branchIter, err := g.repo.Log(&git.LogOptions{ - From: branchCommit, - }) + branchCommit Hash, +) ([]Hash, error) { + hashStr := branchCommit.String() + // Get all commit hashes + output, err := g.runGitCommand("log", "--format=%H", hashStr) if err != nil { return nil, err } - branchIterErr := branchIter.ForEach(func(commit *object.Commit) error { - branchCommits = append(branchCommits, commit.Hash) - return nil - }) - - if branchIterErr != nil { - log.Debugf("Stopped getting commits on branch: %v", branchIterErr) + lines := strings.Split(output, "\n") + var branchCommits []Hash + + for _, line := range lines { + if line == "" { + continue + } + hash, err := NewHash(line) + if err != nil { + log.Debugf("Failed to parse hash %s: %v", line, err) + continue + } + branchCommits = append(branchCommits, hash) } return branchCommits, nil diff --git a/commits_on_branch_simple.go b/commits_on_branch_simple.go index 12fe822..4783770 100644 --- a/commits_on_branch_simple.go +++ b/commits_on_branch_simple.go @@ -1,43 +1,54 @@ package git import ( + "strings" + "github.com/apex/log" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" ) // SimpleCommit is a slimed down commit object of just Hash and Message type SimpleCommit struct { - Hash [20]byte + Hash Hash Message string } // CommitsOnBranchSimple iterates through all references and returns simpleCommits on given branch. \n // Important to note is that this will provide all commits from anything the branch is connected to. func (g *Git) CommitsOnBranchSimple( - branchCommit plumbing.Hash, + branchCommit Hash, ) ([]SimpleCommit, error) { - var branchCommits []SimpleCommit - - branchIter, err := g.repo.Log(&git.LogOptions{ - From: branchCommit, - }) + hashStr := branchCommit.String() + // Get all commit hashes first + hashOutput, err := g.runGitCommand("log", "--format=%H", hashStr) if err != nil { return nil, err } - branchIterErr := branchIter.ForEach(func(commit *object.Commit) error { + hashLines := strings.Split(hashOutput, "\n") + var branchCommits []SimpleCommit + + for _, hashLine := range hashLines { + if hashLine == "" { + continue + } + hash, err := NewHash(hashLine) + if err != nil { + log.Debugf("Failed to parse hash %s: %v", hashLine, err) + continue + } + + // Get message for this commit + message, err := g.runGitCommand("log", "-1", "--format=%B", hashLine) + if err != nil { + log.Debugf("Failed to get message for commit %s: %v", hashLine, err) + continue + } + branchCommits = append(branchCommits, SimpleCommit{ - Hash: commit.Hash, - Message: commit.Message, + Hash: hash, + Message: strings.TrimSpace(message), }) - return nil - }) - - if branchIterErr != nil { - log.Debugf("Stopped getting commits on branch: %v", branchIterErr) } return branchCommits, nil diff --git a/commits_on_branch_simple_test.go b/commits_on_branch_simple_test.go index 66d57d0..4c2b833 100644 --- a/commits_on_branch_simple_test.go +++ b/commits_on_branch_simple_test.go @@ -1,20 +1,19 @@ package git import ( + "os" "testing" "github.com/stretchr/testify/assert" ) func TestCommitsOnBranchSimple(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - head, _ := repo.Head() + lastHash := createTestHistory(t, testGit) - testGit := &Git{repo: repo} - - commits, err := testGit.CommitsOnBranchSimple(head.Hash()) + commits, err := testGit.CommitsOnBranchSimple(lastHash) assert.Equal(t, 4, len(commits)) diff --git a/commits_on_branch_test.go b/commits_on_branch_test.go index 030bc8c..6d4c262 100644 --- a/commits_on_branch_test.go +++ b/commits_on_branch_test.go @@ -1,90 +1,29 @@ package git import ( - "log" + "os" "testing" - "time" - "github.com/go-git/go-billy/v5/memfs" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/storage/memory" "github.com/stretchr/testify/assert" ) -func createBranch(repo *git.Repository) { - headRef, _ := repo.Head() - - ref := plumbing.NewHashReference("refs/heads/my-branch", headRef.Hash()) - - storerErr := repo.Storer.SetReference(ref) - - if storerErr != nil { - log.Println(storerErr) - } - - worktree, _ := repo.Worktree() - - checkoutErr := worktree.Checkout(&git.CheckoutOptions{ - Branch: ref.Name(), - }) - - if checkoutErr != nil { - log.Println(checkoutErr) - } - -} - -func setupRepo() *git.Repository { - repo, _ := git.Init(memory.NewStorage(), memfs.New()) - - return repo -} - -func createCommit(repo *git.Repository, message string) *object.Commit { - w, _ := repo.Worktree() - - commit, _ := w.Commit(message, &git.CommitOptions{ - Author: &object.Signature{ - Name: "John Doe", - Email: "john@doe.org", - When: time.Now(), - }, - }) - - obj, _ := repo.CommitObject(commit) - - return obj -} - -func createTestHistory(repo *git.Repository) { - createCommit(repo, "test commit on master") - createBranch(repo) - createCommit(repo, "commit on new branch") - createCommit(repo, "second commit on new branch\n\n Long message") - createCommit(repo, "third commit on new branch") -} - func TestCommitsOnBranch(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) - - head, _ := repo.Head() + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - testGit := &Git{repo: repo} + lastHash := createTestHistory(t, testGit) - commits, err := testGit.CommitsOnBranch(head.Hash()) + commits, err := testGit.CommitsOnBranch(lastHash) assert.Equal(t, 4, len(commits)) - commit, commitErr := repo.CommitObject(commits[0]) + commit, commitErr := testGit.Commit(commits[0]) assert.NoError(t, commitErr) - assert.Equal(t, "third commit on new branch", commit.Message) + assert.Equal(t, "third commit on new branch\n", commit.Message) assert.Equal(t, err, nil) - lastCommit, _ := repo.CommitObject(commits[3]) + lastCommit, _ := testGit.Commit(commits[3]) - assert.Equal(t, "test commit on master", lastCommit.Message) + assert.Equal(t, "test commit on master\n", lastCommit.Message) } diff --git a/current_branch.go b/current_branch.go index 05ef098..71263bf 100644 --- a/current_branch.go +++ b/current_branch.go @@ -1,16 +1,23 @@ package git -import ( - "github.com/go-git/go-git/v5/plumbing" -) - // CurrentBranch returns the reference HEAD is at right now -func (g *Git) CurrentBranch() (*plumbing.Reference, error) { - head, err := g.repo.Head() +func (g *Git) CurrentBranch() (*Reference, error) { + // Get the symbolic ref name + refName, err := g.runGitCommand("symbolic-ref", "HEAD") + if err != nil { + return nil, err + } + + // Get the commit hash + hashStr, err := g.runGitCommand("rev-parse", "HEAD") + if err != nil { + return nil, err + } + hash, err := NewHash(hashStr) if err != nil { return nil, err } - return head, nil + return NewReference(refName, hash), nil } diff --git a/current_branch_test.go b/current_branch_test.go index 9cf3ea4..d8a15c3 100644 --- a/current_branch_test.go +++ b/current_branch_test.go @@ -1,19 +1,20 @@ package git import ( + "os" "testing" "github.com/stretchr/testify/assert" ) func TestCurrentBranch(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - testGit := &Git{repo: repo} + createTestHistory(t, testGit) currentBranch, err := testGit.CurrentBranch() assert.NoError(t, err) - assert.Equal(t, "refs/heads/my-branch", currentBranch.Name().String()) + assert.Equal(t, "refs/heads/my-branch", currentBranch.Name()) } diff --git a/current_commit.go b/current_commit.go index c1e1bbd..47ab34c 100644 --- a/current_commit.go +++ b/current_commit.go @@ -2,26 +2,16 @@ package git import ( "github.com/apex/log" - "github.com/go-git/go-git/v5/plumbing/object" ) // CurrentCommit returns the commit that HEAD is at -func (g *Git) CurrentCommit() (*object.Commit, error) { - head, err := g.repo.Head() - +func (g *Git) CurrentCommit() (*Commit, error) { + hashStr, err := g.runGitCommand("rev-parse", "HEAD") if err != nil { return nil, err } - currentCommitHash := head.Hash() - - log.Debugf("current commitHash: %v \n", currentCommitHash) - - commitObject, err := g.repo.CommitObject(currentCommitHash) - - if err != nil { - return nil, err - } + log.Debugf("current commitHash: %v \n", hashStr) - return commitObject, nil + return g.Commit(MustHash(hashStr)) } diff --git a/current_commit_test.go b/current_commit_test.go index 6a25749..2ab04ea 100644 --- a/current_commit_test.go +++ b/current_commit_test.go @@ -1,19 +1,20 @@ package git import ( + "os" "testing" "github.com/stretchr/testify/assert" ) func TestCurrentCommit(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - testGit := &Git{repo: repo} + createTestHistory(t, testGit) currentCommit, err := testGit.CurrentCommit() assert.NoError(t, err) - assert.Equal(t, "third commit on new branch", currentCommit.Message) + assert.Equal(t, "third commit on new branch\n", currentCommit.Message) } diff --git a/current_tag.go b/current_tag.go index 4a7bc9d..804cd89 100644 --- a/current_tag.go +++ b/current_tag.go @@ -13,8 +13,12 @@ var ( // CurrentTag returns a Tag if the current HEAD is on a tag func (g *Git) CurrentTag() (*Tag, error) { - head, err := g.repo.Head() + headHashStr, err := g.runGitCommand("rev-parse", "HEAD") + if err != nil { + return nil, err + } + headHash, err := NewHash(headHashStr) if err != nil { return nil, err } @@ -25,12 +29,12 @@ func (g *Git) CurrentTag() (*Tag, error) { return nil, err } - log.Debugf("head hash: %s", head.Hash()) + log.Debugf("head hash: %s", headHash.String()) for _, tag := range tags { - log.Debugf("tag: %v, hash: %v", tag.Name, tag.Hash) + log.Debugf("tag: %v, hash: %v", tag.Name, tag.Hash.String()) - if tag.Hash == head.Hash() { + if tag.Hash == headHash { return tag, nil } } diff --git a/current_tag_test.go b/current_tag_test.go index 40d4331..70cb613 100644 --- a/current_tag_test.go +++ b/current_tag_test.go @@ -1,6 +1,7 @@ package git import ( + "os" "testing" "github.com/stretchr/testify/assert" @@ -29,10 +30,10 @@ func TestCurrentTagAnnotatedHappy(t *testing.T) { } func TestCurrentTagUnhappy(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - testGit := &Git{repo: repo} + createTestHistory(t, testGit) _, err := testGit.CurrentTag() diff --git a/get_tags.go b/get_tags.go index dea594e..e5e3f80 100644 --- a/get_tags.go +++ b/get_tags.go @@ -2,54 +2,121 @@ package git import ( "sort" + "strconv" + "strings" + "time" "github.com/apex/log" - "github.com/go-git/go-git/v5" - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" ) func (g *Git) getTags() ([]*Tag, error) { - tagrefs, err := g.repo.Tags() + var tags []*Tag + // Get all tags with for-each-ref using a unique delimiter + // Use as delimiter between tags to avoid conflicts with field delimiters + refsOutput, err := g.runGitCommand("for-each-ref", "--format=%(refname)|%(objectname)|%(objecttype)|%(taggerdate:unix)", "refs/tags/") if err != nil { return nil, err } - defer tagrefs.Close() - - var tags []*Tag + // Split by tag delimiter + tagBlocks := strings.Split(refsOutput, "") - err = tagrefs.ForEach(func(t *plumbing.Reference) error { - commitDate, err := g.commitDate(t.Hash()) + for _, block := range tagBlocks { + block = strings.TrimSpace(block) + if block == "" { + continue + } - if err != nil { - log.Debugf("tag: %v ignored due to missing commit date: %v", t.Name(), err) - return nil + fields := strings.Split(block, "|") + if len(fields) < 3 { + continue } - tags = append(tags, &Tag{Name: t.Name().String(), Date: commitDate, Hash: t.Hash()}) - return nil - }) + refName := strings.TrimSpace(fields[0]) + if !strings.HasPrefix(refName, "refs/tags/") { + continue + } - if err != nil { - return nil, err - } + objectHash := strings.TrimSpace(fields[1]) + if objectHash == "" { + continue + } - tagObjects, err := g.repo.TagObjects() + objectType := strings.TrimSpace(fields[2]) + if objectType == "" { + continue + } - if err != nil { - return nil, err - } + dateStr := "" + if len(fields) > 3 { + dateStr = strings.TrimSpace(fields[3]) + } - err = tagObjects.ForEach(func(tag *object.Tag) error { - tags = append(tags, &Tag{Name: tag.Name, Date: tag.Tagger.When, Hash: tag.Target}) + var hash Hash + var commitDate time.Time + var tagName string + + if objectType == "tag" { + // Annotated tag - resolve to commit + targetHashStr, err := g.runGitCommand("rev-parse", objectHash+"^{commit}") + if err != nil { + log.Debugf("Failed to resolve annotated tag target %s: %v", refName, err) + continue + } + + hash, err = NewHash(targetHashStr) + if err != nil { + log.Debugf("Failed to parse target hash for annotated tag %s: %v", refName, err) + continue + } + + // Use tagger date if available, otherwise use commit date + if dateStr != "" { + timestamp, err := strconv.ParseInt(dateStr, 10, 64) + if err == nil { + commitDate = time.Unix(timestamp, 0) + } else { + commitDate, err = g.commitDate(hash) + if err != nil { + log.Debugf("tag: %v ignored due to missing date: %v", refName, err) + continue + } + } + } else { + commitDate, err = g.commitDate(hash) + if err != nil { + log.Debugf("tag: %v ignored due to missing commit date: %v", refName, err) + continue + } + } + + // For annotated tags, use just the tag name without refs/tags/ prefix + tagName = strings.TrimPrefix(refName, "refs/tags/") + } else { + // Lightweight tag - objectHash points directly to commit + var err error + hash, err = NewHash(objectHash) + if err != nil { + log.Debugf("Failed to parse hash for tag %s: %v", refName, err) + continue + } + + commitDate, err = g.commitDate(hash) + if err != nil { + log.Debugf("tag: %v ignored due to missing commit date: %v", refName, err) + continue + } + + // For lightweight tags, use full ref name + tagName = refName + } - return nil - }) + tags = append(tags, &Tag{Name: tagName, Date: commitDate, Hash: hash}) + } // Tags are alphabetically ordered. We need to sort them by date. - sortedTags := sortTags(g.repo, tags) + sortedTags := sortTags(tags) log.Debug("Sorted tag output: ") for _, taginstance := range sortedTags { @@ -60,7 +127,7 @@ func (g *Git) getTags() ([]*Tag, error) { } // sortTags sorts the tags according to when their parent commit happened. -func sortTags(repo *git.Repository, tags []*Tag) []*Tag { +func sortTags(tags []*Tag) []*Tag { sort.Slice(tags, func(i, j int) bool { return tags[i].Date.After(tags[j].Date) }) diff --git a/go.mod b/go.mod index a0bd52f..91b2ef6 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,6 @@ go 1.16 require ( github.com/aevea/magefiles v0.0.0-20200424121010-0004d5a7a2fe github.com/apex/log v1.9.0 - github.com/go-git/go-billy/v5 v5.3.1 - github.com/go-git/go-git/v5 v5.4.2 github.com/magefile/mage v1.15.0 github.com/stretchr/testify v1.8.1 ) diff --git a/go.sum b/go.sum index f60c3e0..7a44388 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,6 @@ -github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= -github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= -github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= -github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= -github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/aevea/magefiles v0.0.0-20200424121010-0004d5a7a2fe h1:vQ3Ixsc+ONvCRhhRssVwY5/CnlLAhEkpG9F4TWLrC0k= github.com/aevea/magefiles v0.0.0-20200424121010-0004d5a7a2fe/go.mod h1:ItpR2y3OMV2cqY57fZv61b0EgZTauhyt5UkC3tq8eqc= +github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= @@ -32,16 +26,12 @@ github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= +github.com/go-git/go-billy/v5 v5.0.0 h1:7NQHvd9FVid8VL4qVUMm8XifBK+2xCoZ2lSk0agRrHM= github.com/go-git/go-billy/v5 v5.0.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.2.0/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= -github.com/go-git/go-billy/v5 v5.3.1 h1:CPiOUAzKtMRvolEKw+bG1PLRpT7D3LIs3/3ey4Aiu34= -github.com/go-git/go-billy/v5 v5.3.1/go.mod h1:pmpqyWchKfYfrkb/UVH4otLvyi/5gJlGI4Hb3ZqZ3W0= +github.com/go-git/go-git-fixtures/v4 v4.0.1 h1:q+IFMfLx200Q3scvt2hN79JsEzy4AmBTp/pqnefH+Bc= github.com/go-git/go-git-fixtures/v4 v4.0.1/go.mod h1:m+ICp2rF3jDhFgEZ/8yziagdT1C+ZpZcrJjappBCDSw= -github.com/go-git/go-git-fixtures/v4 v4.2.1 h1:n9gGL1Ct/yIw+nfsfr8s4+sbhT+Ncu2SubfXjIWgci8= -github.com/go-git/go-git-fixtures/v4 v4.2.1/go.mod h1:K8zd3kDUAykwTdDCr+I0per6Y6vMiRR/nnVTBtavnB0= +github.com/go-git/go-git/v5 v5.0.0 h1:k5RWPm4iJwYtfWoxIJy4wJX9ON7ihPeZZYC1fLYDnpg= github.com/go-git/go-git/v5 v5.0.0/go.mod h1:oYD8y9kWsGINPFJoLdaScGCN6dlKg23blmClfZwtUVA= -github.com/go-git/go-git/v5 v5.4.2 h1:BXyZu9t0VkbiHtqrsvdq39UDhGJTl1h55VW6CSC4aY4= -github.com/go-git/go-git/v5 v5.4.2/go.mod h1:gQ1kArt6d+n+BGd+/B/I74HwRTLhth2+zti4ihgckDc= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -49,23 +39,16 @@ github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= -github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0= +github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgyYSX4TO5NpyC606/Z4SxnNYbT+WX27or6Ck= -github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -73,8 +56,6 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/magefile/mage v1.9.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= @@ -82,31 +63,27 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM= github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= @@ -118,50 +95,34 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0= github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao= github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= -github.com/xanzy/ssh-agent v0.3.0 h1:wUMzuKtKilRgBAD1sUb8gOwwRr2FGoBVumcjoOACClI= -github.com/xanzy/ssh-agent v0.3.0/go.mod h1:3s9xbODqPuuhK9JV1R321M/FlMZSBvE5aY6eAcqrDh0= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 h1:xMPOj6Pz6UipU1wXLkrtqpHbR0AVFnyPEQq/wRWz9lM= golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b h1:7mWr3k41Qtv8XlltBkDkl8LoP3mpSgBW8BUoxtEdbXg= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= -golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527 h1:uYVVQ9WP/Ds2ROhcaGPeIdVq0RIXVLwsHlnvJ+cT1So= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79 h1:RX8C8PRZc2hTIod4ds8ij+/4RQX3AqhYj3uOHmyaz4E= -golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= @@ -169,8 +130,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/latest_commit_on_branch.go b/latest_commit_on_branch.go index 106bd83..f79b469 100644 --- a/latest_commit_on_branch.go +++ b/latest_commit_on_branch.go @@ -1,23 +1,16 @@ package git -import ( - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" -) - // LatestCommitOnBranch resolves a revision and then returns the latest commit on it. -func (g *Git) LatestCommitOnBranch(desiredBranch string) (*object.Commit, error) { - desiredHash, err := g.repo.ResolveRevision(plumbing.Revision(desiredBranch)) - +func (g *Git) LatestCommitOnBranch(desiredBranch string) (*Commit, error) { + hashStr, err := g.runGitCommand("rev-parse", desiredBranch) if err != nil { return nil, err } - desiredCommit, err := g.repo.CommitObject(*desiredHash) - + hash, err := NewHash(hashStr) if err != nil { return nil, err } - return desiredCommit, nil + return g.Commit(hash) } diff --git a/latest_commit_on_branch_test.go b/latest_commit_on_branch_test.go index 9004b30..13a0df0 100644 --- a/latest_commit_on_branch_test.go +++ b/latest_commit_on_branch_test.go @@ -1,22 +1,21 @@ package git import ( + "os" "testing" "github.com/stretchr/testify/assert" ) func TestLatestCommitOnBranch(t *testing.T) { - repo := setupRepo() - createTestHistory(repo) + tmpDir, testGit := setupRepo(t) + defer os.RemoveAll(tmpDir) - head, _ := repo.Head() + createTestHistory(t, testGit) - testGit := &Git{repo: repo} - - commit, err := testGit.LatestCommitOnBranch(head.Name().String()) + commit, err := testGit.LatestCommitOnBranch("my-branch") assert.NoError(t, err) - assert.Equal(t, "third commit on new branch", commit.Message) + assert.Equal(t, "third commit on new branch\n", commit.Message) assert.Equal(t, err, nil) } diff --git a/open_git.go b/open_git.go index da00297..07a3095 100644 --- a/open_git.go +++ b/open_git.go @@ -1,21 +1,38 @@ package git import ( - "github.com/go-git/go-git/v5" + "os/exec" + "path/filepath" + "strings" ) // Git is the struct used to house all methods in use in Commitsar. type Git struct { - repo *git.Repository + Path string } // OpenGit loads Repo on path and returns a new Git struct to work with. func OpenGit(path string) (*Git, error) { - repo, repoErr := git.PlainOpen(path) + absPath, err := filepath.Abs(path) + if err != nil { + return nil, err + } - if repoErr != nil { - return nil, repoErr + // Verify it's a git repository + cmd := exec.Command("git", "-C", absPath, "rev-parse", "--git-dir") + if err := cmd.Run(); err != nil { + return nil, err } - return &Git{repo: repo}, nil + return &Git{Path: absPath}, nil +} + +// runGitCommand runs a git command in the repository directory +func (g *Git) runGitCommand(args ...string) (string, error) { + cmd := exec.Command("git", append([]string{"-C", g.Path}, args...)...) + output, err := cmd.Output() + if err != nil { + return "", err + } + return strings.TrimSpace(string(output)), nil } diff --git a/previous_tag.go b/previous_tag.go index 11a1270..8a04ada 100644 --- a/previous_tag.go +++ b/previous_tag.go @@ -4,7 +4,6 @@ import ( "errors" "github.com/apex/log" - "github.com/go-git/go-git/v5/plumbing" ) var ( @@ -14,7 +13,7 @@ var ( // PreviousTag sorts tags based on when their commit happened and returns the one previous // to the current. -func (g *Git) PreviousTag(currentHash plumbing.Hash) (*Tag, error) { +func (g *Git) PreviousTag(currentHash Hash) (*Tag, error) { tags, err := g.getTags() if err != nil { @@ -33,7 +32,7 @@ func (g *Git) PreviousTag(currentHash plumbing.Hash) (*Tag, error) { return tags[0], nil } - log.Debugf("success: previous tag found at %v", tags[1].Hash) + log.Debugf("success: previous tag found at %v", tags[1].Hash.String()) return tags[1], nil } diff --git a/previous_tag_test.go b/previous_tag_test.go index 4fe6208..f40a92a 100644 --- a/previous_tag_test.go +++ b/previous_tag_test.go @@ -3,23 +3,24 @@ package git import ( "testing" - "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" ) func TestPreviousTag(t *testing.T) { - repo, _ := git.PlainOpen("./testdata/git_tags") - testGit := &Git{repo: repo} + testGit, err := OpenGit("./testdata/git_tags") + assert.NoError(t, err) - head, err := repo.Head() + headHashStr, err := testGit.runGitCommand("rev-parse", "HEAD") + assert.NoError(t, err) + headHash, err := NewHash(headHashStr) assert.NoError(t, err) - tag, err := testGit.PreviousTag(head.Hash()) + tag, err := testGit.PreviousTag(headHash) assert.NoError(t, err) - commit, err := repo.CommitObject(tag.Hash) + commit, err := testGit.Commit(tag.Hash) assert.NoError(t, err) assert.Equal(t, "chore: first commit on master\n", commit.Message) diff --git a/tag.go b/tag.go index 160e03d..82e3b8e 100644 --- a/tag.go +++ b/tag.go @@ -2,13 +2,11 @@ package git import ( "time" - - "github.com/go-git/go-git/v5/plumbing" ) // Tag houses some common info about tags. type Tag struct { Name string - Hash plumbing.Hash + Hash Hash Date time.Time } diff --git a/test_helpers.go b/test_helpers.go new file mode 100644 index 0000000..3903392 --- /dev/null +++ b/test_helpers.go @@ -0,0 +1,89 @@ +package git + +import ( + "os" + "os/exec" + "path/filepath" + "testing" +) + +// setupRepo creates a temporary git repository for testing +func setupRepo(t *testing.T) (string, *Git) { + tmpDir, err := os.MkdirTemp("", "git-test-*") + if err != nil { + t.Fatalf("Failed to create temp dir: %v", err) + } + + // Initialize git repo + cmd := exec.Command("git", "init", tmpDir) + if err := cmd.Run(); err != nil { + os.RemoveAll(tmpDir) + t.Fatalf("Failed to init git repo: %v", err) + } + + // Configure git + exec.Command("git", "-C", tmpDir, "config", "user.name", "Test User").Run() + exec.Command("git", "-C", tmpDir, "config", "user.email", "test@example.com").Run() + + git, err := OpenGit(tmpDir) + if err != nil { + os.RemoveAll(tmpDir) + t.Fatalf("Failed to open git repo: %v", err) + } + + return tmpDir, git +} + +// createCommit creates a commit in the test repository +func createCommit(t *testing.T, git *Git, message string) Hash { + // Create a dummy file to commit + testFile := filepath.Join(git.Path, "test.txt") + err := os.WriteFile(testFile, []byte(message), 0644) + if err != nil { + t.Fatalf("Failed to write test file: %v", err) + } + + // Add and commit + exec.Command("git", "-C", git.Path, "add", "test.txt").Run() + cmd := exec.Command("git", "-C", git.Path, "commit", "-m", message) + cmd.Env = append(os.Environ(), "GIT_AUTHOR_DATE=2000-01-01T00:00:00Z", "GIT_COMMITTER_DATE=2000-01-01T00:00:00Z") + if err := cmd.Run(); err != nil { + t.Fatalf("Failed to create commit: %v", err) + } + + hashStr, err := git.runGitCommand("rev-parse", "HEAD") + if err != nil { + t.Fatalf("Failed to get commit hash: %v", err) + } + + hash, err := NewHash(hashStr) + if err != nil { + t.Fatalf("Failed to parse commit hash: %v", err) + } + + return hash +} + +// createBranch creates a branch in the test repository +func createBranch(t *testing.T, git *Git, branchName string) { + cmd := exec.Command("git", "-C", git.Path, "checkout", "-b", branchName) + if err := cmd.Run(); err != nil { + t.Fatalf("Failed to create branch: %v", err) + } +} + +// createTestHistory creates a test history with commits and branches +func createTestHistory(t *testing.T, git *Git) Hash { + // Create initial commit on master + createCommit(t, git, "test commit on master") + + // Create branch + createBranch(t, git, "my-branch") + + // Create commits on branch + createCommit(t, git, "commit on new branch") + createCommit(t, git, "second commit on new branch\n\n Long message") + lastHash := createCommit(t, git, "third commit on new branch") + + return lastHash +} diff --git a/types.go b/types.go new file mode 100644 index 0000000..83cc3df --- /dev/null +++ b/types.go @@ -0,0 +1,70 @@ +package git + +import ( + "encoding/hex" + "time" +) + +// Hash represents a git commit hash (20 bytes) - compatible with plumbing.Hash +type Hash [20]byte + +// String returns the hex string representation of the hash +func (h Hash) String() string { + return hex.EncodeToString(h[:]) +} + +// NewHash creates a Hash from a hex string +func NewHash(s string) (Hash, error) { + var h Hash + bytes, err := hex.DecodeString(s) + if err != nil { + return h, err + } + copy(h[:], bytes) + return h, nil +} + +// MustHash creates a Hash from a hex string, panicking on error +func MustHash(s string) Hash { + h, err := NewHash(s) + if err != nil { + panic(err) + } + return h +} + +// Commit represents a git commit - compatible with object.Commit +type Commit struct { + Hash Hash + Message string + Author Signature + Committer Signature +} + +// Signature represents a git signature +type Signature struct { + Name string + Email string + When time.Time +} + +// Reference represents a git reference - compatible with plumbing.Reference +type Reference struct { + name string + hash Hash +} + +// Name returns the reference name +func (r *Reference) Name() string { + return r.name +} + +// Hash returns the reference hash +func (r *Reference) Hash() Hash { + return r.hash +} + +// NewReference creates a new Reference +func NewReference(name string, hash Hash) *Reference { + return &Reference{name: name, hash: hash} +}