github.com/bitrise-io/bitrise-step-update-gitops-repository@v0.0.0-20240426081835-1466be593380/pkg/gitops/local_repository.go (about) 1 package gitops 2 3 import ( 4 "context" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "os" 9 "os/exec" 10 "strings" 11 "time" 12 ) 13 14 //go:generate moq -out local_repository_moq_test.go . localRepository 15 type localRepository interface { 16 Close(ctx context.Context) 17 localPath() string 18 gitClone() error 19 workingDirectoryClean() (bool, error) 20 gitCheckoutNewBranch() error 21 gitCommitAndPush(message string) error 22 openPullRequest(ctx context.Context, title, body string) (string, error) 23 } 24 25 // gitRepo implements the localRepository interface. 26 var _ localRepository = (*gitRepo)(nil) 27 28 type gitRepo struct { 29 pr pullRequestOpener 30 githubRepo *githubRepo 31 branch string 32 33 tmpRepoPath string 34 } 35 36 // NewGitRepoParams are parameters for NewGitRepo function. 37 type NewGitRepoParams struct { 38 PullRequestOpener pullRequestOpener 39 GithubRepo *githubRepo 40 Branch string 41 } 42 43 // NewGitRepo returns a new local clone of a remote repository. 44 // It should be closed after usage. 45 func NewGitRepo(ctx context.Context, p NewGitRepoParams) (*gitRepo, error) { 46 // Temporary directory for local clone of repository. 47 tmpRepoPath, err := ioutil.TempDir("", "") 48 if err != nil { 49 return nil, fmt.Errorf("create temp dir for repo: %w", err) 50 } 51 repo := &gitRepo{ 52 pr: p.PullRequestOpener, 53 githubRepo: p.GithubRepo, 54 branch: p.Branch, 55 tmpRepoPath: tmpRepoPath, 56 } 57 if err := repo.gitClone(); err != nil { 58 return nil, fmt.Errorf("git clone repo: %w", err) 59 } 60 return repo, nil 61 } 62 63 // Close closes all related resoruces of the repository. 64 // This is a best-effort operation, possible errors are logged as warning, 65 // not returned as an actual error. 66 func (r gitRepo) Close(ctx context.Context) { 67 // Delete temporary repository from the local filesystem. 68 if err := os.RemoveAll(r.tmpRepoPath); err != nil { 69 log.Printf("warning: remove temporary repository: %s\n", err) 70 } 71 } 72 73 func (r gitRepo) localPath() string { 74 return r.tmpRepoPath 75 } 76 77 func (r gitRepo) gitClone() error { 78 _, err := r.git("clone", 79 "--branch", r.branch, "--single-branch", 80 string(r.githubRepo.url), ".") 81 return err 82 } 83 84 func (r gitRepo) workingDirectoryClean() (bool, error) { 85 status, err := r.git("status") 86 if err != nil { 87 return false, err 88 } 89 return strings.Contains(status, "nothing to commit"), nil 90 } 91 92 func (r gitRepo) gitCheckoutNewBranch() error { 93 // Generate branch name based on the current time. 94 t := time.Now() 95 branch := fmt.Sprintf("ci-%d-%02d-%02dT%02d-%02d-%02d", 96 t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second()) 97 // Execute git checkout to a new branch with that name. 98 if _, err := r.git("checkout", "-b", branch); err != nil { 99 return fmt.Errorf("checkout new branch %q: %w", branch, err) 100 } 101 return nil 102 } 103 104 func (r gitRepo) gitCommitAndPush(message string) error { 105 // Stage all changes, commit them to the current branch 106 // and push the commit to the remote repository. 107 gitArgs := [][]string{ 108 {"add", "--all"}, 109 {"commit", "-m", message}, 110 {"push", "--all", "-u"}, 111 } 112 for _, a := range gitArgs { 113 if _, err := r.git(a...); err != nil { 114 return err 115 } 116 } 117 return nil 118 } 119 120 func (r gitRepo) currentBranch() (string, error) { 121 branch, err := r.git("rev-parse", "--abbrev-ref", "HEAD") 122 if err != nil { 123 return "", err 124 } 125 return strings.TrimSpace(branch), nil 126 } 127 128 func (r gitRepo) git(args ...string) (string, error) { 129 // Change current directory to the repositorys local clone. 130 originalDir, err := os.Getwd() 131 if err != nil { 132 return "", fmt.Errorf("get current dir: %w", err) 133 } 134 if err := os.Chdir(r.tmpRepoPath); err != nil { 135 return "", fmt.Errorf("change dir to %q: %w", r.tmpRepoPath, err) 136 } 137 138 cmd := exec.Command("git", args...) 139 // Run git command and returns its combined output of stdout and stderr. 140 out, err := cmd.CombinedOutput() 141 if err != nil { 142 if errChdir := os.Chdir(originalDir); errChdir != nil { 143 err = fmt.Errorf("%w (revert to original dir: %s)", err, errChdir) 144 } 145 return "", fmt.Errorf("run command %v: %w (output: %s)", args, err, out) 146 } 147 if err := os.Chdir(originalDir); err != nil { 148 return "", fmt.Errorf("revert to original dir: %w", err) 149 } 150 return string(out), nil 151 } 152 153 func (r gitRepo) openPullRequest(ctx context.Context, title, body string) (string, error) { 154 // PR will be open from the current branch. 155 currBranch, err := r.currentBranch() 156 if err != nil { 157 return "", fmt.Errorf("current branch: %w", err) 158 } 159 // Open pull request from current branch to the base branch. 160 url, err := r.pr.OpenPullRequest(ctx, openPullRequestParams{ 161 title: title, 162 body: body, 163 head: currBranch, 164 base: r.branch, 165 }) 166 if err != nil { 167 return "", fmt.Errorf("call github: %w", err) 168 } 169 return url, nil 170 }