diff --git a/demo/.gitignore b/demo/.gitignore index 9e6bf7cd6..e544bbd48 100644 --- a/demo/.gitignore +++ b/demo/.gitignore @@ -1 +1,2 @@ workspace +snapshot-plugins diff --git a/demo/Dockerfile-snapshot b/demo/Dockerfile-snapshot new file mode 100644 index 000000000..9617760ba --- /dev/null +++ b/demo/Dockerfile-snapshot @@ -0,0 +1,2 @@ +FROM jenkinsci/pipeline-as-code-github-demo:RELEASE +ADD snapshot-plugins /usr/share/jenkins/ref/plugins diff --git a/demo/Makefile b/demo/Makefile index cf415a3e2..4b945a53c 100644 --- a/demo/Makefile +++ b/demo/Makefile @@ -2,6 +2,7 @@ IMAGE=jenkinsci/pipeline-as-code-github-demo TAG=$(shell sed -ne s/github-branch-source://p < plugins.txt) # an arbitrary directory we can share with the host WORKSPACE=$(shell pwd)/workspace +DOCKER_RUN=docker run --rm -p 8080:8080 -p 4040:4040 -v /var/run/docker.sock:/var/run/docker.sock -v $(WORKSPACE):$(WORKSPACE) -e WORKSPACE=$(WORKSPACE) -ti build: docker build -t $(IMAGE):$(TAG) . @@ -9,9 +10,16 @@ build: run: build rm -rf $(WORKSPACE) mkdir $(WORKSPACE) - docker run --rm -p 8080:8080 -p 4040:4040 -v /var/run/docker.sock:/var/run/docker.sock -v $(WORKSPACE):$(WORKSPACE) -e WORKSPACE=$(WORKSPACE) -ti $(IMAGE):$(TAG) + $(DOCKER_RUN) $(IMAGE):$(TAG) -# TODO support snapshots using technique from workflow-aggregator-plugin/demo/Makefile +build-snapshot: + docker build -t $(IMAGE):RELEASE . + mkdir -p snapshot-plugins + for p in $$(cat plugins.txt|perl -pe s/:.+//g; cat snapshot-only-plugins.txt); do echo looking for snapshot builds of $$p; for g in org/jenkins-ci/plugins org/jenkins-ci/plugins/workflow org/jenkins-ci/plugins/pipeline-stage-view com/coravy/hudson/plugins/github; do if [ -f ~/.m2/repository/$$g/$$p/maven-metadata-local.xml ]; then cp -v $$(ls -1 ~/.m2/repository/$$g/$$p/*-SNAPSHOT/*.hpi | tail -1) snapshot-plugins/$$p.jpi; fi; done; done + docker build -f Dockerfile-snapshot -t $(IMAGE):SNAPSHOT . + +run-snapshot: build-snapshot + $(DOCKER_RUN) $(IMAGE):SNAPSHOT push: docker push $(IMAGE):$(TAG) diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubBuildStatusNotification.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubBuildStatusNotification.java index b3bb3d509..d80a54368 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubBuildStatusNotification.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubBuildStatusNotification.java @@ -44,10 +44,8 @@ import jenkins.scm.api.SCMRevisionAction; import jenkins.scm.api.SCMSource; import jenkins.scm.api.SCMSourceOwner; -import org.kohsuke.github.GHCommit; import org.kohsuke.github.GHCommitState; import org.kohsuke.github.GHPullRequest; -import org.kohsuke.github.GHPullRequestCommitDetail; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GitHub; @@ -56,7 +54,6 @@ import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; -import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; @@ -73,9 +70,9 @@ public class GitHubBuildStatusNotification { private static final Logger LOGGER = Logger.getLogger(GitHubBuildStatusNotification.class.getName()); - private static void createCommitStatus(@Nonnull GHRepository repo, @Nonnull String revision, @Nonnull GHCommitState state, @Nonnull String url, @Nonnull String message) throws IOException { + private static void createCommitStatus(@Nonnull GHRepository repo, @Nonnull String revision, @Nonnull GHCommitState state, @Nonnull String url, @Nonnull String message, @Nonnull Job job) throws IOException { LOGGER.log(Level.FINE, "{0}/commit/{1} {2} from {3}", new Object[] {repo.getHtmlUrl(), revision, state, url}); - repo.createCommitStatus(revision, state, url, message, "Jenkins"); + repo.createCommitStatus(revision, state, url, message, "Jenkins job " + job.getName()); } @SuppressWarnings("deprecation") // Run.getAbsoluteUrl appropriate here @@ -96,17 +93,18 @@ private static void createBuildCommitStatus(Run build, TaskListener listene try { Result result = build.getResult(); String revisionToNotify = resolveHeadCommit(repo, revision); + Job job = build.getParent(); if (Result.SUCCESS.equals(result)) { - createCommitStatus(repo, revisionToNotify, GHCommitState.SUCCESS, url, Messages.GitHubBuildStatusNotification_CommitStatus_Good()); + createCommitStatus(repo, revisionToNotify, GHCommitState.SUCCESS, url, Messages.GitHubBuildStatusNotification_CommitStatus_Good(), job); } else if (Result.UNSTABLE.equals(result)) { - createCommitStatus(repo, revisionToNotify, GHCommitState.FAILURE, url, Messages.GitHubBuildStatusNotification_CommitStatus_Unstable()); + createCommitStatus(repo, revisionToNotify, GHCommitState.FAILURE, url, Messages.GitHubBuildStatusNotification_CommitStatus_Unstable(), job); } else if (Result.FAILURE.equals(result)) { - createCommitStatus(repo, revisionToNotify, GHCommitState.FAILURE, url, Messages.GitHubBuildStatusNotification_CommitStatus_Failure()); + createCommitStatus(repo, revisionToNotify, GHCommitState.FAILURE, url, Messages.GitHubBuildStatusNotification_CommitStatus_Failure(), job); } else if (result != null) { // ABORTED etc. - createCommitStatus(repo, revisionToNotify, GHCommitState.ERROR, url, Messages.GitHubBuildStatusNotification_CommitStatus_Other()); + createCommitStatus(repo, revisionToNotify, GHCommitState.ERROR, url, Messages.GitHubBuildStatusNotification_CommitStatus_Other(), job); } else { ignoreError = true; - createCommitStatus(repo, revisionToNotify, GHCommitState.PENDING, url, Messages.GitHubBuildStatusNotification_CommitStatus_Pending()); + createCommitStatus(repo, revisionToNotify, GHCommitState.PENDING, url, Messages.GitHubBuildStatusNotification_CommitStatus_Pending(), job); } if (result != null) { listener.getLogger().format("%n" + Messages.GitHubBuildStatusNotification_CommitStatusSet() + "%n%n"); @@ -198,7 +196,7 @@ private static GitHubSCMSource getSCMSource(final SCMSourceOwner scmSourceOwner) } // Has not been built yet, so we can only guess that the current PR head is what will be built. // In fact the submitter might push another commit before this build even starts. - createCommitStatus(repo, pr.getHead().getSha(), GHCommitState.PENDING, url, "This pull request is scheduled to be built"); + createCommitStatus(repo, pr.getHead().getSha(), GHCommitState.PENDING, url, "This pull request is scheduled to be built", job); } } catch (FileNotFoundException fnfe) { LOGGER.log(Level.WARNING, "Could not update commit status to PENDING. Valid scan credentials? Valid scopes?"); @@ -239,35 +237,12 @@ private static GitHubSCMSource getSCMSource(final SCMSourceOwner scmSourceOwner) private static String resolveHeadCommit(GHRepository repo, SCMRevision revision) throws IllegalArgumentException { if (revision instanceof SCMRevisionImpl) { - SCMRevisionImpl rev = (SCMRevisionImpl) revision; - try { - GHCommit commit = repo.getCommit(rev.getHash()); - List parents = commit.getParents(); - // if revision has two parent commits, we have really a MergeCommit - if (parents.size() == 2) { - SCMHead head = revision.getHead(); - // MergeCommit is coming from a pull request - if (head instanceof PullRequestSCMHead) { - GHPullRequest pullRequest = repo.getPullRequest(((PullRequestSCMHead) head).getNumber()); - for (GHPullRequestCommitDetail commitDetail : pullRequest.listCommits()) { - if (commitDetail.getSha().equals(parents.get(0).getSHA1())) { - // Parent commit (HeadCommit) found in PR commit list - return parents.get(0).getSHA1(); - } - } - // First parent commit not found in PR commit list, so returning the second one. - return parents.get(1).getSHA1(); - } else { - return rev.getHash(); - } - } - return rev.getHash(); - } catch (IOException e) { - LOGGER.log(Level.WARNING, null, e); - throw new IllegalArgumentException(e); - } + return ((SCMRevisionImpl) revision).getHash(); + } else if (revision instanceof PullRequestSCMRevision) { + return ((PullRequestSCMRevision) revision).getPullHash(); + } else { + throw new IllegalArgumentException("did not recognize " + revision); } - throw new IllegalArgumentException(); } private GitHubBuildStatusNotification() {} diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java index 02fd4fe4d..944a459a6 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java @@ -27,6 +27,7 @@ import com.cloudbees.plugins.credentials.CredentialsNameProvider; import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.AbortException; import hudson.Extension; import hudson.Util; @@ -41,6 +42,7 @@ import java.util.regex.Pattern; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; +import javax.inject.Inject; import jenkins.scm.api.SCMNavigator; import jenkins.scm.api.SCMNavigatorDescriptor; import jenkins.scm.api.SCMSourceObserver; @@ -70,6 +72,18 @@ public class GitHubSCMNavigator extends SCMNavigator { @CheckForNull private String includes; @CheckForNull private String excludes; + /** Whether to build regular origin branches. */ + private @Nonnull Boolean buildOriginBranch = DescriptorImpl.defaultBuildOriginBranch; + /** Whether to build origin branches which happen to also have a PR filed from them (but here we are naming and building as a branch). */ + private @Nonnull Boolean buildOriginBranchWithPR = DescriptorImpl.defaultBuildOriginBranchWithPR; + /** Whether to build PRs filed from the origin, where the build is of the merge with the base branch. */ + private @Nonnull Boolean buildOriginPRMerge = DescriptorImpl.defaultBuildOriginPRMerge; + /** Whether to build PRs filed from the origin, where the build is of the branch head. */ + private @Nonnull Boolean buildOriginPRHead = DescriptorImpl.defaultBuildOriginPRHead; + /** Whether to build PRs filed from a fork, where the build is of the merge with the base branch. */ + private @Nonnull Boolean buildForkPRMerge = DescriptorImpl.defaultBuildForkPRMerge; + /** Whether to build PRs filed from a fork, where the build is of the branch head. */ + private @Nonnull Boolean buildForkPRHead = DescriptorImpl.defaultBuildForkPRHead; @DataBoundConstructor public GitHubSCMNavigator(String apiUri, String repoOwner, String scanCredentialsId, String checkoutCredentialsId) { this.repoOwner = repoOwner; @@ -78,6 +92,30 @@ public class GitHubSCMNavigator extends SCMNavigator { this.apiUri = Util.fixEmpty(apiUri); } + /** Use defaults for old settings. */ + @SuppressFBWarnings(value="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification="Only non-null after we set them here!") + private Object readResolve() { + if (buildOriginBranch == null) { + buildOriginBranch = DescriptorImpl.defaultBuildOriginBranch; + } + if (buildOriginBranchWithPR == null) { + buildOriginBranchWithPR = DescriptorImpl.defaultBuildOriginBranchWithPR; + } + if (buildOriginPRMerge == null) { + buildOriginPRMerge = DescriptorImpl.defaultBuildOriginPRMerge; + } + if (buildOriginPRHead == null) { + buildOriginPRHead = DescriptorImpl.defaultBuildOriginPRHead; + } + if (buildForkPRMerge == null) { + buildForkPRMerge = DescriptorImpl.defaultBuildForkPRMerge; + } + if (buildForkPRHead == null) { + buildForkPRHead = DescriptorImpl.defaultBuildForkPRHead; + } + return this; + } + @Nonnull public String getIncludes() { return includes != null ? includes : DescriptorImpl.defaultIncludes; } @@ -94,6 +132,60 @@ public class GitHubSCMNavigator extends SCMNavigator { this.excludes = excludes.equals(DescriptorImpl.defaultExcludes) ? null : excludes; } + public boolean getBuildOriginBranch() { + return buildOriginBranch; + } + + @DataBoundSetter + public void setBuildOriginBranch(boolean buildOriginBranch) { + this.buildOriginBranch = buildOriginBranch; + } + + public boolean getBuildOriginBranchWithPR() { + return buildOriginBranchWithPR; + } + + @DataBoundSetter + public void setBuildOriginBranchWithPR(boolean buildOriginBranchWithPR) { + this.buildOriginBranchWithPR = buildOriginBranchWithPR; + } + + public boolean getBuildOriginPRMerge() { + return buildOriginPRMerge; + } + + @DataBoundSetter + public void setBuildOriginPRMerge(boolean buildOriginPRMerge) { + this.buildOriginPRMerge = buildOriginPRMerge; + } + + public boolean getBuildOriginPRHead() { + return buildOriginPRHead; + } + + @DataBoundSetter + public void setBuildOriginPRHead(boolean buildOriginPRHead) { + this.buildOriginPRHead = buildOriginPRHead; + } + + public boolean getBuildForkPRMerge() { + return buildForkPRMerge; + } + + @DataBoundSetter + public void setBuildForkPRMerge(boolean buildForkPRMerge) { + this.buildForkPRMerge = buildForkPRMerge; + } + + public boolean getBuildForkPRHead() { + return buildForkPRHead; + } + + @DataBoundSetter + public void setBuildForkPRHead(boolean buildForkPRHead) { + this.buildForkPRHead = buildForkPRHead; + } + public String getRepoOwner() { return repoOwner; } @@ -220,6 +312,12 @@ private void add(TaskListener listener, SCMSourceObserver observer, GHRepository GitHubSCMSource ghSCMSource = new GitHubSCMSource(null, apiUri, checkoutCredentialsId, scanCredentialsId, repoOwner, name); ghSCMSource.setExcludes(getExcludes()); ghSCMSource.setIncludes(getIncludes()); + ghSCMSource.setBuildOriginBranch(getBuildOriginBranch()); + ghSCMSource.setBuildOriginBranchWithPR(getBuildOriginBranchWithPR()); + ghSCMSource.setBuildOriginPRMerge(getBuildOriginPRMerge()); + ghSCMSource.setBuildOriginPRHead(getBuildOriginPRHead()); + ghSCMSource.setBuildForkPRMerge(getBuildForkPRMerge()); + ghSCMSource.setBuildForkPRHead(getBuildForkPRHead()); projectObserver.addSource(ghSCMSource); projectObserver.complete(); @@ -232,6 +330,14 @@ private void add(TaskListener listener, SCMSourceObserver observer, GHRepository public static final String defaultIncludes = GitHubSCMSource.DescriptorImpl.defaultIncludes; public static final String defaultExcludes = GitHubSCMSource.DescriptorImpl.defaultExcludes; public static final String SAME = GitHubSCMSource.DescriptorImpl.SAME; + public static final boolean defaultBuildOriginBranch = GitHubSCMSource.DescriptorImpl.defaultBuildOriginBranch; + public static final boolean defaultBuildOriginBranchWithPR = GitHubSCMSource.DescriptorImpl.defaultBuildOriginBranchWithPR; + public static final boolean defaultBuildOriginPRMerge = GitHubSCMSource.DescriptorImpl.defaultBuildOriginPRMerge; + public static final boolean defaultBuildOriginPRHead = GitHubSCMSource.DescriptorImpl.defaultBuildOriginPRHead; + public static final boolean defaultBuildForkPRMerge = GitHubSCMSource.DescriptorImpl.defaultBuildForkPRMerge; + public static final boolean defaultBuildForkPRHead = GitHubSCMSource.DescriptorImpl.defaultBuildForkPRHead; + + @Inject private GitHubSCMSource.DescriptorImpl delegate; @Override public String getDisplayName() { return Messages.GitHubSCMNavigator_DisplayName(); @@ -299,6 +405,43 @@ public ListBoxModel doFillApiUriItems() { } return result; } + + // TODO repeating configuration blocks like this is clumsy; better to factor shared config into a Describable and use f:property + + @Restricted(NoExternalUse.class) + public FormValidation doCheckIncludes(@QueryParameter String value) { + return delegate.doCheckIncludes(value); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckBuildOriginBranchWithPR( + @QueryParameter boolean buildOriginBranch, + @QueryParameter boolean buildOriginBranchWithPR, + @QueryParameter boolean buildOriginPRMerge, + @QueryParameter boolean buildOriginPRHead, + @QueryParameter boolean buildForkPRMerge, + @QueryParameter boolean buildForkPRHead + ) { + return delegate.doCheckBuildOriginBranchWithPR(buildOriginBranch, buildOriginBranchWithPR, buildOriginPRMerge, buildOriginPRHead, buildForkPRMerge, buildForkPRHead); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckBuildOriginPRHead(@QueryParameter boolean buildOriginBranchWithPR, @QueryParameter boolean buildOriginPRMerge, @QueryParameter boolean buildOriginPRHead) { + return delegate.doCheckBuildOriginPRHead(buildOriginBranchWithPR, buildOriginPRMerge, buildOriginPRHead); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckBuildForkPRHead/* web method name controls UI position of message; we want this at the bottom */( + @QueryParameter boolean buildOriginBranch, + @QueryParameter boolean buildOriginBranchWithPR, + @QueryParameter boolean buildOriginPRMerge, + @QueryParameter boolean buildOriginPRHead, + @QueryParameter boolean buildForkPRMerge, + @QueryParameter boolean buildForkPRHead + ) { + return delegate.doCheckBuildForkPRHead(buildOriginBranch, buildOriginBranchWithPR, buildOriginPRMerge, buildOriginPRHead, buildForkPRMerge, buildForkPRHead); + } + } } diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java index e5300934a..c12b33f8e 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource.java @@ -31,6 +31,7 @@ import com.cloudbees.plugins.credentials.common.StandardCredentials; import com.cloudbees.plugins.credentials.common.StandardListBoxModel; import com.cloudbees.plugins.credentials.domains.DomainRequirement; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.AbortException; import hudson.Extension; import hudson.Util; @@ -57,7 +58,6 @@ import org.kohsuke.github.GHMyself; import org.kohsuke.github.GHOrganization; import org.kohsuke.github.GHPullRequest; -import org.kohsuke.github.GHRef; import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHUser; import org.kohsuke.github.GitHub; @@ -84,10 +84,26 @@ import java.util.logging.Logger; import static hudson.model.Items.XSTREAM2; +import hudson.model.Run; +import hudson.plugins.git.GitException; +import hudson.plugins.git.GitSCM; +import hudson.plugins.git.Revision; +import hudson.plugins.git.extensions.GitSCMExtension; +import hudson.plugins.git.extensions.impl.PreBuildMerge; +import hudson.plugins.git.util.MergeRecord; +import hudson.scm.SCM; +import java.util.HashSet; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectId; +import org.jenkinsci.plugins.gitclient.CheckoutCommand; +import org.jenkinsci.plugins.gitclient.GitClient; +import org.jenkinsci.plugins.gitclient.MergeCommand; import static org.jenkinsci.plugins.github.config.GitHubServerConfig.GITHUB_URL; public class GitHubSCMSource extends AbstractGitSCMSource { + private static final Logger LOGGER = Logger.getLogger(GitHubSCMSource.class.getName()); + private final String apiUri; /** Credentials for actual clone; may be SSH private key. */ @@ -104,6 +120,19 @@ public class GitHubSCMSource extends AbstractGitSCMSource { private @Nonnull String excludes = DescriptorImpl.defaultExcludes; + /** Whether to build regular origin branches. */ + private @Nonnull Boolean buildOriginBranch = DescriptorImpl.defaultBuildOriginBranch; + /** Whether to build origin branches which happen to also have a PR filed from them (but here we are naming and building as a branch). */ + private @Nonnull Boolean buildOriginBranchWithPR = DescriptorImpl.defaultBuildOriginBranchWithPR; + /** Whether to build PRs filed from the origin, where the build is of the merge with the base branch. */ + private @Nonnull Boolean buildOriginPRMerge = DescriptorImpl.defaultBuildOriginPRMerge; + /** Whether to build PRs filed from the origin, where the build is of the branch head. */ + private @Nonnull Boolean buildOriginPRHead = DescriptorImpl.defaultBuildOriginPRHead; + /** Whether to build PRs filed from a fork, where the build is of the merge with the base branch. */ + private @Nonnull Boolean buildForkPRMerge = DescriptorImpl.defaultBuildForkPRMerge; + /** Whether to build PRs filed from a fork, where the build is of the branch head. */ + private @Nonnull Boolean buildForkPRHead = DescriptorImpl.defaultBuildForkPRHead; + @DataBoundConstructor public GitHubSCMSource(String id, String apiUri, String checkoutCredentialsId, String scanCredentialsId, String repoOwner, String repository) { super(id); @@ -114,6 +143,30 @@ public GitHubSCMSource(String id, String apiUri, String checkoutCredentialsId, S this.checkoutCredentialsId = checkoutCredentialsId; } + /** Use defaults for old settings. */ + @SuppressFBWarnings(value="RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE", justification="Only non-null after we set them here!") + private Object readResolve() { + if (buildOriginBranch == null) { + buildOriginBranch = DescriptorImpl.defaultBuildOriginBranch; + } + if (buildOriginBranchWithPR == null) { + buildOriginBranchWithPR = DescriptorImpl.defaultBuildOriginBranchWithPR; + } + if (buildOriginPRMerge == null) { + buildOriginPRMerge = DescriptorImpl.defaultBuildOriginPRMerge; + } + if (buildOriginPRHead == null) { + buildOriginPRHead = DescriptorImpl.defaultBuildOriginPRHead; + } + if (buildForkPRMerge == null) { + buildForkPRMerge = DescriptorImpl.defaultBuildForkPRMerge; + } + if (buildForkPRHead == null) { + buildForkPRHead = DescriptorImpl.defaultBuildForkPRHead; + } + return this; + } + @CheckForNull public String getApiUri() { return apiUri; @@ -130,6 +183,7 @@ public String getApiUri() { */ @CheckForNull @Override + @SuppressWarnings("ConvertToStringSwitch") // more cumbersome with null check public String getCredentialsId() { if (DescriptorImpl.ANONYMOUS.equals(checkoutCredentialsId)) { return null; @@ -160,8 +214,9 @@ public String getRepository() { @Override protected List getRefSpecs() { - return new ArrayList(Arrays.asList(new RefSpec("+refs/heads/*:refs/remotes/origin/*"), - new RefSpec("+refs/pull/*/merge:refs/remotes/origin/pr/*"))); + return new ArrayList<>(Arrays.asList(new RefSpec("+refs/heads/*:refs/remotes/origin/*"), + // For PRs we check out the head, then perhaps merge with the base branch. + new RefSpec("+refs/pull/*/head:refs/remotes/origin/pr/*"))); } /** @@ -216,6 +271,60 @@ Collections. emptyList()), CredentialsMatchers.allOf( this.excludes = excludes; } + public boolean getBuildOriginBranch() { + return buildOriginBranch; + } + + @DataBoundSetter + public void setBuildOriginBranch(boolean buildOriginBranch) { + this.buildOriginBranch = buildOriginBranch; + } + + public boolean getBuildOriginBranchWithPR() { + return buildOriginBranchWithPR; + } + + @DataBoundSetter + public void setBuildOriginBranchWithPR(boolean buildOriginBranchWithPR) { + this.buildOriginBranchWithPR = buildOriginBranchWithPR; + } + + public boolean getBuildOriginPRMerge() { + return buildOriginPRMerge; + } + + @DataBoundSetter + public void setBuildOriginPRMerge(boolean buildOriginPRMerge) { + this.buildOriginPRMerge = buildOriginPRMerge; + } + + public boolean getBuildOriginPRHead() { + return buildOriginPRHead; + } + + @DataBoundSetter + public void setBuildOriginPRHead(boolean buildOriginPRHead) { + this.buildOriginPRHead = buildOriginPRHead; + } + + public boolean getBuildForkPRMerge() { + return buildForkPRMerge; + } + + @DataBoundSetter + public void setBuildForkPRMerge(boolean buildForkPRMerge) { + this.buildForkPRMerge = buildForkPRMerge; + } + + public boolean getBuildForkPRHead() { + return buildForkPRHead; + } + + @DataBoundSetter + public void setBuildForkPRHead(boolean buildForkPRHead) { + this.buildForkPRHead = buildForkPRHead; + } + @Override public String getRemote() { return getUriResolver().getRepositoryUri(apiUri, repoOwner, repository); @@ -263,73 +372,142 @@ public String getRemote() { private void doRetrieve(SCMHeadObserver observer, TaskListener listener, GHRepository repo) throws IOException, InterruptedException { SCMSourceCriteria criteria = getCriteria(); - listener.getLogger().format("%n Getting remote branches...%n"); - int branches = 0; - for (Map.Entry entry : repo.getBranches().entrySet()) { - final String branchName = entry.getKey(); - if (isExcluded(branchName)) { - continue; - } - listener.getLogger().format("%n Checking branch %s%n", HyperlinkNote.encodeTo(repo.getHtmlUrl().toString() + "/tree/" + branchName, branchName)); - if (criteria != null) { - SCMSourceCriteria.Probe probe = getProbe(branchName, "branch", "refs/heads/" + branchName, repo, listener); - if (criteria.isHead(probe, listener)) { - listener.getLogger().format(" Met criteria%n"); - } else { - listener.getLogger().format(" Does not meet criteria%n"); + // To implement buildOriginBranch && !buildOriginBranchWithPR we need to first find the pull requests, so we can skip corresponding origin branches later. Awkward. + Set originBranchesWithPR = new HashSet<>(); + + if (buildOriginPRMerge || buildOriginPRHead || buildForkPRMerge || buildForkPRHead) { + listener.getLogger().format("%n Getting remote pull requests...%n"); + int pullrequests = 0; + for (GHPullRequest ghPullRequest : repo.getPullRequests(GHIssueState.OPEN)) { + int number = ghPullRequest.getNumber(); + listener.getLogger().format("%n Checking pull request %s%n", HyperlinkNote.encodeTo(ghPullRequest.getHtmlUrl().toString(), "#" + number)); + boolean fork = !repo.getOwner().equals(ghPullRequest.getHead().getUser()); + if (fork && !buildForkPRMerge && !buildForkPRHead) { + listener.getLogger().format(" Submitted from fork, skipping%n%n"); continue; } - } - SCMHead head = new SCMHead(branchName); - SCMRevision hash = new SCMRevisionImpl(head, entry.getValue().getSHA1()); - observer.observe(head, hash); - if (!observer.isObserving()) { - return; - } - branches++; - } - listener.getLogger().format("%n %d branches were processed%n", branches); - - listener.getLogger().format("%n Getting remote pull requests...%n"); - int pullrequests = 0; - for (GHPullRequest ghPullRequest : repo.getPullRequests(GHIssueState.OPEN)) { - PullRequestSCMHead head = new PullRequestSCMHead(ghPullRequest); - final String branchName = head.getName(); - listener.getLogger().format("%n Checking pull request %s%n", HyperlinkNote.encodeTo(ghPullRequest.getHtmlUrl().toString(), "#" + branchName)); - if (repo.getOwner().equals(ghPullRequest.getHead().getUser())) { - listener.getLogger().format(" Submitted from origin repository, skipping%n%n"); - continue; - } - if (criteria != null) { - SCMSourceCriteria.Probe probe = getProbe(branchName, "pull request", "refs/pull/" + head.getNumber() + "/head", repo, listener); - if (criteria.isHead(probe, listener)) { - // FYI https://developer.github.com/v3/pulls/#response-1 - Boolean mergeable = ghPullRequest.getMergeable(); - if (Boolean.FALSE.equals(mergeable)) { - listener.getLogger().format(" Not mergeable, but it will be included%n"); - } - listener.getLogger().format(" Met criteria%n"); - } else { - listener.getLogger().format(" Does not meet criteria%n"); + if (!fork && !buildOriginPRMerge && !buildOriginPRHead) { + listener.getLogger().format(" Submitted from origin repository, skipping%n%n"); continue; } + if (!fork) { + originBranchesWithPR.add(ghPullRequest.getHead().getRef()); + } + boolean trusted = isTrusted(repo, ghPullRequest); + if (!trusted) { + listener.getLogger().format(" (not from a trusted source)%n"); + } + for (boolean merge : new boolean[] {false, true}) { + String branchName = "PR-" + number; + if (merge && fork) { + if (!buildForkPRMerge) { + continue; // not doing this combination + } + if (buildForkPRHead) { + branchName += "-merge"; // make sure they are distinct + } + // If we only build merged, or only unmerged, then we use the /PR-\d+/ scheme as before. + } + if (merge && !fork) { + if (!buildOriginPRMerge) { + continue; + } + if (buildForkPRHead) { + branchName += "-merge"; + } + } + if (!merge && fork) { + if (!buildForkPRHead) { + continue; + } + if (buildForkPRMerge) { + branchName += "-head"; + } + } + if (!merge && !fork) { + if (!buildOriginPRHead) { + continue; + } + if (buildOriginPRMerge) { + branchName += "-head"; + } + } + listener.getLogger().format(" Job name: %s%n", branchName); + PullRequestSCMHead head = new PullRequestSCMHead(ghPullRequest, branchName, merge, trusted); + if (criteria != null) { + // Would be more precise to check whether the merge of the base branch head with the PR branch head contains a given file, etc., + // but this would be a lot more work, and is unlikely to differ from using refs/pull/123/merge: + SCMSourceCriteria.Probe probe = getProbe(branchName, "pull request", "refs/pull/" + number + (merge ? "/merge" : "/head"), repo, listener); + if (criteria.isHead(probe, listener)) { + // FYI https://developer.github.com/v3/pulls/#response-1 + Boolean mergeable = ghPullRequest.getMergeable(); + if (Boolean.FALSE.equals(mergeable)) { + if (merge) { + listener.getLogger().format(" Not mergeable, build likely to fail%n"); + } else { + listener.getLogger().format(" Not mergeable, but will be built anyway%n"); + } + } + listener.getLogger().format(" Met criteria%n"); + } else { + listener.getLogger().format(" Does not meet criteria%n"); + continue; + } + } + String baseHash; + if (merge) { + baseHash = repo.getRef("heads/" + ghPullRequest.getBase().getRef()).getObject().getSha(); + } else { + baseHash = ghPullRequest.getBase().getSha(); + } + PullRequestSCMRevision rev = new PullRequestSCMRevision(head, baseHash, ghPullRequest.getHead().getSha()); + observer.observe(head, rev); + if (!observer.isObserving()) { + return; + } + } + pullrequests++; } - String trustedBase = trustedReplacement(repo, ghPullRequest); - SCMRevision hash; - if (trustedBase == null) { - hash = new SCMRevisionImpl(head, ghPullRequest.getHead().getSha()); - } else { - listener.getLogger().format(" (not from a trusted source)%n"); - hash = new UntrustedPullRequestSCMRevision(head, ghPullRequest.getHead().getSha(), trustedBase); - } - observer.observe(head, hash); - if (!observer.isObserving()) { - return; - } - pullrequests++; + listener.getLogger().format("%n %d pull requests were processed%n", pullrequests); } - listener.getLogger().format("%n %d pull requests were processed%n", pullrequests); + if (buildOriginBranch || buildOriginBranchWithPR) { + listener.getLogger().format("%n Getting remote branches...%n"); + int branches = 0; + for (Map.Entry entry : repo.getBranches().entrySet()) { + final String branchName = entry.getKey(); + if (isExcluded(branchName)) { + continue; + } + boolean hasPR = originBranchesWithPR.contains(branchName); + if (!hasPR && !buildOriginBranch) { + listener.getLogger().format("%n Skipping branch %s since there is no corresponding PR%n", branchName); + continue; + } + if (hasPR && !buildOriginBranchWithPR) { + listener.getLogger().format("%n Skipping branch %s since there is a corresponding PR%n", branchName); + continue; + } + listener.getLogger().format("%n Checking branch %s%n", HyperlinkNote.encodeTo(repo.getHtmlUrl().toString() + "/tree/" + branchName, branchName)); + if (criteria != null) { + SCMSourceCriteria.Probe probe = getProbe(branchName, "branch", "refs/heads/" + branchName, repo, listener); + if (criteria.isHead(probe, listener)) { + listener.getLogger().format(" Met criteria%n"); + } else { + listener.getLogger().format(" Does not meet criteria%n"); + continue; + } + } + SCMHead head = new SCMHead(branchName); + SCMRevision hash = new SCMRevisionImpl(head, entry.getValue().getSHA1()); + observer.observe(head, hash); + if (!observer.isObserving()) { + return; + } + branches++; + } + listener.getLogger().format("%n %d branches were processed%n", branches); + } } /** @@ -346,6 +524,7 @@ private void doRetrieve(SCMHeadObserver observer, TaskListener listener, GHRepos protected SCMSourceCriteria.Probe getProbe(final String branch, final String thing, final String ref, final GHRepository repo, final TaskListener listener) { return new SCMSourceCriteria.Probe() { + private static final long serialVersionUID = 5012552654534124387L; @Override public String name() { return branch; } @@ -411,28 +590,113 @@ protected SCMRevision retrieve(SCMHead head, TaskListener listener) throws IOExc } protected SCMRevision doRetrieve(SCMHead head, TaskListener listener, GHRepository repo) throws IOException, InterruptedException { - GHRef ref; if (head instanceof PullRequestSCMHead) { - int number = ((PullRequestSCMHead) head).getNumber(); - ref = repo.getRef("pull/" + number + "/merge"); - // getPullRequests makes an extra API call, but we need its current .base.sha - String trustedBase = trustedReplacement(repo, repo.getPullRequest(number)); - if (trustedBase != null) { - return new UntrustedPullRequestSCMRevision(head, ref.getObject().getSha(), trustedBase); + PullRequestSCMHead prhead = (PullRequestSCMHead) head; + int number = prhead.getNumber(); + GHPullRequest pr = repo.getPullRequest(number); + String baseHash; + if (prhead.isMerge()) { + PullRequestAction metadata = prhead.getAction(PullRequestAction.class); + if (metadata == null) { + throw new IOException("Cannot find base branch metadata from " + prhead); + } + baseHash = repo.getRef("heads/" + metadata.getTarget().getName()).getObject().getSha(); + } else { + baseHash = pr.getBase().getSha(); } + return new PullRequestSCMRevision((PullRequestSCMHead) head, baseHash, pr.getHead().getSha()); } else { - ref = repo.getRef("heads/" + head.getName()); + return new SCMRevisionImpl(head, repo.getRef("heads/" + head.getName()).getObject().getSha()); + } + } + + @Override + public SCM build(SCMHead head, SCMRevision revision) { + if (revision == null) { + // TODO will this work sanely for PRs? Branch.scm seems to be used only as a fallback for SCMBinder/SCMVar where they would perhaps better just report an error. + return super.build(head, null); + } else if (head instanceof PullRequestSCMHead) { + if (!(revision instanceof PullRequestSCMRevision)) { + // TODO this seems to happen for PR-6-unmerged in cloudbeers/PR-demo; why? + LOGGER.log(Level.WARNING, "Unexpected revision class {0} for {1}", new Object[] {revision.getClass().getName(), head}); + return super.build(head, revision); + } + PullRequestSCMRevision prRev = (PullRequestSCMRevision) revision; + GitSCM scm = (GitSCM) super.build(/* does this matter? */head, new SCMRevisionImpl(/* again probably irrelevant */head, prRev.getPullHash())); + if (((PullRequestSCMHead) head).isMerge()) { + PullRequestAction action = head.getAction(PullRequestAction.class); + String baseName; + if (action != null) { + baseName = action.getTarget().getName(); + } else { + baseName = "master?"; // OK if not found, just informational anyway + } + scm.getExtensions().add(new MergeWith(baseName, prRev.getBaseHash())); + } + return scm; + } else { + return super.build(head, /* casting just as an assertion */(SCMRevisionImpl) revision); + } + } + /** + * Similar to {@link PreBuildMerge}, but we cannot use that unmodified: we need to specify the exact base branch hash. + * It is possible to just ask Git to check out {@code refs/pull/123/merge}, but this has two problems: + * GitHub’s implementation is not all that reliable (for example JENKINS-33237, and time-delayed snapshots); + * and it is subject to a race condition between the {@code baseHash} we think we are merging with and a possibly newer one that was just pushed. + * Performing the merge ourselves is simple enough and ensures that we are building exactly what the {@link PullRequestSCMRevision} represented. + */ + private static class MergeWith extends GitSCMExtension { + private final String baseName; + private final String baseHash; + MergeWith(String baseName, String baseHash) { + this.baseName = baseName; + this.baseHash = baseHash; + } + @Override + public Revision decorateRevisionToBuild(GitSCM scm, Run build, GitClient git, TaskListener listener, Revision marked, Revision rev) throws IOException, InterruptedException, GitException { + listener.getLogger().println("Merging " + baseName + " commit " + baseHash + " into PR head commit " + rev.getSha1String()); + checkout(scm, build, git, listener, rev); + try { + git.setAuthor("Jenkins", /* could parse out of JenkinsLocationConfiguration.get().getAdminAddress() but seems overkill */"nobody@nowhere"); + git.setCommitter("Jenkins", "nobody@nowhere"); + MergeCommand cmd = git.merge().setRevisionToMerge(ObjectId.fromString(baseHash)); + for (GitSCMExtension ext : scm.getExtensions()) { + // By default we do a regular merge, allowing it to fast-forward. + ext.decorateMergeCommand(scm, build, git, listener, cmd); + } + cmd.execute(); + } catch (GitException x) { + // Try to revert merge conflict markers. + // TODO IGitAPI offers a reset(hard) method yet GitClient does not. Why? + checkout(scm, build, git, listener, rev); + // TODO would be nicer to throw an AbortException with just the message, but this is actually worse pending https://github.com/jenkinsci/git-client-plugin/pull/208 + throw x; + } + build.addAction(new MergeRecord(baseName, baseHash)); // does not seem to be used, but just in case + ObjectId mergeRev = git.revParse(Constants.HEAD); + listener.getLogger().println("Merge succeeded, producing " + mergeRev.name()); + return new Revision(mergeRev); // note that this ensures Build.revision != Build.marked + } + private void checkout(GitSCM scm, Run build, GitClient git, TaskListener listener, Revision rev) throws InterruptedException, IOException, GitException { + CheckoutCommand checkoutCommand = git.checkout().ref(rev.getSha1String()); + for (GitSCMExtension ext : scm.getExtensions()) { + ext.decorateCheckoutCommand(scm, build, git, listener, checkoutCommand); + } + checkoutCommand.execute(); } - return new SCMRevisionImpl(head, ref.getObject().getSha()); } @Override public SCMRevision getTrustedRevision(SCMRevision revision, TaskListener listener) throws IOException, InterruptedException { - if (revision instanceof UntrustedPullRequestSCMRevision) { - PullRequestSCMHead head = (PullRequestSCMHead) revision.getHead(); - UntrustedPullRequestSCMRevision rev = (UntrustedPullRequestSCMRevision) revision; - listener.getLogger().println("Loading trusted files from target branch at " + rev.baseHash + " rather than " + rev.getHash()); - return new SCMRevisionImpl(head, rev.baseHash); + if (revision instanceof PullRequestSCMRevision && !((PullRequestSCMHead) revision.getHead()).isTrusted()) { + PullRequestAction metadata = revision.getHead().getAction(PullRequestAction.class); + if (metadata != null) { + PullRequestSCMRevision rev = (PullRequestSCMRevision) revision; + listener.getLogger().println("Loading trusted files from base branch " + metadata.getTarget().getName() + " at " + rev.getBaseHash() + " rather than " + rev.getPullHash()); + return new SCMRevisionImpl(metadata.getTarget(), rev.getBaseHash()); + } else { + throw new IOException("Cannot find base branch metadata from " + revision.getHead()); + } } return revision; } @@ -447,26 +711,27 @@ public SCMRevision getTrustedRevision(SCMRevision revision, TaskListener listene * TODO since the GitHub API wrapper currently supports neither, we list all collaborator names and check for membership, * paying the performance penalty without the benefit of the accuracy. * @param ghPullRequest a PR - * @return the base revision, for an untrusted PR; null for a trusted PR + * @return true if this is a trusted PR * @see PR metadata * @see base revision oddity */ - private @CheckForNull String trustedReplacement(@Nonnull GHRepository repo, @Nonnull GHPullRequest ghPullRequest) throws IOException { - if (repo.getCollaboratorNames().contains(ghPullRequest.getUser().getLogin())) { - return null; - } else { - return ghPullRequest.getBase().getSha(); - } + private boolean isTrusted(@Nonnull GHRepository repo, @Nonnull GHPullRequest ghPullRequest) throws IOException { + return repo.getCollaboratorNames().contains(ghPullRequest.getUser().getLogin()); } @Extension public static class DescriptorImpl extends SCMSourceDescriptor { - private static final Logger LOGGER = Logger.getLogger(DescriptorImpl.class.getName()); - public static final String defaultIncludes = "*"; public static final String defaultExcludes = ""; public static final String ANONYMOUS = "ANONYMOUS"; public static final String SAME = "SAME"; + // Prior to JENKINS-33161 the unconditional behavior was to build fork PRs plus origin branches, and try to build a merge revision for PRs. + public static final boolean defaultBuildOriginBranch = true; + public static final boolean defaultBuildOriginBranchWithPR = true; + public static final boolean defaultBuildOriginPRMerge = false; + public static final boolean defaultBuildOriginPRHead = false; + public static final boolean defaultBuildForkPRMerge = true; + public static final boolean defaultBuildForkPRHead = false; @Initializer(before = InitMilestone.PLUGINS_STARTED) public static void addAliases() { @@ -503,7 +768,7 @@ public FormValidation doCheckScanCredentialsId(@AncestorInPath SCMSourceOwner co } } catch (IOException e) { // ignore, never thrown - LOGGER.log(Level.WARNING, "Exception validating credentials " + CredentialsNameProvider.name(credentials) + " on " + apiUri); + LOGGER.log(Level.WARNING, "Exception validating credentials {0} on {1}", new Object[] {CredentialsNameProvider.name(credentials), apiUri}); return FormValidation.error("Exception validating credentials"); } } @@ -512,6 +777,51 @@ public FormValidation doCheckScanCredentialsId(@AncestorInPath SCMSourceOwner co } } + @Restricted(NoExternalUse.class) + public FormValidation doCheckBuildOriginBranchWithPR( + @QueryParameter boolean buildOriginBranch, + @QueryParameter boolean buildOriginBranchWithPR, + @QueryParameter boolean buildOriginPRMerge, + @QueryParameter boolean buildOriginPRHead, + @QueryParameter boolean buildForkPRMerge, + @QueryParameter boolean buildForkPRHead + ) { + if (buildOriginBranch && !buildOriginBranchWithPR && !buildOriginPRMerge && !buildOriginPRHead && !buildForkPRMerge && !buildForkPRHead) { + // TODO in principle we could make doRetrieve populate originBranchesWithPR without actually including any PRs, but it would be more work and probably never wanted anyway. + return FormValidation.warning("If you are not building any PRs, all origin branches will be built."); + } + return FormValidation.ok(); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckBuildOriginPRHead(@QueryParameter boolean buildOriginBranchWithPR, @QueryParameter boolean buildOriginPRMerge, @QueryParameter boolean buildOriginPRHead) { + if (buildOriginBranchWithPR && buildOriginPRHead) { + return FormValidation.warning("Redundant to build an origin PR both as a branch and as an unmerged PR."); + } + if (buildOriginPRMerge && buildOriginPRHead) { + return FormValidation.ok("Merged vs. unmerged PRs will be distinguished in the job name (*-merge vs. *-head)."); + } + return FormValidation.ok(); + } + + @Restricted(NoExternalUse.class) + public FormValidation doCheckBuildForkPRHead/* web method name controls UI position of message; we want this at the bottom */( + @QueryParameter boolean buildOriginBranch, + @QueryParameter boolean buildOriginBranchWithPR, + @QueryParameter boolean buildOriginPRMerge, + @QueryParameter boolean buildOriginPRHead, + @QueryParameter boolean buildForkPRMerge, + @QueryParameter boolean buildForkPRHead + ) { + if (!buildOriginBranch && !buildOriginBranchWithPR && !buildOriginPRMerge && !buildOriginPRHead && !buildForkPRMerge && !buildForkPRHead) { + return FormValidation.warning("You need to build something!"); + } + if (buildForkPRMerge && buildForkPRHead) { + return FormValidation.ok("Merged vs. unmerged PRs will be distinguished in the job name (*-merge vs. *-head)."); + } + return FormValidation.ok(); + } + public ListBoxModel doFillApiUriItems() { ListBoxModel result = new ListBoxModel(); result.add("GitHub", ""); @@ -555,8 +865,8 @@ public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context } catch (IllegalStateException e) { LOGGER.log(Level.WARNING, e.getMessage()); } catch (IOException e) { - LOGGER.log(Level.WARNING, "Exception retrieving the repositories of the owner " + repoOwner + - " on " + apiUri + " with credentials " + CredentialsNameProvider.name(credentials)); + LOGGER.log(Level.WARNING, "Exception retrieving the repositories of the owner {0} on {1} with credentials {2}", + new Object[] {repoOwner, apiUri, CredentialsNameProvider.name(credentials)}); } if (myself != null && repoOwner.equalsIgnoreCase(myself.getLogin())) { for (String name : myself.getAllRepositories().keySet()) { @@ -570,7 +880,7 @@ public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context try { org = github.getOrganization(repoOwner); } catch (FileNotFoundException fnf) { - LOGGER.log(Level.FINE, "There is not any GH Organization named " + repoOwner); + LOGGER.log(Level.FINE, "There is not any GH Organization named {0}", repoOwner); } catch (IOException e) { LOGGER.log(Level.WARNING, e.getMessage()); } @@ -585,7 +895,7 @@ public ListBoxModel doFillRepositoryItems(@AncestorInPath SCMSourceOwner context try { user = github.getUser(repoOwner); } catch (FileNotFoundException fnf) { - LOGGER.log(Level.FINE, "There is not any GH User named " + repoOwner); + LOGGER.log(Level.FINE, "There is not any GH User named {0}", repoOwner); } catch (IOException e) { LOGGER.log(Level.WARNING, e.getMessage()); } diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/PullRequestSCMHead.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/PullRequestSCMHead.java index 0950e1248..ba1c860b9 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/PullRequestSCMHead.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/PullRequestSCMHead.java @@ -32,27 +32,54 @@ /** * Head corresponding to a pull request. - * Named like {@code PR-123}. + * Named like {@code PR-123} or {@code PR-123-merged} or {@code PR-123-unmerged}. */ public final class PullRequestSCMHead extends SCMHead { - private static final String PR_BRANCH_PREFIX = "PR-"; - private static final long serialVersionUID = 1; private final PullRequestAction metadata; + private Boolean merge; + private final boolean trusted; - PullRequestSCMHead(GHPullRequest pr) { - super(PR_BRANCH_PREFIX + pr.getNumber()); + PullRequestSCMHead(GHPullRequest pr, String name, boolean merge, boolean trusted) { + super(name); metadata = new PullRequestAction(pr); + this.merge = merge; + this.trusted = trusted; } public int getNumber() { if (metadata != null) { return Integer.parseInt(metadata.getId()); } else { // settings compatibility - return Integer.parseInt(getName().substring(PR_BRANCH_PREFIX.length())); + // if predating PullRequestAction, then also predate -merged/-unmerged suffices + return Integer.parseInt(getName().substring("PR-".length())); + } + } + + /** Default for old settings. */ + private Object readResolve() { + if (merge == null) { + merge = true; } + // leave trusted at false to be on the safe side + return this; + } + + /** + * Whether we intend to build the merge of the PR head with the base branch. + * + */ + public boolean isMerge() { + return merge; + } + + /** + * Whether this PR was observed to have come from a trusted author. + */ + public boolean isTrusted() { + return trusted; } @Override diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/PullRequestSCMRevision.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/PullRequestSCMRevision.java new file mode 100644 index 000000000..5c88872d2 --- /dev/null +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/PullRequestSCMRevision.java @@ -0,0 +1,82 @@ +/* + * The MIT License + * + * Copyright 2016 CloudBees, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.jenkinsci.plugins.github_branch_source; + +import edu.umd.cs.findbugs.annotations.NonNull; +import jenkins.scm.api.SCMHead; +import jenkins.scm.api.SCMRevision; + +/** + * Revision of a pull request. + */ +class PullRequestSCMRevision extends SCMRevision { + + private static final long serialVersionUID = 1L; + + private final @NonNull String baseHash; + private final @NonNull String pullHash; + + PullRequestSCMRevision(@NonNull PullRequestSCMHead head, @NonNull String baseHash, @NonNull String pullHash) { + super(head); + this.baseHash = baseHash; + this.pullHash = pullHash; + } + + /** + * The commit hash of the base branch we are tracking. + * If {@link PullRequestSCMHead#isMerge}, this would be the current head of the base branch. + * Otherwise it would be the PR’s {@code .base.sha}, the common ancestor of the PR branch and the base branch. + */ + public @NonNull String getBaseHash() { + return baseHash; + } + + /** + * The commit hash of the head of the pull request branch. + */ + public @NonNull String getPullHash() { + return pullHash; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof PullRequestSCMRevision)) { + return false; + } + PullRequestSCMRevision other = (PullRequestSCMRevision) o; + return getHead().equals(other.getHead()) && baseHash.equals(other.baseHash) && pullHash.equals(other.pullHash); + } + + @Override + public int hashCode() { + return pullHash.hashCode(); + } + + @Override + public String toString() { + return getHead() instanceof PullRequestSCMHead && ((PullRequestSCMHead) getHead()).isMerge() ? pullHash + "+" + baseHash : pullHash; + } + +} diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/UntrustedPullRequestSCMRevision.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/UntrustedPullRequestSCMRevision.java index ebb4d821c..40da45bc3 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/UntrustedPullRequestSCMRevision.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/UntrustedPullRequestSCMRevision.java @@ -27,14 +27,14 @@ import jenkins.plugins.git.AbstractGitSCMSource; import jenkins.scm.api.SCMHead; -/** - * Revision of a pull request which should load sensitive files from the base branch. - */ +@Deprecated class UntrustedPullRequestSCMRevision extends AbstractGitSCMSource.SCMRevisionImpl { + private static final long serialVersionUID = -6961458604178249880L; + final String baseHash; - UntrustedPullRequestSCMRevision(SCMHead head, String hash, String baseHash) { + private UntrustedPullRequestSCMRevision(SCMHead head, String hash, String baseHash) { super(head, hash); this.baseHash = baseHash; } @@ -49,4 +49,8 @@ public int hashCode() { return super.hashCode(); // good enough } + private Object readResolve() { + return new PullRequestSCMRevision((PullRequestSCMHead) getHead(), baseHash, getHash()); + } + } diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/config.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/config.jelly index d1675d9ed..11a42b6be 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/config.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/config.jelly @@ -23,5 +23,23 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/config.properties b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/config.properties new file mode 100644 index 000000000..1210f2546 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/config.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2016 CloudBees, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +buildOriginBranch.title=Build origin branches +buildOriginBranchWithPR.title=Build origin branches also filed as PRs +buildOriginPRMerge.title=Build origin PRs (merged with base branch) +buildOriginPRHead.title=Build origin PRs (unmerged head) +buildForkPRMerge.title=Build fork PRs (merged with base branch) +buildForkPRHead.title=Build fork PRs (unmerged head) diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildForkPRHead.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildForkPRHead.html new file mode 100644 index 000000000..e7d2acd52 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildForkPRHead.html @@ -0,0 +1,4 @@ +
+ Whether to build pull requests filed from forks of the main repository. + The job will be named according to the PR and builds will use the head of the pull request, ignoring subsequent changes to the base branch. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildForkPRMerge.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildForkPRMerge.html new file mode 100644 index 000000000..f459fbab2 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildForkPRMerge.html @@ -0,0 +1,4 @@ +
+ Whether to build pull requests filed from forks of the main repository. + The job will be named according to the PR and builds will attempt to merge with the base branch. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginBranch.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginBranch.html new file mode 100644 index 000000000..4a151a7ff --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginBranch.html @@ -0,0 +1,4 @@ +
+ Whether to build branches defined in the origin (primary) repository, not associated with any pull request. + The job name will match the branch name. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginBranchWithPR.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginBranchWithPR.html new file mode 100644 index 000000000..7e8c21a89 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginBranchWithPR.html @@ -0,0 +1,4 @@ +
+ Whether to build branches defined in the origin (primary) repository for which pull requests happen to have been filed. + The job name will match the branch name, not the pull request(s). +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginPRHead.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginPRHead.html new file mode 100644 index 000000000..8307c24b1 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginPRHead.html @@ -0,0 +1,5 @@ +
+ Whether to build pull requests filed from branches in the origin repository. + The job will be named according to the PR and builds will use the head of the pull request, ignoring subsequent changes to the base branch. + Other than naming, the behavior is similar to Build origin branches also filed as PRs. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginPRMerge.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginPRMerge.html new file mode 100644 index 000000000..03564dd39 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator/help-buildOriginPRMerge.html @@ -0,0 +1,4 @@ +
+ Whether to build pull requests filed from branches in the origin repository. + The job will be named according to the PR and builds will attempt to merge with the base branch. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.jelly b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.jelly index 7f7b0209a..1cd9a08e5 100644 --- a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.jelly +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.jelly @@ -22,5 +22,23 @@ + + + + + + + + + + + + + + + + + + - \ No newline at end of file + diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.properties b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.properties new file mode 100644 index 000000000..1210f2546 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/config-detail.properties @@ -0,0 +1,28 @@ +# The MIT License +# +# Copyright 2016 CloudBees, Inc. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +buildOriginBranch.title=Build origin branches +buildOriginBranchWithPR.title=Build origin branches also filed as PRs +buildOriginPRMerge.title=Build origin PRs (merged with base branch) +buildOriginPRHead.title=Build origin PRs (unmerged head) +buildForkPRMerge.title=Build fork PRs (merged with base branch) +buildForkPRHead.title=Build fork PRs (unmerged head) diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildForkPRHead.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildForkPRHead.html new file mode 100644 index 000000000..e7d2acd52 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildForkPRHead.html @@ -0,0 +1,4 @@ +
+ Whether to build pull requests filed from forks of the main repository. + The job will be named according to the PR and builds will use the head of the pull request, ignoring subsequent changes to the base branch. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildForkPRMerge.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildForkPRMerge.html new file mode 100644 index 000000000..f459fbab2 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildForkPRMerge.html @@ -0,0 +1,4 @@ +
+ Whether to build pull requests filed from forks of the main repository. + The job will be named according to the PR and builds will attempt to merge with the base branch. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginBranch.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginBranch.html new file mode 100644 index 000000000..4a151a7ff --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginBranch.html @@ -0,0 +1,4 @@ +
+ Whether to build branches defined in the origin (primary) repository, not associated with any pull request. + The job name will match the branch name. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginBranchWithPR.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginBranchWithPR.html new file mode 100644 index 000000000..7e8c21a89 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginBranchWithPR.html @@ -0,0 +1,4 @@ +
+ Whether to build branches defined in the origin (primary) repository for which pull requests happen to have been filed. + The job name will match the branch name, not the pull request(s). +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginPRHead.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginPRHead.html new file mode 100644 index 000000000..8307c24b1 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginPRHead.html @@ -0,0 +1,5 @@ +
+ Whether to build pull requests filed from branches in the origin repository. + The job will be named according to the PR and builds will use the head of the pull request, ignoring subsequent changes to the base branch. + Other than naming, the behavior is similar to Build origin branches also filed as PRs. +
diff --git a/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginPRMerge.html b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginPRMerge.html new file mode 100644 index 000000000..03564dd39 --- /dev/null +++ b/src/main/resources/org/jenkinsci/plugins/github_branch_source/GitHubSCMSource/help-buildOriginPRMerge.html @@ -0,0 +1,4 @@ +
+ Whether to build pull requests filed from branches in the origin repository. + The job will be named according to the PR and builds will attempt to merge with the base branch. +