package service import ( "errors" "fmt" "os" "os/exec" "path" "strings" "git.server.ky/slackcoder/mirror/internal" ) 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 *internal.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 *internal.URL, src *internal.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) } // Git mirrors are not good places for mirrored assets. cmd = exec.Command("git", "config", "set", "cgit.snapshots", "none") cmd.Dir = dst.Path err = cmd.Run() if err != nil { err = mapExecError(err) return fmt.Errorf("disabling cgit snapshots: %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 }