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 Path string29 Title string30 URL string31}3233type WalkFunc func(string, *RepoPage) error3435const (36 // File name of the git description file.37 descFn = "description"38)3940func NewRepo(fp string, cloneURL *url.URL, commits uint) (*Repo, error) {41 absFp, err := filepath.Abs(fp)42 if err != nil {43 return nil, err44 }4546 r := &Repo{Path: absFp, Title: repoTitle(absFp), maxCommits: commits}47 if cloneURL != nil {48 r.URL = cloneURL.String()49 }5051 fs := osfs.New(absFp)52 if _, err := fs.Stat(git.GitDirName); err == nil {53 // If this is not a bare repository, we change into54 // the .git directory so that we can treat it as such.55 fs, err = fs.Chroot(git.GitDirName)56 if err != nil {57 return nil, err58 }59 }6061 s := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())62 r.git, err = git.Open(s, fs)63 if err != nil {64 return nil, err65 }6667 // TODO: Make head a public member of the Repository struct.68 head, err := r.Tip()69 if err != nil {70 return nil, err71 }72 r.curTree, err = head.Tree()73 if err != nil {74 return nil, err75 }7677 return r, nil78}7980func (r *Repo) ReadState(fp string) error {81 stateFile, err := os.Open(fp)82 if err != nil {83 return err84 }8586 h, err := readHashFile(stateFile)87 if err != nil {88 return err89 }9091 r.prevTree, err = r.git.TreeObject(h)92 if err != nil {93 return err94 }9596 return nil97}9899func (r *Repo) WriteState(fp string) error {100 stateFile, err := os.Create(fp)101 if err != nil {102 return err103 }104105 _, err = stateFile.WriteString(r.curTree.Hash.String())106 if err != nil {107 return err108 }109110 return stateFile.Close()111}112113func (r *Repo) Tip() (*object.Commit, error) {114 head, err := r.git.Head()115 if err != nil {116 return nil, err117 }118119 hash := head.Hash()120 commit, err := r.git.CommitObject(hash)121 if err != nil {122 return nil, err123 }124125 return commit, nil126}127128func (r *Repo) indexPage() *RepoPage {129 return &RepoPage{130 Repo: r,131 tree: r.curTree,132 CurrentFile: RepoFile{mode: filemode.Dir, Path: ""},133 }134}135136func (r *Repo) walkTree(fn WalkFunc) error {137 err := fn(".", r.indexPage())138 if err != nil {139 return err140 }141142 walker := object.NewTreeWalker(r.curTree, true, nil)143 defer walker.Close()144 for {145 fp, entry, err := walker.Next()146 if err == io.EOF {147 break148 } else if err != nil {149 return err150 }151152 page, err := r.page(entry.Hash, entry.Mode, fp)153 if err != nil {154 return err155 }156 err = fn(fp, page)157 if err != nil {158 return err159 }160 }161162 return nil163}164165// Returns a list of all parent directory of the given fp, which are not present166// in the given tree. For example, because they have been removed in the tree.167func (r *Repo) changedParents(tree *object.Tree, fp string) ([]string, error) {168 var parents []string169 for {170 fp = filepath.Dir(fp)171 if fp == "." {172 break173 }174175 _, err := tree.Tree(fp)176 if err == object.ErrDirectoryNotFound {177 parents = append(parents, fp)178 } else if err != nil {179 return []string{}, err180 } else {181 break // fp is unchanged182 }183 }184185 return parents, nil186}187188func (r *Repo) walkDiff(fn WalkFunc) error {189 changes, err := object.DiffTree(r.prevTree, r.curTree)190 if err != nil {191 return err192 }193 patch, err := changes.Patch()194 if err != nil {195 return err196 }197198 rebuildDirs := make(map[string]bool)199 for _, filePatch := range patch.FilePatches() {200 from, to := filePatch.Files()201 if to == nil { // file was removed202 err = fn(from.Path(), nil)203 if err != nil {204 return err205 }206207 // Assuming the file pointed to by fp was deleted in the208 // newTree, check which parent directories are now also209 // deleted implicitly (because they are empty now).210 deadParents, err := r.changedParents(r.curTree, from.Path())211 if err != nil {212 return err213 }214215 for _, p := range deadParents {216 err = fn(p, nil)217 if err != nil {218 return err219 }220 }221222 lastDead := from.Path()223 if len(deadParents) > 0 {224 lastDead = deadParents[len(deadParents)-1]225 }226 rebuildDirs[filepath.Dir(lastDead)] = true227228 continue229 } else if from == nil { // created a new file230 dest := to.Path()231232 newParents, err := r.changedParents(r.prevTree, dest)233 if err != nil {234 return err235 }236 lastNew := dest237 for _, np := range newParents {238 lastNew = np239 rebuildDirs[np] = true240 }241 // rebuild directory index page containing the last new entry.242 rebuildDirs[filepath.Dir(lastNew)] = true243 }244245 fp := to.Path()246 if isReadme(fp) {247 rebuildDirs[filepath.Dir(fp)] = true248 }249250 page, err := r.page(to.Hash(), to.Mode(), fp)251 if err != nil {252 return err253 }254 err = fn(fp, page)255 if err != nil {256 return err257 }258 }259260 // If the tree changed, assume that we need to rebuild the index.261 // For example, because the commits are listed there. This a somewhat262 // depp-specific assumption which is hackily backed into the gitweb library.263 if r.prevTree.Hash != r.curTree.Hash {264 rebuildDirs["."] = true265 }266267 for dir, _ := range rebuildDirs {268 var page *RepoPage269 if dir == "." {270 page = r.indexPage()271 } else {272 entry, err := r.curTree.FindEntry(dir)273 if err != nil {274 return err275 }276277 page, err = r.page(entry.Hash, entry.Mode, dir)278 if err != nil {279 return err280 }281 }282283 err = fn(dir, page)284 if err != nil {285 return err286 }287 }288289 return nil290}291292func (r *Repo) Walk(fn WalkFunc) error {293 if r.prevTree == nil {294 return r.walkTree(fn)295 } else {296 return r.walkDiff(fn)297 }298}299300func (r *Repo) page(hash plumbing.Hash, mode filemode.FileMode, fp string) (*RepoPage, error) {301 page := &RepoPage{302 Repo: r,303 tree: nil,304 CurrentFile: RepoFile{mode, filepath.ToSlash(fp)},305 }306307 var err error308 if page.CurrentFile.IsDir() {309 page.tree, err = r.git.TreeObject(hash)310 if err != nil {311 return nil, err312 }313 }314315 return page, nil316}317318func (r *Repo) Description() (string, error) {319 fp := filepath.Join(r.Path, descFn)320321 desc, err := os.ReadFile(fp)322 if errors.Is(err, os.ErrNotExist) {323 return "", nil324 } else if err != nil {325 return "", err326 }327328 descText := string(desc)329 return strings.TrimSpace(descText), nil330}