aboutsummaryrefslogtreecommitdiffsponsor
path: root/internal/service/git.go
diff options
context:
space:
mode:
authorSlack Coder <slackcoder@server.ky>2024-04-08 15:29:11 -0500
committerSlack Coder <slackcoder@server.ky>2024-07-18 11:47:49 -0500
commitae748859be8d6d3ed3c0929770f0c287ab6d6460 (patch)
tree284d3bf3d7fa496a9d334391eac996affc5a01b9 /internal/service/git.go
parentc2267767ca8ed06018d26a45b483c44b7c4234cf (diff)
downloadmirror-ae748859be8d6d3ed3c0929770f0c287ab6d6460.tar.xz
Port to Golang
Diffstat (limited to 'internal/service/git.go')
-rw-r--r--internal/service/git.go134
1 files changed, 134 insertions, 0 deletions
diff --git a/internal/service/git.go b/internal/service/git.go
new file mode 100644
index 0000000..ad3a653
--- /dev/null
+++ b/internal/service/git.go
@@ -0,0 +1,134 @@
+package service
+
+import (
+ "errors"
+ "fmt"
+ "net/url"
+ "os"
+ "os/exec"
+ "path"
+ "strings"
+)
+
+const gitDescriptionFile = "description"
+
+func mapExecError(err error) error {
+ if ee, ok := err.(*exec.ExitError); ok {
+ return errors.New(string(ee.Stderr))
+ }
+ return err
+}
+
+// Set the description for the projects repository.
+func setDescription(repo string, desc string) error {
+ descPath := path.Join(repo, gitDescriptionFile)
+
+ var curDesc string
+ buf, err := os.ReadFile(descPath)
+ if os.IsNotExist(err) {
+ // empty
+ } else if err != nil {
+ return err
+ } else {
+ curDesc = string(buf)
+ }
+
+ if curDesc != desc {
+ err = os.WriteFile(descPath, []byte(desc), 0750)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// Set the remote origin's URL for the projects repository.
+func setRemoteOrigin(repo string, origin *url.URL) error {
+ cmd := exec.Command("git", "remote", "get-url", "origin")
+ cmd.Dir = repo
+ buf, err := cmd.Output()
+ if err != nil {
+ return fmt.Errorf("getting current project remote origin: %w", err)
+ }
+ currentOrigin := strings.TrimSpace(string(buf))
+
+ if currentOrigin != origin.String() {
+ cmd = exec.Command("git", "remote", "set-url", "origin", origin.String())
+ cmd.Dir = repo
+ err = cmd.Run()
+ if err != nil {
+ err = mapExecError(err)
+ return fmt.Errorf("setting project remote origin: %w", err)
+ }
+ }
+
+ return nil
+}
+
+func getRemoteHeadReference(repo string) (string, error) {
+ cmd := exec.Command("git", "ls-remote", "--symref", "origin", "HEAD")
+ cmd.Dir = repo
+ buf, err := cmd.Output()
+ if err != nil {
+ return "", mapExecError(err)
+ }
+
+ for _, l := range strings.Split(string(buf), "\n") {
+ fields := strings.Fields(l)
+ if len(fields) != 3 {
+ return "", errors.New("unexpected output from 'git ls-remote'")
+ }
+ if fields[0] == "ref:" {
+ return strings.TrimPrefix(fields[1], "refs/heads/"), nil
+ }
+ }
+
+ return "", errors.New("not found")
+}
+
+func MirrorGit(dst *url.URL, src *url.URL, description string) error {
+ if dst.Scheme != "" && dst.Scheme != "file://" {
+ return fmt.Errorf("'%s' scheme not supported", dst.Scheme)
+ }
+
+ if _, err := os.Stat(dst.Path); os.IsNotExist(err) {
+ err = os.MkdirAll(path.Join(dst.Path, ".."), 0750)
+ if err != nil {
+ return fmt.Errorf("creating new mirror: %w", err)
+ }
+
+ cmd := exec.Command("git", "clone", "--bare", "--single-branch", src.String(), dst.String())
+ cmd.Dir = path.Join(dst.Path, "..")
+ err := cmd.Run()
+ if err != nil {
+ err = mapExecError(err)
+ return fmt.Errorf("cloning: %s", err)
+ }
+ }
+
+ err := setDescription(dst.Path, description)
+ if err != nil {
+ return err
+ }
+
+ err = setRemoteOrigin(dst.Path, src)
+ if err != nil {
+ return err
+ }
+
+ branch, err := getRemoteHeadReference(dst.Path)
+ if err != nil {
+ return fmt.Errorf("guessing remote default branch: %w", err)
+ }
+
+ cmd := exec.Command("git", "fetch", "--tags", "origin", branch+":"+branch)
+ cmd.Dir = dst.Path
+ err = cmd.Run()
+ if err != nil {
+ err = mapExecError(err)
+ return fmt.Errorf("fetching project: %s", err)
+ }
+
+ return nil
+}