840 lines
18 KiB
Go
840 lines
18 KiB
Go
/*
|
|
* Wiki - A wiki with editor
|
|
* Copyright (c) 2021-2021 Peter Stuifzand
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"html"
|
|
"html/template"
|
|
"io/ioutil"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/blevesearch/bleve/v2"
|
|
"github.com/sergi/go-diff/diffmatchpatch"
|
|
)
|
|
|
|
const (
|
|
DocumentsFile = "_documents.json"
|
|
LinksFile = "_links.json"
|
|
BlocksDirectory = "_blocks"
|
|
)
|
|
|
|
var BlockNotFound = errors.New("block not found")
|
|
|
|
type saveMessage struct {
|
|
p string
|
|
page Page
|
|
summary string
|
|
author string
|
|
}
|
|
|
|
type Block struct {
|
|
Text string
|
|
Children []string
|
|
Parent string
|
|
}
|
|
|
|
// ListItemV2 is way to convert from old structure to new structure
|
|
type ListItemV2 struct {
|
|
ID ID
|
|
Indented int
|
|
Text string
|
|
}
|
|
|
|
func (v2 ListItemV2) ListItem() ListItem {
|
|
return ListItem{
|
|
v2.ID.StrID,
|
|
v2.Indented,
|
|
v2.Text,
|
|
false,
|
|
}
|
|
}
|
|
|
|
// ListItem is a simplification of the information that was saved by the editor
|
|
type ListItem struct {
|
|
ID string
|
|
Indented int
|
|
Text string
|
|
Fleeting bool `json:"fleeting,omitempty"`
|
|
}
|
|
|
|
type ActualListItem struct {
|
|
ID string `json:"id"`
|
|
Indented int `json:"indented"`
|
|
Text string `json:"text"`
|
|
Fold string `json:"fold"`
|
|
Hidden bool `json:"hidden"`
|
|
Fleeting bool `json:"fleeting,omitempty"`
|
|
}
|
|
|
|
type FilePages struct {
|
|
dirname string
|
|
saveC chan saveMessage
|
|
index bleve.Index
|
|
}
|
|
|
|
type BlockResponse struct {
|
|
PageID string
|
|
ParentID string
|
|
Texts map[string]string
|
|
Children map[string][]string
|
|
Parents []string
|
|
}
|
|
|
|
func NewFilePages(dirname string, index bleve.Index) PagesRepository {
|
|
err := os.MkdirAll(filepath.Join(dirname, "_blocks"), 0777)
|
|
if err != nil {
|
|
log.Fatalln(err)
|
|
}
|
|
|
|
fp := &FilePages{dirname, make(chan saveMessage), index}
|
|
|
|
go func() {
|
|
for msg := range fp.saveC {
|
|
err := fp.save(msg)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return fp
|
|
}
|
|
|
|
func convertBlocksToListItems(current string, blocks BlockResponse, indent int) []ActualListItem {
|
|
listItems := []ActualListItem{}
|
|
|
|
for _, child := range blocks.Children[current] {
|
|
l := convertBlocksToListItems(child, blocks, indent+1)
|
|
listItems = append(listItems,
|
|
ActualListItem{
|
|
ID: child,
|
|
Indented: indent,
|
|
Text: blocks.Texts[child],
|
|
Fold: "open",
|
|
Hidden: false,
|
|
})
|
|
listItems = append(listItems, l...)
|
|
}
|
|
|
|
return listItems
|
|
}
|
|
|
|
type titleOption struct {
|
|
date bool
|
|
timeObj time.Time
|
|
}
|
|
|
|
// 1_januari_2021
|
|
// 2021-01-01
|
|
|
|
func (fp *FilePages) Get(name string) Page {
|
|
var sw stopwatch
|
|
sw.Start("Get " + name)
|
|
defer sw.Stop()
|
|
var names []string
|
|
|
|
var to titleOption
|
|
pageNameDate, err := ParseDatePageName(name)
|
|
|
|
if err == nil {
|
|
to.date = true
|
|
to.timeObj = pageNameDate
|
|
names = append(names, name, pageNameDate.Format("2006-01-02"))
|
|
} else if t, err := time.Parse("2006-01-02", name); err == nil {
|
|
to.date = true
|
|
to.timeObj = t
|
|
names = append(names, formatDatePageName(to.timeObj), name)
|
|
} else {
|
|
names = append(names, name)
|
|
}
|
|
|
|
for _, name := range names {
|
|
blocks, err := loadBlocks(fp.dirname, name)
|
|
if err != nil && errors.Is(err, BlockNotFound) {
|
|
continue
|
|
}
|
|
return fp.blocksBackendGet(name, blocks, to)
|
|
}
|
|
|
|
page, err := fp.oldPagesBackend(name)
|
|
if err != nil {
|
|
log.Println("Deprecated:", err)
|
|
return fp.blocksBackendGet(name, BlockResponse{ParentID: "root", PageID: name}, to)
|
|
}
|
|
|
|
return page
|
|
}
|
|
|
|
func (fp *FilePages) blocksBackendGet(name string, blocks BlockResponse, option titleOption) Page {
|
|
buf := bytes.Buffer{}
|
|
current := blocks.PageID
|
|
listItems := convertBlocksToListItems(current, blocks, 0)
|
|
if listItems == nil {
|
|
listItems = []ActualListItem{}
|
|
}
|
|
|
|
err := json.NewEncoder(&buf).Encode(&listItems)
|
|
if err != nil {
|
|
log.Printf("while encoding blocks: %s", err)
|
|
}
|
|
|
|
refs, err := getBackrefs(fp, name)
|
|
if err != nil {
|
|
refs = nil
|
|
}
|
|
|
|
title := formatTitle(blocks.Texts[name], option)
|
|
if title == "" {
|
|
title = cleanTitle(name)
|
|
}
|
|
|
|
return Page{
|
|
Name: name,
|
|
Title: title,
|
|
Content: buf.String(),
|
|
Refs: refs,
|
|
Blocks: blocks,
|
|
Parent: blocks.ParentID,
|
|
}
|
|
}
|
|
|
|
func formatTitle(title string, option titleOption) string {
|
|
if option.date {
|
|
return formatDateTitle(option.timeObj)
|
|
}
|
|
return title
|
|
}
|
|
|
|
func (fp *FilePages) oldPagesBackend(title string) (Page, error) {
|
|
name := strings.Replace(title, " ", "_", -1)
|
|
title = strings.Replace(title, "_", " ", -1)
|
|
|
|
refs, err := getBackrefs(fp, name)
|
|
if err != nil {
|
|
refs = nil
|
|
}
|
|
|
|
f, err := os.Open(filepath.Join(fp.dirname, name))
|
|
if err != nil {
|
|
return Page{
|
|
Title: title,
|
|
Name: name,
|
|
Content: "",
|
|
Refs: refs,
|
|
}, fmt.Errorf("in old pages backend: %w", err)
|
|
}
|
|
|
|
defer f.Close()
|
|
body, err := ioutil.ReadAll(f)
|
|
if err != nil {
|
|
return Page{
|
|
Name: name,
|
|
Title: title,
|
|
Content: "",
|
|
Refs: refs,
|
|
}, fmt.Errorf("while reading %q in old pages backend: %w", name, err)
|
|
}
|
|
|
|
return Page{
|
|
Name: name,
|
|
Title: title,
|
|
Content: string(body),
|
|
Refs: refs,
|
|
}, nil
|
|
}
|
|
|
|
func (fp *FilePages) Save(p string, page Page, summary, author string) error {
|
|
fp.saveC <- saveMessage{p, page, summary, author}
|
|
return nil
|
|
}
|
|
|
|
func (fp *FilePages) save(msg saveMessage) error {
|
|
var sw stopwatch
|
|
p := msg.p
|
|
page := msg.page
|
|
summary := msg.summary
|
|
author := msg.author
|
|
|
|
if p[0] != '_' {
|
|
page.Name = strings.Replace(p, " ", "_", -1)
|
|
page.Title = strings.Replace(p, "_", " ", -1)
|
|
} else {
|
|
page.Name = p
|
|
page.Title = p
|
|
}
|
|
|
|
sw.Start("create blocks")
|
|
err := saveBlocksFromPage(fp.dirname, page)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
sw.Stop()
|
|
|
|
if p[0] != '_' {
|
|
sw.Start("prepare")
|
|
|
|
f, err := os.Create(filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1)))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
if page.Content[0] == '{' || page.Content[0] == '[' {
|
|
var buf bytes.Buffer
|
|
err = json.Indent(&buf, []byte(page.Content), "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = buf.WriteTo(f)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
f.WriteString(strings.Replace(page.Content, "\r\n", "\n", -1))
|
|
}
|
|
sw.Stop()
|
|
sw.Start("backrefs")
|
|
err = processBackrefs(fp.dirname, page)
|
|
if err != nil {
|
|
return fmt.Errorf("while processing backrefs: %s", err)
|
|
}
|
|
sw.Stop()
|
|
|
|
sw.Start("git")
|
|
err = saveWithGit(fp, p, summary, author)
|
|
if err != nil {
|
|
log.Printf("Error while saving to git: %s", err)
|
|
// return fmt.Errorf("while saving to git: %w", err)
|
|
}
|
|
sw.Stop()
|
|
|
|
sw.Start("index")
|
|
searchObjects, err := createSearchObjects(page.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("while creating search object %s: %w", page.Name, err)
|
|
}
|
|
for _, so := range searchObjects {
|
|
if fp.index != nil {
|
|
err = fp.index.Index(so.ID, so)
|
|
if err != nil {
|
|
return fmt.Errorf("while indexing %s: %w", page.Name, err)
|
|
}
|
|
}
|
|
}
|
|
sw.Stop()
|
|
sw.Start("links")
|
|
err = saveLinksIncremental(fp.dirname, page.Title)
|
|
sw.Stop()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func saveWithNewIDs(dirname string, listItems []ListItemV2, pageName string) ([]ListItem, error) {
|
|
var newListItems []ListItem
|
|
for _, item := range listItems {
|
|
newItem := ListItem{
|
|
ID: item.ID.NewID(),
|
|
Indented: item.Indented,
|
|
Text: item.Text,
|
|
}
|
|
newListItems = append(newListItems, newItem)
|
|
}
|
|
|
|
return newListItems, nil
|
|
}
|
|
|
|
func saveBlocksFromPage(dirname string, page Page) error {
|
|
log.Printf("Processing: %q\n", page.Name)
|
|
var listItems []ListItem
|
|
err := json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItems)
|
|
if err != nil {
|
|
log.Println("decoding default failed: %w", err)
|
|
var listItemsV2 []ListItemV2
|
|
err = json.NewDecoder(strings.NewReader(page.Content)).Decode(&listItemsV2)
|
|
listItems, err = saveWithNewIDs(dirname, listItemsV2, page.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("while rewriting %s to use new ids: %w", page.Name, err)
|
|
}
|
|
}
|
|
|
|
blocks := make(map[string]Block)
|
|
prevList := make(map[string]ListItem)
|
|
|
|
root := "root"
|
|
parentBlock, err := loadBlock(dirname, page.Name)
|
|
if err != nil {
|
|
log.Println(err)
|
|
} else {
|
|
root = parentBlock.Parent
|
|
}
|
|
|
|
title := page.Title
|
|
if page.Name[0] == '_' {
|
|
title = parentBlock.Text
|
|
}
|
|
|
|
rootParentID := page.Name
|
|
var parent = ListItem{
|
|
Text: title,
|
|
Indented: -1,
|
|
ID: page.Name,
|
|
}
|
|
|
|
prevList[parent.ID] = parent
|
|
blocks[parent.ID] = Block{
|
|
Text: title,
|
|
Children: []string{},
|
|
Parent: root,
|
|
}
|
|
|
|
var prev = &parent
|
|
|
|
for i, item := range listItems {
|
|
if item.Fleeting {
|
|
continue
|
|
}
|
|
|
|
prevList[item.ID] = item
|
|
if item.Indented > prev.Indented {
|
|
parent = *prev
|
|
} else if item.Indented == prev.Indented {
|
|
// nothing
|
|
} else if item.Indented <= parent.Indented {
|
|
for item.Indented <= parent.Indented {
|
|
if block, e := blocks[parent.ID]; e {
|
|
parent = prevList[block.Parent]
|
|
}
|
|
}
|
|
}
|
|
|
|
blocks[item.ID] = Block{
|
|
Text: item.Text,
|
|
Children: []string{},
|
|
Parent: parent.ID,
|
|
}
|
|
if block, e := blocks[parent.ID]; e {
|
|
block.Children = append(block.Children, item.ID)
|
|
blocks[parent.ID] = block
|
|
} else {
|
|
log.Println("Block missing")
|
|
}
|
|
prev = &listItems[i]
|
|
}
|
|
|
|
// TODO: found out if this is still necessary
|
|
log.Printf("Loading parent block: %q", rootParentID)
|
|
f, err := os.Open(filepath.Join(dirname, BlocksDirectory, rootParentID))
|
|
if err == nil {
|
|
var parentBlock Block
|
|
err = json.NewDecoder(f).Decode(&parentBlock)
|
|
if err == nil {
|
|
if pb, e := blocks[rootParentID]; e {
|
|
pb.Text = parentBlock.Text
|
|
pb.Parent = parentBlock.Parent
|
|
blocks[rootParentID] = pb
|
|
log.Printf("Text=%q, Parent=%q", parentBlock.Text, parentBlock.Parent)
|
|
}
|
|
}
|
|
f.Close()
|
|
} else {
|
|
log.Println(err)
|
|
err = nil
|
|
}
|
|
|
|
for id, block := range blocks {
|
|
log.Printf("Writing to %q\n", id)
|
|
f, err := os.OpenFile(filepath.Join(dirname, BlocksDirectory, id), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
log.Println(err)
|
|
continue
|
|
}
|
|
enc := json.NewEncoder(f)
|
|
enc.SetIndent("", " ")
|
|
err = enc.Encode(&block)
|
|
if err != nil {
|
|
log.Println(err)
|
|
}
|
|
f.Close()
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func loadBlocks(dirname, rootBlockID string) (BlockResponse, error) {
|
|
resp := BlockResponse{
|
|
rootBlockID,
|
|
"",
|
|
nil,
|
|
nil,
|
|
nil,
|
|
}
|
|
|
|
resp.Texts = make(map[string]string)
|
|
resp.Children = make(map[string][]string)
|
|
|
|
queue := []string{rootBlockID}
|
|
|
|
block, err := loadBlock(dirname, rootBlockID)
|
|
if err != nil {
|
|
return BlockResponse{}, err
|
|
}
|
|
// NOTE: what does this do?
|
|
if rootBlockID[0] != '_' && block.Children == nil {
|
|
return BlockResponse{}, fmt.Errorf("not a block and has no children: %w", BlockNotFound)
|
|
}
|
|
|
|
prevID := rootBlockID
|
|
parentID := block.Parent
|
|
|
|
for parentID != "root" {
|
|
parent, err := loadBlock(dirname, parentID)
|
|
if err != nil {
|
|
return BlockResponse{}, fmt.Errorf("while loading current parent block (%s->%s): %w", prevID, parentID, err)
|
|
}
|
|
|
|
resp.Texts[parentID] = parent.Text
|
|
resp.Children[parentID] = parent.Children
|
|
resp.ParentID = parentID
|
|
|
|
resp.Parents = append(resp.Parents, parentID)
|
|
|
|
prevID = parentID
|
|
parentID = parent.Parent
|
|
}
|
|
if parentID == "root" {
|
|
resp.ParentID = "root"
|
|
}
|
|
|
|
for {
|
|
if len(queue) == 0 {
|
|
break
|
|
}
|
|
current := queue[0]
|
|
queue = queue[1:]
|
|
block, err := loadBlock(dirname, current)
|
|
if err != nil {
|
|
return BlockResponse{}, err
|
|
}
|
|
resp.Texts[current] = block.Text
|
|
resp.Children[current] = block.Children
|
|
queue = append(queue, block.Children...)
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
func loadBlock(dirname, blockID string) (Block, error) {
|
|
f, err := os.Open(filepath.Join(dirname, BlocksDirectory, blockID))
|
|
if err != nil {
|
|
return Block{}, fmt.Errorf("%q: %w", blockID, BlockNotFound)
|
|
}
|
|
defer f.Close()
|
|
var block Block
|
|
err = json.NewDecoder(f).Decode(&block)
|
|
if err != nil {
|
|
return Block{}, fmt.Errorf("%q: %v", blockID, err)
|
|
}
|
|
return block, nil
|
|
}
|
|
|
|
func saveLinksIncremental(dirname, title string) error {
|
|
type Document struct {
|
|
Title string `json:"title"`
|
|
}
|
|
var results []Document
|
|
f, err := os.Open(filepath.Join(dirname, LinksFile))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = json.NewDecoder(f).Decode(&results)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.Close()
|
|
|
|
titles := make(map[string]bool)
|
|
for _, r := range results {
|
|
titles[r.Title] = true
|
|
}
|
|
|
|
// Add new? title
|
|
titles[title] = true
|
|
|
|
results = nil
|
|
for t := range titles {
|
|
results = append(results, Document{t})
|
|
}
|
|
|
|
f, err = os.Create(filepath.Join(dirname, LinksFile))
|
|
err = json.NewEncoder(f).Encode(&results)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
f.Close()
|
|
return nil
|
|
}
|
|
|
|
func saveLinks(mp PagesRepository) error {
|
|
type Document struct {
|
|
Title string `json:"title"`
|
|
}
|
|
var results []Document
|
|
pages, err := mp.AllPages()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, page := range pages {
|
|
results = append(results, Document{page.Title})
|
|
}
|
|
f, err := os.Create(filepath.Join(mp.(*FilePages).dirname, LinksFile))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
err = json.NewEncoder(f).Encode(&results)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func saveWithGit(fp *FilePages, p string, summary, author string) error {
|
|
cmd := exec.Command("git", "add", ".")
|
|
cmd.Dir = fp.dirname
|
|
err := cmd.Run()
|
|
if err != nil {
|
|
return fmt.Errorf("while adding page %s: %s", p, err)
|
|
}
|
|
|
|
cmd = exec.Command("git", "commit", "-m", "Changes to "+p+" by "+author+"\n\n"+summary)
|
|
cmd.Dir = fp.dirname
|
|
// cmd.Stderr = os.Stderr
|
|
// cmd.Stdout = os.Stderr
|
|
err = cmd.Run()
|
|
if err != nil {
|
|
return fmt.Errorf("while commiting page %s: %s", p, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (fp *FilePages) Exist(p string) bool {
|
|
pageName := filepath.Join(fp.dirname, strings.Replace(p, " ", "_", -1))
|
|
_, err := os.Stat(pageName)
|
|
if err != nil {
|
|
return !os.IsNotExist(err)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func DiffPrettyHtml(diffs []diffmatchpatch.Diff) string {
|
|
var buff bytes.Buffer
|
|
for _, diff := range diffs {
|
|
// text := strings.Replace(html.EscapeString(diff.Text), "\n", "<span class=\"lighter\">¶</span><br>", -1)
|
|
text := html.EscapeString(diff.Text)
|
|
switch diff.Type {
|
|
case diffmatchpatch.DiffInsert:
|
|
_, _ = buff.WriteString("<ins style=\"background:#e6ffe6;\">")
|
|
_, _ = buff.WriteString(text)
|
|
_, _ = buff.WriteString("</ins>")
|
|
case diffmatchpatch.DiffDelete:
|
|
_, _ = buff.WriteString("<del style=\"background:#ffe6e6;\">")
|
|
_, _ = buff.WriteString(text)
|
|
_, _ = buff.WriteString("</del>")
|
|
case diffmatchpatch.DiffEqual:
|
|
_, _ = buff.WriteString("<span>")
|
|
_, _ = buff.WriteString(text)
|
|
_, _ = buff.WriteString("</span>")
|
|
}
|
|
}
|
|
return buff.String()
|
|
}
|
|
|
|
func (fp *FilePages) PageHistory(p string) ([]Revision, error) {
|
|
page := strings.Replace(p, " ", "_", -1)
|
|
cmd := exec.Command("git", "log", "--pretty=oneline", "--no-decorate", "--color=never", page)
|
|
cmd.Dir = fp.dirname
|
|
output, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer output.Close()
|
|
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("while starting: %s", err)
|
|
}
|
|
|
|
buf := bufio.NewScanner(output)
|
|
|
|
var revisions []Revision
|
|
|
|
for buf.Scan() {
|
|
line := buf.Text()
|
|
start := strings.Index(line, " ")
|
|
commitId := line[0:start]
|
|
rest := line[start+1:]
|
|
pageText := gitRevision(fp.dirname, page, commitId)
|
|
if pageText == "" {
|
|
return nil, errors.New("git revision failed")
|
|
}
|
|
revisions = append(revisions, Revision{
|
|
Version: commitId,
|
|
Page: DiffPage{Content: pageText},
|
|
Summary: rest,
|
|
})
|
|
}
|
|
|
|
dmp := diffmatchpatch.New()
|
|
prevText := ""
|
|
|
|
for i := len(revisions) - 1; i >= 0; i-- {
|
|
diffs := dmp.DiffMain(prevText, revisions[i].Page.Content, false)
|
|
revisions[i].Page.Diff = template.HTML(DiffPrettyHtml(diffs))
|
|
prevText = revisions[i].Page.Content
|
|
}
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
return nil, fmt.Errorf("while waiting: %s", err)
|
|
}
|
|
|
|
return revisions, nil
|
|
}
|
|
|
|
func gitRevision(dirname, page, version string) string {
|
|
cmd := exec.Command("git", "show", version+":"+page)
|
|
cmd.Dir = dirname
|
|
buf := bytes.Buffer{}
|
|
cmd.Stdout = &buf
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return buf.String()
|
|
}
|
|
|
|
func (fp *FilePages) RecentChanges() ([]Change, error) {
|
|
cmd := exec.Command("git", "log", "--format=--1--%nDate: %aI%n--2--%n%b%n--3--", "--name-only")
|
|
cmd.Dir = fp.dirname
|
|
buf := bytes.Buffer{}
|
|
cmd.Stdout = &buf
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
scanner := bufio.NewScanner(&buf)
|
|
|
|
state := 0
|
|
|
|
var changes []Change
|
|
var change Change
|
|
|
|
body := ""
|
|
|
|
for scanner.Scan() {
|
|
line := scanner.Text()
|
|
|
|
if line == "--1--" {
|
|
state = 1
|
|
|
|
body = ""
|
|
continue
|
|
}
|
|
|
|
if line == "--2--" {
|
|
state = 2
|
|
continue
|
|
}
|
|
|
|
if line == "--3--" {
|
|
state = 3
|
|
continue
|
|
}
|
|
|
|
if state == 1 && strings.HasPrefix(line, "Date: ") {
|
|
line = line[6:]
|
|
changeTime, err := time.Parse(time.RFC3339, line)
|
|
if err != nil {
|
|
return changes, err
|
|
}
|
|
change.Date = changeTime
|
|
continue
|
|
}
|
|
|
|
if state == 2 {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
body = body + line
|
|
continue
|
|
}
|
|
|
|
if state == 3 {
|
|
if line == "" {
|
|
continue
|
|
}
|
|
change.Page = line
|
|
}
|
|
|
|
change.Body = body
|
|
changes = append(changes, change)
|
|
}
|
|
|
|
return changes, nil
|
|
}
|
|
|
|
func (fp *FilePages) AllPages() ([]Page, error) {
|
|
log.Println("AllPages", fp.dirname)
|
|
|
|
files, err := ioutil.ReadDir(fp.dirname)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var pages []Page
|
|
for _, file := range files {
|
|
if file.Name()[0] == '.' || file.Name()[0] == '_' {
|
|
continue
|
|
}
|
|
if file.Name() == "backrefs.json" {
|
|
continue
|
|
}
|
|
pages = append(pages, fp.Get(file.Name()))
|
|
}
|
|
|
|
return pages, nil
|
|
}
|