Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
Go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279package gitutil
import ( "bytes" "cmp" "context" "crypto/sha256" "encoding/hex" "fmt" "io" "mime" "net/http" "net/url" "slices" "strings" "unicode"
"github.com/go-git/go-git/v5/plumbing" "github.com/samber/lo")
type ArchiveFormat string
const ( ArchiveTarGz ArchiveFormat = "tar.gz" ArchiveZip ArchiveFormat = "zip")
var ArchiveFormats = []ArchiveFormat{ArchiveTarGz, ArchiveZip}
func ParseArchiveFormat(raw string) (ArchiveFormat, error) { if format := ArchiveFormat(raw); slices.Contains(ArchiveFormats, format) { return format, nil } return "", fmt.Errorf("only tar.gz and zip formats are supported, got %q", raw)}
func (f ArchiveFormat) String() string { return string(f) }
func (f ArchiveFormat) contentType() string { return lo.Ternary(f == ArchiveZip, "application/zip", "application/gzip")}
var pathSeparators = strings.NewReplacer("/", "-", `\`, "-")
type RepoName string
type Rev string
const RevHead Rev = "HEAD"
func ParseRev(raw string) (Rev, error) { switch { case raw == "": return "", fmt.Errorf("ref is empty") case strings.ContainsFunc(raw, func(c rune) bool { return unicode.IsSpace(c) || unicode.IsControl(c) }): return "", fmt.Errorf("ref contains whitespace or a control character: %q", raw) case strings.HasPrefix(raw, "-"): return "", fmt.Errorf("ref starts with a dash: %q", raw) } return Rev(raw), nil}
func RevFromHash(h plumbing.Hash) Rev { return Rev(h.String()) }
const sha1HexLen, sha256HexLen, hexDigits = 40, 64, "0123456789abcdef"
func (r Rev) IsObjectID() bool { return slices.Contains([]int{sha1HexLen, sha256HexLen}, len(r)) && strings.TrimLeft(string(r), hexDigits) == ""}
func (r Rev) String() string { return string(r) }
func (r Rev) Slug() string { return pathSeparators.Replace(plumbing.ReferenceName(r).Short()) }
func (r Rev) Or(fallback Rev) Rev { return lo.Ternary(r == "", fallback, r) }
type ArchivePrefix string
const MaxArchivePrefixLen = 255
func ParseArchivePrefix(raw string) (ArchivePrefix, error) { switch { case strings.ContainsFunc(raw, unicode.IsControl): return "", fmt.Errorf("prefix contains a control character: %q", raw) case strings.Contains(raw, `\`): return "", fmt.Errorf("prefix contains a backslash: %q", raw) } components := lo.Filter(strings.Split(raw, "/"), func(component string, _ int) bool { return component != "" && component != "." }) if slices.Contains(components, "..") { return "", fmt.Errorf("prefix escapes the archive root: %q", raw) } prefix := ArchivePrefix(strings.Join(components, "/")) if len(prefix) > MaxArchivePrefixLen { return "", fmt.Errorf("prefix is %d bytes, over the %d byte limit", len(prefix), MaxArchivePrefixLen) } return prefix, nil}
func (p ArchivePrefix) String() string { return string(p) }
func archiveStem(repo RepoName, rev Rev) ArchivePrefix { stem := strings.ToValidUTF8(pathSeparators.Replace(string(repo))+"-"+rev.Slug(), "�") return ArchivePrefix(strings.ToValidUTF8(stem[:min(len(stem), MaxArchivePrefixLen)], ""))}
type ArchiveParams struct { Rev Rev Format ArchiveFormat Prefix ArchivePrefix}
func ParseArchiveParams(q url.Values) (ArchiveParams, error) { p := ArchiveParams{Format: ArchiveTarGz} var err error if raw := q.Get("ref"); raw != "" { if p.Rev, err = ParseRev(raw); err != nil { return ArchiveParams{}, err } } if raw := q.Get("format"); raw != "" { if p.Format, err = ParseArchiveFormat(raw); err != nil { return ArchiveParams{}, err } } if p.Prefix, err = ParseArchivePrefix(q.Get("prefix")); err != nil { return ArchiveParams{}, err } return p, nil}
func (p ArchiveParams) WithRev(rev Rev) ArchiveParams { p.Rev = rev return p}
type ServedArchive struct { rev Rev format ArchiveFormat prefix ArchivePrefix}
func (p ArchiveParams) Serve(repo RepoName) ServedArchive { return ServedArchive{p.Rev, p.Format, cmp.Or(p.Prefix, archiveStem(repo, p.Rev))}}
func (a ServedArchive) WithRev(rev Rev) ServedArchive { a.rev = rev return a}
func (a ServedArchive) Query(repo string) url.Values { return url.Values{ "repo": {repo}, "ref": {a.rev.String()}, "format": {a.format.String()}, "prefix": {a.prefix.String()}, }}
func (a ServedArchive) SuffixURL() string { return fmt.Sprintf("%s.%s?%s", url.PathEscape(a.rev.String()), a.format, url.Values{"prefix": {a.prefix.String()}}.Encode())}
func (a ServedArchive) SetHeaders(h http.Header) { h.Set("Content-Type", a.format.contentType()) h.Set("Content-Disposition", mime.FormatMediaType("attachment", map[string]string{ "filename": pathSeparators.Replace(a.prefix.String()) + "." + a.format.String(), })) h.Set("X-Content-Type-Options", "nosniff")}
const archiveETagDomain = "gitutil.archive.v1"
type RepoIdentity string
func (a ServedArchive) ETag(repo RepoIdentity) string { digest := sha256.Sum256([]byte(strings.Join( []string{archiveETagDomain, string(repo), a.rev.String(), a.format.String(), a.prefix.String()}, "\x00", ))) return `"` + hex.EncodeToString(digest[:]) + `"`}
func (a ServedArchive) ServeNotModified(w http.ResponseWriter, r *http.Request, repo RepoIdentity) bool { etag := a.ETag(repo) w.Header().Set("Etag", etag) w.Header().Set("Cache-Control", "no-cache") if !ETagMatches(r.Header, etag) { return false } w.WriteHeader(http.StatusNotModified) return true}
func ETagMatches(h http.Header, etag string) bool { offered := strings.Split(strings.Join(h.Values("If-None-Match"), ","), ",") return lo.SomeBy(offered, func(candidate string) bool { trimmed := strings.TrimSpace(candidate) return trimmed == "*" || strings.TrimPrefix(trimmed, "W/") == etag })}
func ForwardHeaders(dst, src http.Header, keys ...string) { lo.ForEach(keys, func(key string, _ int) { if values := src.Values(key); len(values) > 0 { dst[http.CanonicalHeaderKey(key)] = slices.Clone(values) } })}
func ImmutableLink(target string) string { return `<` + target + `>; rel="immutable"` }
func ParseImmutableLink(header string) (Rev, error) { target := strings.TrimSuffix(strings.TrimPrefix(header, "<"), `>; rel="immutable"`) parsed, err := url.Parse(target) if err != nil { return "", err } return ParseRev(parsed.Query().Get("ref"))}
type ResponseBody struct { inner http.ResponseWriter started bool}
func NewResponseBody(w http.ResponseWriter) *ResponseBody { return &ResponseBody{inner: w} }
func (b *ResponseBody) Write(p []byte) (int, error) { n, err := b.inner.Write(p) b.started = b.started || n > 0 return n, err}
func (b *ResponseBody) Fail() { if b.started { panic(http.ErrAbortHandler) } header := b.inner.Header() lo.ForEach([]string{"Content-Length", "Content-Type", "Content-Disposition", "Etag", "Link"}, func(key string, _ int) { header.Del(key) }) header.Set("Cache-Control", "no-store") b.inner.WriteHeader(http.StatusInternalServerError)}
func WriteArchive(ctx context.Context, w io.Writer, repoPath string, archive ServedArchive) error { ctx, cancel := context.WithCancel(ctx) defer cancel()
cmd := Command(ctx, "archive", "--format="+archive.format.String(), "--prefix="+archive.prefix.String()+"/", "--", archive.rev.String()) cmd.Dir = repoPath stderr := new(bytes.Buffer) cmd.Stderr = stderr
stdout, err := cmd.StdoutPipe() if err != nil { return err } if err := cmd.Start(); err != nil { return err } if _, err := io.Copy(w, stdout); err != nil { cancel() _ = cmd.Wait() return fmt.Errorf("writing the archive body: %w", err) } if err := cmd.Wait(); err != nil { return fmt.Errorf("%w, stderr: %s", err, stderr.String()) } return nil}