Skip to content
Merged
Show file tree
Hide file tree
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
30 changes: 30 additions & 0 deletions cmd/cu/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"os"

"github.com/dagger/container-use/repository"
"github.com/spf13/cobra"
)

var diffCmd = &cobra.Command{
Use: "diff <env>",
Short: "Show changes between the environment and the local branch",
Args: cobra.ExactArgs(1),
ValidArgsFunction: suggestEnvironments,
RunE: func(app *cobra.Command, args []string) error {
ctx := app.Context()

// Ensure we're in a git repository
repo, err := repository.Open(ctx, ".")
if err != nil {
return err
}

return repo.Diff(ctx, args[0], os.Stdout)
},
}

func init() {
rootCmd.AddCommand(diffCmd)
}
16 changes: 5 additions & 11 deletions cmd/cu/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package main

import (
"os"
"os/exec"
"strings"

"github.com/dagger/container-use/repository"
"github.com/spf13/cobra"
Expand All @@ -18,22 +16,18 @@ var logCmd = &cobra.Command{
ctx := app.Context()

// Ensure we're in a git repository
if _, err := repository.Open(ctx, "."); err != nil {
repo, err := repository.Open(ctx, ".")
if err != nil {
return err
}

env := args[0]
// prevent accidental single quotes to mess up command
env = strings.Trim(env, "'")
cmd := exec.CommandContext(app.Context(), "git", "log", "--patch", "container-use/"+env)
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
patch, _ := app.Flags().GetBool("patch")

return cmd.Run()
return repo.Log(ctx, args[0], patch, os.Stdout)
},
}

func init() {
logCmd.Flags().BoolP("patch", "p", false, "Generate patch")
rootCmd.AddCommand(logCmd)
}
70 changes: 70 additions & 0 deletions environment/integration/repository_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package integration

import (
"bytes"
"context"
"os"
"strings"
Expand Down Expand Up @@ -126,3 +127,72 @@ func TestRepositoryCheckout(t *testing.T) {
"Expected branch to be %s or cu-%s, got %s", env.ID, env.ID, actualBranch)
})
}

// TestRepositoryLog tests retrieving commit history for an environment
func TestRepositoryLog(t *testing.T) {
t.Parallel()
WithRepository(t, "repository-log", SetupEmptyRepo, func(t *testing.T, repo *repository.Repository, user *UserActions) {
ctx := context.Background()

// Create an environment and add some commits
env := user.CreateEnvironment("Test Log", "Testing repository log")
user.FileWrite(env.ID, "file1.txt", "initial content", "Initial commit")
user.FileWrite(env.ID, "file1.txt", "updated content", "Update file")
user.FileWrite(env.ID, "file2.txt", "new file", "Add second file")

// Get commit log without patches
var logBuf bytes.Buffer
err := repo.Log(ctx, env.ID, false, &logBuf)
logOutput := logBuf.String()
require.NoError(t, err, logOutput)

// Verify commit messages are present
assert.Contains(t, logOutput, "Add second file")
assert.Contains(t, logOutput, "Update file")
assert.Contains(t, logOutput, "Initial commit")

// Get commit log with patches
logBuf.Reset()
err = repo.Log(ctx, env.ID, true, &logBuf)
logWithPatchOutput := logBuf.String()
require.NoError(t, err, logWithPatchOutput)

// Verify patch information is included
assert.Contains(t, logWithPatchOutput, "diff --git")
assert.Contains(t, logWithPatchOutput, "+updated content")

// Test log for non-existent environment
err = repo.Log(ctx, "non-existent-env", false, &logBuf)
assert.Error(t, err)
})
}

// TestRepositoryDiff tests retrieving changes between commits
func TestRepositoryDiff(t *testing.T) {
t.Parallel()
WithRepository(t, "repository-diff", SetupEmptyRepo, func(t *testing.T, repo *repository.Repository, user *UserActions) {
ctx := context.Background()

// Create an environment and make some changes
env := user.CreateEnvironment("Test Diff", "Testing repository diff")

// First commit - add a file
user.FileWrite(env.ID, "test.txt", "initial content\n", "Initial commit")

// Make changes to the file
user.FileWrite(env.ID, "test.txt", "initial content\nupdated content\n", "Update file")

// Get diff output
var diffBuf bytes.Buffer
err := repo.Diff(ctx, env.ID, &diffBuf)
diffOutput := diffBuf.String()
require.NoError(t, err, diffOutput)

// Verify diff contains expected changes
assert.Contains(t, diffOutput, "+updated content")

// Test diff with non-existent environment
err = repo.Diff(ctx, "non-existent-env", &diffBuf)
assert.Error(t, err)
})
}
2 changes: 2 additions & 0 deletions mcpserver/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type EnvironmentResponse struct {
RemoteRef string `json:"remote_ref"`
CheckoutCommand string `json:"checkout_command_to_share_with_user"`
LogCommand string `json:"log_command_to_share_with_user"`
DiffCommand string `json:"diff_command_to_share_with_user"`
Services []*environment.Service `json:"services,omitempty"`
}

Expand All @@ -142,6 +143,7 @@ func marshalEnvironment(env *environment.Environment) (string, error) {
RemoteRef: fmt.Sprintf("container-use/%s", env.ID),
CheckoutCommand: fmt.Sprintf("cu checkout %s", env.ID),
LogCommand: fmt.Sprintf("cu log %s", env.ID),
DiffCommand: fmt.Sprintf("cu diff %s", env.ID),
Services: env.Services,
}
out, err := json.Marshal(resp)
Expand Down
27 changes: 27 additions & 0 deletions repository/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,33 @@ func (r *Repository) addGitNote(ctx context.Context, env *environment.Environmen
return r.propagateGitNotes(ctx, gitNotesLogRef)
}

func (r *Repository) currentUserBranch(ctx context.Context) (string, error) {
return RunGitCommand(ctx, r.userRepoPath, "branch", "--show-current")
}

func (r *Repository) mergeBase(ctx context.Context, env *environment.EnvironmentInfo) (string, error) {
currentBranch, err := r.currentUserBranch(ctx)
if err != nil {
return "", err
}
currentBranch = strings.TrimSpace(currentBranch)
envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
mergeBase, err := RunGitCommand(ctx, r.userRepoPath, "merge-base", currentBranch, envGitRef)
if err != nil {
return "", err
}
return strings.TrimSpace(mergeBase), nil
}

func (r *Repository) revisionRange(ctx context.Context, env *environment.EnvironmentInfo) (string, error) {
mergeBase, err := r.mergeBase(ctx, env)
if err != nil {
return "", err
}
envGitRef := fmt.Sprintf("%s/%s", containerUseRemote, env.ID)
return fmt.Sprintf("%s..%s", mergeBase, envGitRef), nil
}

func (r *Repository) commitWorktreeChanges(ctx context.Context, worktreePath, name, explanation string) error {
status, err := RunGitCommand(ctx, worktreePath, "status", "--porcelain")
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions repository/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"os"
"os/exec"
Expand Down Expand Up @@ -354,3 +355,62 @@ func (r *Repository) Checkout(ctx context.Context, id, branch string) (string, e

return branch, err
}

func (r *Repository) Log(ctx context.Context, id string, patch bool, w io.Writer) error {
envInfo, err := r.Info(ctx, id)
if err != nil {
return err
}

logArgs := []string{
"git",
"log",
fmt.Sprintf("--notes=%s", gitNotesLogRef),
}

if patch {
logArgs = append(logArgs, "--patch")
}

revisionRange, err := r.revisionRange(ctx, envInfo)
if err != nil {
return err
}

logArgs = append(logArgs, revisionRange)

cmd := exec.CommandContext(ctx, "git")
cmd.Dir = r.userRepoPath
cmd.Args = logArgs
cmd.Stdout = w
cmd.Stderr = w

return cmd.Run()
}

func (r *Repository) Diff(ctx context.Context, id string, w io.Writer) error {
envInfo, err := r.Info(ctx, id)
if err != nil {
return err
}

diffArgs := []string{
"git",
"diff",
}

revisionRange, err := r.revisionRange(ctx, envInfo)
if err != nil {
return err
}

diffArgs = append(diffArgs, revisionRange)

cmd := exec.CommandContext(ctx, "git")
cmd.Dir = r.userRepoPath
cmd.Args = diffArgs
cmd.Stdout = w
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not specific to this PR, but in general i think we should not ignore stderr and either do os.Stderr and/or log it to the log file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workaround implemented -- PTAL

cmd.Stderr = w

return cmd.Run()
}