Repository: coder/envbuilder
Pull request #446 (2025-01-11 03:49:17)

feat: add git clone with thinpack option to support self hosted azure devops
The program uses only the hostname to address the issue of requiring thin pack to perform a git clone operation on Azure DevOps, as shown in the code snippet below.

https://github.com/coder/envbuilder/blob/d045c1ce27a5995ac60409bd48ad8e90861b6bfd/git/git.go#L56-L78

However, since Azure DevOps supports self-hosted servers, the hostname may not necessarily be dev.azure.com. Therefore, it is desired to add a command-line option to enable this functionality.


Patch:
diff --git a/docs/env-variables.md b/docs/env-variables.md
index 861f31b..e6fa7ca 100644
--- a/docs/env-variables.md
+++ b/docs/env-variables.md
@@ -27,6 +27,7 @@
 | `--git-url` | `ENVBUILDER_GIT_URL` |  | The URL of a Git repository containing a Devcontainer or Docker image to clone. This is optional. |
 | `--git-clone-depth` | `ENVBUILDER_GIT_CLONE_DEPTH` |  | The depth to use when cloning the Git repository. |
 | `--git-clone-single-branch` | `ENVBUILDER_GIT_CLONE_SINGLE_BRANCH` |  | Clone only a single branch of the Git repository. |
+| `--git-clone-thinpack` | `ENVBUILDER_GIT_CLONE_THINPACK` | `true` | Git clone with thin pack compatibility enabled, ensuring that even when thin pack compatibility is activated,it will not be turned on for the domain dev.zaure.com. |
 | `--git-username` | `ENVBUILDER_GIT_USERNAME` |  | The username to use for Git authentication. This is optional. |
 | `--git-password` | `ENVBUILDER_GIT_PASSWORD` |  | The password to use for Git authentication. This is optional. |
 | `--git-ssh-private-key-path` | `ENVBUILDER_GIT_SSH_PRIVATE_KEY_PATH` |  | Path to an SSH private key to be used for Git authentication. If this is set, then GIT_SSH_PRIVATE_KEY_BASE64 cannot be set. |
diff --git a/git/git.go b/git/git.go
index f37b968..efcffa9 100644
--- a/git/git.go
+++ b/git/git.go
@@ -37,6 +37,7 @@ type CloneRepoOptions struct {
 	Progress     sideband.Progress
 	Insecure     bool
 	SingleBranch bool
+	ThinPack     bool
 	Depth        int
 	CABundle     []byte
 	ProxyOptions transport.ProxyOptions
@@ -53,7 +54,13 @@ func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOpt
 		return false, fmt.Errorf("parse url %q: %w", opts.RepoURL, err)
 	}
 	logf("Parsed Git URL as %q", parsed.Redacted())
-	if parsed.Hostname() == "dev.azure.com" {
+
+	thinPack := true
+
+	if !opts.ThinPack {
+		thinPack = false
+		logf("ThinPack options is false, Marking thin-pack as unsupported")
+	} else if parsed.Hostname() == "dev.azure.com" {
 		// Azure DevOps requires capabilities multi_ack / multi_ack_detailed,
 		// which are not fully implemented and by default are included in
 		// transport.UnsupportedCapabilities.
@@ -71,10 +78,14 @@ func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOpt
 		//
 		// This is knowingly not safe to call in parallel, but it seemed
 		// like the least-janky place to add a super janky hack.
+		thinPack = false
+		logf("Workaround for Azure DevOps: marking thin-pack as unsupported")
+	}
+
+	if !thinPack {
 		transport.UnsupportedCapabilities = []capability.Capability{
 			capability.ThinPack,
 		}
-		logf("Workaround for Azure DevOps: marking thin-pack as unsupported")
 	}
 
 	err = opts.Storage.MkdirAll(opts.Path, 0o755)
@@ -347,6 +358,7 @@ func CloneOptionsFromOptions(logf func(string, ...any), options options.Options)
 		Storage:      options.Filesystem,
 		Insecure:     options.Insecure,
 		SingleBranch: options.GitCloneSingleBranch,
+		ThinPack:     options.GitCloneThinPack,
 		Depth:        int(options.GitCloneDepth),
 		CABundle:     caBundle,
 	}
diff --git a/options/options.go b/options/options.go
index c2b6efe..8cdf723 100644
--- a/options/options.go
+++ b/options/options.go
@@ -106,6 +106,8 @@ type Options struct {
 	GitCloneDepth int64
 	// GitCloneSingleBranch clone only a single branch of the Git repository.
 	GitCloneSingleBranch bool
+	// GitCloneThinPack clone with thin pack compabilities. This is optional.
+	GitCloneThinPack bool
 	// GitUsername is the username to use for Git authentication. This is
 	// optional.
 	GitUsername string
@@ -375,6 +377,15 @@ func (o *Options) CLI() serpent.OptionSet {
 			Value:       serpent.BoolOf(&o.GitCloneSingleBranch),
 			Description: "Clone only a single branch of the Git repository.",
 		},
+		{
+			Flag:    "git-clone-thinpack",
+			Env:     WithEnvPrefix("GIT_CLONE_THINPACK"),
+			Value:   serpent.BoolOf(&o.GitCloneThinPack),
+			Default: "true",
+			Description: "Git clone with thin pack compatibility enabled, " +
+				"ensuring that even when thin pack compatibility is activated," +
+				"it will not be turned on for the domain dev.zaure.com.",
+		},
 		{
 			Flag:        "git-username",
 			Env:         WithEnvPrefix("GIT_USERNAME"),


Test patch:
diff --git a/options/options_test.go b/options/options_test.go
index bf7a216..ed5dcd3 100644
--- a/options/options_test.go
+++ b/options/options_test.go
@@ -38,31 +38,39 @@ func TestEnvOptionParsing(t *testing.T) {
 		t.Run("lowercase", func(t *testing.T) {
 			t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "true")
 			t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "false")
+			t.Setenv(options.WithEnvPrefix("GIT_CLONE_THINPACK"), "false")
 			o := runCLI()
 			require.True(t, o.SkipRebuild)
 			require.False(t, o.GitCloneSingleBranch)
+			require.False(t, o.GitCloneThinPack)
 		})
 
 		t.Run("uppercase", func(t *testing.T) {
 			t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "TRUE")
 			t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "FALSE")
+			t.Setenv(options.WithEnvPrefix("GIT_CLONE_THINPACK"), "FALSE")
 			o := runCLI()
 			require.True(t, o.SkipRebuild)
 			require.False(t, o.GitCloneSingleBranch)
+			require.False(t, o.GitCloneThinPack)
 		})
 
 		t.Run("numeric", func(t *testing.T) {
 			t.Setenv(options.WithEnvPrefix("SKIP_REBUILD"), "1")
 			t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "0")
+			t.Setenv(options.WithEnvPrefix("GIT_CLONE_THINPACK"), "0")
 			o := runCLI()
 			require.True(t, o.SkipRebuild)
 			require.False(t, o.GitCloneSingleBranch)
+			require.False(t, o.GitCloneThinPack)
 		})
 
 		t.Run("empty", func(t *testing.T) {
 			t.Setenv(options.WithEnvPrefix("GIT_CLONE_SINGLE_BRANCH"), "")
+			t.Setenv(options.WithEnvPrefix("GIT_CLONE_THINPACK"), "")
 			o := runCLI()
 			require.False(t, o.GitCloneSingleBranch)
+			require.True(t, o.GitCloneThinPack)
 		})
 	})
 }
diff --git a/options/testdata/options.golden b/options/testdata/options.golden
index 6a8145a..92a8523 100644
--- a/options/testdata/options.golden
+++ b/options/testdata/options.golden
@@ -99,6 +99,11 @@ OPTIONS:
       --git-clone-single-branch bool, $ENVBUILDER_GIT_CLONE_SINGLE_BRANCH
           Clone only a single branch of the Git repository.
 
+      --git-clone-thinpack bool, $ENVBUILDER_GIT_CLONE_THINPACK (default: true)
+          Git clone with thin pack compatibility enabled, ensuring that even
+          when thin pack compatibility is activated,it will not be turned on for
+          the domain dev.zaure.com.
+
       --git-http-proxy-url string, $ENVBUILDER_GIT_HTTP_PROXY_URL
           The URL for the HTTP proxy. This is optional.