Skip to content

Add RestoreStaged to Worktree that mimics the behaviour of git restore --staged <file>... #493

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions _examples/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var args = map[string][]string{
"progress": {defaultURL, tempFolder()},
"pull": {createRepositoryWithRemote(tempFolder(), defaultURL)},
"push": {setEmptyRemote(cloneRepository(defaultURL, tempFolder()))},
"restore": {cloneRepository(defaultURL, tempFolder())},
"revision": {cloneRepository(defaultURL, tempFolder()), "master~2^"},
"sha256": {tempFolder()},
"showcase": {defaultURL, tempFolder()},
Expand Down
103 changes: 103 additions & 0 deletions _examples/restore/main.go
Original file line number Diff line number Diff line change
@@ -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>")
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)
}
26 changes: 26 additions & 0 deletions options.go
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,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.
Expand Down Expand Up @@ -790,3 +793,26 @@ type PlainInitOptions struct {

// Validate validates the fields and sets the default values.
func (o *PlainInitOptions) 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
}
90 changes: 81 additions & 9 deletions worktree.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,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")
ErrRestoreWorktreeOnlyNotSupported = errors.New("worktree only is not supported")
)

// Worktree represents a git worktree.
Expand Down Expand Up @@ -307,26 +308,61 @@ func (w *Worktree) ResetSparsely(opts *ResetOptions, dirs []string) error {
}

if opts.Mode == MixedReset || opts.Mode == MergeReset || opts.Mode == HardReset {
if err := w.resetIndex(t, dirs); err != nil {
if err := w.resetIndex(t, dirs, 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
}
}

return nil
}

// Restore restores 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 ErrRestoreWorktreeOnlyNotSupported 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.Staged {
opts := &ResetOptions{
Files: o.Files,
}

if o.Worktree {
// If we are doing both Worktree and Staging then it is a hard reset
opts.Mode = HardReset
} else {
// If we are doing just staging then it is a mixed reset
opts.Mode = MixedReset
}

return w.Reset(opts)
}

return ErrRestoreWorktreeOnlyNotSupported
}

// Reset the worktree to a specified state.
func (w *Worktree) Reset(opts *ResetOptions) error {
return w.ResetSparsely(opts, nil)
}

func (w *Worktree) resetIndex(t *object.Tree, dirs []string) error {
func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) error {
idx, err := w.r.Storer.Index()
if len(dirs) > 0 {
idx.SkipUnless(dirs)
Expand Down Expand Up @@ -362,6 +398,13 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string) error {
name = ch.From.String()
}

if len(files) > 0 {
contains := inFiles(files, name)
if !contains {
continue
}
}

b.Remove(name)
if e == nil {
continue
Expand All @@ -379,7 +422,17 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string) 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, false)
if err != nil {
return err
Expand All @@ -395,6 +448,25 @@ func (w *Worktree) resetWorktree(t *object.Tree) error {
if err := w.validChange(ch); err != nil {
return err
}

if len(files) > 0 {
file := ""
if ch.From != nil {
file = ch.From.Name()
} else if ch.To != nil {
file = ch.To.Name()
}

if file == "" {
continue
}

contains := inFiles(files, file)
if !contains {
continue
}
}

if err := w.checkoutChange(ch, t, b); err != nil {
return err
}
Expand Down
Loading
Loading