Skip to content
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

fix(git): improve parsing of worktree list #2548

Merged
merged 1 commit into from
Sep 20, 2024
Merged
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
fix(git): improve parsing of worktree list
Signed-off-by: Hidde Beydals <hidde@hhh.computer>
  • Loading branch information
hiddeco committed Sep 19, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 334547afc53e0a58691e68447f085280317d958b
86 changes: 77 additions & 9 deletions internal/controller/git/bare_repo.go
Original file line number Diff line number Diff line change
@@ -42,6 +42,21 @@
*baseRepo
}

// workTreeInfo represents information about a working tree.
type workTreeInfo struct {
// Path is the absolute path to the working tree.
Path string
// HEAD is the commit ID of the HEAD of the working tree.
HEAD string
// Branch is the name of the branch that the working tree is on,
// or an empty string if the working tree is in a detached HEAD state.
Branch string
// Bare is true if the working tree is a bare repository.
Bare bool
// Detached is true if the working tree is in a detached HEAD state.
Detached bool
}

// BareCloneOptions represents options for cloning a Git repository without a
// working tree.
type BareCloneOptions struct {
@@ -239,21 +254,74 @@
}

func (b *bareRepo) workTrees() ([]string, error) {
res, err := libExec.Exec(b.buildGitCommand("worktree", "list"))
res, err := libExec.Exec(b.buildGitCommand("worktree", "list", "--porcelain"))
if err != nil {
return nil, fmt.Errorf("error listing working trees: %w", err)
}
workTrees := []string{}
scanner := bufio.NewScanner(bytes.NewReader(res))
trees, err := b.parseWorkTreeOutput(res)
if err != nil {
return nil, fmt.Errorf("error listing repository trees: %w", err)

Check warning on line 263 in internal/controller/git/bare_repo.go

Codecov / codecov/patch

internal/controller/git/bare_repo.go#L263

Added line #L263 was not covered by tests
}
return b.filterNonBarePaths(trees), nil
}

func (b *bareRepo) parseWorkTreeOutput(output []byte) ([]workTreeInfo, error) {
var trees []workTreeInfo
var current *workTreeInfo

scanner := bufio.NewScanner(bytes.NewReader(output))
for scanner.Scan() {
line := scanner.Text()
if !strings.HasSuffix(line, "(bare)") {
fields := strings.Fields(line)
if len(fields) != 3 {
return nil, fmt.Errorf("unexpected number of fields: %q", line)
parts := strings.SplitN(line, " ", 2)

key := parts[0]
value := ""
if len(parts) > 1 {
value = parts[1]
}

switch key {
case "worktree":
if current != nil {
trees = append(trees, *current)
}
current = &workTreeInfo{Path: value}
case "HEAD":
if current != nil {
current.HEAD = value
}
case "branch":
if current != nil {
current.Branch = value
}
case "bare":
if current != nil {
current.Bare = true
}
case "detached":
if current != nil {
current.Detached = true
}
workTrees = append(workTrees, fields[0])
}
}
return workTrees, err

if current != nil {
trees = append(trees, *current)
}

if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error scanning worktree output: %w", err)

Check warning on line 313 in internal/controller/git/bare_repo.go

Codecov / codecov/patch

internal/controller/git/bare_repo.go#L313

Added line #L313 was not covered by tests
}

return trees, nil
}

func (b *bareRepo) filterNonBarePaths(trees []workTreeInfo) []string {
var paths []string
for _, info := range trees {
if !info.Bare {
paths = append(paths, info.Path)
}
}
return paths
}
160 changes: 160 additions & 0 deletions internal/controller/git/bare_repo_test.go
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ import (

"github.com/google/uuid"
"github.com/sosedoff/gitkit"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/akuity/kargo/internal/types"
@@ -159,5 +160,164 @@ func TestBareRepo(t *testing.T) {
require.Error(t, err)
require.True(t, os.IsNotExist(err))
})
}

func Test_bareRepo_parseWorkTreeOutput(t *testing.T) {
tests := []struct {
name string
input []byte
assertions func(*testing.T, []workTreeInfo, error)
}{
{
name: "single worktree",
input: []byte(`worktree /path/to/worktree
HEAD abcdef1234567890
branch main
`),
assertions: func(t *testing.T, result []workTreeInfo, err error) {
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result, []workTreeInfo{
{Path: "/path/to/worktree", HEAD: "abcdef1234567890", Branch: "main"},
})
},
},
{
name: "multiple worktrees",
input: []byte(`worktree /path/to/worktree1
HEAD abcdef1234567890
branch main

worktree /path/to/worktree2
HEAD fedcba9876543210
branch feature
bare
detached
`),
assertions: func(t *testing.T, result []workTreeInfo, err error) {
assert.NoError(t, err)
assert.Len(t, result, 2)
assert.Equal(t, result, []workTreeInfo{
{
Path: "/path/to/worktree1",
HEAD: "abcdef1234567890",
Branch: "main",
},
{
Path: "/path/to/worktree2",
HEAD: "fedcba9876543210",
Branch: "feature",
Bare: true,
Detached: true,
},
})
},
},
{
name: "empty input",
input: []byte(``),
assertions: func(t *testing.T, result []workTreeInfo, err error) {
assert.NoError(t, err)
assert.Empty(t, result)
},
},
{
name: "incomplete worktree info",
input: []byte(`worktree /path/to/incomplete
HEAD
branch

worktree /path/to/complete
HEAD abcdef1234567890
branch main
`),
assertions: func(t *testing.T, result []workTreeInfo, err error) {
assert.NoError(t, err)
assert.Len(t, result, 2)
assert.Equal(t, result, []workTreeInfo{
{Path: "/path/to/incomplete"},
{
Path: "/path/to/complete",
HEAD: "abcdef1234567890",
Branch: "main",
},
})
},
},
{
name: "invalid input",
input: []byte(`invalid input
not a worktree
`),
assertions: func(t *testing.T, result []workTreeInfo, err error) {
assert.NoError(t, err)
assert.Empty(t, result)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &bareRepo{}
result, err := b.parseWorkTreeOutput(tt.input)
tt.assertions(t, result, err)
})
}
}

func Test_bareRepo_filterNonBarePaths(t *testing.T) {
tests := []struct {
name string
input []workTreeInfo
assertions func(*testing.T, []string)
}{
{
name: "mixed bare and non-bare worktrees",
input: []workTreeInfo{
{Path: "/path/to/worktree1", Bare: false},
{Path: "/path/to/worktree2", Bare: true},
{Path: "/path/to/worktree3", Bare: false},
},
assertions: func(t *testing.T, result []string) {
assert.Len(t, result, 2)
assert.Equal(t, result, []string{"/path/to/worktree1", "/path/to/worktree3"})
},
},
{
name: "all non-bare worktrees",
input: []workTreeInfo{
{Path: "/path/to/worktree1", Bare: false},
{Path: "/path/to/worktree2", Bare: false},
},
assertions: func(t *testing.T, result []string) {
assert.Len(t, result, 2)
assert.Equal(t, result, []string{"/path/to/worktree1", "/path/to/worktree2"})
},
},
{
name: "all bare worktrees",
input: []workTreeInfo{
{Path: "/path/to/worktree1", Bare: true},
{Path: "/path/to/worktree2", Bare: true},
},
assertions: func(t *testing.T, result []string) {
assert.Empty(t, result)
},
},
{
name: "empty input",
input: []workTreeInfo{},
assertions: func(t *testing.T, result []string) {
assert.Empty(t, result)
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &bareRepo{}
result := b.filterNonBarePaths(tt.input)
tt.assertions(t, result)
})
}
}