2021-05-11 10:14:00 +00:00
|
|
|
|
// Package history provides a git wrapper.
|
2020-08-08 20:10:28 +00:00
|
|
|
|
package history
|
|
|
|
|
|
|
|
|
|
import (
|
2020-08-29 17:54:57 +00:00
|
|
|
|
"bytes"
|
2020-08-08 20:10:28 +00:00
|
|
|
|
"fmt"
|
2021-05-03 18:31:19 +00:00
|
|
|
|
"html"
|
2020-08-08 20:10:28 +00:00
|
|
|
|
"log"
|
2020-08-29 17:54:57 +00:00
|
|
|
|
"os/exec"
|
2020-12-08 15:15:32 +00:00
|
|
|
|
"regexp"
|
2020-08-29 17:54:57 +00:00
|
|
|
|
"strconv"
|
2020-09-26 18:19:17 +00:00
|
|
|
|
"strings"
|
2020-08-29 17:54:57 +00:00
|
|
|
|
"time"
|
2020-08-08 20:10:28 +00:00
|
|
|
|
|
2021-05-11 10:14:00 +00:00
|
|
|
|
"github.com/bouncepaw/mycorrhiza/cfg"
|
2020-08-09 19:33:47 +00:00
|
|
|
|
"github.com/bouncepaw/mycorrhiza/util"
|
2020-08-08 20:10:28 +00:00
|
|
|
|
)
|
|
|
|
|
|
2021-03-09 14:27:14 +00:00
|
|
|
|
// Path to git executable. Set at init()
|
|
|
|
|
var gitpath string
|
|
|
|
|
|
2020-12-08 15:15:32 +00:00
|
|
|
|
var renameMsgPattern = regexp.MustCompile(`^Rename ‘(.*)’ to ‘.*’`)
|
|
|
|
|
|
2021-03-09 14:27:14 +00:00
|
|
|
|
// Start finds git and initializes git credentials.
|
2021-05-11 10:14:00 +00:00
|
|
|
|
func Start() {
|
2021-03-09 14:27:14 +00:00
|
|
|
|
path, err := exec.LookPath("git")
|
|
|
|
|
if err != nil {
|
2021-05-11 10:14:00 +00:00
|
|
|
|
log.Fatal("Could not find the git executable. Check your $PATH.")
|
2021-03-09 14:27:14 +00:00
|
|
|
|
}
|
|
|
|
|
gitpath = path
|
|
|
|
|
|
2021-05-03 18:33:14 +00:00
|
|
|
|
_, err = silentGitsh("config", "user.name", "wikimind")
|
2020-08-08 20:10:28 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
2021-05-03 18:33:14 +00:00
|
|
|
|
_, err = silentGitsh("config", "user.email", "wikimind@mycorrhiza")
|
2020-08-08 20:10:28 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-29 17:54:57 +00:00
|
|
|
|
// Revision represents a revision, duh. Hash is usually short. Username is extracted from email.
|
|
|
|
|
type Revision struct {
|
2020-12-08 15:15:32 +00:00
|
|
|
|
Hash string
|
|
|
|
|
Username string
|
|
|
|
|
Time time.Time
|
|
|
|
|
Message string
|
2021-05-03 18:31:19 +00:00
|
|
|
|
filesAffectedBuf []string
|
2020-12-08 15:15:32 +00:00
|
|
|
|
hyphaeAffectedBuf []string
|
2020-09-26 18:19:17 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-03 18:31:19 +00:00
|
|
|
|
// filesAffected tells what files have been affected by the revision.
|
|
|
|
|
func (rev *Revision) filesAffected() (filenames []string) {
|
|
|
|
|
if nil != rev.filesAffectedBuf {
|
|
|
|
|
return rev.filesAffectedBuf
|
|
|
|
|
}
|
|
|
|
|
// List of files affected by this revision, one per line.
|
|
|
|
|
out, err := silentGitsh("diff-tree", "--no-commit-id", "--name-only", "-r", rev.Hash)
|
|
|
|
|
// There's an error? Well, whatever, let's just assign an empty slice, who cares.
|
|
|
|
|
if err != nil {
|
|
|
|
|
rev.filesAffectedBuf = []string{}
|
|
|
|
|
} else {
|
|
|
|
|
rev.filesAffectedBuf = strings.Split(out.String(), "\n")
|
|
|
|
|
}
|
|
|
|
|
return rev.filesAffectedBuf
|
|
|
|
|
}
|
|
|
|
|
|
2020-12-08 15:15:32 +00:00
|
|
|
|
// determine what hyphae were affected by this revision
|
|
|
|
|
func (rev *Revision) hyphaeAffected() (hyphae []string) {
|
|
|
|
|
if nil != rev.hyphaeAffectedBuf {
|
|
|
|
|
return rev.hyphaeAffectedBuf
|
|
|
|
|
}
|
|
|
|
|
hyphae = make([]string, 0)
|
2020-09-26 18:19:17 +00:00
|
|
|
|
var (
|
2020-12-08 15:15:32 +00:00
|
|
|
|
// set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently).
|
2020-09-26 18:19:17 +00:00
|
|
|
|
set = make(map[string]bool)
|
|
|
|
|
isNewName = func(hyphaName string) bool {
|
|
|
|
|
if _, present := set[hyphaName]; present {
|
|
|
|
|
return false
|
|
|
|
|
}
|
2020-12-08 15:15:32 +00:00
|
|
|
|
set[hyphaName] = true
|
|
|
|
|
return true
|
2020-09-26 18:19:17 +00:00
|
|
|
|
}
|
2021-05-03 18:31:19 +00:00
|
|
|
|
filesAffected = rev.filesAffected()
|
2020-09-26 18:19:17 +00:00
|
|
|
|
)
|
2021-05-03 18:31:19 +00:00
|
|
|
|
for _, filename := range filesAffected {
|
2020-10-25 13:50:14 +00:00
|
|
|
|
if strings.IndexRune(filename, '.') >= 0 {
|
2020-12-08 15:15:32 +00:00
|
|
|
|
dotPos := strings.LastIndexByte(filename, '.')
|
|
|
|
|
hyphaName := string([]byte(filename)[0:dotPos]) // is it safe?
|
2020-09-26 18:19:17 +00:00
|
|
|
|
if isNewName(hyphaName) {
|
2020-12-08 15:15:32 +00:00
|
|
|
|
hyphae = append(hyphae, hyphaName)
|
2020-09-26 18:19:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-12-08 15:15:32 +00:00
|
|
|
|
rev.hyphaeAffectedBuf = hyphae
|
|
|
|
|
return hyphae
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TimeString returns a human readable time representation.
|
|
|
|
|
func (rev Revision) TimeString() string {
|
|
|
|
|
return rev.Time.Format(time.RFC822)
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-20 16:50:25 +00:00
|
|
|
|
// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string.
|
|
|
|
|
func (rev Revision) HyphaeLinksHTML() (html string) {
|
2020-12-08 15:15:32 +00:00
|
|
|
|
hyphae := rev.hyphaeAffected()
|
|
|
|
|
for i, hyphaName := range hyphae {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
html += `<span aria-hidden="true">, </span>`
|
|
|
|
|
}
|
2021-05-03 18:31:19 +00:00
|
|
|
|
html += fmt.Sprintf(`<a href="/hypha/%[1]s">%[1]s</a>`, hyphaName)
|
2020-12-08 15:15:32 +00:00
|
|
|
|
}
|
2020-09-26 18:19:17 +00:00
|
|
|
|
return html
|
|
|
|
|
}
|
|
|
|
|
|
2021-05-03 18:31:19 +00:00
|
|
|
|
// descriptionForFeed generates a good enough HTML contents for a web feed.
|
|
|
|
|
func (rev *Revision) descriptionForFeed() (htmlDesc string) {
|
2020-12-08 15:15:32 +00:00
|
|
|
|
return fmt.Sprintf(
|
|
|
|
|
`<p>%s</p>
|
2021-05-03 18:31:19 +00:00
|
|
|
|
<p><b>Hyphae affected:</b> %s</p>
|
|
|
|
|
<pre><code>%s</code></pre>`, rev.Message, rev.HyphaeLinksHTML(), html.EscapeString(rev.textDiff()))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// textDiff generates a good enough diff to display in a web feed. It is not html-escaped.
|
|
|
|
|
func (rev *Revision) textDiff() (diff string) {
|
|
|
|
|
filenames, ok := rev.mycoFiles()
|
|
|
|
|
if !ok {
|
|
|
|
|
return "No text changes"
|
|
|
|
|
}
|
|
|
|
|
for _, filename := range filenames {
|
|
|
|
|
text, err := PrimitiveDiffAtRevision(filename, rev.Hash)
|
|
|
|
|
if err != nil {
|
|
|
|
|
diff += "\nAn error has occured with " + filename + "\n"
|
|
|
|
|
}
|
|
|
|
|
diff += text + "\n"
|
|
|
|
|
}
|
|
|
|
|
return diff
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mycoFiles returns filenames of .myco file. It is not ok if there are no myco files.
|
|
|
|
|
func (rev *Revision) mycoFiles() (filenames []string, ok bool) {
|
|
|
|
|
filenames = []string{}
|
|
|
|
|
for _, filename := range rev.filesAffected() {
|
|
|
|
|
if strings.HasSuffix(filename, ".myco") {
|
|
|
|
|
filenames = append(filenames, filename)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return filenames, len(filenames) > 0
|
2020-12-08 15:15:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try and guess what link is the most important by looking at the message.
|
|
|
|
|
func (rev *Revision) bestLink() string {
|
|
|
|
|
var (
|
|
|
|
|
revs = rev.hyphaeAffected()
|
|
|
|
|
renameRes = renameMsgPattern.FindStringSubmatch(rev.Message)
|
|
|
|
|
)
|
|
|
|
|
switch {
|
|
|
|
|
case renameRes != nil:
|
2021-05-03 18:31:19 +00:00
|
|
|
|
return "/hypha/" + renameRes[1]
|
2020-12-08 15:15:32 +00:00
|
|
|
|
case len(revs) == 0:
|
|
|
|
|
return ""
|
|
|
|
|
default:
|
2021-05-03 18:31:19 +00:00
|
|
|
|
return "/hypha/" + revs[0]
|
2020-12-08 15:15:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-29 17:54:57 +00:00
|
|
|
|
// I pronounce it as [gɪt͡ʃ].
|
2020-09-29 18:13:24 +00:00
|
|
|
|
// gitsh is async-safe, therefore all other git-related functions in this module are too.
|
2020-08-29 17:54:57 +00:00
|
|
|
|
func gitsh(args ...string) (out bytes.Buffer, err error) {
|
|
|
|
|
fmt.Printf("$ %v\n", args)
|
|
|
|
|
cmd := exec.Command(gitpath, args...)
|
2021-05-09 09:36:39 +00:00
|
|
|
|
cmd.Dir = cfg.WikiDir
|
2020-08-09 19:33:47 +00:00
|
|
|
|
|
2020-08-29 17:54:57 +00:00
|
|
|
|
b, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Println("gitsh:", err)
|
|
|
|
|
}
|
|
|
|
|
return *bytes.NewBuffer(b), err
|
2020-08-09 19:33:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-05-03 18:31:19 +00:00
|
|
|
|
// silentGitsh is like gitsh, except it writes less to the stdout.
|
|
|
|
|
func silentGitsh(args ...string) (out bytes.Buffer, err error) {
|
|
|
|
|
cmd := exec.Command(gitpath, args...)
|
2021-05-09 09:36:39 +00:00
|
|
|
|
cmd.Dir = cfg.WikiDir
|
2021-05-03 18:31:19 +00:00
|
|
|
|
|
|
|
|
|
b, err := cmd.CombinedOutput()
|
|
|
|
|
return *bytes.NewBuffer(b), err
|
|
|
|
|
}
|
|
|
|
|
|
2020-08-29 17:54:57 +00:00
|
|
|
|
// Convert a UNIX timestamp as string into a time. If nil is returned, it means that the timestamp could not be converted.
|
|
|
|
|
func unixTimestampAsTime(ts string) *time.Time {
|
|
|
|
|
i, err := strconv.ParseInt(ts, 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
tm := time.Unix(i, 0)
|
|
|
|
|
return &tm
|
2020-08-09 19:33:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
2020-08-29 17:54:57 +00:00
|
|
|
|
// Rename renames from `from` to `to` using `git mv`.
|
2020-08-10 19:58:02 +00:00
|
|
|
|
func Rename(from, to string) error {
|
|
|
|
|
log.Println(util.ShorterPath(from), util.ShorterPath(to))
|
2020-11-01 19:09:41 +00:00
|
|
|
|
_, err := gitsh("mv", "--force", from, to)
|
2020-08-10 19:58:02 +00:00
|
|
|
|
return err
|
2020-08-08 20:10:28 +00:00
|
|
|
|
}
|