diff --git a/git/git.go b/git/git.go index 7cabe7f98..30f2f8562 100644 --- a/git/git.go +++ b/git/git.go @@ -584,51 +584,41 @@ func (git *Git) Pull(remote, branch string) error { return err } -// ListUntracked lists untracked files in the directories provided in dirs. -func (git *Git) ListUntracked(dirs ...string) ([]string, error) { - args := []string{ - "--others", "--exclude-standard", - } - - if len(dirs) > 0 { - args = append(args, "--") - args = append(args, dirs...) - } - - log.Debug(). - Str("action", "ListUntracked()"). +// ListDirtyFiles lists untracked and uncommitted files in the repository. +func (git *Git) ListDirtyFiles() ([]string, []string, error) { + logger := log.With(). + Str("action", "ListDirtyFiles()"). Str("workingDir", git.cfg().WorkingDir). - Msg("List untracked files.") - out, err := git.exec("ls-files", args...) - if err != nil { - return nil, fmt.Errorf("ls-files: %w", err) - } - - return removeEmptyLines(strings.Split(out, "\n")), nil - -} + Logger() -// ListUncommitted lists uncommitted files in the directories provided in dirs. -func (git *Git) ListUncommitted(dirs ...string) ([]string, error) { - args := []string{ - "--modified", "--exclude-standard", + out, err := git.exec("status", "--porcelain") + if err != nil { + return nil, nil, fmt.Errorf("git status --porcelain: %w", err) } - if len(dirs) > 0 { - args = append(args, "--") - args = append(args, dirs...) - } + logger.Debug().Str("stdout", out).Msg("`git status --porcelain` output") - log.Debug(). - Str("action", "ListUncommitted()"). - Str("workingDir", git.cfg().WorkingDir). - Msg("List uncommitted files.") - out, err := git.exec("ls-files", args...) - if err != nil { - return nil, fmt.Errorf("ls-files: %w", err) + var untracked []string + var uncommitted []string + for _, line := range strings.Split(out, "\n") { + if len(line) < 4 { + continue + } + switch line[0:2] { + case "??": + file := line[3:] + if len(file) > 1 && file[len(file)-1] == os.PathSeparator { + file = file[:len(file)-1] + } + untracked = append(untracked, file) + case " M": + file := line[3:] + uncommitted = append(uncommitted, file) + } } - - return removeEmptyLines(strings.Split(out, "\n")), nil + logger.Debug().Strs("untracked", untracked).Msg("untracked files") + logger.Debug().Strs("uncommitted", uncommitted).Msg("uncommitted files") + return untracked, uncommitted, nil } // ShowCommitMetadata returns common metadata associated with the given object. @@ -774,8 +764,7 @@ func (git *Git) exec(command string, args ...string) (string, error) { } return "", NewCmdError(cmd.String(), stdout, stderr) } - out := strings.TrimSpace(string(stdout)) - return out, nil + return strings.TrimRight(string(stdout), "\n"), nil } func (git *Git) cfg() *Config { return &git.options.config } diff --git a/git/git_test.go b/git/git_test.go index b80226e1a..597d5a17e 100644 --- a/git/git_test.go +++ b/git/git_test.go @@ -462,6 +462,51 @@ func TestGetConfigValue(t *testing.T) { assert.Error(t, err, "git config: non-existing key") } +func TestListDirtyFiles(t *testing.T) { + t.Parallel() + const ( + remote = "origin" + ) + + repodir := mkOneCommitRepo(t) + g := test.NewGitWrapper(t, repodir, []string{}) + + remoteDir := test.EmptyRepo(t, true) + + assert.NoError(t, g.RemoteAdd(remote, remoteDir)) + assert.NoError(t, g.Push(remote, defaultBranch)) + + untracked, uncommitted, err := g.ListDirtyFiles() + assert.NoError(t, err) + assert.EqualInts(t, 0, len(untracked)) + assert.EqualInts(t, 0, len(uncommitted)) + + test.WriteFile(t, repodir, "test.txt", "some content") + untracked, uncommitted, err = g.ListDirtyFiles() + assert.NoError(t, err) + assert.EqualInts(t, 1, len(untracked)) + assert.EqualInts(t, 0, len(uncommitted)) + assert.EqualStrings(t, "test.txt", untracked[0]) + + test.WriteFile(t, filepath.Join(repodir, "deep/nested/path"), "test.txt", "some content") + untracked, uncommitted, err = g.ListDirtyFiles() + assert.NoError(t, err) + assert.EqualInts(t, 2, len(untracked)) + assert.EqualInts(t, 0, len(uncommitted)) + assert.EqualStrings(t, "deep", untracked[0]) + assert.EqualStrings(t, "test.txt", untracked[1]) + + test.WriteFile(t, repodir, "README.md", "# changed") + untracked, uncommitted, err = g.ListDirtyFiles() + assert.NoError(t, err) + assert.EqualInts(t, 2, len(untracked)) + assert.EqualInts(t, 1, len(uncommitted)) + assert.EqualStrings(t, "deep", untracked[0]) + assert.EqualStrings(t, "test.txt", untracked[1]) + assert.EqualStrings(t, "README.md", uncommitted[0]) + +} + const defaultBranch = "main" func mkOneCommitRepo(t *testing.T) string { diff --git a/stack/manager.go b/stack/manager.go index fdf0cceef..340500fa6 100644 --- a/stack/manager.go +++ b/stack/manager.go @@ -663,14 +663,9 @@ func hasChangedWatchedFiles(stack *config.Stack, changedFiles []string) (project } func checkRepoIsClean(g *git.Git) (RepoChecks, error) { - untracked, err := g.ListUntracked() + untracked, uncommitted, err := g.ListDirtyFiles() if err != nil { - return RepoChecks{}, errors.E(err, "listing untracked files") - } - - uncommitted, err := g.ListUncommitted() - if err != nil { - return RepoChecks{}, errors.E(err, "listing uncommitted files") + return RepoChecks{}, errors.E(err, "listing dirty files") } return RepoChecks{