Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions internal/plugin/gitdiff/diff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Package gitdiff produces a unified diff by running git locally, reproducing
// the behaviour of the old scripts/start.sh that was removed in v1.0.1.
package gitdiff

import (
"bytes"
"io"
"os/exec"

"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

// Loader fetches the PR diff by running `git fetch` + `git diff` in the
// current working directory. git must be on $PATH and the remote "origin" must
// be reachable (credentials via .netrc, SSH key, or the git credential helper
// already configured in the calling environment).
type Loader struct {
baseBranch string
module string
}

func NewLoader(baseBranch, module string) *Loader {
return &Loader{baseBranch: baseBranch, module: module}
}

// Load fetches origin/<baseBranch> and returns a unified=0 diff against HEAD.
// When module is non-empty it is passed as a path filter to git diff, matching
// the behaviour of the old start.sh for Gradle multi-module projects.
func (l *Loader) Load() (io.Reader, error) {
if l.baseBranch == "" {
return nil, errors.New("base branch is required for PARAMETER_DIFF_SOURCE=git (set PARAMETER_BASE_BRANCH or VELA_PULL_REQUEST_TARGET)")
}

fetchArgs := []string{"fetch", "--no-tags", "origin", l.baseBranch}
logrus.Infof("running git %v", fetchArgs)
fetchCmd := exec.Command("git", fetchArgs...) //nolint:gosec // args are controlled internally, not user input
if out, err := fetchCmd.CombinedOutput(); err != nil {
return nil, errors.Wrapf(err, "git fetch failed: %s", string(out))
}

diffArgs := []string{"--no-pager", "diff", "--unified=0", "origin/" + l.baseBranch}
if l.module != "" {
diffArgs = append(diffArgs, l.module)
}
logrus.Infof("running git %v", diffArgs)
diffCmd := exec.Command("git", diffArgs...) //nolint:gosec // args are controlled internally, not user input
var stdout, stderr bytes.Buffer
diffCmd.Stdout = &stdout
diffCmd.Stderr = &stderr
if err := diffCmd.Run(); err != nil {
return nil, errors.Wrapf(err, "git diff failed: %s", stderr.String())
}

return &stdout, nil
}
32 changes: 29 additions & 3 deletions internal/plugin/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/target/pull-request-code-coverage/internal/plugin/coverage/jacoco"
"github.com/target/pull-request-code-coverage/internal/plugin/coverage/lcov"
"github.com/target/pull-request-code-coverage/internal/plugin/coverage/pythoncov"
"github.com/target/pull-request-code-coverage/internal/plugin/gitdiff"
"github.com/target/pull-request-code-coverage/internal/plugin/githubdiff"
"github.com/target/pull-request-code-coverage/internal/plugin/pluginhttp"
"github.com/target/pull-request-code-coverage/internal/plugin/pluginjson"
Expand Down Expand Up @@ -101,14 +102,39 @@ func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSou

diffSource, found := propertyGetter("PARAMETER_DIFF_SOURCE")
if !found || diffSource == "" {
logrus.Info("PARAMETER_DIFF_SOURCE was missing, defaulting to stdin")
diffSource = "stdin"
// Auto-detect: when running in Vela CI (VELA_PULL_REQUEST_TARGET is set by
// the Vela runtime) fall back to the git diff mode so pipelines that relied
// on the old start.sh entrypoint continue to work without any config change.
if velaTarget, hasVelaTarget := propertyGetter("VELA_PULL_REQUEST_TARGET"); hasVelaTarget && velaTarget != "" {
logrus.Info("PARAMETER_DIFF_SOURCE was missing but VELA_PULL_REQUEST_TARGET is set, defaulting to git")
diffSource = "git"
} else {
logrus.Info("PARAMETER_DIFF_SOURCE was missing, defaulting to stdin")
diffSource = "stdin"
}
}

switch diffSource {
case "stdin":
// changedSourceLinesSource already points at the piped-in diff (stdin);
// nothing to do. This is the original, default behavior.
case "git":
// Runs git fetch + git diff locally, reproducing what start.sh did before
// it was removed in v1.0.1. The base branch is read from PARAMETER_BASE_BRANCH
// first, then VELA_PULL_REQUEST_TARGET for backward compatibility.
baseBranch, hasBranch := propertyGetter("PARAMETER_BASE_BRANCH")
if !hasBranch || baseBranch == "" {
baseBranch, _ = propertyGetter("VELA_PULL_REQUEST_TARGET")
}

logrus.Infof("PARAMETER_DIFF_SOURCE is git, diffing against origin/%s", baseBranch)

diffReader, fetchErr := gitdiff.NewLoader(baseBranch, module).Load()
if fetchErr != nil {
return errors.Wrap(fetchErr, "Failed fetching diff via git")
}

changedSourceLinesSource = diffReader
case "github":
if !ghAPIKeyFound || !repoPRFound || !repoOwnerFound || !repoNameFound {
return errors.New("PARAMETER_DIFF_SOURCE=github requires a GitHub API key (PARAMETER_GH_API_KEY), BUILD_PULL_REQUEST_NUMBER, REPOSITORY_ORG and REPOSITORY_NAME")
Expand All @@ -123,7 +149,7 @@ func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSou

changedSourceLinesSource = diffReader
default:
return errors.Errorf("Unknown PARAMETER_DIFF_SOURCE %q (expected \"stdin\" or \"github\")", diffSource)
return errors.Errorf("Unknown PARAMETER_DIFF_SOURCE %q (expected \"stdin\", \"git\", or \"github\")", diffSource)
}

changedLines, changedLinesErr := unifieddiff.NewChangedSourceLinesLoader(module, sourceDirs).Load(changedSourceLinesSource)
Expand Down
46 changes: 45 additions & 1 deletion internal/plugin/runner_diffsource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/target/pull-request-code-coverage/internal/test/mocks"
)

Expand Down Expand Up @@ -56,7 +57,7 @@ func TestDefaultRunner_Run_DiffSourceUnknown(t *testing.T) {
propGetter.On("GetProperty", "PARAMETER_DIFF_SOURCE").Return("banana", true)

err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), os.Stdout)
assert.EqualError(t, err, "Unknown PARAMETER_DIFF_SOURCE \"banana\" (expected \"stdin\" or \"github\")")
assert.EqualError(t, err, "Unknown PARAMETER_DIFF_SOURCE \"banana\" (expected \"stdin\", \"git\", or \"github\")")

propGetter.AssertExpectations(t)
}
Expand Down Expand Up @@ -117,3 +118,46 @@ func TestDefaultRunner_Run_DiffSourceGithub_FetchesDiff(t *testing.T) {

propGetter.AssertExpectations(t)
}

// When PARAMETER_DIFF_SOURCE is absent but VELA_PULL_REQUEST_TARGET is set the
// runner should auto-select the "git" path. We verify this by using a branch
// name that is guaranteed to not exist on any remote, so git fetch always fails
// and the error message proves the "git" path was taken (not the silent "stdin"
// path that would return 0 lines with no error).
func TestDefaultRunner_Run_DiffSourceAutodetect_VelaTarget(t *testing.T) {
propGetter := mocks.NewMockPropertyGetter()

propGetter.On("GetProperty", "PARAMETER_COVERAGE_FILE").Return("../test/jacocoTestReport.xml", true)
propGetter.On("GetProperty", "PARAMETER_COVERAGE_TYPE").Return("jacoco", true)
propGetter.On("GetProperty", "PARAMETER_SOURCE_DIRS").Return("src/main/java", true)
// No PARAMETER_DIFF_SOURCE — auto-detect should kick in.
// Use a branch that cannot exist on any remote so git fetch always fails.
propGetter.On("GetProperty", "VELA_PULL_REQUEST_TARGET").Return("this-branch-does-not-exist-xyzzy-99999", true)
propGetter.On("GetProperty", "PARAMETER_BASE_BRANCH").Return("", false)

err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), os.Stdout)
// require (not assert) so the test stops here on nil — prevents a nil-deref
// panic on the Contains check below.
require.Error(t, err)
assert.Contains(t, err.Error(), "Failed fetching diff via git")

propGetter.AssertExpectations(t)
}

// PARAMETER_DIFF_SOURCE=git with an explicit PARAMETER_BASE_BRANCH should also
// take the git path and surface a meaningful error when the branch is empty.
func TestDefaultRunner_Run_DiffSourceGit_MissingBaseBranch(t *testing.T) {
propGetter := mocks.NewMockPropertyGetter()

propGetter.On("GetProperty", "PARAMETER_COVERAGE_FILE").Return("../test/jacocoTestReport.xml", true)
propGetter.On("GetProperty", "PARAMETER_COVERAGE_TYPE").Return("jacoco", true)
propGetter.On("GetProperty", "PARAMETER_SOURCE_DIRS").Return("src/main/java", true)
propGetter.On("GetProperty", "PARAMETER_DIFF_SOURCE").Return("git", true)
propGetter.On("GetProperty", "PARAMETER_BASE_BRANCH").Return("", false)
propGetter.On("GetProperty", "VELA_PULL_REQUEST_TARGET").Return("", false)

err := NewRunner().Run(propGetter.GetProperty, strings.NewReader(""), os.Stdout)
assert.EqualError(t, err, "Failed fetching diff via git: base branch is required for PARAMETER_DIFF_SOURCE=git (set PARAMETER_BASE_BRANCH or VELA_PULL_REQUEST_TARGET)")

propGetter.AssertExpectations(t)
}
Loading