Skip to content

Commit 0f36397

Browse files
author
Ben Talbot
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+
Email: "john@doe.org",
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)
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