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 }