diff options
Diffstat (limited to 'repo.go')
-rw-r--r-- | repo.go | 335 |
1 files changed, 335 insertions, 0 deletions
@@ -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") |