diff --git a/get_git.go b/get_git.go index 28931753..f38e0d29 100644 --- a/get_git.go +++ b/get_git.go @@ -125,7 +125,7 @@ func (g *GitGetter) Get(dst string, u *url.URL) error { return err } if err == nil { - err = g.update(ctx, dst, sshKeyFile, ref, depth) + err = g.update(ctx, dst, sshKeyFile, u, ref, depth) } else { err = g.clone(ctx, dst, sshKeyFile, u, ref, depth) } @@ -228,28 +228,64 @@ func (g *GitGetter) clone(ctx context.Context, dst, sshKeyFile string, u *url.UR return nil } -func (g *GitGetter) update(ctx context.Context, dst, sshKeyFile, ref string, depth int) error { - // Determine if we're a branch. If we're NOT a branch, then we just - // switch to master prior to checking out - cmd := exec.CommandContext(ctx, "git", "show-ref", "-q", "--verify", "refs/heads/"+ref) +func (g *GitGetter) update(ctx context.Context, dst, sshKeyFile string, u *url.URL, ref string, depth int) error { + // Remove all variations of .git directories + err := removeCaseInsensitiveGitDirectory(dst) + if err != nil { + return err + } + + // Initialize the git repository + cmd := exec.CommandContext(ctx, "git", "init") + cmd.Dir = dst + err = getRunCommand(cmd) + if err != nil { + return err + } + + // Add the git remote + cmd = exec.CommandContext(ctx, "git", "remote", "add", "origin", "--", u.String()) + cmd.Dir = dst + err = getRunCommand(cmd) + if err != nil { + return err + } + + // Fetch the remote ref + cmd = exec.CommandContext(ctx, "git", "fetch", "--tags") + cmd.Dir = dst + err = getRunCommand(cmd) + if err != nil { + return err + } + + // Fetch the remote ref + cmd = exec.CommandContext(ctx, "git", "fetch", "origin", "--", ref) cmd.Dir = dst + err = getRunCommand(cmd) + if err != nil { + return err + } - if getRunCommand(cmd) != nil { - // Not a branch, switch to default branch. This will also catch - // non-existent branches, in which case we want to switch to default - // and then checkout the proper branch later. - ref = findDefaultBranch(ctx, dst) + // Reset the branch to the fetched ref + cmd = exec.CommandContext(ctx, "git", "reset", "--hard", "FETCH_HEAD") + cmd.Dir = dst + err = getRunCommand(cmd) + if err != nil { + return err } - // We have to be on a branch to pull - if err := g.checkout(ctx, dst, ref); err != nil { + // Checkout ref branch + err = g.checkout(ctx, dst, ref) + if err != nil { return err } + // Pull the latest changes from the ref branch if depth > 0 { - cmd = exec.CommandContext(ctx, "git", "pull", "--depth", strconv.Itoa(depth), "--ff-only") + cmd = exec.CommandContext(ctx, "git", "pull", "origin", "--depth", strconv.Itoa(depth), "--ff-only", "--", ref) } else { - cmd = exec.CommandContext(ctx, "git", "pull", "--ff-only") + cmd = exec.CommandContext(ctx, "git", "pull", "origin", "--ff-only", "--", ref) } cmd.Dir = dst @@ -377,3 +413,20 @@ func checkGitVersion(ctx context.Context, min string) error { return nil } + +// removeCaseInsensitiveGitDirectory removes all .git directory variations +func removeCaseInsensitiveGitDirectory(dst string) error { + files, err := os.ReadDir(dst) + if err != nil { + return fmt.Errorf("Failed to read the destination directory %s during git update", dst) + } + for _, f := range files { + if strings.EqualFold(f.Name(), ".git") && f.IsDir() { + err := os.RemoveAll(filepath.Join(dst, f.Name())) + if err != nil { + return fmt.Errorf("Failed to remove the .git directory in the destination directory %s during git update", dst) + } + } + } + return nil +} diff --git a/get_git_test.go b/get_git_test.go index 8e397193..64279b76 100644 --- a/get_git_test.go +++ b/get_git_test.go @@ -169,8 +169,9 @@ func TestGitGetter_remoteWithoutMaster(t *testing.T) { t.Fatalf("err: %s", err) } + dst2 := tempDir(t) // Get again should work - if err := g.Get(dst, repo.url); err != nil { + if err := g.Get(dst2, repo.url); err != nil { t.Fatalf("err: %s", err) } @@ -866,6 +867,123 @@ func TestGitGetter_BadRemoteUrl(t *testing.T) { } } +func TestGitGetter_BadGitConfig(t *testing.T) { + if !testHasGit { + t.Log("git not found, skipping") + t.Skip() + } + + ctx := context.Background() + g := new(GitGetter) + dst := tempDir(t) + + url, err := url.Parse("https://github.com/hashicorp/go-getter") + if err != nil { + t.Fatal(err) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + t.Fatalf(err.Error()) + } + if err == nil { + // Update the repository containing the bad git config. + // This should remove the bad git config file and initialize a new one. + err = g.update(ctx, dst, testGitToken, url, "main", 1) + } else { + // Clone a repository with a git config file + err = g.clone(ctx, dst, testGitToken, url, "main", 1) + if err != nil { + t.Fatalf(err.Error()) + } + + // Edit the git config file to simulate a bad git config + gitConfigPath := filepath.Join(dst, ".git", "config") + err = os.WriteFile(gitConfigPath, []byte("bad config"), 0600) + if err != nil { + t.Fatalf(err.Error()) + } + + // Update the repository containing the bad git config. + // This should remove the bad git config file and initialize a new one. + err = g.update(ctx, dst, testGitToken, url, "main", 1) + } + if err != nil { + t.Fatalf(err.Error()) + } + + // Check if the .git/config file contains "bad config" + gitConfigPath := filepath.Join(dst, ".git", "config") + configBytes, err := os.ReadFile(gitConfigPath) + if err != nil { + t.Fatalf(err.Error()) + } + if strings.Contains(string(configBytes), "bad config") { + t.Fatalf("The .git/config file contains 'bad config'") + } +} + +func TestGitGetter_BadGitDirName(t *testing.T) { + if !testHasGit { + t.Log("git not found, skipping") + t.Skip() + } + + ctx := context.Background() + g := new(GitGetter) + dst := tempDir(t) + + url, err := url.Parse("https://github.com/hashicorp/go-getter") + if err != nil { + t.Fatal(err) + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + t.Fatalf(err.Error()) + } + if err == nil { + // Remove all variations of .git directories + err = removeCaseInsensitiveGitDirectory(dst) + if err != nil { + t.Fatalf(err.Error()) + } + } else { + // Clone a repository with a git directory + err = g.clone(ctx, dst, testGitToken, url, "main", 1) + if err != nil { + t.Fatalf(err.Error()) + } + + // Rename the .git directory to .GIT + oldPath := filepath.Join(dst, ".git") + newPath := filepath.Join(dst, ".GIT") + err = os.Rename(oldPath, newPath) + if err != nil { + t.Fatalf(err.Error()) + } + + // Remove all variations of .git directories + err = removeCaseInsensitiveGitDirectory(dst) + if err != nil { + t.Fatalf(err.Error()) + } + } + if err != nil { + t.Fatalf(err.Error()) + } + + // Check if the .GIT directory exists + if _, err := os.Stat(filepath.Join(dst, ".GIT")); !os.IsNotExist(err) { + t.Fatalf(".GIT directory still exists") + } + + // Check if the .git directory exists + if _, err := os.Stat(filepath.Join(dst, ".git")); !os.IsNotExist(err) { + t.Fatalf(".git directory still exists") + } +} + // gitRepo is a helper struct which controls a single temp git repo. type gitRepo struct { t *testing.T