diff --git a/content/_sidebar.md b/content/_sidebar.md index 33cde16..cebeaa0 100644 --- a/content/_sidebar.md +++ b/content/_sidebar.md @@ -7,6 +7,7 @@ * [List all pages](/wiki/index) * [List all files](/wiki/files) +* [Recent changes](/wiki/changes) * [Upload a file](/wiki/upload) * [Change password](/wiki/account) * [Manage users](/wiki/users) diff --git a/git.go b/git.go index e799cca..b5dc4be 100644 --- a/git.go +++ b/git.go @@ -6,6 +6,7 @@ import ( "io" "log" "os" + "path" "path/filepath" "sort" "strings" @@ -62,25 +63,21 @@ func (g *GitBackend) PageExists(title string) bool { return err == nil && !fi.IsDir() } -func (g *GitBackend) PageHistory(title string, start string, end int) (*History, error) { +func (g *GitBackend) PageHistory(title string, start string, count int) (*History, error) { g.mutex.RLock() defer g.mutex.RUnlock() - var history []*LogEntry _, gitPath, err := resolvePath(g.dir, fmt.Sprintf("%s.md", title)) if err != nil { return nil, err } - var revision *plumbing.Hash - var commitIter object.CommitIter - if start == "" { - start = "HEAD" - } - revision, err = g.repo.ResolveRevision(plumbing.Revision(start)) + + revision, err := g.resolveRevision(start) if err != nil { return nil, err } - commitIter, err = g.repo.Log(&git.LogOptions{ + + commitIter, err := g.repo.Log(&git.LogOptions{ From: *revision, PathFilter: func(s string) bool { return s == gitPath @@ -89,7 +86,9 @@ func (g *GitBackend) PageHistory(title string, start string, end int) (*History, if err != nil { return nil, err } - for i := 0; i < end; i++ { + + var history []*LogEntry + for i := 0; i < count; i++ { commit, err := commitIter.Next() if err != nil { if err == io.EOF { @@ -104,6 +103,7 @@ func (g *GitBackend) PageHistory(title string, start string, end int) (*History, Message: commit.Message, }) } + return &History{Entries: history}, nil } @@ -314,6 +314,9 @@ func (g *GitBackend) writeFile(filePath, gitPath string, content io.Reader, user } func (g *GitBackend) RenamePage(name string, newName string, message string, user string) error { + g.mutex.Lock() + defer g.mutex.Unlock() + _, gitPath, err := resolvePath(g.dir, fmt.Sprintf("%s.md", name)) if err != nil { log.Printf("Unable to resolve old path: %s -> %s: %s", name, newName, err.Error()) @@ -345,6 +348,9 @@ func (g *GitBackend) RenamePage(name string, newName string, message string, use } func (g *GitBackend) DeletePage(name string, message string, user string) error { + g.mutex.Lock() + defer g.mutex.Unlock() + _, gitPath, err := resolvePath(g.dir, fmt.Sprintf("%s.md", name)) if err != nil { return err @@ -367,6 +373,70 @@ func (g *GitBackend) DeletePage(name string, message string, user string) error return nil } +func (g *GitBackend) RecentChanges(start string, count int) ([]*RecentChange, error) { + g.mutex.Lock() + defer g.mutex.Unlock() + + revision, err := g.resolveRevision(start) + if err != nil { + return nil, err + } + + commitIter, err := g.repo.Log(&git.LogOptions{From: *revision}) + if err != nil { + return nil, err + } + + var history []*RecentChange + for i := 0; i < count; i++ { + commit, err := commitIter.Next() + if err != nil { + if err == io.EOF { + break + } + return nil, err + } + + stats, err := commit.Stats() + if err != nil { + return nil, err + } + + entry := &RecentChange{ + LogEntry: LogEntry{ + ChangeId: commit.Hash.String(), + User: commit.Author.Name, + Time: commit.Author.When, + Message: commit.Message, + }, + } + + for j := range stats { + file := stats[j] + if filepath.Dir(file.Name) == ".wiki" { + entry.Config = strings.TrimSuffix(filepath.Base(file.Name), ".json.enc") + break + } else if path.Ext(file.Name) == ".md" { + entry.Page = strings.TrimSuffix(file.Name, ".md") + break + } else { + entry.File = file.Name + break + } + } + + history = append(history, entry) + } + return history, nil +} + +func (g *GitBackend) resolveRevision(rv string) (*plumbing.Hash, error) { + if rv == "" { + rv = "HEAD" + } + return g.repo.ResolveRevision(plumbing.Revision(rv)) +} + func resolvePath(base, name string) (string, string, error) { p := filepath.Clean(filepath.Join(base, name)) p = strings.ToLower(p) diff --git a/handlers_history.go b/handlers_history.go index d75c401..972a73d 100644 --- a/handlers_history.go +++ b/handlers_history.go @@ -10,7 +10,7 @@ type HistoryProvider interface { } func PageHistoryHandler(t *Templates, pp HistoryProvider) http.HandlerFunc { - const historySize = 20 + const historySize = 50 return func(w http.ResponseWriter, r *http.Request) { pageTitle := strings.TrimPrefix(r.URL.Path, "/history/") @@ -32,28 +32,49 @@ func PageHistoryHandler(t *Templates, pp HistoryProvider) http.HandlerFunc { return } - var entries []*HistoryEntry - for i := range history.Entries { - c := history.Entries[i] - if c.ChangeId == start { - continue - } - entries = append(entries, &HistoryEntry{ - Id: c.ChangeId, - User: c.User, - Time: c.Time, - Message: c.Message, - }) - if len(entries) == historySize { - break - } + var next string + if len(history.Entries) == number { + next = history.Entries[number-1].ChangeId + } else { + number = len(history.Entries) + 1 + } + + t.RenderHistory(w, r, pageTitle, history.Entries[:number-1], next) + } +} + +type RecentChangesProvider interface { + RecentChanges(start string, count int) ([]*RecentChange, error) +} + +func RecentChangesHandler(t *Templates, rp RecentChangesProvider) http.HandlerFunc { + const historySize = 50 + + return func(w http.ResponseWriter, r *http.Request) { + var start string + var number = historySize + 1 + + q := r.URL.Query()["after"] + if q != nil { + // If the user is paginating, request 22 items so we get the start item, the 20 we want to show, then + // an extra one to tell if there's a next page or not. + start = q[0] + number = historySize + 2 + } + + history, err := rp.RecentChanges(start, number) + if err != nil { + http.NotFound(w, r) + return } var next string - if len(history.Entries) == number { - next = history.Entries[number-2].ChangeId + if len(history) == number { + next = history[number-1].ChangeId + } else { + number = len(history) + 1 } - t.RenderHistory(w, r, pageTitle, entries, next) + t.RenderRecentChanges(w, r, history[:number-1], next) } } diff --git a/main.go b/main.go index 5b0728c..7fbdcab 100644 --- a/main.go +++ b/main.go @@ -130,6 +130,7 @@ func main() { wikiRouter.Path("/wiki/account").Handler(auth(ModifyAccountHandler(userManager))).Methods(http.MethodPost) wikiRouter.Path("/wiki/index").Handler(read(ListPagesHandler(templates, gitBackend))).Methods(http.MethodGet) wikiRouter.Path("/wiki/files").Handler(read(ListFilesHandler(templates, gitBackend))).Methods(http.MethodGet) + wikiRouter.Path("/wiki/changes").Handler(read(RecentChangesHandler(templates, gitBackend))).Methods(http.MethodGet) wikiRouter.Path("/wiki/login").Handler(LoginHandler(userManager)).Methods(http.MethodPost) wikiRouter.Path("/wiki/logout").Handler(LogoutHandler()).Methods(http.MethodPost) wikiRouter.Path("/wiki/upload").Handler(write(UploadFormHandler(templates))).Methods(http.MethodGet) diff --git a/model.go b/model.go index 9072687..f1ec34d 100644 --- a/model.go +++ b/model.go @@ -2,6 +2,13 @@ package main import "time" +type RecentChange struct { + Page string + File string + Config string + LogEntry +} + type LogEntry struct { ChangeId string User string diff --git a/templates.go b/templates.go index 9f3c054..e2fc2a4 100644 --- a/templates.go +++ b/templates.go @@ -136,18 +136,11 @@ func (t *Templates) RenderUploadForm(w http.ResponseWriter, r *http.Request) { type HistoryPageArgs struct { Common CommonArgs - History []*HistoryEntry + History []*LogEntry Next string } -type HistoryEntry struct { - Id string - User string - Time time.Time - Message string -} - -func (t *Templates) RenderHistory(w http.ResponseWriter, r *http.Request, title string, entries []*HistoryEntry, next string) { +func (t *Templates) RenderHistory(w http.ResponseWriter, r *http.Request, title string, entries []*LogEntry, next string) { t.render("history.gohtml", http.StatusOK, w, &HistoryPageArgs{ Common: t.populateArgs(w, r, CommonArgs{ PageTitle: title, @@ -158,6 +151,22 @@ func (t *Templates) RenderHistory(w http.ResponseWriter, r *http.Request, title }) } +type RecentChangesArgs struct { + Common CommonArgs + Changes []*RecentChange + Next string +} + +func (t *Templates) RenderRecentChanges(w http.ResponseWriter, r *http.Request, entries []*RecentChange, next string) { + t.render("changes.gohtml", http.StatusOK, w, &RecentChangesArgs{ + Common: t.populateArgs(w, r, CommonArgs{ + PageTitle: "Recent changes", + }), + Changes: entries, + Next: next, + }) +} + type ManageUsersArgs struct { Common CommonArgs Users []UserInfo diff --git a/templates/changes.gohtml b/templates/changes.gohtml new file mode 100644 index 0000000..50b0756 --- /dev/null +++ b/templates/changes.gohtml @@ -0,0 +1,28 @@ +{{- /*gotype: github.com/mdbot/wiki.RecentChangesArgs*/ -}} +{{template "header" .Common}} + +{{if .Next}} +

Next »

+{{end}} +{{template "footer" .Common}} diff --git a/templates/history.gohtml b/templates/history.gohtml index ff71ea4..fb2e269 100644 --- a/templates/history.gohtml +++ b/templates/history.gohtml @@ -4,7 +4,7 @@