mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Fix several problems with repos whose git dir lives outside the working tree (#5910)
Lazygit assumes a repo can be found again from its working directory: it chdirs there and lets git rediscover the git dir from `<worktree>/.git`. That holds for an ordinary repo and breaks for every setup where the git dir lives somewhere else, which is where these bugs come from. Opening such a repo worked at all only when `--git-dir` happened to leave `GIT_DIR` in the environment for every command to inherit — which is also why entering a submodule, which has to clear it, broke the way back out. Three reported problems: - **`core.worktree` (#5895).** A repo whose work tree is elsewhere panicked on startup with `fatal: not a git repository`: we chdir'd into a work tree with no `.git` in it and every command after that was lost. We now work out at startup whether git can find the repo from its work tree, and when it can't we put `GIT_DIR`/`GIT_WORK_TREE` on every command the repo's command builder produces — as well as in the process environment, for subprocesses that don't come through the builder. Nothing is set for the repos git can find on its own, which is nearly all of them. - **Escaping a submodule of a dotfile repo (#1118).** The repo-path stack we push the superproject onto only held its path, and for a repo opened with `--git-dir`/`--work-tree` the path leads nowhere. Escaping failed with `not a git repository`, or, if some unrelated repo happened to lie above the work tree, quietly switched to that one instead. The stack now carries the environment as well, taken from the repo paths rather than from the process env, so it also covers a repo whose location lazygit worked out itself. - **Opening a directory that holds a bare repo (#5469, #5681).** `git rev-parse --show-toplevel` is fatal when there's no work tree, so we never got an answer at all for a bare repo: `IsBareRepo()` could never come out true, and lazygit either died with a stack trace or decided we weren't in a repository. We now ask again without `--show-toplevel` when the first query fails, and the existing "open most recent repo?" prompt does its job. Some related things that turned up on the way: - **A submodule no longer looks like a linked worktree.** `git worktree list` reports the main worktree as the common git dir with a trailing `/.git` removed, which is not the working tree when the git dir doesn't live inside it. Comparing that against the working tree path matched nothing, so inside a submodule the status bar claimed we were in a linked worktree named after the submodule, the worktrees panel listed it as not current, and its branch got a "checked out elsewhere" marker. Worktrees are now identified by their git dir, which names them unambiguously. - **Commands aimed at another repo no longer resolve against ours.** With `GIT_DIR` set, `git -C mysub log -1` reports the *superproject's* commit, silently. So opening lazygit with `--git-dir`/`--work-tree` quietly broke resolving submodule conflicts, stashing and resetting a submodule, and detaching another worktree. - **Starting lazygit in a repo's `.git` dir opens the repo.** It used to tell you that you were in a bare repo, which you weren't — the work tree was one directory up. git's own convention is that a git dir called `.git` belongs to the directory holding it, so we look there. (A linked worktree's or a submodule's git dir isn't called `.git`, and nothing we look at says where their work tree is, so those still get the prompt.) - **`RepoPath()`** is documented to be the work tree when we're in the main worktree, but was derived from the git dir's location, which is only the same thing when the git dir is inside the work tree. This fixes the repo name shown in the status panel for split setups. Fixes #1118 Fixes #5469 Fixes #5681 Fixes #5736 Fixes #5895
This commit is contained in:
commit
bf5829af3f
|
|
@ -16,7 +16,7 @@ type errorMapping struct {
|
|||
func knownError(tr *i18n.TranslationSet, err error) (string, bool) {
|
||||
errorMessage := err.Error()
|
||||
|
||||
knownErrorMessages := []string{minGitVersionErrorMessage(tr)}
|
||||
knownErrorMessages := []string{minGitVersionErrorMessage(tr), tr.BareRepoNotSupported}
|
||||
|
||||
if lo.Contains(knownErrorMessages, errorMessage) {
|
||||
return errorMessage, true
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import (
|
|||
"github.com/jesseduffield/lazygit/pkg/commands/patch"
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
|
|
@ -67,11 +68,24 @@ func NewGitCommand(
|
|||
return nil, errors.Errorf("Error getting repo paths: %v", err)
|
||||
}
|
||||
|
||||
// A bare repo has no worktree for us to work in. Callers that can offer the
|
||||
// user something better (app.setupRepo) check for this first; getting here
|
||||
// means nobody could, e.g. because --git-dir was pointed at a bare repo.
|
||||
if repoPaths.IsBareRepo() {
|
||||
return nil, errors.New(cmn.Tr.BareRepoNotSupported)
|
||||
}
|
||||
|
||||
err = os.Chdir(repoPaths.WorktreePath())
|
||||
if err != nil {
|
||||
return nil, utils.WrapError(err)
|
||||
}
|
||||
|
||||
// Everything we run through the command builder gets told where the repo is
|
||||
// by the builder itself, but subprocesses don't go through it: user-defined
|
||||
// custom commands, an editor, and the lazygit we re-enter as git's sequence
|
||||
// editor during a rebase. Put it in the process env for those.
|
||||
env.SetGitLocationEnvVars(repoPaths.GitLocationEnvVars())
|
||||
|
||||
// Pin the config reads to the repo directory like all other git commands
|
||||
// (see NewGitCmdObjBuilder); the config commands run outside that builder.
|
||||
gitConfig.SetDir(repoPaths.WorktreePath())
|
||||
|
|
@ -94,7 +108,7 @@ func NewGitCommandAux(
|
|||
repoPaths *git_commands.RepoPaths,
|
||||
diffRendererConfigManager *config.DiffRendererConfigManager,
|
||||
) *GitCommand {
|
||||
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd, repoPaths.WorktreePath())
|
||||
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd, repoPaths.WorktreePath(), repoPaths.GitLocationEnvVars())
|
||||
|
||||
// here we're doing a bunch of dependency injection for each of our commands structs.
|
||||
// This is admittedly messy, but allows us to test each command struct in isolation,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,13 @@ type gitCmdObjBuilder struct {
|
|||
// the old builder) must keep running its commands against the repo it
|
||||
// started in, not whichever one the process has since moved to.
|
||||
repoDir string
|
||||
|
||||
// The env vars every command we produce gets: the optional-locks one below,
|
||||
// plus the repo's git location if it has one (see
|
||||
// RepoPaths.GitLocationEnvVars). Those are in the process env too, but for
|
||||
// the same reason as repoDir we don't rely on that: the process env belongs
|
||||
// to whichever repo lazygit has since switched to.
|
||||
envVars []string
|
||||
}
|
||||
|
||||
var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{}
|
||||
|
|
@ -30,7 +37,7 @@ var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{}
|
|||
// only the foreground files refresh) opt back in via CmdObj.RemoveEnvVar.
|
||||
var defaultEnvVar = git_commands.OptionalLocksEnvVar + "=0"
|
||||
|
||||
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder, repoDir string) *gitCmdObjBuilder {
|
||||
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder, repoDir string, gitLocationEnvVars []string) *gitCmdObjBuilder {
|
||||
// the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase)
|
||||
updatedBuilder := innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner {
|
||||
return &gitCmdObjRunner{
|
||||
|
|
@ -43,15 +50,16 @@ func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuild
|
|||
return &gitCmdObjBuilder{
|
||||
innerBuilder: updatedBuilder,
|
||||
repoDir: repoDir,
|
||||
envVars: append([]string{defaultEnvVar}, gitLocationEnvVars...),
|
||||
}
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) New(args []string) *oscommands.CmdObj {
|
||||
return self.innerBuilder.New(args).AddEnvVars(defaultEnvVar).SetWd(self.repoDir)
|
||||
return self.innerBuilder.New(args).AddEnvVars(self.envVars...).SetWd(self.repoDir)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) NewShell(cmdStr string, shellFunctionsFile string) *oscommands.CmdObj {
|
||||
return self.innerBuilder.NewShell(cmdStr, shellFunctionsFile).AddEnvVars(defaultEnvVar).SetWd(self.repoDir)
|
||||
return self.innerBuilder.NewShell(cmdStr, shellFunctionsFile).AddEnvVars(self.envVars...).SetWd(self.repoDir)
|
||||
}
|
||||
|
||||
func (self *gitCmdObjBuilder) Quote(str string) string {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ func TestGitCmdObjBuilderDisablesOptionalLocksByDefault(t *testing.T) {
|
|||
utils.NewDummyLog(),
|
||||
oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
|
||||
"/path/to/repo",
|
||||
nil,
|
||||
)
|
||||
|
||||
assert.Contains(t, builder.New([]string{"git", "status"}).GetEnvVars(), git_commands.OptionalLocksEnvVar+"=0")
|
||||
|
|
@ -34,8 +35,27 @@ func TestGitCmdObjBuilderPinsCommandsToRepoDir(t *testing.T) {
|
|||
utils.NewDummyLog(),
|
||||
oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
|
||||
"/path/to/repo",
|
||||
nil,
|
||||
)
|
||||
|
||||
assert.Equal(t, "/path/to/repo", builder.New([]string{"git", "status"}).GetCmd().Dir)
|
||||
assert.Equal(t, "/path/to/repo", builder.NewShell("git status", "").GetCmd().Dir)
|
||||
}
|
||||
|
||||
// A repo whose git dir isn't in its worktree can't be found by running a
|
||||
// command there, so the builder has to tell every command where it is; see
|
||||
// RepoPaths.GitLocationEnvVars. The process env says the same thing, but only
|
||||
// for the repo lazygit is in right now, which isn't necessarily this one.
|
||||
func TestGitCmdObjBuilderPinsCommandsToGitLocation(t *testing.T) {
|
||||
builder := NewGitCmdObjBuilder(
|
||||
utils.NewDummyLog(),
|
||||
oscommands.NewDummyCmdObjBuilder(oscommands.NewFakeRunner(t)),
|
||||
"/path/to/worktree",
|
||||
[]string{"GIT_DIR=/path/to/repo/.git", "GIT_WORK_TREE=/path/to/worktree"},
|
||||
)
|
||||
|
||||
assert.Subset(t, builder.New([]string{"git", "status"}).GetEnvVars(),
|
||||
[]string{"GIT_DIR=/path/to/repo/.git", "GIT_WORK_TREE=/path/to/worktree"})
|
||||
assert.Subset(t, builder.NewShell("git status", "").GetEnvVars(),
|
||||
[]string{"GIT_DIR=/path/to/repo/.git", "GIT_WORK_TREE=/path/to/worktree"})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
)
|
||||
|
||||
// OptionalLocksEnvVar is the name of the environment variable that tells git
|
||||
|
|
@ -18,6 +19,15 @@ import (
|
|||
// that opts back in is the foreground files refresh; see FileLoader.gitStatus.
|
||||
const OptionalLocksEnvVar = "GIT_OPTIONAL_LOCKS"
|
||||
|
||||
// forOtherRepo prepares a command that operates on a repo other than the one
|
||||
// we have open — a submodule, or another worktree. GIT_DIR and GIT_WORK_TREE
|
||||
// say where our repo is, and every command we run inherits them, so a command
|
||||
// pointed at a different repo would be resolved against ours instead: `git -C
|
||||
// <submodule> log` would silently log the superproject's commits.
|
||||
func forOtherRepo(cmdObj *oscommands.CmdObj) *oscommands.CmdObj {
|
||||
return cmdObj.RemoveEnvVar(env.GitDirEnvVar).RemoveEnvVar(env.GitWorkTreeEnvVar)
|
||||
}
|
||||
|
||||
// convenience struct for building git commands. Especially useful when
|
||||
// including conditional args
|
||||
type GitCommandBuilder struct {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
|
@ -19,10 +20,12 @@ type RepoPaths struct {
|
|||
repoGitDirPath string
|
||||
repoName string
|
||||
isBareRepo bool
|
||||
gitLocationEnvVars []string
|
||||
}
|
||||
|
||||
// Path to the current worktree. If we're in the main worktree, this will
|
||||
// be the same as RepoPath()
|
||||
// be the same as RepoPath(). It is empty for a bare repo, which has no
|
||||
// worktree at all.
|
||||
func (self *RepoPaths) WorktreePath() string {
|
||||
return self.worktreePath
|
||||
}
|
||||
|
|
@ -53,10 +56,33 @@ func (self *RepoPaths) RepoName() string {
|
|||
return self.repoName
|
||||
}
|
||||
|
||||
// Whether we found no worktree, so that there is nothing for lazygit to show.
|
||||
// Note that this isn't quite git's core.bare: a repo that calls itself non-bare
|
||||
// but whose worktree we couldn't find counts as bare for us too. Concretely,
|
||||
// this is true when we're in
|
||||
//
|
||||
// - a genuinely bare repo;
|
||||
// - the git dir of a linked worktree (.git/worktrees/x), whose worktree is
|
||||
// recorded but not somewhere we look;
|
||||
// - a repo that keeps its worktree somewhere only GIT_WORK_TREE knows, such
|
||||
// as a vcsh-style dotfiles repo that hasn't been given core.worktree.
|
||||
//
|
||||
// The .git dir of an ordinary repo is not one of them: GetRepoPathsForDir
|
||||
// notices the worktree holding it and hands back that repo instead.
|
||||
func (self *RepoPaths) IsBareRepo() bool {
|
||||
return self.isBareRepo
|
||||
}
|
||||
|
||||
// The environment that tells git where this repo is, as "NAME=value" entries.
|
||||
// It is empty for the vast majority of repos, which git finds for itself by
|
||||
// looking for a .git in the directory a command runs in. It is only non-empty
|
||||
// when that doesn't work — when the git dir lives somewhere else entirely,
|
||||
// because of core.worktree or --work-tree — and then every command addressing
|
||||
// the repo has to carry it.
|
||||
func (self *RepoPaths) GitLocationEnvVars() []string {
|
||||
return self.gitLocationEnvVars
|
||||
}
|
||||
|
||||
// Returns the repo paths for a typical repo
|
||||
func MockRepoPaths(currentPath string) *RepoPaths {
|
||||
return &RepoPaths{
|
||||
|
|
@ -84,26 +110,76 @@ func GetRepoPathsForDir(
|
|||
dir string,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) (*RepoPaths, error) {
|
||||
gitDirOutput, err := callGitRevParseWithDir(cmd, dir, "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree")
|
||||
repoPaths, err := repoPathsForDir(dir, cmd)
|
||||
if err != nil || !repoPaths.IsBareRepo() {
|
||||
return repoPaths, err
|
||||
}
|
||||
|
||||
// We're in a git dir rather than in a working tree, which usually just means
|
||||
// somebody ran lazygit in the .git of an ordinary repo. git's convention is
|
||||
// that a git dir called .git belongs to the directory holding it, so look
|
||||
// there: if that is a working tree, it is the repo we were asked about, and
|
||||
// there's no reason to make the user go up a directory and try again.
|
||||
//
|
||||
// The git dirs that aren't called .git keep the paths we have. A linked
|
||||
// worktree's (.git/worktrees/x) and a submodule's (.git/modules/x) do have a
|
||||
// working tree, but only the directory holding a .git tells us where, so we
|
||||
// would be guessing. A bare repo's has none to find.
|
||||
if filepath.Base(repoPaths.WorktreeGitDirPath()) != ".git" {
|
||||
return repoPaths, nil
|
||||
}
|
||||
|
||||
pathsFromWorkTree, err := repoPathsForDir(filepath.Dir(repoPaths.WorktreeGitDirPath()), cmd)
|
||||
if err != nil || pathsFromWorkTree.IsBareRepo() {
|
||||
return repoPaths, nil
|
||||
}
|
||||
return pathsFromWorkTree, nil
|
||||
}
|
||||
|
||||
// repoPathsForDir asks git about the repo at dir, and reports a bare repo when
|
||||
// there is no working tree there. Unlike GetRepoPathsForDir it never looks
|
||||
// anywhere but dir, which is what keeps that one from going round in circles.
|
||||
func repoPathsForDir(
|
||||
dir string,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) (*RepoPaths, error) {
|
||||
gitDirOutput, err := callGitRevParseWithDir(cmd, dir, "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// --show-toplevel is the only one of these that needs a work tree, and
|
||||
// git makes it fatal when there isn't one. So this may just mean we're in
|
||||
// a repo that has no work tree.
|
||||
return getBareRepoPathsForDir(dir, cmd, err)
|
||||
}
|
||||
|
||||
gitDirResults := strings.Split(utils.NormalizeLinefeeds(gitDirOutput), "\n")
|
||||
worktreePath := gitDirResults[0]
|
||||
worktreeGitDirPath := gitDirResults[1]
|
||||
repoGitDirPath := gitDirResults[2]
|
||||
isBareRepo := gitDirResults[3] == "true"
|
||||
|
||||
// If we're in a submodule, --show-superproject-working-tree will return
|
||||
// a value, meaning gitDirResults will be length 5. In that case
|
||||
// return the worktree path as the repoPath. Otherwise we're in a
|
||||
// normal repo or a worktree so return the parent of the git common
|
||||
// dir (repoGitDirPath)
|
||||
isSubmodule := len(gitDirResults) == 5
|
||||
// A worktree that has the repo's common git dir to itself is the repo's main
|
||||
// worktree, so it is the repoPath. That holds for a submodule as well: its
|
||||
// git dir lives under the superproject's .git/modules, but it is still the
|
||||
// submodule's own common dir.
|
||||
isMainWorktree := worktreeGitDirPath == repoGitDirPath
|
||||
|
||||
// If we're in a submodule, --show-superproject-working-tree will return a
|
||||
// value, meaning gitDirResults will be length 4. That only tells us anything
|
||||
// new for a linked worktree of a submodule, which isMainWorktree misses.
|
||||
isSubmodule := len(gitDirResults) == 4
|
||||
|
||||
// Otherwise we're in a linked worktree, and the repoPath is the repo's main
|
||||
// worktree. git won't tell us where that is: `git worktree list` reports it
|
||||
// as the common git dir with a trailing "/.git" removed, which is this same
|
||||
// derivation. So take the directory holding the common git dir. That is the
|
||||
// main worktree of an ordinary repo, and of a bare one it is the directory
|
||||
// its worktrees live in. It is not the main worktree of a repo that moved
|
||||
// that elsewhere with core.worktree; there we end up naming the git dir's
|
||||
// directory, which means that the repo name we display in the status panel
|
||||
// isn't correct, and we start looking for .lazygit.yml in the wrong place.
|
||||
// Both of those are not severe enough to justify the extra git call to get
|
||||
// the real main worktree, so we accept this for this rather niche use case.
|
||||
var repoPath string
|
||||
if isSubmodule {
|
||||
if isMainWorktree || isSubmodule {
|
||||
repoPath = worktreePath
|
||||
} else {
|
||||
repoPath = filepath.Dir(repoGitDirPath)
|
||||
|
|
@ -116,21 +192,110 @@ func GetRepoPathsForDir(
|
|||
repoPath: repoPath,
|
||||
repoGitDirPath: repoGitDirPath,
|
||||
repoName: repoName,
|
||||
isBareRepo: isBareRepo,
|
||||
isBareRepo: false,
|
||||
gitLocationEnvVars: gitLocationEnvVars(cmd, worktreePath, worktreeGitDirPath),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// gitLocationEnvVars works out whether git can find the repo by itself when a
|
||||
// command runs in its worktree, and if it can't, returns the environment that
|
||||
// tells git where it is. See RepoPaths.GitLocationEnvVars.
|
||||
func gitLocationEnvVars(
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
worktreePath string,
|
||||
worktreeGitDirPath string,
|
||||
) []string {
|
||||
// The ordinary repo, where the git dir sits in the worktree. Both paths are
|
||||
// git's own answers from the same invocation, so they are spelled alike and
|
||||
// comparing them is safe.
|
||||
if worktreeGitDirPath == filepath.Join(worktreePath, ".git") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// A linked worktree or a submodule instead has a .git file naming its git
|
||||
// dir, and git follows that just as happily. We could read the file, but the
|
||||
// path in it may well name the same directory differently than git did
|
||||
// above, so ask git to resolve it — from the worktree and nothing else.
|
||||
discoveredGitDirPath, err := callGitRevParseInOtherRepo(cmd, worktreePath, "--absolute-git-dir")
|
||||
if err == nil && discoveredGitDirPath == worktreeGitDirPath {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []string{
|
||||
env.GitDirEnvVar + "=" + worktreeGitDirPath,
|
||||
env.GitWorkTreeEnvVar + "=" + worktreePath,
|
||||
}
|
||||
}
|
||||
|
||||
// getBareRepoPathsForDir is the fallback for when we couldn't ask git for the
|
||||
// work tree. Everything but --show-toplevel works fine without one, so if the
|
||||
// remaining queries succeed we are in a bare repo, and we return what we know
|
||||
// about it with an empty worktreePath. If they fail too we simply aren't in a
|
||||
// repo, and the caller's original error says so better than ours would.
|
||||
func getBareRepoPathsForDir(
|
||||
dir string,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
errWithWorktree error,
|
||||
) (*RepoPaths, error) {
|
||||
output, err := callGitRevParseWithDir(cmd, dir, "--absolute-git-dir", "--git-common-dir")
|
||||
if err != nil {
|
||||
return nil, errWithWorktree
|
||||
}
|
||||
|
||||
results := strings.Split(utils.NormalizeLinefeeds(output), "\n")
|
||||
repoGitDirPath := results[1]
|
||||
// A bare repo has no worktree, and so no repo path in the sense the caller
|
||||
// with a worktree means. It doesn't matter much what we say here, because
|
||||
// nobody reads it: whoever is handed a bare repo either offers to open a
|
||||
// recent one instead (app.setupRepo) or is turned away by NewGitCommand. The
|
||||
// directory holding the git dir is the nearest thing there is to a repo
|
||||
// path.
|
||||
repoPath := filepath.Dir(repoGitDirPath)
|
||||
|
||||
return &RepoPaths{
|
||||
worktreePath: "",
|
||||
worktreeGitDirPath: results[0],
|
||||
repoPath: repoPath,
|
||||
repoGitDirPath: repoGitDirPath,
|
||||
repoName: filepath.Base(repoPath),
|
||||
isBareRepo: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Asks git about the repo at dir. This is how we find our own repo, so it has
|
||||
// to be answered the way git itself would answer it there, GIT_DIR and
|
||||
// GIT_WORK_TREE included.
|
||||
func callGitRevParseWithDir(
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
dir string,
|
||||
gitRevArgs ...string,
|
||||
) (string, error) {
|
||||
return runGitRevParse(newGitRevParseCmd(cmd, dir, gitRevArgs...))
|
||||
}
|
||||
|
||||
// Asks git about a repo that isn't the one we have open; see forOtherRepo.
|
||||
func callGitRevParseInOtherRepo(
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
dir string,
|
||||
gitRevArgs ...string,
|
||||
) (string, error) {
|
||||
return runGitRevParse(forOtherRepo(newGitRevParseCmd(cmd, dir, gitRevArgs...)))
|
||||
}
|
||||
|
||||
func newGitRevParseCmd(
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
dir string,
|
||||
gitRevArgs ...string,
|
||||
) *oscommands.CmdObj {
|
||||
gitRevParse := NewGitCmd("rev-parse").Arg("--path-format=absolute").Arg(gitRevArgs...)
|
||||
if dir != "" {
|
||||
gitRevParse.Dir(dir)
|
||||
}
|
||||
|
||||
gitCmd := cmd.New(gitRevParse.ToArgv()).DontLog()
|
||||
return cmd.New(gitRevParse.ToArgv()).DontLog()
|
||||
}
|
||||
|
||||
func runGitRevParse(gitCmd *oscommands.CmdObj) (string, error) {
|
||||
res, err := gitCmd.RunWithOutput()
|
||||
if err != nil {
|
||||
return "", errors.Errorf("'%s' failed: %v", gitCmd.ToString(), err)
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
`C:\path\to\repo\.git`,
|
||||
// --git-common-dir
|
||||
`C:\path\to\repo\.git`,
|
||||
// --is-bare-repository
|
||||
"false",
|
||||
// --show-superproject-working-tree
|
||||
}, []string{
|
||||
// --show-toplevel
|
||||
|
|
@ -48,12 +46,10 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
"/path/to/repo/.git",
|
||||
// --git-common-dir
|
||||
"/path/to/repo/.git",
|
||||
// --is-bare-repository
|
||||
"false",
|
||||
// --show-superproject-working-tree
|
||||
})
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
strings.Join(mockOutput, "\n"),
|
||||
nil)
|
||||
},
|
||||
|
|
@ -76,53 +72,147 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
Err: nil,
|
||||
},
|
||||
{
|
||||
// git refuses to answer --show-toplevel when there's no work tree, so
|
||||
// we have to ask a second time without it.
|
||||
Name: "bare repo",
|
||||
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
||||
// setup for main worktree
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
"",
|
||||
errors.New("fatal: this operation must be run in a work tree"))
|
||||
|
||||
mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{
|
||||
// --show-toplevel
|
||||
`C:\path\to\repo`,
|
||||
// --git-dir
|
||||
`C:\path\to\bare_repo\bare.git`,
|
||||
`C:\path\to\project\bare.git`,
|
||||
// --git-common-dir
|
||||
`C:\path\to\bare_repo\bare.git`,
|
||||
// --is-bare-repository
|
||||
`true`,
|
||||
// --show-superproject-working-tree
|
||||
`C:\path\to\project\bare.git`,
|
||||
}, []string{
|
||||
// --show-toplevel
|
||||
"/path/to/repo",
|
||||
// --git-dir
|
||||
"/path/to/bare_repo/bare.git",
|
||||
"/path/to/project/bare.git",
|
||||
// --git-common-dir
|
||||
"/path/to/bare_repo/bare.git",
|
||||
// --is-bare-repository
|
||||
"true",
|
||||
// --show-superproject-working-tree
|
||||
"/path/to/project/bare.git",
|
||||
})
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
append(getRevParseArgs(), "--absolute-git-dir", "--git-common-dir"),
|
||||
strings.Join(mockOutput, "\n"),
|
||||
nil)
|
||||
},
|
||||
Path: "/path/to/repo",
|
||||
Path: "/path/to/project",
|
||||
Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{
|
||||
worktreePath: `C:\path\to\repo`,
|
||||
worktreeGitDirPath: `C:\path\to\bare_repo\bare.git`,
|
||||
repoPath: `C:\path\to\bare_repo`,
|
||||
repoGitDirPath: `C:\path\to\bare_repo\bare.git`,
|
||||
repoName: `bare_repo`,
|
||||
worktreePath: "",
|
||||
worktreeGitDirPath: `C:\path\to\project\bare.git`,
|
||||
repoPath: `C:\path\to\project`,
|
||||
repoGitDirPath: `C:\path\to\project\bare.git`,
|
||||
repoName: `project`,
|
||||
isBareRepo: true,
|
||||
}, &RepoPaths{
|
||||
worktreePath: "/path/to/repo",
|
||||
worktreeGitDirPath: "/path/to/bare_repo/bare.git",
|
||||
repoPath: "/path/to/bare_repo",
|
||||
repoGitDirPath: "/path/to/bare_repo/bare.git",
|
||||
repoName: "bare_repo",
|
||||
worktreePath: "",
|
||||
worktreeGitDirPath: "/path/to/project/bare.git",
|
||||
repoPath: "/path/to/project",
|
||||
repoGitDirPath: "/path/to/project/bare.git",
|
||||
repoName: "project",
|
||||
isBareRepo: true,
|
||||
}),
|
||||
Err: nil,
|
||||
},
|
||||
{
|
||||
// Standing in the .git dir of an ordinary repo: git refuses to name a
|
||||
// work tree, but the directory holding the .git is one, so we open the
|
||||
// repo from there.
|
||||
Name: "in a repo's .git dir",
|
||||
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
||||
gitDir := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo\.git`, "/path/to/repo/.git")
|
||||
worktree := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo`, "/path/to/repo")
|
||||
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
"",
|
||||
errors.New("fatal: this operation must be run in a work tree"))
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--absolute-git-dir", "--git-common-dir"),
|
||||
strings.Join([]string{gitDir, gitDir}, "\n"),
|
||||
nil)
|
||||
|
||||
// asking again from the directory holding the .git
|
||||
runner.ExpectGitArgs(
|
||||
append(append([]string{"-C", worktree}, getRevParseArgs()...), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
strings.Join([]string{worktree, gitDir, gitDir}, "\n"),
|
||||
nil)
|
||||
},
|
||||
Path: "/path/to/repo/.git",
|
||||
Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{
|
||||
worktreePath: `C:\path\to\repo`,
|
||||
worktreeGitDirPath: `C:\path\to\repo\.git`,
|
||||
repoPath: `C:\path\to\repo`,
|
||||
repoGitDirPath: `C:\path\to\repo\.git`,
|
||||
repoName: `repo`,
|
||||
isBareRepo: false,
|
||||
}, &RepoPaths{
|
||||
worktreePath: "/path/to/repo",
|
||||
worktreeGitDirPath: "/path/to/repo/.git",
|
||||
repoPath: "/path/to/repo",
|
||||
repoGitDirPath: "/path/to/repo/.git",
|
||||
repoName: "repo",
|
||||
isBareRepo: false,
|
||||
}),
|
||||
Err: nil,
|
||||
},
|
||||
{
|
||||
// A repo whose work tree lives somewhere else entirely, as set up by
|
||||
// core.worktree or by --work-tree. We're in the main worktree, but the
|
||||
// git dir is not inside it.
|
||||
Name: "repo with a separate work tree",
|
||||
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
||||
mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{
|
||||
// --show-toplevel
|
||||
`C:\path\to\worktree`,
|
||||
// --git-dir
|
||||
`C:\path\to\repo\.git`,
|
||||
// --git-common-dir
|
||||
`C:\path\to\repo\.git`,
|
||||
// --show-superproject-working-tree
|
||||
}, []string{
|
||||
// --show-toplevel
|
||||
"/path/to/worktree",
|
||||
// --git-dir
|
||||
"/path/to/repo/.git",
|
||||
// --git-common-dir
|
||||
"/path/to/repo/.git",
|
||||
// --show-superproject-working-tree
|
||||
})
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
strings.Join(mockOutput, "\n"),
|
||||
nil)
|
||||
|
||||
// asking git to find the repo from the work tree gets us nowhere,
|
||||
// because there is no .git there
|
||||
worktree := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\worktree`, "/path/to/worktree")
|
||||
runner.ExpectGitArgs(
|
||||
append([]string{"-C", worktree}, append(getRevParseArgs(), "--absolute-git-dir")...),
|
||||
"",
|
||||
errors.New("fatal: not a git repository (or any of the parent directories): .git"))
|
||||
},
|
||||
Path: "/path/to/repo",
|
||||
Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{
|
||||
worktreePath: `C:\path\to\worktree`,
|
||||
worktreeGitDirPath: `C:\path\to\repo\.git`,
|
||||
repoPath: `C:\path\to\worktree`,
|
||||
repoGitDirPath: `C:\path\to\repo\.git`,
|
||||
repoName: `worktree`,
|
||||
isBareRepo: false,
|
||||
gitLocationEnvVars: []string{`GIT_DIR=C:\path\to\repo\.git`, `GIT_WORK_TREE=C:\path\to\worktree`},
|
||||
}, &RepoPaths{
|
||||
worktreePath: "/path/to/worktree",
|
||||
worktreeGitDirPath: "/path/to/repo/.git",
|
||||
repoPath: "/path/to/worktree",
|
||||
repoGitDirPath: "/path/to/repo/.git",
|
||||
repoName: "worktree",
|
||||
isBareRepo: false,
|
||||
gitLocationEnvVars: []string{"GIT_DIR=/path/to/repo/.git", "GIT_WORK_TREE=/path/to/worktree"},
|
||||
}),
|
||||
Err: nil,
|
||||
},
|
||||
{
|
||||
Name: "submodule",
|
||||
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
||||
|
|
@ -133,8 +223,6 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
`C:\path\to\repo\.git\modules\submodule1`,
|
||||
// --git-common-dir
|
||||
`C:\path\to\repo\.git\modules\submodule1`,
|
||||
// --is-bare-repository
|
||||
`false`,
|
||||
// --show-superproject-working-tree
|
||||
`C:\path\to\repo`,
|
||||
}, []string{
|
||||
|
|
@ -144,15 +232,22 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
"/path/to/repo/.git/modules/submodule1",
|
||||
// --git-common-dir
|
||||
"/path/to/repo/.git/modules/submodule1",
|
||||
// --is-bare-repository
|
||||
"false",
|
||||
// --show-superproject-working-tree
|
||||
"/path/to/repo",
|
||||
})
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
strings.Join(mockOutput, "\n"),
|
||||
nil)
|
||||
|
||||
// git finds the submodule's git dir from its work tree, via the
|
||||
// .git file there
|
||||
worktree := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo\submodule1`, "/path/to/repo/submodule1")
|
||||
gitDir := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo\.git\modules\submodule1`, "/path/to/repo/.git/modules/submodule1")
|
||||
runner.ExpectGitArgs(
|
||||
append([]string{"-C", worktree}, append(getRevParseArgs(), "--absolute-git-dir")...),
|
||||
gitDir,
|
||||
nil)
|
||||
},
|
||||
Path: "/path/to/repo/submodule1",
|
||||
Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{
|
||||
|
|
@ -176,7 +271,12 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
Name: "git rev-parse returns an error",
|
||||
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
"",
|
||||
errors.New("fatal: invalid gitfile format: /path/to/repo/worktree2/.git"))
|
||||
// we're not in a repo at all, so asking about a bare one fails too
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--absolute-git-dir", "--git-common-dir"),
|
||||
"",
|
||||
errors.New("fatal: invalid gitfile format: /path/to/repo/worktree2/.git"))
|
||||
},
|
||||
|
|
@ -184,7 +284,7 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
Expected: nil,
|
||||
Err: func(getRevParseArgs argFn) error {
|
||||
args := strings.Join(getRevParseArgs(), " ")
|
||||
return fmt.Errorf("'git %v --show-toplevel --absolute-git-dir --git-common-dir --is-bare-repository --show-superproject-working-tree' failed: fatal: invalid gitfile format: /path/to/repo/worktree2/.git", args)
|
||||
return fmt.Errorf("'git %v --show-toplevel --absolute-git-dir --git-common-dir --show-superproject-working-tree' failed: fatal: invalid gitfile format: /path/to/repo/worktree2/.git", args)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ func (self *SubmoduleCommands) GetCommitSummary(path string, sha string) (string
|
|||
Config("log.showsignature=false").
|
||||
ToArgv()
|
||||
|
||||
summary, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
summary, err := forOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput()
|
||||
return strings.TrimSpace(summary), err
|
||||
}
|
||||
|
||||
|
|
@ -167,7 +167,7 @@ func (self *SubmoduleCommands) GetCommitSummary(path string, sha string) (string
|
|||
// caller then stages the submodule to record the resolution.
|
||||
func (self *SubmoduleCommands) CheckoutConflictCommit(path string, sha string) error {
|
||||
cmdArgs := NewGitCmd("checkout").Dir(path).Arg(sha).ToArgv()
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
return forOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
}
|
||||
|
||||
// ConflictSideLog returns a oneline log, run inside the submodule, of the commits
|
||||
|
|
@ -179,7 +179,7 @@ func (self *SubmoduleCommands) ConflictSideLog(path string, side string, otherSi
|
|||
Arg("--oneline", "--color=always", otherSide+".."+side).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
return forOtherRepo(self.cmd.New(cmdArgs)).DontLog().RunWithOutput()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
|
||||
|
|
@ -195,20 +195,15 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
|
|||
Arg("--include-untracked").
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
return forOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
|
||||
parentDir := ""
|
||||
if submodule.ParentModule != nil {
|
||||
parentDir = submodule.ParentModule.FullPath()
|
||||
}
|
||||
cmdArgs := NewGitCmd("submodule").
|
||||
Arg("update", "--init", "--force", "--", submodule.Path).
|
||||
DirIf(parentDir != "", parentDir).
|
||||
ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
return self.runInParentModule(submodule, self.cmd.New(cmdArgs))
|
||||
}
|
||||
|
||||
func (self *SubmoduleCommands) UpdateAll() error {
|
||||
|
|
@ -225,9 +220,16 @@ func (self *SubmoduleCommands) UpdateAll() error {
|
|||
// temporarily chdir-ing the process there, which would leak the parent
|
||||
// module's directory into whatever other commands run concurrently (e.g. a
|
||||
// background refresh's).
|
||||
//
|
||||
// That directory is relative, so it resolves against the process working
|
||||
// directory rather than against the repo directory the command builder
|
||||
// otherwise pins commands to. Only foreground commands the user issued end up
|
||||
// here, and lazygit won't switch repos while one of those is in flight, so the
|
||||
// two are the same directory; don't call this from background work, where they
|
||||
// need not be.
|
||||
func (self *SubmoduleCommands) runInParentModule(submodule *models.SubmoduleConfig, cmdObj *oscommands.CmdObj) error {
|
||||
if submodule.ParentModule != nil {
|
||||
cmdObj.SetWd(submodule.ParentModule.FullPath())
|
||||
forOtherRepo(cmdObj.SetWd(submodule.ParentModule.FullPath()))
|
||||
}
|
||||
return cmdObj.Run()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
package git_commands
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/samber/lo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
|
|
@ -80,6 +83,27 @@ func TestSubmoduleCheckoutConflictCommit(t *testing.T) {
|
|||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
// A command that runs inside a submodule mustn't inherit the GIT_DIR and
|
||||
// GIT_WORK_TREE that say where the superproject is; git would answer it from
|
||||
// there instead, and the answer would look perfectly plausible.
|
||||
func TestSubmoduleCommandDoesntUseOurGitLocation(t *testing.T) {
|
||||
t.Setenv(env.GitDirEnvVar, "/path/to/repo/.git")
|
||||
t.Setenv(env.GitWorkTreeEnvVar, "/path/to/repo")
|
||||
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectFunc("has neither GIT_DIR nor GIT_WORK_TREE", func(cmdObj *oscommands.CmdObj) bool {
|
||||
return lo.NoneBy(cmdObj.GetEnvVars(), func(envVar string) bool {
|
||||
return strings.HasPrefix(envVar, env.GitDirEnvVar+"=") ||
|
||||
strings.HasPrefix(envVar, env.GitWorkTreeEnvVar+"=")
|
||||
})
|
||||
}, "bbbbbbb the subject\n", nil)
|
||||
instance := buildSubmoduleCommands(commonDeps{runner: runner})
|
||||
|
||||
_, err := instance.GetCommitSummary("mysub", "bbbbbbb")
|
||||
assert.NoError(t, err)
|
||||
runner.CheckForMissingCalls()
|
||||
}
|
||||
|
||||
func TestSubmoduleConflictSideLog(t *testing.T) {
|
||||
runner := oscommands.NewFakeRunner(t).
|
||||
ExpectGitArgs([]string{"-C", "mysub", "log", "--oneline", "--color=always", "ccccccc..bbbbbbb"}, "bbbbbbb left\n", nil)
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func (self *WorktreeCommands) Delete(worktreePath string, force bool) error {
|
|||
func (self *WorktreeCommands) Detach(worktreePath string) error {
|
||||
cmdArgs := NewGitCmd("checkout").Arg("--detach").GitDir(filepath.Join(worktreePath, ".git")).ToArgv()
|
||||
|
||||
return self.cmd.New(cmdArgs).Run()
|
||||
return forOtherRepo(self.cmd.New(cmdArgs)).Run()
|
||||
}
|
||||
|
||||
func WorktreeForBranch(branch *models.Branch, worktrees []*models.Worktree) (*models.Worktree, bool) {
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@ func NewWorktreeLoader(gitCommon *GitCommon) *WorktreeLoader {
|
|||
}
|
||||
|
||||
func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
||||
currentRepoPath := self.repoPaths.RepoPath()
|
||||
worktreePath := self.repoPaths.WorktreePath()
|
||||
|
||||
cmdArgs := NewGitCmd("worktree").Arg("list", "--porcelain").ToArgv()
|
||||
worktreesOutput, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput()
|
||||
if err != nil {
|
||||
|
|
@ -54,17 +51,13 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
|||
|
||||
if strings.HasPrefix(splitLine, "worktree ") {
|
||||
path := strings.SplitN(splitLine, " ", 2)[1]
|
||||
isMain := path == currentRepoPath
|
||||
isCurrent := path == worktreePath
|
||||
isPathMissing := self.pathExists(path)
|
||||
|
||||
current = &models.Worktree{
|
||||
IsMain: isMain,
|
||||
IsCurrent: isCurrent,
|
||||
IsPathMissing: isPathMissing,
|
||||
IsPathMissing: self.pathExists(path),
|
||||
Path: path,
|
||||
// we defer populating GitDir until a loop below so that
|
||||
// we can parallelize the calls to git rev-parse
|
||||
// we can parallelize the calls to git rev-parse, and
|
||||
// IsMain/IsCurrent because they are derived from GitDir
|
||||
GitDir: "",
|
||||
}
|
||||
} else if strings.HasPrefix(splitLine, "HEAD ") {
|
||||
|
|
@ -84,7 +77,7 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
|||
if worktree.IsPathMissing {
|
||||
return
|
||||
}
|
||||
gitDir, err := callGitRevParseWithDir(self.cmd, worktree.Path, "--absolute-git-dir")
|
||||
gitDir, err := callGitRevParseInOtherRepo(self.cmd, worktree.Path, "--absolute-git-dir")
|
||||
if err != nil {
|
||||
self.Log.Warnf("Could not find git dir for worktree %s: %v", worktree.Path, err)
|
||||
return
|
||||
|
|
@ -95,6 +88,23 @@ func (self *WorktreeLoader) GetWorktrees() ([]*models.Worktree, error) {
|
|||
}
|
||||
wg.Wait()
|
||||
|
||||
// Identify the current and the main worktree by their git dir rather than by
|
||||
// their path: `git worktree list` reports the main worktree as the common
|
||||
// git dir with a trailing "/.git" removed, which is the working tree only
|
||||
// when the git dir sits inside it. In a submodule, a bare repo or a repo
|
||||
// using core.worktree it doesn't, and comparing paths then matches nothing.
|
||||
// A worktree whose directory is gone has no git dir to compare, so there we
|
||||
// have nothing better than its path.
|
||||
for _, worktree := range worktrees {
|
||||
if worktree.GitDir != "" {
|
||||
worktree.IsCurrent = worktree.GitDir == self.repoPaths.WorktreeGitDirPath()
|
||||
worktree.IsMain = worktree.GitDir == self.repoPaths.RepoGitDirPath()
|
||||
} else {
|
||||
worktree.IsCurrent = worktree.Path == self.repoPaths.WorktreePath()
|
||||
worktree.IsMain = worktree.Path == self.repoPaths.RepoPath()
|
||||
}
|
||||
}
|
||||
|
||||
names := getUniqueNamesFromPaths(lo.Map(worktrees, func(worktree *models.Worktree, _ int) string {
|
||||
return worktree.Path
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -23,8 +23,10 @@ func TestGetWorktrees(t *testing.T) {
|
|||
{
|
||||
testName: "Single worktree (main)",
|
||||
repoPaths: &RepoPaths{
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo",
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo",
|
||||
repoGitDirPath: "/path/to/repo/.git",
|
||||
worktreeGitDirPath: "/path/to/repo/.git",
|
||||
},
|
||||
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
||||
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
||||
|
|
@ -55,8 +57,10 @@ branch refs/heads/mybranch
|
|||
{
|
||||
testName: "Multiple worktrees (main + linked)",
|
||||
repoPaths: &RepoPaths{
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo",
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo",
|
||||
repoGitDirPath: "/path/to/repo/.git",
|
||||
worktreeGitDirPath: "/path/to/repo/.git",
|
||||
},
|
||||
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
||||
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
||||
|
|
@ -106,8 +110,10 @@ branch refs/heads/mybranch-worktree
|
|||
{
|
||||
testName: "Worktree missing path",
|
||||
repoPaths: &RepoPaths{
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo",
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo",
|
||||
repoGitDirPath: "/path/to/repo/.git",
|
||||
worktreeGitDirPath: "/path/to/repo/.git",
|
||||
},
|
||||
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
||||
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
||||
|
|
@ -136,8 +142,10 @@ branch refs/heads/missingbranch
|
|||
{
|
||||
testName: "In linked worktree",
|
||||
repoPaths: &RepoPaths{
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo-worktree",
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo-worktree",
|
||||
repoGitDirPath: "/path/to/repo/.git",
|
||||
worktreeGitDirPath: "/path/to/repo/.git/worktrees/repo-worktree",
|
||||
},
|
||||
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
||||
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
||||
|
|
@ -184,11 +192,51 @@ branch refs/heads/mybranch-worktree
|
|||
},
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
testName: "In a submodule",
|
||||
repoPaths: &RepoPaths{
|
||||
repoPath: "/path/to/repo/mysubmodule",
|
||||
worktreePath: "/path/to/repo/mysubmodule",
|
||||
repoGitDirPath: "/path/to/repo/.git/modules/mysubmodule",
|
||||
worktreeGitDirPath: "/path/to/repo/.git/modules/mysubmodule",
|
||||
},
|
||||
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
||||
// A submodule's git dir doesn't live inside its working tree, and
|
||||
// `git worktree list` reports the git dir rather than the working
|
||||
// tree it belongs to.
|
||||
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
||||
`worktree /path/to/repo/.git/modules/mysubmodule
|
||||
HEAD d85cc9d281fa6ae1665c68365fc70e75e82a042d
|
||||
branch refs/heads/mybranch
|
||||
`,
|
||||
nil)
|
||||
|
||||
gitArgs := append(append([]string{"-C", "/path/to/repo/.git/modules/mysubmodule"}, getRevParseArgs()...), "--absolute-git-dir")
|
||||
runner.ExpectGitArgs(gitArgs, "/path/to/repo/.git/modules/mysubmodule", nil)
|
||||
|
||||
_ = fs.MkdirAll("/path/to/repo/.git/modules/mysubmodule", 0o755)
|
||||
},
|
||||
expectedWorktrees: []*models.Worktree{
|
||||
{
|
||||
IsMain: true,
|
||||
IsCurrent: true,
|
||||
Path: "/path/to/repo/.git/modules/mysubmodule",
|
||||
IsPathMissing: false,
|
||||
GitDir: "/path/to/repo/.git/modules/mysubmodule",
|
||||
Branch: "mybranch",
|
||||
Head: "d85cc9d281fa6ae1665c68365fc70e75e82a042d",
|
||||
Name: "mysubmodule",
|
||||
},
|
||||
},
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
testName: "Detached HEAD worktree",
|
||||
repoPaths: &RepoPaths{
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo",
|
||||
repoPath: "/path/to/repo",
|
||||
worktreePath: "/path/to/repo",
|
||||
repoGitDirPath: "/path/to/repo/.git",
|
||||
worktreeGitDirPath: "/path/to/repo/.git",
|
||||
},
|
||||
before: func(runner *oscommands.FakeCmdObjRunner, fs afero.Fs, getRevParseArgs argFn) {
|
||||
runner.ExpectGitArgs([]string{"worktree", "list", "--porcelain"},
|
||||
|
|
|
|||
44
pkg/env/env.go
vendored
44
pkg/env/env.go
vendored
|
|
@ -2,27 +2,59 @@ package env
|
|||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// This package encapsulates accessing/mutating the ENV of the program.
|
||||
|
||||
// The variables with which git can be told where a repo is, rather than having
|
||||
// it find out from the working directory.
|
||||
const (
|
||||
GitDirEnvVar = "GIT_DIR"
|
||||
GitWorkTreeEnvVar = "GIT_WORK_TREE"
|
||||
)
|
||||
|
||||
func GetGitDirEnv() string {
|
||||
return os.Getenv("GIT_DIR")
|
||||
return os.Getenv(GitDirEnvVar)
|
||||
}
|
||||
|
||||
func SetGitDirEnv(value string) {
|
||||
os.Setenv("GIT_DIR", value)
|
||||
os.Setenv(GitDirEnvVar, value)
|
||||
}
|
||||
|
||||
func GetWorkTreeEnv() string {
|
||||
return os.Getenv("GIT_WORK_TREE")
|
||||
return os.Getenv(GitWorkTreeEnvVar)
|
||||
}
|
||||
|
||||
func SetWorkTreeEnv(value string) {
|
||||
os.Setenv("GIT_WORK_TREE", value)
|
||||
os.Setenv(GitWorkTreeEnvVar, value)
|
||||
}
|
||||
|
||||
func UnsetGitLocationEnvVars() {
|
||||
_ = os.Unsetenv("GIT_DIR")
|
||||
_ = os.Unsetenv("GIT_WORK_TREE")
|
||||
_ = os.Unsetenv(GitDirEnvVar)
|
||||
_ = os.Unsetenv(GitWorkTreeEnvVar)
|
||||
}
|
||||
|
||||
// GetGitLocationEnvVars returns the location variables that are set, as
|
||||
// "NAME=value" entries.
|
||||
func GetGitLocationEnvVars() []string {
|
||||
envVars := []string{}
|
||||
for _, name := range []string{GitDirEnvVar, GitWorkTreeEnvVar} {
|
||||
if value := os.Getenv(name); value != "" {
|
||||
envVars = append(envVars, name+"="+value)
|
||||
}
|
||||
}
|
||||
return envVars
|
||||
}
|
||||
|
||||
// SetGitLocationEnvVars sets the location variables from "NAME=value" entries,
|
||||
// clearing both first so that only what is given remains. Passing nothing is
|
||||
// how you say the repo is to be found from the working directory.
|
||||
func SetGitLocationEnvVars(envVars []string) {
|
||||
UnsetGitLocationEnvVars()
|
||||
for _, envVar := range envVars {
|
||||
if name, value, ok := strings.Cut(envVar, "="); ok {
|
||||
os.Setenv(name, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,10 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self.c.State().GetRepoPathStack().Push(wd)
|
||||
self.c.State().GetRepoPathStack().Push(types.RepoLocation{
|
||||
Path: wd,
|
||||
GitLocationEnvVars: self.c.Git().RepoPaths.GitLocationEnvVars(),
|
||||
})
|
||||
|
||||
return self.switchTo(submodule.FullPath(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
}
|
||||
|
|
@ -164,7 +167,7 @@ func (self *ReposHelper) SwitchToParentRepo() error {
|
|||
if self.switchRefusedBecauseBusy() {
|
||||
return nil
|
||||
}
|
||||
return self.switchTo(self.c.State().GetRepoPathStack().Pop(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
return self.switchToLocation(self.c.State().GetRepoPathStack().Pop(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
|
||||
}
|
||||
|
||||
func (self *ReposHelper) DispatchSwitchTo(path string, errMsg string, contextKey types.ContextKey) error {
|
||||
|
|
@ -189,23 +192,41 @@ func (self *ReposHelper) switchRefusedBecauseBusy() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// switchTo switches lazygit to the repository (or worktree) at the given path.
|
||||
// It runs synchronously on the UI thread: the switch swaps gui.State (in
|
||||
// resetState) and reassigns gui.git and the process cwd, all of which the UI
|
||||
// thread also reads, so doing it here rather than on a worker avoids racing
|
||||
// those reads. The heavy data loading is still dispatched asynchronously by the
|
||||
// refresh that onNewRepo kicks off.
|
||||
// switchTo switches lazygit to the repository (or worktree) at the given path,
|
||||
// which git is expected to find from that path alone. That's true of every repo
|
||||
// we switch to without having been there before.
|
||||
func (self *ReposHelper) switchTo(path string, errMsg string, contextKey types.ContextKey) error {
|
||||
env.UnsetGitLocationEnvVars()
|
||||
return self.switchToLocation(types.RepoLocation{Path: path}, errMsg, contextKey)
|
||||
}
|
||||
|
||||
// switchToLocation switches lazygit to the repository (or worktree) at the
|
||||
// given location. It runs synchronously on the UI thread: the switch swaps
|
||||
// gui.State (in resetState) and reassigns gui.git and the process cwd, all of
|
||||
// which the UI thread also reads, so doing it here rather than on a worker
|
||||
// avoids racing those reads. The heavy data loading is still dispatched
|
||||
// asynchronously by the refresh that onNewRepo kicks off.
|
||||
//
|
||||
// Everything from here on has to find the repo the way git does, from the
|
||||
// directory we're about to change to, so the location's environment goes into
|
||||
// the process env before we do. Usually that just clears whatever the repo
|
||||
// we're leaving needed, but going back to a repo whose git dir isn't in its
|
||||
// work tree (a dotfile repo opened with --git-dir/--work-tree, say) is the
|
||||
// reason we remember the environment at all: nothing in the path leads to its
|
||||
// git dir. On failure we put back what the repo we're staying in needs.
|
||||
func (self *ReposHelper) switchToLocation(location types.RepoLocation, errMsg string, contextKey types.ContextKey) error {
|
||||
originalPath, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
originalGitLocationEnvVars := env.GetGitLocationEnvVars()
|
||||
|
||||
msg := utils.ResolvePlaceholderString(self.c.Tr.ChangingDirectoryTo, map[string]string{"path": path})
|
||||
env.SetGitLocationEnvVars(location.GitLocationEnvVars)
|
||||
|
||||
msg := utils.ResolvePlaceholderString(self.c.Tr.ChangingDirectoryTo, map[string]string{"path": location.Path})
|
||||
self.c.LogCommand(msg, false)
|
||||
|
||||
if err := os.Chdir(path); err != nil {
|
||||
if err := os.Chdir(location.Path); err != nil {
|
||||
env.SetGitLocationEnvVars(originalGitLocationEnvVars)
|
||||
if os.IsNotExist(err) {
|
||||
return errors.New(errMsg)
|
||||
}
|
||||
|
|
@ -213,6 +234,7 @@ func (self *ReposHelper) switchTo(path string, errMsg string, contextKey types.C
|
|||
}
|
||||
|
||||
if err := commands.VerifyInGitRepo(self.c.OS()); err != nil {
|
||||
env.SetGitLocationEnvVars(originalGitLocationEnvVars)
|
||||
if err := os.Chdir(originalPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,9 +94,9 @@ type Gui struct {
|
|||
|
||||
Mutexes types.Mutexes
|
||||
|
||||
// when you enter into a submodule we'll append the superproject's path to this array
|
||||
// so that you can return to the superproject
|
||||
RepoPathStack *utils.StringStack
|
||||
// when you enter into a submodule we'll append the superproject's location to
|
||||
// this array so that you can return to the superproject
|
||||
RepoPathStack *utils.Stack[types.RepoLocation]
|
||||
|
||||
// this tells us whether our views have been initially set up
|
||||
ViewsSetup bool
|
||||
|
|
@ -158,7 +158,7 @@ type StateAccessor struct {
|
|||
|
||||
var _ types.IStateAccessor = new(StateAccessor)
|
||||
|
||||
func (self *StateAccessor) GetRepoPathStack() *utils.StringStack {
|
||||
func (self *StateAccessor) GetRepoPathStack() *utils.Stack[types.RepoLocation] {
|
||||
return self.gui.RepoPathStack
|
||||
}
|
||||
|
||||
|
|
@ -340,8 +340,10 @@ func (gui *Gui) onSwitchToNewRepo(startArgs appTypes.StartArgs, contextKey types
|
|||
}
|
||||
|
||||
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.ContextKey) error {
|
||||
var err error
|
||||
gui.git, err = commands.NewGitCommand(
|
||||
// Don't assign to gui.git until we know we have one: this also runs when
|
||||
// switching repos, and leaving the field nil would take down the repo we
|
||||
// were in before, which is where the error puts us back.
|
||||
git, err := commands.NewGitCommand(
|
||||
gui.Common,
|
||||
gui.gitVersion,
|
||||
gui.os,
|
||||
|
|
@ -351,6 +353,7 @@ func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, contextKey types.Context
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
gui.git = git
|
||||
|
||||
err = gui.Config.ReloadUserConfigForRepo(gui.getPerRepoConfigFiles())
|
||||
if err != nil {
|
||||
|
|
@ -796,7 +799,7 @@ func NewGui(
|
|||
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
|
||||
viewPtmxMap: map[string]oscommands.Pty{},
|
||||
showRecentRepos: showRecentRepos,
|
||||
RepoPathStack: &utils.StringStack{},
|
||||
RepoPathStack: &utils.Stack[types.RepoLocation]{},
|
||||
RepoStateMap: map[Repo]*GuiRepoState{},
|
||||
GuiLog: []string{},
|
||||
|
||||
|
|
|
|||
|
|
@ -403,8 +403,17 @@ type HasUrn interface {
|
|||
URN() string
|
||||
}
|
||||
|
||||
// RepoLocation is everything it takes to open a repo again: the directory to
|
||||
// change to, plus the environment telling git where the repo is for the repos
|
||||
// git can't find from that directory (see RepoPaths.GitLocationEnvVars), which
|
||||
// is empty for all the others.
|
||||
type RepoLocation struct {
|
||||
Path string
|
||||
GitLocationEnvVars []string
|
||||
}
|
||||
|
||||
type IStateAccessor interface {
|
||||
GetRepoPathStack() *utils.StringStack
|
||||
GetRepoPathStack() *utils.Stack[RepoLocation]
|
||||
GetRepoState() IRepoStateAccessor
|
||||
GetDiffRendererConfigManager() *config.DiffRendererConfigManager
|
||||
// tells us whether we're currently updating lazygit
|
||||
|
|
|
|||
|
|
@ -461,6 +461,7 @@ type TranslationSet struct {
|
|||
DisabledForGPG string
|
||||
CreateRepo string
|
||||
BareRepo string
|
||||
BareRepoNotSupported string
|
||||
InitialBranch string
|
||||
NoRecentRepositories string
|
||||
IncorrectNotARepository string
|
||||
|
|
@ -1619,7 +1620,8 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
DiscardFileChangesPromptResetPatch: "Are you sure you want to discard changes to the selected file(s) from this commit?\n\nThis action will start a rebase, reverting these file changes. Be aware that if subsequent commits depend on these changes, you may need to resolve conflicts.\n\nNote: This will reset the active custom patch!",
|
||||
DisabledForGPG: "Feature not available for users using GPG.\n\nIf you are using a passphrase agent (e.g. gpg-agent) so that you don't have to type your passphrase when signing, you can enable this feature by adding\n\ngit:\n overrideGpg: true\n\nto your lazygit config file.",
|
||||
CreateRepo: "Not in a git repository. Create a new git repository? (y/N): ",
|
||||
BareRepo: "You've attempted to open Lazygit in a bare repo but Lazygit does not yet support bare repos. Open most recent repo? (y/n) ",
|
||||
BareRepo: "You've attempted to open Lazygit in a bare repo but Lazygit does not support bare repos. Open most recent repo? (y/n) ",
|
||||
BareRepoNotSupported: "Lazygit does not support bare repos.",
|
||||
InitialBranch: "Branch name? (leave empty for git's default): ",
|
||||
NoRecentRepositories: "Must open lazygit in a git repository. No valid recent repositories. Exiting.",
|
||||
IncorrectNotARepository: "The value of 'notARepository' is incorrect. It should be one of 'prompt', 'create', 'skip', or 'quit'.",
|
||||
|
|
|
|||
34
pkg/integration/tests/misc/start_in_git_dir.go
Normal file
34
pkg/integration/tests/misc/start_in_git_dir.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package misc
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var StartInGitDir = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Start lazygit in a repo's .git dir, and have it open the repo",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(cfg *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
shell.CreateFileAndAdd("blah", "original content\n")
|
||||
shell.Commit("initial commit")
|
||||
shell.UpdateFile("blah", "updated content\n")
|
||||
|
||||
// this is where lazygit will start
|
||||
shell.Chdir(".git")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("initial commit"),
|
||||
)
|
||||
|
||||
// we're in the work tree the .git belongs to, not in the .git itself
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains(" M blah"),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
|
@ -29,7 +29,7 @@ var Enter = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
t.Views().Status().Content(Contains("repo"))
|
||||
}
|
||||
assertInSubmodule := func() {
|
||||
t.Views().Status().Content(Contains("my_submodule_path(my_submodule_name)"))
|
||||
t.Views().Status().Content(Contains("my_submodule_path"))
|
||||
}
|
||||
|
||||
assertInParentRepo()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package submodule
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// Entering a submodule and escaping back out again, in a repo that git can only
|
||||
// find because we were told where it is (--git-dir/--work-tree). Entering the
|
||||
// submodule has to leave that behind, since it says where the superproject is,
|
||||
// so coming back out has to bring it along again.
|
||||
|
||||
var EnterFromDotfileBareRepo = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Enter a submodule of a dotfile bare repo and escape back out again",
|
||||
ExtraCmdArgs: []string{"--git-dir={{.actualPath}}/.bare", "--work-tree={{.actualPath}}/repo"},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
// we're going to have a directory structure like this:
|
||||
// project
|
||||
// - .bare (the git dir)
|
||||
// - repo (the work tree, with no .git of its own)
|
||||
// - my_submodule_name (the submodule's remote)
|
||||
//
|
||||
// The work tree is called 'repo' because that's the directory that all
|
||||
// lazygit tests start in
|
||||
|
||||
// make a repo for the submodule to be cloned from, using the .git dir
|
||||
// that every test starts with
|
||||
shell.EmptyCommit("initial submodule commit")
|
||||
shell.Clone("my_submodule_name")
|
||||
|
||||
// now turn the test repo into a dotfile-style bare repo
|
||||
shell.DeleteFile(".git")
|
||||
shell.RunCommand([]string{"git", "init", "--bare", "../.bare"})
|
||||
gitInBareRepo := []string{"git", "--git-dir=../.bare", "--work-tree=."}
|
||||
shell.RunCommand(append(gitInBareRepo, "checkout", "-b", "mybranch"))
|
||||
shell.CreateFile("blah", "blah\n")
|
||||
shell.RunCommand(append(gitInBareRepo, "add", "blah"))
|
||||
shell.RunCommand(append(gitInBareRepo, "commit", "-m", "initial commit"))
|
||||
shell.RunCommand(append(gitInBareRepo, "-c", "protocol.file.allow=always", "submodule",
|
||||
"add", "--name", "my_submodule_name", "../my_submodule_name", "my_submodule_path"))
|
||||
shell.RunCommand(append(gitInBareRepo, "commit", "-m", "add submodule"))
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
assertInParentRepo := func() {
|
||||
t.Views().Status().Content(Contains("repo"))
|
||||
t.Views().Commits().Lines(
|
||||
Contains("add submodule"),
|
||||
Contains("initial commit"),
|
||||
)
|
||||
}
|
||||
|
||||
assertInParentRepo()
|
||||
|
||||
t.Views().Submodules().Focus().
|
||||
Lines(
|
||||
Contains("my_submodule_name").IsSelected(),
|
||||
).
|
||||
PressEnter()
|
||||
|
||||
t.Views().Status().Content(Contains("my_submodule_path"))
|
||||
t.Views().Commits().Lines(
|
||||
Contains("initial submodule commit"),
|
||||
)
|
||||
|
||||
t.Views().Files().IsFocused().PressEscape()
|
||||
|
||||
assertInParentRepo()
|
||||
t.Views().Submodules().IsFocused()
|
||||
},
|
||||
})
|
||||
|
|
@ -37,7 +37,7 @@ var EnterNested = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
// enter the nested submodule
|
||||
PressEnter()
|
||||
|
||||
t.Views().Status().Content(Contains("innerSubPath(innerSubName)"))
|
||||
t.Views().Status().Content(Contains("innerSubPath"))
|
||||
t.Views().Commits().ContainsLines(
|
||||
Contains("initial inner commit"),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ var Reset = NewIntegrationTest(NewIntegrationTestArgs{
|
|||
t.Views().Status().Content(Contains("repo"))
|
||||
}
|
||||
assertInSubmodule := func() {
|
||||
t.Views().Status().Content(Contains("my_submodule_path(my_submodule_name)"))
|
||||
t.Views().Status().Content(Contains("my_submodule_path"))
|
||||
}
|
||||
|
||||
assertInParentRepo()
|
||||
|
|
|
|||
|
|
@ -353,6 +353,7 @@ var tests = []*components.IntegrationTest{
|
|||
misc.DirenvUnloadsOnBlockedEnvrc,
|
||||
misc.InitialOpen,
|
||||
misc.RecentReposOnLaunch,
|
||||
misc.StartInGitDir,
|
||||
patch_building.Apply,
|
||||
patch_building.ApplyInReverse,
|
||||
patch_building.ApplyInReverseWithConflict,
|
||||
|
|
@ -441,6 +442,7 @@ var tests = []*components.IntegrationTest{
|
|||
status.LogCmdStatusPanelAllBranchesLog,
|
||||
submodule.Add,
|
||||
submodule.Enter,
|
||||
submodule.EnterFromDotfileBareRepo,
|
||||
submodule.EnterNested,
|
||||
submodule.Remove,
|
||||
submodule.RemoveNested,
|
||||
|
|
@ -541,6 +543,7 @@ var tests = []*components.IntegrationTest{
|
|||
worktree.RemoveWorktreeAndDeleteLocalAndRemoteBranch,
|
||||
worktree.RemoveWorktreeFromBranch,
|
||||
worktree.ResetWindowTabs,
|
||||
worktree.SeparateWorkTreeConfig,
|
||||
worktree.SymlinkIntoRepoSubdir,
|
||||
worktree.WorktreeInRepo,
|
||||
}
|
||||
|
|
|
|||
70
pkg/integration/tests/worktree/separate_work_tree_config.go
Normal file
70
pkg/integration/tests/worktree/separate_work_tree_config.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package worktree
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
. "github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// This case is like bare_repo_worktree_config.go, except that lazygit isn't
|
||||
// told where the git dir is: it is started in the directory containing it, and
|
||||
// finds it the way git does. The work tree is somewhere else entirely, so git
|
||||
// can't find its way back from there, and every command we run has to be told
|
||||
// where the repo is.
|
||||
|
||||
var SeparateWorkTreeConfig = NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: "Open lazygit in the git dir of a repo whose work tree is elsewhere, and add a file and commit",
|
||||
ExtraCmdArgs: []string{},
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *Shell) {
|
||||
// we're going to have a directory structure like this:
|
||||
// project
|
||||
// - repo (holds the .git dir, and nothing else; lazygit starts here)
|
||||
// - worktree (holds the files)
|
||||
//
|
||||
// 'repo' is the repository/directory that all lazygit tests start in
|
||||
|
||||
shell.CreateFileAndAdd("blah", "original content\n")
|
||||
shell.Commit("initial commit")
|
||||
|
||||
// point the repo at a work tree outside of it (core.worktree is
|
||||
// relative to the .git dir), and fill that work tree from HEAD
|
||||
shell.CreateDir("../worktree")
|
||||
shell.SetConfig("core.worktree", "../../worktree")
|
||||
shell.RunCommand([]string{"git", "reset", "--hard"})
|
||||
|
||||
// the copy of the file we committed from is not in the work tree, so
|
||||
// git no longer knows anything about it
|
||||
shell.DeleteFile("blah")
|
||||
|
||||
shell.UpdateFile("../worktree/blah", "updated content\n")
|
||||
},
|
||||
Run: func(t *TestDriver, keys config.KeybindingConfig) {
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("initial commit"),
|
||||
)
|
||||
|
||||
t.Views().Files().
|
||||
IsFocused().
|
||||
Lines(
|
||||
Contains(" M blah"), // shows as modified
|
||||
).
|
||||
PressPrimaryAction().
|
||||
Press(keys.Files.CommitChanges)
|
||||
|
||||
t.ExpectPopup().CommitMessagePanel().
|
||||
Title(Equals("Commit summary")).
|
||||
Type("Add blah").
|
||||
Confirm()
|
||||
|
||||
t.Views().Files().
|
||||
IsEmpty()
|
||||
|
||||
t.Views().Commits().
|
||||
Lines(
|
||||
Contains("Add blah"),
|
||||
Contains("initial commit"),
|
||||
)
|
||||
},
|
||||
})
|
||||
28
pkg/utils/stack.go
Normal file
28
pkg/utils/stack.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package utils
|
||||
|
||||
type Stack[T any] struct {
|
||||
stack []T
|
||||
}
|
||||
|
||||
func (self *Stack[T]) Push(item T) {
|
||||
self.stack = append(self.stack, item)
|
||||
}
|
||||
|
||||
func (self *Stack[T]) Pop() T {
|
||||
if len(self.stack) == 0 {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
n := len(self.stack) - 1
|
||||
last := self.stack[n]
|
||||
self.stack = self.stack[:n]
|
||||
return last
|
||||
}
|
||||
|
||||
func (self *Stack[T]) IsEmpty() bool {
|
||||
return len(self.stack) == 0
|
||||
}
|
||||
|
||||
func (self *Stack[T]) Clear() {
|
||||
self.stack = nil
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
package utils
|
||||
|
||||
type StringStack struct {
|
||||
stack []string
|
||||
}
|
||||
|
||||
func (self *StringStack) Push(s string) {
|
||||
self.stack = append(self.stack, s)
|
||||
}
|
||||
|
||||
func (self *StringStack) Pop() string {
|
||||
if len(self.stack) == 0 {
|
||||
return ""
|
||||
}
|
||||
n := len(self.stack) - 1
|
||||
last := self.stack[n]
|
||||
self.stack = self.stack[:n]
|
||||
return last
|
||||
}
|
||||
|
||||
func (self *StringStack) IsEmpty() bool {
|
||||
return len(self.stack) == 0
|
||||
}
|
||||
|
||||
func (self *StringStack) Clear() {
|
||||
self.stack = []string{}
|
||||
}
|
||||
Loading…
Reference in a new issue