Monorepo for Tangled
0

Configure Feed

Select the types of activity you want to include in your feed.

at master 8.3 kB View raw
1package knotmirror 2 3import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/url" 8 "os" 9 "os/exec" 10 "path/filepath" 11 "regexp" 12 "strings" 13 14 "github.com/go-git/go-git/v5" 15 gitconfig "github.com/go-git/go-git/v5/config" 16 "github.com/go-git/go-git/v5/plumbing/transport" 17 "tangled.org/core/knotmirror/models" 18) 19 20type GitMirrorManager interface { 21 Exist(repo *models.Repo) (bool, error) 22 // Clone clones the repository as a mirror 23 Clone(ctx context.Context, repo *models.Repo) error 24 // Fetch fetches the repository 25 Fetch(ctx context.Context, repo *models.Repo) error 26 // Sync mirrors the repository. It will clone the repository if repository doesn't exist. 27 Sync(ctx context.Context, repo *models.Repo) error 28} 29 30type CliGitMirrorManager struct { 31 repoBasePath string 32 knotUseSSL bool 33} 34 35func NewCliGitMirrorManager(repoBasePath string, knotUseSSL bool) *CliGitMirrorManager { 36 return &CliGitMirrorManager{ 37 repoBasePath, 38 knotUseSSL, 39 } 40} 41 42var _ GitMirrorManager = new(CliGitMirrorManager) 43 44func (c *CliGitMirrorManager) makeRepoPath(repo *models.Repo) string { 45 return filepath.Join(c.repoBasePath, repo.RepoDid.String()) 46} 47 48func (c *CliGitMirrorManager) Exist(repo *models.Repo) (bool, error) { 49 return isDir(c.makeRepoPath(repo)) 50} 51 52func (c *CliGitMirrorManager) Clone(ctx context.Context, repo *models.Repo) error { 53 path := c.makeRepoPath(repo) 54 url, err := makeRepoRemoteUrl(repo.KnotDomain, repo.RepoIdentifier(), c.knotUseSSL) 55 if err != nil { 56 return fmt.Errorf("constructing repo remote url: %w", err) 57 } 58 return c.clone(ctx, path, url) 59} 60 61func (c *CliGitMirrorManager) clone(ctx context.Context, path, url string) error { 62 cmd := exec.CommandContext(ctx, "git", "clone", "--mirror", url, path) 63 if out, err := cmd.CombinedOutput(); err != nil { 64 if ctx.Err() != nil { 65 return ctx.Err() 66 } 67 msg := string(out) 68 if classification := classifyCliError(msg); classification != nil { 69 return classification 70 } 71 return fmt.Errorf("running 'git clone --mirror %s': %w\n%s", url, err, msg) 72 } 73 return nil 74} 75 76func (c *CliGitMirrorManager) Fetch(ctx context.Context, repo *models.Repo) error { 77 path := c.makeRepoPath(repo) 78 url, err := makeRepoRemoteUrl(repo.KnotDomain, repo.RepoIdentifier(), c.knotUseSSL) 79 if err != nil { 80 return fmt.Errorf("constructing repo remote url: %w", err) 81 } 82 return c.fetch(ctx, path, url) 83} 84 85func (c *CliGitMirrorManager) fetch(ctx context.Context, path, url string) error { 86 cmd := exec.CommandContext(ctx, "git", "-C", path, "fetch", "--prune", url, "+refs/*:refs/*") 87 if out, err := cmd.CombinedOutput(); err != nil { 88 if ctx.Err() != nil { 89 return ctx.Err() 90 } 91 return fmt.Errorf("running 'git fetch': %w\n%s", err, string(out)) 92 } 93 return nil 94} 95 96func (c *CliGitMirrorManager) Sync(ctx context.Context, repo *models.Repo) error { 97 path := c.makeRepoPath(repo) 98 url, err := makeRepoRemoteUrl(repo.KnotDomain, repo.RepoIdentifier(), c.knotUseSSL) 99 if err != nil { 100 return fmt.Errorf("constructing repo remote url: %w", err) 101 } 102 103 exist, err := isDir(path) 104 if err != nil { 105 return fmt.Errorf("checking repo path: %w", err) 106 } 107 if !exist { 108 if err := c.clone(ctx, path, url); err != nil { 109 return fmt.Errorf("cloning repo: %w", err) 110 } 111 } else { 112 if err := c.fetch(ctx, path, url); err != nil { 113 return fmt.Errorf("fetching repo: %w", err) 114 } 115 } 116 return nil 117} 118 119var ( 120 ErrDNSFailure = errors.New("git: knot: dns failure (could not resolve host)") 121 ErrCertExpired = errors.New("git: knot: certificate has expired") 122 ErrCertMismatch = errors.New("git: knot: certificate hostname mismatch") 123 ErrTLSHandshake = errors.New("git: knot: tls handshake failure") 124 ErrHTTPStatus = errors.New("git: knot: request url returned error") 125 ErrUnreachable = errors.New("git: knot: could not connect to server") 126 ErrRepoNotFound = errors.New("git: repo: repository not found") 127) 128 129var ( 130 reDNSFailure = regexp.MustCompile(`Could not resolve host:`) 131 reCertExpired = regexp.MustCompile(`SSL certificate OpenSSL verify result: certificate has expired`) 132 reCertMismatch = regexp.MustCompile(`SSL: no alternative certificate subject name matches target hostname`) 133 reTLSHandshake = regexp.MustCompile(`TLS connect error: (.*)`) 134 reHTTPStatus = regexp.MustCompile(`The requested URL returned error: (\d\d\d)`) 135 reUnreachable = regexp.MustCompile(`Could not connect to server`) 136 reRepoNotFound = regexp.MustCompile(`repository '.*?' not found`) 137) 138 139// classifyCliError classifies git cli error message. It will return nil for unknown error messages 140func classifyCliError(stderr string) error { 141 msg := strings.TrimSpace(stderr) 142 if m := reTLSHandshake.FindStringSubmatch(msg); len(m) > 1 { 143 return fmt.Errorf("%w: %s", ErrTLSHandshake, m[1]) 144 } 145 if m := reHTTPStatus.FindStringSubmatch(msg); len(m) > 1 { 146 return fmt.Errorf("%w: %s", ErrHTTPStatus, m[1]) 147 } 148 switch { 149 case reDNSFailure.MatchString(msg): 150 return ErrDNSFailure 151 case reCertExpired.MatchString(msg): 152 return ErrCertExpired 153 case reCertMismatch.MatchString(msg): 154 return ErrCertMismatch 155 case reUnreachable.MatchString(msg): 156 return ErrUnreachable 157 case reRepoNotFound.MatchString(msg): 158 return ErrRepoNotFound 159 } 160 return nil 161} 162 163type GoGitMirrorManager struct { 164 repoBasePath string 165 knotUseSSL bool 166} 167 168func NewGoGitMirrorClient(repoBasePath string, knotUseSSL bool) *GoGitMirrorManager { 169 return &GoGitMirrorManager{ 170 repoBasePath, 171 knotUseSSL, 172 } 173} 174 175var _ GitMirrorManager = new(GoGitMirrorManager) 176 177func (c *GoGitMirrorManager) makeRepoPath(repo *models.Repo) string { 178 return filepath.Join(c.repoBasePath, repo.RepoDid.String()) 179} 180 181func (c *GoGitMirrorManager) Exist(repo *models.Repo) (bool, error) { 182 return isDir(c.makeRepoPath(repo)) 183} 184 185func (c *GoGitMirrorManager) Clone(ctx context.Context, repo *models.Repo) error { 186 path := c.makeRepoPath(repo) 187 url, err := makeRepoRemoteUrl(repo.KnotDomain, repo.RepoIdentifier(), c.knotUseSSL) 188 if err != nil { 189 return fmt.Errorf("constructing repo remote url: %w", err) 190 } 191 return c.clone(ctx, path, url) 192} 193 194func (c *GoGitMirrorManager) clone(ctx context.Context, path, url string) error { 195 _, err := git.PlainCloneContext(ctx, path, true, &git.CloneOptions{ 196 URL: url, 197 Mirror: true, 198 }) 199 if err != nil && !errors.Is(err, transport.ErrEmptyRemoteRepository) { 200 return fmt.Errorf("cloning repo: %w", err) 201 } 202 return nil 203} 204 205func (c *GoGitMirrorManager) Fetch(ctx context.Context, repo *models.Repo) error { 206 path := c.makeRepoPath(repo) 207 url, err := makeRepoRemoteUrl(repo.KnotDomain, repo.RepoIdentifier(), c.knotUseSSL) 208 if err != nil { 209 return fmt.Errorf("constructing repo remote url: %w", err) 210 } 211 212 return c.fetch(ctx, path, url) 213} 214 215func (c *GoGitMirrorManager) fetch(ctx context.Context, path, url string) error { 216 gr, err := git.PlainOpen(path) 217 if err != nil { 218 return fmt.Errorf("opening local repo: %w", err) 219 } 220 if err := gr.FetchContext(ctx, &git.FetchOptions{ 221 RemoteURL: url, 222 RefSpecs: []gitconfig.RefSpec{gitconfig.RefSpec("+refs/*:refs/*")}, 223 Force: true, 224 Prune: true, 225 }); err != nil { 226 return fmt.Errorf("fetching reppo: %w", err) 227 } 228 return nil 229} 230 231func (c *GoGitMirrorManager) Sync(ctx context.Context, repo *models.Repo) error { 232 path := c.makeRepoPath(repo) 233 url, err := makeRepoRemoteUrl(repo.KnotDomain, repo.RepoIdentifier(), c.knotUseSSL) 234 if err != nil { 235 return fmt.Errorf("constructing repo remote url: %w", err) 236 } 237 238 exist, err := isDir(path) 239 if err != nil { 240 return fmt.Errorf("checking repo path: %w", err) 241 } 242 if !exist { 243 if err := c.clone(ctx, path, url); err != nil { 244 return fmt.Errorf("cloning repo: %w", err) 245 } 246 } else { 247 if err := c.fetch(ctx, path, url); err != nil { 248 return fmt.Errorf("fetching repo: %w", err) 249 } 250 } 251 return nil 252} 253 254func makeRepoRemoteUrl(knot, repoIdentifier string, knotUseSSL bool) (string, error) { 255 if !strings.Contains(knot, "://") { 256 if knotUseSSL { 257 knot = "https://" + knot 258 } else { 259 knot = "http://" + knot 260 } 261 } 262 263 u, err := url.Parse(knot) 264 if err != nil { 265 return "", err 266 } 267 268 if u.Scheme != "http" && u.Scheme != "https" { 269 return "", fmt.Errorf("unsupported scheme: %s", u.Scheme) 270 } 271 272 u = u.JoinPath(repoIdentifier) 273 return u.String(), nil 274} 275 276func isDir(path string) (bool, error) { 277 info, err := os.Stat(path) 278 if err == nil && info.IsDir() { 279 return true, nil 280 } 281 if os.IsNotExist(err) { 282 return false, nil 283 } 284 return false, err 285}