diff --git a/LICENSE b/LICENSE index 6a66aea..2a7cf70 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009 The Go Authors. All rights reserved. +Copyright 2009 The Go Authors. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are @@ -10,7 +10,7 @@ notice, this list of conditions and the following disclaimer. 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 + * Neither the name of Google LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. diff --git a/README.md b/README.md index 94da72d..4bbeeaa 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ That is, it is for direct manipulation of Go modules themselves. It is NOT about supporting general development tools that need to do things like load packages in module mode. That use case, where modules are incidental rather than the focus, -should remain in [x/tools](https://pkg.go.dev/golang/org/x/tools), +should remain in [x/tools](https://pkg.go.dev/golang.org/x/tools), specifically [x/tools/go/packages](https://pkg.go.dev/golang.org/x/tools/go/packages). The specific case of loading packages should still be done by diff --git a/go.mod b/go.mod index dec228a..0339736 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module golang.org/x/mod -go 1.18 +go 1.22.0 require golang.org/x/tools v0.13.0 // tagx:ignore diff --git a/modfile/read.go b/modfile/read.go index 2205682..de1b982 100644 --- a/modfile/read.go +++ b/modfile/read.go @@ -226,8 +226,9 @@ func (x *FileSyntax) Cleanup() { continue } if ww == 1 && len(stmt.RParen.Comments.Before) == 0 { - // Collapse block into single line. - line := &Line{ + // Collapse block into single line but keep the Line reference used by the + // parsed File structure. + *stmt.Line[0] = Line{ Comments: Comments{ Before: commentsAdd(stmt.Before, stmt.Line[0].Before), Suffix: commentsAdd(stmt.Line[0].Suffix, stmt.Suffix), @@ -235,7 +236,7 @@ func (x *FileSyntax) Cleanup() { }, Token: stringsAdd(stmt.Token, stmt.Line[0].Token), } - x.Stmt[w] = line + x.Stmt[w] = stmt.Line[0] w++ continue } diff --git a/modfile/read_test.go b/modfile/read_test.go index efc75e1..8f17eea 100644 --- a/modfile/read_test.go +++ b/modfile/read_test.go @@ -603,3 +603,134 @@ comments before "// k" }) } } + +func TestCleanup(t *testing.T) { + for _, test := range []struct { + desc string + want string + input []Expr + }{ + { + desc: "simple_lines", + want: `line: module a +line: require b v1.0.0 +`, + input: []Expr{ + &Line{ + Token: []string{"module", "a"}, + }, + &Line{ + Token: []string{"require", "b", "v1.0.0"}, + }, + &Line{ + Token: nil, + }, + }, + }, { + desc: "line_block", + want: `line: module a +block: require +blockline: b v1.0.0 +blockline: c v1.0.0 +`, + input: []Expr{ + &Line{ + Token: []string{"module", "a"}, + }, + &LineBlock{ + Token: []string{"require"}, + Line: []*Line{ + { + Token: []string{"b", "v1.0.0"}, + InBlock: true, + }, + { + Token: nil, + InBlock: true, + }, + { + Token: []string{"c", "v1.0.0"}, + InBlock: true, + }, + }, + }, + }, + }, { + desc: "collapse", + want: `line: module a +line: require b v1.0.0 +`, + input: []Expr{ + &Line{ + Token: []string{"module", "a"}, + }, + &LineBlock{ + Token: []string{"require"}, + Line: []*Line{ + { + Token: []string{"b", "v1.0.0"}, + InBlock: true, + }, + { + Token: nil, + InBlock: true, + }, + }, + }, + }, + }, + } { + t.Run(test.desc, func(t *testing.T) { + syntax := &FileSyntax{ + Stmt: test.input, + } + syntax.Cleanup() + + buf := &bytes.Buffer{} + for _, stmt := range syntax.Stmt { + switch stmt := stmt.(type) { + case *Line: + fmt.Fprintf(buf, "line: %v\n", strings.Join(stmt.Token, " ")) + case *LineBlock: + fmt.Fprintf(buf, "block: %v\n", strings.Join(stmt.Token, " ")) + for _, line := range stmt.Line { + fmt.Fprintf(buf, "blockline: %v\n", strings.Join(line.Token, " ")) + } + } + } + + got := strings.TrimSpace(buf.String()) + want := strings.TrimSpace(test.want) + if got != want { + t.Errorf("got:\n%s\nwant:\n%s", got, want) + } + }) + } +} + +// Issue 45130: File.Cleanup breaks references so future edits do nothing +func TestCleanupMaintainsRefs(t *testing.T) { + lineB := &Line{ + Token: []string{"b", "v1.0.0"}, + InBlock: true, + } + syntax := &FileSyntax{ + Stmt: []Expr{ + &LineBlock{ + Token: []string{"require"}, + Line: []*Line{ + lineB, + { + Token: nil, + InBlock: true, + }, + }, + }, + }, + } + syntax.Cleanup() + + if syntax.Stmt[0] != lineB { + t.Errorf("got:\n%v\nwant:\n%v", syntax.Stmt[0], lineB) + } +} diff --git a/modfile/rule.go b/modfile/rule.go index 66dcaf9..3e4a1d0 100644 --- a/modfile/rule.go +++ b/modfile/rule.go @@ -43,6 +43,7 @@ type File struct { Exclude []*Exclude Replace []*Replace Retract []*Retract + Tool []*Tool Syntax *FileSyntax } @@ -93,6 +94,12 @@ type Retract struct { Syntax *Line } +// A Tool is a single tool statement. +type Tool struct { + Path string + Syntax *Line +} + // A VersionInterval represents a range of versions with upper and lower bounds. // Intervals are closed: both bounds are included. When Low is equal to High, // the interval may refer to a single version ('v1.2.3') or an interval @@ -297,7 +304,7 @@ func parseToFile(file string, data []byte, fix VersionFixer, strict bool) (parse }) } continue - case "module", "godebug", "require", "exclude", "replace", "retract": + case "module", "godebug", "require", "exclude", "replace", "retract", "tool": for _, l := range x.Line { f.add(&errs, x, l, x.Token[0], l.Token, fix, strict) } @@ -509,6 +516,21 @@ func (f *File) add(errs *ErrorList, block *LineBlock, line *Line, verb string, a Syntax: line, } f.Retract = append(f.Retract, retract) + + case "tool": + if len(args) != 1 { + errorf("tool directive expects exactly one argument") + return + } + s, err := parseString(&args[0]) + if err != nil { + errorf("invalid quoted string: %v", err) + return + } + f.Tool = append(f.Tool, &Tool{ + Path: s, + Syntax: line, + }) } } @@ -1567,6 +1589,36 @@ func (f *File) DropRetract(vi VersionInterval) error { return nil } +// AddTool adds a new tool directive with the given path. +// It does nothing if the tool line already exists. +func (f *File) AddTool(path string) error { + for _, t := range f.Tool { + if t.Path == path { + return nil + } + } + + f.Tool = append(f.Tool, &Tool{ + Path: path, + Syntax: f.Syntax.addLine(nil, "tool", path), + }) + + f.SortBlocks() + return nil +} + +// RemoveTool removes a tool directive with the given path. +// It does nothing if no such tool directive exists. +func (f *File) DropTool(path string) error { + for _, t := range f.Tool { + if t.Path == path { + t.Syntax.markRemoved() + *t = Tool{} + } + } + return nil +} + func (f *File) SortBlocks() { f.removeDups() // otherwise sorting is unsafe @@ -1593,9 +1645,9 @@ func (f *File) SortBlocks() { } } -// removeDups removes duplicate exclude and replace directives. +// removeDups removes duplicate exclude, replace and tool directives. // -// Earlier exclude directives take priority. +// Earlier exclude and tool directives take priority. // // Later replace directives take priority. // @@ -1605,10 +1657,10 @@ func (f *File) SortBlocks() { // retract directives are not de-duplicated since comments are // meaningful, and versions may be retracted multiple times. func (f *File) removeDups() { - removeDups(f.Syntax, &f.Exclude, &f.Replace) + removeDups(f.Syntax, &f.Exclude, &f.Replace, &f.Tool) } -func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace) { +func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace, tool *[]*Tool) { kill := make(map[*Line]bool) // Remove duplicate excludes. @@ -1649,6 +1701,24 @@ func removeDups(syntax *FileSyntax, exclude *[]*Exclude, replace *[]*Replace) { } *replace = repl + if tool != nil { + haveTool := make(map[string]bool) + for _, t := range *tool { + if haveTool[t.Path] { + kill[t.Syntax] = true + continue + } + haveTool[t.Path] = true + } + var newTool []*Tool + for _, t := range *tool { + if !kill[t.Syntax] { + newTool = append(newTool, t) + } + } + *tool = newTool + } + // Duplicate require and retract directives are not removed. // Drop killed statements from the syntax tree. diff --git a/modfile/rule_test.go b/modfile/rule_test.go index 4d0d12a..c75a77a 100644 --- a/modfile/rule_test.go +++ b/modfile/rule_test.go @@ -1714,6 +1714,72 @@ var dropGodebugTests = []struct { }, } +var addToolTests = []struct { + desc, in, path, want string +}{ + { + `add_first`, + `module example.com/m`, + `example.com/tool/v1`, + `module example.com/m + tool example.com/tool/v1`, + }, + { + `sorted_correctly`, + `module example.com/m + tool example.com/tool2 + `, + `example.com/tool1`, + `module example.com/m + tool ( + example.com/tool1 + example.com/tool2 + )`, + }, + { + `duplicates_ignored`, + `module example.com/m + tool example.com/tool1 + `, + `example.com/tool1`, + `module example.com/m + tool example.com/tool1`, + }, +} + +var dropToolTests = []struct { + desc, in, path, want string +}{ + { + `only`, + `module example.com/m + tool example.com/tool1`, + `example.com/tool1`, + `module example.com/m`, + }, + { + `parenthesized`, + `module example.com/m + tool ( + example.com/tool1 + example.com/tool2 + )`, + `example.com/tool1`, + `module example.com/m + tool example.com/tool2`, + }, + { + `missing`, + `module example.com/m + tool ( + example.com/tool2 + )`, + `example.com/tool1`, + `module example.com/m + tool example.com/tool2`, + }, +} + func fixV(path, version string) (string, error) { if path != "example.com/m" { return "", fmt.Errorf("module path must be example.com/m") @@ -2051,6 +2117,7 @@ func TestAddOnEmptyFile(t *testing.T) { t.Fatal(err) } got, err := f.Format() + if err != nil { t.Fatal(err) } @@ -2061,3 +2128,65 @@ func TestAddOnEmptyFile(t *testing.T) { }) } } + +func TestAddTool(t *testing.T) { + for _, tt := range addToolTests { + t.Run(tt.desc, func(t *testing.T) { + inFile, err := Parse("in", []byte(tt.in), nil) + if err != nil { + t.Fatal(err) + } + if err := inFile.AddTool(tt.path); err != nil { + t.Fatal(err) + } + inFile.Cleanup() + got, err := inFile.Format() + if err != nil { + t.Fatal(err) + } + + outFile, err := Parse("out", []byte(tt.want), nil) + if err != nil { + t.Fatal(err) + } + want, err := outFile.Format() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("got:\n%s\nwant:\n%s", got, want) + } + }) + } +} + +func TestDropTool(t *testing.T) { + for _, tt := range dropToolTests { + t.Run(tt.desc, func(t *testing.T) { + inFile, err := Parse("in", []byte(tt.in), nil) + if err != nil { + t.Fatal(err) + } + if err := inFile.DropTool(tt.path); err != nil { + t.Fatal(err) + } + inFile.Cleanup() + got, err := inFile.Format() + if err != nil { + t.Fatal(err) + } + + outFile, err := Parse("out", []byte(tt.want), nil) + if err != nil { + t.Fatal(err) + } + want, err := outFile.Format() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(got, want) { + t.Fatalf("got:\n%s\nwant:\n%s", got, want) + } + }) + } +} diff --git a/modfile/work.go b/modfile/work.go index 8f54897..5387d0c 100644 --- a/modfile/work.go +++ b/modfile/work.go @@ -331,5 +331,5 @@ func (f *WorkFile) SortBlocks() { // retract directives are not de-duplicated since comments are // meaningful, and versions may be retracted multiple times. func (f *WorkFile) removeDups() { - removeDups(f.Syntax, nil, &f.Replace) + removeDups(f.Syntax, nil, &f.Replace, nil) } diff --git a/module/module.go b/module/module.go index cac1a89..2a364b2 100644 --- a/module/module.go +++ b/module/module.go @@ -506,7 +506,6 @@ var badWindowsNames = []string{ "PRN", "AUX", "NUL", - "COM0", "COM1", "COM2", "COM3", @@ -516,7 +515,6 @@ var badWindowsNames = []string{ "COM7", "COM8", "COM9", - "LPT0", "LPT1", "LPT2", "LPT3", diff --git a/sumdb/client.go b/sumdb/client.go index 04c6e24..04dbdfe 100644 --- a/sumdb/client.go +++ b/sumdb/client.go @@ -142,6 +142,14 @@ func (c *Client) initWork() { c.verifiers = note.VerifierList(verifier) c.name = verifier.Name() + if c.latest.N == 0 { + c.latest.Hash, err = tlog.TreeHash(0, nil) + if err != nil { + c.initErr = err + return + } + } + data, err := c.ops.ReadConfig(c.name + "/latest") if err != nil { c.initErr = err diff --git a/sumdb/tlog/tlog.go b/sumdb/tlog/tlog.go index 6a11a75..f7ea753 100644 --- a/sumdb/tlog/tlog.go +++ b/sumdb/tlog/tlog.go @@ -234,14 +234,22 @@ func (f HashReaderFunc) ReadHashes(indexes []int64) ([]Hash, error) { return f(indexes) } +// emptyHash is the hash of the empty tree, per RFC 6962, Section 2.1. +// It is the hash of the empty string. +var emptyHash = Hash{ + 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, + 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, + 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, + 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55, +} + // TreeHash computes the hash for the root of the tree with n records, // using the HashReader to obtain previously stored hashes // (those returned by StoredHashes during the writes of those n records). // TreeHash makes a single call to ReadHash requesting at most 1 + logâ‚‚ n hashes. -// The tree of size zero is defined to have an all-zero Hash. func TreeHash(n int64, r HashReader) (Hash, error) { if n == 0 { - return Hash{}, nil + return emptyHash, nil } indexes := subTreeIndex(0, n, nil) hashes, err := r.ReadHashes(indexes) diff --git a/sumdb/tlog/tlog_test.go b/sumdb/tlog/tlog_test.go index 584e728..79db244 100644 --- a/sumdb/tlog/tlog_test.go +++ b/sumdb/tlog/tlog_test.go @@ -6,6 +6,7 @@ package tlog import ( "bytes" + "crypto/sha256" "fmt" "testing" ) @@ -267,3 +268,13 @@ func TestTilePath(t *testing.T) { } } } + +func TestEmptyTree(t *testing.T) { + h, err := TreeHash(0, nil) + if err != nil { + t.Fatal(err) + } + if h != sha256.Sum256(nil) { + t.Fatalf("TreeHash(0) = %x, want SHA-256('')", h) + } +} diff --git a/zip/zip.go b/zip/zip.go index 574f83f..5aed6e2 100644 --- a/zip/zip.go +++ b/zip/zip.go @@ -573,7 +573,9 @@ func CreateFromDir(w io.Writer, m module.Version, dir string) (err error) { // VCS repository stored locally. The zip content is written to w. // // repoRoot must be an absolute path to the base of the repository, such as -// "/Users/some-user/some-repo". +// "/Users/some-user/some-repo". If the repository is a Git repository, +// this path is expected to point to its worktree: it can't be a bare git +// repo. // // revision is the revision of the repository to create the zip from. Examples // include HEAD or SHA sums for git repositories. diff --git a/zip/zip_test.go b/zip/zip_test.go index 606e7aa..106df80 100644 --- a/zip/zip_test.go +++ b/zip/zip_test.go @@ -1325,7 +1325,7 @@ func downloadVCSZip(t testing.TB, vcs, url, rev, subdir string) (repoDir string, switch vcs { case "git": // Create a repository and download the revision we want. - if _, err := run(t, repoDir, "git", "init", "--bare"); err != nil { + if _, err := runWithGitDir(t, repoDir, repoDir, "git", "init", "--bare"); err != nil { return "", nil, err } if err := os.MkdirAll(filepath.Join(repoDir, "info"), 0777); err != nil { @@ -1342,7 +1342,7 @@ func downloadVCSZip(t testing.TB, vcs, url, rev, subdir string) (repoDir string, if err := attrFile.Close(); err != nil { return "", nil, err } - if _, err := run(t, repoDir, "git", "remote", "add", "origin", "--", url); err != nil { + if _, err := runWithGitDir(t, repoDir, repoDir, "git", "remote", "add", "origin", "--", url); err != nil { return "", nil, err } var refSpec string @@ -1351,7 +1351,7 @@ func downloadVCSZip(t testing.TB, vcs, url, rev, subdir string) (repoDir string, } else { refSpec = fmt.Sprintf("%s:refs/dummy", rev) } - if _, err := run(t, repoDir, "git", "fetch", "-f", "--depth=1", "origin", refSpec); err != nil { + if _, err := runWithGitDir(t, repoDir, repoDir, "git", "fetch", "-f", "--depth=1", "origin", refSpec); err != nil { return "", nil, err } @@ -1368,6 +1368,7 @@ func downloadVCSZip(t testing.TB, vcs, url, rev, subdir string) (repoDir string, cmd := exec.Command("git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", rev, "--", subdirArg) cmd.Dir = repoDir + cmd.Env = append(cmd.Environ(), "GIT_DIR="+repoDir) cmd.Stdout = tmpZipFile stderr := new(strings.Builder) cmd.Stderr = stderr @@ -1425,17 +1426,20 @@ func downloadVCSFile(t testing.TB, vcs, repo, rev, file string) ([]byte, error) t.Helper() switch vcs { case "git": - return run(t, repo, "git", "cat-file", "blob", rev+":"+file) + return runWithGitDir(t, repo, repo, "git", "cat-file", "blob", rev+":"+file) default: return nil, fmt.Errorf("vcs %q not supported", vcs) } } -func run(t testing.TB, dir string, name string, args ...string) ([]byte, error) { +func runWithGitDir(t testing.TB, gitDir, dir string, name string, args ...string) ([]byte, error) { t.Helper() cmd := exec.Command(name, args...) cmd.Dir = dir + if gitDir != "" { + cmd.Env = append(cmd.Environ(), "GIT_DIR="+gitDir) + } stderr := new(strings.Builder) cmd.Stderr = stderr @@ -1450,6 +1454,12 @@ func run(t testing.TB, dir string, name string, args ...string) ([]byte, error) return out, err } +func run(t testing.TB, dir string, name string, args ...string) ([]byte, error) { + t.Helper() + + return runWithGitDir(t, "", dir, name, args...) +} + type zipFile struct { name string f *zip.File