diff --git a/buildcontext/git.go b/buildcontext/git.go index d6a3c727ea..fea8c71bef 100644 --- a/buildcontext/git.go +++ b/buildcontext/git.go @@ -48,6 +48,8 @@ type resolvedGitProject struct { hash string // shortHash is the short git hash. shortHash string + // contentHash is the git tree hash (content-addressable). + contentHash string // branches is the git branches. branches []string // tags is the git tags. @@ -227,6 +229,7 @@ func (gr *gitResolver) resolveEarthProject( RemoteURL: gitURL, Hash: rgp.hash, ShortHash: rgp.shortHash, + ContentHash: rgp.contentHash, BranchOverrideTagArg: gr.gitBranchOverride != "", Branch: rgp.branches, Tags: rgp.tags, @@ -306,6 +309,7 @@ func (gr *gitResolver) resolveGitProject( "git rev-parse HEAD >/dest/git-hash ; " + "uname -m >/dest/uname-m ;" + "git rev-parse --short=8 HEAD >/dest/git-short-hash ; " + + "git rev-parse HEAD^{tree} >/dest/git-content-hash ; " + "git rev-parse --abbrev-ref HEAD >/dest/git-branch || touch /dest/git-branch ; " + "ls .git/refs/heads/ | head -n 1 >/dest/git-default-branch || touch /dest/git-default-branch ; " + "git describe --exact-match --tags >/dest/git-tags || touch /dest/git-tags ; " + @@ -362,22 +366,23 @@ func (gr *gitResolver) resolveGitProject( gitImage, string(unameM), platr.LLBNative().Architecture) } - var gitHashBytes []byte - - gitHashBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-hash", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-hash") + metaFiles := []string{ + "git-hash", "git-short-hash", "git-content-hash", + "git-default-branch", "git-tags", + "git-committer-ts", "git-author-ts", + "git-author-email", "git-author-name", + "git-body", "git-refs", "Earthfile-paths", } - var gitShortHashBytes []byte + meta := make(map[string][]byte, len(metaFiles)) - gitShortHashBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-short-hash", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-short-hash") + for _, name := range metaFiles { + meta[name], err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ + Filename: name, + }) + if err != nil { + return nil, errors.Wrap(err, "read "+name) + } } var gitBranch string @@ -387,97 +392,17 @@ func (gr *gitResolver) resolveGitProject( return nil, errors.Wrap(err, "read git-branch") } - var gitDefaultBranchBytes []byte - - gitDefaultBranchBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-default-branch", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-default-branch") - } - - var gitTagsBytes []byte - - gitTagsBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-tags", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-tags") - } - - var gitCommitterTsBytes []byte - - gitCommitterTsBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-committer-ts", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-committer-ts") - } - - var gitAuthorTsBytes []byte - - gitAuthorTsBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-author-ts", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-author-ts") - } - - var gitAuthorEmailBytes []byte - - gitAuthorEmailBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-author-email", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-author-email") - } - - var gitAuthorNameBytes []byte - - gitAuthorNameBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-author-name", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-author-name") - } - - var gitBodyBytes []byte - - gitBodyBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-body", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-body") - } - - var gitRefsBytes []byte - - gitRefsBytes, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "git-refs", - }) - if err != nil { - return nil, errors.Wrap(err, "read git-refs") - } - - var earthfilePathsRaw []byte - - earthfilePathsRaw, err = gitMetaRef.ReadFile(ctx, gwclient.ReadRequest{ - Filename: "Earthfile-paths", - }) - if err != nil { - return nil, errors.Wrap(err, "read Earthfile-paths") - } - isNotHead := func(s string) bool { return s != "" && s != "HEAD" } - gitHash := strings.SplitN(string(gitHashBytes), "\n", 2)[0] - gitShortHash := strings.SplitN(string(gitShortHashBytes), "\n", 2)[0] + gitHash := strings.SplitN(string(meta["git-hash"]), "\n", 2)[0] + gitShortHash := strings.SplitN(string(meta["git-short-hash"]), "\n", 2)[0] + gitContentHash := strings.SplitN(string(meta["git-content-hash"]), "\n", 2)[0] gitBranches := strings.SplitN(gitBranch, "\n", 2) - gitAuthorEmail := strings.SplitN(string(gitAuthorEmailBytes), "\n", 2)[0] - gitAuthorName := strings.SplitN(string(gitAuthorNameBytes), "\n", 2)[0] - gitCoAuthors := gitutil.ParseCoAuthorsFromBody(string(gitBodyBytes)) + gitAuthorEmail := strings.SplitN(string(meta["git-author-email"]), "\n", 2)[0] + gitAuthorName := strings.SplitN(string(meta["git-author-name"]), "\n", 2)[0] + gitCoAuthors := gitutil.ParseCoAuthorsFromBody(string(meta["git-body"])) var gitBranches2 []string @@ -495,11 +420,11 @@ func (gr *gitResolver) resolveGitProject( gitBranches2 = []string{gitRef} } } else { - gitBranches2 = []string{strings.SplitN(string(gitDefaultBranchBytes), "\n", 2)[0]} + gitBranches2 = []string{strings.SplitN(string(meta["git-default-branch"]), "\n", 2)[0]} } } - gitTags := strings.SplitN(string(gitTagsBytes), "\n", 2) + gitTags := strings.SplitN(string(meta["git-tags"]), "\n", 2) var gitTags2 []string @@ -509,9 +434,9 @@ func (gr *gitResolver) resolveGitProject( } } - gitCommitterTs := strings.SplitN(string(gitCommitterTsBytes), "\n", 2)[0] - gitAuthorTs := strings.SplitN(string(gitAuthorTsBytes), "\n", 2)[0] - gitRefs := strings.Split(string(gitRefsBytes), "\n") + gitCommitterTs := strings.SplitN(string(meta["git-committer-ts"]), "\n", 2)[0] + gitAuthorTs := strings.SplitN(string(meta["git-author-ts"]), "\n", 2)[0] + gitRefs := strings.Split(string(meta["git-refs"]), "\n") var gitRefs2 []string @@ -537,6 +462,7 @@ func (gr *gitResolver) resolveGitProject( rgp = &resolvedGitProject{ hash: gitHash, shortHash: gitShortHash, + contentHash: gitContentHash, branches: gitBranches2, tags: gitTags2, committerTs: gitCommitterTs, @@ -545,7 +471,7 @@ func (gr *gitResolver) resolveGitProject( authorName: gitAuthorName, coAuthors: gitCoAuthors, refs: gitRefs2, - earthfilePaths: strings.Split(strings.TrimSpace(string(earthfilePathsRaw)), "\n"), + earthfilePaths: strings.Split(strings.TrimSpace(string(meta["Earthfile-paths"])), "\n"), state: pllb.Git( gitURL, gitHash, diff --git a/docs/caching/caching-in-earthfiles.md b/docs/caching/caching-in-earthfiles.md index bf883cc781..2f03b8f5c2 100644 --- a/docs/caching/caching-in-earthfiles.md +++ b/docs/caching/caching-in-earthfiles.md @@ -105,7 +105,7 @@ As auto-skip relies on statically analyzing the structure of the build upfront, For basic `ARG` operations, auto-skip is able to infer the value of the `ARG` statically, and therefore, it is able to support it. Here is a practical example. -``` +```Earthfile # Supported ARG MY_ARG=foo BUILD $MY_ARG @@ -119,7 +119,7 @@ In the first case, the value of `MY_ARG` is known statically as its value can be Similarly, the auto-skip algorithm is able to propagate `ARG`s across targets, as long as the value of the `ARG` is known statically. Here is a practical example: -``` +```Earthfile # Supported ARG MY_ARG=foo BUILD +target --arg=$MY_ARG @@ -135,7 +135,7 @@ BUILD +target --arg=$MY_ARG Here is a practical example: -``` +```Earthfile # Supported and efficient (only +target2 is analyzed) ARG MY_ARG=bar IF [ $MY_ARG = "foo" ] @@ -201,7 +201,7 @@ In Earthly, like in Dockerfiles, ARGs declared in Earthfiles also behave as envi For this reason, it is best to declare ARGs as late as possible within the target they are used in, and try to avoid declaring `--global` ARGs as much as possible. If an ARG is not yet declared, it will not influence the cache state of a layer, allowing for more cache hits. Limiting the scope of ARGs as much as possible will yield better cache performance. -Watch out especially for ARGs that change often, such as the built-in ARG `EARTHLY_GIT_HASH`. Declaring this ARG as late as possible in the build will cause less cache misses. +Watch out especially for ARGs that change often, such as the built-in ARG `EARTH_GIT_HASH`. Declaring this ARG as late as possible in the build will cause less cache misses. If you need a git-derived identifier that doesn't change as often, consider `EARTH_GIT_CONTENT_HASH` instead — it only changes when the file tree actually changes, not on every commit. ### Secrets diff --git a/docs/earthfile/builtin-args.md b/docs/earthfile/builtin-args.md index ca0de631eb..98c075f19a 100644 --- a/docs/earthfile/builtin-args.md +++ b/docs/earthfile/builtin-args.md @@ -11,13 +11,16 @@ SAVE IMAGE --push some/name:$TAG ``` {% hint style='danger' %} + ##### Important + Earthly builtin args need to be pre-declared before they can be used. For example ```Dockerfile ARG EARTHLY_TARGET RUN echo "The current target is $EARTHLY_TARGET" ``` + {% endhint %} ### General args @@ -43,26 +46,27 @@ RUN echo "The current target is $EARTHLY_TARGET" ### Git-related args -| Name | Description | Example value | Feature Flag | -| --- | --- | --- |----------------------------------------| -| `EARTHLY_GIT_AUTHOR` | The git author detected within the build context directory. If no git directory is detected, then the value is an empty string. This is currently the author's email address but the feature flag adds the name as well | `john@example.com` (or `John Doe ` with flag turned on) | `--earthly-git-author-individual-args` | -| `EARTHLY_GIT_AUTHOR_EMAIL` | The git author email detected within the build context directory. If no git directory is detected, then the value is an empty string. | `john@example.com` | `--earthly-git-author-individual-args` | -| `EARTHLY_GIT_AUTHOR_NAME` | The git author name detected within the build context directory. If no git directory is detected, then the value is an empty string. | `John Doe` | `--earthly-git-author-individual-args` | -| `EARTHLY_GIT_CO_AUTHORS` | The git co-authors detected within the build context directory, separated by space. If no git directory is detected, then the value is an empty string. | `Jane Doe ` | | -| `EARTHLY_GIT_COMMIT_AUTHOR_TIMESTAMP` | The author timestamp, as unix seconds, of the git commit detected within the build context directory. If no git directory is detected, then the value is an empty string. | `1626881847` | | -| `EARTHLY_GIT_BRANCH` | The git branch of the git commit detected within the build context directory. If no git directory is detected, then the value is an empty string. | `main` | | -| `EARTHLY_GIT_COMMIT_TIMESTAMP` | The committer timestamp, as unix seconds, of the git commit detected within the build context directory. If no git directory is detected, then the value is an empty string. | `1626881847` | | -| `EARTHLY_GIT_HASH` | The git hash detected within the build context directory. If no git directory is detected, then the value is an empty string. Take care when using this arg, as the frequently changing git hash may be cause for not using the cache. | `41cb5666ade67b29e42bef121144456d3977a67a` | | -| `EARTHLY_GIT_ORIGIN_URL` | The git URL detected within the build context directory. If no git directory is detected, then the value is an empty string. Please note that this may be inconsistent, depending on whether an HTTPS or SSH URL was used. | `git@github.com:bar/buz.git` or `https://github.com/bar/buz.git` | | -| `EARTHLY_GIT_PROJECT_NAME` | The git project name from within the git URL detected within the build context directory. If no git directory is detected, then the value is an empty string. | `bar/buz` | | -| `EARTHLY_GIT_REFS` | The git references of the git commit detected within the build context directory, separated by space. If no git directory is detected, then the value is an empty string. | `issue-2735-git-ref main` | | -| `EARTHLY_GIT_SHORT_HASH` | The first 8 characters of the git hash detected within the build context directory. If no git directory is detected, then the value is an empty string. Take care when using this arg, as the frequently changing git hash may be cause for not using the cache. | `41cb5666` | | -| `EARTHLY_SOURCE_DATE_EPOCH` | The timestamp, as unix seconds, of the git commit detected within the build context directory. If no git directory is detected, then the value is `0` (the unix epoch) | `1626881847`, `0` | | +| Name | Description | Example value | Feature Flag | +| ---- | ----------- | ------------- | ------------ | +| `EARTH_GIT_AUTHOR` | The git author detected within the build context directory. If no git directory is detected, then the value is an empty string. This is currently the author's email address but the feature flag adds the name as well | `john@example.com` (or `John Doe ` with flag turned on) | `--earthly-git-author-individual-args` | +| `EARTH_GIT_AUTHOR_EMAIL` | The git author email detected within the build context directory. If no git directory is detected, then the value is an empty string. | `john@example.com` | `--earthly-git-author-individual-args` | +| `EARTH_GIT_AUTHOR_NAME` | The git author name detected within the build context directory. If no git directory is detected, then the value is an empty string. | `John Doe` | `--earthly-git-author-individual-args` | +| `EARTH_GIT_BRANCH` | The git branch of the git commit detected within the build context directory. If no git directory is detected, then the value is an empty string. | `main` | | +| `EARTH_GIT_CO_AUTHORS` | The git co-authors detected within the build context directory, separated by space. If no git directory is detected, then the value is an empty string. | `Jane Doe ` | | +| `EARTH_GIT_COMMIT_AUTHOR_TIMESTAMP` | The author timestamp, as unix seconds, of the git commit detected within the build context directory. If no git directory is detected, then the value is an empty string. | `1626881847` | | +| `EARTH_GIT_COMMIT_TIMESTAMP` | The committer timestamp, as unix seconds, of the git commit detected within the build context directory. If no git directory is detected, then the value is an empty string. | `1626881847` | | +| `EARTH_GIT_CONTENT_HASH` | The git tree hash (`git rev-parse HEAD^{tree}`) detected within the build context directory. Unlike `EARTH_GIT_HASH`, this is content-addressable: it remains stable across amends, rebases, or cherry-picks that don't change the file tree. If no git directory is detected, then the value is an empty string. | `aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7` | | +| `EARTH_GIT_HASH` | The git hash detected within the build context directory. If no git directory is detected, then the value is an empty string. Take care when using this arg, as the frequently changing git hash may be cause for not using the cache. | `41cb5666ade67b29e42bef121144456d3977a67a` | | +| `EARTH_GIT_ORIGIN_URL` | The git URL detected within the build context directory. If no git directory is detected, then the value is an empty string. Please note that this may be inconsistent, depending on whether an HTTPS or SSH URL was used. | `git@github.com:bar/buz.git` or `https://github.com/bar/buz.git` | | +| `EARTH_GIT_PROJECT_NAME` | The git project name from within the git URL detected within the build context directory. If no git directory is detected, then the value is an empty string. | `bar/buz` | | +| `EARTH_GIT_REFS` | The git references of the git commit detected within the build context directory, separated by space. If no git directory is detected, then the value is an empty string. | `issue-2735-git-ref main` | | +| `EARTH_GIT_SHORT_HASH` | The first 8 characters of the git hash detected within the build context directory. If no git directory is detected, then the value is an empty string. Take care when using this arg, as the frequently changing git hash may be cause for not using the cache. | `41cb5666` | | +| `EARTH_SOURCE_DATE_EPOCH` | The timestamp, as unix seconds, of the git commit detected within the build context directory. If no git directory is detected, then the value is `0` (the unix epoch) | `1626881847`, `0` | | ### Platform-related args | Name | Description | Example value | -| --- | --- | --- | +| ---- | ----------- | ------------- | | `NATIVEARCH` | The native processor architecture of the build runner. | `arm`, `amd64`, `arm64` | | `NATIVEOS` | The native OS of the build runner. | `linux` | | `NATIVEPLATFORM` | The native platform of the build runner. | `linux/arm/v7`, `linux/amd64`, `darwin/arm64` | @@ -81,7 +85,9 @@ The default value of the `TARGETPLATFORM` arg is the native platform of the runn Under `LOCALLY`, the `TARGETPLATFORM` arg is always set to the user platform (the environment the `earthly` binary is invoked from) and it is not overridden by the `--platform` flag. {% hint style='info' %} + ##### Note + Under `LOCALLY` targets, it is important to declare the `TARGETPLATFORM` arg **after** the `LOCALLY` command, to ensure that it gets the appropriate user platform value. For example: ```Dockerfile @@ -90,4 +96,5 @@ my-target: ARG TARGETPLATFORM RUN echo "The target platform under LOCALLY is $TARGETPLATFORM" ``` + {% endhint %} diff --git a/util/gitutil/detectgit.go b/util/gitutil/detectgit.go index 0f34ec1213..832396756d 100644 --- a/util/gitutil/detectgit.go +++ b/util/gitutil/detectgit.go @@ -24,6 +24,8 @@ var ( ErrCouldNotDetectGitHash = errors.New("Could not auto-detect or parse Git hash") // ErrCouldNotDetectGitShortHash is an error returned when git short hash could not be detected. ErrCouldNotDetectGitShortHash = errors.New("Could not auto-detect or parse Git short hash") + // ErrCouldNotDetectGitContentHash is an error returned when git tree hash could not be detected. + ErrCouldNotDetectGitContentHash = errors.New("Could not auto-detect or parse Git content hash") // ErrCouldNotDetectGitBranch is an error returned when git branch could not be detected. ErrCouldNotDetectGitBranch = errors.New("Could not auto-detect or parse Git branch") // ErrCouldNotDetectGitTags is an error returned when git tags could not be detected. @@ -40,6 +42,7 @@ type GitMetadata struct { GitURL string Hash string ShortHash string + ContentHash string RelDir string AuthorName string AuthorEmail string @@ -96,6 +99,12 @@ func Metadata(ctx context.Context, dir, gitBranchOverride string) (*GitMetadata, // Keep going. } + contentHash, err := detectGitContentHash(ctx, dir) + if err != nil { + retErr = err + // Keep going. + } + branch, err := detectGitBranch(ctx, dir, gitBranchOverride) if err != nil { retErr = err @@ -160,6 +169,7 @@ func Metadata(ctx context.Context, dir, gitBranchOverride string) (*GitMetadata, GitURL: gitURL, Hash: hash, ShortHash: shortHash, + ContentHash: contentHash, BranchOverrideTagArg: gitBranchOverride != "", Branch: branch, Tags: tags, @@ -181,6 +191,7 @@ func (gm *GitMetadata) Clone() *GitMetadata { GitURL: gm.GitURL, Hash: gm.Hash, ShortHash: gm.ShortHash, + ContentHash: gm.ContentHash, BranchOverrideTagArg: gm.BranchOverrideTagArg, Branch: gm.Branch, Tags: gm.Tags, @@ -266,38 +277,34 @@ func detectGitBaseDir(ctx context.Context, dir string) (string, error) { return strings.SplitN(outStr, "\n", 2)[0], nil } -func detectGitHash(ctx context.Context, dir string) (string, error) { - cmd := exec.CommandContext(ctx, "git", "rev-parse", "HEAD") +func gitRevParse(ctx context.Context, dir string, sentinel error, args ...string) (string, error) { + revParseArgs := append([]string{"rev-parse"}, args...) + cmd := exec.CommandContext(ctx, "git", revParseArgs...) //nolint:gosec cmd.Dir = dir out, err := cmd.Output() if err != nil { - return "", errors.Wrapf(ErrCouldNotDetectGitHash, "returned error %s: %s", err.Error(), string(out)) + return "", errors.Wrapf(sentinel, "returned error %s: %s", err.Error(), string(out)) } outStr := string(out) if outStr == "" { - return "", errors.Wrapf(ErrCouldNotDetectGitHash, "no remote origin url output") + return "", errors.Wrapf(sentinel, "no output") } return strings.SplitN(outStr, "\n", 2)[0], nil } -func detectGitShortHash(ctx context.Context, dir string) (string, error) { - cmd := exec.CommandContext(ctx, "git", "rev-parse", "--short=8", "HEAD") - cmd.Dir = dir - - out, err := cmd.Output() - if err != nil { - return "", errors.Wrapf(ErrCouldNotDetectGitShortHash, "returned error %s: %s", err.Error(), string(out)) - } +func detectGitHash(ctx context.Context, dir string) (string, error) { + return gitRevParse(ctx, dir, ErrCouldNotDetectGitHash, "HEAD") +} - outStr := string(out) - if outStr == "" { - return "", errors.Wrapf(ErrCouldNotDetectGitShortHash, "no remote origin url output") - } +func detectGitShortHash(ctx context.Context, dir string) (string, error) { + return gitRevParse(ctx, dir, ErrCouldNotDetectGitShortHash, "--short=8", "HEAD") +} - return strings.SplitN(outStr, "\n", 2)[0], nil +func detectGitContentHash(ctx context.Context, dir string) (string, error) { + return gitRevParse(ctx, dir, ErrCouldNotDetectGitContentHash, "HEAD^{tree}") } func detectGitBranch(ctx context.Context, dir, gitBranchOverride string) ([]string, error) { diff --git a/util/gitutil/detectgit_test.go b/util/gitutil/detectgit_test.go index 7851b8d427..88d48eaa16 100644 --- a/util/gitutil/detectgit_test.go +++ b/util/gitutil/detectgit_test.go @@ -1,6 +1,11 @@ package gitutil import ( + "context" + "os" + "os/exec" + "path/filepath" + "strings" "testing" ) @@ -52,3 +57,61 @@ func TestParseGitRemoteURL(t *testing.T) { Equal(t, test.expectedGitURL, gitURL) } } + +func TestDetectGitContentHash(t *testing.T) { + t.Parallel() + + // Create a temp git repo with a known commit. + dir := t.TempDir() + + run := func(args ...string) string { + t.Helper() + + cmd := exec.CommandContext(context.Background(), args[0], args[1:]...) //nolint:gosec // test helper + cmd.Dir = dir + + cmd.Env = append(os.Environ(), + "GIT_AUTHOR_NAME=test", + "GIT_AUTHOR_EMAIL=test@test.com", + "GIT_COMMITTER_NAME=test", + "GIT_COMMITTER_EMAIL=test@test.com", + ) + + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("%v failed: %s\n%s", args, err, out) + } + + return strings.TrimSpace(string(out)) + } + + run("git", "init") + run("git", "checkout", "-b", "main") + run("git", "config", "commit.gpgsign", "false") + + // Write a file and commit. + NoError(t, os.WriteFile(filepath.Join(dir, "hello.txt"), []byte("hello\n"), 0o600)) + run("git", "add", "hello.txt") + run("git", "commit", "--no-verify", "-m", "first") + + ctx := context.Background() + + contentHash, err := detectGitContentHash(ctx, dir) + NoError(t, err) + + // Tree hash for a single file "hello.txt" containing "hello\n" is deterministic. + Equal(t, "aaa96ced2d9a1c8e72c56b253a0e2fe78393feb7", contentHash) + + // The tree hash should differ from the commit hash. + commitHash := run("git", "rev-parse", "HEAD") + True(t, contentHash != commitHash) + + // Amend the commit (different commit hash, same tree hash). + run("git", "commit", "--no-verify", "--amend", "-m", "amended") + newCommitHash := run("git", "rev-parse", "HEAD") + True(t, commitHash != newCommitHash) + + contentHash2, err := detectGitContentHash(ctx, dir) + NoError(t, err) + Equal(t, contentHash, contentHash2) // tree hash unchanged +} diff --git a/variables/builtin.go b/variables/builtin.go index 456037b171..91b467386a 100644 --- a/variables/builtin.go +++ b/variables/builtin.go @@ -74,48 +74,67 @@ func BuiltinArgs( if gitMeta == nil { // Ensure SOURCE_DATE_EPOCH is always available ret.Add(arg.EarthlySourceDateEpoch, "0") + ret.Add(arg.EarthSourceDateEpoch, "0") + return ret } // Populate git-related built-in args ret.Add(arg.EarthlyGitHash, gitMeta.Hash) + ret.Add(arg.EarthGitHash, gitMeta.Hash) ret.Add(arg.EarthlyGitShortHash, gitMeta.ShortHash) + ret.Add(arg.EarthGitShortHash, gitMeta.ShortHash) + ret.Add(arg.EarthGitContentHash, gitMeta.ContentHash) branch := firstOrZero(gitMeta.Branch) ret.Add(arg.EarthlyGitBranch, branch) + ret.Add(arg.EarthGitBranch, branch) tag := firstOrZero(gitMeta.Tags) ret.Add(arg.EarthlyGitTag, tag) + ret.Add(arg.EarthGitTag, tag) ret.Add(arg.EarthlyGitOriginURL, gitMeta.RemoteURL) + ret.Add(arg.EarthGitOriginURL, gitMeta.RemoteURL) ret.Add(arg.EarthlyGitOriginURLScrubbed, stringutil.ScrubCredentials(gitMeta.RemoteURL)) + ret.Add(arg.EarthGitOriginURLScrubbed, stringutil.ScrubCredentials(gitMeta.RemoteURL)) ret.Add(arg.EarthlyGitProjectName, getProjectName(gitMeta.RemoteURL)) + ret.Add(arg.EarthGitProjectName, getProjectName(gitMeta.RemoteURL)) ret.Add(arg.EarthlyGitCommitTimestamp, gitMeta.CommitterTimestamp) + ret.Add(arg.EarthGitCommitTimestamp, gitMeta.CommitterTimestamp) if ftrs.GitCommitAuthorTimestamp { ret.Add(arg.EarthlyGitCommitAuthorTimestamp, gitMeta.AuthorTimestamp) + ret.Add(arg.EarthGitCommitAuthorTimestamp, gitMeta.AuthorTimestamp) } ret.Add(arg.EarthlySourceDateEpoch, max(gitMeta.CommitterTimestamp, "0")) + ret.Add(arg.EarthSourceDateEpoch, max(gitMeta.CommitterTimestamp, "0")) if ftrs.EarthlyGitAuthorArgs { ret.Add(arg.EarthlyGitAuthor, gitMeta.AuthorEmail) + ret.Add(arg.EarthGitAuthor, gitMeta.AuthorEmail) ret.Add(arg.EarthlyGitCoAuthors, strings.Join(gitMeta.CoAuthors, " ")) + ret.Add(arg.EarthGitCoAuthors, strings.Join(gitMeta.CoAuthors, " ")) } if ftrs.GitAuthorEmailNameArgs { if gitMeta.AuthorName != "" && gitMeta.AuthorEmail != "" { ret.Add(arg.EarthlyGitAuthor, fmt.Sprintf("%s <%s>", gitMeta.AuthorName, gitMeta.AuthorEmail)) + ret.Add(arg.EarthGitAuthor, fmt.Sprintf("%s <%s>", gitMeta.AuthorName, gitMeta.AuthorEmail)) } ret.Add(arg.EarthlyGitAuthorEmail, gitMeta.AuthorEmail) + ret.Add(arg.EarthGitAuthorEmail, gitMeta.AuthorEmail) ret.Add(arg.EarthlyGitAuthorName, gitMeta.AuthorName) + ret.Add(arg.EarthGitAuthorName, gitMeta.AuthorName) } if ftrs.GitRefs { ret.Add(arg.EarthlyGitRefs, strings.Join(gitMeta.Refs, " ")) + ret.Add(arg.EarthGitRefs, strings.Join(gitMeta.Refs, " ")) } return ret diff --git a/variables/builtin_test.go b/variables/builtin_test.go index fa861d8d11..23d6b9de04 100644 --- a/variables/builtin_test.go +++ b/variables/builtin_test.go @@ -2,6 +2,11 @@ package variables import ( "testing" + + "github.com/EarthBuild/earthbuild/domain" + "github.com/EarthBuild/earthbuild/features" + "github.com/EarthBuild/earthbuild/util/gitutil" + arg "github.com/EarthBuild/earthbuild/variables/reserved" ) func TestGetProjectName(t *testing.T) { @@ -71,3 +76,31 @@ func TestGetProjectName(t *testing.T) { Equal(t, tt.safe, ans) } } + +func TestBuiltinArgsContentHash(t *testing.T) { + t.Parallel() + + ftrs := &features.Features{} + gitMeta := &gitutil.GitMetadata{ + Hash: "abc123def456abc123def456abc123def456abc1", + ShortHash: "abc123de", + ContentHash: "deadbeef01234567890abcdef01234567890abcd", + } + + scope := BuiltinArgs(domain.Target{Target: "test"}, nil, gitMeta, DefaultArgs{}, ftrs, false, false) + + val, found := scope.Get(arg.EarthGitContentHash) + Equal(t, true, found) + Equal(t, "deadbeef01234567890abcdef01234567890abcd", val) +} + +func TestBuiltinArgsContentHashNilGitMeta(t *testing.T) { + t.Parallel() + + ftrs := &features.Features{} + + scope := BuiltinArgs(domain.Target{Target: "test"}, nil, nil, DefaultArgs{}, ftrs, false, false) + + _, found := scope.Get(arg.EarthGitContentHash) + Equal(t, false, found) +} diff --git a/variables/reserved/names.go b/variables/reserved/names.go index 414a876509..9d2f73d260 100644 --- a/variables/reserved/names.go +++ b/variables/reserved/names.go @@ -1,6 +1,22 @@ package reserved const ( + EarthGitAuthor = "EARTH_GIT_AUTHOR" + EarthGitAuthorEmail = "EARTH_GIT_AUTHOR_EMAIL" + EarthGitAuthorName = "EARTH_GIT_AUTHOR_NAME" + EarthGitBranch = "EARTH_GIT_BRANCH" + EarthGitCoAuthors = "EARTH_GIT_CO_AUTHORS" + EarthGitCommitAuthorTimestamp = "EARTH_GIT_COMMIT_AUTHOR_TIMESTAMP" + EarthGitCommitTimestamp = "EARTH_GIT_COMMIT_TIMESTAMP" + EarthGitContentHash = "EARTH_GIT_CONTENT_HASH" + EarthGitHash = "EARTH_GIT_HASH" + EarthGitOriginURL = "EARTH_GIT_ORIGIN_URL" + EarthGitOriginURLScrubbed = "EARTH_GIT_ORIGIN_URL_SCRUBBED" + EarthGitProjectName = "EARTH_GIT_PROJECT_NAME" + EarthGitRefs = "EARTH_GIT_REFS" + EarthGitShortHash = "EARTH_GIT_SHORT_HASH" + EarthGitTag = "EARTH_GIT_TAG" + EarthSourceDateEpoch = "EARTH_SOURCE_DATE_EPOCH" EarthlyBuildSha = "EARTHLY_BUILD_SHA" EarthlyGitBranch = "EARTHLY_GIT_BRANCH" EarthlyGitCommitTimestamp = "EARTHLY_GIT_COMMIT_TIMESTAMP" @@ -46,6 +62,22 @@ var args map[string]struct{} func init() { args = map[string]struct{}{ + EarthGitAuthor: {}, + EarthGitAuthorEmail: {}, + EarthGitAuthorName: {}, + EarthGitBranch: {}, + EarthGitCoAuthors: {}, + EarthGitCommitAuthorTimestamp: {}, + EarthGitCommitTimestamp: {}, + EarthGitContentHash: {}, + EarthGitHash: {}, + EarthGitOriginURL: {}, + EarthGitOriginURLScrubbed: {}, + EarthGitProjectName: {}, + EarthGitRefs: {}, + EarthGitShortHash: {}, + EarthGitTag: {}, + EarthSourceDateEpoch: {}, EarthlyBuildSha: {}, EarthlyGitBranch: {}, EarthlyGitCommitTimestamp: {},