feature/add-git-timeline-renderer (#863)

- In order to create a useless but cool git timeline view like (and refactor the code to be more expandable):

```
git log --graph --pretty='%Cred%h%Creset -%C(auto)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --all
```

I did some Refactor : I remove the if else on the timeline.go, to create :
- a timeline package to encapsulate the behavior
- an interface "Parser" to handle the behavior (+ a factory "GetParser")
- an interface "Printer" to handle the print.
- an Unit Test to check gitparser.

For the current code I have just :
- move to timeline folder
- adapt some logic to fit on the new interface system
- add some logic that git_parser needed (like more color and an hashaArray to change the color)

For Git_parser :
- I sort twts to create a Daq (encapsulating twt)
- I create my own printer (to handle the different markdown interpretation)

To test :
```
make build
./yarnc timeline --git

Co-authored-by: f_dutratineesilva <felipe.dutratineesilva@mindgeek.com>
Reviewed-on: #863
Reviewed-by: James Mills <james@mills.io>
Co-authored-by: tkanos <tkanos@noreply@mills.io>
Co-committed-by: tkanos <tkanos@noreply@mills.io>
pull/866/head
tkanos 9 months ago committed by James Mills
parent 134c32c07d
commit f647be900c
  1. 2
      Makefile
  2. 2
      cmd/yarnc/hash.go
  3. 43
      cmd/yarnc/timeline.go
  4. 30
      cmd/yarnc/timeline/default_parser.go
  5. 104
      cmd/yarnc/timeline/git_parser.go
  6. 125
      cmd/yarnc/timeline/git_parser_test.go
  7. 20
      cmd/yarnc/timeline/json_parser.go
  8. 27
      cmd/yarnc/timeline/parser.go
  9. 14
      cmd/yarnc/timeline/printer.go
  10. 2
      cmd/yarnc/timeline/renderer/default_renderer.go
  11. 205
      cmd/yarnc/timeline/renderer/git_renderer.go
  12. 38
      cmd/yarnc/timeline/ui.go

@ -38,7 +38,7 @@ cli:
-ldflags "-w \
-X $(shell go list).Version=$(VERSION) \
-X $(shell go list).Commit=$(COMMIT)" \
./cmd/yarnc/...
./cmd/yarnc/
server: generate
@$(GOCMD) build $(FLAGS) -tags "netgo static_build" -installsuffix netgo \

@ -10,9 +10,9 @@ import (
"strings"
"time"
"go.yarn.social/types"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"go.yarn.social/types"
)
// hashCmd represents the hash command

@ -4,16 +4,14 @@
package main
import (
"encoding/json"
"fmt"
"os"
"sort"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/viper"
timelinec "git.mills.io/yarnsocial/yarn/cmd/yarnc/timeline"
"go.yarn.social/client"
)
@ -49,6 +47,12 @@ Yarn.social account.`,
os.Exit(1)
}
outputGIT, err := cmd.Flags().GetBool("git")
if err != nil {
log.WithError(err).Error("error getting git flag")
os.Exit(1)
}
reverseOrder, err := cmd.Flags().GetBool("reverse")
if err != nil {
log.WithError(err).Error("error getting reverse flag")
@ -67,7 +71,7 @@ Yarn.social account.`,
os.Exit(1)
}
timeline(cli, outputJSON, outputRAW, reverseOrder, nTwts, page, args)
timeline(cli, outputJSON, outputRAW, outputGIT, reverseOrder, nTwts, page, args)
},
}
@ -98,9 +102,14 @@ func init() {
"raw", "R", false,
"Output in raw text for processing with eg grep",
)
timelineCmd.Flags().BoolP(
"git", "G", false,
"Output in git log format",
)
}
func timeline(cli *client.Client, outputJSON, outputRAW, reverseOrder bool, nTwts, page int, args []string) {
func timeline(cli *client.Client, outputJSON, outputRAW, outputGIT, reverseOrder bool, nTwts, page int, args []string) {
res, err := cli.Timeline(page)
if err != nil {
log.WithError(err).Error("error retrieving timeline")
@ -119,21 +128,13 @@ func timeline(cli *client.Client, outputJSON, outputRAW, reverseOrder bool, nTwt
twts = twts[(len(twts) - nTwts):]
}
if outputJSON {
data, err := json.Marshal(twts)
if err != nil {
log.WithError(err).Error("error marshalling json")
os.Exit(1)
}
fmt.Println(string(data))
} else {
for _, twt := range twts {
if outputRAW {
PrintTwtRaw(twt)
} else {
PrintTwt(twt, time.Now(), cli.Twter)
fmt.Println()
}
}
err = timelinec.GetParser(timelinec.Options{
OutputJSON: outputJSON,
OutputRAW: outputRAW,
OutputGIT: outputGIT,
}).Parse(twts, cli.Twter)
if err != nil {
os.Exit(1)
}
}

@ -0,0 +1,30 @@
package timeline
import (
"fmt"
"time"
"go.yarn.social/types"
)
type defaultParser struct {
}
func (d defaultParser) Parse(twts types.Twts, me types.Twter) error {
for _, twt := range twts {
PrintTwt(twt, time.Now(), me)
fmt.Println()
}
return nil
}
type defaultRawParser struct {
}
func (d defaultRawParser) Parse(twts types.Twts, me types.Twter) error {
for _, twt := range twts {
PrintTwtRaw(twt)
}
return nil
}

@ -0,0 +1,104 @@
package timeline
import (
"strings"
"git.mills.io/yarnsocial/yarn/cmd/yarnc/timeline/renderer"
"github.com/dustin/go-humanize"
"github.com/russross/blackfriday"
"go.yarn.social/types"
)
type twtWithChild struct {
types.Twt
child []*twtWithChild
ParentId string
}
func (c *twtWithChild) HasParent() bool {
return c.ParentId != ""
}
func (c *twtWithChild) GetChildren() []*twtWithChild {
return c.child
}
func (c *twtWithChild) AddChilddren(twt *twtWithChild) {
c.child = append(c.child, twt)
}
func (c *twtWithChild) HasChildren() bool {
return len(c.child) != 0
}
type twtWithChilds []*twtWithChild
func newTwtWithChilds(twts types.Twts) twtWithChilds {
m := make(map[string]*twtWithChild)
twtc := []*twtWithChild{}
for _, v := range twts {
t := twtWithChild{v, []*twtWithChild{}, ""}
twtc = append(twtc, &t)
m[v.Hash()] = &t
}
for i, v := range twtc {
for _, tag := range v.Tags() {
if m1, ok := m[tag.Text()]; ok {
twtc[i].ParentId = tag.Text()
m1.AddChilddren(twtc[i])
}
}
}
return twtc
}
type gitParser struct {
printer printer
}
func (d gitParser) Parse(twts types.Twts, me types.Twter) error {
twtc := newTwtWithChilds(twts)
for _, v := range twtc {
if !v.HasParent() {
d.recursivePrint([]string{}, v)
}
}
return nil
}
func (d gitParser) recursivePrint(prefix []string, twt *twtWithChild) {
d.print(strings.Join(prefix, ""), twt)
if twt.HasChildren() {
d.printer.Print(strings.Join(append(prefix, HashColor(twt.Hash())(" | \\ \n")), ""))
}
for _, c := range twt.GetChildren() {
d.recursivePrint(append(prefix, HashColor(twt.Hash())(" |")), c)
}
}
func (d gitParser) print(prefix string, twt *twtWithChild) {
r := &renderer.ConsoleGit{}
extensions := 0 |
blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
blackfriday.EXTENSION_FENCED_CODE |
blackfriday.EXTENSION_AUTOLINK |
blackfriday.EXTENSION_STRIKETHROUGH |
blackfriday.EXTENSION_SPACE_HEADERS |
blackfriday.EXTENSION_HEADER_IDS |
blackfriday.EXTENSION_DEFINITION_LISTS |
blackfriday.EXTENSION_NO_EMPTY_LINE_BEFORE_BLOCK |
blackfriday.EXTENSION_JOIN_LINES
input := []byte(twt.FormatText(types.MarkdownFmt, nil))
output := blackfriday.Markdown(input, r, extensions)
d.printer.Print("%s %s - %s (%s) <%s>\n",
prefix+white(" * "),
redBold(twt.Hash()),
string(output),
green(humanize.Time(twt.Created())),
blue(twt.Twter().DomainNick()))
}

@ -0,0 +1,125 @@
package timeline
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
_ "go.yarn.social/lextwt"
"go.yarn.social/types"
)
type testPrinter struct {
Lines []string
}
func (t *testPrinter) Print(format string, a ...interface{}) {
t.Lines = append(t.Lines, fmt.Sprintf(format, a...))
}
func TestParser(t *testing.T) {
mainTwter := types.Twter{
Nick: "nick",
}
mainNode1 := types.MakeTwt(mainTwter, time.Now(), "1")
mainNode2 := types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.2", mainNode1.Hash()))
mainNode3 := types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.2.2", mainNode2.Hash()))
tests := []struct {
name string
twts types.Twts
wants []string
}{
{
name: "1 - When only root nodes",
twts: []types.Twt{
types.MakeTwt(mainTwter, time.Now(), "Hello"),
types.MakeTwt(mainTwter, time.Now(), "World"),
types.MakeTwt(mainTwter, time.Now(), "!"),
},
wants: []string{
`\* .*- Hello`,
`\* .*- World`,
`\* .*- !`,
},
},
{
name: "2- * and simple branch",
twts: []types.Twt{
mainNode1,
types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.1", mainNode1.Hash())),
types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.2", mainNode1.Hash())),
},
wants: []string{
`\* .*- 1`,
`| \\`,
`| \* .* 1.1`,
`| \* .* 1.2`,
},
},
{
name: "3- Simple branch with * in the middle",
twts: []types.Twt{
mainNode1,
types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.1", mainNode1.Hash())),
types.MakeTwt(mainTwter, time.Now(), "Hello"),
types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.2", mainNode1.Hash())),
},
wants: []string{
`\* .*- 1`,
`| \\`,
`| \* .* 1.1`,
`| \* .* 1.2`,
`\* .*- Hello`,
},
},
{
name: "4- Triple Level",
twts: []types.Twt{
mainNode1,
mainNode2,
types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.3", mainNode1.Hash())),
types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.2.1", mainNode2.Hash())),
mainNode3,
types.MakeTwt(mainTwter, time.Now(), fmt.Sprintf("(#%s) 1.2.2.1", mainNode3.Hash())),
},
wants: []string{
`\* .*- 1`,
`| \\`,
`| \* .* 1.2`,
`| | \\`,
`| | \* .* 1.2.1`,
`| | \* .* 1.2.2`,
`| | | \\`,
`| | | \* .* 1.2.2.1`,
`| \* .* 1.3`,
},
},
{
name: "5- Child without Parent",
twts: []types.Twt{
types.MakeTwt(mainTwter, time.Now(), "Hello"),
types.MakeTwt(mainTwter, time.Now(), "(#abcdefg) World"),
},
wants: []string{
`\* .*- Hello`,
`\* .* World`,
},
},
}
for _, test := range tests {
printer := testPrinter{}
gp := gitParser{&printer}
gp.Parse(test.twts, mainTwter)
assert.Equal(t, len(test.wants), len(printer.Lines))
if len(test.wants) == len(printer.Lines) {
for i := range printer.Lines {
assert.Regexpf(t, test.wants[i], printer.Lines[i], test.name)
}
}
}
}

@ -0,0 +1,20 @@
package timeline
import (
"encoding/json"
"fmt"
"go.yarn.social/types"
)
type jsontParser struct {
}
func (d jsontParser) Parse(twts types.Twts, me types.Twter) error {
data, err := json.Marshal(twts)
if err != nil {
return err
}
fmt.Println(string(data))
return nil
}

@ -0,0 +1,27 @@
package timeline
import "go.yarn.social/types"
type Parser interface {
Parse(twts types.Twts, me types.Twter) error
}
type Options struct {
OutputJSON bool
OutputRAW bool
OutputGIT bool
}
// GetParser Factory ...
func GetParser(opts Options) Parser {
switch {
case opts.OutputJSON:
return jsontParser{}
case opts.OutputRAW:
return defaultRawParser{}
case opts.OutputGIT:
return gitParser{defaultPrinter{}}
default:
return defaultParser{}
}
}

@ -0,0 +1,14 @@
package timeline
import "fmt"
type printer interface {
Print(format string, a ...interface{})
}
type defaultPrinter struct {
}
func (d defaultPrinter) Print(format string, a ...interface{}) {
fmt.Printf(format, a...)
}

@ -1,7 +1,7 @@
// Copyright 2020-present Yarn.social
// SPDX-License-Identifier: AGPL-3.0-or-later
package main
package renderer
import (
"bytes"

@ -0,0 +1,205 @@
// Copyright 2020-present Yarn.social
// SPDX-License-Identifier: AGPL-3.0-or-later
package renderer
import (
"bytes"
"fmt"
"html"
"regexp"
"strings"
"github.com/mgutz/ansi"
"github.com/russross/blackfriday"
)
type ConsoleGit struct {
lists []*list
}
func (options *ConsoleGit) BlockCode(out *bytes.Buffer, text []byte, lang string) {
s := string(text)
reg, _ := regexp.Compile(`\n`)
out.WriteString("\n ")
out.WriteString(ansi.ColorCode("015"))
out.WriteString(reg.ReplaceAllString(s, "\n "))
out.WriteString(ansi.ColorCode("reset"))
out.WriteString("\n")
}
func (options *ConsoleGit) BlockQuote(out *bytes.Buffer, text []byte) {
s := strings.TrimSpace(string(text))
reg, _ := regexp.Compile(`\n`)
out.WriteString("\n | ")
out.WriteString(reg.ReplaceAllString(s, "\n | "))
out.WriteString("\n\n")
}
func (options *ConsoleGit) BlockHtml(out *bytes.Buffer, text []byte) {
out.Write(text)
}
func (options *ConsoleGit) Header(out *bytes.Buffer, text func() bool, level int, id string) {
out.WriteString("\n")
out.WriteString(headerStyles[level-1])
marker := out.Len()
if !text() {
out.Truncate(marker)
return
}
out.WriteString(ansi.ColorCode("reset"))
out.WriteString("\n\n")
}
func (options *ConsoleGit) HRule(out *bytes.Buffer) {
out.WriteString("\n\u2015\u2015\u2015\u2015\u2015\n\n")
}
func (options *ConsoleGit) List(out *bytes.Buffer, text func() bool, flags int) {
out.WriteString("\n")
kind := UNORDERED
if flags&blackfriday.LIST_TYPE_ORDERED != 0 {
kind = ORDERED
}
options.lists = append(options.lists, &list{kind, 1})
text()
options.lists = options.lists[:len(options.lists)-1]
out.WriteString("\n")
}
func (options *ConsoleGit) ListItem(out *bytes.Buffer, text []byte, flags int) {
current := options.lists[len(options.lists)-1]
for i := 0; i < len(options.lists); i++ {
out.WriteString(" ")
}
if current.kind == ORDERED {
out.WriteString(fmt.Sprintf("%d. ", current.index))
current.index += 1
} else {
out.WriteString(ansi.ColorCode("red+bh"))
out.WriteString("* ")
out.WriteString(ansi.ColorCode("reset"))
}
out.Write(text)
out.WriteString("\n\n")
}
func (options *ConsoleGit) Paragraph(out *bytes.Buffer, text func() bool) {
marker := out.Len()
if !text() {
out.Truncate(marker)
return
}
}
func (options *ConsoleGit) Table(out *bytes.Buffer, header []byte, body []byte, columnData []int) {}
func (options *ConsoleGit) TableRow(out *bytes.Buffer, text []byte) {}
func (options *ConsoleGit) TableHeaderCell(out *bytes.Buffer, text []byte, flags int) {}
func (options *ConsoleGit) TableCell(out *bytes.Buffer, text []byte, flags int) {}
func (options *ConsoleGit) Footnotes(out *bytes.Buffer, text func() bool) {}
func (options *ConsoleGit) FootnoteItem(out *bytes.Buffer, name, text []byte, flags int) {}
func (options *ConsoleGit) TitleBlock(out *bytes.Buffer, text []byte) {
out.WriteString("\n")
out.WriteString(headerStyles[0])
out.Write(text)
out.WriteString(ansi.ColorCode("reset"))
out.WriteString("\n\n")
}
func (options *ConsoleGit) AutoLink(out *bytes.Buffer, link []byte, kind int) {
out.WriteString(linkStyle)
out.Write(link)
out.WriteString(ansi.ColorCode("reset"))
}
func (options *ConsoleGit) CodeSpan(out *bytes.Buffer, text []byte) {
out.WriteString(ansi.ColorCode("015+b"))
out.Write(text)
out.WriteString(ansi.ColorCode("reset"))
}
func (options *ConsoleGit) DoubleEmphasis(out *bytes.Buffer, text []byte) {
out.WriteString(emphasisStyles[1])
out.Write(text)
out.WriteString(ansi.ColorCode("reset"))
}
func (options *ConsoleGit) Emphasis(out *bytes.Buffer, text []byte) {
out.WriteString(emphasisStyles[0])
out.Write(text)
out.WriteString(ansi.ColorCode("reset"))
}
func (options *ConsoleGit) Image(out *bytes.Buffer, link []byte, title []byte, alt []byte) {
out.WriteString(" [ image ] ")
}
func (options *ConsoleGit) LineBreak(out *bytes.Buffer) {
out.WriteString("\n")
}
func (options *ConsoleGit) Link(out *bytes.Buffer, link []byte, title []byte, content []byte) {
out.Write(content)
out.WriteString(" (")
out.WriteString(linkStyle)
out.Write(link)
out.WriteString(ansi.ColorCode("reset"))
out.WriteString(")")
}
func (options *ConsoleGit) RawHtmlTag(out *bytes.Buffer, tag []byte) {
out.WriteString(ansi.ColorCode("magenta"))
out.Write(tag)
out.WriteString(ansi.ColorCode("reset"))
}
func (options *ConsoleGit) TripleEmphasis(out *bytes.Buffer, text []byte) {
out.WriteString(emphasisStyles[2])
out.Write(text)
out.WriteString(ansi.ColorCode("reset"))
}
func (options *ConsoleGit) StrikeThrough(out *bytes.Buffer, text []byte) {
out.WriteString(ansi.ColorCode("008+s"))
out.WriteString("\u2015")
out.Write(text)
out.WriteString("\u2015")
out.WriteString(ansi.ColorCode("reset"))
}
func (options *ConsoleGit) FootnoteRef(out *bytes.Buffer, ref []byte, id int) {
}
func (options *ConsoleGit) Entity(out *bytes.Buffer, entity []byte) {
out.WriteString(html.UnescapeString(string(entity)))
}
func (options *ConsoleGit) NormalText(out *bytes.Buffer, text []byte) {
s := string(text)
reg, _ := regexp.Compile(`\s+`)
out.WriteString(reg.ReplaceAllString(s, " "))
}
func (options *ConsoleGit) DocumentHeader(out *bytes.Buffer) {
}
func (options *ConsoleGit) DocumentFooter(out *bytes.Buffer) {
}
func (options *ConsoleGit) GetFlags() int {
return 0
}

@ -1,35 +1,59 @@
// Copyright 2020-present Yarn.social
// SPDX-License-Identifier: AGPL-3.0-or-later
package main
package timeline
import (
"fmt"
"hash/fnv"
"strings"
"time"
"git.mills.io/yarnsocial/yarn/cmd/yarnc/timeline/renderer"
"github.com/dustin/go-humanize"
"github.com/russross/blackfriday"
"go.yarn.social/types"
)
/* red Currently unused
func red(s string) string {
return fmt.Sprintf("\033[31m%s\033[0m", s)
}
*/
func green(s string) string {
return fmt.Sprintf("\033[32m%s\033[0m", s)
}
func yellow(s string) string {
return fmt.Sprintf("\033[33m%s\033[0m", s)
}
func blue(s string) string {
return fmt.Sprintf("\033[34m%s\033[0m", s)
}
func purple(s string) string {
return fmt.Sprintf("\033[35m%s\033[0m", s)
}
func cyan(s string) string {
return fmt.Sprintf("\033[36m%s\033[0m", s)
}
func white(s string) string {
return fmt.Sprintf("\033[0;37m%s\033[0m", s)
}
func redBold(s string) string {
return fmt.Sprintf("\033[1;31m%s\033[0m", s)
}
func boldgreen(s string) string {
return fmt.Sprintf("\033[32;1m%s\033[0m", s)
}
func blue(s string) string {
return fmt.Sprintf("\033[34m%s\033[0m", s)
var colorArray = []func(string) string{red, green, yellow, blue, purple, cyan}
func HashColor(hash string) func(string) string {
//Hash
h := fnv.New32a()
h.Write([]byte(hash))
n := h.Sum32() % uint32(len(colorArray))
//Get func
f := colorArray[n]
return f
}
func PrintFollowee(nick, url string) {
@ -52,7 +76,7 @@ func PrintTwt(twt types.Twt, now time.Time, me types.Twter) {
nick = boldgreen(twt.Twter().DomainNick())
}
renderer := &Console{}
renderer := &renderer.Console{}
extensions := 0 |
blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
blackfriday.EXTENSION_FENCED_CODE |
Loading…
Cancel
Save