| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "io" |
| 6 | "sort" |
| 7 | "strings" |
| 8 | |
| 9 | "github.com/go-git/go-git/v5" |
| 10 | "github.com/go-git/go-git/v5/plumbing" |
| 11 | "github.com/go-git/go-git/v5/plumbing/filemode" |
| 12 | "github.com/go-git/go-git/v5/plumbing/object" |
| 13 | ) |
| 14 | |
| 15 | func resolveRef(repo *git.Repository, ref string) (*object.Commit, error) { |
| 16 | if ref == "" { |
| 17 | ref = defaultBranchName(repo) |
| 18 | } |
| 19 | hash, err := repo.ResolveRevision(plumbing.Revision(ref)) |
| 20 | if err != nil { |
| 21 | return nil, err |
| 22 | } |
| 23 | return repo.CommitObject(*hash) |
| 24 | } |
| 25 | |
| 26 | func listBranches(repo *git.Repository) ([]RefInfo, error) { |
| 27 | iter, err := repo.Branches() |
| 28 | if err != nil { |
| 29 | return nil, err |
| 30 | } |
| 31 | var out []RefInfo |
| 32 | _ = iter.ForEach(func(r *plumbing.Reference) error { |
| 33 | info := RefInfo{Name: r.Name().Short(), SHA: r.Hash().String()} |
| 34 | if c, err := repo.CommitObject(r.Hash()); err == nil { |
| 35 | info.Time = c.Author.When |
| 36 | info.Author = c.Author.Name |
| 37 | } |
| 38 | out = append(out, info) |
| 39 | return nil |
| 40 | }) |
| 41 | sort.Slice(out, func(i, j int) bool { return out[i].Time.After(out[j].Time) }) |
| 42 | return out, nil |
| 43 | } |
| 44 | |
| 45 | func listTags(repo *git.Repository) ([]RefInfo, error) { |
| 46 | iter, err := repo.Tags() |
| 47 | if err != nil { |
| 48 | return nil, err |
| 49 | } |
| 50 | var out []RefInfo |
| 51 | _ = iter.ForEach(func(r *plumbing.Reference) error { |
| 52 | info := RefInfo{Name: r.Name().Short(), SHA: r.Hash().String()} |
| 53 | if tag, err := repo.TagObject(r.Hash()); err == nil { |
| 54 | info.Time = tag.Tagger.When |
| 55 | info.Author = tag.Tagger.Name |
| 56 | if c, err := tag.Commit(); err == nil { |
| 57 | info.SHA = c.Hash.String() |
| 58 | } |
| 59 | } else if c, err := repo.CommitObject(r.Hash()); err == nil { |
| 60 | info.Time = c.Author.When |
| 61 | info.Author = c.Author.Name |
| 62 | } |
| 63 | out = append(out, info) |
| 64 | return nil |
| 65 | }) |
| 66 | sort.Slice(out, func(i, j int) bool { return out[i].Time.After(out[j].Time) }) |
| 67 | return out, nil |
| 68 | } |
| 69 | |
| 70 | func commitLog(repo *git.Repository, ref string, page, perPage int) ([]CommitInfo, bool, error) { |
| 71 | head, err := resolveRef(repo, ref) |
| 72 | if err != nil { |
| 73 | return nil, false, err |
| 74 | } |
| 75 | iter, err := repo.Log(&git.LogOptions{From: head.Hash}) |
| 76 | if err != nil { |
| 77 | return nil, false, err |
| 78 | } |
| 79 | defer iter.Close() |
| 80 | |
| 81 | skip := page * perPage |
| 82 | var out []CommitInfo |
| 83 | i := 0 |
| 84 | hasNext := false |
| 85 | err = iter.ForEach(func(c *object.Commit) error { |
| 86 | if i < skip { |
| 87 | i++ |
| 88 | return nil |
| 89 | } |
| 90 | if len(out) >= perPage { |
| 91 | hasNext = true |
| 92 | return io.EOF |
| 93 | } |
| 94 | out = append(out, toCommitInfo(c)) |
| 95 | i++ |
| 96 | return nil |
| 97 | }) |
| 98 | if err != nil && !errors.Is(err, io.EOF) { |
| 99 | return nil, false, err |
| 100 | } |
| 101 | return out, hasNext, nil |
| 102 | } |
| 103 | |
| 104 | func toCommitInfo(c *object.Commit) CommitInfo { |
| 105 | subject, body := splitCommitMessage(c.Message) |
| 106 | parent := "" |
| 107 | if c.NumParents() > 0 { |
| 108 | parent = c.ParentHashes[0].String() |
| 109 | } |
| 110 | return CommitInfo{ |
| 111 | SHA: c.Hash.String(), |
| 112 | ShortSHA: c.Hash.String()[:8], |
| 113 | Author: c.Author.Name, |
| 114 | Email: c.Author.Email, |
| 115 | When: c.Author.When, |
| 116 | Subject: subject, |
| 117 | Body: body, |
| 118 | ParentSHA: parent, |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | func splitCommitMessage(msg string) (subject, body string) { |
| 123 | msg = strings.TrimRight(msg, "\n") |
| 124 | if i := strings.Index(msg, "\n"); i >= 0 { |
| 125 | return strings.TrimSpace(msg[:i]), strings.TrimSpace(msg[i+1:]) |
| 126 | } |
| 127 | return msg, "" |
| 128 | } |
| 129 | |
| 130 | func treeAt(repo *git.Repository, ref, path string) ([]TreeEntry, *object.Tree, error) { |
| 131 | c, err := resolveRef(repo, ref) |
| 132 | if err != nil { |
| 133 | return nil, nil, err |
| 134 | } |
| 135 | root, err := c.Tree() |
| 136 | if err != nil { |
| 137 | return nil, nil, err |
| 138 | } |
| 139 | tree := root |
| 140 | if path != "" { |
| 141 | t, err := root.Tree(path) |
| 142 | if err != nil { |
| 143 | return nil, nil, err |
| 144 | } |
| 145 | tree = t |
| 146 | } |
| 147 | var out []TreeEntry |
| 148 | for _, e := range tree.Entries { |
| 149 | entryPath := e.Name |
| 150 | if path != "" { |
| 151 | entryPath = path + "/" + e.Name |
| 152 | } |
| 153 | te := TreeEntry{ |
| 154 | Name: e.Name, |
| 155 | Path: entryPath, |
| 156 | IsDir: e.Mode == filemode.Dir, |
| 157 | Mode: e.Mode.String(), |
| 158 | } |
| 159 | if !te.IsDir { |
| 160 | if f, err := tree.File(e.Name); err == nil { |
| 161 | te.Size = f.Size |
| 162 | } |
| 163 | } |
| 164 | out = append(out, te) |
| 165 | } |
| 166 | sort.Slice(out, func(i, j int) bool { |
| 167 | if out[i].IsDir != out[j].IsDir { |
| 168 | return out[i].IsDir |
| 169 | } |
| 170 | return out[i].Name < out[j].Name |
| 171 | }) |
| 172 | return out, tree, nil |
| 173 | } |
| 174 | |
| 175 | func blobAt(repo *git.Repository, ref, path string) (string, int64, bool, error) { |
| 176 | c, err := resolveRef(repo, ref) |
| 177 | if err != nil { |
| 178 | return "", 0, false, err |
| 179 | } |
| 180 | tree, err := c.Tree() |
| 181 | if err != nil { |
| 182 | return "", 0, false, err |
| 183 | } |
| 184 | f, err := tree.File(path) |
| 185 | if err != nil { |
| 186 | return "", 0, false, err |
| 187 | } |
| 188 | bin, err := f.IsBinary() |
| 189 | if err != nil { |
| 190 | return "", 0, false, err |
| 191 | } |
| 192 | if bin { |
| 193 | return "", f.Size, true, nil |
| 194 | } |
| 195 | content, err := f.Contents() |
| 196 | if err != nil { |
| 197 | return "", 0, false, err |
| 198 | } |
| 199 | return content, f.Size, false, nil |
| 200 | } |
| 201 | |
| 202 | func blobBytes(repo *git.Repository, ref, path string) ([]byte, error) { |
| 203 | c, err := resolveRef(repo, ref) |
| 204 | if err != nil { |
| 205 | return nil, err |
| 206 | } |
| 207 | tree, err := c.Tree() |
| 208 | if err != nil { |
| 209 | return nil, err |
| 210 | } |
| 211 | f, err := tree.File(path) |
| 212 | if err != nil { |
| 213 | return nil, err |
| 214 | } |
| 215 | r, err := f.Blob.Reader() |
| 216 | if err != nil { |
| 217 | return nil, err |
| 218 | } |
| 219 | defer r.Close() |
| 220 | return io.ReadAll(r) |
| 221 | } |
| 222 | |
| 223 | func commitDiff(repo *git.Repository, sha string) (CommitInfo, []FilePatch, DiffStats, error) { |
| 224 | h := plumbing.NewHash(sha) |
| 225 | c, err := repo.CommitObject(h) |
| 226 | if err != nil { |
| 227 | return CommitInfo{}, nil, DiffStats{}, err |
| 228 | } |
| 229 | info := toCommitInfo(c) |
| 230 | |
| 231 | var parentTree *object.Tree |
| 232 | if c.NumParents() > 0 { |
| 233 | p, err := c.Parent(0) |
| 234 | if err != nil { |
| 235 | return info, nil, DiffStats{}, err |
| 236 | } |
| 237 | parentTree, err = p.Tree() |
| 238 | if err != nil { |
| 239 | return info, nil, DiffStats{}, err |
| 240 | } |
| 241 | } |
| 242 | thisTree, err := c.Tree() |
| 243 | if err != nil { |
| 244 | return info, nil, DiffStats{}, err |
| 245 | } |
| 246 | patch, err := diffTrees(parentTree, thisTree) |
| 247 | if err != nil { |
| 248 | return info, nil, DiffStats{}, err |
| 249 | } |
| 250 | files, stats := convertPatch(patch) |
| 251 | return info, files, stats, nil |
| 252 | } |
| 253 | |
| 254 | func diffTrees(from, to *object.Tree) (*object.Patch, error) { |
| 255 | if from == nil { |
| 256 | return (&object.Tree{}).Patch(to) |
| 257 | } |
| 258 | return from.Patch(to) |
| 259 | } |