@@ -0,0 +1,27 @@ Copyright (c) 2019 The Go Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,116 @@ // Package prefixtitle parses prefixed issue and change titles // for Go packages. // // It uses the conventions set by the Go project. // These conventions are documented on // https://golang.org/wiki/HandlingIssues, // https://golang.org/wiki/Gardening, // https://golang.org/wiki/CommitMessage, and // https://golang.org/wiki/MinorReleases wiki pages. package prefixtitle import ( "path" "strings" ) // ParseIssue parses a prefixed issue title. // It returns 1 or more paths from the prefix joined with module, // and the remaining issue title. // It does not try to verify whether each path is an existing Go package. // // The value of module is expected to be the empty string // for issues in the "github.com/golang/go" repository. // It should be the module path for all other repositories. // // 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. // "mod", "import/path: issue title" -> ["mod/import/path"], "issue title" // "mod", "path1, path2: issue title" -> ["mod/path1", "mod/path2"], "issue title" # Multiple comma-separated paths. // // If there's no path prefix (preceded by ": "), prefixedTitle is returned unmodified: // // "", "issue title" -> [""], "issue title" // "mod", "issue title" -> ["mod"], "issue title" // func ParseIssue(module, 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 []string{module}, 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(module, strings.TrimSpace(prefix))}, title } paths = strings.Split(prefix, ",") for i := range paths { paths[i] = path.Join(module, 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 } // ParseChange parses a prefixed change title. // It returns 1 or more paths from the prefix joined with module, // and the remaining change title. // It does not try to verify whether each path is an existing Go package. // // The value of module is expected to be the empty string // for CLs in the "go.googlesource.com/go" repository // and PRs in its "github.com/golang/go" GitHub mirror. // It should be the module path for all other repositories. // // Supported forms include: // // "", "import/path: change title" -> ["import/path"], "change title" // "", "path1, path2: change title" -> ["path1", "path2"], "change title" # Multiple comma-separated paths. // "mod", "import/path: change title" -> ["mod/import/path"], "change title" // "mod", "path1, path2: change title" -> ["mod/path1", "mod/path2"], "change title" # Multiple comma-separated paths. // // If there's no path prefix (preceded by ": "), prefixedTitle is returned unmodified: // // "", "change title" -> [""], "change title" // "mod", "change title" -> ["mod"], "change title" // // If there's a branch prefix in square brackets, title is returned with said prefix: // // "", "[branch] path: change title" -> ["path"], "[branch] change title" // "mod", "[branch] path: change title" -> ["mod/path"], "[branch] change title" // func ParseChange(module, prefixedTitle string) (paths []string, title string) { // Parse branch prefix in square brackets, if any. // E.g., "[branch] path: change title" -> "[branch] ", "path: change title". var branch string // "[branch] " or empty string. if strings.HasPrefix(prefixedTitle, "[") { if idx := strings.Index(prefixedTitle, "] "); idx != -1 { branch, prefixedTitle = prefixedTitle[:idx+len("] ")], prefixedTitle[idx+len("] "):] } } // Parse the rest of the prefixed change title. // E.g., "path1, path2: change title" -> ["path1", "path2"], "change title". idx := strings.Index(prefixedTitle, ": ") if idx == -1 { return []string{module}, branch + 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(module, strings.TrimSpace(prefix))}, branch + title } paths = strings.Split(prefix, ",") for i := range paths { paths[i] = path.Join(module, strings.TrimSpace(paths[i])) } return paths, branch + title }
@@ -0,0 +1,138 @@ package prefixtitle_test import ( "reflect" "testing" "dmitri.shuralyov.com/go/prefixtitle" ) func TestParseIssue(t *testing.T) { tests := []struct { inRoot string in string wantPaths []string wantTitle string }{ // Go standard library and subrepo packages. { 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: []string{""}, wantTitle: "issue title", }, // Third-party packages. { inRoot: "example.org", in: "import/path: issue title", wantPaths: []string{"example.org/import/path"}, wantTitle: "issue title", }, { // Multiple comma-separated paths. inRoot: "example.org", in: "path1, path2: issue title", wantPaths: []string{"example.org/path1", "example.org/path2"}, wantTitle: "issue title", }, { // No path prefix. inRoot: "example.org", in: "issue title", wantPaths: []string{"example.org"}, wantTitle: "issue title", }, } for i, tc := range tests { gotPaths, gotTitle := prefixtitle.ParseIssue(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 TestParseChange(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: "golang.org/x/image", in: "import/path: change title", wantPaths: []string{"golang.org/x/image/import/path"}, wantTitle: "change title", }, { inRoot: "golang.org/x/image", in: "[release-branch.go1.11] import/path: change title", wantPaths: []string{"golang.org/x/image/import/path"}, wantTitle: "[release-branch.go1.11] change title", }, // Multiple comma-separated paths. { in: "path1, path2: change title", wantPaths: []string{"path1", "path2"}, wantTitle: "change title", }, { inRoot: "golang.org/x/image", in: "path1, path2: change title", wantPaths: []string{"golang.org/x/image/path1", "golang.org/x/image/path2"}, wantTitle: "change title", }, { inRoot: "golang.org/x/image", in: "[release-branch.go1.11] path1, path2: change title", wantPaths: []string{"golang.org/x/image/path1", "golang.org/x/image/path2"}, wantTitle: "[release-branch.go1.11] change title", }, // No path prefix. { in: "change title", wantPaths: []string{""}, wantTitle: "change title", }, { inRoot: "golang.org/x/image", in: "change title", wantPaths: []string{"golang.org/x/image"}, wantTitle: "change title", }, { inRoot: "golang.org/x/image", in: "[release-branch.go1.11] change title", wantPaths: []string{"golang.org/x/image"}, wantTitle: "[release-branch.go1.11] change title", }, } for i, tc := range tests { gotPaths, gotTitle := prefixtitle.ParseChange(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) } } }