summaryrefslogtreecommitdiff
path: root/pkg
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2026-01-30 18:16:31 -0700
committermo khan <mo@mokhan.ca>2026-01-30 18:16:31 -0700
commitfeee7d43ef63ae607c6fd4cca88a356a93553ebe (patch)
tree2969055a894dc4e72d8d79a9ac74cc30d78aff64 /pkg
parente0db8f82e96acadf6968e0cf9c805a7b22d835db (diff)
refactor: move packages to internal/
Diffstat (limited to 'pkg')
-rw-r--r--pkg/git/git.go365
-rw-r--r--pkg/git/types.go76
-rw-r--r--pkg/git/utils.go91
-rw-r--r--pkg/gitdiff/apply.go147
-rw-r--r--pkg/gitdiff/apply_binary.go206
-rw-r--r--pkg/gitdiff/apply_text.go158
-rw-r--r--pkg/gitdiff/base85.go91
-rw-r--r--pkg/gitdiff/binary.go186
-rw-r--r--pkg/gitdiff/file_header.go546
-rw-r--r--pkg/gitdiff/format.go281
-rw-r--r--pkg/gitdiff/gitdiff.go230
-rw-r--r--pkg/gitdiff/io.go220
-rw-r--r--pkg/gitdiff/parser.go142
-rw-r--r--pkg/gitdiff/patch_header.go470
-rw-r--r--pkg/gitdiff/patch_identity.go166
-rw-r--r--pkg/gitdiff/text.go192
-rw-r--r--pkg/links/links.go179
-rw-r--r--pkg/progress_bar/progress_bar.go84
-rw-r--r--pkg/templates/blob.gohtml72
-rw-r--r--pkg/templates/branches.gohtml58
-rw-r--r--pkg/templates/commit.gohtml314
-rw-r--r--pkg/templates/commits_list.gohtml203
-rw-r--r--pkg/templates/css/markdown_dark.css1042
-rw-r--r--pkg/templates/css/markdown_light.css1059
-rw-r--r--pkg/templates/file_tree.gohtml43
-rw-r--r--pkg/templates/header.gohtml35
-rw-r--r--pkg/templates/layout.gohtml318
-rw-r--r--pkg/templates/list.gohtml145
-rw-r--r--pkg/templates/markdown.gohtml45
-rw-r--r--pkg/templates/preview.gohtml144
-rw-r--r--pkg/templates/svg.gohtml50
-rw-r--r--pkg/templates/tags.gohtml71
-rw-r--r--pkg/templates/templates.go213
33 files changed, 0 insertions, 7642 deletions
diff --git a/pkg/git/git.go b/pkg/git/git.go
deleted file mode 100644
index 1e05d60..0000000
--- a/pkg/git/git.go
+++ /dev/null
@@ -1,365 +0,0 @@
-package git
-
-import (
- "bufio"
- "fmt"
- "io"
- "os/exec"
- "path/filepath"
- "regexp"
- "strconv"
- "strings"
- "time"
-)
-
-func Branches(repoDir string, filter *regexp.Regexp, defaultBranch string) ([]Ref, error) {
- cmd := exec.Command("git", "for-each-ref", "--format=%(refname:short)", "refs/heads/")
- if repoDir != "" {
- cmd.Dir = repoDir
- }
- out, err := cmd.Output()
- if err != nil {
- return nil, fmt.Errorf("failed to list branches: %w", err)
- }
- lines := strings.Split(string(out), "\n")
- branches := make([]Ref, 0, len(lines))
- for _, line := range lines {
- if line == "" {
- continue
- }
-
- if filter != nil && !filter.MatchString(line) && line != defaultBranch {
- continue
- }
- branches = append(branches, NewRef(line))
- }
- return branches, nil
-}
-
-func Tags(repoDir string) ([]Tag, error) {
- format := []string{
- "%(refname:short)", // tag name
- "%(creatordate:unix)", // creation date
- "%(objectname)", // commit hash for lightweight tags
- "%(*objectname)", // peeled object => commit hash
- }
- args := []string{
- "for-each-ref",
- "--sort=-creatordate",
- "--format=" + strings.Join(format, "%00"),
- "refs/tags",
- }
- cmd := exec.Command("git", args...)
- if repoDir != "" {
- cmd.Dir = repoDir
- }
- out, err := cmd.Output()
- if err != nil {
- return nil, fmt.Errorf("failed to list tags: %w", err)
- }
-
- lines := strings.Split(strings.TrimSpace(string(out)), "\n")
- tags := make([]Tag, 0, len(lines))
-
- for _, line := range lines {
- if line == "" {
- continue
- }
- parts := strings.Split(line, "\x00")
- if len(parts) != len(format) {
- continue
- }
- name, timestamp, objectName, commitHash := parts[0], parts[1], parts[2], parts[3]
- timestampInt, err := strconv.Atoi(timestamp)
- if err != nil {
- return nil, fmt.Errorf("failed to parse tag creation date: %w", err)
- }
- if commitHash == "" {
- commitHash = objectName // tag is lightweight
- }
- tags = append(tags, Tag{
- Name: name,
- Date: time.Unix(int64(timestampInt), 0),
- CommitHash: commitHash,
- })
- }
-
- return tags, nil
-}
-
-func Files(ref Ref, repoDir string) ([]Blob, error) {
- if ref.IsEmpty() {
- ref = NewRef("HEAD")
- }
-
- // -r: recurse into subtrees
- // -l: include blob size
- cmd := exec.Command("git", "ls-tree", "--full-tree", "-r", "-l", ref.String())
- if repoDir != "" {
- cmd.Dir = repoDir
- }
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- return nil, fmt.Errorf("failed to get stdout pipe: %w", err)
- }
-
- stderr, err := cmd.StderrPipe()
- if err != nil {
- return nil, fmt.Errorf("failed to get stderr pipe: %w", err)
- }
-
- if err := cmd.Start(); err != nil {
- return nil, fmt.Errorf("failed to start git ls-tree: %w", err)
- }
-
- files := make([]Blob, 0, 256)
-
- // Read stdout line by line; each line is like:
- // <mode> <type> <object> <size>\t<path>
- // Example: "100644 blob e69de29... 12\tREADME.md"
- scanner := bufio.NewScanner(stdout)
-
- // Allow long paths by increasing the scanner buffer limit
- scanner.Buffer(make([]byte, 0, 64*1024), 2*1024*1024)
-
- for scanner.Scan() {
- line := scanner.Text()
- if line == "" {
- continue
- }
-
- // Split header and path using the tab delimiter
- // to preserve spaces in file names
- tab := strings.IndexByte(line, '\t')
- if tab == -1 {
- return nil, fmt.Errorf("expected tab delimiter in ls-tree output: %s", line)
- }
- header := line[:tab]
- path := line[tab+1:]
-
- // header fields: mode, type, object, size
- parts := strings.Fields(header)
- if len(parts) < 4 {
- return nil, fmt.Errorf("unexpected ls-tree output format: %s", line)
- }
- modeNumber := parts[0]
- typ := parts[1]
- // object := parts[2]
- sizeStr := parts[3]
-
- if typ != "blob" {
- // We only care about files (blobs)
- continue
- }
-
- // Size could be "-" for non-blobs in some forms;
- // for blobs it should be a number.
- size, err := strconv.ParseInt(sizeStr, 10, 64)
- if err != nil {
- return nil, err
- }
-
- mode, err := ParseFileMode(modeNumber)
- if err != nil {
- return nil, err
- }
-
- files = append(files, Blob{
- Ref: ref,
- Mode: mode,
- Path: path,
- FileName: filepath.Base(path),
- Size: size,
- })
- }
-
- if err := scanner.Err(); err != nil {
- // Drain stderr to include any git error message
- _ = cmd.Wait()
- b, _ := io.ReadAll(stderr)
- if len(b) > 0 {
- return nil, fmt.Errorf("failed to read ls-tree output: %v: %s", err, string(b))
- }
- return nil, fmt.Errorf("failed to read ls-tree output: %w", err)
- }
-
- // Ensure the command completed successfully
- if err := cmd.Wait(); err != nil {
- b, _ := io.ReadAll(stderr)
- if len(b) > 0 {
- return nil, fmt.Errorf("git ls-tree %q failed: %v: %s", ref, err, string(b))
- }
- return nil, fmt.Errorf("git ls-tree %q failed: %w", ref, err)
- }
-
- return files, nil
-}
-
-func BlobContent(ref Ref, path string, repoDir string) ([]byte, bool, error) {
- if ref.IsEmpty() {
- ref = NewRef("HEAD")
- }
- // Use `git show ref:path` to get the blob content at that ref
- cmd := exec.Command("git", "show", ref.String()+":"+path)
- if repoDir != "" {
- cmd.Dir = repoDir
- }
- out, err := cmd.Output()
- if err != nil {
- // include stderr if available
- if ee, ok := err.(*exec.ExitError); ok {
- return nil, false, fmt.Errorf("git show failed: %v: %s", err, string(ee.Stderr))
- }
- return nil, false, fmt.Errorf("git show failed: %w", err)
- }
- return out, IsBinary(out), nil
-}
-
-func Commits(ref Ref, repoDir string) ([]Commit, error) {
- format := []string{
- "%H", // commit hash
- "%h", // abbreviated commit hash
- "%s", // subject
- "%b", // body
- "%an", // author name
- "%ae", // author email
- "%ad", // author date
- "%cn", // committer name
- "%ce", // committer email
- "%cd", // committer date
- "%P", // parent hashes
- "%D", // ref names without the "(", ")" wrapping.
- }
-
- args := []string{
- "log",
- "--date=unix",
- "--pretty=format:" + strings.Join(format, "\x1F"),
- "-z", // Separate the commits with NULs instead of newlines
- ref.String(),
- }
-
- cmd := exec.Command("git", args...)
- if repoDir != "" {
- cmd.Dir = repoDir
- }
-
- out, err := cmd.Output()
- if err != nil {
- return nil, err
- }
-
- lines := strings.Split(string(out), "\x00")
- commits := make([]Commit, 0, len(lines))
- for _, line := range lines {
- if line == "" {
- continue
- }
- parts := strings.Split(line, "\x1F")
- if len(parts) != len(format) {
- return nil, fmt.Errorf("unexpected commit format: %s", line)
- }
- full, short, subject, body, author, email, date :=
- parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6]
- committerName, committerEmail, committerDate, parents, refs :=
- parts[7], parts[8], parts[9], parts[10], parts[11]
- timestamp, err := strconv.Atoi(date)
- if err != nil {
- return nil, fmt.Errorf("failed to parse commit date: %w", err)
- }
- committerTimestamp, err := strconv.Atoi(committerDate)
- if err != nil {
- return nil, fmt.Errorf("failed to parse committer date: %w", err)
- }
- commits = append(commits, Commit{
- Hash: full,
- ShortHash: short,
- Subject: subject,
- Body: body,
- Author: author,
- Email: email,
- Date: time.Unix(int64(timestamp), 0),
- CommitterName: committerName,
- CommitterEmail: committerEmail,
- CommitterDate: time.Unix(int64(committerTimestamp), 0),
- Parents: strings.Fields(parents),
- RefNames: parseRefNames(refs),
- })
- }
- return commits, nil
-}
-
-func parseRefNames(refNames string) []RefName {
- refNames = strings.TrimSpace(refNames)
- if refNames == "" {
- return nil
- }
-
- parts := strings.Split(refNames, ", ")
- out := make([]RefName, 0, len(parts))
- for _, p := range parts {
- p = strings.TrimSpace(p)
- if p == "" {
- continue
- }
-
- // tag: v1.2.3
- if strings.HasPrefix(p, "tag: ") {
- out = append(out, RefName{
- Kind: RefKindTag,
- Name: strings.TrimSpace(strings.TrimPrefix(p, "tag: ")),
- })
- continue
- }
-
- // HEAD -> main
- if strings.HasPrefix(p, "HEAD -> ") {
- out = append(out, RefName{
- Kind: RefKindHEAD,
- Name: "HEAD",
- Target: strings.TrimSpace(strings.TrimPrefix(p, "HEAD -> ")),
- })
- continue
- }
-
- // origin/HEAD -> origin/main
- if strings.Contains(p, " -> ") && strings.HasSuffix(strings.SplitN(p, " -> ", 2)[0], "/HEAD") {
- leftRight := strings.SplitN(p, " -> ", 2)
- out = append(out, RefName{
- Kind: RefKindRemoteHEAD,
- Name: strings.TrimSpace(leftRight[0]),
- Target: strings.TrimSpace(leftRight[1]),
- })
- continue
- }
-
- // Remote branch like origin/main
- if strings.Contains(p, "/") {
- out = append(out, RefName{
- Kind: RefKindRemote,
- Name: p,
- })
- continue
- }
-
- // Local branch
- out = append(out, RefName{
- Kind: RefKindBranch,
- Name: p,
- })
- }
- return out
-}
-
-func CommitDiff(hash, repoDir string) (string, error) {
- // unified diff without a commit header
- cmd := exec.Command("git", "show", "--pretty=format:", "--patch", hash)
- if repoDir != "" {
- cmd.Dir = repoDir
- }
- out, err := cmd.Output()
- if err != nil {
- return "", err
- }
- return string(out), nil
-}
diff --git a/pkg/git/types.go b/pkg/git/types.go
deleted file mode 100644
index beecfa4..0000000
--- a/pkg/git/types.go
+++ /dev/null
@@ -1,76 +0,0 @@
-package git
-
-import (
- "time"
-)
-
-type Ref struct {
- ref string
- dirName string
-}
-
-func NewRef(ref string) Ref {
- return Ref{
- ref: ref,
- dirName: RefToFileName(ref),
- }
-}
-
-func (r Ref) IsEmpty() bool {
- return r.ref == ""
-}
-
-func (r Ref) String() string {
- return r.ref
-}
-
-func (r Ref) DirName() string {
- return r.dirName
-}
-
-type Blob struct {
- Ref Ref
- Mode string
- Path string
- FileName string
- Size int64
-}
-
-type Commit struct {
- Hash string
- ShortHash string
- Subject string
- Body string
- Author string
- Email string
- Date time.Time
- CommitterName string
- CommitterEmail string
- CommitterDate time.Time
- Parents []string
- Branch Ref
- RefNames []RefName
- Href string
-}
-
-type RefKind string
-
-const (
- RefKindHEAD RefKind = "HEAD"
- RefKindRemoteHEAD RefKind = "RemoteHEAD"
- RefKindBranch RefKind = "Branch"
- RefKindRemote RefKind = "Remote"
- RefKindTag RefKind = "Tag"
-)
-
-type RefName struct {
- Kind RefKind
- Name string // Name is the primary name of the ref as shown by `git log %D` token (left side for pointers)
- Target string // Target is set for symbolic refs like "HEAD -> main" or "origin/HEAD -> origin/main"
-}
-
-type Tag struct {
- Name string
- Date time.Time
- CommitHash string
-}
diff --git a/pkg/git/utils.go b/pkg/git/utils.go
deleted file mode 100644
index 68e4497..0000000
--- a/pkg/git/utils.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package git
-
-import (
- "fmt"
- "strconv"
- "strings"
-)
-
-// ParseFileMode converts a git-style file mode (e.g. "100644")
-// into a human-readable string like "rw-r--r--".
-func ParseFileMode(modeStr string) (string, error) {
- // Git modes are typically 6 digits. The last 3 represent permissions.
- // e.g. 100644 → 644
- if len(modeStr) < 3 {
- return "", fmt.Errorf("invalid mode: %s", modeStr)
- }
-
- permStr := modeStr[len(modeStr)-3:]
- permVal, err := strconv.Atoi(permStr)
- if err != nil {
- return "", err
- }
-
- return numericPermToLetters(permVal), nil
-}
-
-func numericPermToLetters(perm int) string {
- // Map each octal digit to rwx letters
- lookup := map[int]string{
- 0: "---",
- 1: "--x",
- 2: "-w-",
- 3: "-wx",
- 4: "r--",
- 5: "r-x",
- 6: "rw-",
- 7: "rwx",
- }
-
- u := perm / 100 // user
- g := (perm / 10) % 10 // group
- o := perm % 10 // others
-
- return lookup[u] + lookup[g] + lookup[o]
-}
-
-// IsBinary performs a heuristic check to determine if data is binary.
-// Rules:
-// - Any NUL byte => binary
-// - Consider only a sample (up to 8 KiB). If >30% of bytes are control characters
-// outside the common whitespace/newline range, treat as binary.
-func IsBinary(b []byte) bool {
- n := len(b)
- if n == 0 {
- return false
- }
- if n > 8192 {
- n = 8192
- }
- sample := b[:n]
- bad := 0
- for _, c := range sample {
- if c == 0x00 {
- return true
- }
- // Allow common whitespace and control: tab(9), LF(10), CR(13)
- if c == 9 || c == 10 || c == 13 {
- continue
- }
- // Count other control chars and DEL as non-text
- if c < 32 || c == 127 {
- bad++
- }
- }
- // If more than 30% of sampled bytes are non-text, consider binary
- return bad*100 > n*30
-}
-
-func RefToFileName(ref string) string {
- var result strings.Builder
- for _, c := range ref {
- if (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.' {
- result.WriteByte(byte(c))
- } else if c >= 'A' && c <= 'Z' {
- result.WriteByte(byte(c - 'A' + 'a'))
- } else {
- result.WriteByte('-')
- }
- }
- return result.String()
-}
diff --git a/pkg/gitdiff/apply.go b/pkg/gitdiff/apply.go
deleted file mode 100644
index 44bbcca..0000000
--- a/pkg/gitdiff/apply.go
+++ /dev/null
@@ -1,147 +0,0 @@
-package gitdiff
-
-import (
- "errors"
- "fmt"
- "io"
- "sort"
-)
-
-// Conflict indicates an apply failed due to a conflict between the patch and
-// the source content.
-//
-// Users can test if an error was caused by a conflict by using errors.Is with
-// an empty Conflict:
-//
-// if errors.Is(err, &Conflict{}) {
-// // handle conflict
-// }
-type Conflict struct {
- msg string
-}
-
-func (c *Conflict) Error() string {
- return "conflict: " + c.msg
-}
-
-// Is implements error matching for Conflict. Passing an empty instance of
-// Conflict always returns true.
-func (c *Conflict) Is(other error) bool {
- if other, ok := other.(*Conflict); ok {
- return other.msg == "" || other.msg == c.msg
- }
- return false
-}
-
-// ApplyError wraps an error that occurs during patch application with
-// additional location information, if it is available.
-type ApplyError struct {
- // Line is the one-indexed line number in the source data
- Line int64
- // Fragment is the one-indexed fragment number in the file
- Fragment int
- // FragmentLine is the one-indexed line number in the fragment
- FragmentLine int
-
- err error
-}
-
-// Unwrap returns the wrapped error.
-func (e *ApplyError) Unwrap() error {
- return e.err
-}
-
-func (e *ApplyError) Error() string {
- return fmt.Sprintf("%v", e.err)
-}
-
-type lineNum int
-type fragNum int
-type fragLineNum int
-
-// applyError creates a new *ApplyError wrapping err or augments the information
-// in err with args if it is already an *ApplyError. Returns nil if err is nil.
-func applyError(err error, args ...interface{}) error {
- if err == nil {
- return nil
- }
-
- e, ok := err.(*ApplyError)
- if !ok {
- if err == io.EOF {
- err = io.ErrUnexpectedEOF
- }
- e = &ApplyError{err: err}
- }
- for _, arg := range args {
- switch v := arg.(type) {
- case lineNum:
- e.Line = int64(v) + 1
- case fragNum:
- e.Fragment = int(v) + 1
- case fragLineNum:
- e.FragmentLine = int(v) + 1
- }
- }
- return e
-}
-
-var (
- errApplyInProgress = errors.New("gitdiff: incompatible apply in progress")
- errApplierClosed = errors.New("gitdiff: applier is closed")
-)
-
-// Apply applies the changes in f to src, writing the result to dst. It can
-// apply both text and binary changes.
-//
-// If an error occurs while applying, Apply returns an *ApplyError that
-// annotates the error with additional information. If the error is because of
-// a conflict with the source, the wrapped error will be a *Conflict.
-func Apply(dst io.Writer, src io.ReaderAt, f *File) error {
- if f.IsBinary {
- if len(f.TextFragments) > 0 {
- return applyError(errors.New("binary file contains text fragments"))
- }
- if f.BinaryFragment == nil {
- return applyError(errors.New("binary file does not contain a binary fragment"))
- }
- } else {
- if f.BinaryFragment != nil {
- return applyError(errors.New("text file contains a binary fragment"))
- }
- }
-
- switch {
- case f.BinaryFragment != nil:
- applier := NewBinaryApplier(dst, src)
- if err := applier.ApplyFragment(f.BinaryFragment); err != nil {
- return err
- }
- return applier.Close()
-
- case len(f.TextFragments) > 0:
- frags := make([]*TextFragment, len(f.TextFragments))
- copy(frags, f.TextFragments)
-
- sort.Slice(frags, func(i, j int) bool {
- return frags[i].OldPosition < frags[j].OldPosition
- })
-
- // TODO(bkeyes): consider merging overlapping fragments
- // right now, the application fails if fragments overlap, but it should be
- // possible to precompute the result of applying them in order
-
- applier := NewTextApplier(dst, src)
- for i, frag := range frags {
- if err := applier.ApplyFragment(frag); err != nil {
- return applyError(err, fragNum(i))
- }
- }
- return applier.Close()
-
- default:
- // nothing to apply, just copy all the data
- _, err := copyFrom(dst, src, 0)
- return err
- }
-}
diff --git a/pkg/gitdiff/apply_binary.go b/pkg/gitdiff/apply_binary.go
deleted file mode 100644
index b34772d..0000000
--- a/pkg/gitdiff/apply_binary.go
+++ /dev/null
@@ -1,206 +0,0 @@
-package gitdiff
-
-import (
- "errors"
- "fmt"
- "io"
-)
-
-// BinaryApplier applies binary changes described in a fragment to source data.
-// The applier must be closed after use.
-type BinaryApplier struct {
- dst io.Writer
- src io.ReaderAt
-
- closed bool
- dirty bool
-}
-
-// NewBinaryApplier creates an BinaryApplier that reads data from src and
-// writes modified data to dst.
-func NewBinaryApplier(dst io.Writer, src io.ReaderAt) *BinaryApplier {
- a := BinaryApplier{
- dst: dst,
- src: src,
- }
- return &a
-}
-
-// ApplyFragment applies the changes in the fragment f and writes the result to
-// dst. ApplyFragment can be called at most once.
-//
-// If an error occurs while applying, ApplyFragment returns an *ApplyError that
-// annotates the error with additional information. If the error is because of
-// a conflict between a fragment and the source, the wrapped error will be a
-// *Conflict.
-func (a *BinaryApplier) ApplyFragment(f *BinaryFragment) error {
- if f == nil {
- return applyError(errors.New("nil fragment"))
- }
- if a.closed {
- return applyError(errApplierClosed)
- }
- if a.dirty {
- return applyError(errApplyInProgress)
- }
-
- // mark an apply as in progress, even if it fails before making changes
- a.dirty = true
-
- switch f.Method {
- case BinaryPatchLiteral:
- if _, err := a.dst.Write(f.Data); err != nil {
- return applyError(err)
- }
- case BinaryPatchDelta:
- if err := applyBinaryDeltaFragment(a.dst, a.src, f.Data); err != nil {
- return applyError(err)
- }
- default:
- return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method))
- }
- return nil
-}
-
-// Close writes any data following the last applied fragment and prevents
-// future calls to ApplyFragment.
-func (a *BinaryApplier) Close() (err error) {
- if a.closed {
- return nil
- }
-
- a.closed = true
- if !a.dirty {
- _, err = copyFrom(a.dst, a.src, 0)
- } else {
- // do nothing, applying a binary fragment copies all data
- }
- return err
-}
-
-func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error {
- srcSize, delta := readBinaryDeltaSize(frag)
- if err := checkBinarySrcSize(src, srcSize); err != nil {
- return err
- }
-
- dstSize, delta := readBinaryDeltaSize(delta)
-
- for len(delta) > 0 {
- op := delta[0]
- if op == 0 {
- return errors.New("invalid delta opcode 0")
- }
-
- var n int64
- var err error
- switch op & 0x80 {
- case 0x80:
- n, delta, err = applyBinaryDeltaCopy(dst, op, delta[1:], src)
- case 0x00:
- n, delta, err = applyBinaryDeltaAdd(dst, op, delta[1:])
- }
- if err != nil {
- return err
- }
- dstSize -= n
- }
-
- if dstSize != 0 {
- return errors.New("corrupt binary delta: insufficient or extra data")
- }
- return nil
-}
-
-// readBinaryDeltaSize reads a variable length size from a delta-encoded binary
-// fragment, returing the size and the unused data. Data is encoded as:
-//
-// [[1xxxxxxx]...] [0xxxxxxx]
-//
-// in little-endian order, with 7 bits of the value per byte.
-func readBinaryDeltaSize(d []byte) (size int64, rest []byte) {
- shift := uint(0)
- for i, b := range d {
- size |= int64(b&0x7F) << shift
- shift += 7
- if b <= 0x7F {
- return size, d[i+1:]
- }
- }
- return size, nil
-}
-
-// applyBinaryDeltaAdd applies an add opcode in a delta-encoded binary
-// fragment, returning the amount of data written and the usused part of the
-// fragment. An add operation takes the form:
-//
-// [0xxxxxx][[data1]...]
-//
-// where the lower seven bits of the opcode is the number of data bytes
-// following the opcode. See also pack-format.txt in the Git source.
-func applyBinaryDeltaAdd(w io.Writer, op byte, delta []byte) (n int64, rest []byte, err error) {
- size := int(op)
- if len(delta) < size {
- return 0, delta, errors.New("corrupt binary delta: incomplete add")
- }
- _, err = w.Write(delta[:size])
- return int64(size), delta[size:], err
-}
-
-// applyBinaryDeltaCopy applies a copy opcode in a delta-encoded binary
-// fragment, returing the amount of data written and the unused part of the
-// fragment. A copy operation takes the form:
-//
-// [1xxxxxxx][offset1][offset2][offset3][offset4][size1][size2][size3]
-//
-// where the lower seven bits of the opcode determine which non-zero offset and
-// size bytes are present in little-endian order: if bit 0 is set, offset1 is
-// present, etc. If no offset or size bytes are present, offset is 0 and size
-// is 0x10000. See also pack-format.txt in the Git source.
-func applyBinaryDeltaCopy(w io.Writer, op byte, delta []byte, src io.ReaderAt) (n int64, rest []byte, err error) {
- const defaultSize = 0x10000
-
- unpack := func(start, bits uint) (v int64) {
- for i := uint(0); i < bits; i++ {
- mask := byte(1 << (i + start))
- if op&mask > 0 {
- if len(delta) == 0 {
- err = errors.New("corrupt binary delta: incomplete copy")
- return
- }
- v |= int64(delta[0]) << (8 * i)
- delta = delta[1:]
- }
- }
- return
- }
-
- offset := unpack(0, 4)
- size := unpack(4, 3)
- if err != nil {
- return 0, delta, err
- }
- if size == 0 {
- size = defaultSize
- }
-
- // TODO(bkeyes): consider pooling these buffers
- b := make([]byte, size)
- if _, err := src.ReadAt(b, offset); err != nil {
- return 0, delta, err
- }
-
- _, err = w.Write(b)
- return size, delta, err
-}
-
-func checkBinarySrcSize(r io.ReaderAt, size int64) error {
- ok, err := isLen(r, size)
- if err != nil {
- return err
- }
- if !ok {
- return &Conflict{"fragment src size does not match actual src size"}
- }
- return nil
-}
diff --git a/pkg/gitdiff/apply_text.go b/pkg/gitdiff/apply_text.go
deleted file mode 100644
index 8d0accb..0000000
--- a/pkg/gitdiff/apply_text.go
+++ /dev/null
@@ -1,158 +0,0 @@
-package gitdiff
-
-import (
- "errors"
- "io"
-)
-
-// TextApplier applies changes described in text fragments to source data. If
-// changes are described in multiple fragments, those fragments must be applied
-// in order. The applier must be closed after use.
-//
-// By default, TextApplier operates in "strict" mode, where fragment content
-// and positions must exactly match those of the source.
-type TextApplier struct {
- dst io.Writer
- src io.ReaderAt
- lineSrc LineReaderAt
- nextLine int64
-
- closed bool
- dirty bool
-}
-
-// NewTextApplier creates a TextApplier that reads data from src and writes
-// modified data to dst. If src implements LineReaderAt, it is used directly.
-func NewTextApplier(dst io.Writer, src io.ReaderAt) *TextApplier {
- a := TextApplier{
- dst: dst,
- src: src,
- }
-
- if lineSrc, ok := src.(LineReaderAt); ok {
- a.lineSrc = lineSrc
- } else {
- a.lineSrc = &lineReaderAt{r: src}
- }
-
- return &a
-}
-
-// ApplyFragment applies the changes in the fragment f, writing unwritten data
-// before the start of the fragment and any changes from the fragment. If
-// multiple text fragments apply to the same content, ApplyFragment must be
-// called in order of increasing start position. As a result, each fragment can
-// be applied at most once.
-//
-// If an error occurs while applying, ApplyFragment returns an *ApplyError that
-// annotates the error with additional information. If the error is because of
-// a conflict between the fragment and the source, the wrapped error will be a
-// *Conflict.
-func (a *TextApplier) ApplyFragment(f *TextFragment) error {
- if a.closed {
- return applyError(errApplierClosed)
- }
-
- // mark an apply as in progress, even if it fails before making changes
- a.dirty = true
-
- // application code assumes fragment fields are consistent
- if err := f.Validate(); err != nil {
- return applyError(err)
- }
-
- // lines are 0-indexed, positions are 1-indexed (but new files have position = 0)
- fragStart := f.OldPosition - 1
- if fragStart < 0 {
- fragStart = 0
- }
- fragEnd := fragStart + f.OldLines
-
- start := a.nextLine
- if fragStart < start {
- return applyError(&Conflict{"fragment overlaps with an applied fragment"})
- }
-
- if f.OldPosition == 0 {
- ok, err := isLen(a.src, 0)
- if err != nil {
- return applyError(err)
- }
- if !ok {
- return applyError(&Conflict{"cannot create new file from non-empty src"})
- }
- }
-
- preimage := make([][]byte, fragEnd-start)
- n, err := a.lineSrc.ReadLinesAt(preimage, start)
- if err != nil {
- // an EOF indicates that source file is shorter than the patch expects,
- // which should be reported as a conflict rather than a generic error
- if errors.Is(err, io.EOF) {
- err = &Conflict{"src has fewer lines than required by fragment"}
- }
- return applyError(err, lineNum(start+int64(n)))
- }
-
- // copy leading data before the fragment starts
- for i, line := range preimage[:fragStart-start] {
- if _, err := a.dst.Write(line); err != nil {
- a.nextLine = start + int64(i)
- return applyError(err, lineNum(a.nextLine))
- }
- }
- preimage = preimage[fragStart-start:]
-
- // apply the changes in the fragment
- used := int64(0)
- for i, line := range f.Lines {
- if err := applyTextLine(a.dst, line, preimage, used); err != nil {
- a.nextLine = fragStart + used
- return applyError(err, lineNum(a.nextLine), fragLineNum(i))
- }
- if line.Old() {
- used++
- }
- }
- a.nextLine = fragStart + used
-
- // new position of +0,0 mean a full delete, so check for leftovers
- if f.NewPosition == 0 && f.NewLines == 0 {
- var b [1][]byte
- n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine)
- if err != nil && err != io.EOF {
- return applyError(err, lineNum(a.nextLine))
- }
- if n > 0 {
- return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine))
- }
- }
-
- return nil
-}
-
-func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) {
- if line.Old() && string(preimage[i]) != line.Line {
- return &Conflict{"fragment line does not match src line"}
- }
- if line.New() {
- _, err = io.WriteString(dst, line.Line)
- }
- return err
-}
-
-// Close writes any data following the last applied fragment and prevents
-// future calls to ApplyFragment.
-func (a *TextApplier) Close() (err error) {
- if a.closed {
- return nil
- }
-
- a.closed = true
- if !a.dirty {
- _, err = copyFrom(a.dst, a.src, 0)
- } else {
- _, err = copyLinesFrom(a.dst, a.lineSrc, a.nextLine)
- }
- return err
-}
diff --git a/pkg/gitdiff/base85.go b/pkg/gitdiff/base85.go
deleted file mode 100644
index 86db117..0000000
--- a/pkg/gitdiff/base85.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package gitdiff
-
-import (
- "fmt"
-)
-
-var (
- b85Table map[byte]byte
- b85Alpha = []byte(
- "0123456789" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "abcdefghijklmnopqrstuvwxyz" + "!#$%&()*+-;<=>?@^_`{|}~",
- )
-)
-
-func init() {
- b85Table = make(map[byte]byte)
- for i, c := range b85Alpha {
- b85Table[c] = byte(i)
- }
-}
-
-// base85Decode decodes Base85-encoded data from src into dst. It uses the
-// alphabet defined by base85.c in the Git source tree. src must contain at
-// least len(dst) bytes of encoded data.
-func base85Decode(dst, src []byte) error {
- var v uint32
- var n, ndst int
- for i, b := range src {
- if b, ok := b85Table[b]; ok {
- v = 85*v + uint32(b)
- n++
- } else {
- return fmt.Errorf("invalid base85 byte at index %d: 0x%X", i, src[i])
- }
- if n == 5 {
- rem := len(dst) - ndst
- for j := 0; j < 4 && j < rem; j++ {
- dst[ndst] = byte(v >> 24)
- ndst++
- v <<= 8
- }
- v = 0
- n = 0
- }
- }
- if n > 0 {
- return fmt.Errorf("base85 data terminated by underpadded sequence")
- }
- if ndst < len(dst) {
- return fmt.Errorf("base85 data underrun: %d < %d", ndst, len(dst))
- }
- return nil
-}
-
-// base85Encode encodes src in Base85, writing the result to dst. It uses the
-// alphabet defined by base85.c in the Git source tree.
-func base85Encode(dst, src []byte) {
- var di, si int
-
- encode := func(v uint32) {
- dst[di+0] = b85Alpha[(v/(85*85*85*85))%85]
- dst[di+1] = b85Alpha[(v/(85*85*85))%85]
- dst[di+2] = b85Alpha[(v/(85*85))%85]
- dst[di+3] = b85Alpha[(v/85)%85]
- dst[di+4] = b85Alpha[v%85]
- }
-
- n := (len(src) / 4) * 4
- for si < n {
- encode(uint32(src[si+0])<<24 | uint32(src[si+1])<<16 | uint32(src[si+2])<<8 | uint32(src[si+3]))
- si += 4
- di += 5
- }
-
- var v uint32
- switch len(src) - si {
- case 3:
- v |= uint32(src[si+2]) << 8
- fallthrough
- case 2:
- v |= uint32(src[si+1]) << 16
- fallthrough
- case 1:
- v |= uint32(src[si+0]) << 24
- encode(v)
- }
-}
-
-// base85Len returns the length of n bytes of Base85 encoded data.
-func base85Len(n int) int {
- return (n + 3) / 4 * 5
-}
diff --git a/pkg/gitdiff/binary.go b/pkg/gitdiff/binary.go
deleted file mode 100644
index 282e323..0000000
--- a/pkg/gitdiff/binary.go
+++ /dev/null
@@ -1,186 +0,0 @@
-package gitdiff
-
-import (
- "bytes"
- "compress/zlib"
- "fmt"
- "io"
- "io/ioutil"
- "strconv"
- "strings"
-)
-
-func (p *parser) ParseBinaryFragments(f *File) (n int, err error) {
- isBinary, hasData, err := p.ParseBinaryMarker()
- if err != nil || !isBinary {
- return 0, err
- }
-
- f.IsBinary = true
- if !hasData {
- return 0, nil
- }
-
- forward, err := p.ParseBinaryFragmentHeader()
- if err != nil {
- return 0, err
- }
- if forward == nil {
- return 0, p.Errorf(0, "missing data for binary patch")
- }
- if err := p.ParseBinaryChunk(forward); err != nil {
- return 0, err
- }
- f.BinaryFragment = forward
-
- // valid for reverse to not exist, but it must be valid if present
- reverse, err := p.ParseBinaryFragmentHeader()
- if err != nil {
- return 1, err
- }
- if reverse == nil {
- return 1, nil
- }
- if err := p.ParseBinaryChunk(reverse); err != nil {
- return 1, err
- }
- f.ReverseBinaryFragment = reverse
-
- return 1, nil
-}
-
-func (p *parser) ParseBinaryMarker() (isBinary bool, hasData bool, err error) {
- line := p.Line(0)
- switch {
- case line == "GIT binary patch\n":
- hasData = true
- case isBinaryNoDataMarker(line):
- default:
- return false, false, nil
- }
-
- if err = p.Next(); err != nil && err != io.EOF {
- return false, false, err
- }
- return true, hasData, nil
-}
-
-func isBinaryNoDataMarker(line string) bool {
- if strings.HasSuffix(line, " differ\n") {
- return strings.HasPrefix(line, "Binary files ") || strings.HasPrefix(line, "Files ")
- }
- return false
-}
-
-func (p *parser) ParseBinaryFragmentHeader() (*BinaryFragment, error) {
- parts := strings.SplitN(strings.TrimSuffix(p.Line(0), "\n"), " ", 2)
- if len(parts) < 2 {
- return nil, nil
- }
-
- frag := &BinaryFragment{}
- switch parts[0] {
- case "delta":
- frag.Method = BinaryPatchDelta
- case "literal":
- frag.Method = BinaryPatchLiteral
- default:
- return nil, nil
- }
-
- var err error
- if frag.Size, err = strconv.ParseInt(parts[1], 10, 64); err != nil {
- nerr := err.(*strconv.NumError)
- return nil, p.Errorf(0, "binary patch: invalid size: %v", nerr.Err)
- }
-
- if err := p.Next(); err != nil && err != io.EOF {
- return nil, err
- }
- return frag, nil
-}
-
-func (p *parser) ParseBinaryChunk(frag *BinaryFragment) error {
- // Binary fragments are encoded as a series of base85 encoded lines. Each
- // line starts with a character in [A-Za-z] giving the number of bytes on
- // the line, where A = 1 and z = 52, and ends with a newline character.
- //
- // The base85 encoding means each line is a multiple of 5 characters + 2
- // additional characters for the length byte and the newline. The fragment
- // ends with a blank line.
- const (
- shortestValidLine = "A00000\n"
- maxBytesPerLine = 52
- )
-
- var data bytes.Buffer
- buf := make([]byte, maxBytesPerLine)
- for {
- line := p.Line(0)
- if line == "\n" {
- break
- }
- if len(line) < len(shortestValidLine) || (len(line)-2)%5 != 0 {
- return p.Errorf(0, "binary patch: corrupt data line")
- }
-
- byteCount, seq := int(line[0]), line[1:len(line)-1]
- switch {
- case 'A' <= byteCount && byteCount <= 'Z':
- byteCount = byteCount - 'A' + 1
- case 'a' <= byteCount && byteCount <= 'z':
- byteCount = byteCount - 'a' + 27
- default:
- return p.Errorf(0, "binary patch: invalid length byte")
- }
-
- // base85 encodes every 4 bytes into 5 characters, with up to 3 bytes of end padding
- maxByteCount := len(seq) / 5 * 4
- if byteCount > maxByteCount || byteCount < maxByteCount-3 {
- return p.Errorf(0, "binary patch: incorrect byte count")
- }
-
- if err := base85Decode(buf[:byteCount], []byte(seq)); err != nil {
- return p.Errorf(0, "binary patch: %v", err)
- }
- data.Write(buf[:byteCount])
-
- if err := p.Next(); err != nil {
- if err == io.EOF {
- return p.Errorf(0, "binary patch: unexpected EOF")
- }
- return err
- }
- }
-
- if err := inflateBinaryChunk(frag, &data); err != nil {
- return p.Errorf(0, "binary patch: %v", err)
- }
-
- // consume the empty line that ended the fragment
- if err := p.Next(); err != nil && err != io.EOF {
- return err
- }
- return nil
-}
-
-func inflateBinaryChunk(frag *BinaryFragment, r io.Reader) error {
- zr, err := zlib.NewReader(r)
- if err != nil {
- return err
- }
-
- data, err := ioutil.ReadAll(zr)
- if err != nil {
- return err
- }
- if err := zr.Close(); err != nil {
- return err
- }
-
- if int64(len(data)) != frag.Size {
- return fmt.Errorf("%d byte fragment inflated to %d", frag.Size, len(data))
- }
- frag.Data = data
- return nil
-}
diff --git a/pkg/gitdiff/file_header.go b/pkg/gitdiff/file_header.go
deleted file mode 100644
index 7ae4bc9..0000000
--- a/pkg/gitdiff/file_header.go
+++ /dev/null
@@ -1,546 +0,0 @@
-package gitdiff
-
-import (
- "fmt"
- "io"
- "os"
- "strconv"
- "strings"
- "time"
-)
-
-const (
- devNull = "/dev/null"
-)
-
-// ParseNextFileHeader finds and parses the next file header in the stream. If
-// a header is found, it returns a file and all input before the header. It
-// returns nil if no headers are found before the end of the input.
-func (p *parser) ParseNextFileHeader() (*File, string, error) {
- var preamble strings.Builder
- var file *File
- for {
- // check for disconnected fragment headers (corrupt patch)
- frag, err := p.ParseTextFragmentHeader()
- if err != nil {
- // not a valid header, nothing to worry about
- goto NextLine
- }
- if frag != nil {
- return nil, "", p.Errorf(-1, "patch fragment without file header: %s", frag.Header())
- }
-
- // check for a git-generated patch
- file, err = p.ParseGitFileHeader()
- if err != nil {
- return nil, "", err
- }
- if file != nil {
- return file, preamble.String(), nil
- }
-
- // check for a "traditional" patch
- file, err = p.ParseTraditionalFileHeader()
- if err != nil {
- return nil, "", err
- }
- if file != nil {
- return file, preamble.String(), nil
- }
-
- NextLine:
- preamble.WriteString(p.Line(0))
- if err := p.Next(); err != nil {
- if err == io.EOF {
- break
- }
- return nil, "", err
- }
- }
- return nil, preamble.String(), nil
-}
-
-func (p *parser) ParseGitFileHeader() (*File, error) {
- const prefix = "diff --git "
-
- if !strings.HasPrefix(p.Line(0), prefix) {
- return nil, nil
- }
- header := p.Line(0)[len(prefix):]
-
- defaultName, err := parseGitHeaderName(header)
- if err != nil {
- return nil, p.Errorf(0, "git file header: %v", err)
- }
-
- f := &File{}
- for {
- end, err := parseGitHeaderData(f, p.Line(1), defaultName)
- if err != nil {
- return nil, p.Errorf(1, "git file header: %v", err)
- }
-
- if err := p.Next(); err != nil {
- if err == io.EOF {
- break
- }
- return nil, err
- }
-
- if end {
- break
- }
- }
-
- if f.OldName == "" && f.NewName == "" {
- if defaultName == "" {
- return nil, p.Errorf(0, "git file header: missing filename information")
- }
- f.OldName = defaultName
- f.NewName = defaultName
- }
-
- if (f.NewName == "" && !f.IsDelete) || (f.OldName == "" && !f.IsNew) {
- return nil, p.Errorf(0, "git file header: missing filename information")
- }
-
- return f, nil
-}
-
-func (p *parser) ParseTraditionalFileHeader() (*File, error) {
- const shortestValidFragHeader = "@@ -1 +1 @@\n"
- const (
- oldPrefix = "--- "
- newPrefix = "+++ "
- )
-
- oldLine, newLine := p.Line(0), p.Line(1)
-
- if !strings.HasPrefix(oldLine, oldPrefix) || !strings.HasPrefix(newLine, newPrefix) {
- return nil, nil
- }
- // heuristic: only a file header if followed by a (probable) fragment header
- if len(p.Line(2)) < len(shortestValidFragHeader) || !strings.HasPrefix(p.Line(2), "@@ -") {
- return nil, nil
- }
-
- // advance past the first two lines so parser is after the header
- // no EOF check needed because we know there are >=3 valid lines
- if err := p.Next(); err != nil {
- return nil, err
- }
- if err := p.Next(); err != nil {
- return nil, err
- }
-
- oldName, _, err := parseName(oldLine[len(oldPrefix):], '\t', 0)
- if err != nil {
- return nil, p.Errorf(0, "file header: %v", err)
- }
-
- newName, _, err := parseName(newLine[len(newPrefix):], '\t', 0)
- if err != nil {
- return nil, p.Errorf(1, "file header: %v", err)
- }
-
- f := &File{}
- switch {
- case oldName == devNull || hasEpochTimestamp(oldLine):
- f.IsNew = true
- f.NewName = newName
- case newName == devNull || hasEpochTimestamp(newLine):
- f.IsDelete = true
- f.OldName = oldName
- default:
- // if old name is a prefix of new name, use that instead
- // this avoids picking variants like "file.bak" or "file~"
- if strings.HasPrefix(newName, oldName) {
- f.OldName = oldName
- f.NewName = oldName
- } else {
- f.OldName = newName
- f.NewName = newName
- }
- }
-
- return f, nil
-}
-
-// parseGitHeaderName extracts a default file name from the Git file header
-// line. This is required for mode-only changes and creation/deletion of empty
-// files. Other types of patch include the file name(s) in the header data.
-// If the names in the header do not match because the patch is a rename,
-// return an empty default name.
-func parseGitHeaderName(header string) (string, error) {
- header = strings.TrimSuffix(header, "\n")
- if len(header) == 0 {
- return "", nil
- }
-
- var err error
- var first, second string
-
- // there are 4 cases to account for:
- //
- // 1) unquoted unquoted
- // 2) unquoted "quoted"
- // 3) "quoted" unquoted
- // 4) "quoted" "quoted"
- //
- quote := strings.IndexByte(header, '"')
- switch {
- case quote < 0:
- // case 1
- first = header
-
- case quote > 0:
- // case 2
- first = header[:quote-1]
- if !isSpace(header[quote-1]) {
- return "", fmt.Errorf("missing separator")
- }
-
- second, _, err = parseQuotedName(header[quote:])
- if err != nil {
- return "", err
- }
-
- case quote == 0:
- // case 3 or case 4
- var n int
- first, n, err = parseQuotedName(header)
- if err != nil {
- return "", err
- }
-
- // git accepts multiple spaces after a quoted name, but not after an
- // unquoted name, since the name might end with one or more spaces
- for n < len(header) && isSpace(header[n]) {
- n++
- }
- if n == len(header) {
- return "", nil
- }
-
- if header[n] == '"' {
- second, _, err = parseQuotedName(header[n:])
- if err != nil {
- return "", err
- }
- } else {
- second = header[n:]
- }
- }
-
- first = trimTreePrefix(first, 1)
- if second != "" {
- if first == trimTreePrefix(second, 1) {
- return first, nil
- }
- return "", nil
- }
-
- // at this point, both names are unquoted (case 1)
- // since names may contain spaces, we can't use a known separator
- // instead, look for a split that produces two equal names
-
- for i := 0; i < len(first)-1; i++ {
- if !isSpace(first[i]) {
- continue
- }
- second = trimTreePrefix(first[i+1:], 1)
- if name := first[:i]; name == second {
- return name, nil
- }
- }
- return "", nil
-}
-
-// parseGitHeaderData parses a single line of metadata from a Git file header.
-// It returns true when header parsing is complete; in that case, line was the
-// first line of non-header content.
-func parseGitHeaderData(f *File, line, defaultName string) (end bool, err error) {
- if len(line) > 0 && line[len(line)-1] == '\n' {
- line = line[:len(line)-1]
- }
-
- for _, hdr := range []struct {
- prefix string
- end bool
- parse func(*File, string, string) error
- }{
- {"@@ -", true, nil},
- {"--- ", false, parseGitHeaderOldName},
- {"+++ ", false, parseGitHeaderNewName},
- {"old mode ", false, parseGitHeaderOldMode},
- {"new mode ", false, parseGitHeaderNewMode},
- {"deleted file mode ", false, parseGitHeaderDeletedMode},
- {"new file mode ", false, parseGitHeaderCreatedMode},
- {"copy from ", false, parseGitHeaderCopyFrom},
- {"copy to ", false, parseGitHeaderCopyTo},
- {"rename old ", false, parseGitHeaderRenameFrom},
- {"rename new ", false, parseGitHeaderRenameTo},
- {"rename from ", false, parseGitHeaderRenameFrom},
- {"rename to ", false, parseGitHeaderRenameTo},
- {"similarity index ", false, parseGitHeaderScore},
- {"dissimilarity index ", false, parseGitHeaderScore},
- {"index ", false, parseGitHeaderIndex},
- } {
- if strings.HasPrefix(line, hdr.prefix) {
- if hdr.parse != nil {
- err = hdr.parse(f, line[len(hdr.prefix):], defaultName)
- }
- return hdr.end, err
- }
- }
-
- // unknown line indicates the end of the header
- // this usually happens if the diff is empty
- return true, nil
-}
-
-func parseGitHeaderOldName(f *File, line, defaultName string) error {
- name, _, err := parseName(line, '\t', 1)
- if err != nil {
- return err
- }
- if f.OldName == "" && !f.IsNew {
- f.OldName = name
- return nil
- }
- return verifyGitHeaderName(name, f.OldName, f.IsNew, "old")
-}
-
-func parseGitHeaderNewName(f *File, line, defaultName string) error {
- name, _, err := parseName(line, '\t', 1)
- if err != nil {
- return err
- }
- if f.NewName == "" && !f.IsDelete {
- f.NewName = name
- return nil
- }
- return verifyGitHeaderName(name, f.NewName, f.IsDelete, "new")
-}
-
-func parseGitHeaderOldMode(f *File, line, defaultName string) (err error) {
- f.OldMode, err = parseMode(strings.TrimSpace(line))
- return
-}
-
-func parseGitHeaderNewMode(f *File, line, defaultName string) (err error) {
- f.NewMode, err = parseMode(strings.TrimSpace(line))
- return
-}
-
-func parseGitHeaderDeletedMode(f *File, line, defaultName string) error {
- f.IsDelete = true
- f.OldName = defaultName
- return parseGitHeaderOldMode(f, line, defaultName)
-}
-
-func parseGitHeaderCreatedMode(f *File, line, defaultName string) error {
- f.IsNew = true
- f.NewName = defaultName
- return parseGitHeaderNewMode(f, line, defaultName)
-}
-
-func parseGitHeaderCopyFrom(f *File, line, defaultName string) (err error) {
- f.IsCopy = true
- f.OldName, _, err = parseName(line, 0, 0)
- return
-}
-
-func parseGitHeaderCopyTo(f *File, line, defaultName string) (err error) {
- f.IsCopy = true
- f.NewName, _, err = parseName(line, 0, 0)
- return
-}
-
-func parseGitHeaderRenameFrom(f *File, line, defaultName string) (err error) {
- f.IsRename = true
- f.OldName, _, err = parseName(line, 0, 0)
- return
-}
-
-func parseGitHeaderRenameTo(f *File, line, defaultName string) (err error) {
- f.IsRename = true
- f.NewName, _, err = parseName(line, 0, 0)
- return
-}
-
-func parseGitHeaderScore(f *File, line, defaultName string) error {
- score, err := strconv.ParseInt(strings.TrimSuffix(line, "%"), 10, 32)
- if err != nil {
- nerr := err.(*strconv.NumError)
- return fmt.Errorf("invalid score line: %v", nerr.Err)
- }
- if score <= 100 {
- f.Score = int(score)
- }
- return nil
-}
-
-func parseGitHeaderIndex(f *File, line, defaultName string) error {
- const sep = ".."
-
- // note that git stops parsing if the OIDs are too long to be valid
- // checking this requires knowing if the repository uses SHA1 or SHA256
- // hashes, which we don't know, so we just skip that check
-
- parts := strings.SplitN(line, " ", 2)
- oids := strings.SplitN(parts[0], sep, 2)
-
- if len(oids) < 2 {
- return fmt.Errorf("invalid index line: missing %q", sep)
- }
- f.OldOIDPrefix, f.NewOIDPrefix = oids[0], oids[1]
-
- if len(parts) > 1 {
- return parseGitHeaderOldMode(f, parts[1], defaultName)
- }
- return nil
-}
-
-func parseMode(s string) (os.FileMode, error) {
- mode, err := strconv.ParseInt(s, 8, 32)
- if err != nil {
- nerr := err.(*strconv.NumError)
- return os.FileMode(0), fmt.Errorf("invalid mode line: %v", nerr.Err)
- }
- return os.FileMode(mode), nil
-}
-
-// parseName extracts a file name from the start of a string and returns the
-// name and the index of the first character after the name. If the name is
-// unquoted and term is non-zero, parsing stops at the first occurrence of
-// term.
-//
-// If the name is exactly "/dev/null", no further processing occurs. Otherwise,
-// if dropPrefix is greater than zero, that number of prefix components
-// separated by forward slashes are dropped from the name and any duplicate
-// slashes are collapsed.
-func parseName(s string, term byte, dropPrefix int) (name string, n int, err error) {
- if len(s) > 0 && s[0] == '"' {
- name, n, err = parseQuotedName(s)
- } else {
- name, n, err = parseUnquotedName(s, term)
- }
- if err != nil {
- return "", 0, err
- }
- if name == devNull {
- return name, n, nil
- }
- return cleanName(name, dropPrefix), n, nil
-}
-
-func parseQuotedName(s string) (name string, n int, err error) {
- for n = 1; n < len(s); n++ {
- if s[n] == '"' && s[n-1] != '\\' {
- n++
- break
- }
- }
- if n == 2 {
- return "", 0, fmt.Errorf("missing name")
- }
- if name, err = strconv.Unquote(s[:n]); err != nil {
- return "", 0, err
- }
- return name, n, err
-}
-
-func parseUnquotedName(s string, term byte) (name string, n int, err error) {
- for n = 0; n < len(s); n++ {
- if s[n] == '\n' {
- break
- }
- if term > 0 && s[n] == term {
- break
- }
- }
- if n == 0 {
- return "", 0, fmt.Errorf("missing name")
- }
- return s[:n], n, nil
-}
-
-// verifyGitHeaderName checks a parsed name against state set by previous lines
-func verifyGitHeaderName(parsed, existing string, isNull bool, side string) error {
- if existing != "" {
- if isNull {
- return fmt.Errorf("expected %s, but filename is set to %s", devNull, existing)
- }
- if existing != parsed {
- return fmt.Errorf("inconsistent %s filename", side)
- }
- }
- if isNull && parsed != devNull {
- return fmt.Errorf("expected %s", devNull)
- }
- return nil
-}
-
-// cleanName removes double slashes and drops prefix segments.
-func cleanName(name string, drop int) string {
- var b strings.Builder
- for i := 0; i < len(name); i++ {
- if name[i] == '/' {
- if i < len(name)-1 && name[i+1] == '/' {
- continue
- }
- if drop > 0 {
- drop--
- b.Reset()
- continue
- }
- }
- b.WriteByte(name[i])
- }
- return b.String()
-}
-
-// trimTreePrefix removes up to n leading directory components from name.
-func trimTreePrefix(name string, n int) string {
- i := 0
- for ; i < len(name) && n > 0; i++ {
- if name[i] == '/' {
- n--
- }
- }
- return name[i:]
-}
-
-// hasEpochTimestamp returns true if the string ends with a POSIX-formatted
-// timestamp for the UNIX epoch after a tab character. According to git, this
-// is used by GNU diff to mark creations and deletions.
-func hasEpochTimestamp(s string) bool {
- const posixTimeLayout = "2006-01-02 15:04:05.9 -0700"
-
- start := strings.IndexRune(s, '\t')
- if start < 0 {
- return false
- }
-
- ts := strings.TrimSuffix(s[start+1:], "\n")
-
- // a valid timestamp can have optional ':' in zone specifier
- // remove that if it exists so we have a single format
- if len(ts) >= 3 && ts[len(ts)-3] == ':' {
- ts = ts[:len(ts)-3] + ts[len(ts)-2:]
- }
-
- t, err := time.Parse(posixTimeLayout, ts)
- if err != nil {
- return false
- }
- if !t.Equal(time.Unix(0, 0)) {
- return false
- }
- return true
-}
-
-func isSpace(c byte) bool {
- return c == ' ' || c == '\t' || c == '\n'
-}
diff --git a/pkg/gitdiff/format.go b/pkg/gitdiff/format.go
deleted file mode 100644
index d97aba9..0000000
--- a/pkg/gitdiff/format.go
+++ /dev/null
@@ -1,281 +0,0 @@
-package gitdiff
-
-import (
- "bytes"
- "compress/zlib"
- "fmt"
- "io"
- "strconv"
-)
-
-type formatter struct {
- w io.Writer
- err error
-}
-
-func newFormatter(w io.Writer) *formatter {
- return &formatter{w: w}
-}
-
-func (fm *formatter) Write(p []byte) (int, error) {
- if fm.err != nil {
- return len(p), nil
- }
- if _, err := fm.w.Write(p); err != nil {
- fm.err = err
- }
- return len(p), nil
-}
-
-func (fm *formatter) WriteString(s string) (int, error) {
- fm.Write([]byte(s))
- return len(s), nil
-}
-
-func (fm *formatter) WriteByte(c byte) error {
- fm.Write([]byte{c})
- return nil
-}
-
-func (fm *formatter) WriteQuotedName(s string) {
- qpos := 0
- for i := 0; i < len(s); i++ {
- ch := s[i]
- if q, quoted := quoteByte(ch); quoted {
- if qpos == 0 {
- fm.WriteByte('"')
- }
- fm.WriteString(s[qpos:i])
- fm.Write(q)
- qpos = i + 1
- }
- }
- fm.WriteString(s[qpos:])
- if qpos > 0 {
- fm.WriteByte('"')
- }
-}
-
-var quoteEscapeTable = map[byte]byte{
- '\a': 'a',
- '\b': 'b',
- '\t': 't',
- '\n': 'n',
- '\v': 'v',
- '\f': 'f',
- '\r': 'r',
- '"': '"',
- '\\': '\\',
-}
-
-func quoteByte(b byte) ([]byte, bool) {
- if q, ok := quoteEscapeTable[b]; ok {
- return []byte{'\\', q}, true
- }
- if b < 0x20 || b >= 0x7F {
- return []byte{
- '\\',
- '0' + (b>>6)&0o3,
- '0' + (b>>3)&0o7,
- '0' + (b>>0)&0o7,
- }, true
- }
- return nil, false
-}
-
-func (fm *formatter) FormatFile(f *File) {
- fm.WriteString("diff --git ")
-
- var aName, bName string
- switch {
- case f.OldName == "":
- aName = f.NewName
- bName = f.NewName
-
- case f.NewName == "":
- aName = f.OldName
- bName = f.OldName
-
- default:
- aName = f.OldName
- bName = f.NewName
- }
-
- fm.WriteQuotedName("a/" + aName)
- fm.WriteByte(' ')
- fm.WriteQuotedName("b/" + bName)
- fm.WriteByte('\n')
-
- if f.OldMode != 0 {
- if f.IsDelete {
- fmt.Fprintf(fm, "deleted file mode %o\n", f.OldMode)
- } else if f.NewMode != 0 {
- fmt.Fprintf(fm, "old mode %o\n", f.OldMode)
- }
- }
-
- if f.NewMode != 0 {
- if f.IsNew {
- fmt.Fprintf(fm, "new file mode %o\n", f.NewMode)
- } else if f.OldMode != 0 {
- fmt.Fprintf(fm, "new mode %o\n", f.NewMode)
- }
- }
-
- if f.Score > 0 {
- if f.IsCopy || f.IsRename {
- fmt.Fprintf(fm, "similarity index %d%%\n", f.Score)
- } else {
- fmt.Fprintf(fm, "dissimilarity index %d%%\n", f.Score)
- }
- }
-
- if f.IsCopy {
- if f.OldName != "" {
- fm.WriteString("copy from ")
- fm.WriteQuotedName(f.OldName)
- fm.WriteByte('\n')
- }
- if f.NewName != "" {
- fm.WriteString("copy to ")
- fm.WriteQuotedName(f.NewName)
- fm.WriteByte('\n')
- }
- }
-
- if f.IsRename {
- if f.OldName != "" {
- fm.WriteString("rename from ")
- fm.WriteQuotedName(f.OldName)
- fm.WriteByte('\n')
- }
- if f.NewName != "" {
- fm.WriteString("rename to ")
- fm.WriteQuotedName(f.NewName)
- fm.WriteByte('\n')
- }
- }
-
- if f.OldOIDPrefix != "" && f.NewOIDPrefix != "" {
- fmt.Fprintf(fm, "index %s..%s", f.OldOIDPrefix, f.NewOIDPrefix)
-
- // Mode is only included on the index line when it is not changing
- if f.OldMode != 0 && ((f.NewMode == 0 && !f.IsDelete) || f.OldMode == f.NewMode) {
- fmt.Fprintf(fm, " %o", f.OldMode)
- }
-
- fm.WriteByte('\n')
- }
-
- if f.IsBinary {
- if f.BinaryFragment == nil {
- fm.WriteString("Binary files ")
- fm.WriteQuotedName("a/" + aName)
- fm.WriteString(" and ")
- fm.WriteQuotedName("b/" + bName)
- fm.WriteString(" differ\n")
- } else {
- fm.WriteString("GIT binary patch\n")
- fm.FormatBinaryFragment(f.BinaryFragment)
- if f.ReverseBinaryFragment != nil {
- fm.FormatBinaryFragment(f.ReverseBinaryFragment)
- }
- }
- }
-
- // The "---" and "+++" lines only appear for text patches with fragments
- if len(f.TextFragments) > 0 {
- fm.WriteString("--- ")
- if f.OldName == "" {
- fm.WriteString("/dev/null")
- } else {
- fm.WriteQuotedName("a/" + f.OldName)
- }
- fm.WriteByte('\n')
-
- fm.WriteString("+++ ")
- if f.NewName == "" {
- fm.WriteString("/dev/null")
- } else {
- fm.WriteQuotedName("b/" + f.NewName)
- }
- fm.WriteByte('\n')
-
- for _, frag := range f.TextFragments {
- fm.FormatTextFragment(frag)
- }
- }
-}
-
-func (fm *formatter) FormatTextFragment(f *TextFragment) {
- fm.FormatTextFragmentHeader(f)
- fm.WriteByte('\n')
-
- for _, line := range f.Lines {
- fm.WriteString(line.Op.String())
- fm.WriteString(line.Line)
- if line.NoEOL() {
- fm.WriteString("\n\\ No newline at end of file\n")
- }
- }
-}
-
-func (fm *formatter) FormatTextFragmentHeader(f *TextFragment) {
- fmt.Fprintf(fm, "@@ -%d,%d +%d,%d @@", f.OldPosition, f.OldLines, f.NewPosition, f.NewLines)
- if f.Comment != "" {
- fm.WriteByte(' ')
- fm.WriteString(f.Comment)
- }
-}
-
-func (fm *formatter) FormatBinaryFragment(f *BinaryFragment) {
- const (
- maxBytesPerLine = 52
- )
-
- switch f.Method {
- case BinaryPatchDelta:
- fm.WriteString("delta ")
- case BinaryPatchLiteral:
- fm.WriteString("literal ")
- }
- fm.Write(strconv.AppendInt(nil, f.Size, 10))
- fm.WriteByte('\n')
-
- data := deflateBinaryChunk(f.Data)
- n := (len(data) / maxBytesPerLine) * maxBytesPerLine
-
- buf := make([]byte, base85Len(maxBytesPerLine))
- for i := 0; i < n; i += maxBytesPerLine {
- base85Encode(buf, data[i:i+maxBytesPerLine])
- fm.WriteByte('z')
- fm.Write(buf)
- fm.WriteByte('\n')
- }
- if remainder := len(data) - n; remainder > 0 {
- buf = buf[0:base85Len(remainder)]
-
- sizeChar := byte(remainder)
- if remainder <= 26 {
- sizeChar = 'A' + sizeChar - 1
- } else {
- sizeChar = 'a' + sizeChar - 27
- }
-
- base85Encode(buf, data[n:])
- fm.WriteByte(sizeChar)
- fm.Write(buf)
- fm.WriteByte('\n')
- }
- fm.WriteByte('\n')
-}
-
-func deflateBinaryChunk(data []byte) []byte {
- var b bytes.Buffer
-
- zw := zlib.NewWriter(&b)
- _, _ = zw.Write(data)
- _ = zw.Close()
-
- return b.Bytes()
-}
diff --git a/pkg/gitdiff/gitdiff.go b/pkg/gitdiff/gitdiff.go
deleted file mode 100644
index 5365645..0000000
--- a/pkg/gitdiff/gitdiff.go
+++ /dev/null
@@ -1,230 +0,0 @@
-package gitdiff
-
-import (
- "errors"
- "fmt"
- "os"
- "strings"
-)
-
-// File describes changes to a single file. It can be either a text file or a
-// binary file.
-type File struct {
- OldName string
- NewName string
-
- IsNew bool
- IsDelete bool
- IsCopy bool
- IsRename bool
-
- OldMode os.FileMode
- NewMode os.FileMode
-
- OldOIDPrefix string
- NewOIDPrefix string
- Score int
-
- // TextFragments contains the fragments describing changes to a text file. It
- // may be empty if the file is empty or if only the mode changes.
- TextFragments []*TextFragment
-
- // IsBinary is true if the file is a binary file. If the patch includes
- // binary data, BinaryFragment will be non-nil and describe the changes to
- // the data. If the patch is reversible, ReverseBinaryFragment will also be
- // non-nil and describe the changes needed to restore the original file
- // after applying the changes in BinaryFragment.
- IsBinary bool
- BinaryFragment *BinaryFragment
- ReverseBinaryFragment *BinaryFragment
-}
-
-// String returns a git diff representation of this file. The value can be
-// parsed by this library to obtain the same File, but may not be the same as
-// the original input.
-func (f *File) String() string {
- var diff strings.Builder
- newFormatter(&diff).FormatFile(f)
- return diff.String()
-}
-
-// TextFragment describes changed lines starting at a specific line in a text file.
-type TextFragment struct {
- Comment string
-
- OldPosition int64
- OldLines int64
-
- NewPosition int64
- NewLines int64
-
- LinesAdded int64
- LinesDeleted int64
-
- LeadingContext int64
- TrailingContext int64
-
- Lines []Line
-}
-
-// String returns a git diff format of this fragment. See [File.String] for
-// more details on this format.
-func (f *TextFragment) String() string {
- var diff strings.Builder
- newFormatter(&diff).FormatTextFragment(f)
- return diff.String()
-}
-
-// Header returns a git diff header of this fragment. See [File.String] for
-// more details on this format.
-func (f *TextFragment) Header() string {
- var hdr strings.Builder
- newFormatter(&hdr).FormatTextFragmentHeader(f)
- return hdr.String()
-}
-
-// Validate checks that the fragment is self-consistent and appliable. Validate
-// returns an error if and only if the fragment is invalid.
-func (f *TextFragment) Validate() error {
- if f == nil {
- return errors.New("nil fragment")
- }
-
- var (
- oldLines, newLines int64
- leadingContext, trailingContext int64
- contextLines, addedLines, deletedLines int64
- )
-
- // count the types of lines in the fragment content
- for i, line := range f.Lines {
- switch line.Op {
- case OpContext:
- oldLines++
- newLines++
- contextLines++
- if addedLines == 0 && deletedLines == 0 {
- leadingContext++
- } else {
- trailingContext++
- }
- case OpAdd:
- newLines++
- addedLines++
- trailingContext = 0
- case OpDelete:
- oldLines++
- deletedLines++
- trailingContext = 0
- default:
- return fmt.Errorf("unknown operator %q on line %d", line.Op, i+1)
- }
- }
-
- // check the actual counts against the reported counts
- if oldLines != f.OldLines {
- return lineCountErr("old", oldLines, f.OldLines)
- }
- if newLines != f.NewLines {
- return lineCountErr("new", newLines, f.NewLines)
- }
- if leadingContext != f.LeadingContext {
- return lineCountErr("leading context", leadingContext, f.LeadingContext)
- }
- if trailingContext != f.TrailingContext {
- return lineCountErr("trailing context", trailingContext, f.TrailingContext)
- }
- if addedLines != f.LinesAdded {
- return lineCountErr("added", addedLines, f.LinesAdded)
- }
- if deletedLines != f.LinesDeleted {
- return lineCountErr("deleted", deletedLines, f.LinesDeleted)
- }
-
- // if a file is being created, it can only contain additions
- if f.OldPosition == 0 && f.OldLines != 0 {
- return errors.New("file creation fragment contains context or deletion lines")
- }
-
- return nil
-}
-
-func lineCountErr(kind string, actual, reported int64) error {
- return fmt.Errorf("fragment contains %d %s lines but reports %d", actual, kind, reported)
-}
-
-// Line is a line in a text fragment.
-type Line struct {
- Op LineOp
- Line string
-}
-
-func (fl Line) String() string {
- return fl.Op.String() + fl.Line
-}
-
-// Old returns true if the line appears in the old content of the fragment.
-func (fl Line) Old() bool {
- return fl.Op == OpContext || fl.Op == OpDelete
-}
-
-// New returns true if the line appears in the new content of the fragment.
-func (fl Line) New() bool {
- return fl.Op == OpContext || fl.Op == OpAdd
-}
-
-// NoEOL returns true if the line is missing a trailing newline character.
-func (fl Line) NoEOL() bool {
- return len(fl.Line) == 0 || fl.Line[len(fl.Line)-1] != '\n'
-}
-
-// LineOp describes the type of a text fragment line: context, added, or removed.
-type LineOp int
-
-const (
- // OpContext indicates a context line
- OpContext LineOp = iota
- // OpDelete indicates a deleted line
- OpDelete
- // OpAdd indicates an added line
- OpAdd
-)
-
-func (op LineOp) String() string {
- switch op {
- case OpContext:
- return " "
- case OpDelete:
- return "-"
- case OpAdd:
- return "+"
- }
- return "?"
-}
-
-// BinaryFragment describes changes to a binary file.
-type BinaryFragment struct {
- Method BinaryPatchMethod
- Size int64
- Data []byte
-}
-
-// BinaryPatchMethod is the method used to create and apply the binary patch.
-type BinaryPatchMethod int
-
-const (
- // BinaryPatchDelta indicates the data uses Git's packfile encoding
- BinaryPatchDelta BinaryPatchMethod = iota
- // BinaryPatchLiteral indicates the data is the exact file content
- BinaryPatchLiteral
-)
-
-// String returns a git diff format of this fragment. Due to differences in
-// zlib implementation between Go and Git, encoded binary data in the result
-// will likely differ from what Git produces for the same input. See
-// [File.String] for more details on this format.
-func (f *BinaryFragment) String() string {
- var diff strings.Builder
- newFormatter(&diff).FormatBinaryFragment(f)
- return diff.String()
-}
diff --git a/pkg/gitdiff/io.go b/pkg/gitdiff/io.go
deleted file mode 100644
index 8143238..0000000
--- a/pkg/gitdiff/io.go
+++ /dev/null
@@ -1,220 +0,0 @@
-package gitdiff
-
-import (
- "errors"
- "io"
-)
-
-const (
- byteBufferSize = 32 * 1024 // from io.Copy
- lineBufferSize = 32
- indexBufferSize = 1024
-)
-
-// LineReaderAt is the interface that wraps the ReadLinesAt method.
-//
-// ReadLinesAt reads len(lines) into lines starting at line offset. It returns
-// the number of lines read (0 <= n <= len(lines)) and any error encountered.
-// Line numbers are zero-indexed.
-//
-// If n < len(lines), ReadLinesAt returns a non-nil error explaining why more
-// lines were not returned.
-//
-// Lines read by ReadLinesAt include the newline character. The last line does
-// not have a final newline character if the input ends without one.
-type LineReaderAt interface {
- ReadLinesAt(lines [][]byte, offset int64) (n int, err error)
-}
-
-type lineReaderAt struct {
- r io.ReaderAt
- index []int64
- eof bool
-}
-
-func (r *lineReaderAt) ReadLinesAt(lines [][]byte, offset int64) (n int, err error) {
- if offset < 0 {
- return 0, errors.New("ReadLinesAt: negative offset")
- }
- if len(lines) == 0 {
- return 0, nil
- }
-
- count := len(lines)
- startLine := offset
- endLine := startLine + int64(count)
-
- if endLine > int64(len(r.index)) && !r.eof {
- if err := r.indexTo(endLine); err != nil {
- return 0, err
- }
- }
- if startLine >= int64(len(r.index)) {
- return 0, io.EOF
- }
-
- buf, byteOffset, err := r.readBytes(startLine, int64(count))
- if err != nil {
- return 0, err
- }
-
- for n = 0; n < count && startLine+int64(n) < int64(len(r.index)); n++ {
- lineno := startLine + int64(n)
- start, end := int64(0), r.index[lineno]-byteOffset
- if lineno > 0 {
- start = r.index[lineno-1] - byteOffset
- }
- lines[n] = buf[start:end]
- }
-
- if n < count {
- return n, io.EOF
- }
- return n, nil
-}
-
-// indexTo reads data and computes the line index until there is information
-// for line or a read returns io.EOF. It returns an error if and only if there
-// is an error reading data.
-func (r *lineReaderAt) indexTo(line int64) error {
- var buf [indexBufferSize]byte
-
- offset := r.lastOffset()
- for int64(len(r.index)) < line {
- n, err := r.r.ReadAt(buf[:], offset)
- if err != nil && err != io.EOF {
- return err
- }
- for _, b := range buf[:n] {
- offset++
- if b == '\n' {
- r.index = append(r.index, offset)
- }
- }
- if err == io.EOF {
- if offset > r.lastOffset() {
- r.index = append(r.index, offset)
- }
- r.eof = true
- break
- }
- }
- return nil
-}
-
-func (r *lineReaderAt) lastOffset() int64 {
- if n := len(r.index); n > 0 {
- return r.index[n-1]
- }
- return 0
-}
-
-// readBytes reads the bytes of the n lines starting at line and returns the
-// bytes and the offset of the first byte in the underlying source.
-func (r *lineReaderAt) readBytes(line, n int64) (b []byte, offset int64, err error) {
- indexLen := int64(len(r.index))
-
- var size int64
- if line > indexLen {
- offset = r.index[indexLen-1]
- } else if line > 0 {
- offset = r.index[line-1]
- }
- if n > 0 {
- if line+n > indexLen {
- size = r.index[indexLen-1] - offset
- } else {
- size = r.index[line+n-1] - offset
- }
- }
-
- b = make([]byte, size)
- if _, err := r.r.ReadAt(b, offset); err != nil {
- if err == io.EOF {
- err = errors.New("ReadLinesAt: corrupt line index or changed source data")
- }
- return nil, 0, err
- }
- return b, offset, nil
-}
-
-func isLen(r io.ReaderAt, n int64) (bool, error) {
- off := n - 1
- if off < 0 {
- off = 0
- }
-
- var b [2]byte
- nr, err := r.ReadAt(b[:], off)
- if err == io.EOF {
- return (n == 0 && nr == 0) || (n > 0 && nr == 1), nil
- }
- return false, err
-}
-
-// copyFrom writes bytes starting from offset off in src to dst stopping at the
-// end of src or at the first error. copyFrom returns the number of bytes
-// written and any error.
-func copyFrom(dst io.Writer, src io.ReaderAt, off int64) (written int64, err error) {
- buf := make([]byte, byteBufferSize)
- for {
- nr, rerr := src.ReadAt(buf, off)
- if nr > 0 {
- nw, werr := dst.Write(buf[0:nr])
- if nw > 0 {
- written += int64(nw)
- }
- if werr != nil {
- err = werr
- break
- }
- if nr != nw {
- err = io.ErrShortWrite
- break
- }
- off += int64(nr)
- }
- if rerr != nil {
- if rerr != io.EOF {
- err = rerr
- }
- break
- }
- }
- return written, err
-}
-
-// copyLinesFrom writes lines starting from line off in src to dst stopping at
-// the end of src or at the first error. copyLinesFrom returns the number of
-// lines written and any error.
-func copyLinesFrom(dst io.Writer, src LineReaderAt, off int64) (written int64, err error) {
- buf := make([][]byte, lineBufferSize)
-ReadLoop:
- for {
- nr, rerr := src.ReadLinesAt(buf, off)
- if nr > 0 {
- for _, line := range buf[0:nr] {
- nw, werr := dst.Write(line)
- if nw > 0 {
- written++
- }
- if werr != nil {
- err = werr
- break ReadLoop
- }
- if len(line) != nw {
- err = io.ErrShortWrite
- break ReadLoop
- }
- }
- off += int64(nr)
- }
- if rerr != nil {
- if rerr != io.EOF {
- err = rerr
- }
- break
- }
- }
- return written, err
-}
diff --git a/pkg/gitdiff/parser.go b/pkg/gitdiff/parser.go
deleted file mode 100644
index e8f8430..0000000
--- a/pkg/gitdiff/parser.go
+++ /dev/null
@@ -1,142 +0,0 @@
-// Package gitdiff parses and applies patches generated by Git. It supports
-// line-oriented text patches, binary patches, and can also parse standard
-// unified diffs generated by other tools.
-package gitdiff
-
-import (
- "bufio"
- "fmt"
- "io"
-)
-
-// Parse parses a patch with changes to one or more files. Any content before
-// the first file is returned as the second value. If an error occurs while
-// parsing, it returns all files parsed before the error.
-//
-// Parse expects to receive a single patch. If the input may contain multiple
-// patches (for example, if it is an mbox file), callers should split it into
-// individual patches and call Parse on each one.
-func Parse(r io.Reader) ([]*File, string, error) {
- p := newParser(r)
-
- if err := p.Next(); err != nil {
- if err == io.EOF {
- return nil, "", nil
- }
- return nil, "", err
- }
-
- var preamble string
- var files []*File
- for {
- file, pre, err := p.ParseNextFileHeader()
- if err != nil {
- return files, preamble, err
- }
- if len(files) == 0 {
- preamble = pre
- }
- if file == nil {
- break
- }
-
- for _, fn := range []func(*File) (int, error){
- p.ParseTextFragments,
- p.ParseBinaryFragments,
- } {
- n, err := fn(file)
- if err != nil {
- return files, preamble, err
- }
- if n > 0 {
- break
- }
- }
-
- files = append(files, file)
- }
-
- return files, preamble, nil
-}
-
-// TODO(bkeyes): consider exporting the parser type with configuration
-// this would enable OID validation, p-value guessing, and prefix stripping
-// by allowing users to set or override defaults
-
-// parser invariants:
-// - methods that parse objects:
-// - start with the parser on the first line of the first object
-// - if returning nil, do not advance
-// - if returning an error, do not advance past the object
-// - if returning an object, advance to the first line after the object
-// - any exported parsing methods must initialize the parser by calling Next()
-
-type stringReader interface {
- ReadString(delim byte) (string, error)
-}
-
-type parser struct {
- r stringReader
-
- eof bool
- lineno int64
- lines [3]string
-}
-
-func newParser(r io.Reader) *parser {
- if r, ok := r.(stringReader); ok {
- return &parser{r: r}
- }
- return &parser{r: bufio.NewReader(r)}
-}
-
-// Next advances the parser by one line. It returns any error encountered while
-// reading the line, including io.EOF when the end of stream is reached.
-func (p *parser) Next() error {
- if p.eof {
- return io.EOF
- }
-
- if p.lineno == 0 {
- // on first call to next, need to shift in all lines
- for i := 0; i < len(p.lines)-1; i++ {
- if err := p.shiftLines(); err != nil && err != io.EOF {
- return err
- }
- }
- }
-
- err := p.shiftLines()
- if err != nil && err != io.EOF {
- return err
- }
-
- p.lineno++
- if p.lines[0] == "" {
- p.eof = true
- return io.EOF
- }
- return nil
-}
-
-func (p *parser) shiftLines() (err error) {
- for i := 0; i < len(p.lines)-1; i++ {
- p.lines[i] = p.lines[i+1]
- }
- p.lines[len(p.lines)-1], err = p.r.ReadString('\n')
- return
-}
-
-// Line returns a line from the parser without advancing it. A delta of 0
-// returns the current line, while higher deltas return read-ahead lines. It
-// returns an empty string if the delta is higher than the available lines,
-// either because of the buffer size or because the parser reached the end of
-// the input. Valid lines always contain at least a newline character.
-func (p *parser) Line(delta uint) string {
- return p.lines[delta]
-}
-
-// Errorf generates an error and appends the current line information.
-func (p *parser) Errorf(delta int64, msg string, args ...interface{}) error {
- return fmt.Errorf("gitdiff: line %d: %s", p.lineno+delta, fmt.Sprintf(msg, args...))
-}
diff --git a/pkg/gitdiff/patch_header.go b/pkg/gitdiff/patch_header.go
deleted file mode 100644
index f047059..0000000
--- a/pkg/gitdiff/patch_header.go
+++ /dev/null
@@ -1,470 +0,0 @@
-package gitdiff
-
-import (
- "bufio"
- "errors"
- "fmt"
- "io"
- "io/ioutil"
- "mime/quotedprintable"
- "net/mail"
- "strconv"
- "strings"
- "time"
- "unicode"
-)
-
-const (
- mailHeaderPrefix = "From "
- prettyHeaderPrefix = "commit "
- mailMinimumHeaderPrefix = "From:"
-)
-
-// PatchHeader is a parsed version of the preamble content that appears before
-// the first diff in a patch. It includes metadata about the patch, such as the
-// author and a subject.
-type PatchHeader struct {
- // The SHA of the commit the patch was generated from. Empty if the SHA is
- // not included in the header.
- SHA string
-
- // The author details of the patch. If these details are not included in
- // the header, Author is nil and AuthorDate is the zero time.
- Author *PatchIdentity
- AuthorDate time.Time
-
- // The committer details of the patch. If these details are not included in
- // the header, Committer is nil and CommitterDate is the zero time.
- Committer *PatchIdentity
- CommitterDate time.Time
-
- // The title and body of the commit message describing the changes in the
- // patch. Empty if no message is included in the header.
- Title string
- Body string
-
- // If the preamble looks like an email, ParsePatchHeader will
- // remove prefixes such as `Re: ` and `[PATCH v3 5/17]` from the
- // Title and place them here.
- SubjectPrefix string
-
- // If the preamble looks like an email, and it contains a `---`
- // line, that line will be removed and everything after it will be
- // placed in BodyAppendix.
- BodyAppendix string
-}
-
-// Message returns the commit message for the header. The message consists of
-// the title and the body separated by an empty line.
-func (h *PatchHeader) Message() string {
- var msg strings.Builder
- if h != nil {
- msg.WriteString(h.Title)
- if h.Body != "" {
- msg.WriteString("\n\n")
- msg.WriteString(h.Body)
- }
- }
- return msg.String()
-}
-
-// ParsePatchDate parses a patch date string. It returns the parsed time or an
-// error if s has an unknown format. ParsePatchDate supports the iso, rfc,
-// short, raw, unix, and default formats (with local variants) used by the
-// --date flag in Git.
-func ParsePatchDate(s string) (time.Time, error) {
- const (
- isoFormat = "2006-01-02 15:04:05 -0700"
- isoStrictFormat = "2006-01-02T15:04:05-07:00"
- rfc2822Format = "Mon, 2 Jan 2006 15:04:05 -0700"
- shortFormat = "2006-01-02"
- defaultFormat = "Mon Jan 2 15:04:05 2006 -0700"
- defaultLocalFormat = "Mon Jan 2 15:04:05 2006"
- )
-
- if s == "" {
- return time.Time{}, nil
- }
-
- for _, fmt := range []string{
- isoFormat,
- isoStrictFormat,
- rfc2822Format,
- shortFormat,
- defaultFormat,
- defaultLocalFormat,
- } {
- if t, err := time.ParseInLocation(fmt, s, time.Local); err == nil {
- return t, nil
- }
- }
-
- // unix format
- if unix, err := strconv.ParseInt(s, 10, 64); err == nil {
- return time.Unix(unix, 0), nil
- }
-
- // raw format
- if space := strings.IndexByte(s, ' '); space > 0 {
- unix, uerr := strconv.ParseInt(s[:space], 10, 64)
- zone, zerr := time.Parse("-0700", s[space+1:])
- if uerr == nil && zerr == nil {
- return time.Unix(unix, 0).In(zone.Location()), nil
- }
- }
-
- return time.Time{}, fmt.Errorf("unknown date format: %s", s)
-}
-
-// A PatchHeaderOption modifies the behavior of ParsePatchHeader.
-type PatchHeaderOption func(*patchHeaderOptions)
-
-// SubjectCleanMode controls how ParsePatchHeader cleans subject lines when
-// parsing mail-formatted patches.
-type SubjectCleanMode int
-
-const (
- // SubjectCleanWhitespace removes leading and trailing whitespace.
- SubjectCleanWhitespace SubjectCleanMode = iota
-
- // SubjectCleanAll removes leading and trailing whitespace, leading "Re:",
- // "re:", and ":" strings, and leading strings enclosed by '[' and ']'.
- // This is the default behavior of git (see `git mailinfo`) and this
- // package.
- SubjectCleanAll
-
- // SubjectCleanPatchOnly is the same as SubjectCleanAll, but only removes
- // leading strings enclosed by '[' and ']' if they start with "PATCH".
- SubjectCleanPatchOnly
-)
-
-// WithSubjectCleanMode sets the SubjectCleanMode for header parsing. By
-// default, uses SubjectCleanAll.
-func WithSubjectCleanMode(m SubjectCleanMode) PatchHeaderOption {
- return func(opts *patchHeaderOptions) {
- opts.subjectCleanMode = m
- }
-}
-
-type patchHeaderOptions struct {
- subjectCleanMode SubjectCleanMode
-}
-
-// ParsePatchHeader parses the preamble string returned by [Parse] into a
-// PatchHeader. Due to the variety of header formats, some fields of the parsed
-// PatchHeader may be unset after parsing.
-//
-// Supported formats are the short, medium, full, fuller, and email pretty
-// formats used by `git diff`, `git log`, and `git show` and the UNIX mailbox
-// format used by `git format-patch`.
-//
-// When parsing mail-formatted headers, ParsePatchHeader tries to remove
-// email-specific content from the title and body:
-//
-// - Based on the SubjectCleanMode, remove prefixes like reply markers and
-// "[PATCH]" strings from the subject, saving any removed content in the
-// SubjectPrefix field. Parsing always discards leading and trailing
-// whitespace from the subject line. The default mode is SubjectCleanAll.
-//
-// - If the body contains a "---" line (3 hyphens), remove that line and any
-// content after it from the body and save it in the BodyAppendix field.
-//
-// ParsePatchHeader tries to process content it does not understand wthout
-// returning errors, but will return errors if well-identified content like
-// dates or identies uses unknown or invalid formats.
-func ParsePatchHeader(header string, options ...PatchHeaderOption) (*PatchHeader, error) {
- opts := patchHeaderOptions{
- subjectCleanMode: SubjectCleanAll, // match git defaults
- }
- for _, optFn := range options {
- optFn(&opts)
- }
-
- header = strings.TrimSpace(header)
- if header == "" {
- return &PatchHeader{}, nil
- }
-
- var firstLine, rest string
- if idx := strings.IndexByte(header, '\n'); idx >= 0 {
- firstLine = header[:idx]
- rest = header[idx+1:]
- } else {
- firstLine = header
- rest = ""
- }
-
- switch {
- case strings.HasPrefix(firstLine, mailHeaderPrefix):
- return parseHeaderMail(firstLine, strings.NewReader(rest), opts)
-
- case strings.HasPrefix(firstLine, mailMinimumHeaderPrefix):
- // With a minimum header, the first line is part of the actual mail
- // content and needs to be parsed as part of the "rest"
- return parseHeaderMail("", strings.NewReader(header), opts)
-
- case strings.HasPrefix(firstLine, prettyHeaderPrefix):
- return parseHeaderPretty(firstLine, strings.NewReader(rest))
- }
-
- return nil, errors.New("unrecognized patch header format")
-}
-
-func parseHeaderPretty(prettyLine string, r io.Reader) (*PatchHeader, error) {
- const (
- authorPrefix = "Author:"
- commitPrefix = "Commit:"
- datePrefix = "Date:"
- authorDatePrefix = "AuthorDate:"
- commitDatePrefix = "CommitDate:"
- )
-
- h := &PatchHeader{}
-
- prettyLine = strings.TrimPrefix(prettyLine, prettyHeaderPrefix)
- if i := strings.IndexByte(prettyLine, ' '); i > 0 {
- h.SHA = prettyLine[:i]
- } else {
- h.SHA = prettyLine
- }
-
- s := bufio.NewScanner(r)
- for s.Scan() {
- line := s.Text()
-
- // empty line marks end of fields, remaining lines are title/message
- if strings.TrimSpace(line) == "" {
- break
- }
-
- switch {
- case strings.HasPrefix(line, authorPrefix):
- u, err := ParsePatchIdentity(line[len(authorPrefix):])
- if err != nil {
- return nil, err
- }
- h.Author = &u
-
- case strings.HasPrefix(line, commitPrefix):
- u, err := ParsePatchIdentity(line[len(commitPrefix):])
- if err != nil {
- return nil, err
- }
- h.Committer = &u
-
- case strings.HasPrefix(line, datePrefix):
- d, err := ParsePatchDate(strings.TrimSpace(line[len(datePrefix):]))
- if err != nil {
- return nil, err
- }
- h.AuthorDate = d
-
- case strings.HasPrefix(line, authorDatePrefix):
- d, err := ParsePatchDate(strings.TrimSpace(line[len(authorDatePrefix):]))
- if err != nil {
- return nil, err
- }
- h.AuthorDate = d
-
- case strings.HasPrefix(line, commitDatePrefix):
- d, err := ParsePatchDate(strings.TrimSpace(line[len(commitDatePrefix):]))
- if err != nil {
- return nil, err
- }
- h.CommitterDate = d
- }
- }
- if s.Err() != nil {
- return nil, s.Err()
- }
-
- title, indent := scanMessageTitle(s)
- if s.Err() != nil {
- return nil, s.Err()
- }
- h.Title = title
-
- if title != "" {
- // Don't check for an appendix, pretty headers do not contain them
- body, _ := scanMessageBody(s, indent, false)
- if s.Err() != nil {
- return nil, s.Err()
- }
- h.Body = body
- }
-
- return h, nil
-}
-
-func scanMessageTitle(s *bufio.Scanner) (title string, indent string) {
- var b strings.Builder
- for i := 0; s.Scan(); i++ {
- line := s.Text()
- trimLine := strings.TrimSpace(line)
- if trimLine == "" {
- break
- }
-
- if i == 0 {
- if start := strings.IndexFunc(line, func(c rune) bool { return !unicode.IsSpace(c) }); start > 0 {
- indent = line[:start]
- }
- }
- if b.Len() > 0 {
- b.WriteByte(' ')
- }
- b.WriteString(trimLine)
- }
- return b.String(), indent
-}
-
-func scanMessageBody(s *bufio.Scanner, indent string, separateAppendix bool) (string, string) {
- // Body and appendix
- var body, appendix strings.Builder
- c := &body
- var empty int
- for i := 0; s.Scan(); i++ {
- line := s.Text()
-
- line = strings.TrimRightFunc(line, unicode.IsSpace)
- line = strings.TrimPrefix(line, indent)
-
- if line == "" {
- empty++
- continue
- }
-
- // If requested, parse out "appendix" information (often added
- // by `git format-patch` and removed by `git am`).
- if separateAppendix && c == &body && line == "---" {
- c = &appendix
- continue
- }
-
- if c.Len() > 0 {
- c.WriteByte('\n')
- if empty > 0 {
- c.WriteByte('\n')
- }
- }
- empty = 0
-
- c.WriteString(line)
- }
- return body.String(), appendix.String()
-}
-
-func parseHeaderMail(mailLine string, r io.Reader, opts patchHeaderOptions) (*PatchHeader, error) {
- msg, err := mail.ReadMessage(r)
- if err != nil {
- return nil, err
- }
-
- h := &PatchHeader{}
-
- if strings.HasPrefix(mailLine, mailHeaderPrefix) {
- mailLine = strings.TrimPrefix(mailLine, mailHeaderPrefix)
- if i := strings.IndexByte(mailLine, ' '); i > 0 {
- h.SHA = mailLine[:i]
- }
- }
-
- from := msg.Header.Get("From")
- if from != "" {
- u, err := ParsePatchIdentity(from)
- if err != nil {
- return nil, err
- }
- h.Author = &u
- }
-
- date := msg.Header.Get("Date")
- if date != "" {
- d, err := ParsePatchDate(date)
- if err != nil {
- return nil, err
- }
- h.AuthorDate = d
- }
-
- subject := msg.Header.Get("Subject")
- h.SubjectPrefix, h.Title = cleanSubject(subject, opts.subjectCleanMode)
-
- s := bufio.NewScanner(msg.Body)
- h.Body, h.BodyAppendix = scanMessageBody(s, "", true)
- if s.Err() != nil {
- return nil, s.Err()
- }
-
- return h, nil
-}
-
-func cleanSubject(s string, mode SubjectCleanMode) (prefix string, subject string) {
- switch mode {
- case SubjectCleanAll, SubjectCleanPatchOnly:
- case SubjectCleanWhitespace:
- return "", strings.TrimSpace(decodeSubject(s))
- default:
- panic(fmt.Sprintf("unknown clean mode: %d", mode))
- }
-
- // Based on the algorithm from Git in mailinfo.c:cleanup_subject()
- // If compatibility with `git am` drifts, go there to see if there are any updates.
-
- at := 0
- for at < len(s) {
- switch s[at] {
- case 'r', 'R':
- // Detect re:, Re:, rE: and RE:
- if at+2 < len(s) && (s[at+1] == 'e' || s[at+1] == 'E') && s[at+2] == ':' {
- at += 3
- continue
- }
-
- case ' ', '\t', ':':
- // Delete whitespace and duplicate ':' characters
- at++
- continue
-
- case '[':
- if i := strings.IndexByte(s[at:], ']'); i > 0 {
- if mode == SubjectCleanAll || strings.Contains(s[at:at+i+1], "PATCH") {
- at += i + 1
- continue
- }
- }
- }
-
- // Nothing was removed, end processing
- break
- }
-
- prefix = strings.TrimLeftFunc(s[:at], unicode.IsSpace)
- subject = strings.TrimRightFunc(decodeSubject(s[at:]), unicode.IsSpace)
- return
-}
-
-// Decodes a subject line. Currently only supports quoted-printable UTF-8. This format is the result
-// of a `git format-patch` when the commit title has a non-ASCII character (i.e. an emoji).
-// See for reference: https://stackoverflow.com/questions/27695749/gmail-api-not-respecting-utf-encoding-in-subject
-func decodeSubject(encoded string) string {
- if !strings.HasPrefix(encoded, "=?UTF-8?q?") {
- // not UTF-8 encoded
- return encoded
- }
-
- // If the subject is too long, `git format-patch` may produce a subject line across
- // multiple lines. When parsed, this can look like the following:
- // <UTF8-prefix><first-line> <UTF8-prefix><second-line>
- payload := " " + encoded
- payload = strings.ReplaceAll(payload, " =?UTF-8?q?", "")
- payload = strings.ReplaceAll(payload, "?=", "")
-
- decoded, err := ioutil.ReadAll(quotedprintable.NewReader(strings.NewReader(payload)))
- if err != nil {
- // if err, abort decoding and return original subject
- return encoded
- }
-
- return string(decoded)
-}
diff --git a/pkg/gitdiff/patch_identity.go b/pkg/gitdiff/patch_identity.go
deleted file mode 100644
index 018f80c..0000000
--- a/pkg/gitdiff/patch_identity.go
+++ /dev/null
@@ -1,166 +0,0 @@
-package gitdiff
-
-import (
- "fmt"
- "strings"
-)
-
-// PatchIdentity identifies a person who authored or committed a patch.
-type PatchIdentity struct {
- Name string
- Email string
-}
-
-func (i PatchIdentity) String() string {
- name := i.Name
- if name == "" {
- name = `""`
- }
- return fmt.Sprintf("%s <%s>", name, i.Email)
-}
-
-// ParsePatchIdentity parses a patch identity string. A patch identity contains
-// an email address and an optional name in [RFC 5322] format. This is either a
-// plain email adddress or a name followed by an address in angle brackets:
-//
-// author@example.com
-// Author Name <author@example.com>
-//
-// If the input is not one of these formats, ParsePatchIdentity applies a
-// heuristic to separate the name and email portions. If both the name and
-// email are missing or empty, ParsePatchIdentity returns an error. It
-// otherwise does not validate the result.
-//
-// [RFC 5322]: https://datatracker.ietf.org/doc/html/rfc5322
-func ParsePatchIdentity(s string) (PatchIdentity, error) {
- s = normalizeSpace(s)
- s = unquotePairs(s)
-
- var name, email string
- if at := strings.IndexByte(s, '@'); at >= 0 {
- start, end := at, at
- for start >= 0 && !isRFC5332Space(s[start]) && s[start] != '<' {
- start--
- }
- for end < len(s) && !isRFC5332Space(s[end]) && s[end] != '>' {
- end++
- }
- email = s[start+1 : end]
-
- // Adjust the boundaries so that we drop angle brackets, but keep
- // spaces when removing the email to form the name.
- if start < 0 || s[start] != '<' {
- start++
- }
- if end >= len(s) || s[end] != '>' {
- end--
- }
- name = s[:start] + s[end+1:]
- } else {
- start, end := 0, 0
- for i := 0; i < len(s); i++ {
- if s[i] == '<' && start == 0 {
- start = i + 1
- }
- if s[i] == '>' && start > 0 {
- end = i
- break
- }
- }
- if start > 0 && end >= start {
- email = strings.TrimSpace(s[start:end])
- name = s[:start-1]
- }
- }
-
- // After extracting the email, the name might contain extra whitespace
- // again and may be surrounded by comment characters. The git source gives
- // these examples of when this can happen:
- //
- // "Name <email@domain>"
- // "email@domain (Name)"
- // "Name <email@domain> (Comment)"
- //
- name = normalizeSpace(name)
- if strings.HasPrefix(name, "(") && strings.HasSuffix(name, ")") {
- name = name[1 : len(name)-1]
- }
- name = strings.TrimSpace(name)
-
- // If the name is empty or contains email-like characters, use the email
- // instead (assuming one exists)
- if name == "" || strings.ContainsAny(name, "@<>") {
- name = email
- }
-
- if name == "" && email == "" {
- return PatchIdentity{}, fmt.Errorf("invalid identity string %q", s)
- }
- return PatchIdentity{Name: name, Email: email}, nil
-}
-
-// unquotePairs process the RFC5322 tokens "quoted-string" and "comment" to
-// remove any "quoted-pairs" (backslash-espaced characters). It also removes
-// the quotes from any quoted strings, but leaves the comment delimiters.
-func unquotePairs(s string) string {
- quote := false
- comments := 0
- escaped := false
-
- var out strings.Builder
- for i := 0; i < len(s); i++ {
- if escaped {
- escaped = false
- } else {
- switch s[i] {
- case '\\':
- // quoted-pair is only allowed in quoted-string/comment
- if quote || comments > 0 {
- escaped = true
- continue // drop '\' character
- }
-
- case '"':
- if comments == 0 {
- quote = !quote
- continue // drop '"' character
- }
-
- case '(':
- if !quote {
- comments++
- }
- case ')':
- if comments > 0 {
- comments--
- }
- }
- }
- out.WriteByte(s[i])
- }
- return out.String()
-}
-
-// normalizeSpace trims leading and trailing whitespace from s and converts
-// inner sequences of one or more whitespace characters to single spaces.
-func normalizeSpace(s string) string {
- var sb strings.Builder
- for i := 0; i < len(s); i++ {
- c := s[i]
- if !isRFC5332Space(c) {
- if sb.Len() > 0 && isRFC5332Space(s[i-1]) {
- sb.WriteByte(' ')
- }
- sb.WriteByte(c)
- }
- }
- return sb.String()
-}
-
-func isRFC5332Space(c byte) bool {
- switch c {
- case '\t', '\n', '\r', ' ':
- return true
- }
- return false
-}
diff --git a/pkg/gitdiff/text.go b/pkg/gitdiff/text.go
deleted file mode 100644
index ee30792..0000000
--- a/pkg/gitdiff/text.go
+++ /dev/null
@@ -1,192 +0,0 @@
-package gitdiff
-
-import (
- "fmt"
- "io"
- "strconv"
- "strings"
-)
-
-// ParseTextFragments parses text fragments until the next file header or the
-// end of the stream and attaches them to the given file. It returns the number
-// of fragments that were added.
-func (p *parser) ParseTextFragments(f *File) (n int, err error) {
- for {
- frag, err := p.ParseTextFragmentHeader()
- if err != nil {
- return n, err
- }
- if frag == nil {
- return n, nil
- }
-
- if f.IsNew && frag.OldLines > 0 {
- return n, p.Errorf(-1, "new file depends on old contents")
- }
- if f.IsDelete && frag.NewLines > 0 {
- return n, p.Errorf(-1, "deleted file still has contents")
- }
-
- if err := p.ParseTextChunk(frag); err != nil {
- return n, err
- }
-
- f.TextFragments = append(f.TextFragments, frag)
- n++
- }
-}
-
-func (p *parser) ParseTextFragmentHeader() (*TextFragment, error) {
- const (
- startMark = "@@ -"
- endMark = " @@"
- )
-
- if !strings.HasPrefix(p.Line(0), startMark) {
- return nil, nil
- }
-
- parts := strings.SplitAfterN(p.Line(0), endMark, 2)
- if len(parts) < 2 {
- return nil, p.Errorf(0, "invalid fragment header")
- }
-
- f := &TextFragment{}
- f.Comment = strings.TrimSpace(parts[1])
-
- header := parts[0][len(startMark) : len(parts[0])-len(endMark)]
- ranges := strings.Split(header, " +")
- if len(ranges) != 2 {
- return nil, p.Errorf(0, "invalid fragment header")
- }
-
- var err error
- if f.OldPosition, f.OldLines, err = parseRange(ranges[0]); err != nil {
- return nil, p.Errorf(0, "invalid fragment header: %v", err)
- }
- if f.NewPosition, f.NewLines, err = parseRange(ranges[1]); err != nil {
- return nil, p.Errorf(0, "invalid fragment header: %v", err)
- }
-
- if err := p.Next(); err != nil && err != io.EOF {
- return nil, err
- }
- return f, nil
-}
-
-func (p *parser) ParseTextChunk(frag *TextFragment) error {
- if p.Line(0) == "" {
- return p.Errorf(0, "no content following fragment header")
- }
-
- oldLines, newLines := frag.OldLines, frag.NewLines
- for oldLines > 0 || newLines > 0 {
- line := p.Line(0)
- op, data := line[0], line[1:]
-
- switch op {
- case '\n':
- data = "\n"
- fallthrough // newer GNU diff versions create empty context lines
- case ' ':
- oldLines--
- newLines--
- if frag.LinesAdded == 0 && frag.LinesDeleted == 0 {
- frag.LeadingContext++
- } else {
- frag.TrailingContext++
- }
- frag.Lines = append(frag.Lines, Line{OpContext, data})
- case '-':
- oldLines--
- frag.LinesDeleted++
- frag.TrailingContext = 0
- frag.Lines = append(frag.Lines, Line{OpDelete, data})
- case '+':
- newLines--
- frag.LinesAdded++
- frag.TrailingContext = 0
- frag.Lines = append(frag.Lines, Line{OpAdd, data})
- case '\\':
- // this may appear in middle of fragment if it's for a deleted line
- if isNoNewlineMarker(line) {
- removeLastNewline(frag)
- break
- }
- fallthrough
- default:
- // TODO(bkeyes): if this is because we hit the next header, it
- // would be helpful to return the miscounts line error. We could
- // either test for the common headers ("@@ -", "diff --git") or
- // assume any invalid op ends the fragment; git returns the same
- // generic error in all cases so either is compatible
- return p.Errorf(0, "invalid line operation: %q", op)
- }
-
- if err := p.Next(); err != nil {
- if err == io.EOF {
- break
- }
- return err
- }
- }
-
- if oldLines != 0 || newLines != 0 {
- hdr := max(frag.OldLines-oldLines, frag.NewLines-newLines) + 1
- return p.Errorf(-hdr, "fragment header miscounts lines: %+d old, %+d new", -oldLines, -newLines)
- }
- if frag.LinesAdded == 0 && frag.LinesDeleted == 0 {
- return p.Errorf(0, "fragment contains no changes")
- }
-
- // check for a final "no newline" marker since it is not included in the
- // counters used to stop the loop above
- if isNoNewlineMarker(p.Line(0)) {
- removeLastNewline(frag)
- if err := p.Next(); err != nil && err != io.EOF {
- return err
- }
- }
-
- return nil
-}
-
-func isNoNewlineMarker(s string) bool {
- // test for "\ No newline at end of file" by prefix because the text
- // changes by locale (git claims all versions are at least 12 chars)
- return len(s) >= 12 && s[:2] == "\\ "
-}
-
-func removeLastNewline(frag *TextFragment) {
- if len(frag.Lines) > 0 {
- last := &frag.Lines[len(frag.Lines)-1]
- last.Line = strings.TrimSuffix(last.Line, "\n")
- }
-}
-
-func parseRange(s string) (start int64, end int64, err error) {
- parts := strings.SplitN(s, ",", 2)
-
- if start, err = strconv.ParseInt(parts[0], 10, 64); err != nil {
- nerr := err.(*strconv.NumError)
- return 0, 0, fmt.Errorf("bad start of range: %s: %v", parts[0], nerr.Err)
- }
-
- if len(parts) > 1 {
- if end, err = strconv.ParseInt(parts[1], 10, 64); err != nil {
- nerr := err.(*strconv.NumError)
- return 0, 0, fmt.Errorf("bad end of range: %s: %v", parts[1], nerr.Err)
- }
- } else {
- end = 1
- }
-
- return
-}
-
-func max(a, b int64) int64 {
- if a > b {
- return a
- }
- return b
-}
diff --git a/pkg/links/links.go b/pkg/links/links.go
deleted file mode 100644
index 0795dfc..0000000
--- a/pkg/links/links.go
+++ /dev/null
@@ -1,179 +0,0 @@
-package links
-
-import (
- "bytes"
- "net/url"
- "path"
- "strings"
-
- "golang.org/x/net/html"
-
- "mokhan.ca/antonmedv/gitmal/pkg/git"
-)
-
-type Set map[string]struct{}
-
-func BuildDirSet(files []git.Blob) Set {
- dirs := make(Set)
- for _, f := range files {
- dir := path.Dir(f.Path)
- for dir != "." && dir != "/" {
- if _, ok := dirs[dir]; ok {
- break
- }
- dirs[dir] = struct{}{}
- if i := strings.LastIndex(dir, "/"); i != -1 {
- dir = dir[:i]
- } else {
- break
- }
- }
- }
- return dirs
-}
-
-func BuildFileSet(files []git.Blob) Set {
- filesSet := make(Set)
- for _, f := range files {
- filesSet[f.Path] = struct{}{}
- }
- return filesSet
-}
-
-func Resolve(content, currentPath, rootHref, ref string, dirs, files Set) string {
- doc, err := html.Parse(strings.NewReader(content))
- if err != nil {
- return content
- }
-
- baseDir := path.Dir(currentPath)
-
- var walk func(*html.Node)
- walk = func(n *html.Node) {
- if n.Type == html.ElementNode {
- switch n.Data {
- case "a":
- for i, attr := range n.Attr {
- if attr.Key == "href" {
- newHref := transformHref(attr.Val, baseDir, rootHref, ref, files, dirs)
- n.Attr[i].Val = newHref
- break
- }
- }
- case "img":
- for i, attr := range n.Attr {
- if attr.Key == "src" {
- newSrc := transformImgSrc(attr.Val, baseDir, rootHref, ref)
- n.Attr[i].Val = newSrc
- break
- }
- }
- }
- }
-
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- walk(c)
- }
- }
- walk(doc)
-
- var buf bytes.Buffer
- if err := html.Render(&buf, doc); err != nil {
- return content
- }
-
- return buf.String()
-}
-
-func transformHref(href, baseDir, rootHref, ref string, files, dirs Set) string {
- if href == "" {
- return href
- }
- if strings.HasPrefix(href, "#") {
- return href
- }
-
- u, err := url.Parse(href)
- if err != nil {
- return href
- }
-
- // Absolute URLs are left untouched
- if u.IsAbs() {
- return href
- }
-
- // Skip mailto:, javascript:, data: etc. (url.Parse sets Scheme)
- if u.Scheme != "" {
- return href
- }
-
- // Resolve against the directory of the current file
- relPath := u.Path
- if relPath == "" {
- return href
- }
-
- var repoPath string
-
- if strings.HasPrefix(relPath, "/") {
- // Root-relative repo path
- relPath = strings.TrimPrefix(relPath, "/")
- repoPath = path.Clean(relPath)
- } else {
- // Relative to current file directory
- repoPath = path.Clean(path.Join(baseDir, relPath))
- }
-
- // Decide if this is a file or a directory in the repo
- var newPath string
-
- // 1) Exact file match
- if _, ok := files[repoPath]; ok {
- newPath = repoPath + ".html"
- } else if _, ok := files[repoPath+".md"]; ok {
- // 2) Maybe the link omitted ".md" but the repo has it
- newPath = repoPath + ".md.html"
- } else if _, ok := dirs[repoPath]; ok {
- // 3) Directory: add /index.html
- newPath = path.Join(repoPath, "index.html")
- } else {
- // Unknown target, leave as-is
- return href
- }
-
- // Link from the root href
- newPath = path.Join(rootHref, "blob", ref, newPath)
-
- // Preserve any query/fragment if they existed
- u.Path = newPath
- return u.String()
-}
-
-func transformImgSrc(src, baseDir, rootHref, ref string) string {
- u, err := url.Parse(src)
- if err != nil {
- return src
- }
-
- if u.IsAbs() {
- return src
- }
-
- relPath := u.Path
-
- var repoPath string
- if strings.HasPrefix(relPath, "/") {
- // Root-relative: drop leading slash
- repoPath = strings.TrimPrefix(relPath, "/")
- } else {
- // Resolve against current file directory
- repoPath = path.Clean(path.Join(baseDir, relPath))
- }
-
- final := path.Join(rootHref, "raw", ref, repoPath)
-
- // Preserve any query/fragment if they existed
- u.Path = final
- return u.String()
-}
diff --git a/pkg/progress_bar/progress_bar.go b/pkg/progress_bar/progress_bar.go
deleted file mode 100644
index 8f39ec5..0000000
--- a/pkg/progress_bar/progress_bar.go
+++ /dev/null
@@ -1,84 +0,0 @@
-package progress_bar
-
-import (
- "fmt"
- "os"
- "strings"
- "sync"
- "sync/atomic"
- "time"
-)
-
-type ProgressBar struct {
- label string
- total int64
- current int64
- stop chan struct{}
- wg sync.WaitGroup
-}
-
-func NewProgressBar(label string, total int) *ProgressBar {
- p := &ProgressBar{label: label, total: int64(total), stop: make(chan struct{})}
- p.wg.Add(1)
- go func() {
- defer p.wg.Done()
- ticker := time.NewTicker(100 * time.Millisecond)
- defer ticker.Stop()
- // initial draw
- p.draw(atomic.LoadInt64(&p.current))
- for {
- select {
- case <-p.stop:
- return
- case <-ticker.C:
- cur := atomic.LoadInt64(&p.current)
- if cur > p.total {
- cur = p.total
- }
- p.draw(cur)
- }
- }
- }()
- return p
-}
-
-func (p *ProgressBar) Inc() {
- for {
- cur := atomic.LoadInt64(&p.current)
- if cur >= p.total {
- return
- }
- if atomic.CompareAndSwapInt64(&p.current, cur, cur+1) {
- return
- }
- // retry on race
- }
-}
-
-func (p *ProgressBar) Done() {
- atomic.StoreInt64(&p.current, p.total)
- close(p.stop)
- p.wg.Wait()
- p.draw(p.total)
- _, _ = fmt.Fprintln(os.Stderr)
-}
-
-func (p *ProgressBar) draw(current int64) {
- if p.total <= 0 {
- return
- }
- percent := 0
- if p.total > 0 {
- percent = int(current * 100 / p.total)
- }
- barLen := 24
- filled := 0
- if p.total > 0 {
- filled = int(current * int64(barLen) / p.total)
- }
- if filled > barLen {
- filled = barLen
- }
- bar := strings.Repeat("#", filled) + strings.Repeat(" ", barLen-filled)
- _, _ = fmt.Fprintf(os.Stderr, "\r[%s] %4d/%-4d (%3d%%) %s", bar, current, p.total, percent, p.label)
-}
diff --git a/pkg/templates/blob.gohtml b/pkg/templates/blob.gohtml
deleted file mode 100644
index b98e2b3..0000000
--- a/pkg/templates/blob.gohtml
+++ /dev/null
@@ -1,72 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.BlobParams */ -}}
-{{ define "head" }}
- <style>
- {{ .CSS }}
-
- [id] {
- scroll-margin-top: var(--header-height);
- }
-
- pre {
- border: 1px solid var(--c-border);
- border-top: none;
- border-bottom-left-radius: var(--border-radius);
- border-bottom-right-radius: var(--border-radius);
- }
-
- pre {
- margin: 0;
- padding: 8px 16px;
- overflow-x: auto;
- white-space: pre;
- word-spacing: normal;
- word-break: normal;
- word-wrap: normal;
- tab-size: 4;
- font-family: var(--font-family-mono), monospace;
- }
-
- pre > code {
- display: block;
- padding: 0 16px;
- width: fit-content;
- min-width: 100%;
- line-height: var(--code-line-height);
- font-size: var(--code-font-size);
- }
-
- .border {
- border: 1px solid var(--c-border);
- border-top: none;
- border-bottom-left-radius: 6px;
- border-bottom-right-radius: 6px;
- }
-
- .binary-file {
- padding: 8px 16px;
- font-style: italic;
- }
-
- .image {
- padding: 8px 16px;
- text-align: center;
- }
-
- .image img {
- max-width: 100%;
- height: auto;
- }
- </style>
-{{ end }}
-
-{{ define "body" }}
- {{ template "header" . }}
- {{ if .IsImage }}
- <div class="image border">{{.Content}}</div>
- {{ else if .IsBinary}}
- <div class="binary-file border">Binary file</div>
- {{ else }}
- {{ .Content }}
- {{ end }}
-{{ end }}
-
diff --git a/pkg/templates/branches.gohtml b/pkg/templates/branches.gohtml
deleted file mode 100644
index 743461d..0000000
--- a/pkg/templates/branches.gohtml
+++ /dev/null
@@ -1,58 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.BranchesParams*/ -}}
-{{ define "head" }}
- <style>
- .branches {
- border: 1px solid var(--c-border);
- border-radius: var(--border-radius);
- overflow: hidden;
- }
-
- .branch-row {
- display: flex;
- align-items: center;
- gap: 12px;
- height: 44px;
- padding-inline: 16px;
- border-bottom: 1px solid var(--c-border);
- background-color: var(--c-bg-elv);
- }
-
- .branch-row:last-child {
- border-bottom: none;
- }
-
- .branch-row a {
- color: var(--c-text-1);
- }
-
- .branch-row a:hover {
- color: var(--c-brand-2);
- text-decoration: none;
- }
-
- .badge {
- font-size: 12px;
- color: var(--c-text-2);
- border: 1px solid var(--c-border);
- padding: 2px 6px;
- border-radius: 999px;
- }
- </style>
-{{ end }}
-
-{{ define "body" }}
- <h1>Branches</h1>
- <div class="branches">
- {{ if .Branches }}
- {{ range .Branches }}
- <div class="branch-row">
- <a href="{{ .Href }}">{{ .Name }}</a>
- {{ if .IsDefault }}<span class="badge">default</span>{{ end }}
- <a href="{{ .CommitsHref }}" style="margin-left:auto; font-size:13px; color: var(--c-text-2)">commits →</a>
- </div>
- {{ end }}
- {{ else }}
- <div class="branch-row">(no branches)</div>
- {{ end }}
- </div>
-{{ end }}
diff --git a/pkg/templates/commit.gohtml b/pkg/templates/commit.gohtml
deleted file mode 100644
index 3176f97..0000000
--- a/pkg/templates/commit.gohtml
+++ /dev/null
@@ -1,314 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.CommitParams*/ -}}
-{{ define "head" }}
- <style>
- h1 code {
- border-radius: var(--border-radius);
- background: var(--c-bg-alt);
- padding: 4px 8px;
- font-family: var(--font-family-mono), monospace;
- font-weight: 500;
- }
-
- .commit {
- display: flex;
- flex-direction: column;
- gap: 16px;
- margin-bottom: 16px;
- }
-
- .commit-info {
- display: flex;
- flex-direction: row;
- gap: 8px;
- align-items: flex-end;
- }
-
- .commit-author {
- color: var(--c-text-1);
- font-weight: 600;
- min-width: 0;
- overflow-wrap: break-word;
- }
-
- .commit-date {
- color: var(--c-text-2);
- font-size: 12px;
- font-family: var(--font-family-mono), monospace;
- }
-
- .commit-message {
- border: 1px solid var(--c-border);
- border-radius: var(--border-radius);
- padding: 16px;
- }
-
- .ref-badges {
- display: inline-flex;
- gap: 6px;
- margin-left: 8px;
- }
-
- .badge {
- font-size: 12px;
- color: var(--c-text-2);
- border: 1px solid var(--c-border);
- padding: 2px 6px;
- border-radius: 999px;
- white-space: nowrap;
- }
-
- .commit-subject {
- font-size: 16px;
- font-weight: 600;
- line-height: 1.3;
- hyphens: auto;
- }
-
- .commit-body {
- margin-top: 16px;
- white-space: pre-wrap;
- line-height: 1.5;
- hyphens: auto;
- }
-
- .commit-subinfo {
- display: flex;
- flex-direction: row;
- gap: 16px;
- justify-content: space-between;
- }
-
- .commit-layout {
- display: grid;
- grid-template-columns: 1fr;
- gap: 16px;
- }
-
- @media (min-width: 960px) {
- .commit-layout {
- grid-template-columns: 300px 1fr;
- align-items: start;
- }
-
- .files-tree {
- position: sticky;
- top: 16px;
- }
-
- .files-tree-content {
- max-height: calc(100vh - var(--header-height) - 40px);
- overflow: auto;
- }
- }
-
- .files-tree {
- border: 1px solid var(--c-border);
- border-radius: var(--border-radius);
- }
-
- .files-tree-header {
- display: flex;
- flex-direction: row;
- align-items: center;
- padding-inline: 16px;
- height: var(--header-height);
- border-bottom: 1px solid var(--c-border);
- border-top-left-radius: var(--border-radius);
- border-top-right-radius: var(--border-radius);
- background: var(--c-bg-alt);
- font-size: 14px;
- font-weight: 600;
- }
-
- .files-tree-content {
- display: block;
- padding-block: 6px;
- }
-
- .tree .children {
- margin-left: 16px;
- border-left: 1px dashed var(--c-border);
- }
-
- .tree .node {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 10px 16px;
- }
-
- .tree .file-name {
- flex: 1;
- font-weight: 500;
- color: var(--c-text-1);
- cursor: pointer;
- }
-
- .tree .file-name:hover {
- color: var(--c-brand-2);
- text-decoration: underline;
- }
-
- .tree .dir {
- color: var(--c-dir);
- }
-
- .tree .file-added {
- color: var(--c-green);
- }
-
- .tree .file-deleted {
- color: var(--c-red);
- }
-
- .tree .file-renamed {
- color: var(--c-yellow);
- }
-
- /* Per-file sections */
-
- .files {
- min-width: 0;
- }
-
- .file-section + .file-section {
- margin-top: 16px;
- }
-
- pre {
- border: 1px solid var(--c-border);
- border-top: none;
- border-bottom-left-radius: var(--border-radius);
- border-bottom-right-radius: var(--border-radius);
- }
-
- pre {
- margin: 0;
- padding: 8px 16px;
- overflow-x: auto;
- white-space: pre;
- word-spacing: normal;
- word-break: normal;
- word-wrap: normal;
- tab-size: 4;
- font-family: var(--font-family-mono), monospace;
- }
-
- pre > code {
- display: block;
- padding: 0 16px;
- width: fit-content;
- min-width: 100%;
- line-height: var(--code-line-height);
- font-size: var(--code-font-size);
- }
-
- .binary-file {
- padding: 8px 16px;
- font-style: italic;
- }
-
- .border {
- border: 1px solid var(--c-border);
- border-top: none;
- border-bottom-left-radius: 6px;
- border-bottom-right-radius: 6px;
- }
-
- {{ .DiffCSS }}
- </style>
-{{ end }}
-
-{{ define "body" }}
- <h1>Commit <code>{{ .Commit.ShortHash }}</code></h1>
-
- <div class="commit">
- <div class="commit-info">
- <div class="commit-author">{{ .Commit.Author }} &lt;{{ .Commit.Email }}&gt;</div>
- <div class="commit-date">{{ .Commit.Date | FormatDate }}</div>
- </div>
- <div class="commit-message">
- <div class="commit-subject">
- {{ .Commit.Subject }}
- {{ if .Commit.RefNames }}
- <span class="ref-badges">
- {{ range .Commit.RefNames }}
- {{ if or (eq .Kind "Branch") (eq .Kind "Tag") }}
- <span class="badge">{{ if eq .Kind "Tag" }}tag: {{ end }}{{ .Name }}</span>
- {{ end }}
- {{ end }}
- </span>
- {{ end }}
- </div>
- {{ if .Commit.Body }}
- <div class="commit-body">{{ .Commit.Body }}</div>
- {{ end }}
- </div>
- <div class="commit-subinfo">
- <div class="commit-branch">
- {{ if .Commit.Branch }}
- <a href="../commits/{{ .Commit.Branch.DirName }}/index.html" class="badge">{{ .Commit.Branch }}</a>
- {{ end }}
- </div>
- {{ if .Commit.Parents }}
- <div class="commit-parents">
- {{ if eq (len .Commit.Parents) 1 }}
- 1 parent
- {{ else }}
- {{ .Commit.Parents | len }} parents
- {{ end }}
- {{ range $i, $p := .Commit.Parents }}
- {{ if gt $i 0 }}, {{ end }}
- <a href="{{ $p }}.html"><code>{{ $p | ShortHash }}</code></a>
- {{ end }}
- </div>
- {{ end }}
- </div>
- </div>
-
- <div class="commit-layout">
- <div class="files-tree">
- <div class="files-tree-header">Changed files ({{ len .FileViews }})</div>
- <div class="files-tree-content">
- {{ if .FileTree }}
- <div class="tree">
- {{ template "file_tree" (FileTreeParams .FileTree) }}
- </div>
- {{ else }}
- <div style="color: var(--c-text-3)">(no files)</div>
- {{ end }}
- </div>
- </div>
-
- <div class="files">
- {{ range .FileViews }}
- <section id="{{ .Path }}" class="file-section">
- <div class="header-container">
- <header class="file-header">
- {{ if .IsRename }}
- <div class="path">{{ .OldName }} → {{ .NewName }}</div>
- {{ else }}
- <div class="path">{{ .Path }}</div>
- {{ end }}
- <button class="goto-top" onclick="window.scrollTo({top: 0, behavior: 'smooth'})">
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#arrow-top"></use>
- </svg>
- Top
- </button>
- </header>
- </div>
- {{ if .IsBinary }}
- <div class="binary-file border">Binary file</div>
- {{ else if (and .IsRename (not .HasChanges)) }}
- <div class="binary-file border">File renamed without changes</div>
- {{ else }}
- <div class="file-diff">
- {{ .HTML }}
- </div>
- {{ end }}
- </section>
- {{ end }}
- </div>
- </div>
-{{ end }}
diff --git a/pkg/templates/commits_list.gohtml b/pkg/templates/commits_list.gohtml
deleted file mode 100644
index 76dd43c..0000000
--- a/pkg/templates/commits_list.gohtml
+++ /dev/null
@@ -1,203 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.CommitsListParams*/ -}}
-{{ define "head" }}
- <style>
- .commits {
- border: 1px solid var(--c-border);
- border-top: none;
- border-bottom-left-radius: var(--border-radius);
- border-bottom-right-radius: var(--border-radius);
- overflow-x: auto;
- }
-
- .row {
- display: flex;
- height: 40px;
- border-bottom: 1px solid var(--c-border);
- padding-inline: 16px;
- gap: 16px;
- }
-
- .row:last-child {
- border-bottom: none;
- }
-
- .row:hover {
- background-color: var(--c-bg-alt);
- }
-
- .cell {
- display: flex;
- gap: 8px;
- align-items: center;
- }
-
- .hash a {
- font-family: var(--font-family-mono), monospace;
- color: var(--c-text-2);
- }
-
- .commit-title {
- flex: 1;
- }
-
- .commit-title a {
- color: var(--c-text-1);
- line-height: 1.5;
- }
-
- .commit-title a:hover {
- color: var(--c-brand-2);
- }
-
- .ref-badges {
- display: inline-flex;
- gap: 6px;
- margin-left: 8px;
- }
-
- .badge {
- font-size: 12px;
- color: var(--c-text-2);
- border: 1px solid var(--c-border);
- padding: 2px 6px;
- border-radius: 999px;
- white-space: nowrap;
- }
-
- .date {
- font-family: var(--font-family-mono), monospace;
- font-size: 12px;
- color: var(--c-text-2);
- }
-
- .pagination {
- display: flex;
- gap: 8px;
- align-items: center;
- justify-content: center;
- margin-top: 12px;
- padding: 8px 0;
- color: var(--c-text-2);
- }
-
- .pagination a,
- .pagination span.btn {
- display: inline-flex;
- align-items: center;
- gap: 6px;
- padding: 6px 10px;
- border: 1px solid var(--c-border);
- border-radius: 6px;
- background: var(--c-bg-elv);
- color: var(--c-text-1);
- text-decoration: none;
- font-size: 13px;
- }
-
- .pagination a:hover {
- color: var(--c-brand-2);
- border-color: var(--c-brand-2);
- }
-
- .pagination .disabled {
- opacity: 0.5;
- pointer-events: none;
- }
-
- .pagination .page-indicator {
- margin: 0 4px;
- font-size: 13px;
- color: var(--c-text-2);
- }
-
- /* Mobile optimizations */
- @media (max-width: 767px) {
- .row {
- height: auto;
- padding: 10px 12px;
- gap: 12px;
- flex-wrap: wrap;
- align-items: flex-start;
- }
-
- .commit-title {
- flex-basis: 100%;
- order: 1;
- }
-
- .author, .date, .hash {
- font-size: 12px;
- }
-
- .author {
- color: var(--c-text-2);
- order: 2;
- }
-
- .date {
- order: 2;
- }
-
- .hash {
- margin-left: auto;
- order: 3;
- }
- }
- </style>
-{{ end }}
-
-{{ define "body" }}
- {{ template "header" . }}
- <div class="commits">
- {{ range .Commits }}
- <div class="row">
- <div class="cell hash">
- <a href="{{ .Href }}">{{ .ShortHash }}</a>
- </div>
- <div class="cell commit-title">
- <a href="{{ .Href }}">{{ .Subject }}</a>
- {{ if .RefNames }}
- <div class="ref-badges">
- {{ range .RefNames }}
- {{ if or (eq .Kind "Branch") (eq .Kind "Tag") }}
- <span class="badge">{{ if eq .Kind "Tag" }}tag: {{ end }}{{ .Name }}</span>
- {{ end }}
- {{ end }}
- </div>
- {{ end }}
- </div>
- <div class="cell author">{{ .Author }}</div>
- <div class="cell date">{{ .Date | FormatDate }}</div>
- </div>
- {{ else}}
- <div class="row">(no commits)</div>
- {{ end }}
- </div>
- <div class="pagination">
- {{ if .Page.FirstHref }}
- <a class="btn" href="{{ .Page.FirstHref }}">« First</a>
- {{ else }}
- <span class="btn disabled">« First</span>
- {{ end }}
-
- {{ if .Page.PrevHref }}
- <a class="btn" href="{{ .Page.PrevHref }}">← Newer</a>
- {{ else }}
- <span class="btn disabled">← Newer</span>
- {{ end }}
-
- <span class="page-indicator">Page {{ .Page.Page }} of {{ .Page.TotalPages }}</span>
-
- {{ if .Page.NextHref }}
- <a class="btn" href="{{ .Page.NextHref }}">Older →</a>
- {{ else }}
- <span class="btn disabled">Older →</span>
- {{ end }}
-
- {{ if .Page.LastHref }}
- <a class="btn" href="{{ .Page.LastHref }}">Last »</a>
- {{ else }}
- <span class="btn disabled">Last »</span>
- {{ end }}
- </div>
-{{ end }}
diff --git a/pkg/templates/css/markdown_dark.css b/pkg/templates/css/markdown_dark.css
deleted file mode 100644
index 0d5d47d..0000000
--- a/pkg/templates/css/markdown_dark.css
+++ /dev/null
@@ -1,1042 +0,0 @@
-.markdown-body details,
-.markdown-body figcaption,
-.markdown-body figure {
- display: block;
-}
-
-.markdown-body summary {
- display: list-item;
-}
-
-.markdown-body [hidden] {
- display: none !important;
-}
-
-.markdown-body a {
- background-color: transparent;
- text-decoration: none;
-}
-
-.markdown-body abbr[title] {
- border-bottom: none;
- text-decoration: underline dotted;
-}
-
-.markdown-body b,
-.markdown-body strong {
- font-weight: 600;
-}
-
-.markdown-body dfn {
- font-style: italic;
-}
-
-.markdown-body h1 {
- margin: .67em 0;
- font-weight: 600;
- padding-bottom: .3em;
- font-size: 2em;
- border-bottom: 1px solid #3d444db3;
-}
-
-.markdown-body mark {
- background-color: #bb800926;
- color: #f0f6fc;
-}
-
-.markdown-body small {
- font-size: 90%;
-}
-
-.markdown-body sub,
-.markdown-body sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-.markdown-body sub {
- bottom: -0.25em;
-}
-
-.markdown-body sup {
- top: -0.5em;
-}
-
-.markdown-body img {
- border-style: none;
- max-width: 100%;
- box-sizing: content-box;
-}
-
-.markdown-body code,
-.markdown-body kbd,
-.markdown-body pre,
-.markdown-body samp {
- font-family: monospace;
- font-size: 1em;
-}
-
-.markdown-body figure {
- margin: 1em 2.5rem;
-}
-
-.markdown-body hr {
- box-sizing: content-box;
- overflow: hidden;
- border-bottom: 1px solid #3d444db3;
- height: .25em;
- padding: 0;
- margin: 1.5rem 0;
- background-color: #3d444d;
-}
-
-.markdown-body input {
- font: inherit;
- margin: 0;
- overflow: visible;
- line-height: inherit;
-}
-
-.markdown-body [type=button],
-.markdown-body [type=reset],
-.markdown-body [type=submit] {
- -webkit-appearance: button;
- appearance: button;
-}
-
-.markdown-body [type=checkbox],
-.markdown-body [type=radio] {
- box-sizing: border-box;
- padding: 0;
-}
-
-.markdown-body [type=number]::-webkit-inner-spin-button,
-.markdown-body [type=number]::-webkit-outer-spin-button {
- height: auto;
-}
-
-.markdown-body [type=search]::-webkit-search-cancel-button,
-.markdown-body [type=search]::-webkit-search-decoration {
- -webkit-appearance: none;
- appearance: none;
-}
-
-.markdown-body ::-webkit-input-placeholder {
- color: inherit;
- opacity: .54;
-}
-
-.markdown-body ::-webkit-file-upload-button {
- -webkit-appearance: button;
- appearance: button;
- font: inherit;
-}
-
-.markdown-body a:hover {
- text-decoration: underline;
-}
-
-.markdown-body ::placeholder {
- color: #9198a1;
- opacity: 1;
-}
-
-.markdown-body hr::before {
- display: table;
- content: "";
-}
-
-.markdown-body hr::after {
- display: table;
- clear: both;
- content: "";
-}
-
-.markdown-body table {
- border-spacing: 0;
- border-collapse: collapse;
- display: block;
- width: max-content;
- max-width: 100%;
- overflow: auto;
- font-variant: tabular-nums;
-}
-
-.markdown-body td,
-.markdown-body th {
- padding: 0;
-}
-
-.markdown-body details summary {
- cursor: pointer;
-}
-
-.markdown-body a:focus,
-.markdown-body [role=button]:focus,
-.markdown-body input[type=radio]:focus,
-.markdown-body input[type=checkbox]:focus {
- outline: 2px solid #1f6feb;
- outline-offset: -2px;
- box-shadow: none;
-}
-
-.markdown-body a:focus:not(:focus-visible),
-.markdown-body [role=button]:focus:not(:focus-visible),
-.markdown-body input[type=radio]:focus:not(:focus-visible),
-.markdown-body input[type=checkbox]:focus:not(:focus-visible) {
- outline: solid 1px transparent;
-}
-
-.markdown-body a:focus-visible,
-.markdown-body [role=button]:focus-visible,
-.markdown-body input[type=radio]:focus-visible,
-.markdown-body input[type=checkbox]:focus-visible {
- outline: 2px solid #1f6feb;
- outline-offset: -2px;
- box-shadow: none;
-}
-
-.markdown-body a:not([class]):focus,
-.markdown-body a:not([class]):focus-visible,
-.markdown-body input[type=radio]:focus,
-.markdown-body input[type=radio]:focus-visible,
-.markdown-body input[type=checkbox]:focus,
-.markdown-body input[type=checkbox]:focus-visible {
- outline-offset: 0;
-}
-
-.markdown-body kbd {
- display: inline-block;
- padding: 0.25rem;
- font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
- line-height: 10px;
- color: #f0f6fc;
- vertical-align: middle;
- background-color: #151b23;
- border: solid 1px #3d444db3;
- border-radius: 6px;
- box-shadow: inset 0 -1px 0 #3d444db3;
-}
-
-.markdown-body h1,
-.markdown-body h2,
-.markdown-body h3,
-.markdown-body h4,
-.markdown-body h5,
-.markdown-body h6 {
- margin-top: 1.5rem;
- margin-bottom: 1rem;
- font-weight: 600;
- line-height: 1.25;
-}
-
-.markdown-body h2 {
- font-weight: 600;
- padding-bottom: .3em;
- font-size: 1.5em;
- border-bottom: 1px solid #3d444db3;
-}
-
-.markdown-body h3 {
- font-weight: 600;
- font-size: 1.25em;
-}
-
-.markdown-body h4 {
- font-weight: 600;
- font-size: 1em;
-}
-
-.markdown-body h5 {
- font-weight: 600;
- font-size: .875em;
-}
-
-.markdown-body h6 {
- font-weight: 600;
- font-size: .85em;
- color: #9198a1;
-}
-
-.markdown-body p {
- margin-top: 0;
- margin-bottom: 10px;
-}
-
-.markdown-body blockquote {
- margin: 0;
- padding: 0 1em;
- color: #9198a1;
- border-left: .25em solid #3d444d;
-}
-
-.markdown-body ul,
-.markdown-body ol {
- margin-top: 0;
- margin-bottom: 0;
- padding-left: 2em;
-}
-
-.markdown-body ol ol,
-.markdown-body ul ol {
- list-style-type: lower-roman;
-}
-
-.markdown-body ul ul ol,
-.markdown-body ul ol ol,
-.markdown-body ol ul ol,
-.markdown-body ol ol ol {
- list-style-type: lower-alpha;
-}
-
-.markdown-body dd {
- margin-left: 0;
-}
-
-.markdown-body tt,
-.markdown-body code,
-.markdown-body samp {
- font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
- font-size: 12px;
-}
-
-.markdown-body pre {
- margin-top: 0;
- margin-bottom: 0;
- font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
- font-size: 12px;
- word-wrap: normal;
-}
-
-.markdown-body .octicon {
- display: inline-block;
- overflow: visible !important;
- vertical-align: text-bottom;
- fill: currentColor;
-}
-
-.markdown-body input::-webkit-outer-spin-button,
-.markdown-body input::-webkit-inner-spin-button {
- margin: 0;
- appearance: none;
-}
-
-.markdown-body .mr-2 {
- margin-right: 0.5rem !important;
-}
-
-.markdown-body::before {
- display: table;
- content: "";
-}
-
-.markdown-body::after {
- display: table;
- clear: both;
- content: "";
-}
-
-.markdown-body > *:first-child {
- margin-top: 0 !important;
-}
-
-.markdown-body > *:last-child {
- margin-bottom: 0 !important;
-}
-
-.markdown-body a:not([href]) {
- color: inherit;
- text-decoration: none;
-}
-
-.markdown-body .absent {
- color: #f85149;
-}
-
-.markdown-body .anchor {
- float: left;
- padding-right: 0.25rem;
- margin-left: -20px;
- line-height: 1;
-}
-
-.markdown-body .anchor:focus {
- outline: none;
-}
-
-.markdown-body p,
-.markdown-body blockquote,
-.markdown-body ul,
-.markdown-body ol,
-.markdown-body dl,
-.markdown-body table,
-.markdown-body pre,
-.markdown-body details {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-
-.markdown-body blockquote > :first-child {
- margin-top: 0;
-}
-
-.markdown-body blockquote > :last-child {
- margin-bottom: 0;
-}
-
-.markdown-body h1:hover .anchor,
-.markdown-body h2:hover .anchor,
-.markdown-body h3:hover .anchor,
-.markdown-body h4:hover .anchor,
-.markdown-body h5:hover .anchor,
-.markdown-body h6:hover .anchor {
- text-decoration: none;
-}
-
-.markdown-body h1 tt,
-.markdown-body h1 code,
-.markdown-body h2 tt,
-.markdown-body h2 code,
-.markdown-body h3 tt,
-.markdown-body h3 code,
-.markdown-body h4 tt,
-.markdown-body h4 code,
-.markdown-body h5 tt,
-.markdown-body h5 code,
-.markdown-body h6 tt,
-.markdown-body h6 code {
- padding: 0 .2em;
- font-size: inherit;
-}
-
-.markdown-body summary h1,
-.markdown-body summary h2,
-.markdown-body summary h3,
-.markdown-body summary h4,
-.markdown-body summary h5,
-.markdown-body summary h6 {
- display: inline-block;
-}
-
-.markdown-body summary h1 .anchor,
-.markdown-body summary h2 .anchor,
-.markdown-body summary h3 .anchor,
-.markdown-body summary h4 .anchor,
-.markdown-body summary h5 .anchor,
-.markdown-body summary h6 .anchor {
- margin-left: -40px;
-}
-
-.markdown-body summary h1,
-.markdown-body summary h2 {
- padding-bottom: 0;
- border-bottom: 0;
-}
-
-.markdown-body ul.no-list,
-.markdown-body ol.no-list {
- padding: 0;
- list-style-type: none;
-}
-
-.markdown-body ol[type="a s"] {
- list-style-type: lower-alpha;
-}
-
-.markdown-body ol[type="A s"] {
- list-style-type: upper-alpha;
-}
-
-.markdown-body ol[type="i s"] {
- list-style-type: lower-roman;
-}
-
-.markdown-body ol[type="I s"] {
- list-style-type: upper-roman;
-}
-
-.markdown-body ol[type="1"] {
- list-style-type: decimal;
-}
-
-.markdown-body div > ol:not([type]) {
- list-style-type: decimal;
-}
-
-.markdown-body ul ul,
-.markdown-body ul ol,
-.markdown-body ol ol,
-.markdown-body ol ul {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-.markdown-body li > p {
- margin-top: 1rem;
-}
-
-.markdown-body li + li {
- margin-top: .25em;
-}
-
-.markdown-body dl {
- padding: 0;
-}
-
-.markdown-body dl dt {
- padding: 0;
- margin-top: 1rem;
- font-size: 1em;
- font-style: italic;
- font-weight: 600;
-}
-
-.markdown-body dl dd {
- padding: 0 1rem;
- margin-bottom: 1rem;
-}
-
-.markdown-body table th {
- font-weight: 600;
-}
-
-.markdown-body table th,
-.markdown-body table td {
- padding: 6px 13px;
- border: 1px solid #3d444d;
-}
-
-.markdown-body table td > :last-child {
- margin-bottom: 0;
-}
-
-.markdown-body table tr {
- background-color: #0d1117;
- border-top: 1px solid #3d444db3;
-}
-
-.markdown-body table tr:nth-child(2n) {
- background-color: #151b23;
-}
-
-.markdown-body table img {
- background-color: transparent;
-}
-
-.markdown-body img[align=right] {
- padding-left: 20px;
-}
-
-.markdown-body img[align=left] {
- padding-right: 20px;
-}
-
-.markdown-body .emoji {
- max-width: none;
- vertical-align: text-top;
- background-color: transparent;
-}
-
-.markdown-body span.frame {
- display: block;
- overflow: hidden;
-}
-
-.markdown-body span.frame > span {
- display: block;
- float: left;
- width: auto;
- padding: 7px;
- margin: 13px 0 0;
- overflow: hidden;
- border: 1px solid #3d444d;
-}
-
-.markdown-body span.frame span img {
- display: block;
- float: left;
-}
-
-.markdown-body span.frame span span {
- display: block;
- padding: 5px 0 0;
- clear: both;
- color: #f0f6fc;
-}
-
-.markdown-body span.align-center {
- display: block;
- overflow: hidden;
- clear: both;
-}
-
-.markdown-body span.align-center > span {
- display: block;
- margin: 13px auto 0;
- overflow: hidden;
- text-align: center;
-}
-
-.markdown-body span.align-center span img {
- margin: 0 auto;
- text-align: center;
-}
-
-.markdown-body span.align-right {
- display: block;
- overflow: hidden;
- clear: both;
-}
-
-.markdown-body span.align-right > span {
- display: block;
- margin: 13px 0 0;
- overflow: hidden;
- text-align: right;
-}
-
-.markdown-body span.align-right span img {
- margin: 0;
- text-align: right;
-}
-
-.markdown-body span.float-left {
- display: block;
- float: left;
- margin-right: 13px;
- overflow: hidden;
-}
-
-.markdown-body span.float-left span {
- margin: 13px 0 0;
-}
-
-.markdown-body span.float-right {
- display: block;
- float: right;
- margin-left: 13px;
- overflow: hidden;
-}
-
-.markdown-body span.float-right > span {
- display: block;
- margin: 13px auto 0;
- overflow: hidden;
- text-align: right;
-}
-
-.markdown-body code,
-.markdown-body tt {
- padding: .2em .4em;
- margin: 0;
- font-size: 85%;
- white-space: break-spaces;
- background-color: #656c7633;
- border-radius: 6px;
-}
-
-.markdown-body code br,
-.markdown-body tt br {
- display: none;
-}
-
-.markdown-body del code {
- text-decoration: inherit;
-}
-
-.markdown-body samp {
- font-size: 85%;
-}
-
-.markdown-body pre code {
- font-size: 100%;
-}
-
-.markdown-body pre > code {
- padding: 0;
- margin: 0;
- word-break: normal;
- white-space: pre;
- background: transparent;
- border: 0;
-}
-
-.markdown-body .highlight {
- margin-bottom: 1rem;
-}
-
-.markdown-body .highlight pre {
- margin-bottom: 0;
- word-break: normal;
-}
-
-.markdown-body .highlight pre,
-.markdown-body pre {
- padding: 1rem;
- overflow: auto;
- font-size: 85%;
- line-height: 1.45;
- color: #f0f6fc;
- background-color: #151b23;
- border-radius: 6px;
-}
-
-.markdown-body pre code,
-.markdown-body pre tt {
- display: inline;
- max-width: auto;
- padding: 0;
- margin: 0;
- overflow: visible;
- line-height: inherit;
- word-wrap: normal;
- background-color: transparent;
- border: 0;
-}
-
-.markdown-body .csv-data td,
-.markdown-body .csv-data th {
- padding: 5px;
- overflow: hidden;
- font-size: 12px;
- line-height: 1;
- text-align: left;
- white-space: nowrap;
-}
-
-.markdown-body .csv-data .blob-num {
- padding: 10px 0.5rem 9px;
- text-align: right;
- background: #0d1117;
- border: 0;
-}
-
-.markdown-body .csv-data tr {
- border-top: 0;
-}
-
-.markdown-body .csv-data th {
- font-weight: 600;
- background: #151b23;
- border-top: 0;
-}
-
-.markdown-body [data-footnote-ref]::before {
- content: "[";
-}
-
-.markdown-body [data-footnote-ref]::after {
- content: "]";
-}
-
-.markdown-body .footnotes {
- font-size: 12px;
- color: #9198a1;
- border-top: 1px solid #3d444d;
-}
-
-.markdown-body .footnotes ol {
- padding-left: 1rem;
-}
-
-.markdown-body .footnotes ol ul {
- display: inline-block;
- padding-left: 1rem;
- margin-top: 1rem;
-}
-
-.markdown-body .footnotes li {
- position: relative;
-}
-
-.markdown-body .footnotes li:target::before {
- position: absolute;
- top: calc(0.5rem * -1);
- right: calc(0.5rem * -1);
- bottom: calc(0.5rem * -1);
- left: calc(1.5rem * -1);
- pointer-events: none;
- content: "";
- border: 2px solid #1f6feb;
- border-radius: 6px;
-}
-
-.markdown-body .footnotes li:target {
- color: #f0f6fc;
-}
-
-.markdown-body .footnotes .data-footnote-backref g-emoji {
- font-family: monospace;
-}
-
-.markdown-body body:has(:modal) {
- padding-right: var(--dialog-scrollgutter) !important;
-}
-
-.markdown-body .pl-c {
- color: #9198a1;
-}
-
-.markdown-body .pl-c1,
-.markdown-body .pl-s .pl-v {
- color: #79c0ff;
-}
-
-.markdown-body .pl-e,
-.markdown-body .pl-en {
- color: #d2a8ff;
-}
-
-.markdown-body .pl-smi,
-.markdown-body .pl-s .pl-s1 {
- color: #f0f6fc;
-}
-
-.markdown-body .pl-ent {
- color: #7ee787;
-}
-
-.markdown-body .pl-k {
- color: #ff7b72;
-}
-
-.markdown-body .pl-s,
-.markdown-body .pl-pds,
-.markdown-body .pl-s .pl-pse .pl-s1,
-.markdown-body .pl-sr,
-.markdown-body .pl-sr .pl-cce,
-.markdown-body .pl-sr .pl-sre,
-.markdown-body .pl-sr .pl-sra {
- color: #a5d6ff;
-}
-
-.markdown-body .pl-v,
-.markdown-body .pl-smw {
- color: #ffa657;
-}
-
-.markdown-body .pl-bu {
- color: #f85149;
-}
-
-.markdown-body .pl-ii {
- color: #f0f6fc;
- background-color: #8e1519;
-}
-
-.markdown-body .pl-c2 {
- color: #f0f6fc;
- background-color: #b62324;
-}
-
-.markdown-body .pl-sr .pl-cce {
- font-weight: bold;
- color: #7ee787;
-}
-
-.markdown-body .pl-ml {
- color: #f2cc60;
-}
-
-.markdown-body .pl-mh,
-.markdown-body .pl-mh .pl-en,
-.markdown-body .pl-ms {
- font-weight: bold;
- color: #1f6feb;
-}
-
-.markdown-body .pl-mi {
- font-style: italic;
- color: #f0f6fc;
-}
-
-.markdown-body .pl-mb {
- font-weight: bold;
- color: #f0f6fc;
-}
-
-.markdown-body .pl-md {
- color: #ffdcd7;
- background-color: #67060c;
-}
-
-.markdown-body .pl-mi1 {
- color: #aff5b4;
- background-color: #033a16;
-}
-
-.markdown-body .pl-mc {
- color: #ffdfb6;
- background-color: #5a1e02;
-}
-
-.markdown-body .pl-mi2 {
- color: #f0f6fc;
- background-color: #1158c7;
-}
-
-.markdown-body .pl-mdr {
- font-weight: bold;
- color: #d2a8ff;
-}
-
-.markdown-body .pl-ba {
- color: #9198a1;
-}
-
-.markdown-body .pl-sg {
- color: #3d444d;
-}
-
-.markdown-body .pl-corl {
- text-decoration: underline;
- color: #a5d6ff;
-}
-
-.markdown-body [role=button]:focus:not(:focus-visible),
-.markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible),
-.markdown-body button:focus:not(:focus-visible),
-.markdown-body summary:focus:not(:focus-visible),
-.markdown-body a:focus:not(:focus-visible) {
- outline: none;
- box-shadow: none;
-}
-
-.markdown-body [tabindex="0"]:focus:not(:focus-visible),
-.markdown-body details-dialog:focus:not(:focus-visible) {
- outline: none;
-}
-
-.markdown-body g-emoji {
- display: inline-block;
- min-width: 1ch;
- font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
- font-size: 1em;
- font-style: normal !important;
- font-weight: 400;
- line-height: 1;
- vertical-align: -0.075em;
-}
-
-.markdown-body g-emoji img {
- width: 1em;
- height: 1em;
-}
-
-.markdown-body .task-list-item {
- list-style-type: none;
-}
-
-.markdown-body .task-list-item label {
- font-weight: 400;
-}
-
-.markdown-body .task-list-item.enabled label {
- cursor: pointer;
-}
-
-.markdown-body .task-list-item + .task-list-item {
- margin-top: 0.25rem;
-}
-
-.markdown-body .task-list-item .handle {
- display: none;
-}
-
-.markdown-body .task-list-item-checkbox {
- margin: 0 .2em .25em -1.4em;
- vertical-align: middle;
-}
-
-.markdown-body ul:dir(rtl) .task-list-item-checkbox {
- margin: 0 -1.6em .25em .2em;
-}
-
-.markdown-body ol:dir(rtl) .task-list-item-checkbox {
- margin: 0 -1.6em .25em .2em;
-}
-
-.markdown-body .contains-task-list:hover .task-list-item-convert-container,
-.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
- display: block;
- width: auto;
- height: 24px;
- overflow: visible;
- clip: auto;
-}
-
-.markdown-body ::-webkit-calendar-picker-indicator {
- filter: invert(50%);
-}
-
-.markdown-body .markdown-alert {
- padding: 0.5rem 1rem;
- margin-bottom: 1rem;
- color: inherit;
- border-left: .25em solid #3d444d;
-}
-
-.markdown-body .markdown-alert > :first-child {
- margin-top: 0;
-}
-
-.markdown-body .markdown-alert > :last-child {
- margin-bottom: 0;
-}
-
-.markdown-body .markdown-alert .markdown-alert-title {
- display: flex;
- font-weight: 500;
- align-items: center;
- line-height: 1;
-}
-
-.markdown-body .markdown-alert.markdown-alert-note {
- border-left-color: #1f6feb;
-}
-
-.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title {
- color: #4493f8;
-}
-
-.markdown-body .markdown-alert.markdown-alert-important {
- border-left-color: #8957e5;
-}
-
-.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title {
- color: #ab7df8;
-}
-
-.markdown-body .markdown-alert.markdown-alert-warning {
- border-left-color: #9e6a03;
-}
-
-.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title {
- color: #d29922;
-}
-
-.markdown-body .markdown-alert.markdown-alert-tip {
- border-left-color: #238636;
-}
-
-.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title {
- color: #3fb950;
-}
-
-.markdown-body .markdown-alert.markdown-alert-caution {
- border-left-color: #da3633;
-}
-
-.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title {
- color: #f85149;
-}
-
-.markdown-body > *:first-child > .heading-element:first-child {
- margin-top: 0 !important;
-}
-
-.markdown-body .highlight pre:has(+.zeroclipboard-container) {
- min-height: 52px;
-}
diff --git a/pkg/templates/css/markdown_light.css b/pkg/templates/css/markdown_light.css
deleted file mode 100644
index 7c36e19..0000000
--- a/pkg/templates/css/markdown_light.css
+++ /dev/null
@@ -1,1059 +0,0 @@
-.markdown-body details,
-.markdown-body figcaption,
-.markdown-body figure {
- display: block;
-}
-
-.markdown-body summary {
- display: list-item;
-}
-
-.markdown-body [hidden] {
- display: none !important;
-}
-
-.markdown-body a {
- background-color: transparent;
- text-decoration: none;
-}
-
-.markdown-body abbr[title] {
- border-bottom: none;
- text-decoration: underline dotted;
-}
-
-.markdown-body b,
-.markdown-body strong {
- font-weight: 600;
-}
-
-.markdown-body dfn {
- font-style: italic;
-}
-
-.markdown-body h1 {
- margin: .67em 0;
- font-weight: 600;
- padding-bottom: .3em;
- font-size: 2em;
- border-bottom: 1px solid #d1d9e0b3;
-}
-
-.markdown-body mark {
- background-color: #fff8c5;
- color: #1f2328;
-}
-
-.markdown-body small {
- font-size: 90%;
-}
-
-.markdown-body sub,
-.markdown-body sup {
- font-size: 75%;
- line-height: 0;
- position: relative;
- vertical-align: baseline;
-}
-
-.markdown-body sub {
- bottom: -0.25em;
-}
-
-.markdown-body sup {
- top: -0.5em;
-}
-
-.markdown-body img {
- border-style: none;
- max-width: 100%;
- box-sizing: content-box;
-}
-
-.markdown-body code,
-.markdown-body kbd,
-.markdown-body pre,
-.markdown-body samp {
- font-family: monospace;
- font-size: 1em;
-}
-
-.markdown-body figure {
- margin: 1em 2.5rem;
-}
-
-.markdown-body hr {
- box-sizing: content-box;
- overflow: hidden;
- border-bottom: 1px solid #d1d9e0b3;
- height: .25em;
- padding: 0;
- margin: 1.5rem 0;
- background-color: #d1d9e0;
-}
-
-.markdown-body input {
- font: inherit;
- margin: 0;
- overflow: visible;
- line-height: inherit;
-}
-
-.markdown-body [type=button],
-.markdown-body [type=reset],
-.markdown-body [type=submit] {
- -webkit-appearance: button;
- appearance: button;
-}
-
-.markdown-body [type=checkbox],
-.markdown-body [type=radio] {
- box-sizing: border-box;
- padding: 0;
-}
-
-.markdown-body [type=number]::-webkit-inner-spin-button,
-.markdown-body [type=number]::-webkit-outer-spin-button {
- height: auto;
-}
-
-.markdown-body [type=search]::-webkit-search-cancel-button,
-.markdown-body [type=search]::-webkit-search-decoration {
- -webkit-appearance: none;
- appearance: none;
-}
-
-.markdown-body ::-webkit-input-placeholder {
- color: inherit;
- opacity: .54;
-}
-
-.markdown-body ::-webkit-file-upload-button {
- -webkit-appearance: button;
- appearance: button;
- font: inherit;
-}
-
-.markdown-body a:hover {
- text-decoration: underline;
-}
-
-.markdown-body ::placeholder {
- color: #59636e;
- opacity: 1;
-}
-
-.markdown-body hr::before {
- display: table;
- content: "";
-}
-
-.markdown-body hr::after {
- display: table;
- clear: both;
- content: "";
-}
-
-.markdown-body table {
- border-spacing: 0;
- border-collapse: collapse;
- display: block;
- width: max-content;
- max-width: 100%;
- overflow: auto;
- font-variant: tabular-nums;
-}
-
-.markdown-body td,
-.markdown-body th {
- padding: 0;
-}
-
-.markdown-body details summary {
- cursor: pointer;
-}
-
-.markdown-body a:focus,
-.markdown-body [role=button]:focus,
-.markdown-body input[type=radio]:focus,
-.markdown-body input[type=checkbox]:focus {
- outline: 2px solid #0969da;
- outline-offset: -2px;
- box-shadow: none;
-}
-
-.markdown-body a:focus:not(:focus-visible),
-.markdown-body [role=button]:focus:not(:focus-visible),
-.markdown-body input[type=radio]:focus:not(:focus-visible),
-.markdown-body input[type=checkbox]:focus:not(:focus-visible) {
- outline: solid 1px transparent;
-}
-
-.markdown-body a:focus-visible,
-.markdown-body [role=button]:focus-visible,
-.markdown-body input[type=radio]:focus-visible,
-.markdown-body input[type=checkbox]:focus-visible {
- outline: 2px solid #0969da;
- outline-offset: -2px;
- box-shadow: none;
-}
-
-.markdown-body a:not([class]):focus,
-.markdown-body a:not([class]):focus-visible,
-.markdown-body input[type=radio]:focus,
-.markdown-body input[type=radio]:focus-visible,
-.markdown-body input[type=checkbox]:focus,
-.markdown-body input[type=checkbox]:focus-visible {
- outline-offset: 0;
-}
-
-.markdown-body kbd {
- display: inline-block;
- padding: 0.25rem;
- font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
- line-height: 10px;
- color: #1f2328;
- vertical-align: middle;
- background-color: #f6f8fa;
- border: solid 1px #d1d9e0b3;
- border-bottom-color: #d1d9e0b3;
- border-radius: 6px;
- box-shadow: inset 0 -1px 0 #d1d9e0b3;
-}
-
-.markdown-body h1,
-.markdown-body h2,
-.markdown-body h3,
-.markdown-body h4,
-.markdown-body h5,
-.markdown-body h6 {
- margin-top: 1.5rem;
- margin-bottom: 1rem;
- font-weight: 600;
- line-height: 1.25;
-}
-
-.markdown-body h2 {
- font-weight: 600;
- padding-bottom: .3em;
- font-size: 1.5em;
- border-bottom: 1px solid #d1d9e0b3;
-}
-
-.markdown-body h3 {
- font-weight: 600;
- font-size: 1.25em;
-}
-
-.markdown-body h4 {
- font-weight: 600;
- font-size: 1em;
-}
-
-.markdown-body h5 {
- font-weight: 600;
- font-size: .875em;
-}
-
-.markdown-body h6 {
- font-weight: 600;
- font-size: .85em;
- color: #59636e;
-}
-
-.markdown-body p {
- margin-top: 0;
- margin-bottom: 10px;
-}
-
-.markdown-body blockquote {
- margin: 0;
- padding: 0 1em;
- color: #59636e;
- border-left: .25em solid #d1d9e0;
-}
-
-.markdown-body ul,
-.markdown-body ol {
- margin-top: 0;
- margin-bottom: 0;
- padding-left: 2em;
-}
-
-.markdown-body ol ol,
-.markdown-body ul ol {
- list-style-type: lower-roman;
-}
-
-.markdown-body ul ul ol,
-.markdown-body ul ol ol,
-.markdown-body ol ul ol,
-.markdown-body ol ol ol {
- list-style-type: lower-alpha;
-}
-
-.markdown-body dd {
- margin-left: 0;
-}
-
-.markdown-body tt,
-.markdown-body code,
-.markdown-body samp {
- font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
- font-size: 12px;
-}
-
-.markdown-body pre {
- margin-top: 0;
- margin-bottom: 0;
- font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace;
- font-size: 12px;
- word-wrap: normal;
-}
-
-.markdown-body .octicon {
- display: inline-block;
- overflow: visible !important;
- vertical-align: text-bottom;
- fill: currentColor;
-}
-
-.markdown-body input::-webkit-outer-spin-button,
-.markdown-body input::-webkit-inner-spin-button {
- margin: 0;
- appearance: none;
-}
-
-.markdown-body .mr-2 {
- margin-right: 0.5rem !important;
-}
-
-.markdown-body::before {
- display: table;
- content: "";
-}
-
-.markdown-body::after {
- display: table;
- clear: both;
- content: "";
-}
-
-.markdown-body > *:first-child {
- margin-top: 0 !important;
-}
-
-.markdown-body > *:last-child {
- margin-bottom: 0 !important;
-}
-
-.markdown-body a:not([href]) {
- color: inherit;
- text-decoration: none;
-}
-
-.markdown-body .absent {
- color: #d1242f;
-}
-
-.markdown-body .anchor {
- float: left;
- padding-right: 0.25rem;
- margin-left: -20px;
- line-height: 1;
-}
-
-.markdown-body .anchor:focus {
- outline: none;
-}
-
-.markdown-body p,
-.markdown-body blockquote,
-.markdown-body ul,
-.markdown-body ol,
-.markdown-body dl,
-.markdown-body table,
-.markdown-body pre,
-.markdown-body details {
- margin-top: 0;
- margin-bottom: 1rem;
-}
-
-.markdown-body blockquote > :first-child {
- margin-top: 0;
-}
-
-.markdown-body blockquote > :last-child {
- margin-bottom: 0;
-}
-
-.markdown-body h1 .octicon-link,
-.markdown-body h2 .octicon-link,
-.markdown-body h3 .octicon-link,
-.markdown-body h4 .octicon-link,
-.markdown-body h5 .octicon-link,
-.markdown-body h6 .octicon-link {
- color: #1f2328;
- vertical-align: middle;
- visibility: hidden;
-}
-
-.markdown-body h1:hover .anchor,
-.markdown-body h2:hover .anchor,
-.markdown-body h3:hover .anchor,
-.markdown-body h4:hover .anchor,
-.markdown-body h5:hover .anchor,
-.markdown-body h6:hover .anchor {
- text-decoration: none;
-}
-
-.markdown-body h1:hover .anchor .octicon-link,
-.markdown-body h2:hover .anchor .octicon-link,
-.markdown-body h3:hover .anchor .octicon-link,
-.markdown-body h4:hover .anchor .octicon-link,
-.markdown-body h5:hover .anchor .octicon-link,
-.markdown-body h6:hover .anchor .octicon-link {
- visibility: visible;
-}
-
-.markdown-body h1 tt,
-.markdown-body h1 code,
-.markdown-body h2 tt,
-.markdown-body h2 code,
-.markdown-body h3 tt,
-.markdown-body h3 code,
-.markdown-body h4 tt,
-.markdown-body h4 code,
-.markdown-body h5 tt,
-.markdown-body h5 code,
-.markdown-body h6 tt,
-.markdown-body h6 code {
- padding: 0 .2em;
- font-size: inherit;
-}
-
-.markdown-body summary h1,
-.markdown-body summary h2,
-.markdown-body summary h3,
-.markdown-body summary h4,
-.markdown-body summary h5,
-.markdown-body summary h6 {
- display: inline-block;
-}
-
-.markdown-body summary h1 .anchor,
-.markdown-body summary h2 .anchor,
-.markdown-body summary h3 .anchor,
-.markdown-body summary h4 .anchor,
-.markdown-body summary h5 .anchor,
-.markdown-body summary h6 .anchor {
- margin-left: -40px;
-}
-
-.markdown-body summary h1,
-.markdown-body summary h2 {
- padding-bottom: 0;
- border-bottom: 0;
-}
-
-.markdown-body ul.no-list,
-.markdown-body ol.no-list {
- padding: 0;
- list-style-type: none;
-}
-
-.markdown-body ol[type="a s"] {
- list-style-type: lower-alpha;
-}
-
-.markdown-body ol[type="A s"] {
- list-style-type: upper-alpha;
-}
-
-.markdown-body ol[type="i s"] {
- list-style-type: lower-roman;
-}
-
-.markdown-body ol[type="I s"] {
- list-style-type: upper-roman;
-}
-
-.markdown-body ol[type="1"] {
- list-style-type: decimal;
-}
-
-.markdown-body div > ol:not([type]) {
- list-style-type: decimal;
-}
-
-.markdown-body ul ul,
-.markdown-body ul ol,
-.markdown-body ol ol,
-.markdown-body ol ul {
- margin-top: 0;
- margin-bottom: 0;
-}
-
-.markdown-body li > p {
- margin-top: 1rem;
-}
-
-.markdown-body li + li {
- margin-top: .25em;
-}
-
-.markdown-body dl {
- padding: 0;
-}
-
-.markdown-body dl dt {
- padding: 0;
- margin-top: 1rem;
- font-size: 1em;
- font-style: italic;
- font-weight: 600;
-}
-
-.markdown-body dl dd {
- padding: 0 1rem;
- margin-bottom: 1rem;
-}
-
-.markdown-body table th {
- font-weight: 600;
-}
-
-.markdown-body table th,
-.markdown-body table td {
- padding: 6px 13px;
- border: 1px solid #d1d9e0;
-}
-
-.markdown-body table td > :last-child {
- margin-bottom: 0;
-}
-
-.markdown-body table tr {
- background-color: #ffffff;
- border-top: 1px solid #d1d9e0b3;
-}
-
-.markdown-body table tr:nth-child(2n) {
- background-color: #f6f8fa;
-}
-
-.markdown-body table img {
- background-color: transparent;
-}
-
-.markdown-body img[align=right] {
- padding-left: 20px;
-}
-
-.markdown-body img[align=left] {
- padding-right: 20px;
-}
-
-.markdown-body .emoji {
- max-width: none;
- vertical-align: text-top;
- background-color: transparent;
-}
-
-.markdown-body span.frame {
- display: block;
- overflow: hidden;
-}
-
-.markdown-body span.frame > span {
- display: block;
- float: left;
- width: auto;
- padding: 7px;
- margin: 13px 0 0;
- overflow: hidden;
- border: 1px solid #d1d9e0;
-}
-
-.markdown-body span.frame span img {
- display: block;
- float: left;
-}
-
-.markdown-body span.frame span span {
- display: block;
- padding: 5px 0 0;
- clear: both;
- color: #1f2328;
-}
-
-.markdown-body span.align-center {
- display: block;
- overflow: hidden;
- clear: both;
-}
-
-.markdown-body span.align-center > span {
- display: block;
- margin: 13px auto 0;
- overflow: hidden;
- text-align: center;
-}
-
-.markdown-body span.align-center span img {
- margin: 0 auto;
- text-align: center;
-}
-
-.markdown-body span.align-right {
- display: block;
- overflow: hidden;
- clear: both;
-}
-
-.markdown-body span.align-right > span {
- display: block;
- margin: 13px 0 0;
- overflow: hidden;
- text-align: right;
-}
-
-.markdown-body span.align-right span img {
- margin: 0;
- text-align: right;
-}
-
-.markdown-body span.float-left {
- display: block;
- float: left;
- margin-right: 13px;
- overflow: hidden;
-}
-
-.markdown-body span.float-left span {
- margin: 13px 0 0;
-}
-
-.markdown-body span.float-right {
- display: block;
- float: right;
- margin-left: 13px;
- overflow: hidden;
-}
-
-.markdown-body span.float-right > span {
- display: block;
- margin: 13px auto 0;
- overflow: hidden;
- text-align: right;
-}
-
-.markdown-body code,
-.markdown-body tt {
- padding: .2em .4em;
- margin: 0;
- font-size: 85%;
- white-space: break-spaces;
- background-color: #818b981f;
- border-radius: 6px;
-}
-
-.markdown-body code br,
-.markdown-body tt br {
- display: none;
-}
-
-.markdown-body del code {
- text-decoration: inherit;
-}
-
-.markdown-body samp {
- font-size: 85%;
-}
-
-.markdown-body pre code {
- font-size: 100%;
-}
-
-.markdown-body pre > code {
- padding: 0;
- margin: 0;
- word-break: normal;
- white-space: pre;
- background: transparent;
- border: 0;
-}
-
-.markdown-body .highlight {
- margin-bottom: 1rem;
-}
-
-.markdown-body .highlight pre {
- margin-bottom: 0;
- word-break: normal;
-}
-
-.markdown-body .highlight pre,
-.markdown-body pre {
- padding: 1rem;
- overflow: auto;
- font-size: 85%;
- line-height: 1.45;
- color: #1f2328;
- background-color: #f6f8fa;
- border-radius: 6px;
-}
-
-.markdown-body pre code,
-.markdown-body pre tt {
- display: inline;
- max-width: auto;
- padding: 0;
- margin: 0;
- overflow: visible;
- line-height: inherit;
- word-wrap: normal;
- background-color: transparent;
- border: 0;
-}
-
-.markdown-body .csv-data td,
-.markdown-body .csv-data th {
- padding: 5px;
- overflow: hidden;
- font-size: 12px;
- line-height: 1;
- text-align: left;
- white-space: nowrap;
-}
-
-.markdown-body .csv-data .blob-num {
- padding: 10px 0.5rem 9px;
- text-align: right;
- background: #ffffff;
- border: 0;
-}
-
-.markdown-body .csv-data tr {
- border-top: 0;
-}
-
-.markdown-body .csv-data th {
- font-weight: 600;
- background: #f6f8fa;
- border-top: 0;
-}
-
-.markdown-body [data-footnote-ref]::before {
- content: "[";
-}
-
-.markdown-body [data-footnote-ref]::after {
- content: "]";
-}
-
-.markdown-body .footnotes {
- font-size: 12px;
- color: #59636e;
- border-top: 1px solid #d1d9e0;
-}
-
-.markdown-body .footnotes ol {
- padding-left: 1rem;
-}
-
-.markdown-body .footnotes ol ul {
- display: inline-block;
- padding-left: 1rem;
- margin-top: 1rem;
-}
-
-.markdown-body .footnotes li {
- position: relative;
-}
-
-.markdown-body .footnotes li:target::before {
- position: absolute;
- top: calc(0.5rem * -1);
- right: calc(0.5rem * -1);
- bottom: calc(0.5rem * -1);
- left: calc(1.5rem * -1);
- pointer-events: none;
- content: "";
- border: 2px solid #0969da;
- border-radius: 6px;
-}
-
-.markdown-body .footnotes li:target {
- color: #1f2328;
-}
-
-.markdown-body .footnotes .data-footnote-backref g-emoji {
- font-family: monospace;
-}
-
-.markdown-body .pl-c {
- color: #59636e;
-}
-
-.markdown-body .pl-c1,
-.markdown-body .pl-s .pl-v {
- color: #0550ae;
-}
-
-.markdown-body .pl-e,
-.markdown-body .pl-en {
- color: #6639ba;
-}
-
-.markdown-body .pl-smi,
-.markdown-body .pl-s .pl-s1 {
- color: #1f2328;
-}
-
-.markdown-body .pl-ent {
- color: #0550ae;
-}
-
-.markdown-body .pl-k {
- color: #cf222e;
-}
-
-.markdown-body .pl-s,
-.markdown-body .pl-pds,
-.markdown-body .pl-s .pl-pse .pl-s1,
-.markdown-body .pl-sr,
-.markdown-body .pl-sr .pl-cce,
-.markdown-body .pl-sr .pl-sre,
-.markdown-body .pl-sr .pl-sra {
- color: #0a3069;
-}
-
-.markdown-body .pl-v,
-.markdown-body .pl-smw {
- color: #953800;
-}
-
-.markdown-body .pl-bu {
- color: #82071e;
-}
-
-.markdown-body .pl-ii {
- color: #f6f8fa;
- background-color: #82071e;
-}
-
-.markdown-body .pl-c2 {
- color: #f6f8fa;
- background-color: #cf222e;
-}
-
-.markdown-body .pl-sr .pl-cce {
- font-weight: bold;
- color: #116329;
-}
-
-.markdown-body .pl-ml {
- color: #3b2300;
-}
-
-.markdown-body .pl-mh,
-.markdown-body .pl-mh .pl-en,
-.markdown-body .pl-ms {
- font-weight: bold;
- color: #0550ae;
-}
-
-.markdown-body .pl-mi {
- font-style: italic;
- color: #1f2328;
-}
-
-.markdown-body .pl-mb {
- font-weight: bold;
- color: #1f2328;
-}
-
-.markdown-body .pl-md {
- color: #82071e;
- background-color: #ffebe9;
-}
-
-.markdown-body .pl-mi1 {
- color: #116329;
- background-color: #dafbe1;
-}
-
-.markdown-body .pl-mc {
- color: #953800;
- background-color: #ffd8b5;
-}
-
-.markdown-body .pl-mi2 {
- color: #d1d9e0;
- background-color: #0550ae;
-}
-
-.markdown-body .pl-mdr {
- font-weight: bold;
- color: #8250df;
-}
-
-.markdown-body .pl-ba {
- color: #59636e;
-}
-
-.markdown-body .pl-sg {
- color: #818b98;
-}
-
-.markdown-body .pl-corl {
- text-decoration: underline;
- color: #0a3069;
-}
-
-.markdown-body [role=button]:focus:not(:focus-visible),
-.markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible),
-.markdown-body button:focus:not(:focus-visible),
-.markdown-body summary:focus:not(:focus-visible),
-.markdown-body a:focus:not(:focus-visible) {
- outline: none;
- box-shadow: none;
-}
-
-.markdown-body [tabindex="0"]:focus:not(:focus-visible),
-.markdown-body details-dialog:focus:not(:focus-visible) {
- outline: none;
-}
-
-.markdown-body g-emoji {
- display: inline-block;
- min-width: 1ch;
- font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
- font-size: 1em;
- font-style: normal !important;
- font-weight: 400;
- line-height: 1;
- vertical-align: -0.075em;
-}
-
-.markdown-body g-emoji img {
- width: 1em;
- height: 1em;
-}
-
-.markdown-body .task-list-item {
- list-style-type: none;
-}
-
-.markdown-body .task-list-item label {
- font-weight: 400;
-}
-
-.markdown-body .task-list-item.enabled label {
- cursor: pointer;
-}
-
-.markdown-body .task-list-item + .task-list-item {
- margin-top: 0.25rem;
-}
-
-.markdown-body .task-list-item .handle {
- display: none;
-}
-
-.markdown-body .task-list-item-checkbox {
- margin: 0 .2em .25em -1.4em;
- vertical-align: middle;
-}
-
-.markdown-body ul:dir(rtl) .task-list-item-checkbox {
- margin: 0 -1.6em .25em .2em;
-}
-
-.markdown-body ol:dir(rtl) .task-list-item-checkbox {
- margin: 0 -1.6em .25em .2em;
-}
-
-.markdown-body .contains-task-list:hover .task-list-item-convert-container,
-.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
- display: block;
- width: auto;
- height: 24px;
- overflow: visible;
- clip: auto;
-}
-
-.markdown-body ::-webkit-calendar-picker-indicator {
- filter: invert(50%);
-}
-
-.markdown-body .markdown-alert {
- padding: 0.5rem 1rem;
- margin-bottom: 1rem;
- color: inherit;
- border-left: .25em solid #d1d9e0;
-}
-
-.markdown-body .markdown-alert > :first-child {
- margin-top: 0;
-}
-
-.markdown-body .markdown-alert > :last-child {
- margin-bottom: 0;
-}
-
-.markdown-body .markdown-alert .markdown-alert-title {
- display: flex;
- font-weight: 500;
- align-items: center;
- line-height: 1;
-}
-
-.markdown-body .markdown-alert.markdown-alert-note {
- border-left-color: #0969da;
-}
-
-.markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title {
- color: #0969da;
-}
-
-.markdown-body .markdown-alert.markdown-alert-important {
- border-left-color: #8250df;
-}
-
-.markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title {
- color: #8250df;
-}
-
-.markdown-body .markdown-alert.markdown-alert-warning {
- border-left-color: #9a6700;
-}
-
-.markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title {
- color: #9a6700;
-}
-
-.markdown-body .markdown-alert.markdown-alert-tip {
- border-left-color: #1a7f37;
-}
-
-.markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title {
- color: #1a7f37;
-}
-
-.markdown-body .markdown-alert.markdown-alert-caution {
- border-left-color: #cf222e;
-}
-
-.markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title {
- color: #d1242f;
-}
-
-.markdown-body > *:first-child > .heading-element:first-child {
- margin-top: 0 !important;
-}
-
-.markdown-body .highlight pre:has(+.zeroclipboard-container) {
- min-height: 52px;
-}
diff --git a/pkg/templates/file_tree.gohtml b/pkg/templates/file_tree.gohtml
deleted file mode 100644
index 21fc07c..0000000
--- a/pkg/templates/file_tree.gohtml
+++ /dev/null
@@ -1,43 +0,0 @@
-{{- /*gotype:mokhan.ca/antonmedv/gitmal/pkg/templates.FileTreeParams*/ -}}
-{{ define "file_tree" }}
- {{ range .Nodes }}
- {{ if .IsDir }}
- <details open>
- <summary class="node">
- <svg aria-hidden="true" focusable="false" width="16" height="16" class="dir">
- <use xlink:href="#dir"></use>
- </svg>
- <span class="file-name">{{ .Name }}</span>
- </summary>
- <div class="children">
- {{ template "file_tree" (FileTreeParams .Children) }}
- </div>
- </details>
- {{ else }}
- <div class="node">
- <div class="icon" aria-hidden="true">
- {{ if .IsNew }}
- <svg aria-hidden="true" focusable="false" width="16" height="16" class="file-added">
- <use xlink:href="#file-added"></use>
- </svg>
- {{ else if .IsDelete }}
- <svg aria-hidden="true" focusable="false" width="16" height="16" class="file-deleted">
- <use xlink:href="#file-deleted"></use>
- </svg>
- {{ else if .IsRename }}
- <svg aria-hidden="true" focusable="false" width="16" height="16" class="file-renamed">
- <use xlink:href="#file-renamed"></use>
- </svg>
- {{ else }}
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#file-modified"></use>
- </svg>
- {{ end }}
- </div>
- <a href="#{{.Path}}" class="file-name">
- {{ .Path | BaseName }}
- </a>
- </div>
- {{ end }}
- {{ end }}
-{{ end }}
diff --git a/pkg/templates/header.gohtml b/pkg/templates/header.gohtml
deleted file mode 100644
index 4b5af05..0000000
--- a/pkg/templates/header.gohtml
+++ /dev/null
@@ -1,35 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.HeaderParams*/ -}}
-{{ define "header" }}
- <div class="header-container">
- <header>
- {{ if .Ref }}
- <div class="header-ref" aria-label="Ref">{{ .Ref }}</div>
- {{ end }}
- {{ if .Header }}
- <h1 class="title">{{ .Header }}</h1>
- {{ else if .Breadcrumbs }}
- <nav class="breadcrumbs" aria-label="Breadcrumbs">
- {{ range $i, $b := .Breadcrumbs }}
- {{ if gt $i 0 }}
- <div>/</div>
- {{ end }}
- {{ if $b.Href }}
- <a href="{{$b.Href}}">{{$b.Name}}</a>
- {{ else }}
- <h1>{{$b.Name}}</h1>
- {{ if $b.IsDir}}
- <div>/</div>
- {{ end}}
- {{ end }}
- {{ end }}
- </nav>
- {{ end }}
- <button class="goto-top" onclick="window.scrollTo({top: 0, behavior: 'smooth'})">
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#arrow-top"></use>
- </svg>
- Top
- </button>
- </header>
- </div>
-{{ end }}
diff --git a/pkg/templates/layout.gohtml b/pkg/templates/layout.gohtml
deleted file mode 100644
index 9438396..0000000
--- a/pkg/templates/layout.gohtml
+++ /dev/null
@@ -1,318 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.LayoutParams*/ -}}
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>{{ .Title }}</title>
- <style>
- {{ if .Dark }}
- :root {
- --c-indigo-1: #a8b1ff;
- --c-indigo-2: #5c73e7;
- --c-indigo-3: #3e63dd;
- --c-green: #57ab5a;
- --c-red: #e5534b;
- --c-yellow: #c69026;
- --c-dir: #9198a1;
- --c-gray-soft: rgba(101, 117, 133, .16);
- --c-bg: #1b1b1f;
- --c-bg-alt: #161618;
- --c-bg-elv: #202127;
- --c-text-1: rgba(255, 255, 245, .86);
- --c-text-2: rgba(235, 235, 245, .6);
- --c-text-3: rgba(235, 235, 245, .38);
- --c-border: #3c3f44;
- --c-divider: #2e2e32;
- }
-
- {{ else }}
- :root {
- --c-indigo-1: #3451b2;
- --c-indigo-2: #3a5ccc;
- --c-indigo-3: #5672cd;
- --c-green: #1a7f37;
- --c-red: #c53030;
- --c-yellow: #9a6700;
- --c-dir: #54aeff;
- --c-gray-soft: rgba(142, 150, 170, .14);
- --c-bg: #ffffff;
- --c-bg-alt: #f6f6f7;
- --c-bg-elv: #ffffff;
- --c-text-1: rgba(60, 60, 67);
- --c-text-2: rgba(60, 60, 67, .78);
- --c-text-3: rgba(60, 60, 67, .56);
- --c-border: #c2c2c4;
- --c-divider: #e2e2e3;
- }
-
- {{ end }}
-
- :root {
- --c-brand-1: var(--c-indigo-1);
- --c-brand-2: var(--c-indigo-2);
- --c-brand-3: var(--c-indigo-3);
- --font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- --font-family-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- --code-line-height: 20px;
- --code-font-size: 12px;
- --code-color: var(--c-brand-1);
- --code-bg: var(--c-gray-soft);
- --code-block-bg: var(--c-bg-alt);
- --code-block-color: var(--c-text-1);
- --header-height: 46px;
- --border-radius: 6px;
- --max-content-width: 1470px;
- }
-
- * {
- box-sizing: border-box;
- }
-
- body {
- margin: 0;
- padding: 0;
- font-family: var(--font-family), sans-serif;
- font-size: 14px;
- line-height: 1;
- color: var(--c-text-1);
- background-color: var(--c-bg);
- text-rendering: optimizeLegibility;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- -moz-text-size-adjust: none;
- -webkit-text-size-adjust: none;
- text-size-adjust: none;
- min-height: 100vh;
- display: flex;
- flex-direction: column;
- }
-
- .nowrap {
- white-space: nowrap;
- }
-
- h1 {
- margin-inline: 0;
- margin-block: 16px;
- font-size: 20px;
- font-weight: 600;
- }
-
- a {
- color: var(--c-brand-1);
- font-weight: 500;
- text-decoration: underline;
- text-underline-offset: 2px;
- text-decoration: inherit;
- touch-action: manipulation;
- }
-
- a:hover {
- color: var(--c-brand-2);
- text-decoration: underline;
- }
-
- .menu {
- background-color: var(--c-bg-alt);
- border-bottom: 1px solid var(--c-divider);
- overflow-x: auto;
- }
-
- .menu-content {
- display: flex;
- flex-direction: row;
- align-items: center;
- gap: 16px;
- padding-inline: 16px;
- max-width: var(--max-content-width);
- margin-inline: auto;
- }
-
- .menu-item {
- display: flex;
- align-items: center;
- border-bottom: 2px solid transparent;
- height: 56px;
- padding-inline: 8px;
- }
-
- .menu-item a {
- display: flex;
- flex-direction: row;
- gap: 8px;
- align-items: center;
- color: var(--c-text-1);
- padding: 8px 10px;
- border-radius: 4px;
- }
-
- .menu-item a:hover {
- background-color: var(--c-bg-elv);
- text-decoration: none;
- }
-
- .menu-item.selected {
- border-bottom-color: var(--c-brand-1);
- }
-
- .project-name {
- font-weight: 600;
- font-size: 16px;
- margin-inline: 16px;
- color: var(--c-text-1);
- text-decoration: none;
- }
-
- main {
- flex-grow: 1;
- width: 100%;
- max-width: var(--max-content-width);
- margin: 16px auto;
- }
-
- .main-content {
- padding-inline: 16px;
- }
-
- footer {
- padding: 12px 16px;
- background-color: var(--c-bg-alt);
- border-top: 1px solid var(--c-divider);
- color: var(--c-text-3);
- font-size: 12px;
- text-align: center;
- }
-
- .header-container {
- container-type: scroll-state;
- position: sticky;
- top: 0;
- }
-
- .header-container {
- @container scroll-state(stuck: top) {
- header {
- border-top: none;
- border-top-left-radius: 0;
- border-top-right-radius: 0;
- }
-
- .goto-top {
- display: flex;
- }
- }
- }
-
- header {
- display: flex;
- flex-direction: row;
- align-items: center;
- min-height: var(--header-height);
- padding-inline: 16px;
- background: var(--c-bg-alt);
- border: 1px solid var(--c-border);
- border-top-left-radius: var(--border-radius);
- border-top-right-radius: var(--border-radius);
- }
-
- header h1 {
- word-break: break-all;
- font-weight: 600;
- font-size: 16px;
- margin: 0;
- padding: 0;
- }
-
- .header-ref {
- color: var(--c-text-2);
- border: 1px solid var(--c-border);
- border-radius: 6px;
- padding: 6px 10px;
- margin-right: 10px;
- margin-left: -6px;
- }
-
- header .path {
- font-size: 16px;
- }
-
- .breadcrumbs {
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- gap: 6px;
- font-size: 16px;
- }
-
- .breadcrumbs a {
- word-break: break-all;
- }
-
- .goto-top {
- display: none;
- margin-left: auto;
- padding: 6px 10px;
- background: none;
- border: none;
- border-radius: 6px;
- gap: 4px;
- align-items: center;
- color: var(--c-text-1);
- cursor: pointer;
- }
-
- .goto-top:hover {
- background: var(--c-bg-elv);
- }
- </style>
- {{ template "head" . }}
-</head>
-<body>
-{{ template "svg" . }}
-<div class="menu">
- <div class="menu-content">
- <a href="{{ .RootHref }}index.html" class="project-name">{{ .Name }}</a>
- <div class="menu-item {{ if eq .Selected "code" }}selected{{ end }}">
- <a href="{{ .RootHref }}blob/{{ .CurrentRefDir }}/index.html">
- <svg aria-hidden="true" width="16" height="16">
- <use xlink:href="#code"></use>
- </svg>
- Code
- </a>
- </div>
- <div class="menu-item {{ if eq .Selected "branches" }}selected{{ end }}">
- <a href="{{ .RootHref }}branches.html">
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#branch"></use>
- </svg>
- Branches
- </a>
- </div>
- <div class="menu-item {{ if eq .Selected "tags" }}selected{{ end }}">
- <a href="{{ .RootHref }}tags.html">
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#tag"></use>
- </svg>
- Tags
- </a>
- </div>
- <div class="menu-item {{ if eq .Selected "commits" }}selected{{ end }}">
- <a href="{{ .RootHref }}commits/{{ .CurrentRefDir }}/index.html">
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#commit"></use>
- </svg>
- Commits
- </a>
- </div>
- </div>
-</div>
-<main>
- <div class="main-content">
- {{ template "body" . }}
- </div>
-</main>
-<footer>
-</footer>
-</body>
-</html>
diff --git a/pkg/templates/list.gohtml b/pkg/templates/list.gohtml
deleted file mode 100644
index 9780504..0000000
--- a/pkg/templates/list.gohtml
+++ /dev/null
@@ -1,145 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.ListParams*/ -}}
-{{ define "head" }}
- <style>
- .files {
- border: 1px solid var(--c-border);
- border-top: none;
- width: 100%;
- border-bottom-left-radius: var(--border-radius);
- border-bottom-right-radius: var(--border-radius);
- overflow-x: auto;
- }
-
- .files .dir {
- color: var(--c-dir);
- }
-
- .files a {
- color: var(--c-text-1);
- }
-
- .files a:hover {
- color: var(--c-brand-2);
- }
-
- .row {
- display: flex;
- height: 40px;
- border-bottom: 1px solid var(--c-border);
- padding-inline: 16px;
- gap: 16px;
- }
-
- .row:last-child {
- border-bottom: none;
- }
-
- .row:hover {
- background-color: var(--c-bg-alt);
- }
-
- .cell {
- flex: 1;
- display: flex;
- gap: 8px;
- align-items: center;
- white-space: nowrap;
- }
-
- .cell:not(:first-child) {
- justify-content: flex-end;
- max-width: 100px;
- }
-
- @media (max-width: 767px) {
- .file-mode {
- display: none;
- }
- }
-
- {{ if .Readme }}
- .markdown-container {
- padding-inline: 16px;
- border: 1px solid var(--c-divider);
- border-radius: 8px;
- overflow-x: auto;
- margin-top: 16px;
- }
-
- .markdown-body {
- width: 100%;
- min-width: 200px;
- max-width: 980px;
- margin: 0 auto;
- padding: 32px;
- word-wrap: break-word;
- font-size: 16px;
- line-height: 1.5;
- }
-
- @media (max-width: 767px) {
- .markdown-body {
- padding: 16px;
- }
- }
-
- {{.CSSMarkdown}}
- {{ end }}
- </style>
-{{ end }}
-
-{{ define "body" }}
- {{ template "header" . }}
-
- <div class="files">
- {{ if .ParentHref }}
- <div class="row">
- <div class="cell">
- <svg aria-hidden="true" focusable="false" width="16" height="16" class="dir">
- <use xlink:href="#dir"></use>
- </svg>
- <a href="{{ .ParentHref }}">..</a>
- </div>
- </div>
- {{ end }}
-
- {{ if or .Dirs .Files }}
- {{ range .Dirs }}
- <div class="row">
- <div class="cell">
- <svg aria-hidden="true" focusable="false" width="16" height="16" class="dir">
- <use xlink:href="#dir"></use>
- </svg>
- <a href="{{ .Href }}">{{ .Name }}</a>
- </div>
- </div>
- {{ end }}
-
- {{ range .Files }}
- <div class="row">
- <div class="cell">
- <svg aria-hidden="true" focusable="false" width="16" height="16">
- <use xlink:href="#file"></use>
- </svg>
- <a href="{{ .Href }}">{{ .Name }}</a>
- </div>
- <div class="cell file-mode">{{ .Mode }}</div>
- <div class="cell file-size">{{ .Size }}</div>
- </div>
- {{ end }}
- {{ else }}
- <div class="row">
- <div class="cell">(empty)</div>
- </div>
- {{ end }}
- </div>
-
- {{ if .Readme }}
- <div class="markdown-container">
- <div class="markdown-body">
- {{ .Readme }}
- </div>
- </div>
- {{ end }}
-{{ end }}
-
diff --git a/pkg/templates/markdown.gohtml b/pkg/templates/markdown.gohtml
deleted file mode 100644
index 7827950..0000000
--- a/pkg/templates/markdown.gohtml
+++ /dev/null
@@ -1,45 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.MarkdownParams*/ -}}
-{{ define "head" }}
- <style>
- [id] {
- scroll-margin-top: var(--header-height);
- }
-
- .markdown-container {
- border: 1px solid var(--c-divider);
- border-top: none;
- border-bottom-left-radius: 8px;
- border-bottom-right-radius: 8px;
- overflow-x: auto;
- }
-
- .markdown-body {
- width: 100%;
- min-width: 200px;
- max-width: 980px;
- margin: 0 auto;
- padding: 32px;
- word-wrap: break-word;
- font-size: 16px;
- line-height: 1.5;
- }
-
- @media (max-width: 767px) {
- .markdown-body {
- padding: 16px;
- }
- }
-
- {{.CSSMarkdown}}
- </style>
-{{ end }}
-
-{{ define "body" }}
- {{ template "header" . }}
- <article class="markdown-container">
- <div class="markdown-body">
- {{ .Content }}
- </div>
- </article>
-{{ end }}
-
diff --git a/pkg/templates/preview.gohtml b/pkg/templates/preview.gohtml
deleted file mode 100644
index bc6f898..0000000
--- a/pkg/templates/preview.gohtml
+++ /dev/null
@@ -1,144 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.PreviewParams*/ -}}
-<!DOCTYPE html>
-<html lang="en">
-<head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
- <title>Chroma Themes Preview</title>
- <style>
- * {
- box-sizing: border-box;
- }
-
- body {
- margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- background: #ffffff;
- color: #3c3c43;
- line-height: 1.4;
- }
-
- header {
- position: sticky;
- top: 0;
- background: #f6f6f7;
- border-bottom: 1px solid #c2c2c4;
- padding: 12px 16px;
- z-index: 10;
- }
-
- .meta {
- color: rgba(60, 60, 67, .78);
- font-size: 12px
- }
-
- main {
- padding: 16px
- }
-
- .grid {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(420px, 1fr));
- gap: 16px;
- justify-content: center;
- }
-
- .card {
- border: 1px solid #c2c2c4;
- border-radius: 10px;
- overflow: hidden;
- background: #fff;
- }
-
- /* Dark tone variant for theme cards */
- .card.dark {
- background: #1b1b1f; /* dark bg */
- color: rgba(255, 255, 245, .86); /* light text */
- border-color: #3c3f44; /* dark border */
- }
-
- .card h2 {
- margin: 0;
- padding: 10px 12px;
- font-size: 14px;
- border-bottom: 1px solid #c2c2c4;
- display: flex;
- justify-content: space-between;
- align-items: center;
- }
-
- .card.dark h2 {
- border-bottom-color: #3c3f44;
- }
-
- .badge {
- font-size: 11px;
- padding: 2px 6px;
- border-radius: 6px;
- border: 1px solid #c2c2c4;
- color: rgba(60, 60, 67, .78);
- background: #ffffff;
- }
-
- .card.dark .badge {
- border-color: #3c3f44;
- color: rgba(235, 235, 245, .6);
- background: #1b1b1f;
- }
-
- .sample {
- padding: 12px;
- }
-
- pre {
- margin: 0;
- padding: 8px 16px;
- overflow-x: auto;
- white-space: pre;
- word-spacing: normal;
- word-break: normal;
- word-wrap: normal;
- tab-size: 4;
- font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace, monospace;
- border-radius: 6px;
- background-color: #f6f6f7;
- }
-
- .dark pre {
- background-color: #161618;
- }
-
- pre > code {
- display: block;
- padding: 0 16px;
- width: fit-content;
- min-width: 100%;
- line-height: 20px;
- font-size: 12px;
- color: rgba(60, 60, 67);
- }
-
- .dark pre > code {
- color: rgba(255, 255, 245, .86);
- }
- </style>
-</head>
-<body>
-<header>
- <div><strong>Chroma themes preview</strong></div>
- <div class="meta">Showing {{ .Count }} themes. Use <code>--theme &lt;name&gt;</code> in the app.</div>
-</header>
-<main>
- <div class="grid">
- {{ range .Themes }}
- <section class="card {{ .Tone }}">
- <h2><span>{{ .Name }}</span><span class="badge">{{ .Tone }}</span></h2>
- <div class="sample">
- {{ .HTML }}
- </div>
- </section>
- {{ end }}
- </div>
-</main>
-</body>
-</html>
diff --git a/pkg/templates/svg.gohtml b/pkg/templates/svg.gohtml
deleted file mode 100644
index 833af42..0000000
--- a/pkg/templates/svg.gohtml
+++ /dev/null
@@ -1,50 +0,0 @@
-{{ define "svg" }}
- <svg style="display: none" aria-hidden="true" focusable="false">
- <symbol id="dir" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M1.75 1A1.75 1.75 0 0 0 0 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0 0 16 13.25v-8.5A1.75 1.75 0 0 0 14.25 3H7.5a.25.25 0 0 1-.2-.1l-.9-1.2C6.07 1.26 5.55 1 5 1H1.75Z"/>
- </symbol>
- <symbol id="file" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V6h-2.75A1.75 1.75 0 0 1 9 4.25V1.5Zm6.75.062V4.25c0 .138.112.25.25.25h2.688l-.011-.013-2.914-2.914-.013-.011Z"/>
- </symbol>
- <symbol id="file-added" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073Zm4.48 3.758a.75.75 0 0 1 .755.745l.01 1.497h1.497a.75.75 0 0 1 0 1.5H9v1.507a.75.75 0 0 1-1.5 0V9.005l-1.502.01a.75.75 0 0 1-.01-1.5l1.507-.01-.01-1.492a.75.75 0 0 1 .745-.755Z"/>
- </symbol>
- <symbol id="file-modified" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M1 1.75C1 .784 1.784 0 2.75 0h7.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073ZM8 3.25a.75.75 0 0 1 .75.75v1.5h1.5a.75.75 0 0 1 0 1.5h-1.5v1.5a.75.75 0 0 1-1.5 0V7h-1.5a.75.75 0 0 1 0-1.5h1.5V4A.75.75 0 0 1 8 3.25Zm-3 8a.75.75 0 0 1 .75-.75h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1-.75-.75Z"/>
- </symbol>
- <symbol id="file-deleted" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-9.5A1.75 1.75 0 0 1 2 14.25Zm1.75-.25a.25.25 0 0 0-.25.25v12.5c0 .138.112.25.25.25h9.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073Zm4.5 6h2.242a.75.75 0 0 1 0 1.5h-2.24l-2.254.015a.75.75 0 0 1-.01-1.5Z"/>
- </symbol>
- <symbol id="file-renamed" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0 1 13.25 16h-3.5a.75.75 0 0 1 0-1.5h3.5a.25.25 0 0 0 .25-.25V4.664a.25.25 0 0 0-.073-.177l-2.914-2.914a.25.25 0 0 0-.177-.073H3.75a.25.25 0 0 0-.25.25v6.5a.75.75 0 0 1-1.5 0v-6.5Z"/>
- <path fill="currentColor"
- d="m5.427 15.573 3.146-3.146a.25.25 0 0 0 0-.354L5.427 8.927A.25.25 0 0 0 5 9.104V11.5H.75a.75.75 0 0 0 0 1.5H5v2.396c0 .223.27.335.427.177Z"/>
- </symbol>
- <symbol id="arrow-top" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M3.47 7.78a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0l4.25 4.25a.751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018L9 4.81v7.44a.75.75 0 0 1-1.5 0V4.81L4.53 7.78a.75.75 0 0 1-1.06 0Z"></path>
- </symbol>
- <symbol id="commit" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M11.93 8.5a4.002 4.002 0 0 1-7.86 0H.75a.75.75 0 0 1 0-1.5h3.32a4.002 4.002 0 0 1 7.86 0h3.32a.75.75 0 0 1 0 1.5Zm-1.43-.75a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"></path>
- </symbol>
- <symbol id="branch" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"></path>
- </symbol>
- <symbol id="tag" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="M1 7.775V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.752 1.752 0 0 1 1 7.775Zm1.5 0c0 .066.026.13.073.177l6.25 6.25a.25.25 0 0 0 .354 0l5.025-5.025a.25.25 0 0 0 0-.354l-6.25-6.25a.25.25 0 0 0-.177-.073H2.75a.25.25 0 0 0-.25.25ZM6 5a1 1 0 1 1 0 2 1 1 0 0 1 0-2Z"></path>
- </symbol>
- <symbol id="code" viewBox="0 0 16 16">
- <path fill="currentColor"
- d="m11.28 3.22 4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.275-.326.749.749 0 0 1 .215-.734L13.94 8l-3.72-3.72a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215Zm-6.56 0a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L2.06 8l3.72 3.72a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L.47 8.53a.75.75 0 0 1 0-1.06Z"></path>
- </symbol>
- </svg>
-{{ end }}
diff --git a/pkg/templates/tags.gohtml b/pkg/templates/tags.gohtml
deleted file mode 100644
index 97b77b0..0000000
--- a/pkg/templates/tags.gohtml
+++ /dev/null
@@ -1,71 +0,0 @@
-{{- /*gotype: mokhan.ca/antonmedv/gitmal/pkg/templates.TagsParams*/ -}}
-{{ define "head" }}
- <style>
- .tags {
- border: 1px solid var(--c-border);
- border-radius: var(--border-radius);
- overflow: hidden;
- }
-
- .tag-row {
- display: flex;
- align-items: center;
- gap: 12px;
- height: 44px;
- padding-inline: 16px;
- border-bottom: 1px solid var(--c-border);
- background-color: var(--c-bg-elv);
- }
-
- .tag-row:last-child {
- border-bottom: none;
- }
-
- .cell {
- display: flex;
- gap: 8px;
- align-items: center;
- }
-
- .tag-title {
- flex: 1;
- }
-
- .tag-row a {
- color: var(--c-text-1);
- }
-
- .tag-row a:hover {
- color: var(--c-brand-2);
- text-decoration: none;
- }
-
- .date {
- font-family: var(--font-family-mono), monospace;
- font-size: 12px;
- color: var(--c-text-2);
- }
-
- .hash a {
- font-family: var(--font-family-mono), monospace;
- color: var(--c-text-2);
- }
- </style>
-{{ end }}
-
-{{ define "body" }}
- <h1>Tags</h1>
- <div class="tags">
- {{ if .Tags }}
- {{ range .Tags }}
- <div class="tag-row">
- <div class="cell tag-title"><a href="commit/{{ .CommitHash }}.html">{{ .Name }}</a></div>
- <div class="cell date">{{ .Date | FormatDate }}</div>
- <div class="cell hash"><a href="commit/{{ .CommitHash }}.html">{{ ShortHash .CommitHash }}</a></div>
- </div>
- {{ end }}
- {{ else }}
- <div class="tag-row">(no tags)</div>
- {{ end }}
- </div>
-{{ end }}
diff --git a/pkg/templates/templates.go b/pkg/templates/templates.go
deleted file mode 100644
index 593f0a3..0000000
--- a/pkg/templates/templates.go
+++ /dev/null
@@ -1,213 +0,0 @@
-package templates
-
-import (
- "embed"
- _ "embed"
- . "html/template"
- "path/filepath"
- "time"
-
- "mokhan.ca/antonmedv/gitmal/pkg/git"
-)
-
-var funcs = FuncMap{
- "BaseName": filepath.Base,
- "FormatDate": func(date time.Time) HTML {
- return HTML(date.Format(`<span class="nowrap">2006-01-02</span> <span class="nowrap">15:04:05</span>`))
- },
- "ShortHash": func(hash string) string {
- return hash[:7]
- },
- "FileTreeParams": func(node []*FileTree) FileTreeParams {
- return FileTreeParams{Nodes: node}
- },
-}
-
-//go:embed css/markdown_light.css
-var CSSMarkdownLight string
-
-//go:embed css/markdown_dark.css
-var CSSMarkdownDark string
-
-//go:embed layout.gohtml header.gohtml file_tree.gohtml svg.gohtml
-var layoutContent embed.FS
-var layout = Must(New("layout").Funcs(funcs).ParseFS(layoutContent, "*.gohtml"))
-
-//go:embed blob.gohtml
-var blobContent string
-var BlobTemplate = Must(Must(layout.Clone()).Parse(blobContent))
-
-//go:embed markdown.gohtml
-var markdownContent string
-var MarkdownTemplate = Must(Must(layout.Clone()).Parse(markdownContent))
-
-//go:embed list.gohtml
-var listContent string
-var ListTemplate = Must(Must(layout.Clone()).Parse(listContent))
-
-//go:embed branches.gohtml
-var branchesContent string
-var BranchesTemplate = Must(Must(layout.Clone()).Parse(branchesContent))
-
-//go:embed tags.gohtml
-var tagsContent string
-var TagsTemplate = Must(Must(layout.Clone()).Parse(tagsContent))
-
-//go:embed commits_list.gohtml
-var commitsListContent string
-var CommitsListTemplate = Must(Must(layout.Clone()).Parse(commitsListContent))
-
-//go:embed commit.gohtml
-var commitContent string
-var CommitTemplate = Must(Must(layout.Clone()).Parse(commitContent))
-
-//go:embed preview.gohtml
-var previewContent string
-var PreviewTemplate = Must(New("preview").Parse(previewContent))
-
-type LayoutParams struct {
- Title string
- Name string
- Dark bool
- CSSMarkdown CSS
- RootHref string
- CurrentRefDir string
- Selected string
-}
-
-type HeaderParams struct {
- Ref git.Ref
- Header string
- Breadcrumbs []Breadcrumb
-}
-
-type Breadcrumb struct {
- Name string
- Href string
- IsDir bool
-}
-
-type BlobParams struct {
- LayoutParams
- HeaderParams
- CSS CSS
- Blob git.Blob
- IsImage bool
- IsBinary bool
- Content HTML
-}
-
-type MarkdownParams struct {
- LayoutParams
- HeaderParams
- Blob git.Blob
- Content HTML
-}
-
-type ListParams struct {
- LayoutParams
- HeaderParams
- Ref git.Ref
- ParentHref string
- Dirs []ListEntry
- Files []ListEntry
- Readme HTML
-}
-
-type ListEntry struct {
- Name string
- Href string
- IsDir bool
- Mode string
- Size string
-}
-
-type BranchesParams struct {
- LayoutParams
- Branches []BranchEntry
-}
-
-type BranchEntry struct {
- Name string
- Href string
- IsDefault bool
- CommitsHref string
-}
-
-type TagsParams struct {
- LayoutParams
- Tags []git.Tag
-}
-
-type Pagination struct {
- Page int
- TotalPages int
- PrevHref string
- NextHref string
- FirstHref string
- LastHref string
-}
-
-type CommitsListParams struct {
- LayoutParams
- HeaderParams
- Ref git.Ref
- Commits []git.Commit
- Page Pagination
-}
-
-type CommitParams struct {
- LayoutParams
- Commit git.Commit
- DiffCSS CSS
- FileTree []*FileTree
- FileViews []FileView
-}
-
-type FileTreeParams struct {
- Nodes []*FileTree
-}
-
-// FileTree represents a directory or file in a commit's changed files tree.
-// For directories, IsDir is true and Children contains nested nodes.
-// For files, status flags indicate the type of change.
-type FileTree struct {
- Name string
- Path string
- IsDir bool
- Children []*FileTree
-
- // File status (applicable when IsDir == false)
- IsNew bool
- IsDelete bool
- IsRename bool
- OldName string
- NewName string
- // Anchor id for this file (empty for directories)
- Anchor string
-}
-
-// FileView represents a single file section on the commit page with its
-// pre-rendered HTML diff and metadata used by the template.
-type FileView struct {
- Path string
- OldName string
- NewName string
- IsNew bool
- IsDelete bool
- IsRename bool
- IsBinary bool
- HasChanges bool
- HTML HTML // pre-rendered HTML for diff of this file
-}
-
-type PreviewCard struct {
- Name string
- Tone string
- HTML HTML
-}
-
-type PreviewParams struct {
- Count int
- Themes []PreviewCard
-}