diff --git a/pkg/git/stack_struct.go b/pkg/git/stack_struct.go index 5cf3f6579dd833b69566a052b6abffa938cfd723..af11b353d91ea3f0053dcda64b8ed496e2344d8d 100644 --- a/pkg/git/stack_struct.go +++ b/pkg/git/stack_struct.go @@ -7,6 +7,7 @@ import ( "io/fs" "iter" "os" + "os/exec" "path/filepath" "strings" ) @@ -29,8 +30,12 @@ type StackRef struct { // All stacks must be created with GatherStackRefs // which validates the stack for consistency. type Stack struct { - Title string - Refs map[string]StackRef + Title string + Refs map[string]StackRef + MetadataHash string `json:"metadata_hash,omitempty"` + Name string + Base string + Head string } func (s Stack) Empty() bool { return len(s.Refs) == 0 } @@ -300,3 +305,13 @@ func (r StackRef) Subject() string { return ls[0][:69] + "..." } + +func PushStackMetadata(remote string) error { + cmd := exec.Command("git", "push", remote, "+refs/stacked/*:refs/stacked/*") + return cmd.Run() +} + +func FetchStackMetadata(remote string) error { + cmd := exec.Command("git", "fetch", remote, "+refs/stacked/*:refs/stacked/*") + return cmd.Run() +} diff --git a/pkg/git/stack_struct_test.go b/pkg/git/stack_struct_test.go index c6d6062f6bb1645f2a5c64cc7fd2aa5f979fa845..12a484ef7031ab760ac5c91662a4a157cc659f9d 100644 --- a/pkg/git/stack_struct_test.go +++ b/pkg/git/stack_struct_test.go @@ -1,6 +1,7 @@ package git import ( + "os/exec" "path" "slices" "testing" @@ -620,3 +621,87 @@ func createBranches(t *testing.T, refs map[string]StackRef) { require.Nil(t, err) } } + +func TestPushStackMetadata(t *testing.T) { + tests := []struct { + name string + remote string + wantErr bool + }{ + { + name: "push to origin", + remote: "origin", + wantErr: false, + }, + { + name: "push to non-existent remote", + remote: "non-existent", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up a mock git environment + + oldExecCommand := GitCommand + defer func() { GitCommand = oldExecCommand }() + + GitCommand = func(args ...string) *exec.Cmd { + if tt.wantErr { + return exec.Command("false") + } + return exec.Command("true") + } + err := PushStackMetadata(tt.remote) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} + +func TestFetchStackMetadata(t *testing.T) { + tests := []struct { + name string + remote string + wantErr bool + }{ + { + name: "fetch from origin", + remote: "origin", + wantErr: false, + }, + { + name: "fetch from non-existent remote", + remote: "non-existent", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Set up a mock git environment + oldExecCommand := GitCommand + defer func() { GitCommand = oldExecCommand }() + + GitCommand = func(args ...string) *exec.Cmd { + if tt.wantErr { + return exec.Command("false") + } + return exec.Command("true") + } + + err := FetchStackMetadata(tt.remote) + + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + }) + } +} diff --git a/pkg/git/stacked.go b/pkg/git/stacked.go index cde17dbbae40e711d9931fa96782b6ad5fbb29ec..b2067789cf3950b0eecccec1de12dd23ffb4ad79 100644 --- a/pkg/git/stacked.go +++ b/pkg/git/stacked.go @@ -1,10 +1,13 @@ package git import ( + "bytes" "encoding/json" "fmt" "os" + "os/exec" "path/filepath" + "strings" "gitlab.com/gitlab-org/cli/internal/run" ) @@ -158,3 +161,43 @@ func GetStacks() (stacks []Stack, err error) { } return } + +func CreateStack(name string, base string, head string) error { + stack := &Stack{ + Title: name, + Refs: map[string]StackRef{ + base: {SHA: base}, + head: {SHA: head}, + }, + } + // Serialize stack to JSON + jsonData, err := json.Marshal(stack) + if err != nil { + return fmt.Errorf("failed to marshal stack: %w", err) + } + + // Create Git object + cmd := exec.Command("git", "hash-object", "-w", "--stdin") + cmd.Stdin = bytes.NewReader(jsonData) + output, err := cmd.CombinedOutput() + if err != nil { + return fmt.Errorf("failed to create git object: %w, output: %s", err, string(output)) + } + + stack.MetadataHash = strings.TrimSpace(string(output)) + + // Ensure the refs/stacked directory exists + refDir := filepath.Join(".git", "refs", "stacked") + if err := os.MkdirAll(refDir, 0o755); err != nil { + return fmt.Errorf("failed to create refs/stacked directory: %w", err) + } + + // Write ref + refPath := filepath.Join(refDir, name) + err = os.WriteFile(refPath, []byte(stack.MetadataHash), 0o644) + if err != nil { + return fmt.Errorf("failed to write ref file: %w", err) + } + + return nil +} diff --git a/pkg/git/stacked_test.go b/pkg/git/stacked_test.go index 72fe3686f611c51d3d9aeb5433cf7cbd4fe43fc6..c1f1209e367c1951e951c1393ed7834e671d1a6b 100644 --- a/pkg/git/stacked_test.go +++ b/pkg/git/stacked_test.go @@ -3,6 +3,7 @@ package git import ( "encoding/json" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -246,3 +247,90 @@ func createRefFiles(refs map[string]StackRef, title string) error { return nil } + +func TestCreateStack(t *testing.T) { + tests := []struct { + name string + stack Stack + wantErr bool + }{ + { + name: "create valid stack", + stack: Stack{ + Name: "test-stack", + Base: "main", + Head: "feature", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary directory + dir, err := os.MkdirTemp("", "TestCreateStack") + require.NoError(t, err) + defer os.RemoveAll(dir) + + // Change to the temporary directory + oldWd, err := os.Getwd() + require.NoError(t, err) + err = os.Chdir(dir) + require.NoError(t, err) + defer func() { + err := os.Chdir(oldWd) + require.NoError(t, err) + }() + + // Initialize Git repository + cmd := exec.Command("git", "init") + output, err := cmd.CombinedOutput() + require.NoError(t, err, "Git init failed: %s", string(output)) + + // Create an initial commit + cmd = exec.Command("git", "commit", "--allow-empty", "-m", "Initial commit") + output, err = cmd.CombinedOutput() + require.NoError(t, err, "Initial commit failed: %s", string(output)) + + // Set up Git user configuration + cmd = exec.Command("git", "config", "user.name", "Test User") + output, err = cmd.CombinedOutput() + require.NoError(t, err, "Setting Git user.name failed: %s", string(output)) + + cmd = exec.Command("git", "config", "user.email", "test@example.com") + output, err = cmd.CombinedOutput() + require.NoError(t, err, "Setting Git user.email failed: %s", string(output)) + + // Log Git status before creating stack + cmd = exec.Command("git", "status") + output, err = cmd.CombinedOutput() + require.NoError(t, err, "Git status failed: %s", string(output)) + + err = CreateStack(tt.stack.Name, tt.stack.Base, tt.stack.Head) + if tt.wantErr { + require.Error(t, err) + } else { + if err != nil { + t.Fatalf("CreateStack failed: %v", err) + } + + // Check if the stack metadata file was created + metadataPath := filepath.Join(dir, ".git", "refs", "stacked", tt.stack.Name) + require.FileExists(t, metadataPath) + + // Read the metadata file and verify its contents + content, err := os.ReadFile(metadataPath) + require.NoError(t, err) + + var stack Stack + err = json.Unmarshal(content, &stack) + require.NoError(t, err) + + require.Equal(t, tt.stack.Name, stack.Name) + require.Equal(t, tt.stack.Base, stack.Base) + require.Equal(t, tt.stack.Head, stack.Head) + require.NotEmpty(t, stack.MetadataHash) + } + }) + } +}