feat: support packing multiple packages #13

Open
Jvlegod wants to merge 1 commits from Jvlegod/go2spec:feature/pack-multiple-packages into master
3 changed files with 169 additions and 30 deletions
+17 -4
View File
@@ -36,6 +36,18 @@ func shortCommitHash(hash string) string {
return hash
}
func preferredRevisionCandidates(preferredRev string) []string {
preferredRev = strings.TrimSpace(preferredRev)
if preferredRev == "" {
return nil
}
candidates := []string{preferredRev}
if !strings.HasPrefix(preferredRev, "v") && semverRegexp.MatchString("v"+preferredRev) {
candidates = append(candidates, "v"+preferredRev)
}
return candidates
}
// pkgVersionFromGit determines the actual version to be packaged
// from the git repository status and user preference.
// Besides returning the upstream version, the "upstream" struct
@@ -50,11 +62,12 @@ func pkgVersionFromGit(gitdir string, u *upstream, preferredRev string, forcePre
var cmd *exec.Cmd // the temporary shell commands we execute
// If the user specifies a valid tag as the preferred revision, that tag should be used without additional heuristics.
if preferredRev != "" {
cmd = exec.Command("git", "tag", "--list", preferredRev)
for _, candidate := range preferredRevisionCandidates(preferredRev) {
cmd = exec.Command("git", "tag", "--list", candidate)
cmd.Dir = gitdir
if out, err := cmd.Output(); err == nil && slices.Contains(strings.Fields(string(out)), preferredRev) {
latestTag = preferredRev
if out, err := cmd.Output(); err == nil && slices.Contains(strings.Fields(string(out)), candidate) {
latestTag = candidate
break
}
}
+54 -26
View File
@@ -959,10 +959,10 @@ func mainPack(args []string, usage func()) {
flagSet.Usage = usage
} else {
flagSet.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [pack] [FLAG]... <go-package-importpath>\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Example: %s pack golang.org/x/oauth2\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Usage: %s [pack] [FLAG]... <go-package-importpath>[@revision]...\n", os.Args[0])
fmt.Fprintf(os.Stderr, "Example: %s pack golang.org/x/oauth2@v0.30.0 github.com/pquerna/cachecontrol@v0.2.0\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "\"%s pack\" downloads the specified Go package from the Internet,\nand creates new files and directories in the current working directory.\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\"%s pack\" downloads the specified Go packages from the Internet,\nand creates new files and directories in the current working directory.\n", os.Args[0])
fmt.Fprintf(os.Stderr, "\n")
fmt.Fprintf(os.Stderr, "Flags:\n")
flagSet.PrintDefaults()
@@ -973,7 +973,7 @@ func mainPack(args []string, usage func()) {
flagSet.StringVar(&gitRevision,
"git_revision",
"",
"git revision (see gitrevisions(7)) of the specified Go package\n"+
"legacy default git revision (see gitrevisions(7)) for package arguments without @revision\n"+
"to check out, defaulting to the default behavior of git clone.\n"+
"Useful in case you do not want to package e.g. current HEAD.")
@@ -1026,28 +1026,6 @@ func mainPack(args []string, usage func()) {
}
gitRevision = strings.TrimSpace(gitRevision)
gopkg := flagSet.Arg(0)
// Remove URL scheme if present (https://, http://, git://, etc.)
gopkg = strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(gopkg, "https://"), "http://"), "git://")
// Verify the provided argument using the official pkg.go.dev API. Keep the
// requested path for naming and go_import_path; use the module path only for
// source checkout and tarball generation.
info, err := getPkgsiteInfo(context.TODO(), gopkg)
if err != nil {
log.Fatalf("Verifying arguments: %v — did you specify a Go package import path?", err)
}
sourceGopkg := sourceImportPathForPackage(gopkg, info)
if sourceGopkg != gopkg {
log.Printf("Using module root %q as source checkout for specified import path %q", sourceGopkg, gopkg)
}
// Set default source and binary package names.
openRuyiSrc := nameFromGopkg(gopkg, typeLibrary, customProgPkgName, allowUnknownHoster)
openRuyiLib := openRuyiSrc
openRuyiProgram := nameFromGopkg(gopkg, typeProgram, customProgPkgName, allowUnknownHoster)
var pkgType packageType
switch strings.TrimSpace(pkgTypeString) {
@@ -1068,6 +1046,56 @@ func mainPack(args []string, usage func()) {
log.Fatalf("-type=%q not recognized, aborting\n", pkgTypeString)
}
for _, arg := range flagSet.Args() {
gopkg, revision, err := parsePackArg(arg, gitRevision)
if err != nil {
log.Fatalf("parse package argument %q: %v", arg, err)
}
packPackage(gopkg, revision, forcePrerelease, pkgType, customProgPkgName, allowUnknownHoster)
}
}
func parsePackArg(arg, defaultRevision string) (string, string, error) {
arg = strings.TrimSpace(arg)
defaultRevision = strings.TrimSpace(defaultRevision)
if arg == "" {
return "", "", fmt.Errorf("package import path is empty")
}
if at := strings.LastIndex(arg, "@"); at >= 0 {
revision := strings.TrimSpace(arg[at+1:])
if revision == "" {
return "", "", fmt.Errorf("package revision is empty")
}
path := strings.TrimSpace(arg[:at])
if path == "" {
return "", "", fmt.Errorf("package import path is empty")
}
return path, revision, nil
}
return arg, defaultRevision, nil
}
func packPackage(gopkg, gitRevision string, forcePrerelease bool, pkgType packageType, customProgPkgName string, allowUnknownHoster bool) {
// Remove URL scheme if present (https://, http://, git://, etc.)
gopkg = strings.TrimPrefix(strings.TrimPrefix(strings.TrimPrefix(gopkg, "https://"), "http://"), "git://")
// Verify the provided argument using the official pkg.go.dev API. Keep the
// requested path for naming and go_import_path; use the module path only for
// source checkout and tarball generation.
info, err := getPkgsiteInfo(context.TODO(), gopkg)
if err != nil {
log.Fatalf("Verifying arguments: %v — did you specify a Go package import path?", err)
}
sourceGopkg := sourceImportPathForPackage(gopkg, info)
if sourceGopkg != gopkg {
log.Printf("Using module root %q as source checkout for specified import path %q", sourceGopkg, gopkg)
}
// Set default source and binary package names.
openRuyiSrc := nameFromGopkg(gopkg, typeLibrary, customProgPkgName, allowUnknownHoster)
openRuyiLib := openRuyiSrc
openRuyiProgram := nameFromGopkg(gopkg, typeProgram, customProgPkgName, allowUnknownHoster)
if pkgType != typeGuess {
openRuyiSrc = nameFromGopkg(gopkg, pkgType, customProgPkgName, allowUnknownHoster)
}
+98
View File
@@ -547,6 +547,104 @@ func TestPkgVersionFromGitUsesPackagingDateAndSevenCharHash(t *testing.T) {
}
}
func TestPkgVersionFromGitAcceptsUnprefixedSemverPreferredRevision(t *testing.T) {
dir := t.TempDir()
runGit(t, dir, nil, "init")
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example.com/project\n"), 0644); err != nil {
t.Fatalf("write go.mod: %v", err)
}
runGit(t, dir, nil, "add", "go.mod")
runGit(t, dir, []string{
"GIT_AUTHOR_NAME=Test",
"GIT_AUTHOR_EMAIL=test@example.invalid",
"GIT_COMMITTER_NAME=Test",
"GIT_COMMITTER_EMAIL=test@example.invalid",
}, "commit", "-m", "initial")
runGit(t, dir, nil, "tag", "v1.2.0")
u := upstream{}
got, err := pkgVersionFromGit(dir, &u, "1.2.0", false)
if err != nil {
t.Fatalf("pkgVersionFromGit() returned error: %v", err)
}
if got != "1.2.0" {
t.Fatalf("pkgVersionFromGit() = %q, want %q", got, "1.2.0")
}
if u.tag != "v1.2.0" || !u.isRelease {
t.Fatalf("tag = %q, isRelease = %v, want v1.2.0 release", u.tag, u.isRelease)
}
}
func TestParsePackArg(t *testing.T) {
tests := []struct {
name string
arg string
defaultRevision string
wantPath string
wantRevision string
wantErr bool
}{
{
name: "explicit revision",
arg: "github.com/a/pkg@v1.2.3",
wantPath: "github.com/a/pkg",
wantRevision: "v1.2.3",
},
{
name: "explicit unprefixed semver revision",
arg: "github.com/a/pkg@1.2.3",
wantPath: "github.com/a/pkg",
wantRevision: "1.2.3",
},
{
name: "uses default revision",
arg: "github.com/a/pkg",
defaultRevision: "v0.9.0",
wantPath: "github.com/a/pkg",
wantRevision: "v0.9.0",
},
{
name: "keeps empty default revision",
arg: "github.com/a/pkg",
wantPath: "github.com/a/pkg",
},
{
name: "semantic import version with explicit revision",
arg: "github.com/a/pkg/v2@v2.1.0",
wantPath: "github.com/a/pkg/v2",
wantRevision: "v2.1.0",
},
{
name: "rejects empty explicit revision",
arg: "github.com/a/pkg@",
wantErr: true,
},
{
name: "rejects empty import path",
arg: "@v1.2.3",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPath, gotRevision, err := parsePackArg(tt.arg, tt.defaultRevision)
if tt.wantErr {
if err == nil {
t.Fatal("expected error")
}
return
}
if err != nil {
t.Fatalf("parsePackArg() returned error: %v", err)
}
if gotPath != tt.wantPath || gotRevision != tt.wantRevision {
t.Fatalf("parsePackArg() = (%q, %q), want (%q, %q)", gotPath, gotRevision, tt.wantPath, tt.wantRevision)
}
})
}
}
func runGit(t *testing.T, dir string, env []string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)