diff --git a/bridge/gitlab/event.go b/bridge/gitlab/event.go index 6070c8e8afe5c0add2aa4b2ab43c7dd074fb159f..69e7e5439369009c1d2072e28a133a7f23f88dc0 100644 --- a/bridge/gitlab/event.go +++ b/bridge/gitlab/event.go @@ -7,6 +7,7 @@ import ( "gitlab.com/gitlab-org/api/client-go" + "github.com/git-bug/git-bug/bridge/gitlab/parser" "github.com/git-bug/git-bug/util/text" ) @@ -50,7 +51,11 @@ type NoteEvent struct{ gitlab.Note } func (n NoteEvent) ID() string { return fmt.Sprintf("%d", n.Note.ID) } func (n NoteEvent) UserID() int { return n.Author.ID } func (n NoteEvent) CreatedAt() time.Time { return *n.Note.CreatedAt } + func (n NoteEvent) Kind() EventKind { + if _, err := parser.NewWithInput(parser.TitleParser, n.Body).Parse(); err == nil { + return EventTitleChanged + } switch { case !n.System: @@ -71,9 +76,6 @@ func (n NoteEvent) Kind() EventKind { case n.Body == "unlocked this issue": return EventUnlocked - case strings.HasPrefix(n.Body, "changed title from"): - return EventTitleChanged - case strings.HasPrefix(n.Body, "changed due date to"): return EventChangedDuedate @@ -107,11 +109,15 @@ func (n NoteEvent) Kind() EventKind { } -func (n NoteEvent) Title() string { +func (n NoteEvent) Title() (string, error) { if n.Kind() == EventTitleChanged { - return getNewTitle(n.Body) + t, err := parser.NewWithInput(parser.TitleParser, n.Body).Parse() + if err != nil { + return "", err + } + return t, nil } - return text.CleanupOneLine(n.Body) + return text.CleanupOneLine(n.Body), nil } var _ Event = &LabelEvent{} @@ -207,14 +213,3 @@ func SortedEvents(inputs ...<-chan Event) chan Event { return out } - -// getNewTitle parses body diff given by gitlab api and return it final form -// examples: -// - "changed title from **fourth issue** to **fourth issue{+ changed+}**" -// - "changed title from **fourth issue{- changed-}** to **fourth issue**" -func getNewTitle(diff string) string { - newTitle := strings.Split(diff, "** to **")[1] - newTitle = strings.Replace(newTitle, "{+", "", -1) - newTitle = strings.Replace(newTitle, "+}", "", -1) - return strings.TrimSuffix(newTitle, "**") -} diff --git a/bridge/gitlab/event_test.go b/bridge/gitlab/event_test.go index 860570d1f09b464129bf3cd23d95ead23c80cc71..b8ef5aaf24d70603acd1860b13a95f5562d54e1e 100644 --- a/bridge/gitlab/event_test.go +++ b/bridge/gitlab/event_test.go @@ -4,59 +4,9 @@ import ( "testing" "time" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func TestGetNewTitle(t *testing.T) { - type args struct { - diff string - } - type want struct { - title string - } - tests := []struct { - name string - args args - want want - }{ - { - name: "addition diff", - args: args{ - diff: "**first issue** to **first issue{+ edited+}**", - }, - want: want{ - title: "first issue edited", - }, - }, - { - name: "deletion diff", - args: args{ - diff: "**first issue{- edited-}** to **first issue**", - }, - want: want{ - title: "first issue", - }, - }, - { - name: "mixed diff", - args: args{ - diff: "**first {-issue-}** to **first {+bug+}**", - }, - want: want{ - title: "first bug", - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - title := getNewTitle(tt.args.diff) - assert.Equal(t, tt.want.title, title) - }) - } -} - var _ Event = mockEvent(0) type mockEvent int64 diff --git a/bridge/gitlab/export_test.go b/bridge/gitlab/export_test.go index 6846cb215b6f09de0e0b9b50a7472a606ee38371..2f59f5ad3739ec16cebdd5cf86ea6607abc23b1f 100644 --- a/bridge/gitlab/export_test.go +++ b/bridge/gitlab/export_test.go @@ -179,14 +179,14 @@ func TestGitlabPushPull(t *testing.T) { projectID, err := createRepository(context.TODO(), projectName, token) require.NoError(t, err) - fmt.Println("created repository", projectName) + fmt.Printf("created project: %s (%d)\n", projectName, projectID) // Make sure to remove the Gitlab repository when the test end defer func(t *testing.T) { if err := deleteRepository(context.TODO(), projectID, token); err != nil { t.Fatal(err) } - fmt.Println("deleted repository:", projectName) + fmt.Printf("deleted repository: %s (%d)\n", projectName, projectID) }(t) interrupt.RegisterCleaner(func() error { @@ -215,7 +215,7 @@ func TestGitlabPushPull(t *testing.T) { } require.NoError(t, err) - fmt.Printf("test repository exported in %f seconds\n", time.Since(start).Seconds()) + fmt.Printf("repository exported in %f seconds\n", time.Since(start).Seconds()) repoTwo := repository.CreateGoGitTestRepo(t, false) diff --git a/bridge/gitlab/import.go b/bridge/gitlab/import.go index b33a21796022d2ed6a008af633c9f9624f8cc4b7..68efbf5a5e4d2e99d603d952aeb93fca0446247a 100644 --- a/bridge/gitlab/import.go +++ b/bridge/gitlab/import.go @@ -81,7 +81,7 @@ func (gi *gitlabImporter) ImportAll(ctx context.Context, repo *cache.RepoCache, continue } if err := gi.ensureIssueEvent(repo, b, issue, e); err != nil { - err := fmt.Errorf("issue event creation: %v", err) + err := fmt.Errorf("unable to create issue event: %v", err) out <- core.NewImportError(err, entity.Id(e.ID())) } } @@ -277,10 +277,15 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa return nil } + title, err := event.(NoteEvent).Title() + if err != nil { + return err + } + op, err := b.SetTitleRaw( author, event.CreatedAt().Unix(), - event.(NoteEvent).Title(), + title, map[string]string{ metaKeyGitlabId: event.ID(), }, @@ -330,7 +335,7 @@ func (gi *gitlabImporter) ensureIssueEvent(repo *cache.RepoCache, b *cache.BugCa return nil default: - return fmt.Errorf("unexpected event") + return fmt.Errorf("unexpected event: %v", event) } return nil diff --git a/bridge/gitlab/parser/parser.go b/bridge/gitlab/parser/parser.go new file mode 100644 index 0000000000000000000000000000000000000000..7ab0d58fd573b6e063f7207a55200ad03618f948 --- /dev/null +++ b/bridge/gitlab/parser/parser.go @@ -0,0 +1,107 @@ +package parser + +import ( + "bytes" + "fmt" + "golang.org/x/net/html" + "regexp" + "strings" +) + +type parser interface { + Parse() (string, error) +} + +type parserType int + +const ( + TitleParser parserType = iota +) + +// NewWithInput returns a new parser instance +func NewWithInput(t parserType, input string) parser { + var p parser + + switch t { + case TitleParser: + p = titleParser{input: input} + } + + return p +} + +type titleParser struct { + input string +} + +// Parse is used to fetch the new title from a "changed title" event +// +// this func is a great example of something that is _extremely_ fragile; the +// input string is pulled from the body of a gitlab message containing html +// fragments, and has changed on at least [one occasion][0], breaking our test +// pipelines and preventing feature development. i think querying for an issue's +// _iterations_ [1] would likely be a better approach. +// +// example p.input values: +// - changed title from **some title** to **some{+ new +}title** +// - changed title from **some{- new-} title** to **some title** +// -
changed title from some title to some new title
(.*?)`)
+ var reMD = regexp.MustCompile(`.* to \*\*(.*)\*\*`)
+
+ matchHTML := reHTML.FindAllStringSubmatch(p.input, -1)
+ matchMD := reMD.FindAllStringSubmatch(p.input, -1)
+
+ if len(matchHTML) == 1 {
+ t, err := p.stripHTML(matchHTML[0][1])
+ if err != nil {
+ return "", fmt.Errorf("unable to strip HTML from new title: %q", t)
+ }
+ return strings.TrimSpace(t), nil
+ }
+
+ if len(matchMD) == 1 {
+ reDiff := regexp.MustCompile(`{\+(.*?)\+}`)
+
+ t := matchMD[0][1]
+ t = reDiff.ReplaceAllString(t, "$1")
+
+ return strings.TrimSpace(t), nil
+ }
+
+ return "", fmt.Errorf(
+ "failed to extract title: html=%d md=%d input=%q",
+ len(matchHTML),
+ len(matchMD),
+ p.input,
+ )
+}
+
+// stripHTML removes all html tags from a provided string
+func (p titleParser) stripHTML(s string) (string, error) {
+ nodes, err := html.Parse(strings.NewReader(s))
+ if err != nil {
+ // return the original unmodified string in the event html.Parse()
+ // fails; let the downstream callsites decide if they want to proceed
+ // with the value or not.
+ return s, err
+ }
+
+ var buf bytes.Buffer
+ var walk func(*html.Node)
+ walk = func(n *html.Node) {
+ if n.Type == html.TextNode {
+ buf.WriteString(n.Data)
+ }
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ walk(c)
+ }
+ }
+ walk(nodes)
+
+ return buf.String(), nil
+}
diff --git a/bridge/gitlab/parser/parser_test.go b/bridge/gitlab/parser/parser_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..953cf271209290085b64aaf6dcb6f9bd7cf79dac
--- /dev/null
+++ b/bridge/gitlab/parser/parser_test.go
@@ -0,0 +1,122 @@
+package parser
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTitleParser(t *testing.T) {
+ type args struct {
+ diff string
+ }
+ type want struct {
+ title string
+ }
+ tests := []struct {
+ name string
+ args args
+ want want
+ }{
+ {
+ name: "simple addition (html)",
+ args: args{
+ diff: `changed title from simple title to simple title addition
changed title from simple deleted title to simple title
changed title from tail title to tail replacement
changed title from title replacement to head replacement
changed title from this is an issue to this may be an amazing bug