Skip to content

Commit 9be9769

Browse files
committed
git url: support specifying tag and commit together
e.g., https://github.com/user/repo.git##tag=mytag,commit=cafebab Fix issue 5871 Signed-off-by: Akihiro Suda <akihiro.suda.cz@hco.ntt.co.jp>
1 parent 0df34b7 commit 9be9769

12 files changed

Lines changed: 132 additions & 24 deletions

File tree

client/llb/source.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,15 @@ func Git(url, ref string, opts ...GitOption) State {
322322
addCap(&gi.Constraints, pb.CapSourceGitMountSSHSock)
323323
}
324324

325+
commitHash := gi.CommitHash
326+
if commitHash == "" && remote != nil && remote.Fragment != nil {
327+
commitHash = remote.Fragment.CommitHash
328+
}
329+
if commitHash != "" {
330+
attrs[pb.AttrCommitHash] = commitHash
331+
addCap(&gi.Constraints, pb.CapSourceGitCommitHash)
332+
}
333+
325334
addCap(&gi.Constraints, pb.CapSourceGit)
326335

327336
source := NewSource("git://"+id, attrs, gi.Constraints)
@@ -345,6 +354,7 @@ type GitInfo struct {
345354
addAuthCap bool
346355
KnownSSHHosts string
347356
MountSSHSock string
357+
CommitHash string
348358
}
349359

350360
func KeepGitDir() GitOption {
@@ -373,6 +383,12 @@ func MountSSHSock(sshID string) GitOption {
373383
})
374384
}
375385

386+
func CommitHash(v string) GitOption {
387+
return gitOptionFunc(func(gi *GitInfo) {
388+
gi.CommitHash = v
389+
})
390+
}
391+
376392
// AuthOption can be used with either HTTP or Git sources.
377393
type AuthOption interface {
378394
GitOption

frontend/dockerfile/dockerfile2llb/convert.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1513,7 +1513,7 @@ func dispatchCopy(d *dispatchState, cfg copyConfig) error {
15131513
if gitRef.SubDir != "" {
15141514
commit += ":" + gitRef.SubDir
15151515
}
1516-
gitOptions := []llb.GitOption{llb.WithCustomName(pgName)}
1516+
gitOptions := []llb.GitOption{llb.WithCustomName(pgName), llb.CommitHash(gitRef.CommitHash)}
15171517
if cfg.keepGitDir {
15181518
gitOptions = append(gitOptions, llb.KeepGitDir())
15191519
}

frontend/dockerui/context.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ func DetectGitContext(ref string, keepGit bool) (*llb.State, bool) {
149149
if g.SubDir != "" {
150150
commit += ":" + g.SubDir
151151
}
152-
gitOpts := []llb.GitOption{WithInternalName("load git source " + ref)}
152+
gitOpts := []llb.GitOption{WithInternalName("load git source " + ref), llb.CommitHash(g.CommitHash)}
153153
if keepGit {
154154
gitOpts = append(gitOpts, llb.KeepGitDir())
155155
}

frontend/gateway/grpcclient/client.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ func defaultLLBCaps() []*apicaps.PBCap {
272272
{ID: string(opspb.CapSourceGit), Enabled: true},
273273
{ID: string(opspb.CapSourceGitKeepDir), Enabled: true},
274274
{ID: string(opspb.CapSourceGitFullURL), Enabled: true},
275+
{ID: string(opspb.CapSourceGitCommitHash), Enabled: true},
275276
{ID: string(opspb.CapSourceHTTP), Enabled: true},
276277
{ID: string(opspb.CapSourceHTTPChecksum), Enabled: true},
277278
{ID: string(opspb.CapSourceHTTPPerm), Enabled: true},

solver/pb/attr.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ const AttrAuthHeaderSecret = "git.authheadersecret"
66
const AttrAuthTokenSecret = "git.authtokensecret"
77
const AttrKnownSSHHosts = "git.knownsshhosts"
88
const AttrMountSSHSock = "git.mountsshsock"
9+
const AttrCommitHash = "git.commithash"
910
const AttrLocalSessionID = "local.session"
1011
const AttrLocalUniqueID = "local.unique"
1112
const AttrIncludePatterns = "local.includepattern"

solver/pb/caps.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
CapSourceGitKnownSSHHosts apicaps.CapID = "source.git.knownsshhosts"
3030
CapSourceGitMountSSHSock apicaps.CapID = "source.git.mountsshsock"
3131
CapSourceGitSubdir apicaps.CapID = "source.git.subdir"
32+
CapSourceGitCommitHash apicaps.CapID = "source.git.commithash"
3233

3334
CapSourceHTTP apicaps.CapID = "source.http"
3435
CapSourceHTTPAuth apicaps.CapID = "source.http.auth"
@@ -214,6 +215,12 @@ func init() {
214215
Status: apicaps.CapStatusExperimental,
215216
})
216217

218+
Caps.Init(apicaps.Cap{
219+
ID: CapSourceGitCommitHash,
220+
Enabled: true,
221+
Status: apicaps.CapStatusExperimental,
222+
})
223+
217224
Caps.Init(apicaps.Cap{
218225
ID: CapSourceHTTP,
219226
Enabled: true,

source/git/identifier.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package git
22

33
import (
4+
"fmt"
45
"path"
56

67
"github.com/moby/buildkit/solver/llbsolver/provenance"
@@ -13,6 +14,7 @@ import (
1314
type GitIdentifier struct {
1415
Remote string
1516
Ref string
17+
CommitHash string
1618
Subdir string
1719
KeepGitDir bool
1820
AuthTokenSecret string
@@ -33,6 +35,10 @@ func NewGitIdentifier(remoteURL string) (*GitIdentifier, error) {
3335
repo := GitIdentifier{Remote: u.Remote}
3436
if u.Fragment != nil {
3537
repo.Ref = u.Fragment.Ref
38+
repo.CommitHash = u.Fragment.CommitHash
39+
if repo.CommitHash != "" && !gitutil.IsCommitSHA(repo.CommitHash) {
40+
return nil, fmt.Errorf("invalid commit hash: %q", repo.CommitHash)
41+
}
3642
repo.Subdir = u.Fragment.Subdir
3743
}
3844
if sd := path.Clean(repo.Subdir); sd == "/" || sd == "." {

source/git/source.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,11 @@ func (gs *gitSource) Identifier(scheme, ref string, attrs map[string]string, pla
9292
id.KnownSSHHosts = v
9393
case pb.AttrMountSSHSock:
9494
id.MountSSHSock = v
95+
case pb.AttrCommitHash:
96+
if !gitutil.IsCommitSHA(v) {
97+
return nil, fmt.Errorf("invalid commit hash %q", v)
98+
}
99+
id.CommitHash = v
95100
}
96101
}
97102

@@ -349,10 +354,14 @@ func (gs *gitSourceHandler) CacheKey(ctx context.Context, g session.Group, index
349354
gs.locker.Lock(remote)
350355
defer gs.locker.Unlock(remote)
351356

352-
if ref := gs.src.Ref; ref != "" && gitutil.IsCommitSHA(ref) {
353-
cacheKey := gs.shaToCacheKey(ref, "")
357+
refCommitHash := gs.src.CommitHash
358+
if refCommitHash == "" && gitutil.IsCommitSHA(gs.src.Ref) {
359+
refCommitHash = gs.src.Ref
360+
}
361+
if refCommitHash != "" {
362+
cacheKey := gs.shaToCacheKey(refCommitHash, "")
354363
gs.cacheKey = cacheKey
355-
return cacheKey, ref, nil, true, nil
364+
return cacheKey, refCommitHash, nil, true, nil
356365
}
357366

358367
gs.getAuthToken(ctx, g)
@@ -536,6 +545,7 @@ func (gs *gitSourceHandler) Snapshot(ctx context.Context, g session.Group) (out
536545
subdir = "."
537546
}
538547

548+
checkedoutRef := "HEAD"
539549
if gs.src.KeepGitDir && subdir == "." {
540550
checkoutDirGit := filepath.Join(checkoutDir, ".git")
541551
if err := os.MkdirAll(checkoutDir, 0711); err != nil {
@@ -605,6 +615,7 @@ func (gs *gitSourceHandler) Snapshot(ctx context.Context, g session.Group) (out
605615
if err != nil {
606616
return nil, errors.Wrapf(err, "failed to checkout remote %s", urlutil.RedactCredentials(gs.src.Remote))
607617
}
618+
checkedoutRef = ref // HEAD may not exist
608619
if subdir != "." {
609620
d, err := os.Open(filepath.Join(cd, subdir))
610621
if err != nil {
@@ -635,6 +646,16 @@ func (gs *gitSourceHandler) Snapshot(ctx context.Context, g session.Group) (out
635646
}
636647

637648
git = git.New(gitutil.WithWorkTree(checkoutDir), gitutil.WithGitDir(gitDir))
649+
if gs.src.CommitHash != "" {
650+
actualHashBuf, err := git.Run(ctx, "rev-parse", checkedoutRef)
651+
if err != nil {
652+
return nil, errors.Wrapf(err, "failed to rev-parse %s for %s", checkedoutRef, urlutil.RedactCredentials(gs.src.Remote))
653+
}
654+
actualHash := strings.TrimSpace(string(actualHashBuf))
655+
if actualHash != gs.src.CommitHash {
656+
return nil, fmt.Errorf("expected commit hash %s, got %s", gs.src.CommitHash, actualHash)
657+
}
658+
}
638659
_, err = git.Run(ctx, "submodule", "update", "--init", "--recursive", "--depth=1")
639660
if err != nil {
640661
return nil, errors.Wrapf(err, "failed to update submodules for %s", urlutil.RedactCredentials(gs.src.Remote))

source/git/source_test.go

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -304,54 +304,70 @@ func testFetchUnreferencedRefSha(t *testing.T, ref string, keepGitDir bool) {
304304
}
305305

306306
func TestFetchByTag(t *testing.T) {
307-
testFetchByTag(t, "lightweight-tag", "third", false, true, false)
307+
testFetchByTag(t, "lightweight-tag", "third", false, true, false, testCommitHashModeNone)
308308
}
309309

310310
func TestFetchByTagKeepGitDir(t *testing.T) {
311-
testFetchByTag(t, "lightweight-tag", "third", false, true, true)
311+
testFetchByTag(t, "lightweight-tag", "third", false, true, true, testCommitHashModeNone)
312312
}
313313

314314
func TestFetchByTagFull(t *testing.T) {
315-
testFetchByTag(t, "refs/tags/lightweight-tag", "third", false, true, true)
315+
testFetchByTag(t, "refs/tags/lightweight-tag", "third", false, true, true, testCommitHashModeNone)
316316
}
317317

318318
func TestFetchByAnnotatedTag(t *testing.T) {
319-
testFetchByTag(t, "v1.2.3", "second", true, false, false)
319+
testFetchByTag(t, "v1.2.3", "second", true, false, false, testCommitHashModeNone)
320320
}
321321

322322
func TestFetchByAnnotatedTagKeepGitDir(t *testing.T) {
323-
testFetchByTag(t, "v1.2.3", "second", true, false, true)
323+
testFetchByTag(t, "v1.2.3", "second", true, false, true, testCommitHashModeNone)
324324
}
325325

326326
func TestFetchByAnnotatedTagFull(t *testing.T) {
327-
testFetchByTag(t, "refs/tags/v1.2.3", "second", true, false, true)
327+
testFetchByTag(t, "refs/tags/v1.2.3", "second", true, false, true, testCommitHashModeNone)
328328
}
329329

330330
func TestFetchByBranch(t *testing.T) {
331-
testFetchByTag(t, "feature", "withsub", false, true, false)
331+
testFetchByTag(t, "feature", "withsub", false, true, false, testCommitHashModeNone)
332332
}
333333

334334
func TestFetchByBranchKeepGitDir(t *testing.T) {
335-
testFetchByTag(t, "feature", "withsub", false, true, true)
335+
testFetchByTag(t, "feature", "withsub", false, true, true, testCommitHashModeNone)
336336
}
337337

338338
func TestFetchByBranchFull(t *testing.T) {
339-
testFetchByTag(t, "refs/heads/feature", "withsub", false, true, true)
339+
testFetchByTag(t, "refs/heads/feature", "withsub", false, true, true, testCommitHashModeNone)
340340
}
341341

342342
func TestFetchByRef(t *testing.T) {
343-
testFetchByTag(t, "test", "feature", false, true, false)
343+
testFetchByTag(t, "test", "feature", false, true, false, testCommitHashModeNone)
344344
}
345345

346346
func TestFetchByRefKeepGitDir(t *testing.T) {
347-
testFetchByTag(t, "test", "feature", false, true, true)
347+
testFetchByTag(t, "test", "feature", false, true, true, testCommitHashModeNone)
348348
}
349349

350350
func TestFetchByRefFull(t *testing.T) {
351-
testFetchByTag(t, "refs/test", "feature", false, true, true)
351+
testFetchByTag(t, "refs/test", "feature", false, true, true, testCommitHashModeNone)
352352
}
353353

354-
func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotatedTag, hasFoo13File, keepGitDir bool) {
354+
func TestFetchByTagWithCommitHash(t *testing.T) {
355+
testFetchByTag(t, "lightweight-tag", "third", false, true, false, testCommitHashModeValid)
356+
}
357+
358+
func TestFetchByTagWithCommitHashInvalid(t *testing.T) {
359+
testFetchByTag(t, "lightweight-tag", "third", false, true, false, testCommitHashModeInvalid)
360+
}
361+
362+
type testCommitHashMode int
363+
364+
const (
365+
testCommitHashModeNone testCommitHashMode = iota
366+
testCommitHashModeValid
367+
testCommitHashModeInvalid
368+
)
369+
370+
func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotatedTag, hasFoo13File, keepGitDir bool, commitHashMode testCommitHashMode) {
355371
if runtime.GOOS == "windows" {
356372
t.Skip("Depends on unimplemented containerd bind-mount support on Windows")
357373
}
@@ -366,6 +382,24 @@ func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotated
366382

367383
id := &GitIdentifier{Remote: repo.mainURL, Ref: tag, KeepGitDir: keepGitDir}
368384

385+
if commitHashMode != testCommitHashModeNone {
386+
cmd := exec.Command("git", "rev-parse", tag)
387+
cmd.Dir = repo.mainPath
388+
389+
out, err := cmd.Output()
390+
require.NoError(t, err)
391+
392+
sha := strings.TrimSpace(string(out))
393+
require.Equal(t, 40, len(sha))
394+
395+
id.CommitHash = sha
396+
397+
if commitHashMode == testCommitHashModeInvalid {
398+
// Make the hash value invalid
399+
id.CommitHash = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef"
400+
}
401+
}
402+
369403
g, err := gs.Resolve(ctx, id, nil, nil)
370404
require.NoError(t, err)
371405

@@ -383,6 +417,10 @@ func testFetchByTag(t *testing.T, tag, expectedCommitSubject string, isAnnotated
383417
require.Equal(t, 40, len(pin1))
384418

385419
ref1, err := g.Snapshot(ctx, nil)
420+
if commitHashMode == testCommitHashModeInvalid {
421+
require.ErrorContains(t, err, "expected commit hash "+id.CommitHash)
422+
return
423+
}
386424
require.NoError(t, err)
387425
defer ref1.Release(context.TODO())
388426

util/gitutil/git_ref.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ type GitRef struct {
2525
// Commit is optional.
2626
Commit string
2727

28+
// CommitHash verifies Commit if specified.
29+
// CommitHash is optional.
30+
CommitHash string
31+
2832
// SubDir is a directory path inside the repo.
2933
// SubDir is optional.
3034
SubDir string
@@ -97,7 +101,7 @@ func ParseGitRef(ref string) (*GitRef, error) {
97101
_, res.Remote, _ = strings.Cut(res.Remote, "://")
98102
}
99103
if remote.Fragment != nil {
100-
res.Commit, res.SubDir = remote.Fragment.Ref, remote.Fragment.Subdir
104+
res.Commit, res.CommitHash, res.SubDir = remote.Fragment.Ref, remote.Fragment.CommitHash, remote.Fragment.Subdir
101105
}
102106

103107
repoSplitBySlash := strings.Split(res.Remote, "/")

0 commit comments

Comments
 (0)