-
Notifications
You must be signed in to change notification settings - Fork 58
feat: add git submodule support #485
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 10 commits
81b6fd7
5de553d
adbdccf
18384d5
4d3e529
e183aff
2d1df11
5407a5b
463fd91
04f798b
db5e40d
223d3e4
5c59408
8134964
442477e
fb31cac
68ace4b
149ae9a
3f81838
ea25253
cb11759
76138b0
79d1fc5
92783ef
92f5137
6f87394
ad64adf
334ade4
e371318
afcce7f
b82aee0
d6ec3d5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -28,6 +28,7 @@ | |
| | `--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-clone-submodules` | `ENVBUILDER_GIT_CLONE_SUBMODULES` | | Recursively clone Git submodules after cloning the repository. | | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We may also want to add
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WDYT about
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Works for me 👍🏻
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just add conditional string eval, default, and 0 is 'false' and a number is Depth? |
||
| | `--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. | | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -7,14 +7,17 @@ import ( | |||
| "fmt" | ||||
| "io" | ||||
| "net" | ||||
| "net/url" | ||||
| "os" | ||||
| "path" | ||||
| "strings" | ||||
|
|
||||
| "github.com/coder/envbuilder/options" | ||||
|
|
||||
| giturls "github.com/chainguard-dev/git-urls" | ||||
| "github.com/go-git/go-billy/v5" | ||||
| "github.com/go-git/go-git/v5" | ||||
| "github.com/go-git/go-git/v5/config" | ||||
| "github.com/go-git/go-git/v5/plumbing" | ||||
| "github.com/go-git/go-git/v5/plumbing/cache" | ||||
| "github.com/go-git/go-git/v5/plumbing/protocol/packp/capability" | ||||
|
|
@@ -41,6 +44,7 @@ type CloneRepoOptions struct { | |||
| Depth int | ||||
| CABundle []byte | ||||
| ProxyOptions transport.ProxyOptions | ||||
| Submodules bool | ||||
| } | ||||
|
|
||||
| // CloneRepo will clone the repository at the given URL into the given path. | ||||
|
|
@@ -119,7 +123,7 @@ func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOpt | |||
| return false, nil | ||||
| } | ||||
|
|
||||
| _, err = git.CloneContext(ctx, gitStorage, fs, &git.CloneOptions{ | ||||
| repo, err = git.CloneContext(ctx, gitStorage, fs, &git.CloneOptions{ | ||||
| URL: parsed.String(), | ||||
| Auth: opts.RepoAuth, | ||||
| Progress: opts.Progress, | ||||
|
|
@@ -136,6 +140,15 @@ func CloneRepo(ctx context.Context, logf func(string, ...any), opts CloneRepoOpt | |||
| if err != nil { | ||||
| return false, fmt.Errorf("clone %q: %w", opts.RepoURL, err) | ||||
| } | ||||
|
|
||||
| // Initialize submodules if requested | ||||
| if opts.Submodules { | ||||
| err = initSubmodules(ctx, logf, repo, opts) | ||||
| if err != nil { | ||||
| return true, fmt.Errorf("init submodules: %w", err) | ||||
| } | ||||
| } | ||||
|
johnstcn marked this conversation as resolved.
Outdated
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just saw this -- apaprently https://pkg.go.dev/github.com/go-git/go-git#CloneOptions
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems like the preferable option to me vs bespoke implementation. 👍🏻
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That function breaks because of the relative path, and reason for handling this manually (it was confounding and took some time).
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh, and v6 renames it, fixes the typo but we're not at v6 yet.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I take it you're referencing this commit? We could fork and backport this fix to v5 until v6 comes out. WDYT?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is nothing concrete that the relative path issues are resolved, more they're not. Checks indicate that there are known bugs with:
|
||||
|
|
||||
| return true, nil | ||||
| } | ||||
|
|
||||
|
|
@@ -361,6 +374,7 @@ func CloneOptionsFromOptions(logf func(string, ...any), options options.Options) | |||
| ThinPack: options.GitCloneThinPack, | ||||
| Depth: int(options.GitCloneDepth), | ||||
| CABundle: caBundle, | ||||
| Submodules: options.GitCloneSubmodules, | ||||
| } | ||||
|
|
||||
| cloneOpts.RepoAuth = SetupRepoAuth(logf, &options) | ||||
|
|
@@ -418,3 +432,271 @@ func ProgressWriter(write func(line string, args ...any)) io.WriteCloser { | |||
| done: done, | ||||
| } | ||||
| } | ||||
|
|
||||
| // resolveSubmoduleURL resolves a potentially relative submodule URL against the parent repository URL | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||
| // ResolveSubmoduleURLForTest is exported for testing resolveSubmoduleURL logic | ||||
| func ResolveSubmoduleURLForTest(parentURL, submoduleURL string) (string, error) { | ||||
|
johnstcn marked this conversation as resolved.
Outdated
|
||||
| // If the submodule URL is absolute (contains ://) or doesn't start with ./ or ../, return it as-is | ||||
| if strings.Contains(submoduleURL, "://") || (!strings.HasPrefix(submoduleURL, "../") && !strings.HasPrefix(submoduleURL, "./")) { | ||||
| return submoduleURL, nil | ||||
| } | ||||
|
|
||||
| // Parse the parent URL | ||||
| parentParsed, err := url.Parse(parentURL) | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the parent URL is SCP-like ( Suggestion: Use
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You're right that However, I'd prefer to keep the current implementation for now for a few reasons: This function was built specifically to work around go-git's broken relative URL resolution. The native HTTPS URLs lose a slash when using Using If we do see issues with SCP-like parent URLs in the wild, we can add detection similar to what we did in RedactURL and handle that case separately. But I'd rather not expand scope here without a concrete use case driving it.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In that case, we should
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bjornrobertsson I had an LLM try out this suggestion, and from what I can tell it works as expected? See change + test cases in this patch: diff --git git/git.go git/git.go
index 65edbcf..8f048aa 100644
--- git/git.go
+++ git/git.go
@@ -509,32 +509,23 @@ func RedactURL(u string) string {
}
// ResolveSubmoduleURL resolves a potentially relative submodule URL against a parent repository URL.
-//
-// Limitation: SCP-like URLs (e.g., git@github.com:org/repo.git) are not supported as parent URLs
-// when the submodule uses a relative path. This is a known limitation.
-// See: https://github.com/coder/envbuilder/issues/492
func ResolveSubmoduleURL(parentURL, submoduleURL string) (string, error) {
// If the submodule URL is absolute (contains ://) or doesn't start with ./ or ../, return it as-is
if strings.Contains(submoduleURL, "://") || (!strings.HasPrefix(submoduleURL, "../") && !strings.HasPrefix(submoduleURL, "./")) {
return submoduleURL, nil
}
- // Check if parent URL is SCP-like (e.g., git@github.com:org/repo.git)
- // These cannot be properly parsed by net/url and relative submodule resolution is not supported.
- if scpLikeURLRegex.MatchString(parentURL) {
- return "", fmt.Errorf("relative submodule URL %q cannot be resolved: parent URL %q uses SCP-like syntax which is not supported for relative submodule resolution (see https://github.com/coder/envbuilder/issues/492)", submoduleURL, RedactURL(parentURL))
- }
-
- // Parse the parent URL
- parentParsed, err := url.Parse(parentURL)
+ // Parse the parent URL using go-git's endpoint parser, which handles
+ // SCP-like URLs (git@host:path) in addition to standard URLs.
+ parentEP, err := transport.NewEndpoint(parentURL)
if err != nil {
return "", fmt.Errorf("parse parent URL: %w", err)
}
- // For relative URLs, we need to resolve them against the parent's path
- // The parent path represents a repository (like a file in filesystem terms)
- // So ../something means "sibling repository"
- parentPath := strings.TrimSuffix(parentParsed.Path, "/")
+ // For relative URLs, we need to resolve them against the parent's path.
+ // The parent path represents a repository (like a file in filesystem terms),
+ // so ../something means "sibling repository".
+ parentPath := strings.TrimSuffix(parentEP.Path, "/")
// Split the submodule URL into components
// and manually walk up the directory tree for each ../
@@ -554,18 +545,9 @@ func ResolveSubmoduleURL(parentURL, submoduleURL string) (string, error) {
}
}
- // Clean the final path
- resolvedPath := path.Clean(currentPath)
-
- // Construct the absolute URL
- resolvedParsed := &url.URL{
- Scheme: parentParsed.Scheme,
- User: parentParsed.User,
- Host: parentParsed.Host,
- Path: resolvedPath,
- }
-
- return resolvedParsed.String(), nil
+ // Reconstruct the URL with the resolved path.
+ parentEP.Path = path.Clean(currentPath)
+ return parentEP.String(), nil
}
// initSubmodules recursively initializes and updates all submodules in the repository.
diff --git git/git_test.go git/git_test.go
index c3a0b69..a324a2a 100644
--- git/git_test.go
+++ git/git_test.go
@@ -807,16 +807,28 @@ func TestResolveSubmoduleURL(t *testing.T) {
expect: "https://example.com/org/main.git/extras/tool.git",
},
{
- name: "badParent",
- parentURL: "://bad",
- subURL: "./child",
- expectErr: "parse parent URL",
+ name: "scpRelativeSibling",
+ parentURL: "git@github.com:org/main.git",
+ subURL: "../deps/lib.git",
+ expect: "ssh://git@github.com/org/deps/lib.git",
},
{
- name: "scpParentWithRelativeSubmodule",
+ name: "scpRelativeChild",
parentURL: "git@github.com:org/main.git",
- subURL: "../other/submodule.git",
- expectErr: "SCP-like syntax which is not supported",
+ subURL: "./extras/tool.git",
+ expect: "ssh://git@github.com/org/main.git/extras/tool.git",
+ },
+ {
+ name: "scpMultiLevelUp",
+ parentURL: "git@github.com:a/b/c/repo.git",
+ subURL: "../../other/lib.git",
+ expect: "ssh://git@github.com/a/b/other/lib.git",
+ },
+ {
+ name: "httpsMultiLevelUp",
+ parentURL: "https://example.com/a/b/c/repo.git",
+ subURL: "../../other/lib.git",
+ expect: "https://example.com/a/b/other/lib.git",
},
{
name: "scpParentWithAbsoluteSubmodule",
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This also supports ports in SCP-like URLs: {
name: "scpWithPort",
parentURL: "git@github.com:2222:org/main.git",
subURL: "../deps/lib.git",
expect: "ssh://git@github.com:2222/org/deps/lib.git",
}, |
||||
| if err != nil { | ||||
| return "", fmt.Errorf("parse parent URL: %w", err) | ||||
| } | ||||
|
|
||||
| // For relative URLs, we need to resolve them against the parent's path | ||||
| // The parent path represents a repository (like a file in filesystem terms) | ||||
| // So ../something means "sibling repository" | ||||
| parentPath := strings.TrimSuffix(parentParsed.Path, "/") | ||||
|
|
||||
| // Split the submodule URL into components | ||||
| // and manually walk up the directory tree for each ../ | ||||
| currentPath := parentPath | ||||
| relativeParts := strings.Split(submoduleURL, "/") | ||||
|
|
||||
| for _, part := range relativeParts { | ||||
| if part == ".." { | ||||
| // Go up one directory | ||||
| currentPath = path.Dir(currentPath) | ||||
| } else if part == "." { | ||||
| // Stay in current directory | ||||
| continue | ||||
| } else if part != "" { | ||||
| // Add this component to the path | ||||
| currentPath = currentPath + "/" + part | ||||
| } | ||||
| } | ||||
|
|
||||
| // Clean the final path | ||||
| resolvedPath := path.Clean(currentPath) | ||||
|
|
||||
| // Construct the absolute URL | ||||
| resolvedParsed := &url.URL{ | ||||
| Scheme: parentParsed.Scheme, | ||||
| User: parentParsed.User, | ||||
| Host: parentParsed.Host, | ||||
| Path: resolvedPath, | ||||
| } | ||||
|
|
||||
| return resolvedParsed.String(), nil | ||||
| } | ||||
|
|
||||
| // initSubmodules recursively initializes and updates all submodules in the repository. | ||||
| func initSubmodules(ctx context.Context, logf func(string, ...any), repo *git.Repository, opts CloneRepoOptions) error { | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm always a bit wary about recursive functions. I'd feel better about this if we had a maximum depth of which to recurse. I'd wager that most repos won't need more than 2 iterations. If
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed the acceptance to 0-10, true, false - and added a Depth testing |
||||
| logf("🔗 Initializing git submodules...") | ||||
|
|
||||
| w, err := repo.Worktree() | ||||
| if err != nil { | ||||
| return fmt.Errorf("get worktree: %w", err) | ||||
| } | ||||
|
|
||||
| subs, err := w.Submodules() | ||||
| if err != nil { | ||||
| return fmt.Errorf("get submodules: %w", err) | ||||
| } | ||||
|
|
||||
| if len(subs) == 0 { | ||||
| logf("No submodules found") | ||||
| return nil | ||||
| } | ||||
|
|
||||
| logf("Found %d submodule(s)", len(subs)) | ||||
|
|
||||
| // Get the parent repository URL for resolving relative submodule URLs | ||||
| cfg, err := repo.Config() | ||||
| if err != nil { | ||||
| return fmt.Errorf("get repo config: %w", err) | ||||
| } | ||||
|
|
||||
| parentURL := opts.RepoURL | ||||
| if origin, hasOrigin := cfg.Remotes["origin"]; hasOrigin && len(origin.URLs) > 0 { | ||||
| parentURL = origin.URLs[0] | ||||
| } | ||||
| logf("Parent repository URL: %s", parentURL) | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we consider logging the redacted URL here? This URL could contain credentials.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
|
|
||||
| for _, sub := range subs { | ||||
| subConfig := sub.Config() | ||||
| logf("📦 Initializing submodule: %s", subConfig.Name) | ||||
| logf(" Submodule path: %s", subConfig.Path) | ||||
| logf(" Submodule URL (from .gitmodules): %s", subConfig.URL) | ||||
|
|
||||
| // Get the expected commit hash | ||||
| subStatus, err := sub.Status() | ||||
| if err != nil { | ||||
| return fmt.Errorf("get submodule status for %q: %w", subConfig.Name, err) | ||||
| } | ||||
| logf(" Expected commit: %s", subStatus.Expected) | ||||
|
|
||||
| // Resolve the submodule URL | ||||
| resolvedURL, err := ResolveSubmoduleURLForTest(parentURL, subConfig.URL) | ||||
| if err != nil { | ||||
| return fmt.Errorf("resolve submodule URL for %q: %w", subConfig.Name, err) | ||||
| } | ||||
| logf(" Resolved URL: %s", resolvedURL) | ||||
|
|
||||
| // Clone the submodule manually | ||||
| err = cloneSubmodule(ctx, logf, w, subConfig, subStatus.Expected, resolvedURL, opts) | ||||
| if err != nil { | ||||
| return fmt.Errorf("clone submodule %q: %w", subConfig.Name, err) | ||||
| } | ||||
|
|
||||
| logf("✓ Submodule initialized: %s", subConfig.Name) | ||||
|
|
||||
| // Recursively handle nested submodules | ||||
| subRepo, err := sub.Repository() | ||||
| if err != nil { | ||||
| logf(" ⚠ Could not open submodule repository %s: %v", subConfig.Name, err) | ||||
| continue | ||||
| } | ||||
|
|
||||
| // Check for nested submodules | ||||
| subWorktree, err := subRepo.Worktree() | ||||
| if err == nil { | ||||
| nestedSubs, err := subWorktree.Submodules() | ||||
| if err == nil && len(nestedSubs) > 0 { | ||||
| logf(" Found %d nested submodule(s) in %s", len(nestedSubs), subConfig.Name) | ||||
| // Create new opts with the submodule's URL as the parent | ||||
| nestedOpts := opts | ||||
| nestedOpts.RepoURL = resolvedURL | ||||
| err = initSubmodules(ctx, logf, subRepo, nestedOpts) | ||||
| if err != nil { | ||||
| return fmt.Errorf("init nested submodules in %q: %w", subConfig.Name, err) | ||||
| } | ||||
| } | ||||
| } | ||||
| } | ||||
|
|
||||
| logf("✓ All submodules initialized successfully") | ||||
| return nil | ||||
| } | ||||
|
|
||||
| // cloneSubmodule manually clones a submodule repository | ||||
| func cloneSubmodule(ctx context.Context, logf func(string, ...any), parentWorktree *git.Worktree, subConfig *config.Submodule, expectedHash plumbing.Hash, resolvedURL string, opts CloneRepoOptions) error { | ||||
| // Get the submodule directory within the parent worktree | ||||
| submodulePath := subConfig.Path | ||||
|
|
||||
| // Create the submodule directory | ||||
| subFS, err := parentWorktree.Filesystem.Chroot(submodulePath) | ||||
| if err != nil { | ||||
| return fmt.Errorf("chroot to submodule path: %w", err) | ||||
| } | ||||
|
|
||||
| // Check if already cloned | ||||
| _, err = subFS.Stat(".git") | ||||
| if err == nil { | ||||
| logf(" Submodule already cloned, checking out expected commit...") | ||||
| // Open the existing repository | ||||
| subRepo, err := git.Open( | ||||
| filesystem.NewStorage(subFS, cache.NewObjectLRU(cache.DefaultMaxSize)), | ||||
| subFS, | ||||
| ) | ||||
| if err != nil { | ||||
| return fmt.Errorf("open existing submodule: %w", err) | ||||
| } | ||||
|
|
||||
| subWorktree, err := subRepo.Worktree() | ||||
| if err != nil { | ||||
| return fmt.Errorf("get submodule worktree: %w", err) | ||||
| } | ||||
|
|
||||
| // Checkout the expected commit | ||||
| err = subWorktree.Checkout(&git.CheckoutOptions{ | ||||
| Hash: expectedHash, | ||||
| }) | ||||
| if err != nil { | ||||
| return fmt.Errorf("checkout expected commit: %w", err) | ||||
| } | ||||
| return nil | ||||
| } | ||||
|
|
||||
| // Clone the submodule | ||||
| logf(" Cloning submodule from: %s", resolvedURL) | ||||
|
|
||||
| // Create .git directory for the submodule | ||||
| err = subFS.MkdirAll(".git", 0o755) | ||||
| if err != nil { | ||||
| return fmt.Errorf("create .git directory: %w", err) | ||||
| } | ||||
|
|
||||
| subGitDir, err := subFS.Chroot(".git") | ||||
| if err != nil { | ||||
| return fmt.Errorf("chroot to .git: %w", err) | ||||
| } | ||||
|
|
||||
| gitStorage := filesystem.NewStorage(subGitDir, cache.NewObjectLRU(cache.DefaultMaxSize*10)) | ||||
|
|
||||
| // Clone the submodule repository | ||||
| // Use SingleBranch=false to fetch all branches so we can find the commit | ||||
| subRepo, err := git.CloneContext(ctx, gitStorage, subFS, &git.CloneOptions{ | ||||
| URL: resolvedURL, | ||||
| Auth: opts.RepoAuth, | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Security: This allows the parent repo auth to be exfiltrated by a malicious Git host. We should only forward auth if the submodule shares the same host as the parent repo. Or we could consider adding an auth allow list if users need more control.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||
| Progress: opts.Progress, | ||||
| InsecureSkipTLS: opts.Insecure, | ||||
| CABundle: opts.CABundle, | ||||
| ProxyOptions: opts.ProxyOptions, | ||||
| SingleBranch: false, // Fetch all branches | ||||
| NoCheckout: true, // Don't checkout yet, we'll do it manually | ||||
| }) | ||||
| if err != nil && !errors.Is(err, git.ErrRepositoryAlreadyExists) { | ||||
| return fmt.Errorf("clone submodule repository: %w", err) | ||||
| } | ||||
|
|
||||
| // Verify the commit exists | ||||
| logf(" Verifying commit exists: %s", expectedHash) | ||||
| _, err = subRepo.CommitObject(expectedHash) | ||||
| if err != nil { | ||||
| // Commit not found, try fetching with the specific hash | ||||
| logf(" Commit not found, attempting to fetch it directly...") | ||||
| err = subRepo.FetchContext(ctx, &git.FetchOptions{ | ||||
| RemoteName: "origin", | ||||
| RefSpecs: []config.RefSpec{ | ||||
| config.RefSpec("+" + expectedHash.String() + ":" + expectedHash.String()), | ||||
| }, | ||||
| Auth: opts.RepoAuth, | ||||
| Progress: opts.Progress, | ||||
| InsecureSkipTLS: opts.Insecure, | ||||
| CABundle: opts.CABundle, | ||||
| ProxyOptions: opts.ProxyOptions, | ||||
| }) | ||||
| if err != nil && err != git.NoErrAlreadyUpToDate { | ||||
| // If that fails, try fetching all refs | ||||
| logf(" Direct fetch failed, fetching all refs...") | ||||
| err = subRepo.FetchContext(ctx, &git.FetchOptions{ | ||||
| RemoteName: "origin", | ||||
| Auth: opts.RepoAuth, | ||||
| Progress: opts.Progress, | ||||
| InsecureSkipTLS: opts.Insecure, | ||||
| CABundle: opts.CABundle, | ||||
| ProxyOptions: opts.ProxyOptions, | ||||
| }) | ||||
| if err != nil && err != git.NoErrAlreadyUpToDate { | ||||
| return fmt.Errorf("fetch commit %s: %w", expectedHash, err) | ||||
| } | ||||
| } | ||||
|
|
||||
| // Verify again | ||||
| _, err = subRepo.CommitObject(expectedHash) | ||||
| if err != nil { | ||||
| return fmt.Errorf("commit %s still not found after fetch: %w", expectedHash, err) | ||||
| } | ||||
| } | ||||
|
|
||||
| // Checkout the specific commit expected by the parent repository | ||||
| logf(" Checking out commit: %s", expectedHash) | ||||
| subWorktree, err := subRepo.Worktree() | ||||
| if err != nil { | ||||
| return fmt.Errorf("get submodule worktree: %w", err) | ||||
| } | ||||
|
|
||||
| err = subWorktree.Checkout(&git.CheckoutOptions{ | ||||
| Hash: expectedHash, | ||||
| }) | ||||
| if err != nil { | ||||
| return fmt.Errorf("checkout expected commit %s: %w", expectedHash, err) | ||||
| } | ||||
|
|
||||
| return nil | ||||
| } | ||||
Uh oh!
There was an error while loading. Please reload this page.