From e2d6e8f264604b1b71dc2c958f7bdd44a90d029b Mon Sep 17 00:00:00 2001 From: Paulo Gomes Date: Sat, 3 Aug 2024 10:23:28 +0100 Subject: git: worktree, Add StatusWithOptions The fix for #119 improves the Worktree.Status() behaviour by preloading all existing files and setting their status to unmodified. Which makes it more reliable when doing per file status verification, however breaks backwards compatibility in two ways: - Increased execution time and space: the preloading can be slow in very large repositories and will increase memory usage when representing the state. - Behaviour: the previous behaviour returned a map with a small subset of entries. The new behaviour will include a new entry for every file within the repository. This commit introduces reverts the change in the default behaviour, and introduces StatusWithOptions so that users can opt-in the new option. Signed-off-by: Paulo Gomes --- status.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ worktree_status.go | 53 +++++++++++++---------------------------- worktree_test.go | 10 +++++++- 3 files changed, 94 insertions(+), 38 deletions(-) diff --git a/status.go b/status.go index 7f18e02..d14f7e6 100644 --- a/status.go +++ b/status.go @@ -4,6 +4,9 @@ import ( "bytes" "fmt" "path/filepath" + + mindex "github.com/go-git/go-git/v5/utils/merkletrie/index" + "github.com/go-git/go-git/v5/utils/merkletrie/noder" ) // Status represents the current status of a Worktree. @@ -77,3 +80,69 @@ const ( Copied StatusCode = 'C' UpdatedButUnmerged StatusCode = 'U' ) + +// StatusStrategy defines the different types of strategies when processing +// the worktree status. +type StatusStrategy int + +const ( + // TODO: (V6) Review the default status strategy. + // TODO: (V6) Review the type used to represent Status, to enable lazy + // processing of statuses going direct to the backing filesystem. + defaultStatusStrategy = Empty + + // Empty starts its status map from empty. Missing entries for a given + // path means that the file is untracked. This causes a known issue (#119) + // whereby unmodified files can be incorrectly reported as untracked. + // + // This can be used when returning the changed state within a modified Worktree. + // For example, to check whether the current worktree is clean. + Empty StatusStrategy = 0 + // Preload goes through all existing nodes from the index and add them to the + // status map as unmodified. This is currently the most reliable strategy + // although it comes at a performance cost in large repositories. + // + // This method is recommended when fetching the status of unmodified files. + // For example, to confirm the status of a specific file that is either + // untracked or unmodified. + Preload StatusStrategy = 1 +) + +func (s StatusStrategy) new(w *Worktree) (Status, error) { + switch s { + case Preload: + return preloadStatus(w) + case Empty: + return make(Status), nil + } + return nil, fmt.Errorf("%w: %+v", ErrUnsupportedStatusStrategy, s) +} + +func preloadStatus(w *Worktree) (Status, error) { + idx, err := w.r.Storer.Index() + if err != nil { + return nil, err + } + + idxRoot := mindex.NewRootNode(idx) + nodes := []noder.Noder{idxRoot} + + status := make(Status) + for len(nodes) > 0 { + var node noder.Noder + node, nodes = nodes[0], nodes[1:] + if node.IsDir() { + children, err := node.Children() + if err != nil { + return nil, err + } + nodes = append(nodes, children...) + continue + } + fs := status.File(node.Name()) + fs.Worktree = Unmodified + fs.Staging = Unmodified + } + + return status, nil +} diff --git a/worktree_status.go b/worktree_status.go index 2f865ce..6a0049c 100644 --- a/worktree_status.go +++ b/worktree_status.go @@ -29,10 +29,23 @@ var ( // ErrGlobNoMatches in an AddGlob if the glob pattern does not match any // files in the worktree. ErrGlobNoMatches = errors.New("glob pattern did not match any files") + // ErrUnsupportedStatusStrategy occurs when an invalid StatusStrategy is used + // when processing the Worktree status. + ErrUnsupportedStatusStrategy = errors.New("unsupported status strategy") ) // Status returns the working tree status. func (w *Worktree) Status() (Status, error) { + return w.StatusWithOptions(StatusOptions{Strategy: defaultStatusStrategy}) +} + +// StatusOptions defines the options for Worktree.StatusWithOptions(). +type StatusOptions struct { + Strategy StatusStrategy +} + +// StatusWithOptions returns the working tree status. +func (w *Worktree) StatusWithOptions(o StatusOptions) (Status, error) { var hash plumbing.Hash ref, err := w.r.Head() @@ -44,45 +57,11 @@ func (w *Worktree) Status() (Status, error) { hash = ref.Hash() } - return w.status(hash) -} - -func (w *Worktree) newStatusFromIndex() (Status, error) { - idx, err := w.r.Storer.Index() - if err != nil { - return nil, err - } - - idxRoot := mindex.NewRootNode(idx) - nodes := []noder.Noder{idxRoot} - - if err != nil { - return nil, err - } - - status := make(Status) - - for len(nodes) > 0 { - var node noder.Noder - node, nodes = nodes[0], nodes[1:] - if node.IsDir() { - children, err := node.Children() - if err != nil { - return nil, err - } - nodes = append(nodes, children...) - continue - } - fs := status.File(node.Name()) - fs.Worktree = Unmodified - fs.Staging = Unmodified - } - - return status, nil + return w.status(o.Strategy, hash) } -func (w *Worktree) status(commit plumbing.Hash) (Status, error) { - s, err := w.newStatusFromIndex() +func (w *Worktree) status(ss StatusStrategy, commit plumbing.Hash) (Status, error) { + s, err := ss.new(w) if err != nil { return nil, err } diff --git a/worktree_test.go b/worktree_test.go index deaf5e5..7ecd818 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -1062,13 +1062,21 @@ func (s *WorktreeSuite) TestStatusUnmodified(c *C) { err := w.Checkout(&CheckoutOptions{Force: true}) c.Assert(err, IsNil) - status, err := w.Status() + status, err := w.StatusWithOptions(StatusOptions{Strategy: Preload}) c.Assert(err, IsNil) c.Assert(status.IsClean(), Equals, true) c.Assert(status.IsUntracked("LICENSE"), Equals, false) c.Assert(status.File("LICENSE").Staging, Equals, Unmodified) c.Assert(status.File("LICENSE").Worktree, Equals, Unmodified) + + status, err = w.StatusWithOptions(StatusOptions{Strategy: Empty}) + c.Assert(err, IsNil) + c.Assert(status.IsClean(), Equals, true) + c.Assert(status.IsUntracked("LICENSE"), Equals, false) + + c.Assert(status.File("LICENSE").Staging, Equals, Untracked) + c.Assert(status.File("LICENSE").Worktree, Equals, Untracked) } func (s *WorktreeSuite) TestReset(c *C) { -- cgit