Skip to content

Commit 697f9c4

Browse files
committed
fetch instead of pull, try to detect different remote
1 parent 644e198 commit 697f9c4

File tree

2 files changed

+102
-28
lines changed

2 files changed

+102
-28
lines changed

git/git.go

Lines changed: 87 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
giturls "github.com/chainguard-dev/git-urls"
1616
"github.com/go-git/go-billy/v5"
1717
"github.com/go-git/go-git/v5"
18+
gitconfig "github.com/go-git/go-git/v5/config"
1819
"github.com/go-git/go-git/v5/plumbing"
1920
"github.com/go-git/go-git/v5/plumbing/cache"
2021
"github.com/go-git/go-git/v5/plumbing/protocol/packp/capability"
@@ -99,8 +100,7 @@ func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOpt
99100
return false, fmt.Errorf("chroot .git: %w", err)
100101
}
101102
gitStorage := filesystem.NewStorage(gitDir, cache.NewObjectLRU(cache.DefaultMaxSize*10))
102-
fsStorage := filesystem.NewStorage(fs, cache.NewObjectLRU(cache.DefaultMaxSize*10))
103-
repo, err := git.Open(fsStorage, gitDir)
103+
repo, err := git.Open(gitStorage, fs)
104104
if err == nil {
105105
// Repo exists, so fast-forward it.
106106
return fastForwardRepo(ctx, logf, opts, repo, reference)
@@ -129,44 +129,107 @@ func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOpt
129129
if err != nil {
130130
return false, fmt.Errorf("clone %q: %w", opts.RepoURL, err)
131131
}
132-
if head, err := repo.Head(); err == nil && head != nil {
133-
logf("cloned repo HEAD: %s", head.Hash().String())
132+
if head, err := repoHead(repo); err != nil {
133+
logf("failed to get repo HEAD: %s", err)
134+
} else {
135+
logf("cloned repo at %s", head)
134136
}
135137
return true, nil
136138
}
137139

138140
func fastForwardRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOptions, repo *git.Repository, referenceName string) (bool, error) {
139-
if head, err := repo.Head(); err == nil && head != nil {
140-
logf("existing repo HEAD: %s", head.Hash().String())
141+
if referenceName == "" {
142+
referenceName = "refs/heads/main"
141143
}
142-
wt, err := repo.Worktree()
144+
ref := plumbing.ReferenceName(referenceName)
145+
headBefore, err := repoHead(repo)
143146
if err != nil {
144-
return false, fmt.Errorf("worktree: %w", err)
147+
return false, err
145148
}
146-
err = wt.PullContext(ctx, &git.PullOptions{
147-
RemoteName: "", // use default remote
148-
ReferenceName: plumbing.ReferenceName(referenceName),
149-
SingleBranch: opts.SingleBranch,
150-
Depth: opts.Depth,
149+
logf("existing repo HEAD: %s", headBefore)
150+
151+
remote, err := repo.Remote("origin")
152+
if err != nil {
153+
return false, fmt.Errorf("remote: %w", err)
154+
}
155+
156+
if len(remote.Config().URLs) == 0 {
157+
logf("remote %q has no URLs configured!", remote.String())
158+
return false, nil
159+
}
160+
161+
// If the remote URL differs, stop! We don't want to accidentally
162+
// fetch from a different remote.
163+
for _, u := range remote.Config().URLs {
164+
if u != opts.RepoURL {
165+
logf("remote %q has URL %q, expected %q", remote.String(), u, opts.RepoURL)
166+
return false, nil
167+
}
168+
}
169+
170+
var refSpecs []gitconfig.RefSpec
171+
if referenceName != "" {
172+
// git checkout -b <branch> --track <remote>/<branch>
173+
refSpecs = []gitconfig.RefSpec{
174+
gitconfig.RefSpec(fmt.Sprintf("%s:%s", referenceName, referenceName)),
175+
}
176+
}
177+
178+
if err := remote.FetchContext(ctx, &git.FetchOptions{
151179
Auth: opts.RepoAuth,
152-
Progress: opts.Progress,
180+
CABundle: opts.CABundle,
181+
Depth: opts.Depth,
153182
Force: false,
154183
InsecureSkipTLS: opts.Insecure,
155-
CABundle: opts.CABundle,
184+
Progress: opts.Progress,
156185
ProxyOptions: opts.ProxyOptions,
157-
})
158-
if err == nil {
159-
if head, err := repo.Head(); err == nil && head != nil {
160-
logf("fast-forwarded to %s", head.Hash().String())
186+
RefSpecs: refSpecs,
187+
RemoteName: "origin",
188+
}); err != nil {
189+
if err != git.NoErrAlreadyUpToDate {
190+
return false, fmt.Errorf("fetch changes from remote: %w", err)
161191
}
162-
return true, nil
192+
logf("repository is already up-to-date")
193+
}
194+
195+
// Now that we've fetched changes from the remote,
196+
// attempt to check out the requested reference.
197+
wt, err := repo.Worktree()
198+
if err != nil {
199+
return false, fmt.Errorf("worktree: %w", err)
163200
}
164-
if errors.Is(err, git.NoErrAlreadyUpToDate) {
165-
logf("existing repo already up-to-date")
201+
st, err := wt.Status()
202+
if err != nil {
203+
return false, fmt.Errorf("status: %w", err)
204+
}
205+
// If the working tree is dirty, assume that the user wishes to
206+
// test local changes. Skip the checkout.
207+
if !st.IsClean() {
208+
logf("working tree is dirty, skipping checkout")
166209
return false, nil
167210
}
168-
logf("failed to fast-forward: %s", err.Error())
169-
return false, err
211+
if err := wt.Checkout(&git.CheckoutOptions{
212+
Branch: ref,
213+
// Note: we already checked that the working tree is clean, but I'd rather
214+
// err on the side of caution.
215+
Keep: true,
216+
}); err != nil {
217+
return false, fmt.Errorf("checkout branch %s: %w", ref.String(), err)
218+
}
219+
headAfter, err := repoHead(repo)
220+
if err != nil {
221+
return false, fmt.Errorf("check repo HEAD: %w", err)
222+
}
223+
logf("checked out %s", headAfter)
224+
return headBefore != headAfter, nil
225+
}
226+
227+
func repoHead(repo *git.Repository) (string, error) {
228+
head, err := repo.Head()
229+
if err != nil {
230+
return "", err
231+
}
232+
return head.Hash().String(), nil
170233
}
171234

172235
// ShallowCloneRepo will clone the repository at the given URL into the given path

git/git_test.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/go-git/go-billy/v5/osfs"
2323
githttp "github.com/go-git/go-git/v5/plumbing/transport/http"
2424
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
25+
"github.com/stretchr/testify/assert"
2526
"github.com/stretchr/testify/require"
2627
gossh "golang.org/x/crypto/ssh"
2728
)
@@ -89,14 +90,24 @@ func TestCloneRepo(t *testing.T) {
8990
srv := httptest.NewServer(authMW(gittest.NewServer(srvFS)))
9091
clientFS := memfs.New()
9192
// A repo already exists!
92-
_ = gittest.NewRepo(t, clientFS)
93+
_ = gittest.NewRepo(t, clientFS).Commit(gittest.Commit(t, "WRITEME.md", "I'm already here!", "Wow!"))
9394
cloned, err := git.CloneRepo(context.Background(), t.Logf, git.CloneRepoOptions{
94-
Path: "/",
95+
Path: "/workspace",
9596
RepoURL: srv.URL,
9697
Storage: clientFS,
98+
RepoAuth: &githttp.BasicAuth{
99+
Username: tc.username,
100+
Password: tc.password,
101+
},
97102
})
98-
require.NoError(t, err)
99-
require.False(t, cloned)
103+
if tc.expectError != "" {
104+
assert.ErrorContains(t, err, tc.expectError)
105+
}
106+
assert.False(t, cloned)
107+
108+
// Ensure we do not overwrite the existing repo.
109+
readme := mustRead(t, clientFS, "/workspace/WRITEME.md")
110+
assert.Equal(t, "I'm already here!", readme)
100111
})
101112

102113
// Basic Auth

0 commit comments

Comments
 (0)