diff --git a/cmd/root.go b/cmd/root.go index d1ad666..3d76c08 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -10,11 +10,10 @@ import ( "time" "github.com/choffmeister/git-describe-semver/internal" - "github.com/go-git/go-git/v5" ) func run(dir string, opts internal.GenerateVersionOptions) (*string, error) { - repo, err := git.PlainOpen(dir) + repo, err := internal.OpenRepository(dir) if err != nil { return nil, fmt.Errorf("unable to open git repository: %v", err) } diff --git a/internal/git.go b/internal/git.go index 43fcfe7..51509bf 100644 --- a/internal/git.go +++ b/internal/git.go @@ -1,13 +1,25 @@ package internal import ( + "errors" "fmt" + "os" + "path/filepath" + "strings" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" ) +const ( + // Prefix found in .git files that point to another location + GitDirPrefix = "gitdir: " + + GitDirName = ".git" + CommonDirName = "commondir" +) + // GitTagMap ... func GitTagMap(repo git.Repository) (*map[string]string, error) { iter, err := repo.Tags() @@ -103,3 +115,56 @@ func GitDescribe(repo git.Repository) (*string, *int, *string, error) { tagName := (*tags)[tagHash] return &tagName, &counter, &headHash, nil } + +func OpenRepository(dir string) (*git.Repository, error) { + gitDir, err := FindGitDir(dir) + if err != nil { + return nil, err + } + enableCommonDir, err := shouldEnableCommondDir(gitDir) + if err != nil { + return nil, err + } + openOpts := &git.PlainOpenOptions{EnableDotGitCommonDir: enableCommonDir} + return git.PlainOpenWithOptions(dir, openOpts) +} + +func shouldEnableCommondDir(gitDir string) (bool, error) { + cdPath := filepath.Join(gitDir, CommonDirName) + st, err := os.Stat(cdPath) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return false, nil + } + return false, err + } + if st.IsDir() { + return false, fmt.Errorf("expected to be a file, not directory: %s", cdPath) + } + return true, nil +} + +func FindGitDir(dir string) (string, error) { + gitDirPath := filepath.Join(dir, GitDirName) + st, err := os.Stat(gitDirPath) + if err != nil { + return "", err + } + if st.IsDir() { + return gitDirPath, nil + } + // It is a file, read the contents + contents, err := os.ReadFile(gitDirPath) + if err != nil { + return "", err + } + + line := string(contents) + if !strings.HasPrefix(line, GitDirPrefix) { + return "", fmt.Errorf(".git file has no %s prefix", GitDirPrefix) + } + + gitdir := strings.Split(line[len(GitDirPrefix):], "\n")[0] + gitdir = strings.TrimSpace(gitdir) + return gitdir, nil +} diff --git a/internal/git_test.go b/internal/git_test.go index 67fb96f..8fd4b6c 100644 --- a/internal/git_test.go +++ b/internal/git_test.go @@ -2,6 +2,8 @@ package internal import ( "io/ioutil" + "os" + "path/filepath" "testing" "github.com/go-git/go-git/v5" @@ -127,3 +129,81 @@ func TestGitDescribeWithBranch(t *testing.T) { repo.CreateTag("v2.0.0", commit3, nil) test("v2.0.0", 1, commit4.String()) } + +func setUpDotGitDirTest(assert *assert.Assertions) (string, string) { + testDir, err := os.MkdirTemp("", "test") + assert.NoError(err, "failed to create temp dir") + + gitDirPath := filepath.Join(testDir, GitDirName) + err = os.Mkdir(gitDirPath, 0750) + assert.NoError(err, "failed to create git dir") + + return testDir, gitDirPath +} + +func setUpDotGitFileTest(assert *assert.Assertions) (string, string, string) { + testDir, err := os.MkdirTemp("", "test") + assert.NoError(err, "failed to create temp dir") + + actualDotGitPath := filepath.Join(testDir, "actual") + err = os.Mkdir(actualDotGitPath, 0750) + assert.NoError(err, "failed to create actual git dir") + + wtPath := filepath.Join(testDir, "my_worktree") + err = os.Mkdir(wtPath, 0750) + assert.NoError(err, "failed to create worktree dir") + + wtDotGitPath := filepath.Join(wtPath, GitDirName) + contents := GitDirPrefix + actualDotGitPath + err = os.WriteFile(wtDotGitPath, []byte(contents), 0666) + assert.NoError(err, "failed to write git dir file in worktree") + + return testDir, actualDotGitPath, wtPath +} + +func TestFindGitDir(t *testing.T) { + t.Run(".git is a directory", func(t *testing.T) { + assert := assert.New(t) + testDir, gitDirPath := setUpDotGitDirTest(assert) + defer os.RemoveAll(testDir) + + result, err := FindGitDir(testDir) + assert.NoError(err, "failed to find git dir") + assert.Equal(gitDirPath, result) + }) + t.Run(".git is a file pointing to another directory", func(t *testing.T) { + assert := assert.New(t) + testDir, actualDotGitPath, wtPath := setUpDotGitFileTest(assert) + defer os.RemoveAll(testDir) + + result, err := FindGitDir(wtPath) + assert.NoError(err, "failed to find git dir in worktree") + assert.Equal(actualDotGitPath, result) + }) +} + +func TestShouldEnableCommonDir(t *testing.T) { + t.Run(".git is a directory", func(t *testing.T) { + assert := assert.New(t) + testDir, gitDirPath := setUpDotGitDirTest(assert) + defer os.RemoveAll(testDir) + + result, err := shouldEnableCommondDir(gitDirPath) + assert.NoError(err, "failed evaluating whether to enable commond dir") + assert.False(result) + }) + t.Run(".git is a file pointing to another directory", func(t *testing.T) { + assert := assert.New(t) + testDir, actualDotGitPath, _ := setUpDotGitFileTest(assert) + defer os.RemoveAll(testDir) + + cdPath := filepath.Join(actualDotGitPath, CommonDirName) + contents := "../my_worktree" + err := os.WriteFile(cdPath, []byte(contents), 0666) + assert.NoError(err, "failed writing commondir file") + + result, err := shouldEnableCommondDir(actualDotGitPath) + assert.NoError(err, "failed evaluating whether to enable commond dir") + assert.True(result) + }) +}