Skip to content

Commit f162ba3

Browse files
authored
Merge pull request #42 from per1234/reset
Improve handling of erroneous dirty library repo state after release checkout
2 parents 422a572 + d29bc2d commit f162ba3

File tree

13 files changed

+179
-42
lines changed

13 files changed

+179
-42
lines changed

.prettierignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# See: https://prettier.io/docs/en/ignore.html#ignoring-files-prettierignore
22

3-
/test/testdata/golden/logs/
3+
/test/testdata/test_all/golden/logs/

internal/libraries/git_integration_test.go

+1-10
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import (
3131

3232
"arduino.cc/repository/internal/libraries/db"
3333
"arduino.cc/repository/internal/libraries/gitutils"
34-
"github.com/go-git/go-git/v5"
3534
"github.com/stretchr/testify/require"
3635
)
3736

@@ -63,15 +62,7 @@ func TestUpdateLibraryJson(t *testing.T) {
6362
tag, err := tags.Next()
6463
require.NoError(t, err)
6564

66-
repoTree, err := r.Repository.Worktree()
67-
require.NoError(t, err)
68-
// Annotated tags have their own hash, different from the commit hash, so the tag must be resolved before checkout
69-
resolvedTag, err := gitutils.ResolveTag(tag, r.Repository)
70-
require.NoError(t, err)
71-
err = repoTree.Checkout(&git.CheckoutOptions{Hash: *resolvedTag, Force: true})
72-
require.NoError(t, err)
73-
err = repoTree.Clean(&git.CleanOptions{Dir: true})
74-
require.NoError(t, err)
65+
err = gitutils.CheckoutTag(r.Repository, tag)
7566

7667
library, err := GenerateLibraryFromRepo(r)
7768
require.NoError(t, err)

internal/libraries/gitutils/gitutils.go

+67-3
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,16 @@
2424
package gitutils
2525

2626
import (
27+
"fmt"
2728
"sort"
2829

2930
"github.com/go-git/go-git/v5"
3031
"github.com/go-git/go-git/v5/plumbing"
3132
"github.com/go-git/go-git/v5/plumbing/object"
3233
)
3334

34-
// ResolveTag returns the commit hash associated with a tag.
35-
func ResolveTag(tag *plumbing.Reference, repository *git.Repository) (*plumbing.Hash, error) {
35+
// resolveTag returns the commit hash associated with a tag.
36+
func resolveTag(tag *plumbing.Reference, repository *git.Repository) (*plumbing.Hash, error) {
3637
// Annotated tags have their own hash, different from the commit hash, so the tag must be resolved to get the has for
3738
// the associated commit.
3839
// Tags may point to any Git object. Although not common, this can include tree and blob objects in addition to commits.
@@ -117,7 +118,7 @@ func SortedCommitTags(repository *git.Repository) ([]*plumbing.Reference, error)
117118

118119
// Annotated tags have their own hash, different from the commit hash, so tags must be resolved before
119120
// cross-referencing against the commit hashes.
120-
resolvedTag, err := ResolveTag(tag, repository)
121+
resolvedTag, err := resolveTag(tag, repository)
121122
if err != nil {
122123
// Non-commit object tags are not included in the sorted list.
123124
continue
@@ -156,3 +157,66 @@ func SortedCommitTags(repository *git.Repository) ([]*plumbing.Reference, error)
156157

157158
return sortedTags, nil
158159
}
160+
161+
// CheckoutTag checks out the repository to the given tag.
162+
func CheckoutTag(repository *git.Repository, tag *plumbing.Reference) error {
163+
repoTree, err := repository.Worktree()
164+
if err != nil {
165+
return err
166+
}
167+
168+
// Annotated tags have their own hash, different from the commit hash, so the tag must be resolved before checkout
169+
resolvedTag, err := resolveTag(tag, repository)
170+
if err != nil {
171+
return err
172+
}
173+
174+
if err = repoTree.Checkout(&git.CheckoutOptions{Hash: *resolvedTag, Force: true}); err != nil {
175+
return err
176+
}
177+
178+
// Ensure the repository is checked out to a clean state.
179+
// Because it might not succeed on the first attempt, a retry is allowed.
180+
for range [2]int{} {
181+
clean, err := cleanRepository(repoTree)
182+
if err != nil {
183+
return err
184+
}
185+
if clean {
186+
return nil
187+
}
188+
}
189+
190+
return fmt.Errorf("failed to get repository to clean state")
191+
}
192+
193+
func cleanRepository(repoTree *git.Worktree) (bool, error) {
194+
// Remove now-empty folders which are left behind after checkout. These would not be removed by the reset action.
195+
// Remove untracked files. These would also be removed by the reset action.
196+
if err := repoTree.Clean(&git.CleanOptions{Dir: true}); err != nil {
197+
return false, err
198+
}
199+
200+
// Remove untracked files and reset tracked files to clean state.
201+
// Even though in theory it shouldn't ever be necessary to do a hard reset in this application, under certain
202+
// circumstances, go-git can fail to complete checkout, while not even returning an error. This results in an
203+
// unexpected dirty repository state, which is corrected via a hard reset.
204+
// See: https://github.com/go-git/go-git/issues/99
205+
if err := repoTree.Reset(&git.ResetOptions{Mode: git.HardReset}); err != nil {
206+
return false, err
207+
}
208+
209+
// Get status to detect some forms of failed cleaning.
210+
repoStatus, err := repoTree.Status()
211+
if err != nil {
212+
return false, err
213+
}
214+
215+
// IsClean() detects:
216+
// - Untracked files
217+
// - Modified tracked files
218+
// This does not detect:
219+
// - Empty directories
220+
// - Ignored files
221+
return repoStatus.IsClean(), nil
222+
}

internal/libraries/gitutils/gitutils_test.go

+63-3
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func TestResolveTag(t *testing.T) {
8484
} {
8585
testName := fmt.Sprintf("%s, %s", testTable.objectTypeName, annotationConfig.descriptor)
8686
tag := makeTag(t, repository, testName, testTable.objectHash, annotationConfig.annotated)
87-
resolvedTag, err := ResolveTag(tag, repository)
87+
resolvedTag, err := resolveTag(tag, repository)
8888
testTable.errorAssertion(t, err, fmt.Sprintf("%s tag resolution error", testName))
8989
if err == nil {
9090
assert.Equal(t, testTable.objectHash, *resolvedTag, fmt.Sprintf("%s tag resolution", testName))
@@ -139,9 +139,69 @@ func TestSortedCommitTags(t *testing.T) {
139139
assert.Equal(t, tags, sorted)
140140
}
141141

142+
func TestCheckoutTag(t *testing.T) {
143+
// Create a folder for the test repository.
144+
repositoryPath, err := paths.TempDir().MkTempDir("gitutils-TestCheckoutTag-repo")
145+
require.NoError(t, err)
146+
147+
// Create test repository.
148+
repository, err := git.PlainInit(repositoryPath.String(), false)
149+
require.NoError(t, err)
150+
151+
// Generate meaningless commit history, creating some tags along the way.
152+
var tags []*plumbing.Reference
153+
tags = append(tags, makeTag(t, repository, "1.0.0", makeCommit(t, repository, repositoryPath), true))
154+
makeCommit(t, repository, repositoryPath)
155+
makeCommit(t, repository, repositoryPath)
156+
tags = append(tags, makeTag(t, repository, "1.0.1", makeCommit(t, repository, repositoryPath), true))
157+
makeCommit(t, repository, repositoryPath)
158+
makeTag(t, repository, "tree-tag", getTreeHash(t, repository), true)
159+
tags = append(tags, makeTag(t, repository, "1.0.2", makeCommit(t, repository, repositoryPath), true))
160+
makeTag(t, repository, "blob-tag", getBlobHash(t, repository), true)
161+
trackedFilePath, _ := commitFile(t, repository, repositoryPath)
162+
163+
for _, tag := range tags {
164+
// Put the repository into a dirty state.
165+
// Add an untracked file.
166+
_, err = paths.WriteToTempFile([]byte{}, repositoryPath, "gitutils-TestCheckoutTag-tempfile")
167+
require.NoError(t, err)
168+
// Modify a tracked file.
169+
err = trackedFilePath.WriteFile([]byte{42})
170+
require.NoError(t, err)
171+
// Create empty folder.
172+
emptyFolderPath, err := repositoryPath.MkTempDir("gitutils-TestCheckoutTag-emptyFolder")
173+
require.NoError(t, err)
174+
175+
err = CheckoutTag(repository, tag)
176+
assert.NoError(t, err, fmt.Sprintf("Checking out tag %s", tag))
177+
178+
expectedHash, err := resolveTag(tag, repository)
179+
require.NoError(t, err)
180+
headRef, err := repository.Head()
181+
require.NoError(t, err)
182+
assert.Equal(t, *expectedHash, headRef.Hash(), "HEAD is at tag")
183+
184+
// Check if cleanup was successful.
185+
tree, err := repository.Worktree()
186+
require.NoError(t, err)
187+
status, err := tree.Status()
188+
require.NoError(t, err)
189+
assert.True(t, status.IsClean(), "Repository is clean")
190+
emptyFolderExists, err := emptyFolderPath.ExistCheck()
191+
require.NoError(t, err)
192+
assert.False(t, emptyFolderExists, "Empty folder was removed")
193+
}
194+
}
195+
142196
// makeCommit creates a test commit in the given repository and returns its plumbing.Hash object.
143197
func makeCommit(t *testing.T, repository *git.Repository, repositoryPath *paths.Path) plumbing.Hash {
144-
_, err := paths.WriteToTempFile([]byte{}, repositoryPath, "gitutils-makeCommit-tempfile")
198+
_, hash := commitFile(t, repository, repositoryPath)
199+
return hash
200+
}
201+
202+
// commitFile commits a file in the given repository and returns its path and the commit's plumbing.Hash object.
203+
func commitFile(t *testing.T, repository *git.Repository, repositoryPath *paths.Path) (*paths.Path, plumbing.Hash) {
204+
filePath, err := paths.WriteToTempFile([]byte{}, repositoryPath, "gitutils-makeCommit-tempfile")
145205
require.Nil(t, err)
146206

147207
worktree, err := repository.Worktree()
@@ -163,7 +223,7 @@ func makeCommit(t *testing.T, repository *git.Repository, repositoryPath *paths.
163223
)
164224
require.Nil(t, err)
165225

166-
return commit
226+
return filePath, commit
167227
}
168228

169229
// getTreeHash returns the plumbing.Hash object for an arbitrary Git tree object.

sync_libraries.go

+1-19
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import (
3737
"arduino.cc/repository/internal/libraries/gitutils"
3838
"arduino.cc/repository/internal/libraries/hash"
3939
cc "github.com/arduino/golang-concurrent-workers"
40-
"github.com/go-git/go-git/v5"
4140
"github.com/go-git/go-git/v5/plumbing"
4241
)
4342

@@ -249,27 +248,10 @@ func syncLibraryTaggedRelease(logger *log.Logger, repo *libraries.Repository, ta
249248

250249
// Checkout desired tag
251250
logger.Printf("Checking out tag: %s", tag.Name().Short())
252-
253-
repoTree, err := repo.Repository.Worktree()
254-
if err != nil {
255-
return err
256-
}
257-
258-
// Annotated tags have their own hash, different from the commit hash, so the tag must be resolved before checkout
259-
resolvedTag, err := gitutils.ResolveTag(tag, repo.Repository)
260-
if err != nil {
261-
// All unresolvable tags were already removed by gitutils.SortedCommitTags(), so there will never be an error under normal conditions.
262-
panic(err)
263-
}
264-
265-
if err = repoTree.Checkout(&git.CheckoutOptions{Hash: *resolvedTag, Force: true}); err != nil {
251+
if err := gitutils.CheckoutTag(repo.Repository, tag); err != nil {
266252
return fmt.Errorf("Error checking out repo: %s", err)
267253
}
268254

269-
if err = repoTree.Clean(&git.CleanOptions{Dir: true}); err != nil {
270-
return fmt.Errorf("Error cleaning repo: %s", err)
271-
}
272-
273255
// Create library metadata from library.properties
274256
library, err := libraries.GenerateLibraryFromRepo(repo)
275257
if err != nil {

test/test_all.py

+45-6
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def test_all(run_command, working_dir):
5858

5959
libraries_repository_engine_command = [
6060
working_dir_path.joinpath("config.json"),
61-
test_data_path.joinpath("repos.txt"),
61+
test_data_path.joinpath("test_all", "repos.txt"),
6262
]
6363

6464
# Run the engine
@@ -69,12 +69,12 @@ def test_all(run_command, working_dir):
6969
check_libraries(configuration=configuration)
7070
check_logs(
7171
configuration=configuration,
72-
golden_logs_parent_path=test_data_path.joinpath("golden", "logs", "generate"),
72+
golden_logs_parent_path=test_data_path.joinpath("test_all", "golden", "logs", "generate"),
7373
logs_subpath=pathlib.Path("github.com", "arduino-libraries", "ArduinoCloudThing", "index.html"),
7474
)
7575
check_logs(
7676
configuration=configuration,
77-
golden_logs_parent_path=test_data_path.joinpath("golden", "logs", "generate"),
77+
golden_logs_parent_path=test_data_path.joinpath("test_all", "golden", "logs", "generate"),
7878
logs_subpath=pathlib.Path("github.com", "arduino-libraries", "SpacebrewYun", "index.html"),
7979
)
8080
check_index(configuration=configuration)
@@ -87,12 +87,12 @@ def test_all(run_command, working_dir):
8787
check_libraries(configuration=configuration)
8888
check_logs(
8989
configuration=configuration,
90-
golden_logs_parent_path=test_data_path.joinpath("golden", "logs", "update"),
90+
golden_logs_parent_path=test_data_path.joinpath("test_all", "golden", "logs", "update"),
9191
logs_subpath=pathlib.Path("github.com", "arduino-libraries", "ArduinoCloudThing", "index.html"),
9292
)
9393
check_logs(
9494
configuration=configuration,
95-
golden_logs_parent_path=test_data_path.joinpath("golden", "logs", "update"),
95+
golden_logs_parent_path=test_data_path.joinpath("test_all", "golden", "logs", "update"),
9696
logs_subpath=pathlib.Path("github.com", "arduino-libraries", "SpacebrewYun", "index.html"),
9797
)
9898
check_index(configuration=configuration)
@@ -160,7 +160,9 @@ def check_index(configuration):
160160
del release["checksum"]
161161

162162
# Load golden index
163-
golden_library_index_template = test_data_path.joinpath("golden", "library_index.json").read_text(encoding="utf-8")
163+
golden_library_index_template = test_data_path.joinpath("test_all", "golden", "library_index.json").read_text(
164+
encoding="utf-8"
165+
)
164166
# Fill in mutable content
165167
golden_library_index_string = string.Template(template=golden_library_index_template).substitute(
166168
base_download_url=configuration["BaseDownloadUrl"]
@@ -173,6 +175,43 @@ def check_index(configuration):
173175
assert release in golden_library_index["libraries"]
174176

175177

178+
# The engine's Git code struggles to get a clean checkout of releases under some circumstances.
179+
def test_clean_checkout(run_command, working_dir):
180+
working_dir_path = pathlib.Path(working_dir)
181+
configuration = {
182+
"BaseDownloadUrl": "http://www.example.com/libraries/",
183+
"LibrariesFolder": working_dir_path.joinpath("libraries").as_posix(),
184+
"LogsFolder": working_dir_path.joinpath("logs").as_posix(),
185+
"LibrariesDB": working_dir_path.joinpath("libraries_db.json").as_posix(),
186+
"LibrariesIndex": working_dir_path.joinpath("libraries", "library_index.json").as_posix(),
187+
"GitClonesFolder": working_dir_path.joinpath("gitclones").as_posix(),
188+
"DoNotRunClamav": True,
189+
# Arduino Lint should be installed under PATH
190+
"ArduinoLintPath": "",
191+
}
192+
193+
# Generate configuration file
194+
with working_dir_path.joinpath("config.json").open("w", encoding="utf-8") as configuration_file:
195+
json.dump(obj=configuration, fp=configuration_file, indent=2)
196+
197+
libraries_repository_engine_command = [
198+
working_dir_path.joinpath("config.json"),
199+
test_data_path.joinpath("test_clean_checkout", "repos.txt"),
200+
]
201+
202+
# Run the engine
203+
result = run_command(cmd=libraries_repository_engine_command)
204+
assert result.ok
205+
206+
# Load generated index
207+
with pathlib.Path(configuration["LibrariesIndex"]).open(mode="r", encoding="utf-8") as library_index_file:
208+
library_index = json.load(fp=library_index_file)
209+
210+
for release in library_index["libraries"]:
211+
# [email protected] contains a .exe and so should fail validation.
212+
assert not (release["name"] == "ssd1306" and release["version"] == "1.0.0")
213+
214+
176215
@pytest.fixture(scope="function")
177216
def run_command(pytestconfig, working_dir) -> typing.Callable[..., invoke.runners.Result]:
178217
"""Provide a wrapper around invoke's `run` API so that every test will work in the same temporary folder.
File renamed without changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
https://github.com/lexus2k/ssd1306.git|Contributed|ssd1306

0 commit comments

Comments
 (0)