chore: rework --git-dir to behave like Git

Steve Moyer created

Change summary

commands/execenv/env.go          | 12 ++--
commands/execenv/loading.go      | 34 ++++++++++++--
commands/execenv/loading_test.go | 78 ++++++++++++++++++++++++++++++++++
commands/root.go                 |  6 ++
4 files changed, 117 insertions(+), 13 deletions(-)

Detailed changes

commands/execenv/env.go 🔗

@@ -19,12 +19,12 @@ const gitBugNamespace = "git-bug"
 
 // Env is the environment of a command
 type Env struct {
-	Repo     repository.ClockedRepo
-	Backend  *cache.RepoCache
-	In       In
-	Out      Out
-	Err      Out
-	RepoPath []string
+	Repo    repository.ClockedRepo
+	Backend *cache.RepoCache
+	In      In
+	Out     Out
+	Err     Out
+	GitDir  string
 }
 
 func NewEnv() *Env {

commands/execenv/loading.go 🔗

@@ -1,9 +1,9 @@
 package execenv
 
 import (
+	"errors"
 	"fmt"
 	"os"
-	"path/filepath"
 
 	"github.com/spf13/cobra"
 	"github.com/vbauerster/mpb/v8"
@@ -173,14 +173,36 @@ func CacheBuildProgressBar(env *Env, events chan cache.BuildEvent) error {
 	return nil
 }
 
+var errNotDirectory = errors.New("path must be a directory for --git-dir or GIT_DIR")
+
 func getRepoPath(env *Env) (string, error) {
-	if len(env.RepoPath) > 0 {
-		return filepath.Join(env.RepoPath...), nil
+	var path string
+
+	if gitDir := os.Getenv("GIT_DIR"); gitDir != "" {
+		path = gitDir
+	}
+
+	if path == "" && env.GitDir != "" {
+		path = env.GitDir
+	}
+
+	if path == "" {
+		wd, err := os.Getwd()
+		if err != nil {
+			return "", fmt.Errorf("unable to get the current working directory: %q", err)
+		}
+
+		path = wd
 	}
 
-	cwd, err := os.Getwd()
+	fi, err := os.Stat(path)
 	if err != nil {
-		return "", fmt.Errorf("unable to get the current working directory: %q", err)
+		return "", err
 	}
-	return cwd, nil
+
+	if !fi.IsDir() {
+		return "", errNotDirectory
+	}
+
+	return path, nil
 }

commands/execenv/loading_test.go 🔗

@@ -0,0 +1,78 @@
+package execenv
+
+import (
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// ***** WARNING ***** - due to the use of testing with an environment
+//
+//	variable, do NOT use t.Parallel() with these
+//	test cases.
+func TestGetRepoPath(t *testing.T) {
+	t.Run("no Git path provided (use WD)", func(t *testing.T) {
+		// WD during tests is the directory containing the test file
+		path, err := getRepoPath(&Env{})
+		require.NoError(t, err)
+		assert.True(t, strings.HasSuffix(path, filepath.Join("commands", "execenv")))
+	})
+
+	t.Run("flag provided", func(t *testing.T) {
+		gitDir := filepath.Join(string(filepath.Separator), "tmp")
+
+		path, err := getRepoPath(&Env{
+			GitDir: gitDir,
+		})
+		require.NoError(t, err)
+		assert.Equal(t, gitDir, path)
+	})
+
+	t.Run("environment variable provided", func(t *testing.T) {
+		gitDir := filepath.Join(string(filepath.Separator), "tmp")
+		t.Setenv("GIT_DIR", gitDir)
+
+		path, err := getRepoPath(&Env{})
+		require.NoError(t, err)
+		assert.Equal(t, gitDir, path)
+	})
+
+	t.Run("GIT_DIR supercedes --git-dir", func(t *testing.T) {
+		homeDir, err := os.UserHomeDir()
+		require.NoError(t, err)
+
+		tmpDir := filepath.Join(string(filepath.Separator), "tmp")
+		t.Setenv("GIT_DIR", tmpDir)
+
+		path, err := getRepoPath(&Env{
+			GitDir: homeDir,
+		})
+		require.NoError(t, err)
+		assert.Equal(t, tmpDir, path)
+
+	})
+
+	t.Run("path does not exist", func(t *testing.T) {
+		_, err := getRepoPath(&Env{
+			GitDir: filepath.Join("/", "tmp", "does-not-exist"),
+		})
+		require.ErrorIs(t, err, os.ErrNotExist)
+	})
+
+	t.Run("path is not a directory", func(t *testing.T) {
+		gitDir := filepath.Join(os.TempDir(), "plain-file")
+
+		f, err := os.Create(gitDir)
+		require.NoError(t, err)
+		require.NoError(t, f.Close())
+
+		_, err = getRepoPath(&Env{
+			GitDir: gitDir,
+		})
+		require.ErrorIs(t, err, errNotDirectory)
+	})
+}

commands/root.go 🔗

@@ -21,6 +21,10 @@ git-bug use git objects to store the bug tracking separated from the files
 history. As bugs are regular git objects, they can be pushed and pulled from/to
 the same git remote you are already using to collaborate with other people.
 
+By default, git-bug manages bugs and users in the Git repository containing
+the current working directory.  This behavior can be changed using the --git-dir
+flag described below, or by setting the GIT-DIR environment variable, which
+takes precedence over the CLI flag.
 `,
 
 		PersistentPreRun: func(cmd *cobra.Command, args []string) {
@@ -55,7 +59,7 @@ the same git remote you are already using to collaborate with other people.
 	}
 
 	env := execenv.NewEnv()
-	cmd.PersistentFlags().StringArrayVarP(&env.RepoPath, "repo-path", "C", []string{}, `Run as if git-bug was started in <path> instead of the current working directory.`)
+	cmd.PersistentFlags().StringVar(&env.GitDir, "git-dir", "", `run as if git-bug was started in <path>`)
 
 	addCmdWithGroup(bugcmd.NewBugCommand(env), entityGroup)
 	addCmdWithGroup(usercmd.NewUserCommand(env), entityGroup)