dmitri.shuralyov.com/website/gido/...

factor out prefixed title parsing code

This functionality is factored out into a separate package
so that it can be reused across a wider set of projects.
dmitshur committed 2 months ago commit af55c674dcfc239acf7613fdb485d827f54a64fd
service.go
@@ -2,16 +2,16 @@ package main
 
 import (
 	"context"
 	"fmt"
 	"log"
-	"path"
 	"sort"
 	"strings"
 	"sync"
 	"time"
 
+	"dmitri.shuralyov.com/go/prefixtitle"
 	"dmitri.shuralyov.com/service/change"
 	"github.com/shurcooL/issues"
 	"github.com/shurcooL/users"
 	"golang.org/x/build/maintner"
 	"golang.org/x/build/maintner/godata"
@@ -159,11 +159,11 @@ func issuesAndChanges(repo *maintner.GitHubRepo, gerrit *maintner.Gerrit) map[st
 	err := repo.ForeachIssue(func(i *maintner.GitHubIssue) error {
 		if i.NotExist || i.PullRequest {
 			return nil
 		}
 
-		pkgs, title := ParsePrefixedTitle(i.Title)
+		pkgs, title := prefixtitle.ParseIssue("", i.Title)
 		var labels []issues.Label
 		for _, l := range i.Labels {
 			labels = append(labels, issues.Label{
 				Name: l.Name,
 				// TODO: Can we use label ID to figure out its color?
@@ -236,11 +236,11 @@ func issuesAndChanges(repo *maintner.GitHubRepo, gerrit *maintner.Gerrit) map[st
 			if !ok {
 				return nil
 			}
 
 			prefixedTitle := cl.Subject()
-			pkgs, title := ParsePrefixedChangeTitle(root, prefixedTitle)
+			pkgs, title := prefixtitle.ParseChange(root, prefixedTitle)
 			var labels []issues.Label
 			cl.Meta.Hashtags().Foreach(func(name string) {
 				labels = append(labels, issues.Label{
 					Name:  name,
 					Color: issues.RGB{R: 0xed, G: 0xed, B: 0xed}, // Light gray.
@@ -334,80 +334,10 @@ var gerritProjects = map[string]string{
 	"go.googlesource.com/vgo":        "golang.org/x/vgo",
 }
 
 const otherPackages = "other"
 
-// ParsePrefixedTitle parses a prefixed issue title.
-// It returns a list of paths from the prefix, and the remaining issue title.
-// It does not try to verify whether each path is an existing Go package.
-//
-// Supported forms include:
-//
-// 	"import/path: Issue title."    -> ["import/path"],       "Issue title."
-// 	"proposal: path: Issue title." -> ["path"],              "Issue title."  # Proposal.
-// 	"Proposal: path: Issue title." -> ["path"],              "Issue title."  # Proposal.
-// 	"x/path: Issue title."         -> ["golang.org/x/path"], "Issue title."  # "x/..." refers to "golang.org/x/...".
-// 	"path1, path2: Issue title."   -> ["path1", "path2"],    "Issue title."  # Multiple comma-separated paths.
-//
-// If there's no path prefix (preceded by ": "), title is returned unmodified
-// with an empty paths list:
-//
-// 	"Issue title."                 -> [], "Issue title."
-//
-func ParsePrefixedTitle(prefixedTitle string) (paths []string, title string) {
-	prefixedTitle = strings.TrimPrefix(prefixedTitle, "proposal: ") // TODO: Consider preserving the "proposal: " prefix?
-	prefixedTitle = strings.TrimPrefix(prefixedTitle, "Proposal: ")
-	idx := strings.Index(prefixedTitle, ": ")
-	if idx == -1 {
-		return nil, prefixedTitle
-	}
-	prefix, title := prefixedTitle[:idx], prefixedTitle[idx+len(": "):]
-	if strings.ContainsAny(prefix, "{}") {
-		// TODO: Parse "image/{png,jpeg}" as ["image/png", "image/jpeg"], maybe?
-		return []string{strings.TrimSpace(prefix)}, title
-	}
-	paths = strings.Split(prefix, ",")
-	for i := range paths {
-		paths[i] = strings.TrimSpace(paths[i])
-		if strings.HasPrefix(paths[i], "x/") || paths[i] == "x" { // Map "x/..." to "golang.org/x/...".
-			paths[i] = "golang.org/" + paths[i]
-		}
-	}
-	return paths, title
-}
-
-// ParsePrefixedChangeTitle parses a prefixed change title.
-// It returns a list of paths from the prefix joined with root, and the remaining change title.
-// It does not try to verify whether each path is an existing Go package.
-//
-// Supported forms include:
-//
-// 	"root", "import/path: Change title."  -> ["root/import/path"],         "Change title."
-// 	"root", "path1, path2: Change title." -> ["root/path1", "root/path2"], "Change title."  # Multiple comma-separated paths.
-//
-// If there's no path prefix (preceded by ": "), title is returned unmodified
-// with a paths list containing root:
-//
-// 	"root", "Change title."               -> ["root"], "Change title."
-//
-func ParsePrefixedChangeTitle(root, prefixedTitle string) (paths []string, title string) {
-	idx := strings.Index(prefixedTitle, ": ")
-	if idx == -1 {
-		return []string{root}, prefixedTitle
-	}
-	prefix, title := prefixedTitle[:idx], prefixedTitle[idx+len(": "):]
-	if strings.ContainsAny(prefix, "{}") {
-		// TODO: Parse "image/{png,jpeg}" as ["image/png", "image/jpeg"], maybe?
-		return []string{path.Join(root, strings.TrimSpace(prefix))}, title
-	}
-	paths = strings.Split(prefix, ",")
-	for i := range paths {
-		paths[i] = path.Join(root, strings.TrimSpace(paths[i]))
-	}
-	return paths, title
-}
-
 // ImportPathToFullPrefix returns the an issue title prefix (including ": ") for the given import path.
 // If path equals to otherPackages, an empty prefix is returned.
 func ImportPathToFullPrefix(path string) string {
 	switch {
 	default:
service_test.go
@@ -1,106 +1,13 @@
 package main_test
 
 import (
-	"reflect"
 	"testing"
 
 	gido "dmitri.shuralyov.com/website/gido"
 )
 
-func TestParsePrefixedTitle(t *testing.T) {
-	tests := []struct {
-		in        string
-		wantPaths []string
-		wantTitle string
-	}{
-		{
-			in:        "import/path: Issue title.",
-			wantPaths: []string{"import/path"}, wantTitle: "Issue title.",
-		},
-		{ // Proposal.
-			in:        "proposal: path: Issue title.",
-			wantPaths: []string{"path"}, wantTitle: "Issue title.",
-		},
-		{ // Proposal.
-			in:        "Proposal: path: Issue title.",
-			wantPaths: []string{"path"}, wantTitle: "Issue title.",
-		},
-		{ // "x/..." refers to "golang.org/x/...".
-			in:        "x/path: Issue title.",
-			wantPaths: []string{"golang.org/x/path"}, wantTitle: "Issue title.",
-		},
-		{ // "x" refers to "golang.org/x".
-			in:        "x: Issue title.",
-			wantPaths: []string{"golang.org/x"}, wantTitle: "Issue title.",
-		},
-		{ // Multiple comma-separated paths.
-			in:        "path1, path2: Issue title.",
-			wantPaths: []string{"path1", "path2"}, wantTitle: "Issue title.",
-		},
-		{ // No path prefix.
-			in:        "Issue title.",
-			wantPaths: nil, wantTitle: "Issue title.",
-		},
-	}
-	for i, tc := range tests {
-		gotPaths, gotTitle := gido.ParsePrefixedTitle(tc.in)
-		if !reflect.DeepEqual(gotPaths, tc.wantPaths) {
-			t.Errorf("%d: got paths: %q, want: %q", i, gotPaths, tc.wantPaths)
-		}
-		if gotTitle != tc.wantTitle {
-			t.Errorf("%d: got title: %q, want: %q", i, gotTitle, tc.wantTitle)
-		}
-	}
-}
-
-func TestParsePrefixedChangeTitle(t *testing.T) {
-	tests := []struct {
-		inRoot    string
-		in        string
-		wantPaths []string
-		wantTitle string
-	}{
-		{
-			in:        "import/path: Change title.",
-			wantPaths: []string{"import/path"}, wantTitle: "Change title.",
-		},
-		{
-			inRoot:    "root",
-			in:        "import/path: Change title.",
-			wantPaths: []string{"root/import/path"}, wantTitle: "Change title.",
-		},
-		{ // Multiple comma-separated paths.
-			in:        "path1, path2: Change title.",
-			wantPaths: []string{"path1", "path2"}, wantTitle: "Change title.",
-		},
-		{
-			inRoot:    "root",
-			in:        "path1, path2: Change title.",
-			wantPaths: []string{"root/path1", "root/path2"}, wantTitle: "Change title.",
-		},
-		{ // No path prefix.
-			in:        "Change title.",
-			wantPaths: []string{""}, wantTitle: "Change title.",
-		},
-		{
-			inRoot:    "root",
-			in:        "Change title.",
-			wantPaths: []string{"root"}, wantTitle: "Change title.",
-		},
-	}
-	for i, tc := range tests {
-		gotPaths, gotTitle := gido.ParsePrefixedChangeTitle(tc.inRoot, tc.in)
-		if !reflect.DeepEqual(gotPaths, tc.wantPaths) {
-			t.Errorf("%d: got paths: %q, want: %q", i, gotPaths, tc.wantPaths)
-		}
-		if gotTitle != tc.wantTitle {
-			t.Errorf("%d: got title: %q, want: %q", i, gotTitle, tc.wantTitle)
-		}
-	}
-}
-
 func TestImportPathToFullPrefix(t *testing.T) {
 	tests := []struct {
 		in   string
 		want string
 	}{