diff --git a/README.md b/README.md index 47ccafc..98bd1f4 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is a bit like hashicorp/go-getter, but much simpler, and with less supporte ## Features -- **Git repositories**: Clone from git://, git+ssh://, or git+https:// URLs with optional ref and depth parameters +- **Git repositories**: Clone from git://, git+ssh://, or git+https:// URLs with optional ref, commit and depth parameters - **TAR archives**: Fetch and extract .tar, .tar.gz, .tar.bz2, .tar.xz, and other compressed tarballs - **ZIP archives**: Download and unzip .zip files - **GitHub shortcuts**: Map shorthand URLs like `user/repo` or `github.com/user/repo` to full git URLs diff --git a/git.go b/git.go index 694e690..e4107e4 100644 --- a/git.go +++ b/git.go @@ -21,7 +21,12 @@ import ( // All forms support the following query parameters that control cloning behaviour: // // ref= +// commit= // depth= +// +// A commit is checked out detached and cannot be combined with ref. It needs +// git 2.49 or later, and a server that allows fetching a commit by hash, such +// as any server speaking git protocol v2. type Git struct{} var _ Resolver = (*Git)(nil) @@ -40,6 +45,9 @@ func (g *Git) Fetch(ctx context.Context, source Source, dest string) error { if ref := source.URL.Query().Get("ref"); ref != "" { args = append(args, "--branch", ref) } + if commit := source.URL.Query().Get("commit"); commit != "" { + args = append(args, "--revision", commit) + } repoURL := convertGitURL(source.URL) args = append(args, repoURL, dest) diff --git a/git_test.go b/git_test.go index 865a317..425d372 100644 --- a/git_test.go +++ b/git_test.go @@ -7,6 +7,7 @@ import ( "os/exec" "path/filepath" "runtime" + "strings" "testing" "time" @@ -189,6 +190,61 @@ func TestGitFetchWithDepth(t *testing.T) { assert.Equal(t, "1\n", string(output)) } +func TestGitFetchWithCommit(t *testing.T) { + repoDir, runGit := createTestRepo(t) + gitOutput := func(dir string, args ...string) string { + t.Helper() + cmd := exec.Command("git", args...) + cmd.Dir = dir + output, err := cmd.Output() + assert.NoError(t, err) + return strings.TrimSpace(string(output)) + } + + for _, content := range []string{"second", "third"} { + assert.NoError(t, os.WriteFile(filepath.Join(repoDir, "file.txt"), []byte(content+"\n"), 0o644)) + runGit("add", ".") + runGit("commit", "-m", content) + } + commit := gitOutput(repoDir, "rev-parse", "HEAD~1") + + tests := []struct { + name string + query string + commits string + }{ + {name: "FullHistory", query: "?commit=" + commit, commits: "2"}, + {name: "Shallow", query: "?commit=" + commit + "&depth=1", commits: "1"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + u, err := url.Parse("git+file://" + repoDir + tt.query) + assert.NoError(t, err) + + dest := filepath.Join(t.TempDir(), "dest") + err = NewGit().Fetch(context.Background(), Source{URL: u}, dest) + assert.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dest, "file.txt")) + assert.NoError(t, err) + assert.Equal(t, "second\n", string(content)) + assert.Equal(t, commit, gitOutput(dest, "rev-parse", "HEAD")) + assert.Equal(t, tt.commits, gitOutput(dest, "rev-list", "--count", "HEAD")) + }) + } +} + +func TestGitFetchWithCommitAndRef(t *testing.T) { + repoDir, _ := createTestRepo(t) + + u, err := url.Parse("git+file://" + repoDir + "?commit=" + strings.Repeat("a", 40) + "&ref=master") + assert.NoError(t, err) + + err = NewGit().Fetch(context.Background(), Source{URL: u}, t.TempDir()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot be used together") +} + func TestGitFetchCancelledContext(t *testing.T) { repoDir, _ := createTestRepo(t)