From 4cf5937361a7bec15f6dacc0605021ded3512097 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Sat, 26 Sep 2020 23:19:17 +0500 Subject: [PATCH 1/8] Add recent changes page --- README.md | 10 +- go.mod | 2 +- go.sum | 8 +- history/history.go | 53 +++++++++++ history/information.go | 25 ++++- main.go | 17 ++++ metarrhiza | 2 +- templates/recent_changes.qtpl | 40 ++++++++ templates/recent_changes.qtpl.go | 156 +++++++++++++++++++++++++++++++ 9 files changed, 303 insertions(+), 10 deletions(-) create mode 100644 templates/recent_changes.qtpl create mode 100644 templates/recent_changes.qtpl.go diff --git a/README.md b/README.md index a733e95..e87ddfc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,13 @@ -# ๐Ÿ„ MycorrhizaWiki 0.8 +# ๐Ÿ„ MycorrhizaWiki 0.9 A wiki engine. +## 0.9 +This is a development branch for 0.9 version. Features I want to implement in this release: +* [x] Recent changes page. +* [ ] Hypha deletion. +* [ ] Hypha renaming. +* [ ] Support async git ops. + ## Installation ```sh git clone --recurse-submodules https://github.com/bouncepaw/mycorrhiza @@ -31,5 +38,4 @@ Help is always needed. We have a [tg chat](https://t.me/mycorrhizadev) where som * Tagging system * Authorization * Better history viewing -* Recent changes page * More markups diff --git a/go.mod b/go.mod index 17f2ff6..e2ffb7c 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module github.com/bouncepaw/mycorrhiza go 1.14 -require github.com/valyala/quicktemplate v1.6.2 +require github.com/valyala/quicktemplate v1.6.3 diff --git a/go.sum b/go.sum index 344ceb3..a9a43a1 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,11 @@ github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= -github.com/valyala/quicktemplate v1.6.2 h1:k0vgK7zlmFzqAoIBIOrhrfmZ6JoTGJlLRPLbkPGr2/M= -github.com/valyala/quicktemplate v1.6.2/go.mod h1:mtEJpQtUiBV0SHhMX6RtiJtqxncgrfmjcUy5T68X8TM= +github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA= +github.com/valyala/quicktemplate v1.6.3 h1:O7EuMwuH7Q94U2CXD6sOX8AYHqQqWtmIk690IhmpkKA= +github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY= github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= diff --git a/history/history.go b/history/history.go index 787b358..cbd689b 100644 --- a/history/history.go +++ b/history/history.go @@ -6,6 +6,7 @@ import ( "log" "os/exec" "strconv" + "strings" "time" "github.com/bouncepaw/mycorrhiza/util" @@ -31,6 +32,58 @@ type Revision struct { Message string } +// TimeString returns a human readable time representation. +func (rev Revision) TimeString() string { + return rev.Time.Format(time.RFC822) +} + +// HyphaeLinks returns a comma-separated list of hyphae that were affected by this revision as HTML string. +func (rev Revision) HyphaeLinks() (html string) { + // diff-tree --no-commit-id --name-only -r + var ( + // List of files affected by this revision, one per line. + out, err = gitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash) + // set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most). + set = make(map[string]bool) + isNewName = func(hyphaName string) bool { + if _, present := set[hyphaName]; present { + return false + } else { + set[hyphaName] = true + return true + } + } + ) + if err != nil { + return "" + } + for _, filename := range strings.Split(out.String(), "\n") { + // If filename has an ampersand: + if strings.IndexRune(filename, '&') >= 0 { + // Remove ampersanded suffix from filename: + ampersandPos := strings.LastIndexByte(filename, '&') + hyphaName := string([]byte(filename)[0:ampersandPos]) // is it safe? + if isNewName(hyphaName) { + // Entries are separated by commas + if len(set) > 1 { + html += `` + } + html += fmt.Sprintf(`%[2]s`, rev.Hash, hyphaName) + } + } + } + return html +} + +func (rev Revision) RecentChangesEntry() (html string) { + return fmt.Sprintf(` +
  • +
  • %s
  • +
  • %s
  • +
  • %s
  • +`, rev.TimeString(), rev.Hash, rev.HyphaeLinks(), rev.Message) +} + // Path to git executable. Set at init() var gitpath string diff --git a/history/information.go b/history/information.go index ff6e654..bb20e6a 100644 --- a/history/information.go +++ b/history/information.go @@ -6,9 +6,30 @@ import ( "fmt" "regexp" "strings" - "time" + + "github.com/bouncepaw/mycorrhiza/templates" ) +func RecentChanges(n int) string { + var ( + out, err = gitsh( + "log", "--oneline", "--no-merges", + "--pretty=format:\"%h\t%ce\t%ct\t%s\"", + ) + revs []Revision + ) + if err == nil { + for _, line := range strings.Split(out.String(), "\n") { + revs = append(revs, parseRevisionLine(line)) + } + } + entries := make([]string, len(revs)) + for i, rev := range revs { + entries[i] = rev.RecentChangesEntry() + } + return templates.RecentChangesHTML(entries, n) +} + // Revisions returns slice of revisions for the given hypha name. func Revisions(hyphaName string) ([]Revision, error) { var ( @@ -48,7 +69,7 @@ func (rev *Revision) AsHtmlTableRow(hyphaName string) string { %s %s -`, rev.Time.Format(time.RFC822), rev.Hash, hyphaName, rev.Hash, rev.Message) +`, rev.TimeString(), rev.Hash, hyphaName, rev.Hash, rev.Message) } // See how the file with `filepath` looked at commit with `hash`. diff --git a/main.go b/main.go index cb9588f..5ef555d 100644 --- a/main.go +++ b/main.go @@ -10,6 +10,8 @@ import ( "os" "path/filepath" "regexp" + "strconv" + "strings" "github.com/bouncepaw/mycorrhiza/history" "github.com/bouncepaw/mycorrhiza/templates" @@ -83,6 +85,20 @@ func handlerRandom(w http.ResponseWriter, rq *http.Request) { http.Redirect(w, rq, "/page/"+randomHyphaName, http.StatusSeeOther) } +// Recent changes +func handlerRecentChanges(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + noPrefix = strings.TrimPrefix(rq.URL.String(), "/recent-changes/") + n, err = strconv.Atoi(noPrefix) + ) + if err == nil { + util.HTTP200Page(w, base(strconv.Itoa(n)+" recent changes", history.RecentChanges(n))) + } else { + http.Redirect(w, rq, "/recent-changes/20", http.StatusSeeOther) + } +} + func main() { log.Println("Running MycorrhizaWiki ฮฒ") @@ -108,6 +124,7 @@ func main() { http.HandleFunc("/list", handlerList) http.HandleFunc("/reindex", handlerReindex) http.HandleFunc("/random", handlerRandom) + http.HandleFunc("/recent-changes/", handlerRecentChanges) http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, rq *http.Request) { http.ServeFile(w, rq, WikiDir+"/static/favicon.ico") }) diff --git a/metarrhiza b/metarrhiza index bdaaab6..2c0e431 160000 --- a/metarrhiza +++ b/metarrhiza @@ -1 +1 @@ -Subproject commit bdaaab62574023487610d608d1e9f2f351707a7f +Subproject commit 2c0e43199ed28f7022a38463a0eec3af3ecb03c9 diff --git a/templates/recent_changes.qtpl b/templates/recent_changes.qtpl new file mode 100644 index 0000000..2f9f8c4 --- /dev/null +++ b/templates/recent_changes.qtpl @@ -0,0 +1,40 @@ +{% func RecentChangesHTML(changes []string, n int) %} +
    +

    Recent Changes

    + + + + {% comment %} + Here I am, willing to add some accesibility using ARIA. Turns out, + role="feed" is not supported in any screen reader as of September + 2020. At least web search says so. Even JAWS doesn't support it! + How come? I'll add the role anyway. -- bouncepaw + {% endcomment %} + +
    + {% if len(changes) == 0 %} +

    Could not find any recent changes.

    + {% else %} + {% for i, entry := range changes %} +
      + {%s= entry %} +
    + {% endfor %} + {% endif %} +
    +
    +{% endfunc %} diff --git a/templates/recent_changes.qtpl.go b/templates/recent_changes.qtpl.go new file mode 100644 index 0000000..3814504 --- /dev/null +++ b/templates/recent_changes.qtpl.go @@ -0,0 +1,156 @@ +// Code generated by qtc from "recent_changes.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +//line templates/recent_changes.qtpl:1 +package templates + +//line templates/recent_changes.qtpl:1 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line templates/recent_changes.qtpl:1 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line templates/recent_changes.qtpl:1 +func StreamRecentChangesHTML(qw422016 *qt422016.Writer, changes []string, n int) { +//line templates/recent_changes.qtpl:1 + qw422016.N().S(` +
    +

    Recent Changes

    + + + + `) +//line templates/recent_changes.qtpl:25 + qw422016.N().S(` + +
    + `) +//line templates/recent_changes.qtpl:28 + if len(changes) == 0 { +//line templates/recent_changes.qtpl:28 + qw422016.N().S(` +

    Could not find any recent changes.

    + `) +//line templates/recent_changes.qtpl:30 + } else { +//line templates/recent_changes.qtpl:30 + qw422016.N().S(` + `) +//line templates/recent_changes.qtpl:31 + for i, entry := range changes { +//line templates/recent_changes.qtpl:31 + qw422016.N().S(` +
      + `) +//line templates/recent_changes.qtpl:34 + qw422016.N().S(entry) +//line templates/recent_changes.qtpl:34 + qw422016.N().S(` +
    + `) +//line templates/recent_changes.qtpl:36 + } +//line templates/recent_changes.qtpl:36 + qw422016.N().S(` + `) +//line templates/recent_changes.qtpl:37 + } +//line templates/recent_changes.qtpl:37 + qw422016.N().S(` +
    +
    +`) +//line templates/recent_changes.qtpl:40 +} + +//line templates/recent_changes.qtpl:40 +func WriteRecentChangesHTML(qq422016 qtio422016.Writer, changes []string, n int) { +//line templates/recent_changes.qtpl:40 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/recent_changes.qtpl:40 + StreamRecentChangesHTML(qw422016, changes, n) +//line templates/recent_changes.qtpl:40 + qt422016.ReleaseWriter(qw422016) +//line templates/recent_changes.qtpl:40 +} + +//line templates/recent_changes.qtpl:40 +func RecentChangesHTML(changes []string, n int) string { +//line templates/recent_changes.qtpl:40 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/recent_changes.qtpl:40 + WriteRecentChangesHTML(qb422016, changes, n) +//line templates/recent_changes.qtpl:40 + qs422016 := string(qb422016.B) +//line templates/recent_changes.qtpl:40 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/recent_changes.qtpl:40 + return qs422016 +//line templates/recent_changes.qtpl:40 +} From 00bc9d4b17ba60b2236a6d9e10b5b63c30ecca99 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Tue, 29 Sep 2020 20:04:22 +0500 Subject: [PATCH 2/8] Make hyphae deletable --- history/information.go | 4 +- history/operations.go | 13 +++ http_mutators.go | 36 +++++++ http_readers.go | 20 ++-- hypha.go | 10 ++ main.go | 2 +- metarrhiza | 2 +- templates/http_delete.qtpl | 33 ++++++ templates/http_delete.qtpl.go | 171 +++++++++++++++++++++++++++++++ templates/http_readers.qtpl | 3 + templates/http_readers.qtpl.go | 181 ++++++++++++++++++--------------- 11 files changed, 379 insertions(+), 96 deletions(-) create mode 100644 templates/http_delete.qtpl create mode 100644 templates/http_delete.qtpl.go diff --git a/history/information.go b/history/information.go index bb20e6a..03d6aae 100644 --- a/history/information.go +++ b/history/information.go @@ -43,7 +43,9 @@ func Revisions(hyphaName string) ([]Revision, error) { ) if err == nil { for _, line := range strings.Split(out.String(), "\n") { - revs = append(revs, parseRevisionLine(line)) + if line != "" { + revs = append(revs, parseRevisionLine(line)) + } } } return revs, err diff --git a/history/operations.go b/history/operations.go index 3567d78..fecee81 100644 --- a/history/operations.go +++ b/history/operations.go @@ -15,6 +15,8 @@ const ( TypeNone OpType = iota TypeEditText TypeEditBinary + TypeDeleteHypha + TypeRenameHypha ) // HistoryOp is an object representing a history operation. @@ -46,6 +48,17 @@ func (hop *HistoryOp) gitop(args ...string) *HistoryOp { return hop } +// WithFilesRemoved git-rm-s all passed `paths`. Paths can be rooted or not. Paths that are empty strings are ignored. +func (hop *HistoryOp) WithFilesRemoved(paths ...string) *HistoryOp { + args := []string{"rm", "--quiet", "--"} + for _, path := range paths { + if path != "" { + args = append(args, path) + } + } + return hop.gitop(args...) +} + // WithFiles stages all passed `paths`. Paths can be rooted or not. func (hop *HistoryOp) WithFiles(paths ...string) *HistoryOp { for i, path := range paths { diff --git a/http_mutators.go b/http_mutators.go index e3cd397..3557a87 100644 --- a/http_mutators.go +++ b/http_mutators.go @@ -17,6 +17,42 @@ func init() { http.HandleFunc("/upload-binary/", handlerUploadBinary) http.HandleFunc("/upload-text/", handlerUploadText) http.HandleFunc("/edit/", handlerEdit) + http.HandleFunc("/delete-ask/", handlerDeleteAsk) + http.HandleFunc("/delete-confirm/", handlerDeleteConfirm) +} + +// handlerDeleteAsk shows a delete dialog. +func handlerDeleteAsk(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + hyphaName = HyphaNameFromRq(rq, "delete-ask") + _, isOld = HyphaStorage[hyphaName] + ) + util.HTTP200Page(w, base("Delete "+hyphaName+"?", templates.DeleteAskHTML(hyphaName, isOld))) +} + +// handlerDeleteConfirm deletes a hypha for sure +func handlerDeleteConfirm(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + hyphaName = HyphaNameFromRq(rq, "delete-confirm") + hyphaData, isOld = HyphaStorage[hyphaName] + ) + if isOld { + // If deleted successfully + if hop := hyphaData.DeleteHypha(hyphaName); len(hop.Errs) == 0 { + http.Redirect(w, rq, "/page/"+hyphaName, http.StatusSeeOther) + } else { + HttpErr(w, http.StatusInternalServerError, hyphaName, + "Error: could not delete hypha", + fmt.Sprintf("Could not delete this hypha due to an internal error. Server errors: %v", hop.Errs)) + } + } else { + // The precondition is to have the hypha in the first place. + HttpErr(w, http.StatusPreconditionFailed, hyphaName, + "Error: no such hypha", + "Could not delete this hypha because it does not exist.") + } } // handlerEdit shows the edit form. It doesn't edit anything actually. diff --git a/http_readers.go b/http_readers.go index f288210..5843e4d 100644 --- a/http_readers.go +++ b/http_readers.go @@ -6,7 +6,6 @@ import ( "log" "net/http" "os" - "path" "strings" "github.com/bouncepaw/mycorrhiza/gemtext" @@ -29,8 +28,9 @@ func handlerRevision(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) var ( shorterUrl = strings.TrimPrefix(rq.URL.Path, "/rev/") - revHash = path.Dir(shorterUrl) - hyphaName = CanonicalName(strings.TrimPrefix(shorterUrl, revHash+"/")) + firstSlashIndex = strings.IndexRune(shorterUrl, '/') + revHash = shorterUrl[:firstSlashIndex] + hyphaName = CanonicalName(shorterUrl[firstSlashIndex+1:]) contents = fmt.Sprintf(`

    This hypha had no text at this revision.

    `) textPath = hyphaName + "&.gmi" textContents, err = history.FileAtRevision(textPath, revHash) @@ -55,15 +55,15 @@ func handlerHistory(w http.ResponseWriter, rq *http.Request) { log.Println(rq.URL) hyphaName := HyphaNameFromRq(rq, "history") var tbody string - if _, ok := HyphaStorage[hyphaName]; ok { - revs, err := history.Revisions(hyphaName) - if err == nil { - for _, rev := range revs { - tbody += rev.AsHtmlTableRow(hyphaName) - } + + // History can be found for files that do not exist anymore. + revs, err := history.Revisions(hyphaName) + if err == nil { + for _, rev := range revs { + tbody += rev.AsHtmlTableRow(hyphaName) } - log.Println(revs) } + log.Println("Found", len(revs), "revisions for", hyphaName) util.HTTP200Page(w, base(hyphaName, templates.HistoryHTML(hyphaName, tbody))) diff --git a/hypha.go b/hypha.go index d3be116..7a37cb9 100644 --- a/hypha.go +++ b/hypha.go @@ -9,6 +9,7 @@ import ( "path/filepath" "github.com/bouncepaw/mycorrhiza/gemtext" + "github.com/bouncepaw/mycorrhiza/history" ) func init() { @@ -37,6 +38,15 @@ type HyphaData struct { binaryType BinaryType } +// DeleteHypha deletes hypha and makes a history record about that. +func (hd *HyphaData) DeleteHypha(hyphaName string) *history.HistoryOp { + return history.Operation(history.TypeDeleteHypha). + WithFilesRemoved(hd.textPath, hd.binaryPath). + WithMsg(fmt.Sprintf("Delete โ€˜%sโ€™", hyphaName)). + WithSignature("anon"). + Apply() +} + // binaryHtmlBlock creates an html block for binary part of the hypha. func binaryHtmlBlock(hyphaName string, d *HyphaData) string { switch d.binaryType { diff --git a/main.go b/main.go index 5ef555d..981ab4b 100644 --- a/main.go +++ b/main.go @@ -120,7 +120,7 @@ func main() { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static")))) // See http_readers.go for /page/, /text/, /binary/, /history/. - // See http_mutators.go for /upload-binary/, /upload-text/, /edit/. + // See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/. http.HandleFunc("/list", handlerList) http.HandleFunc("/reindex", handlerReindex) http.HandleFunc("/random", handlerRandom) diff --git a/metarrhiza b/metarrhiza index 2c0e431..ecaa76f 160000 --- a/metarrhiza +++ b/metarrhiza @@ -1 +1 @@ -Subproject commit 2c0e43199ed28f7022a38463a0eec3af3ecb03c9 +Subproject commit ecaa76f841afcb3514b7061eb6708092bc17ee08 diff --git a/templates/http_delete.qtpl b/templates/http_delete.qtpl new file mode 100644 index 0000000..4044c09 --- /dev/null +++ b/templates/http_delete.qtpl @@ -0,0 +1,33 @@ +This dialog is to be shown to a user when they try to delete a hypha. +{% func DeleteAskHTML(hyphaName string, isOld bool) %} +
    + +{% if isOld %} +
    +

    Delete {%s hyphaName %}?

    +

    Do you really want to delete hypha {%s hyphaName %}?

    +

    In this version of MycorrhizaWiki you cannot undelete a deleted hypha but the history can still be accessed.

    +

    Confirm

    +

    Cancel

    +
    +{% else %} + {%= cannotDeleteDueToNonExistence(hyphaName) %} +{% endif %} +
    +{% endfunc %} + +{% func cannotDeleteDueToNonExistence(hyphaName string) %} +
    +

    Cannot delete {%s hyphaName %}

    +

    This hypha does not exist.

    +

    Go back

    +
    +{% endfunc %} diff --git a/templates/http_delete.qtpl.go b/templates/http_delete.qtpl.go new file mode 100644 index 0000000..297b5ec --- /dev/null +++ b/templates/http_delete.qtpl.go @@ -0,0 +1,171 @@ +// Code generated by qtc from "http_delete.qtpl". DO NOT EDIT. +// See https://github.com/valyala/quicktemplate for details. + +// This dialog is to be shown to a user when they try to delete a hypha. + +//line templates/http_delete.qtpl:2 +package templates + +//line templates/http_delete.qtpl:2 +import ( + qtio422016 "io" + + qt422016 "github.com/valyala/quicktemplate" +) + +//line templates/http_delete.qtpl:2 +var ( + _ = qtio422016.Copy + _ = qt422016.AcquireByteBuffer +) + +//line templates/http_delete.qtpl:2 +func StreamDeleteAskHTML(qw422016 *qt422016.Writer, hyphaName string, isOld bool) { +//line templates/http_delete.qtpl:2 + qw422016.N().S(` +
    + +`) +//line templates/http_delete.qtpl:13 + if isOld { +//line templates/http_delete.qtpl:13 + qw422016.N().S(` +
    +

    Delete `) +//line templates/http_delete.qtpl:15 + qw422016.E().S(hyphaName) +//line templates/http_delete.qtpl:15 + qw422016.N().S(`?

    +

    Do you really want to delete hypha `) +//line templates/http_delete.qtpl:16 + qw422016.E().S(hyphaName) +//line templates/http_delete.qtpl:16 + qw422016.N().S(`?

    +

    In this version of MycorrhizaWiki you cannot undelete a deleted hypha but the history can still be accessed.

    +

    Confirm

    +

    Cancel

    +
    +`) +//line templates/http_delete.qtpl:21 + } else { +//line templates/http_delete.qtpl:21 + qw422016.N().S(` + `) +//line templates/http_delete.qtpl:22 + streamcannotDeleteDueToNonExistence(qw422016, hyphaName) +//line templates/http_delete.qtpl:22 + qw422016.N().S(` +`) +//line templates/http_delete.qtpl:23 + } +//line templates/http_delete.qtpl:23 + qw422016.N().S(` +
    +`) +//line templates/http_delete.qtpl:25 +} + +//line templates/http_delete.qtpl:25 +func WriteDeleteAskHTML(qq422016 qtio422016.Writer, hyphaName string, isOld bool) { +//line templates/http_delete.qtpl:25 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_delete.qtpl:25 + StreamDeleteAskHTML(qw422016, hyphaName, isOld) +//line templates/http_delete.qtpl:25 + qt422016.ReleaseWriter(qw422016) +//line templates/http_delete.qtpl:25 +} + +//line templates/http_delete.qtpl:25 +func DeleteAskHTML(hyphaName string, isOld bool) string { +//line templates/http_delete.qtpl:25 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_delete.qtpl:25 + WriteDeleteAskHTML(qb422016, hyphaName, isOld) +//line templates/http_delete.qtpl:25 + qs422016 := string(qb422016.B) +//line templates/http_delete.qtpl:25 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_delete.qtpl:25 + return qs422016 +//line templates/http_delete.qtpl:25 +} + +//line templates/http_delete.qtpl:27 +func streamcannotDeleteDueToNonExistence(qw422016 *qt422016.Writer, hyphaName string) { +//line templates/http_delete.qtpl:27 + qw422016.N().S(` +
    +

    Cannot delete `) +//line templates/http_delete.qtpl:29 + qw422016.E().S(hyphaName) +//line templates/http_delete.qtpl:29 + qw422016.N().S(`

    +

    This hypha does not exist.

    +

    Go back

    +
    +`) +//line templates/http_delete.qtpl:33 +} + +//line templates/http_delete.qtpl:33 +func writecannotDeleteDueToNonExistence(qq422016 qtio422016.Writer, hyphaName string) { +//line templates/http_delete.qtpl:33 + qw422016 := qt422016.AcquireWriter(qq422016) +//line templates/http_delete.qtpl:33 + streamcannotDeleteDueToNonExistence(qw422016, hyphaName) +//line templates/http_delete.qtpl:33 + qt422016.ReleaseWriter(qw422016) +//line templates/http_delete.qtpl:33 +} + +//line templates/http_delete.qtpl:33 +func cannotDeleteDueToNonExistence(hyphaName string) string { +//line templates/http_delete.qtpl:33 + qb422016 := qt422016.AcquireByteBuffer() +//line templates/http_delete.qtpl:33 + writecannotDeleteDueToNonExistence(qb422016, hyphaName) +//line templates/http_delete.qtpl:33 + qs422016 := string(qb422016.B) +//line templates/http_delete.qtpl:33 + qt422016.ReleaseByteBuffer(qb422016) +//line templates/http_delete.qtpl:33 + return qs422016 +//line templates/http_delete.qtpl:33 +} diff --git a/templates/http_readers.qtpl b/templates/http_readers.qtpl index 0c3b4ea..cf85467 100644 --- a/templates/http_readers.qtpl +++ b/templates/http_readers.qtpl @@ -6,6 +6,7 @@
  • Edit
  • Raw text
  • History
  • +
  • Delete
  • @@ -32,6 +33,7 @@
  • Raw text
  • History
  • {%s revHash %}
  • +
  • Delete
  • @@ -55,6 +57,7 @@ If `contents` == "", a helpful message is shown instead.
  • Edit
  • Raw text
  • History
  • +
  • Delete
  • diff --git a/templates/http_readers.qtpl.go b/templates/http_readers.qtpl.go index 97d4a0b..fdccdf4 100644 --- a/templates/http_readers.qtpl.go +++ b/templates/http_readers.qtpl.go @@ -40,6 +40,11 @@ func StreamHistoryHTML(qw422016 *qt422016.Writer, hyphaName, tbody string) { //line templates/http_readers.qtpl:7 qw422016.N().S(`">Raw text
  • History
  • +
  • Delete
  • @@ -52,193 +57,203 @@ func StreamHistoryHTML(qw422016 *qt422016.Writer, hyphaName, tbody string) { `) -//line templates/http_readers.qtpl:20 +//line templates/http_readers.qtpl:21 qw422016.N().S(tbody) -//line templates/http_readers.qtpl:20 +//line templates/http_readers.qtpl:21 qw422016.N().S(`
    `) -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 } -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 func WriteHistoryHTML(qq422016 qtio422016.Writer, hyphaName, tbody string) { -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 StreamHistoryHTML(qw422016, hyphaName, tbody) -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 qt422016.ReleaseWriter(qw422016) -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 } -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 func HistoryHTML(hyphaName, tbody string) string { -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 qb422016 := qt422016.AcquireByteBuffer() -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 WriteHistoryHTML(qb422016, hyphaName, tbody) -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 qs422016 := string(qb422016.B) -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 qt422016.ReleaseByteBuffer(qb422016) -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 return qs422016 -//line templates/http_readers.qtpl:24 +//line templates/http_readers.qtpl:25 } -//line templates/http_readers.qtpl:26 +//line templates/http_readers.qtpl:27 func StreamRevisionHTML(qw422016 *qt422016.Writer, hyphaName, naviTitle, contents, tree, revHash string) { -//line templates/http_readers.qtpl:26 +//line templates/http_readers.qtpl:27 qw422016.N().S(`

    Please note that viewing binary parts of hyphae is not supported in history for now.

    `) -//line templates/http_readers.qtpl:39 +//line templates/http_readers.qtpl:41 qw422016.N().S(naviTitle) -//line templates/http_readers.qtpl:39 +//line templates/http_readers.qtpl:41 qw422016.N().S(` `) -//line templates/http_readers.qtpl:40 +//line templates/http_readers.qtpl:42 qw422016.N().S(contents) -//line templates/http_readers.qtpl:40 +//line templates/http_readers.qtpl:42 qw422016.N().S(`

    `) -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 } -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 func WriteRevisionHTML(qq422016 qtio422016.Writer, hyphaName, naviTitle, contents, tree, revHash string) { -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 StreamRevisionHTML(qw422016, hyphaName, naviTitle, contents, tree, revHash) -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 qt422016.ReleaseWriter(qw422016) -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 } -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 func RevisionHTML(hyphaName, naviTitle, contents, tree, revHash string) string { -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 qb422016 := qt422016.AcquireByteBuffer() -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 WriteRevisionHTML(qb422016, hyphaName, naviTitle, contents, tree, revHash) -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 qs422016 := string(qb422016.B) -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 qt422016.ReleaseByteBuffer(qb422016) -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 return qs422016 -//line templates/http_readers.qtpl:47 +//line templates/http_readers.qtpl:49 } // If `contents` == "", a helpful message is shown instead. -//line templates/http_readers.qtpl:50 +//line templates/http_readers.qtpl:52 func StreamPageHTML(qw422016 *qt422016.Writer, hyphaName, naviTitle, contents, tree string) { -//line templates/http_readers.qtpl:50 +//line templates/http_readers.qtpl:52 qw422016.N().S(`
    `) -//line templates/http_readers.qtpl:61 +//line templates/http_readers.qtpl:64 qw422016.N().S(naviTitle) -//line templates/http_readers.qtpl:61 +//line templates/http_readers.qtpl:64 qw422016.N().S(` `) -//line templates/http_readers.qtpl:62 +//line templates/http_readers.qtpl:65 if contents == "" { -//line templates/http_readers.qtpl:62 +//line templates/http_readers.qtpl:65 qw422016.N().S(`

    This hypha has no text. Why not create it?

    `) -//line templates/http_readers.qtpl:64 +//line templates/http_readers.qtpl:67 } else { -//line templates/http_readers.qtpl:64 +//line templates/http_readers.qtpl:67 qw422016.N().S(` `) -//line templates/http_readers.qtpl:65 +//line templates/http_readers.qtpl:68 qw422016.N().S(contents) -//line templates/http_readers.qtpl:65 +//line templates/http_readers.qtpl:68 qw422016.N().S(` `) -//line templates/http_readers.qtpl:66 +//line templates/http_readers.qtpl:69 } -//line templates/http_readers.qtpl:66 +//line templates/http_readers.qtpl:69 qw422016.N().S(`

    @@ -249,38 +264,38 @@ func StreamPageHTML(qw422016 *qt422016.Writer, hyphaName, naviTitle, contents, t
    `) -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 } -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 func WritePageHTML(qq422016 qtio422016.Writer, hyphaName, naviTitle, contents, tree string) { -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 qw422016 := qt422016.AcquireWriter(qq422016) -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 StreamPageHTML(qw422016, hyphaName, naviTitle, contents, tree) -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 qt422016.ReleaseWriter(qw422016) -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 } -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 func PageHTML(hyphaName, naviTitle, contents, tree string) string { -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 qb422016 := qt422016.AcquireByteBuffer() -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 WritePageHTML(qb422016, hyphaName, naviTitle, contents, tree) -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 qs422016 := string(qb422016.B) -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 qt422016.ReleaseByteBuffer(qb422016) -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 return qs422016 -//line templates/http_readers.qtpl:81 +//line templates/http_readers.qtpl:84 } From 3a1d1c2ce2c641404705059cb739ff21262080cc Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Tue, 29 Sep 2020 23:13:24 +0500 Subject: [PATCH 3/8] Support async gitops --- README.md | 4 ++-- history/history.go | 2 +- history/operations.go | 6 ++++++ metarrhiza | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index e87ddfc..b1ea98a 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ A wiki engine. ## 0.9 This is a development branch for 0.9 version. Features I want to implement in this release: * [x] Recent changes page. -* [ ] Hypha deletion. +* [x] Hypha deletion. * [ ] Hypha renaming. -* [ ] Support async git ops. +* [x] Support async git ops. ## Installation ```sh diff --git a/history/history.go b/history/history.go index cbd689b..c57ef96 100644 --- a/history/history.go +++ b/history/history.go @@ -99,10 +99,10 @@ func init() { } // I pronounce it as [gษชtอกสƒ]. +// gitsh is async-safe, therefore all other git-related functions in this module are too. func gitsh(args ...string) (out bytes.Buffer, err error) { fmt.Printf("$ %v\n", args) cmd := exec.Command(gitpath, args...) - cmd.Dir = util.WikiDir b, err := cmd.CombinedOutput() diff --git a/history/operations.go b/history/operations.go index fecee81..149e795 100644 --- a/history/operations.go +++ b/history/operations.go @@ -4,10 +4,14 @@ package history import ( "fmt" + "sync" "github.com/bouncepaw/mycorrhiza/util" ) +// gitMutex is used for blocking git operations to avoid clashes. +var gitMutex = sync.Mutex{} + // OpType is the type a history operation has. Callers shall set appropriate optypes when creating history operations. type OpType int @@ -31,6 +35,7 @@ type HistoryOp struct { // Operation is a constructor of a history operation. func Operation(opType OpType) *HistoryOp { + gitMutex.Lock() hop := &HistoryOp{ Errs: []error{}, opType: opType, @@ -75,6 +80,7 @@ func (hop *HistoryOp) Apply() *HistoryOp { "--author='"+hop.name+" <"+hop.email+">'", "--message="+hop.userMsg, ) + gitMutex.Unlock() return hop } diff --git a/metarrhiza b/metarrhiza index ecaa76f..2c0e431 160000 --- a/metarrhiza +++ b/metarrhiza @@ -1 +1 @@ -Subproject commit ecaa76f841afcb3514b7061eb6708092bc17ee08 +Subproject commit 2c0e43199ed28f7022a38463a0eec3af3ecb03c9 From 2add791ba9189d18cd0852a0f6e14db50d68bbbf Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Fri, 2 Oct 2020 20:31:59 +0500 Subject: [PATCH 4/8] Add renaming support --- README.md | 2 +- history/operations.go | 10 + http_mutators.go | 43 ++++ hypha.go | 24 ++ main.go | 4 +- metarrhiza | 2 +- templates/common.qtpl | 32 +++ templates/common.qtpl.go | 117 ++++++++++ templates/http_delete.qtpl | 10 +- templates/http_delete.qtpl.go | 126 +++++----- templates/http_mutators.qtpl | 1 - templates/http_mutators.qtpl.go | 56 ++--- templates/http_readers.qtpl | 125 ++++------ templates/http_readers.qtpl.go | 395 +++++++++++++------------------- templates/rename.qtpl | 27 +++ templates/rename.qtpl.go | 146 ++++++++++++ 16 files changed, 698 insertions(+), 422 deletions(-) create mode 100644 templates/common.qtpl create mode 100644 templates/common.qtpl.go create mode 100644 templates/rename.qtpl create mode 100644 templates/rename.qtpl.go diff --git a/README.md b/README.md index b1ea98a..c7b62f0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A wiki engine. This is a development branch for 0.9 version. Features I want to implement in this release: * [x] Recent changes page. * [x] Hypha deletion. -* [ ] Hypha renaming. +* [x] Hypha renaming. * [x] Support async git ops. ## Installation diff --git a/history/operations.go b/history/operations.go index 149e795..7b94436 100644 --- a/history/operations.go +++ b/history/operations.go @@ -64,6 +64,16 @@ func (hop *HistoryOp) WithFilesRemoved(paths ...string) *HistoryOp { return hop.gitop(args...) } +// WithFilesRenamed git-mv-s all passed keys of `pairs` to values of `pairs`. Paths can be rooted ot not. Empty keys are ignored. +func (hop *HistoryOp) WithFilesRenamed(pairs map[string]string) *HistoryOp { + for from, to := range pairs { + if from != "" { + hop.gitop(append([]string{"mv"}, from, to)...) + } + } + return hop +} + // WithFiles stages all passed `paths`. Paths can be rooted or not. func (hop *HistoryOp) WithFiles(paths ...string) *HistoryOp { for i, path := range paths { diff --git a/http_mutators.go b/http_mutators.go index 3557a87..d7569f5 100644 --- a/http_mutators.go +++ b/http_mutators.go @@ -19,6 +19,49 @@ func init() { http.HandleFunc("/edit/", handlerEdit) http.HandleFunc("/delete-ask/", handlerDeleteAsk) http.HandleFunc("/delete-confirm/", handlerDeleteConfirm) + http.HandleFunc("/rename-ask/", handlerRenameAsk) + http.HandleFunc("/rename-confirm/", handlerRenameConfirm) +} + +func handlerRenameAsk(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + hyphaName = HyphaNameFromRq(rq, "rename-ask") + _, isOld = HyphaStorage[hyphaName] + ) + util.HTTP200Page(w, base("Rename "+hyphaName+"?", templates.RenameAskHTML(hyphaName, isOld))) +} + +func handlerRenameConfirm(w http.ResponseWriter, rq *http.Request) { + log.Println(rq.URL) + var ( + hyphaName = HyphaNameFromRq(rq, "rename-confirm") + hyphaData, isOld = HyphaStorage[hyphaName] + newName = CanonicalName(rq.PostFormValue("new-name")) + _, newNameIsUsed = HyphaStorage[newName] + ) + switch { + case newNameIsUsed: + HttpErr(w, http.StatusBadRequest, hyphaName, "Error: hypha exists", + fmt.Sprintf("Hypha named %s already exists.", hyphaName, hyphaName)) + case newName == "": + HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no name", + "No new name is given.") + case !isOld: + HttpErr(w, http.StatusBadRequest, hyphaName, "Error: no such hypha", + "Cannot rename a hypha that does not exist yet.") + case !HyphaPattern.MatchString(newName): + HttpErr(w, http.StatusBadRequest, hyphaName, "Error: invalid name", + "Invalid new name. Names cannot contain characters ^?!:#@><*|\"\\'&%") + default: + if hop := hyphaData.RenameHypha(hyphaName, newName); len(hop.Errs) == 0 { + http.Redirect(w, rq, "/page/"+newName, http.StatusSeeOther) + } else { + HttpErr(w, http.StatusInternalServerError, hyphaName, + "Error: could not rename hypha", + fmt.Sprintf("Could not rename this hypha due to an internal error. Server errors: %v", hop.Errs)) + } + } } // handlerDeleteAsk shows a delete dialog. diff --git a/hypha.go b/hypha.go index 7a37cb9..5c9bb9e 100644 --- a/hypha.go +++ b/hypha.go @@ -7,6 +7,7 @@ import ( "log" "os" "path/filepath" + "strings" "github.com/bouncepaw/mycorrhiza/gemtext" "github.com/bouncepaw/mycorrhiza/history" @@ -47,6 +48,29 @@ func (hd *HyphaData) DeleteHypha(hyphaName string) *history.HistoryOp { Apply() } +// RenameHypha renames hypha from old name `hyphaName` to `newName` and makes a history record about that. +func (hd *HyphaData) RenameHypha(hyphaName, newName string) *history.HistoryOp { + var ( + newTextPath = strings.Replace(hd.textPath, hyphaName, newName, 1) + newBinaryPath = strings.Replace(hd.binaryPath, hyphaName, newName, 1) + hop = history.Operation(history.TypeRenameHypha). + WithFilesRenamed(map[string]string{ + hd.textPath: newTextPath, + hd.binaryPath: newBinaryPath, + }). + WithMsg(fmt.Sprintf("Rename โ€˜%sโ€™ to โ€˜%sโ€™", hyphaName, newName)). + WithSignature("anon"). + Apply() + ) + if len(hop.Errs) == 0 { + hd.textPath = newTextPath + hd.binaryPath = newBinaryPath + HyphaStorage[newName] = hd + delete(HyphaStorage, hyphaName) + } + return hop +} + // binaryHtmlBlock creates an html block for binary part of the hypha. func binaryHtmlBlock(hyphaName string, d *HyphaData) string { switch d.binaryType { diff --git a/main.go b/main.go index 981ab4b..7956270 100644 --- a/main.go +++ b/main.go @@ -21,7 +21,7 @@ import ( // WikiDir is a rooted path to the wiki storage directory. var WikiDir string -// HyphaPattern is a pattern which all hyphae must match. Not used currently. +// HyphaPattern is a pattern which all hyphae must match. var HyphaPattern = regexp.MustCompile(`[^?!:#@><*|"\'&%]+`) // HyphaStorage is a mapping between canonical hypha names and their meta information. @@ -120,7 +120,7 @@ func main() { http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir(WikiDir+"/static")))) // See http_readers.go for /page/, /text/, /binary/, /history/. - // See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/. + // See http_mutators.go for /upload-binary/, /upload-text/, /edit/, /delete-ask/, /delete-confirm/, /rename-ask/, /rename-confirm/. http.HandleFunc("/list", handlerList) http.HandleFunc("/reindex", handlerReindex) http.HandleFunc("/random", handlerRandom) diff --git a/metarrhiza b/metarrhiza index 2c0e431..c21b477 160000 --- a/metarrhiza +++ b/metarrhiza @@ -1 +1 @@ -Subproject commit 2c0e43199ed28f7022a38463a0eec3af3ecb03c9 +Subproject commit c21b47739bc149456acc205e2c5acfa4b9eeb9d7 diff --git a/templates/common.qtpl b/templates/common.qtpl new file mode 100644 index 0000000..c199fa2 --- /dev/null +++ b/templates/common.qtpl @@ -0,0 +1,32 @@ +This is the