1package gitweb23import (4 "errors"5 "io"6 "net/url"7 "os"8 "path/filepath"9 "strings"1011 "github.com/go-git/go-git/v5"12 "github.com/go-git/go-git/v5/plumbing"13 "github.com/go-git/go-git/v5/plumbing/cache"14 "github.com/go-git/go-git/v5/plumbing/filemode"15 "github.com/go-git/go-git/v5/plumbing/object"16 "github.com/go-git/go-git/v5/storage/filesystem"1718 "github.com/go-git/go-billy/v5/osfs"19)2021type Repo struct {22 curTree *object.Tree23 prevTree *object.Tree // may be nil2425 git *git.Repository26 maxCommits uint2728 Conf Config29 Path string30 Title string31 URL string32}3334type WalkFunc func(string, *RepoPage) error3536const (37 // File name of the git description file.38 descFn = "description"39)4041func NewRepo(fp string, cloneURL *url.URL, commits uint) (*Repo, error) {42 absFp, err := filepath.Abs(fp)43 if err != nil {44 return nil, err45 }4647 r := &Repo{Path: absFp, Title: repoTitle(absFp), maxCommits: commits}48 if cloneURL != nil {49 r.URL = cloneURL.String()50 }5152 fs := osfs.New(absFp)53 if _, err := fs.Stat(git.GitDirName); err == nil {54 // If this is not a bare repository, we change into55 // the .git directory so that we can treat it as such.56 fs, err = fs.Chroot(git.GitDirName)57 if err != nil {58 return nil, err59 }60 }6162 s := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())63 r.git, err = git.Open(s, fs)64 if err != nil {65 return nil, err66 }6768 // TODO: Make head a public member of the Repository struct.69 head, err := r.Tip()70 if err != nil {71 return nil, err72 }73 r.curTree, err = head.Tree()74 if err != nil {75 return nil, err76 }7778 r.Conf, err = loadConfig(r.git)79 if err != nil {80 return nil, err81 }8283 return r, nil84}8586func (r *Repo) ReadState(fp string) error {87 stateFile, err := os.Open(fp)88 if err != nil {89 return err90 }9192 h, err := readHashFile(stateFile)93 if err != nil {94 return err95 }9697 r.prevTree, err = r.git.TreeObject(h)98 if err != nil {99 return err100 }101102 return nil103}104105func (r *Repo) WriteState(fp string) error {106 stateFile, err := os.Create(fp)107 if err != nil {108 return err109 }110111 _, err = stateFile.WriteString(r.curTree.Hash.String())112 if err != nil {113 return err114 }115116 return stateFile.Close()117}118119func (r *Repo) Tip() (*object.Commit, error) {120 head, err := r.git.Head()121 if err != nil {122 return nil, err123 }124125 hash := head.Hash()126 commit, err := r.git.CommitObject(hash)127 if err != nil {128 return nil, err129 }130131 return commit, nil132}133134func (r *Repo) indexPage() *RepoPage {135 return &RepoPage{136 Repo: r,137 tree: r.curTree,138 CurrentFile: RepoFile{mode: filemode.Dir, Path: ""},139 }140}141142func (r *Repo) walkTree(fn WalkFunc) error {143 err := fn(".", r.indexPage())144 if err != nil {145 return err146 }147148 walker := object.NewTreeWalker(r.curTree, true, nil)149 defer walker.Close()150 for {151 fp, entry, err := walker.Next()152 if err == io.EOF {153 break154 } else if err != nil {155 return err156 }157158 page, err := r.page(entry.Hash, entry.Mode, fp)159 if err != nil {160 return err161 }162 err = fn(fp, page)163 if err != nil {164 return err165 }166 }167168 return nil169}170171// Returns a list of all parent directory of the given fp, which are not present172// in the given tree. For example, because they have been removed in the tree.173func (r *Repo) changedParents(tree *object.Tree, fp string) ([]string, error) {174 var parents []string175 for {176 fp = filepath.Dir(fp)177 if fp == "." {178 break179 }180181 _, err := tree.Tree(fp)182 if err == object.ErrDirectoryNotFound {183 parents = append(parents, fp)184 } else if err != nil {185 return []string{}, err186 } else {187 break // fp is unchanged188 }189 }190191 return parents, nil192}193194func (r *Repo) walkDiff(fn WalkFunc) error {195 changes, err := object.DiffTree(r.prevTree, r.curTree)196 if err != nil {197 return err198 }199 patch, err := changes.Patch()200 if err != nil {201 return err202 }203204 rebuildDirs := make(map[string]bool)205 for _, filePatch := range patch.FilePatches() {206 from, to := filePatch.Files()207 if to == nil { // file was removed208 err = fn(from.Path(), nil)209 if err != nil {210 return err211 }212213 // Assuming the file pointed to by fp was deleted in the214 // newTree, check which parent directories are now also215 // deleted implicitly (because they are empty now).216 deadParents, err := r.changedParents(r.curTree, from.Path())217 if err != nil {218 return err219 }220221 for _, p := range deadParents {222 err = fn(p, nil)223 if err != nil {224 return err225 }226 }227228 lastDead := from.Path()229 if len(deadParents) > 0 {230 lastDead = deadParents[len(deadParents)-1]231 }232 rebuildDirs[filepath.Dir(lastDead)] = true233234 continue235 } else if from == nil { // created a new file236 dest := to.Path()237238 newParents, err := r.changedParents(r.prevTree, dest)239 if err != nil {240 return err241 }242 lastNew := dest243 for _, np := range newParents {244 lastNew = np245 rebuildDirs[np] = true246 }247 // rebuild directory index page containing the last new entry.248 rebuildDirs[filepath.Dir(lastNew)] = true249 }250251 fp := to.Path()252 if isReadme(fp) {253 rebuildDirs[filepath.Dir(fp)] = true254 }255256 page, err := r.page(to.Hash(), to.Mode(), fp)257 if err != nil {258 return err259 }260 err = fn(fp, page)261 if err != nil {262 return err263 }264 }265266 // If the tree changed, assume that we need to rebuild the index.267 // For example, because the commits are listed there. This a somewhat268 // depp-specific assumption which is hackily backed into the gitweb library.269 if r.prevTree.Hash != r.curTree.Hash {270 rebuildDirs["."] = true271 }272273 for dir, _ := range rebuildDirs {274 var page *RepoPage275 if dir == "." {276 page = r.indexPage()277 } else {278 entry, err := r.curTree.FindEntry(dir)279 if err != nil {280 return err281 }282283 page, err = r.page(entry.Hash, entry.Mode, dir)284 if err != nil {285 return err286 }287 }288289 err = fn(dir, page)290 if err != nil {291 return err292 }293 }294295 return nil296}297298func (r *Repo) Walk(fn WalkFunc) error {299 if r.prevTree == nil {300 return r.walkTree(fn)301 } else {302 return r.walkDiff(fn)303 }304}305306func (r *Repo) page(hash plumbing.Hash, mode filemode.FileMode, fp string) (*RepoPage, error) {307 page := &RepoPage{308 Repo: r,309 tree: nil,310 CurrentFile: RepoFile{mode, filepath.ToSlash(fp)},311 }312313 var err error314 if page.CurrentFile.IsDir() {315 page.tree, err = r.git.TreeObject(hash)316 if err != nil {317 return nil, err318 }319 }320321 return page, nil322}323324func (r *Repo) Description() (string, error) {325 fp := filepath.Join(r.Path, descFn)326327 desc, err := os.ReadFile(fp)328 if errors.Is(err, os.ErrNotExist) {329 return "", nil330 } else if err != nil {331 return "", err332 }333334 descText := string(desc)335 return strings.TrimSpace(descText), nil336}