summaryrefslogtreecommitdiff
path: root/repo.go
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-09-16 20:43:19 -0600
committertjpcc <tjp@ctrl-c.club>2023-09-16 20:43:19 -0600
commit90fa2795ad177b9add2fe46382576993e96ece4b (patch)
tree0e2847f8694fa66a914680329c4515101b5a45ab /repo.go
Initial commit
Gemini repository browsing
Diffstat (limited to 'repo.go')
-rw-r--r--repo.go335
1 files changed, 335 insertions, 0 deletions
diff --git a/repo.go b/repo.go
new file mode 100644
index 0000000..820e8b2
--- /dev/null
+++ b/repo.go
@@ -0,0 +1,335 @@
+package syw
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "time"
+
+ "tildegit.org/tjp/sliderule/logging"
+)
+
+type Repository string
+
+func Open(dirpath string) *Repository {
+ check := []string{dirpath}
+ if !strings.HasSuffix(dirpath, ".git") {
+ check = append(check, dirpath+".git")
+ }
+ check = append(check, filepath.Join(dirpath, ".git"))
+
+ for _, p := range check {
+ if st, err := os.Stat(filepath.Join(p, "objects")); err != nil || !st.IsDir() {
+ continue
+ }
+ if st, err := os.Stat(filepath.Join(p, "refs")); err != nil || !st.IsDir() {
+ continue
+ }
+
+ r := Repository(p)
+ return &r
+ }
+
+ return nil
+}
+
+func (r *Repository) Name() string {
+ name := filepath.Base(string(*r))
+ if name == ".git" {
+ name = filepath.Base(filepath.Dir(string(*r)))
+ }
+ return strings.TrimSuffix(name, ".git")
+}
+
+func (r *Repository) cmd(ctx context.Context, cmdname string, args ...string) (*cmdResult, error) {
+ args = append([]string{"--git-dir=" + string(*r), cmdname}, args...)
+ start := time.Now()
+
+ result, err := runCmd(ctx, args)
+
+ log, ok := ctx.Value("debuglog").(logging.Logger)
+ if ok {
+ _ = log.Log("msg", "ran git command", "args", fmt.Sprintf("%+v", args), "dur", time.Since(start))
+ }
+
+ return result, err
+}
+
+func (r *Repository) Type(ctx context.Context, hash string) (string, error) {
+ res, err := r.cmd(ctx, "cat-file", "-t", hash)
+ if err != nil {
+ return "", err
+ }
+ if res.status != 0 {
+ return "", errors.New(res.err.String())
+ }
+
+ return strings.Trim(res.out.String(), "\n"), nil
+}
+
+func (r *Repository) Refs(ctx context.Context) ([]Ref, error) {
+ res, err := r.cmd(ctx, "show-ref", "--head", "--heads", "--tags")
+ if err != nil {
+ return nil, err
+ }
+
+ lines := strings.Split(res.out.String(), "\n")
+
+ branches := make([]Ref, 0, len(lines))
+ for _, line := range lines {
+ hash, name, found := strings.Cut(line, " ")
+ if !found {
+ continue
+ }
+ branches = append(branches, Ref{
+ Repo: r,
+ Name: name,
+ Hash: hash,
+ })
+ }
+ return branches, nil
+}
+
+var badRevListOutput = errors.New("unexpected 'git rev-list' output")
+
+func (r *Repository) Commits(ctx context.Context, head string, count int) ([]Commit, error) {
+ res, err := r.cmd(ctx, "rev-list",
+ "--format=%an%n%ae%n%aI%n%cn%n%ce%n%cI%n%P%n%B$$END$$",
+ "-n", strconv.Itoa(count),
+ head,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ commits := make([]Commit, 0, count)
+ for _, revstr := range strings.Split(res.out.String(), "\n$$END$$\n") {
+ if revstr == "" {
+ continue
+ }
+ commits = append(commits, Commit{Repo: r})
+ commit := &commits[len(commits)-1]
+
+ commitHash, rest, found := strings.Cut(revstr, "\n")
+ if !found {
+ return nil, badRevListOutput
+ }
+ commit.Hash = commitHash[7:]
+
+ commit.AuthorName, rest, found = strings.Cut(rest, "\n")
+ if !found {
+ return nil, badRevListOutput
+ }
+ commit.AuthorEmail, rest, found = strings.Cut(rest, "\n")
+ if !found {
+ return nil, badRevListOutput
+ }
+
+ adate, rest, found := strings.Cut(rest, "\n")
+ if !found {
+ return nil, badRevListOutput
+ }
+ commit.AuthorDate, err = time.Parse(time.RFC3339, adate)
+ if err != nil {
+ return nil, err
+ }
+
+ commit.CommitterName, rest, found = strings.Cut(rest, "\n")
+ if !found {
+ return nil, badRevListOutput
+ }
+ commit.CommitterEmail, rest, found = strings.Cut(rest, "\n")
+ if !found {
+ return nil, badRevListOutput
+ }
+
+ cdate, rest, found := strings.Cut(rest, "\n")
+ if !found {
+ return nil, badRevListOutput
+ }
+ commit.CommitDate, err = time.Parse(time.RFC3339, cdate)
+ if err != nil {
+ return nil, err
+ }
+
+ parents, rest, found := strings.Cut(rest, "\n")
+ if !found {
+ return nil, badRevListOutput
+ }
+ commit.Parents = strings.Split(parents, " ")
+ if len(commit.Parents) == 1 && commit.Parents[0] == "" {
+ commit.Parents = nil
+ }
+
+ commit.Message = rest
+ }
+
+ return commits, nil
+}
+
+func (r *Repository) Commit(ctx context.Context, ref string) (*Commit, error) {
+ commits, err := r.Commits(ctx, ref, 1)
+ if err != nil {
+ return nil, err
+ }
+ return &commits[0], nil
+}
+
+func (r *Repository) Diffstat(ctx context.Context, fromref, toref string) (string, error) {
+ res, err := r.cmd(ctx, "diff-tree", "-r", "--stat", fromref, toref)
+ if err != nil {
+ return "", err
+ }
+ if res.status != 0 {
+ return "", errors.New(res.err.String())
+ }
+ return res.out.String(), nil
+}
+
+func (r *Repository) Diff(ctx context.Context, fromref, toref string) (string, error) {
+ res, err := r.cmd(ctx, "diff-tree", "-r", "-p", "-u", fromref, toref)
+ if err != nil {
+ return "", err
+ }
+ if res.status != 0 {
+ return "", errors.New(res.err.String())
+ }
+ return res.out.String(), nil
+}
+
+type Readme struct {
+ Filename string
+ RawContents string
+}
+
+func (r Readme) EscapedContents() string {
+ return strings.ReplaceAll(r.RawContents, "\n```", "\n ```")
+}
+
+func (r *Repository) Readme(ctx context.Context, ref string) (*Readme, error) {
+ dir, err := r.Tree(ctx, ref, "")
+ if err != nil {
+ return nil, err
+ }
+
+ filename := ""
+ for i := range dir {
+ if dir[i].Type == "blob" && strings.HasPrefix(strings.ToLower(dir[i].Path), "readme") {
+ filename = dir[i].Path
+ break
+ }
+ }
+
+ if filename != "" {
+ body, err := r.Blob(ctx, ref, filename)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Readme{
+ Filename: filename,
+ RawContents: string(body),
+ }, nil
+ }
+
+ return nil, nil
+}
+
+func (r *Repository) Description() string {
+ f, err := os.Open(filepath.Join(string(*r), "description"))
+ if err != nil {
+ return ""
+ }
+ defer func() { _ = f.Close() }()
+
+ b, err := io.ReadAll(f)
+ if err != nil {
+ return ""
+ }
+
+ return strings.TrimRight(string(b), "\n")
+}
+
+func (r *Repository) Blob(ctx context.Context, ref, path string) ([]byte, error) {
+ res, err := r.cmd(ctx, "cat-file", "blob", ref+":"+path)
+ switch {
+ case res == nil:
+ return nil, err
+ case res.status == 0:
+ return res.out.Bytes(), nil
+ case res.status == 128:
+ return nil, objectDoesNotExist
+ default:
+ return nil, errors.New(res.err.String())
+ }
+}
+
+type ObjectDescription struct {
+ Mode int
+ Type string
+ Hash string
+ Size int
+ Path string
+}
+
+func (r *Repository) Tree(ctx context.Context, ref, path string) ([]ObjectDescription, error) {
+ pattern := ref
+ if path != "" && path != "." {
+ pattern += ":" + path
+ }
+
+ res, err := r.cmd(ctx, "ls-tree", "-l", pattern)
+ if err != nil {
+ return nil, err
+ }
+
+ out := []ObjectDescription{}
+ for _, line := range strings.Split(res.out.String(), "\n") {
+ if line == "" {
+ continue
+ }
+
+ spl := dropEmpty(strings.Split(strings.ReplaceAll(line, "\t", " "), " "))
+
+ mode, err := strconv.ParseInt(spl[0], 8, 64)
+ if err != nil {
+ return nil, err
+ }
+
+ var size int
+ if spl[3] != "-" {
+ size, err = strconv.Atoi(spl[3])
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ out = append(out, ObjectDescription{
+ Mode: int(mode),
+ Type: spl[1],
+ Hash: spl[2],
+ Size: size,
+ Path: spl[4],
+ })
+ }
+ return out, nil
+}
+
+func dropEmpty(sl []string) []string {
+ n := 0
+ for i := 0; i < len(sl); i += 1 {
+ if sl[i] == "" {
+ copy(sl[i:], sl[i+1:])
+ i -= 1
+ n += 1
+ }
+ }
+ return sl[:len(sl)-n]
+}
+
+var objectDoesNotExist = errors.New("object does not exist")