1package gitweb23import (4 "errors"5 "html/template"6 "io"7 "net/url"8 "os"9 "path/filepath"10 "strings"1112 "github.com/go-git/go-git/v5"13 "github.com/go-git/go-git/v5/plumbing"14 "github.com/go-git/go-git/v5/plumbing/cache"15 "github.com/go-git/go-git/v5/plumbing/filemode"16 "github.com/go-git/go-git/v5/plumbing/object"17 "github.com/go-git/go-git/v5/storage/filesystem"1819 "github.com/go-git/go-billy/v5/osfs"20)2122type Repo struct {23 curTree *object.Tree24 prevTree *object.Tree // may be nil2526 git *git.Repository27 maxCommits uint2829 Conf Config30 Path string31 Title string32 URL template.URL33}3435type WalkFunc func(string, *RepoPage) error3637const (38 // File name of the git description file.39 descFn = "description"40)4142func NewRepo(fp string, cloneURL *url.URL, commits uint) (*Repo, error) {43 absFp, err := filepath.Abs(fp)44 if err != nil {45 return nil, err46 }4748 r := &Repo{Path: absFp, Title: repoTitle(absFp), maxCommits: commits}49 if cloneURL != nil {50 r.URL = template.URL(cloneURL.String())51 }5253 fs := osfs.New(absFp)54 if _, err := fs.Stat(git.GitDirName); err == nil {55 // If this is not a bare repository, we change into56 // the .git directory so that we can treat it as such.57 fs, err = fs.Chroot(git.GitDirName)58 if err != nil {59 return nil, err60 }61 }6263 s := filesystem.NewStorage(fs, cache.NewObjectLRUDefault())64 r.git, err = git.Open(s, fs)65 if err != nil {66 return nil, err67 }6869 // TODO: Make head a public member of the Repository struct.70 head, err := r.Tip()71 if err != nil {72 return nil, err73 }74 r.curTree, err = head.Tree()75 if err != nil {76 return nil, err77 }7879 r.Conf, err = loadConfig(r.git)80 if err != nil {81 return nil, err82 }8384 return r, nil85}8687func (r *Repo) ReadState(fp string) error {88 stateFile, err := os.Open(fp)89 if err != nil {90 return err91 }9293 h, err := readHashFile(stateFile)94 if err != nil {95 return err96 }9798 r.prevTree, err = r.git.TreeObject(h)99 if err != nil {100 return err101 }102103 return nil104}105106func (r *Repo) WriteState(fp string) error {107 stateFile, err := os.Create(fp)108 if err != nil {109 return err110 }111112 _, err = stateFile.WriteString(r.curTree.Hash.String())113 if err != nil {114 return err115 }116117 return stateFile.Close()118}119120func (r *Repo) Tip() (*object.Commit, error) {121 head, err := r.git.Head()122 if err != nil {123 return nil, err124 }125126 hash := head.Hash()127 commit, err := r.git.CommitObject(hash)128 if err != nil {129 return nil, err130 }131132 return commit, nil133}134135func (r *Repo) indexPage() *RepoPage {136 return &RepoPage{137 Repo: r,138 tree: r.curTree,139 CurrentFile: RepoFile{mode: filemode.Dir, Path: ""},140 }141}142143func (r *Repo) walkTree(fn WalkFunc) error {144 err := fn(".", r.indexPage())145 if err != nil {146 return err147 }148149 walker := object.NewTreeWalker(r.curTree, true, nil)150 defer walker.Close()151 for {152 fp, entry, err := walker.Next()153 if err == io.EOF {154 break155 } else if err != nil {156 return err157 }158159 page, err := r.page(entry.Hash, entry.Mode, fp)160 if err != nil {161 return err162 }163 err = fn(fp, page)164 if err != nil {165 return err166 }167 }168169 return nil170}171172// Returns a list of all parent directory of the given fp, which are not present173// in the given tree. For example, because they have been removed in the tree.174func (r *Repo) changedParents(tree *object.Tree, fp string) ([]string, error) {175 var parents []string176 for {177 fp = filepath.Dir(fp)178 if fp == "." {179 break180 }181182 _, err := tree.Tree(fp)183 if err == object.ErrDirectoryNotFound {184 parents = append(parents, fp)185 } else if err != nil {186 return []string{}, err187 } else {188 break // fp is unchanged189 }190 }191192 return parents, nil193}194195func (r *Repo) walkDiff(fn WalkFunc) error {196 changes, err := object.DiffTree(r.prevTree, r.curTree)197 if err != nil {198 return err199 }200 patch, err := changes.Patch()201 if err != nil {202 return err203 }204205 rebuildDirs := make(map[string]bool)206 for _, filePatch := range patch.FilePatches() {207 from, to := filePatch.Files()208 if to == nil { // file was removed209 err = fn(from.Path(), nil)210 if err != nil {211 return err212 }213214 // Assuming the file pointed to by fp was deleted in the215 // newTree, check which parent directories are now also216 // deleted implicitly (because they are empty now).217 deadParents, err := r.changedParents(r.curTree, from.Path())218 if err != nil {219 return err220 }221222 for _, p := range deadParents {223 err = fn(p, nil)224 if err != nil {225 return err226 }227 }228229 lastDead := from.Path()230 if len(deadParents) > 0 {231 lastDead = deadParents[len(deadParents)-1]232 }233 rebuildDirs[filepath.Dir(lastDead)] = true234235 continue236 } else if from == nil { // created a new file237 dest := to.Path()238239 newParents, err := r.changedParents(r.prevTree, dest)240 if err != nil {241 return err242 }243 lastNew := dest244 for _, np := range newParents {245 lastNew = np246 rebuildDirs[np] = true247 }248 // rebuild directory index page containing the last new entry.249 rebuildDirs[filepath.Dir(lastNew)] = true250 }251252 fp := to.Path()253 if isReadme(fp) {254 rebuildDirs[filepath.Dir(fp)] = true255 }256257 page, err := r.page(to.Hash(), to.Mode(), fp)258 if err != nil {259 return err260 }261 err = fn(fp, page)262 if err != nil {263 return err264 }265 }266267 // If the tree changed, assume that we need to rebuild the index.268 // For example, because the commits are listed there. This a somewhat269 // depp-specific assumption which is hackily backed into the gitweb library.270 if r.prevTree.Hash != r.curTree.Hash {271 rebuildDirs["."] = true272 }273274 for dir, _ := range rebuildDirs {275 var page *RepoPage276 if dir == "." {277 page = r.indexPage()278 } else {279 entry, err := r.curTree.FindEntry(dir)280 if err != nil {281 return err282 }283284 page, err = r.page(entry.Hash, entry.Mode, dir)285 if err != nil {286 return err287 }288 }289290 err = fn(dir, page)291 if err != nil {292 return err293 }294 }295296 return nil297}298299func (r *Repo) Walk(fn WalkFunc) error {300 if r.prevTree == nil {301 return r.walkTree(fn)302 } else {303 return r.walkDiff(fn)304 }305}306307func (r *Repo) page(hash plumbing.Hash, mode filemode.FileMode, fp string) (*RepoPage, error) {308 page := &RepoPage{309 Repo: r,310 tree: nil,311 CurrentFile: RepoFile{mode, filepath.ToSlash(fp)},312 }313314 var err error315 if page.CurrentFile.IsDir() {316 page.tree, err = r.git.TreeObject(hash)317 if err != nil {318 return nil, err319 }320 }321322 return page, nil323}324325func (r *Repo) Description() (string, error) {326 fp := filepath.Join(r.Path, descFn)327328 desc, err := os.ReadFile(fp)329 if errors.Is(err, os.ErrNotExist) {330 return "", nil331 } else if err != nil {332 return "", err333 }334335 descText := string(desc)336 return strings.TrimSpace(descText), nil337}