mirror

Mirror free and open-source projects you like with minimal effort
git clone git://git.server.ky/slackcoder/mirror
Log | Files | Refs | README

git.go (3538B)


      1 package service
      2 
      3 import (
      4 	"errors"
      5 	"fmt"
      6 	"os"
      7 	"os/exec"
      8 	"path"
      9 	"strings"
     10 
     11 	"git.server.ky/slackcoder/mirror/internal"
     12 )
     13 
     14 const gitDescriptionFile = "description"
     15 
     16 func mapExecError(err error) error {
     17 	if ee, ok := err.(*exec.ExitError); ok {
     18 		return errors.New(string(ee.Stderr))
     19 	}
     20 	return err
     21 }
     22 
     23 // Set the description  for the projects repository.
     24 func setDescription(repo string, desc string) error {
     25 	descPath := path.Join(repo, gitDescriptionFile)
     26 
     27 	var curDesc string
     28 	buf, err := os.ReadFile(descPath)
     29 	if os.IsNotExist(err) {
     30 		// empty
     31 	} else if err != nil {
     32 		return err
     33 	} else {
     34 		curDesc = string(buf)
     35 	}
     36 
     37 	if curDesc != desc {
     38 		err = os.WriteFile(descPath, []byte(desc), 0750)
     39 		if err != nil {
     40 			return err
     41 		}
     42 	}
     43 
     44 	return nil
     45 }
     46 
     47 // Set the remote origin's URL  for the projects repository.
     48 func setRemoteOrigin(repo string, origin *internal.URL) error {
     49 	cmd := exec.Command("git", "remote", "get-url", "origin")
     50 	cmd.Dir = repo
     51 	buf, err := cmd.Output()
     52 	if err != nil {
     53 		return fmt.Errorf("getting current project remote origin: %w", err)
     54 	}
     55 	currentOrigin := strings.TrimSpace(string(buf))
     56 
     57 	if currentOrigin != origin.String() {
     58 		cmd = exec.Command("git", "remote", "set-url", "origin", origin.String())
     59 		cmd.Dir = repo
     60 		err = cmd.Run()
     61 		if err != nil {
     62 			err = mapExecError(err)
     63 			return fmt.Errorf("setting project remote origin: %w", err)
     64 		}
     65 	}
     66 
     67 	return nil
     68 }
     69 
     70 func getRemoteHeadReference(repo string) (string, error) {
     71 	cmd := exec.Command("git", "ls-remote", "--symref", "origin", "HEAD")
     72 	cmd.Dir = repo
     73 	buf, err := cmd.Output()
     74 	if err != nil {
     75 		return "", mapExecError(err)
     76 	}
     77 
     78 	for _, l := range strings.Split(string(buf), "\n") {
     79 		fields := strings.Fields(l)
     80 		if len(fields) != 3 {
     81 			return "", errors.New("unexpected output from 'git ls-remote'")
     82 		}
     83 		if fields[0] == "ref:" {
     84 			return strings.TrimPrefix(fields[1], "refs/heads/"), nil
     85 		}
     86 	}
     87 
     88 	return "", errors.New("not found")
     89 }
     90 
     91 func MirrorGit(dst *internal.URL, src *internal.URL, description string) error {
     92 	if dst.Scheme != "" && dst.Scheme != "file://" {
     93 		return fmt.Errorf("'%s' scheme not supported", dst.Scheme)
     94 	}
     95 
     96 	entries, err := os.ReadDir(dst.Path)
     97 	if err != nil && !os.IsNotExist(err) {
     98 		return fmt.Errorf("creating directory '%s': %w", dst.Path, err)
     99 	} else if os.IsNotExist(err) || len(entries) == 0 {
    100 		err = os.MkdirAll(path.Join(dst.Path, ".."), 0750)
    101 		if err != nil {
    102 			return fmt.Errorf("creating new mirror: %w", err)
    103 		}
    104 
    105 		cmd := exec.Command("git", "clone", "--bare", "--single-branch", src.String(), dst.String())
    106 		cmd.Dir = path.Join(dst.Path, "..")
    107 		err := cmd.Run()
    108 		if err != nil {
    109 			err = mapExecError(err)
    110 			return fmt.Errorf("cloning: %s", err)
    111 		}
    112 
    113 		// Git mirrors are not good places for mirrored assets.
    114 		cmd = exec.Command("git", "config", "--file", "config", "--add", "cgit.snapshots", "none")
    115 		cmd.Dir = dst.Path
    116 		err = cmd.Run()
    117 		if err != nil {
    118 			err = mapExecError(err)
    119 			return fmt.Errorf("setting git configuration cgit.snapshot to 'none': %s", err)
    120 		}
    121 	}
    122 
    123 	err = setRemoteOrigin(dst.Path, src)
    124 	if err != nil {
    125 		return err
    126 	}
    127 
    128 	branch, err := getRemoteHeadReference(dst.Path)
    129 	if err != nil {
    130 		return fmt.Errorf("guessing remote default branch: %w", err)
    131 	}
    132 
    133 	cmd := exec.Command("git", "fetch", "--tags", "origin", branch+":"+branch)
    134 	cmd.Dir = dst.Path
    135 	err = cmd.Run()
    136 	if err != nil {
    137 		err = mapExecError(err)
    138 		return fmt.Errorf("fetching project: %s", err)
    139 	}
    140 
    141 	err = setDescription(dst.Path, description)
    142 	if err != nil {
    143 		return err
    144 	}
    145 
    146 	return nil
    147 }