From a03b2cb07a36f157771c85f21b9801ef79c0808e Mon Sep 17 00:00:00 2001 From: Anshul Khandelwal <12948312+k-anshul@users.noreply.github.com> Date: Fri, 2 May 2025 14:53:06 +0530 Subject: [PATCH 1/4] git: worktree, Don't delete local untracked files when resetting worktree. Fixes https://github.com/go-git/go-git/issues/53 --- worktree.go | 33 +++++++++++++++++++++++---------- worktree_test.go | 11 ++++++++++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/worktree.go b/worktree.go index 2a498e1f4..e1a81a39b 100644 --- a/worktree.go +++ b/worktree.go @@ -309,14 +309,15 @@ func (w *Worktree) ResetSparsely(opts *ResetOptions, dirs []string) error { return err } + var removedFiles []string if opts.Mode == MixedReset || opts.Mode == MergeReset || opts.Mode == HardReset { - if err := w.resetIndex(t, dirs, opts.Files); err != nil { + if removedFiles, err = w.resetIndex(t, dirs, opts.Files); err != nil { return err } } if opts.Mode == MergeReset || opts.Mode == HardReset { - if err := w.resetWorktree(t, opts.Files); err != nil { + if err := w.resetWorktree(t, removedFiles); err != nil { return err } } @@ -369,23 +370,24 @@ func (w *Worktree) Reset(opts *ResetOptions) error { return w.ResetSparsely(opts, nil) } -func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) error { +func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) ([]string, error) { idx, err := w.r.Storer.Index() if err != nil { - return err + return nil, err } b := newIndexBuilder(idx) changes, err := w.diffTreeWithStaging(t, true) if err != nil { - return err + return nil, err } + var removedFiles []string for _, ch := range changes { a, err := ch.Action() if err != nil { - return err + return nil, err } var name string @@ -396,7 +398,7 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) err name = ch.To.String() e, err = t.FindEntry(name) if err != nil { - return err + return nil, err } case merkletrie.Delete: name = ch.From.String() @@ -410,6 +412,7 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) err } b.Remove(name) + removedFiles = append(removedFiles, name) if e == nil { continue } @@ -428,7 +431,7 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) err idx.SkipUnless(dirs) } - return w.r.Storer.SetIndex(idx) + return removedFiles, w.r.Storer.SetIndex(idx) } func inFiles(files []string, v string) bool { @@ -454,6 +457,11 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { } b := newIndexBuilder(idx) + status, err := w.Status() + if err != nil { + return err + } + for _, ch := range changes { if err := w.validChange(ch); err != nil { return err @@ -477,8 +485,13 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { } } - if err := w.checkoutChange(ch, t, b); err != nil { - return err + // only checkout an untracked file if it is in the list of files + // a reset should leave untracked files alone + file := nameFromAction(&ch) + if status.File(file).Worktree != Untracked || inFiles(files, file) { + if err := w.checkoutChange(ch, t, b); err != nil { + return err + } } } diff --git a/worktree_test.go b/worktree_test.go index c550205c1..bdb478d3a 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1138,7 +1138,16 @@ func (s *WorktreeSuite) TestResetWithUntracked() { status, err := w.Status() s.NoError(err) - s.True(status.IsClean()) + for file, st := range status { + if file == "foo" { + s.Equal(Untracked, st.Worktree) + s.Equal(Untracked, st.Staging) + continue + } + if st.Worktree != Unmodified || st.Staging != Unmodified { + s.Fail("file %s not unmodified", file) + } + } } func (s *WorktreeSuite) TestResetSoft() { From 0e87c1f60e4a910050895f7513e564cc3c370357 Mon Sep 17 00:00:00 2001 From: Anshul Khandelwal <12948312+k-anshul@users.noreply.github.com> Date: Fri, 2 May 2025 15:02:16 +0530 Subject: [PATCH 2/4] git: worktree, Don't delete local untracked files when resetting worktree. Fixes https://github.com/go-git/go-git/issues/53 --- worktree_test.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worktree_test.go b/worktree_test.go index bdb478d3a..7c7a12faa 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1130,12 +1130,16 @@ func (s *WorktreeSuite) TestResetWithUntracked() { err := w.Checkout(&CheckoutOptions{}) s.NoError(err) - err = util.WriteFile(fs, "foo", nil, 0o755) + err = util.WriteFile(fs, "foo", []byte("bar"), 0o755) s.NoError(err) err = w.Reset(&ResetOptions{Mode: MergeReset, Commit: commit}) s.NoError(err) + contents, err := util.ReadFile(fs, "foo") + s.NoError(err) + s.Equal("bar", string(contents)) + status, err := w.Status() s.NoError(err) for file, st := range status { From ad102d20dea10667239534f8310a7e7396c9ca27 Mon Sep 17 00:00:00 2001 From: Anshul Khandelwal <12948312+k-anshul@users.noreply.github.com> Date: Mon, 5 May 2025 12:08:08 +0530 Subject: [PATCH 3/4] git: worktree, Don't delete local untracked files when resetting worktree. Fixes https://github.com/go-git/go-git/issues/53 --- worktree.go | 22 +++++++++------------- worktree_test.go | 2 +- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/worktree.go b/worktree.go index e1a81a39b..3a47849db 100644 --- a/worktree.go +++ b/worktree.go @@ -316,12 +316,18 @@ func (w *Worktree) ResetSparsely(opts *ResetOptions, dirs []string) error { } } - if opts.Mode == MergeReset || opts.Mode == HardReset { + if opts.Mode == MergeReset && len(removedFiles) > 0 { if err := w.resetWorktree(t, removedFiles); err != nil { return err } } + if opts.Mode == HardReset { + if err := w.resetWorktree(t, opts.Files); err != nil { + return err + } + } + return nil } @@ -457,11 +463,6 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { } b := newIndexBuilder(idx) - status, err := w.Status() - if err != nil { - return err - } - for _, ch := range changes { if err := w.validChange(ch); err != nil { return err @@ -485,13 +486,8 @@ func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { } } - // only checkout an untracked file if it is in the list of files - // a reset should leave untracked files alone - file := nameFromAction(&ch) - if status.File(file).Worktree != Untracked || inFiles(files, file) { - if err := w.checkoutChange(ch, t, b); err != nil { - return err - } + if err := w.checkoutChange(ch, t, b); err != nil { + return err } } diff --git a/worktree_test.go b/worktree_test.go index 7c7a12faa..ae6e1f73e 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1149,7 +1149,7 @@ func (s *WorktreeSuite) TestResetWithUntracked() { continue } if st.Worktree != Unmodified || st.Staging != Unmodified { - s.Fail("file %s not unmodified", file) + s.Failf("file %s not unmodified", file) } } } From bf0bb78d2479b593f61bb3cc1d189e1579ea4509 Mon Sep 17 00:00:00 2001 From: Daan Schoone Date: Sun, 25 May 2025 11:03:27 +0200 Subject: [PATCH 4/4] git: test for branch checkout should not delete untracked file --- worktree_test.go | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/worktree_test.go b/worktree_test.go index ae6e1f73e..b85f4d8c3 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -764,6 +764,39 @@ func (s *WorktreeSuite) TestCheckoutBranch() { s.True(status.IsClean()) } +func (s *WorktreeSuite) TestCheckoutBranchUntracked() { + w := &Worktree{ + r: s.Repository, + Filesystem: memfs.New(), + } + + uf, err := w.Filesystem.Create("untracked_file") + s.NoError(err) + _, err = uf.Write([]byte("don't delete me")) + s.NoError(err) + + err = w.Checkout(&CheckoutOptions{ + Branch: "refs/heads/branch", + }) + s.NoError(err) + + head, err := w.r.Head() + s.NoError(err) + s.Equal("refs/heads/branch", head.Name().String()) + + status, err := w.Status() + s.NoError(err) + // The untracked file should still be there, so it's not clean + s.False(status.IsClean()) + s.True(status.IsUntracked("untracked_file")) + err = w.Filesystem.Remove("untracked_file") + s.NoError(err) + status, err = w.Status() + s.NoError(err) + // After deleting the untracked file it should now be clean + s.True(status.IsClean()) +} + func (s *WorktreeSuite) TestCheckoutCreateWithHash() { w := &Worktree{ r: s.Repository,