Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 0f36397

Browse files
committed
git: worktree, add RestoreStaged which works like the "git restore --staged <file>..." command
Small formatting and style fixes before rebasing against master Setup args for restore in TestExamples Fix typo in error message and remove dependency on fmt in worktree_test
1 parent 302ddde commit 0f36397

File tree

5 files changed

+382
-9
lines changed

5 files changed

+382
-9
lines changed

_examples/common_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ var args = map[string][]string{
3030
"progress": {defaultURL, tempFolder()},
3131
"pull": {createRepositoryWithRemote(tempFolder(), defaultURL)},
3232
"push": {setEmptyRemote(cloneRepository(defaultURL, tempFolder()))},
33+
"restore": {cloneRepository(defaultURL, tempFolder())},
3334
"revision": {cloneRepository(defaultURL, tempFolder()), "master~2^"},
3435
"sha256": {tempFolder()},
3536
"showcase": {defaultURL, tempFolder()},

_examples/restore/main.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io/ioutil"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
10+
"github.com/go-git/go-git/v5"
11+
. "github.com/go-git/go-git/v5/_examples"
12+
"github.com/go-git/go-git/v5/plumbing/object"
13+
)
14+
15+
func prepareRepo(w *git.Worktree, directory string) {
16+
// We need a known state of files inside the worktree for testing revert a modify and delete
17+
Info("echo \"hello world! Modify\" > for-modify")
18+
err := ioutil.WriteFile(filepath.Join(directory, "for-modify"), []byte("hello world! Modify"), 0644)
19+
CheckIfError(err)
20+
Info("git add for-modify")
21+
_, err = w.Add("for-modify")
22+
CheckIfError(err)
23+
24+
Info("echo \"hello world! Delete\" > for-delete")
25+
err = ioutil.WriteFile(filepath.Join(directory, "for-delete"), []byte("hello world! Delete"), 0644)
26+
CheckIfError(err)
27+
Info("git add for-delete")
28+
_, err = w.Add("for-delete")
29+
CheckIfError(err)
30+
31+
Info("git commit -m \"example go-git commit\"")
32+
_, err = w.Commit("example go-git commit", &git.CommitOptions{
33+
Author: &object.Signature{
34+
Name: "John Doe",
35+
36+
When: time.Now(),
37+
},
38+
})
39+
CheckIfError(err)
40+
}
41+
42+
// An example of how to restore AKA unstage files
43+
func main() {
44+
CheckArgs("<directory>")
45+
directory := os.Args[1]
46+
47+
// Opens an already existing repository.
48+
r, err := git.PlainOpen(directory)
49+
CheckIfError(err)
50+
51+
w, err := r.Worktree()
52+
CheckIfError(err)
53+
54+
prepareRepo(w, directory)
55+
56+
// Perform the operation and stage them
57+
Info("echo \"hello world! Modify 2\" > for-modify")
58+
err = ioutil.WriteFile(filepath.Join(directory, "for-modify"), []byte("hello world! Modify 2"), 0644)
59+
CheckIfError(err)
60+
Info("git add for-modify")
61+
_, err = w.Add("for-modify")
62+
CheckIfError(err)
63+
64+
Info("echo \"hello world! Add\" > for-add")
65+
err = ioutil.WriteFile(filepath.Join(directory, "for-add"), []byte("hello world! Add"), 0644)
66+
CheckIfError(err)
67+
Info("git add for-add")
68+
_, err = w.Add("for-add")
69+
CheckIfError(err)
70+
71+
Info("rm for-delete")
72+
err = os.Remove(filepath.Join(directory, "for-delete"))
73+
CheckIfError(err)
74+
Info("git add for-delete")
75+
_, err = w.Add("for-delete")
76+
CheckIfError(err)
77+
78+
// We can verify the current status of the worktree using the method Status.
79+
Info("git status --porcelain")
80+
status, err := w.Status()
81+
CheckIfError(err)
82+
fmt.Println(status)
83+
84+
// Unstage a single file and see the status
85+
Info("git restore --staged for-modify")
86+
err = w.Restore(&git.RestoreOptions{Staged: true, Files: []string{"for-modify"}})
87+
CheckIfError(err)
88+
89+
Info("git status --porcelain")
90+
status, err = w.Status()
91+
CheckIfError(err)
92+
fmt.Println(status)
93+
94+
// Unstage the other 2 files and see the status
95+
Info("git restore --staged for-add for-delete")
96+
err = w.Restore(&git.RestoreOptions{Staged: true, Files: []string{"for-add", "for-delete"}})
97+
CheckIfError(err)
98+
99+
Info("git status --porcelain")
100+
status, err = w.Status()
101+
CheckIfError(err)
102+
fmt.Println(status)
103+
}

options.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,9 @@ type ResetOptions struct {
416416
// the index (resetting it to the tree of Commit) and the working tree
417417
// depending on Mode. If empty MixedReset is used.
418418
Mode ResetMode
419+
// Files, if not empty will constrain the reseting the index to only files
420+
// specified in this list.
421+
Files []string
419422
}
420423

421424
// Validate validates the fields and sets the default values.
@@ -790,3 +793,26 @@ type PlainInitOptions struct {
790793

791794
// Validate validates the fields and sets the default values.
792795
func (o *PlainInitOptions) Validate() error { return nil }
796+
797+
var (
798+
ErrNoRestorePaths = errors.New("you must specify path(s) to restore")
799+
)
800+
801+
// RestoreOptions describes how a restore should be performed.
802+
type RestoreOptions struct {
803+
// Marks to restore the content in the index
804+
Staged bool
805+
// Marks to restore the content of the working tree
806+
Worktree bool
807+
// List of file paths that will be restored
808+
Files []string
809+
}
810+
811+
// Validate validates the fields and sets the default values.
812+
func (o *RestoreOptions) Validate() error {
813+
if len(o.Files) == 0 {
814+
return ErrNoRestorePaths
815+
}
816+
817+
return nil
818+
}

worktree.go

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ import (
2525
)
2626

2727
var (
28-
ErrWorktreeNotClean = errors.New("worktree is not clean")
29-
ErrSubmoduleNotFound = errors.New("submodule not found")
30-
ErrUnstagedChanges = errors.New("worktree contains unstaged changes")
31-
ErrGitModulesSymlink = errors.New(gitmodulesFile + " is a symlink")
32-
ErrNonFastForwardUpdate = errors.New("non-fast-forward update")
28+
ErrWorktreeNotClean = errors.New("worktree is not clean")
29+
ErrSubmoduleNotFound = errors.New("submodule not found")
30+
ErrUnstagedChanges = errors.New("worktree contains unstaged changes")
31+
ErrGitModulesSymlink = errors.New(gitmodulesFile + " is a symlink")
32+
ErrNonFastForwardUpdate = errors.New("non-fast-forward update")
33+
ErrRestoreWorktreeOnlyNotSupported = errors.New("worktree only is not supported")
3334
)
3435

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

309310
if opts.Mode == MixedReset || opts.Mode == MergeReset || opts.Mode == HardReset {
310-
if err := w.resetIndex(t, dirs); err != nil {
311+
if err := w.resetIndex(t, dirs, opts.Files); err != nil {
311312
return err
312313
}
313314
}
314315

315316
if opts.Mode == MergeReset || opts.Mode == HardReset {
316-
if err := w.resetWorktree(t); err != nil {
317+
if err := w.resetWorktree(t, opts.Files); err != nil {
317318
return err
318319
}
319320
}
320321

321322
return nil
322323
}
323324

325+
// Restore restores specified files in the working tree or stage with contents from
326+
// a restore source. If a path is tracked but does not exist in the restore,
327+
// source, it will be removed to match the source.
328+
//
329+
// If Staged and Worktree are true, then the restore source will be the index.
330+
// If only Staged is true, then the restore source will be HEAD.
331+
// If only Worktree is true or neither Staged nor Worktree are true, will
332+
// result in ErrRestoreWorktreeOnlyNotSupported because restoring the working
333+
// tree while leaving the stage untouched is not currently supported.
334+
//
335+
// Restore with no files specified will return ErrNoRestorePaths.
336+
func (w *Worktree) Restore(o *RestoreOptions) error {
337+
if err := o.Validate(); err != nil {
338+
return err
339+
}
340+
341+
if o.Staged {
342+
opts := &ResetOptions{
343+
Files: o.Files,
344+
}
345+
346+
if o.Worktree {
347+
// If we are doing both Worktree and Staging then it is a hard reset
348+
opts.Mode = HardReset
349+
} else {
350+
// If we are doing just staging then it is a mixed reset
351+
opts.Mode = MixedReset
352+
}
353+
354+
return w.Reset(opts)
355+
}
356+
357+
return ErrRestoreWorktreeOnlyNotSupported
358+
}
359+
324360
// Reset the worktree to a specified state.
325361
func (w *Worktree) Reset(opts *ResetOptions) error {
326362
return w.ResetSparsely(opts, nil)
327363
}
328364

329-
func (w *Worktree) resetIndex(t *object.Tree, dirs []string) error {
365+
func (w *Worktree) resetIndex(t *object.Tree, dirs []string, files []string) error {
330366
idx, err := w.r.Storer.Index()
331367
if len(dirs) > 0 {
332368
idx.SkipUnless(dirs)
@@ -362,6 +398,13 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string) error {
362398
name = ch.From.String()
363399
}
364400

401+
if len(files) > 0 {
402+
contains := inFiles(files, name)
403+
if !contains {
404+
continue
405+
}
406+
}
407+
365408
b.Remove(name)
366409
if e == nil {
367410
continue
@@ -379,7 +422,17 @@ func (w *Worktree) resetIndex(t *object.Tree, dirs []string) error {
379422
return w.r.Storer.SetIndex(idx)
380423
}
381424

382-
func (w *Worktree) resetWorktree(t *object.Tree) error {
425+
func inFiles(files []string, v string) bool {
426+
for _, s := range files {
427+
if s == v {
428+
return true
429+
}
430+
}
431+
432+
return false
433+
}
434+
435+
func (w *Worktree) resetWorktree(t *object.Tree, files []string) error {
383436
changes, err := w.diffStagingWithWorktree(true, false)
384437
if err != nil {
385438
return err
@@ -395,6 +448,25 @@ func (w *Worktree) resetWorktree(t *object.Tree) error {
395448
if err := w.validChange(ch); err != nil {
396449
return err
397450
}
451+
452+
if len(files) > 0 {
453+
file := ""
454+
if ch.From != nil {
455+
file = ch.From.Name()
456+
} else if ch.To != nil {
457+
file = ch.To.Name()
458+
}
459+
460+
if file == "" {
461+
continue
462+
}
463+
464+
contains := inFiles(files, file)
465+
if !contains {
466+
continue
467+
}
468+
}
469+
398470
if err := w.checkoutChange(ch, t, b); err != nil {
399471
return err
400472
}

0 commit comments

Comments
 (0)