diff --git a/history/feed.go b/history/feed.go new file mode 100644 index 0000000..b3a0bd4 --- /dev/null +++ b/history/feed.go @@ -0,0 +1,84 @@ +package history + +import ( + "fmt" + "strings" + "time" + + "github.com/bouncepaw/mycorrhiza/cfg" + + "github.com/gorilla/feeds" +) + +var groupPeriod, _ = time.ParseDuration("30m") + +func recentChangesFeed() *feeds.Feed { + feed := &feeds.Feed{ + Title: "Recent changes", + Link: &feeds.Link{Href: cfg.URL}, + Description: "List of 30 recent changes on the wiki", + Author: &feeds.Author{Name: "Wikimind", Email: "wikimind@mycorrhiza"}, + Updated: time.Now(), + } + revs := RecentChanges(30) + groups := groupRevisionsByPeriod(revs, groupPeriod) + for _, grp := range groups { + item := grp.feedItem() + feed.Add(&item) + } + return feed +} + +// RecentChangesRSS creates recent changes feed in RSS format. +func RecentChangesRSS() (string, error) { + return recentChangesFeed().ToRss() +} + +// RecentChangesAtom creates recent changes feed in Atom format. +func RecentChangesAtom() (string, error) { + return recentChangesFeed().ToAtom() +} + +// RecentChangesJSON creates recent changes feed in JSON format. +func RecentChangesJSON() (string, error) { + return recentChangesFeed().ToJSON() +} + +func (grp revisionGroup) feedItem() feeds.Item { + return feeds.Item{ + Title: grp.title(), + Author: grp.author(), + Id: grp[0].Hash, + Description: grp.descriptionForFeed(), + Created: grp[len(grp)-1].Time, // earliest revision + Updated: grp[0].Time, // latest revision + Link: &feeds.Link{Href: cfg.URL + grp[0].bestLink()}, + } +} + +func (grp revisionGroup) title() string { + if len(grp) == 1 { + return grp[0].Message + } else { + return fmt.Sprintf("%d edits (%s, ...)", len(grp), grp[0].Message) + } +} + +func (grp revisionGroup) author() *feeds.Author { + author := grp[0].Username + for _, rev := range grp[1:] { + // if they don't all have the same author, return nil + if rev.Username != author { + return nil + } + } + return &feeds.Author{Name: author} +} + +func (grp revisionGroup) descriptionForFeed() string { + builder := strings.Builder{} + for _, rev := range grp { + builder.WriteString(rev.descriptionForFeed()) + } + return builder.String() +} diff --git a/history/history.go b/history/history.go index 37ce429..559d9d5 100644 --- a/history/history.go +++ b/history/history.go @@ -4,14 +4,10 @@ package history import ( "bytes" "fmt" - "html" "log" "os/exec" "path/filepath" "regexp" - "strconv" - "strings" - "time" "github.com/bouncepaw/mycorrhiza/files" "github.com/bouncepaw/mycorrhiza/util" @@ -54,131 +50,6 @@ func InitGitRepo() { } } -// Revision represents a revision, duh. Hash is usually short. Username is extracted from email. -type Revision struct { - Hash string - Username string - Time time.Time - Message string - filesAffectedBuf []string - hyphaeAffectedBuf []string -} - -// 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 -} - -// 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) - var ( - // set is used to determine if a certain hypha has been already noted (hyphae are stored in 2 files at most currently). - set = make(map[string]bool) - isNewName = func(hyphaName string) bool { - if _, present := set[hyphaName]; present { - return false - } - set[hyphaName] = true - return true - } - filesAffected = rev.filesAffected() - ) - for _, filename := range filesAffected { - if strings.IndexRune(filename, '.') >= 0 { - dotPos := strings.LastIndexByte(filename, '.') - hyphaName := string([]byte(filename)[0:dotPos]) // is it safe? - if isNewName(hyphaName) { - hyphae = append(hyphae, hyphaName) - } - } - } - rev.hyphaeAffectedBuf = hyphae - return hyphae -} - -// TimeString returns a human readable time representation. -func (rev Revision) TimeString() string { - return rev.Time.Format(time.RFC822) -} - -// HyphaeLinksHTML returns a comma-separated list of hyphae that were affected by this revision as HTML string. -func (rev Revision) HyphaeLinksHTML() (html string) { - hyphae := rev.hyphaeAffected() - for i, hyphaName := range hyphae { - if i > 0 { - html += `` - } - html += fmt.Sprintf(`%[1]s`, hyphaName) - } - return html -} - -// descriptionForFeed generates a good enough HTML contents for a web feed. -func (rev *Revision) descriptionForFeed() (htmlDesc string) { - return fmt.Sprintf( - `

%s

-

Hyphae affected: %s

-
%s
`, 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 occurred 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 -} - -// 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: - return "/hypha/" + renameRes[1] - case len(revs) == 0: - return "" - default: - return "/hypha/" + revs[0] - } -} - // 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) { @@ -204,16 +75,6 @@ func silentGitsh(args ...string) (out bytes.Buffer, err error) { return *bytes.NewBuffer(b), err } -// 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 -} - // Rename renames from `from` to `to` using `git mv`. func Rename(from, to string) error { log.Println(util.ShorterPath(from), util.ShorterPath(to)) diff --git a/history/information.go b/history/information.go deleted file mode 100644 index 8237c1c..0000000 --- a/history/information.go +++ /dev/null @@ -1,199 +0,0 @@ -package history - -// information.go -// Things related to gathering existing information. -import ( - "fmt" - "log" - "regexp" - "strconv" - "strings" - "time" - - "github.com/bouncepaw/mycorrhiza/cfg" - "github.com/bouncepaw/mycorrhiza/files" - - "github.com/gorilla/feeds" -) - -func recentChangesFeed() *feeds.Feed { - feed := &feeds.Feed{ - Title: "Recent changes", - Link: &feeds.Link{Href: cfg.URL}, - Description: "List of 30 recent changes on the wiki", - Author: &feeds.Author{Name: "Wikimind", Email: "wikimind@mycorrhiza"}, - Updated: time.Now(), - } - var ( - out, err = silentGitsh( - "log", "--oneline", "--no-merges", - "--pretty=format:\"%h\t%ae\t%at\t%s\"", - "--max-count=30", - ) - revs []Revision - ) - if err == nil { - for _, line := range strings.Split(out.String(), "\n") { - revs = append(revs, parseRevisionLine(line)) - } - } - log.Printf("Found %d recent changes", len(revs)) - for _, rev := range revs { - feed.Add(&feeds.Item{ - Title: rev.Message, - Author: &feeds.Author{Name: rev.Username}, - Id: rev.Hash, - Description: rev.descriptionForFeed(), - Created: rev.Time, - Updated: rev.Time, - Link: &feeds.Link{Href: cfg.URL + rev.bestLink()}, - }) - } - return feed -} - -// RecentChangesRSS creates recent changes feed in RSS format. -func RecentChangesRSS() (string, error) { - return recentChangesFeed().ToRss() -} - -// RecentChangesAtom creates recent changes feed in Atom format. -func RecentChangesAtom() (string, error) { - return recentChangesFeed().ToAtom() -} - -// RecentChangesJSON creates recent changes feed in JSON format. -func RecentChangesJSON() (string, error) { - return recentChangesFeed().ToJSON() -} - -// RecentChanges gathers an arbitrary number of latest changes in form of revisions slice. -func RecentChanges(n int) []Revision { - var ( - out, err = silentGitsh( - "log", "--oneline", "--no-merges", - "--pretty=format:\"%h\t%ae\t%at\t%s\"", - "--max-count="+strconv.Itoa(n), - ) - revs []Revision - ) - if err == nil { - for _, line := range strings.Split(out.String(), "\n") { - revs = append(revs, parseRevisionLine(line)) - } - } - log.Printf("Found %d recent changes", len(revs)) - return revs -} - -// FileChanged tells you if the file has been changed. -func FileChanged(path string) bool { - _, err := gitsh("diff", "--exit-code", path) - return err != nil -} - -// Revisions returns slice of revisions for the given hypha name. -func Revisions(hyphaName string) ([]Revision, error) { - var ( - out, err = silentGitsh( - "log", "--oneline", "--no-merges", - // Hash, author email, author time, commit msg separated by tab - "--pretty=format:\"%h\t%ae\t%at\t%s\"", - "--", hyphaName+".*", - ) - revs []Revision - ) - if err == nil { - for _, line := range strings.Split(out.String(), "\n") { - if line != "" { - revs = append(revs, parseRevisionLine(line)) - } - } - } - log.Printf("Found %d revisions for ‘%s’\n", len(revs), hyphaName) - return revs, err -} - -// WithRevisions returns an html representation of `revs` that is meant to be inserted in a history page. -func WithRevisions(hyphaName string, revs []Revision) (html string) { - var ( - currentYear int - currentMonth time.Month - ) - for i, rev := range revs { - if rev.Time.Month() != currentMonth || rev.Time.Year() != currentYear { - currentYear = rev.Time.Year() - currentMonth = rev.Time.Month() - if i != 0 { - html += ` - -` - } - html += fmt.Sprintf(` -
- -

%[3]s

-
-