diff --git a/internal/gitrepo/gitrepo.go b/internal/gitrepo/gitrepo.go index 2d356588..9e9ab335 100644 --- a/internal/gitrepo/gitrepo.go +++ b/internal/gitrepo/gitrepo.go @@ -23,7 +23,7 @@ import ( "golang.org/x/vulndb/internal/worker/log" ) -// Clone returns a repo by cloning the repo at repoURL. +// Clone returns a bare repo by cloning the repo at repoURL. func Clone(ctx context.Context, repoURL string) (repo *git.Repository, err error) { defer derrors.Wrap(&err, "gitrepo.Clone(%q)", repoURL) ctx = event.Start(ctx, "gitrepo.Clone") @@ -39,6 +39,22 @@ func Clone(ctx context.Context, repoURL string) (repo *git.Repository, err error }) } +// PlainClone returns a (non-bare) repo with its history by cloning the repo at repoURL. +func PlainClone(ctx context.Context, dir, repoURL string) (repo *git.Repository, err error) { + defer derrors.Wrap(&err, "gitrepo.PlainClone(%q)", repoURL) + ctx = event.Start(ctx, "gitrepo.PlainClone") + defer event.End(ctx) + + log.Infof(ctx, "Plain cloning repo %q at HEAD", repoURL) + return git.PlainCloneContext(ctx, dir, false, &git.CloneOptions{ + URL: repoURL, + ReferenceName: plumbing.HEAD, + SingleBranch: true, // allow branches other than master + Depth: 0, // pull in history + Tags: git.NoTags, + }) +} + // Open returns a repo by opening the repo at the local path dirpath. func Open(ctx context.Context, dirpath string) (repo *git.Repository, err error) { defer derrors.Wrap(&err, "gitrepo.Open(%q)", dirpath) diff --git a/internal/symbols/patched_functions.go b/internal/symbols/patched_functions.go index ba92248f..1e3ee2f1 100644 --- a/internal/symbols/patched_functions.go +++ b/internal/symbols/patched_functions.go @@ -6,6 +6,7 @@ package symbols import ( "bytes" + "context" "errors" "fmt" "go/ast" @@ -19,9 +20,84 @@ import ( "reflect" "strings" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" "golang.org/x/mod/modfile" + "golang.org/x/vulndb/internal/derrors" + "golang.org/x/vulndb/internal/gitrepo" ) +// Patched returns symbols of module patched in commit identified +// by commitHash. repoURL is URL of the git repository containing +// the module. +// +// Patched returns a map from package import paths to symbols +// patched in the package. Test packages and symbols are omitted. +// +// If the commit has more than one parent, an error is returned. +func Patched(module, repoURL, commitHash string) (_ map[string][]string, err error) { + defer derrors.Wrap(&err, "Patched(%s ,%s, %s)", module, repoURL, commitHash) + + repoRoot, err := os.MkdirTemp("", commitHash) + if err != nil { + return nil, err + } + defer func() { + _ = os.RemoveAll(repoRoot) + }() + + ctx := context.Background() + repo, err := gitrepo.PlainClone(ctx, repoRoot, repoURL) + if err != nil { + return nil, err + } + + w, err := repo.Worktree() + if err != nil { + return nil, err + } + + hash := plumbing.NewHash(commitHash) + commit, err := repo.CommitObject(hash) + if err != nil { + return nil, err + } + + if commit.NumParents() != 1 { + return nil, fmt.Errorf("more than 1 parent: %d", commit.NumParents()) + } + + parent, err := commit.Parent(0) + if err != nil { + return nil, err + } + + if err := w.Checkout(&git.CheckoutOptions{Hash: hash, Force: true}); err != nil { + return nil, err + } + + newSymbols, err := moduleSymbols(repoRoot, module) + if err != nil { + return nil, err + } + + if err := w.Checkout(&git.CheckoutOptions{Hash: parent.Hash, Force: true}); err != nil { + return nil, err + } + + oldSymbols, err := moduleSymbols(repoRoot, module) + if err != nil { + return nil, err + } + + patched := patchedSymbols(oldSymbols, newSymbols) + pkgSyms := make(map[string][]string) + for _, sym := range patched { + pkgSyms[sym.pkg] = append(pkgSyms[sym.pkg], sym.symbol) + } + return pkgSyms, nil +} + // patchedSymbols returns symbol indices in oldSymbols that either 1) cannot // be identified in newSymbols or 2) the corresponding functions have their // source code changed.