diff --git a/_examples/restore/main.go b/_examples/restore/main.go new file mode 100644 index 000000000..8016b06f0 --- /dev/null +++ b/_examples/restore/main.go @@ -0,0 +1,103 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + "github.com/go-git/go-git/v5" + . "github.com/go-git/go-git/v5/_examples" + "github.com/go-git/go-git/v5/plumbing/object" +) + +func prepareRepo(w *git.Worktree, directory string) { + // We need a known state of files inside the worktree for testing revert a modify and delete + Info("echo \"hello world! Modify\" > for-modify") + err := ioutil.WriteFile(filepath.Join(directory, "for-modify"), []byte("hello world! Modify"), 0644) + CheckIfError(err) + Info("git add for-modify") + _, err = w.Add("for-modify") + CheckIfError(err) + + Info("echo \"hello world! Delete\" > for-delete") + err = ioutil.WriteFile(filepath.Join(directory, "for-delete"), []byte("hello world! Delete"), 0644) + CheckIfError(err) + Info("git add for-delete") + _, err = w.Add("for-delete") + CheckIfError(err) + + Info("git commit -m \"example go-git commit\"") + _, err = w.Commit("example go-git commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "John Doe", + Email: "john@doe.org", + When: time.Now(), + }, + }) + CheckIfError(err) +} + +// An example of how to restore AKA unstage files +func main() { + CheckArgs("") + directory := os.Args[1] + + // Opens an already existing repository. + r, err := git.PlainOpen(directory) + CheckIfError(err) + + w, err := r.Worktree() + CheckIfError(err) + + prepareRepo(w, directory) + + // Perform the operation and stage them + Info("echo \"hello world! Modify 2\" > for-modify") + err = ioutil.WriteFile(filepath.Join(directory, "for-modify"), []byte("hello world! Modify 2"), 0644) + CheckIfError(err) + Info("git add for-modify") + _, err = w.Add("for-modify") + CheckIfError(err) + + Info("echo \"hello world! Add\" > for-add") + err = ioutil.WriteFile(filepath.Join(directory, "for-add"), []byte("hello world! Add"), 0644) + CheckIfError(err) + Info("git add for-add") + _, err = w.Add("for-add") + CheckIfError(err) + + Info("rm for-delete") + err = os.Remove(filepath.Join(directory, "for-delete")) + CheckIfError(err) + Info("git add for-delete") + _, err = w.Add("for-delete") + CheckIfError(err) + + // We can verify the current status of the worktree using the method Status. + Info("git status --porcelain") + status, err := w.Status() + CheckIfError(err) + fmt.Println(status) + + // Unstage a single file and see the status + Info("git restore --staged for-modify") + err = w.Restore(&git.RestoreOptions{Staged: true, Files: []string{"for-modify"}}) + CheckIfError(err) + + Info("git status --porcelain") + status, err = w.Status() + CheckIfError(err) + fmt.Println(status) + + // Unstage the other 2 files and see the status + Info("git restore --staged for-add for-delete") + err = w.Restore(&git.RestoreOptions{Staged: true, Files: []string{"for-add", "for-delete"}}) + CheckIfError(err) + + Info("git status --porcelain") + status, err = w.Status() + CheckIfError(err) + fmt.Println(status) +} diff --git a/options.go b/options.go index 70687965b..603b6a80e 100644 --- a/options.go +++ b/options.go @@ -326,6 +326,9 @@ type ResetOptions struct { // the index (resetting it to the tree of Commit) and the working tree // depending on Mode. If empty MixedReset is used. Mode ResetMode + // Files, if not empty will constrain the reseting the index to only files + // specified in this list + Files []string } // Validate validates the fields and sets the default values. @@ -632,3 +635,26 @@ type PlainOpenOptions struct { // Validate validates the fields and sets the default values. func (o *PlainOpenOptions) Validate() error { return nil } + +var ( + ErrNoRestorePaths = errors.New("you must specify path(s) to restore") +) + +// RestoreOptions describes how a restore should be performed. +type RestoreOptions struct { + // Marks to restore the content in the index + Staged bool + // Marks to restore the content of the working tree + Worktree bool + // List of file paths that will be restored + Files []string +} + +// Validate validates the fields and sets the default values. +func (o *RestoreOptions) Validate() error { + if len(o.Files) == 0 { + return ErrNoRestorePaths + } + + return nil +} diff --git a/worktree.go b/worktree.go index f23d9f170..3ca54a8cb 100644 --- a/worktree.go +++ b/worktree.go @@ -26,11 +26,12 @@ import ( ) var ( - ErrWorktreeNotClean = errors.New("worktree is not clean") - ErrSubmoduleNotFound = errors.New("submodule not found") - ErrUnstagedChanges = errors.New("worktree contains unstaged changes") - ErrGitModulesSymlink = errors.New(gitmodulesFile + " is a symlink") - ErrNonFastForwardUpdate = errors.New("non-fast-forward update") + ErrWorktreeNotClean = errors.New("worktree is not clean") + ErrSubmoduleNotFound = errors.New("submodule not found") + ErrUnstagedChanges = errors.New("worktree contains unstaged changes") + ErrGitModulesSymlink = errors.New(gitmodulesFile + " is a symlink") + ErrNonFastForwardUpdate = errors.New("non-fast-forward update") + ErrRestoreWorktreeeOnlyNotSupported = errors.New("worktree only is not supported") ) // Worktree represents a git worktree. @@ -262,6 +263,41 @@ func (w *Worktree) setHEADToBranch(branch plumbing.ReferenceName, commit plumbin return w.r.Storer.SetReference(head) } +// Restore specified files in the working tree or stage with contents from +// a restore source. If a path is tracked but does not exist in the restore, +// source, it will be removed to match the source. +// +// If Staged and Worktree are true, then the restore source will be the index. +// If only Staged is true, then the restore source will be HEAD. +// If only Worktree is true or neither Staged nor Worktree are true, will +// result in ErrRestoreWorktreeeOnlyNotSupported because restoring the working +// tree while leaving the stage untouched is not currently supported +// +// Restore with no files specified will return ErrNoRestorePaths +func (w *Worktree) Restore(o *RestoreOptions) error { + if err := o.Validate(); err != nil { + return err + } + + if o.Worktree && o.Staged { + // If we are doing both Worktree and Staging then it is a hard reset + opts := &ResetOptions{ + Mode: HardReset, + Files: o.Files, + } + return w.Reset(opts) + } else if o.Staged { + // If we are doing just staging then it is a mixed reset + opts := &ResetOptions{ + Mode: MixedReset, + Files: o.Files, + } + return w.Reset(opts) + } else { + return ErrRestoreWorktreeeOnlyNotSupported + } +} + // Reset the worktree to a specified state. func (w *Worktree) Reset(opts *ResetOptions) error { if err := opts.Validate(w.r); err != nil { @@ -293,13 +329,13 @@ func (w *Worktree) Reset(opts *ResetOptions) error { } if opts.Mode == MixedReset || opts.Mode == MergeReset || opts.Mode == HardReset { - if err := w.resetIndex(t); err != nil { + if err := w.resetIndex(t, opts.Files); err != nil { return err } } if opts.Mode == MergeReset || opts.Mode == HardReset { - if err := w.resetWorktree(t); err != nil { + if err := w.resetWorktree(t, opts.Files); err != nil { return err } } @@ -307,7 +343,7 @@ func (w *Worktree) Reset(opts *ResetOptions) error { return nil } -func (w *Worktree) resetIndex(t *object.Tree) error { +func (w *Worktree) resetIndex(t *object.Tree, files []string) error { idx, err := w.r.Storer.Index() if err != nil { return err @@ -339,6 +375,13 @@ func (w *Worktree) resetIndex(t *object.Tree) error { name = ch.From.String() } + if len(files) > 0 { + contains := inFiles(files, name) + if !contains { + continue + } + } + b.Remove(name) if e == nil { continue @@ -356,7 +399,17 @@ func (w *Worktree) resetIndex(t *object.Tree) error { return w.r.Storer.SetIndex(idx) } -func (w *Worktree) resetWorktree(t *object.Tree) error { +func inFiles(files []string, v string) bool { + for _, s := range files { + if s == v { + return true + } + } + + return false +} + +func (w *Worktree) resetWorktree(t *object.Tree, files []string) error { changes, err := w.diffStagingWithWorktree(true) if err != nil { return err @@ -369,6 +422,20 @@ func (w *Worktree) resetWorktree(t *object.Tree) error { b := newIndexBuilder(idx) for _, ch := range changes { + if len(files) > 0 { + file := "" + if ch.From != nil { + file = ch.From.String() + } else { + file = ch.To.String() + } + + contains := inFiles(files, file) + if !contains { + continue + } + } + if err := w.checkoutChange(ch, t, b); err != nil { return err } diff --git a/worktree_test.go b/worktree_test.go index 79cbefd77..d64270436 100644 --- a/worktree_test.go +++ b/worktree_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "errors" + "fmt" "io" "io/ioutil" "os" @@ -22,6 +23,7 @@ import ( "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-git/go-git/v5/storage/memory" + "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-billy/v5/util" @@ -2242,3 +2244,231 @@ func (s *WorktreeSuite) TestLinkedWorktree(c *C) { c.Assert(err, Equals, ErrRepositoryIncomplete) } } + +var statusCodeNames = map[StatusCode]string{ + Unmodified: "Unmodified", + Untracked: "Untracked", + Modified: "Modified", + Added: "Added", + Deleted: "Deleted", + Renamed: "Renamed", + Copied: "Copied", + UpdatedButUnmerged: "UpdatedButUnmerged", +} + +func setupForRestore(c *C, s *WorktreeSuite) (fs billy.Filesystem, w *Worktree, names []string) { + fs = memfs.New() + w = &Worktree{ + r: s.Repository, + Filesystem: fs, + } + + err := w.Checkout(&CheckoutOptions{}) + c.Assert(err, IsNil) + + names = []string{"foo", "CHANGELOG", "LICENSE", "binary.jpg", "json/short.json", "json/medium.json", "json/long.json"} + verifyStatus(c, "Checkout", w, names, []FileStatus{ + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + }) + + // Touch of bunch of files including create a new file and delete an exsiting file + for i, name := range names { + contents := fmt.Sprintf("Foo Bar:%d", i) + err = util.WriteFile(fs, name, []byte(contents), 0755) + c.Assert(err, IsNil) + } + err = util.RemoveAll(fs, names[3]) + c.Assert(err, IsNil) + + //Confirm the status after doing the edits without staging anything + verifyStatus(c, "Edits", w, names, []FileStatus{ + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Deleted, Staging: Unmodified}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Modified, Staging: Unmodified}, + }) + + // Stage all files and verify the updated status + for _, name := range names { + _, err = w.Add(name) + c.Assert(err, IsNil) + } + verifyStatus(c, "Staged", w, names, []FileStatus{ + {Worktree: Unmodified, Staging: Added}, + {Worktree: Unmodified, Staging: Modified}, + {Worktree: Unmodified, Staging: Modified}, + {Worktree: Unmodified, Staging: Deleted}, + {Worktree: Unmodified, Staging: Modified}, + {Worktree: Unmodified, Staging: Added}, + {Worktree: Unmodified, Staging: Modified}, + }) + + // Add secondary changes to a file to make sure we only restore the staged file + err = util.WriteFile(fs, names[1], []byte("Foo Bar:11"), 0755) + c.Assert(err, IsNil) + err = util.WriteFile(fs, names[2], []byte("Foo Bar:22"), 0755) + c.Assert(err, IsNil) + err = util.WriteFile(fs, names[4], []byte("Foo Bar:44"), 0755) + c.Assert(err, IsNil) + + verifyStatus(c, "Secondary Edits", w, names, []FileStatus{ + {Worktree: Unmodified, Staging: Added}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Unmodified, Staging: Deleted}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Unmodified, Staging: Added}, + {Worktree: Unmodified, Staging: Modified}, + }) + + return +} + +func verifyStatus(c *C, marker string, w *Worktree, files []string, statuses []FileStatus) { + c.Assert(len(files), Equals, len(statuses), Commentf("%s - Incorrect number of statuses %d != %d", marker, len(files), len(statuses))) + + status, err := w.Status() + c.Assert(err, IsNil) + + for i, file := range files { + current := status.File(file) + expected := statuses[i] + c.Assert(current.Worktree, Equals, expected.Worktree, Commentf("%s - [%d] : %s Worktree %s != %s", marker, i, file, statusCodeNames[current.Worktree], statusCodeNames[expected.Worktree])) + c.Assert(current.Staging, Equals, expected.Staging, Commentf("%s - [%d] : %s Staging %s != %s", marker, i, file, statusCodeNames[current.Staging], statusCodeNames[expected.Staging])) + } +} + +func (s *WorktreeSuite) TestRestoreStaged(c *C) { + fs, w, names := setupForRestore(c, s) + + //Attempt without files should throw an error like the git restore --staged + opts := RestoreOptions{Staged: true} + err := w.Restore(&opts) + c.Assert(err, Equals, ErrNoRestorePaths) + + // Restore Staged files in 2 groups and confirm status + opts.Files = []string{names[0], names[1]} + err = w.Restore(&opts) + c.Assert(err, IsNil) + verifyStatus(c, "Restored First", w, names, []FileStatus{ + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Unmodified, Staging: Deleted}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Unmodified, Staging: Added}, + {Worktree: Unmodified, Staging: Modified}, + }) + + //Make sure the restore didn't overwrite our secondary changes + contents, err := util.ReadFile(fs, names[1]) + c.Assert(err, IsNil) + c.Assert(string(contents), Equals, "Foo Bar:11") + + opts.Files = []string{names[2], names[3]} + err = w.Restore(&opts) + c.Assert(err, IsNil) + verifyStatus(c, "Restored Second", w, names, []FileStatus{ + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Deleted, Staging: Unmodified}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Unmodified, Staging: Added}, + {Worktree: Unmodified, Staging: Modified}, + }) + + //Make sure the restore didn't overwrite our secondary changes + contents, err = util.ReadFile(fs, names[2]) + c.Assert(err, IsNil) + c.Assert(string(contents), Equals, "Foo Bar:22") + + opts.Files = []string{names[4], names[5], names[6]} + err = w.Restore(&opts) + c.Assert(err, IsNil) + verifyStatus(c, "Restored Third", w, names, []FileStatus{ + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Deleted, Staging: Unmodified}, + {Worktree: Modified, Staging: Unmodified}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Modified, Staging: Unmodified}, + }) + + //Make sure the restore didn't overwrite our secondary changes + contents, err = util.ReadFile(fs, names[4]) + c.Assert(err, IsNil) + c.Assert(string(contents), Equals, "Foo Bar:44") +} + +func (s *WorktreeSuite) TestRestoreWorktree(c *C) { + _, w, names := setupForRestore(c, s) + + //Attempt without files should throw an error like the git restore + opts := RestoreOptions{} + err := w.Restore(&opts) + c.Assert(err, Equals, ErrNoRestorePaths) + + opts.Files = []string{names[0], names[1]} + err = w.Restore(&opts) + c.Assert(err, Equals, ErrRestoreWorktreeeOnlyNotSupported) +} + +func (s *WorktreeSuite) TestRestoreBoth(c *C) { + _, w, names := setupForRestore(c, s) + + //Attempt without files should throw an error like the git restore --staged --worktree + opts := RestoreOptions{Staged: true, Worktree: true} + err := w.Restore(&opts) + c.Assert(err, Equals, ErrNoRestorePaths) + + // Restore Staged files in 2 groups and confirm status + opts.Files = []string{names[0], names[1]} + err = w.Restore(&opts) + c.Assert(err, IsNil) + verifyStatus(c, "Restored First", w, names, []FileStatus{ + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Unmodified, Staging: Deleted}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Unmodified, Staging: Added}, + {Worktree: Unmodified, Staging: Modified}, + }) + + opts.Files = []string{names[2], names[3]} + err = w.Restore(&opts) + c.Assert(err, IsNil) + verifyStatus(c, "Restored Second", w, names, []FileStatus{ + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Modified, Staging: Modified}, + {Worktree: Unmodified, Staging: Added}, + {Worktree: Unmodified, Staging: Modified}, + }) + + opts.Files = []string{names[4], names[5], names[6]} + err = w.Restore(&opts) + c.Assert(err, IsNil) + verifyStatus(c, "Restored Third", w, names, []FileStatus{ + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + {Worktree: Untracked, Staging: Untracked}, + }) +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy