Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Commit b926156

Browse files
authored
Merge pull request #1279 from sdboyer/git-autorecover
gps: Adaptively clean dirty git repos
2 parents b92b332 + 123ba6b commit b926156

6 files changed

+174
-7
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
BUG FIXES:
44

55
* Releases targeting Windows now have a `.exe` suffix (#1291).
6+
* Adaptively recover from dirty and corrupted git repositories in cache (#1279).
67

78
IMPROVEMENTS:
89

internal/gps/manager_test.go

-6
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,6 @@ func TestSourceInit(t *testing.T) {
259259

260260
os.Stat(filepath.Join(cpath, "metadata", "github.com", "sdboyer", "gpkt", "cache.json"))
261261

262-
// TODO(sdboyer) disabled until we get caching working
263-
//_, err = os.Stat(filepath.Join(cpath, "metadata", "github.com", "sdboyer", "gpkt", "cache.json"))
264-
//if err != nil {
265-
//t.Error("Metadata cache json file does not exist in expected location")
266-
//}
267-
268262
// Ensure source existence values are what we expect
269263
var exists bool
270264
exists, err = sm.SourceExists(id)

internal/gps/maybe_source.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -100,13 +100,21 @@ func (m maybeGitSource) try(ctx context.Context, cachedir string, c singleSource
100100
return nil, 0, err
101101
}
102102

103-
c.setVersionMap(vl)
104103
state := sourceIsSetUp | sourceExistsUpstream | sourceHasLatestVersionList
105104

106105
if r.CheckLocal() {
107106
state |= sourceExistsLocally
107+
108+
if err := superv.do(ctx, "git", ctValidateLocal, func(ctx context.Context) error {
109+
// If repository already exists on disk, make a pass to be sure
110+
// everything's clean.
111+
return src.ensureClean(ctx)
112+
}); err != nil {
113+
return nil, 0, err
114+
}
108115
}
109116

117+
c.setVersionMap(vl)
110118
return src, state, nil
111119
}
112120

internal/gps/source_manager.go

+1
Original file line numberDiff line numberDiff line change
@@ -738,6 +738,7 @@ const (
738738
ctSourceInit
739739
ctSourceFetch
740740
ctExportTree
741+
ctValidateLocal
741742
)
742743

743744
func (ct callType) String() string {

internal/gps/vcs_source.go

+68
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,74 @@ type gitSource struct {
123123
baseVCSSource
124124
}
125125

126+
// ensureClean sees to it that a git repository is clean and in working order,
127+
// or returns an error if the adaptive recovery attempts fail.
128+
func (s *gitSource) ensureClean(ctx context.Context) error {
129+
r := s.repo.(*gitRepo)
130+
cmd := commandContext(
131+
ctx,
132+
"git",
133+
"status",
134+
"--porcelain",
135+
)
136+
cmd.SetDir(r.LocalPath())
137+
138+
out, err := cmd.CombinedOutput()
139+
if err != nil {
140+
// An error on simple git status indicates some aggressive repository
141+
// corruption, outside of the purview that we can deal with here.
142+
return err
143+
}
144+
145+
if len(bytes.TrimSpace(out)) == 0 {
146+
// No output from status indicates a clean tree, without any modified or
147+
// untracked files - we're in good shape.
148+
return nil
149+
}
150+
151+
// We could be more parsimonious about this, but it's probably not worth it
152+
// - it's a rare case to have to do any cleanup anyway, so when we do, we
153+
// might as well just throw the kitchen sink at it.
154+
cmd = commandContext(
155+
ctx,
156+
"git",
157+
"reset",
158+
"--hard",
159+
)
160+
cmd.SetDir(r.LocalPath())
161+
_, err = cmd.CombinedOutput()
162+
if err != nil {
163+
return err
164+
}
165+
166+
// We also need to git clean -df; just reuse defendAgainstSubmodules here,
167+
// even though it's a bit layer-breaky.
168+
err = r.defendAgainstSubmodules(ctx)
169+
if err != nil {
170+
return err
171+
}
172+
173+
// Check status one last time. If it's still not clean, give up.
174+
cmd = commandContext(
175+
ctx,
176+
"git",
177+
"status",
178+
"--porcelain",
179+
)
180+
cmd.SetDir(r.LocalPath())
181+
182+
out, err = cmd.CombinedOutput()
183+
if err != nil {
184+
return err
185+
}
186+
187+
if len(bytes.TrimSpace(out)) != 0 {
188+
return errors.Errorf("failed to clean up git repository at %s - dirty? corrupted? status output: \n%s", r.LocalPath(), string(out))
189+
}
190+
191+
return nil
192+
}
193+
126194
func (s *gitSource) exportRevisionTo(ctx context.Context, rev Revision, to string) error {
127195
r := s.repo
128196

internal/gps/vcs_source_test.go

+95
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package gps
77
import (
88
"context"
99
"io/ioutil"
10+
"log"
1011
"net/url"
1112
"os"
1213
"os/exec"
@@ -647,6 +648,100 @@ func TestGitSourceListVersionsNoDupes(t *testing.T) {
647648
}
648649
}
649650

651+
func TestGitSourceAdaptiveCleanup(t *testing.T) {
652+
t.Parallel()
653+
654+
// This test is slowish, skip it on -short
655+
if testing.Short() {
656+
t.Skip("Skipping git adaptive failure recovery test in short mode")
657+
}
658+
requiresBins(t, "git")
659+
660+
cpath, err := ioutil.TempDir("", "smcache")
661+
if err != nil {
662+
t.Fatalf("Failed to create temp dir: %s", err)
663+
}
664+
665+
var sm *SourceMgr
666+
mkSM := func() {
667+
// If sm is already set, make sure it's released, then create a new one.
668+
if sm != nil {
669+
sm.Release()
670+
}
671+
672+
var err error
673+
sm, err = NewSourceManager(SourceManagerConfig{
674+
Cachedir: cpath,
675+
Logger: log.New(test.Writer{TB: t}, "", 0),
676+
})
677+
if err != nil {
678+
t.Fatalf("Unexpected error on SourceManager creation: %s", err)
679+
}
680+
}
681+
682+
mkSM()
683+
id := mkPI("github.com/sdboyer/gpkt")
684+
err = sm.SyncSourceFor(id)
685+
if err != nil {
686+
t.Fatal(err)
687+
}
688+
689+
repodir := filepath.Join(sm.cachedir, "sources", "https---github.lhy31512.workers.dev-sdboyer-gpkt")
690+
if _, err := os.Stat(repodir); err != nil {
691+
if os.IsNotExist(err) {
692+
t.Fatalf("expected location for repodir did not exist: %q", repodir)
693+
} else {
694+
t.Fatal(err)
695+
}
696+
}
697+
698+
// Create a file that git will see as untracked.
699+
untrackedPath := filepath.Join(repodir, "untrackedfile")
700+
err = ioutil.WriteFile(untrackedPath, []byte("foo"), 0644)
701+
if err != nil {
702+
t.Fatal(err)
703+
}
704+
705+
mkSM()
706+
err = sm.SyncSourceFor(id)
707+
if err != nil {
708+
t.Fatalf("choked after adding dummy file: %q", err)
709+
}
710+
711+
if _, err := os.Stat(untrackedPath); err == nil {
712+
t.Fatal("untracked file still existed after cleanup should've been triggered")
713+
}
714+
715+
// Remove a file that we know exists, which `git status` checks should catch.
716+
readmePath := filepath.Join(repodir, "README.md")
717+
os.Remove(readmePath)
718+
719+
mkSM()
720+
err = sm.SyncSourceFor(id)
721+
if err != nil {
722+
t.Fatalf("choked after removing known file: %q", err)
723+
}
724+
725+
if _, err := os.Stat(readmePath); err != nil {
726+
t.Fatal("README was still absent after cleanup should've been triggered")
727+
}
728+
729+
// Remove .git/objects directory, which should make git bite it.
730+
err = os.RemoveAll(filepath.Join(repodir, ".git", "objects"))
731+
if err != nil {
732+
t.Fatal(err)
733+
}
734+
735+
mkSM()
736+
err = sm.SyncSourceFor(id)
737+
if err != nil {
738+
t.Fatalf("choked after removing .git/objects directory: %q", err)
739+
}
740+
741+
sm.Release()
742+
os.RemoveAll(cpath)
743+
}
744+
650745
func Test_bzrSource_exportRevisionTo_removeVcsFiles(t *testing.T) {
651746
t.Parallel()
652747

0 commit comments

Comments
 (0)