1
0
Fork 0
mirror of https://github.com/vbatts/sl-feeds.git synced 2025-07-14 12:09:10 +00:00

changelog: a slackware ChangeLog parser

and ability to export to feeds

Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
This commit is contained in:
Vincent Batts 2017-01-26 10:39:06 -05:00
parent f522293398
commit 8e97e3d16f
Signed by: vbatts
GPG key ID: 10937E57733F1362
14 changed files with 1750 additions and 535 deletions

51
changelog/feeds.go Normal file
View file

@ -0,0 +1,51 @@
package changelog
import (
"fmt"
"time"
"github.com/gorilla/feeds"
)
// ToFeed produces a github.com/gorilla/feeds.Feed that can be written to Atom or Rss
func ToFeed(link string, entries []Entry) (*feeds.Feed, error) {
var newestEntryTime time.Time
var oldestEntryTime time.Time
for _, e := range entries {
if e.Date.After(newestEntryTime) {
newestEntryTime = e.Date
}
if e.Date.Before(oldestEntryTime) {
oldestEntryTime = e.Date
}
}
feed := &feeds.Feed{
Title: "",
Link: &feeds.Link{Href: link},
Description: "Generated ChangeLog.txt feeds by sl-feeds (github.com/vbatts/sl-feeds)",
Created: oldestEntryTime,
Updated: newestEntryTime,
}
feed.Items = make([]*feeds.Item, len(entries))
for i, e := range entries {
feed.Items[i] = &feeds.Item{
Created: e.Date,
Link: &feeds.Link{Href: ""},
Description: e.ToChangeLog(),
}
updateWord := "updates"
if len(e.Updates) == 1 {
updateWord = "update"
}
if e.SecurityFix() {
feed.Items[i].Title = fmt.Sprintf("%d %s. Including a %s!", len(e.Updates), updateWord, securityFixStr)
} else {
feed.Items[i].Title = fmt.Sprintf("%d %s.", len(e.Updates), updateWord)
}
}
return feed, nil
}

38
changelog/feeds_test.go Normal file
View file

@ -0,0 +1,38 @@
package changelog
import (
"io/ioutil"
"os"
"testing"
)
func TestFeed(t *testing.T) {
fh, err := os.Open("testdata/ChangeLog.txt")
if err != nil {
t.Fatal(err)
}
defer fh.Close()
e, err := Parse(fh)
if err != nil {
t.Fatal(err)
}
f, err := ToFeed("http://slackware.osuosl.org/slackware64-current/ChangeLog.txt", e)
if err != nil {
t.Fatal(err)
}
rss, err := f.ToRss()
if err != nil {
t.Fatal(err)
}
//println(rss)
if len(rss) == 0 {
t.Error("rss output is empty")
}
if err := f.WriteRss(ioutil.Discard); err != nil {
t.Error(err)
}
}

124
changelog/parse.go Normal file
View file

@ -0,0 +1,124 @@
package changelog
import (
"bufio"
"fmt"
"io"
"regexp"
"strings"
"time"
)
const (
dividerStr = `+--------------------------+`
securityFixStr = `(* Security fix *)`
dayPat = `^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s.*\d{4}$`
updatePat = `^([a-z].*/.*): (Added|Rebuilt|Removed|Updated|Upgraded)\.$`
)
var (
dayReg = regexp.MustCompile(dayPat)
updateReg = regexp.MustCompile(updatePat)
)
// Parse takes in a slackware ChangeLog.txt and returns its collections of Entries
func Parse(r io.Reader) ([]Entry, error) {
buf := bufio.NewReader(r)
entries := []Entry{}
curEntry := Entry{}
var curUpdate *Update
for {
line, err := buf.ReadString('\n')
if err != nil && err != io.EOF {
return nil, err
}
isEOF := err == io.EOF
trimmedline := strings.TrimSuffix(line, "\n")
if trimmedline == dividerStr {
if curUpdate != nil {
curEntry.Updates = append(curEntry.Updates, *curUpdate)
curUpdate = nil
}
entries = append(entries, curEntry)
if isEOF {
break
}
curEntry = Entry{}
} else if dayReg.MatchString(trimmedline) {
// this date means it is the beginning of an entry
t, err := time.Parse(time.UnixDate, trimmedline)
if err != nil {
return nil, err
}
curEntry.Date = t
} else if updateReg.MatchString(trimmedline) {
// match on whether this is an update line
if curUpdate != nil {
curEntry.Updates = append(curEntry.Updates, *curUpdate)
curUpdate = nil
}
m := updateReg.FindStringSubmatch(trimmedline)
curUpdate = &Update{
Name: m[1],
Action: m[2],
}
} else if curUpdate != nil && strings.HasPrefix(trimmedline, " ") {
curUpdate.Comment = curUpdate.Comment + line
} else {
// Everything else is a comment on the Entry
curEntry.Comment = curEntry.Comment + line
}
if isEOF {
break
}
}
return entries, nil
}
// Entry is an section of updates (or release comments) in a ChangeLog.txt
type Entry struct {
Date time.Time
Comment string
Updates []Update
}
// SecurityFix is whether an update in this ChangeLog Entry includes a SecurityFix
func (e Entry) SecurityFix() bool {
for _, u := range e.Updates {
if u.SecurityFix() {
return true
}
}
return false
}
// ToChangeLog reformats the struct as the text for ChangeLog.txt output
func (e Entry) ToChangeLog() string {
str := e.Date.Format(time.UnixDate) + "\n"
if strings.Trim(e.Comment, " \n") != "" {
str = str + e.Comment
}
for _, u := range e.Updates {
str = str + u.ToChangeLog()
}
return str
}
// Update is a package or component that is updated in a ChangeLog Entry
type Update struct {
Name string
Action string
Comment string
}
// SecurityFix that this update is a security fix (that the comment includes `(* Security fix *)`)
func (u Update) SecurityFix() bool {
return strings.Contains(u.Comment, securityFixStr)
}
// ToChangeLog reformats the struct as the text for ChangeLog.txt output
func (u Update) ToChangeLog() string {
return fmt.Sprintf("%s: %s.\n%s", u.Name, u.Action, u.Comment)
}

61
changelog/parse_test.go Normal file
View file

@ -0,0 +1,61 @@
package changelog
import (
"os"
"strings"
"testing"
)
func TestParse(t *testing.T) {
fh, err := os.Open("testdata/ChangeLog.txt")
if err != nil {
t.Fatal(err)
}
defer fh.Close()
e, err := Parse(fh)
if err != nil {
t.Fatal(err)
}
// Make sure we got all the entries
expectedLen := 52
if len(e) != expectedLen {
t.Errorf("expected %d entries; got %d", expectedLen, len(e))
}
// Make sure we got as many security fix entries as expected
expectedSec := 34
secCount := 0
for i := range e {
if e[i].SecurityFix() {
secCount++
}
}
if secCount != expectedSec {
t.Errorf("expected %d security fix entries; got %d", expectedSec, secCount)
}
// Make sure we got as many individual updates as expected
expectedUp := 597
upCount := 0
for i := range e {
upCount += len(e[i].Updates)
}
if upCount != expectedUp {
t.Errorf("expected %d updates across the entries; got %d", expectedUp, upCount)
}
// Make sure the top comment of an entry is working
foundWorkmanComment := false
expectedComment := "Thanks to Robby Workman for most of these updates."
for i := range e {
foundWorkmanComment = strings.Contains(e[i].Comment, expectedComment)
if foundWorkmanComment {
break
}
}
if !foundWorkmanComment {
t.Errorf("expected to find an Entry with comment %q", expectedComment)
}
}

1430
changelog/testdata/ChangeLog.txt vendored Normal file

File diff suppressed because it is too large Load diff