service/git/service.go (385 lines of code) (raw):
package git
import (
"crypto/sha1"
"fmt"
"io/ioutil"
"os"
"path"
"regexp"
"strings"
"time"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/go-git/go-git/v5/plumbing/transport/ssh"
"github.com/pkg/errors"
cssh "golang.org/x/crypto/ssh"
)
const (
tempDir = "/tmp"
)
type Service struct {
path string
key string
_keyFilePath string
user string
commandCreator func(name string, arg ...string) Command
TempDir string
}
type User struct {
Name string
Email string
}
func Make(path, user, key string) *Service {
return &Service{
path: path,
TempDir: tempDir,
user: user,
key: key,
}
}
func (s *Service) GenerateChangeID() (string, error) {
h := sha1.New()
if _, err := h.Write([]byte(time.Now().Format(time.RFC3339))); err != nil {
return "", fmt.Errorf("unable to write hash, %w", err)
}
return fmt.Sprintf("I%x", h.Sum(nil)), nil
}
func (s *Service) Clean() error {
if s._keyFilePath != "" {
if err := os.RemoveAll(s._keyFilePath); err != nil {
return fmt.Errorf("unable to clear key file, %w", err)
}
}
if err := os.RemoveAll(s.path); err != nil {
return fmt.Errorf("unable to clear repo path, %w", err)
}
return nil
}
func (s *Service) Clone(url string) error {
keyPath, err := s.keyFilePath()
if err != nil {
return fmt.Errorf("unable to create key file, %w", err)
}
cloneCMD := s.commandCreate("git", "clone", "--mirror", url, s.path)
cloneCMD.SetEnv(s.authEnv(keyPath))
if bts, err := cloneCMD.CombinedOutput(); err != nil {
return fmt.Errorf("unable to clone repo: %s, %w", string(bts), err)
}
if err := s.bareToNormal(s.path); err != nil {
return fmt.Errorf("unable to covert bare repo to normal, %w", err)
}
fetchCMD := s.commandCreate("git", "--git-dir", path.Join(s.path, ".git"), "pull", "origin", "master",
"--unshallow", "--no-rebase")
fetchCMD.SetEnv(s.authEnv(keyPath))
bts, err := fetchCMD.CombinedOutput()
if err != nil && !strings.Contains(string(bts), "does not make sense") {
return fmt.Errorf("unable to pull unshallow repo: %s, %w", string(bts), err)
}
return nil
}
func (s *Service) SetAuthor(user *User) error {
cmd := s.commandCreate("git", "config", "user.email", user.Email)
cmd.SetDir(s.path)
bts, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("unable to commit: %s, %w", string(bts), err)
}
cmd = s.commandCreate("git", "config", "user.name", user.Name)
cmd.SetDir(s.path)
bts, err = cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("unable to commit: %s, %w", string(bts), err)
}
return nil
}
func (s *Service) RawCommit(u *User, message string, params ...string) error {
if err := s.SetAuthor(u); err != nil {
return fmt.Errorf("unable to set author, %w", err)
}
baseParams := []string{"commit", "-m", message}
baseParams = append(baseParams, params...)
cmd := s.commandCreate("git", baseParams...)
cmd.SetDir(s.path)
msg, err := cmd.StrCombinedOutput()
if err != nil {
return fmt.Errorf("unable to commit: %s, %w", msg, err)
}
return nil
}
func (s *Service) Commit(message string, files []string, user *User) error {
_, w, err := s.worktree()
if err != nil {
return fmt.Errorf("unable to get worktree, %w", err)
}
for _, f := range files {
if _, err := w.Add(f); err != nil {
return fmt.Errorf("unable to add file: %s, %w", f, err)
}
}
if _, err := w.Commit(message, &git.CommitOptions{
Author: &object.Signature{Name: user.Name, Email: user.Email, When: time.Now()},
}); err != nil {
return fmt.Errorf("unable to perform git commit, %w", err)
}
return nil
}
func (s *Service) authEnv(keyPath string) []string {
return []string{fmt.Sprintf(`GIT_SSH_COMMAND=ssh -i %s -l %s -o StrictHostKeyChecking=no`, keyPath,
s.user), "GIT_SSH_VARIANT=ssh"}
}
func (s *Service) bareToNormal(path string) error {
if err := os.MkdirAll(fmt.Sprintf("%s/.git", path), 0777); err != nil {
return fmt.Errorf("unable to create .git folder, %w", err)
}
files, err := ioutil.ReadDir(path)
if err != nil {
return fmt.Errorf("unable to list dir, %w", err)
}
for _, f := range files {
if f.Name() == ".git" {
continue
}
if err := os.Rename(fmt.Sprintf("%s/%s", path, f.Name()),
fmt.Sprintf("%s/.git/%s", path, f.Name())); err != nil {
return fmt.Errorf("unable to rename file, %w", err)
}
}
gitDir := fmt.Sprintf("%s/.git", path)
cmd := s.commandCreate("git", "--git-dir", gitDir, "config", "--local",
"--bool", "core.bare", "false")
if bts, err := cmd.CombinedOutput(); err != nil {
return errors.Wrapf(err, string(bts))
}
cmd = s.commandCreate("git", "--git-dir", gitDir, "config", "--local",
"--bool", "remote.origin.mirror", "false")
if bts, err := cmd.CombinedOutput(); err != nil {
return errors.Wrapf(err, string(bts))
}
cmd = s.commandCreate("git", "--git-dir", gitDir, "reset", "--hard")
cmd.SetDir(path)
if bts, err := cmd.CombinedOutput(); err != nil {
return errors.Wrapf(err, string(bts))
}
return nil
}
func (s *Service) keyFilePath() (string, error) {
if s._keyFilePath != "" {
return s._keyFilePath, nil
}
keyFile, err := os.Create(fmt.Sprintf("%s/sshkey_%d", s.TempDir, time.Now().UnixNano()))
if err != nil {
return "", fmt.Errorf("unable to create temp file for ssh key, %w", err)
}
keyFileInfo, _ := keyFile.Stat()
keyFilePath := fmt.Sprintf("%s/%s", s.TempDir, keyFileInfo.Name())
if _, err = keyFile.WriteString(s.key); err != nil {
return "", fmt.Errorf("unable to write ssh key, %w", err)
}
if err = keyFile.Close(); err != nil {
return "", fmt.Errorf("unable to close file, %w", err)
}
if err := os.Chmod(keyFilePath, 0400); err != nil {
return "", fmt.Errorf("unable to chmod ssh key file, %w", err)
}
s._keyFilePath = keyFilePath
return keyFilePath, nil
}
func IsErrReferenceNotFound(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "reference not found")
}
func (s *Service) RawPull(params ...string) error {
keyPath, err := s.keyFilePath()
if err != nil {
return fmt.Errorf("unable to init auth, %w", err)
}
baseParams := []string{"pull"}
baseParams = append(baseParams, params...)
cmd := s.commandCreate("git", baseParams...)
cmd.SetEnv(s.authEnv(keyPath))
cmd.SetDir(s.path)
msg, err := cmd.StrCombinedOutput()
if err != nil {
return fmt.Errorf("unable to pull: %s, %w", msg, err)
}
return nil
}
func IsErrNonFastForwardUpdate(err error) bool {
if err == nil {
return false
}
return strings.Contains(err.Error(), "non-fast-forward update")
}
func (s *Service) Pull(remoteName string) (*object.Commit, error) {
r, w, err := s.worktree()
if err != nil {
return nil, fmt.Errorf("unable to get worktree, %w", err)
}
keyPath, err := s.keyFilePath()
if err != nil {
return nil, fmt.Errorf("unable to create key file, %w", err)
}
publicKeys, err := ssh.NewPublicKeysFromFile(s.user, keyPath, "")
if err != nil {
return nil, fmt.Errorf("unable to create public keys, %w", err)
}
publicKeys.HostKeyCallback = cssh.InsecureIgnoreHostKey()
if err := w.Pull(&git.PullOptions{RemoteName: remoteName, Auth: publicKeys}); err != nil {
return nil, fmt.Errorf("unable to pull, %w", err)
}
ref, err := r.Head()
if err != nil {
return nil, fmt.Errorf("unable to get ref, %w", err)
}
commit, err := r.CommitObject(ref.Hash())
if err != nil {
return nil, fmt.Errorf("unable to get last commit, %w", err)
}
return commit, nil
}
func (s *Service) Push(remoteName string, pushParams ...string) error {
keyPath, err := s.keyFilePath()
if err != nil {
return fmt.Errorf("unable to init auth, %w", err)
}
basePushParams := []string{"--git-dir", path.Join(s.path, ".git"),
"push", remoteName}
basePushParams = append(basePushParams, pushParams...)
pushCMD := s.commandCreate("git", basePushParams...)
pushCMD.SetEnv(s.authEnv(keyPath))
pushCMD.SetDir(s.path)
if bts, err := pushCMD.CombinedOutput(); err != nil {
return fmt.Errorf("unable to push changes, err: %s, %w", string(bts), err)
}
return nil
}
func (s *Service) SetFileContents(filePath, contents string) error {
filePath = path.Join(s.path, filePath)
dir := path.Dir(filePath)
if _, err := os.Stat(dir); err != nil {
if err := os.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("unable to create dir, %w", err)
}
}
fp, err := os.Create(filePath)
if err != nil {
return fmt.Errorf("unable to create file: %s, %w", filePath, err)
}
if _, err := fp.WriteString(contents); err != nil {
return fmt.Errorf("unable to put file contents, file: %s, %w", filePath, err)
}
if err := fp.Close(); err != nil {
return fmt.Errorf("unable to close file: %s, %w", filePath, err)
}
return nil
}
func (s *Service) GetFileContents(filePath string) (string, error) {
filePath = path.Join(s.path, filePath)
fp, err := os.Open(filePath)
if err != nil {
return "", fmt.Errorf("unable to open file: %s, %w", filePath, err)
}
bts, err := ioutil.ReadAll(fp)
if err != nil {
return "", fmt.Errorf("unable to read file: %s, %w", filePath, err)
}
return string(bts), nil
}
func (s *Service) AddRemote(remoteName, url string) error {
cmd := s.commandCreate("git", "remote", "add", remoteName, url)
cmd.SetDir(s.path)
if bts, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("unable to add remote, err: %s, %w", string(bts), err)
}
return nil
}
func (s *Service) RawCheckout(branch string, create bool) error {
baseParams := []string{"checkout", branch}
if create {
baseParams = []string{"checkout", "-b", branch}
}
cmd := s.commandCreate("git", baseParams...)
cmd.SetDir(s.path)
if bts, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("unable to checkout, err: %s, %w", string(bts), err)
}
return nil
}
func (s *Service) Checkout(branch string, create bool) error {
_, w, err := s.worktree()
if err != nil {
return fmt.Errorf("unable to get worktree, %w", err)
}
if err := w.Checkout(&git.CheckoutOptions{
Branch: plumbing.NewBranchReferenceName(branch), Create: create}); err != nil {
return fmt.Errorf("unable to checkout branch, %w", err)
}
return nil
}
func (s *Service) DeleteBranch(branchName string) error {
cmd := s.commandCreate("git", "branch", "-D", branchName)
cmd.SetDir(s.path)
msg, err := cmd.StrCombinedOutput()
if err != nil {
return fmt.Errorf("unable to delete branch, msg: %s, %w", msg, err)
}
return nil
}
func (s *Service) Add(file string) error {
cmd := s.commandCreate("git", "add", file)
cmd.SetDir(s.path)
msg, err := cmd.StrCombinedOutput()
if err != nil {
return fmt.Errorf("unable to run add, msg: %s, %w", msg, err)
}
return nil
}
func (s *Service) Rebase(targetBranch string, params ...string) (string, error) {
gitArgs := []string{"rebase", targetBranch}
gitArgs = append(gitArgs, params...)
cmd := s.commandCreate("git", gitArgs...)
cmd.SetDir(s.path)
msg, err := cmd.StrCombinedOutput()
if err != nil {
return msg, fmt.Errorf("unable to run rebase, %w", err)
}
return msg, nil
}
func (s *Service) worktree() (*git.Repository, *git.Worktree, error) {
r, err := git.PlainOpen(s.path)
if err != nil {
return nil, nil, fmt.Errorf("unable to open repo, %w", err)
}
w, err := r.Worktree()
if err != nil {
return nil, nil, fmt.Errorf("unable to get worktree, %w", err)
}
return r, w, nil
}
func (s *Service) RemoveBranch(name string) error {
r, err := git.PlainOpen(s.path)
if err != nil {
return fmt.Errorf("unable to open repo, %w", err)
}
headRef, err := r.Head()
if err != nil {
return fmt.Errorf("unable to get head ref, %w", err)
}
err = r.Storer.RemoveReference(
plumbing.NewHashReference(plumbing.NewBranchReferenceName(name), headRef.Hash()).Name())
if err != nil {
return fmt.Errorf("unable to remove branch, %w", err)
}
return nil
}
func ExtractMrURL(pushMessage string) string {
return regexp.MustCompile(
`https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)`).
FindString(pushMessage)
}
func CommitMessageWithChangeID(commitMessage, changeID string) string {
return fmt.Sprintf("%s\n\nChange-Id: %s", commitMessage, changeID)
}