/*
* The MIT License
*
* Copyright (c) 2004-2011, Oracle Corporation, Andrew Bayer, Anton Kozak, Nikita Levyankov
*
* 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 hudson.plugins.git;
import hudson.EnvVars;
import hudson.Extension;
import hudson.FilePath;
import hudson.FilePath.FileCallable;
import hudson.Launcher;
import hudson.matrix.MatrixAggregatable;
import hudson.matrix.MatrixAggregator;
import hudson.matrix.MatrixBuild;
import hudson.matrix.MatrixRun;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Result;
import hudson.plugins.git.opt.PreBuildMergeOptions;
import hudson.plugins.git.util.GitConstants;
import hudson.remoting.VirtualChannel;
import hudson.scm.SCM;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildStepMonitor;
import hudson.tasks.Publisher;
import hudson.tasks.Recorder;
import hudson.util.FormValidation;
import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.transport.RemoteConfig;
import org.kohsuke.stapler.AncestorInPath;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.QueryParameter;
public class GitPublisher extends Recorder implements Serializable, MatrixAggregatable {
private static final long serialVersionUID = 1L;
/**
* Store a config version so we're able to migrate config on various
* functionality upgrades.
*/
private Long configVersion;
private boolean pushMerge;
private boolean pushOnlyIfSuccess;
private List<TagToPush> tagsToPush;
// Pushes HEAD to these locations
private List<BranchToPush> branchesToPush;
@DataBoundConstructor
public GitPublisher(List<TagToPush> tagsToPush,
List<BranchToPush> branchesToPush,
boolean pushOnlyIfSuccess,
boolean pushMerge) {
this.tagsToPush = tagsToPush;
this.branchesToPush = branchesToPush;
this.pushMerge = pushMerge;
this.pushOnlyIfSuccess = pushOnlyIfSuccess;
this.configVersion = 2L;
}
public boolean isPushOnlyIfSuccess() {
return pushOnlyIfSuccess;
}
public boolean isPushMerge() {
return pushMerge;
}
public boolean isPushTags() {
if (tagsToPush == null) {
return false;
}
return !tagsToPush.isEmpty();
}
public boolean isPushBranches() {
if (branchesToPush == null) {
return false;
}
return !branchesToPush.isEmpty();
}
public List<TagToPush> getTagsToPush() {
if (tagsToPush == null) {
tagsToPush = new ArrayList<TagToPush>();
}
return tagsToPush;
}
public List<BranchToPush> getBranchesToPush() {
if (branchesToPush == null) {
branchesToPush = new ArrayList<BranchToPush>();
}
return branchesToPush;
}
public BuildStepMonitor getRequiredMonitorService() {
return BuildStepMonitor.BUILD;
}
/**
* For a matrix project, push should only happen once.
*/
public MatrixAggregator createAggregator(MatrixBuild build, Launcher launcher, BuildListener listener) {
return new MatrixAggregator(build, launcher, listener) {
@Override
public boolean endBuild() throws InterruptedException, IOException {
return GitPublisher.this.perform(build, launcher, listener);
}
};
}
@Override
public boolean perform(AbstractBuild<?, ?> build,
Launcher launcher, final BuildListener listener)
throws InterruptedException {
// during matrix build, the push back would happen at the very end only once for the whole matrix,
// not for individual configuration build.
if (build instanceof MatrixRun) {
return true;
}
SCM scm = build.getProject().getScm();
if (!(scm instanceof GitSCM)) {
return false;
}
final GitSCM gitSCM = (GitSCM) scm;
final String projectName = build.getProject().getName();
final FilePath workspacePath = build.getWorkspace();
final int buildNumber = build.getNumber();
final Result buildResult = build.getResult();
// If pushOnlyIfSuccess is selected and the build is not a success, don't push.
if (pushOnlyIfSuccess && buildResult.isWorseThan(Result.SUCCESS)) {
listener.getLogger()
.println(
"Build did not succeed and the project is configured to only push after a successful build, so no pushing will occur.");
return true;
} else {
final String gitExe = gitSCM.getGitExe(build.getBuiltOn(), listener);
EnvVars tempEnvironment;
try {
tempEnvironment = build.getEnvironment(listener);
} catch (IOException e) {
listener.error("IOException publishing in git plugin");
tempEnvironment = new EnvVars();
}
String confName = gitSCM.getGitConfigNameToUse();
if (StringUtils.isNotBlank(confName)) {
tempEnvironment.put(GitConstants.GIT_COMMITTER_NAME_ENV_VAR, confName);
tempEnvironment.put(GitConstants.GIT_AUTHOR_NAME_ENV_VAR, confName);
}
String confEmail = gitSCM.getGitConfigEmailToUse();
if (StringUtils.isNotBlank(confEmail)) {
tempEnvironment.put(GitConstants.GIT_COMMITTER_EMAIL_ENV_VAR, confEmail);
tempEnvironment.put(GitConstants.GIT_AUTHOR_EMAIL_ENV_VAR, confEmail);
}
final EnvVars environment = tempEnvironment;
final FilePath workingDirectory = gitSCM.workingDirectory(workspacePath);
boolean pushResult = true;
// If we're pushing the merge back...
if (pushMerge) {
boolean mergeResult;
try {
mergeResult = workingDirectory.act(new FileCallable<Boolean>() {
private static final long serialVersionUID = 1L;
public Boolean invoke(File workspace,
VirtualChannel channel) throws IOException {
IGitAPI git = new GitAPI(
gitExe, new FilePath(workspace),
listener, environment);
// We delete the old tag generated by the SCM plugin
String tagName = new StringBuilder()
.append(GitConstants.INTERNAL_TAG_NAME_PREFIX)
.append(GitConstants.HYPHEN_SYMBOL)
.append(projectName)
.append(GitConstants.HYPHEN_SYMBOL)
.append(buildNumber)
.toString();
git.deleteTag(tagName);
// And add the success / fail state into the tag.
tagName += "-" + buildResult.toString();
git.tag(tagName, GitConstants.INTERNAL_TAG_COMMENT_PREFIX + buildNumber);
PreBuildMergeOptions mergeOptions = gitSCM.getMergeOptions();
if (mergeOptions.doMerge() && buildResult.isBetterOrEqualTo(
Result.SUCCESS)) {
RemoteConfig remote = mergeOptions.getMergeRemote();
listener.getLogger().println(new StringBuilder().append("Pushing result ")
.append(tagName)
.append(" to ")
.append(mergeOptions.getMergeTarget())
.append(" branch of ")
.append(remote.getName())
.append(" repository")
.toString());
git.push(remote, "HEAD:" + mergeOptions.getMergeTarget());
// } else {
//listener.getLogger().println("Pushing result " + buildnumber + " to origin repository");
//git.push(null);
}
return true;
}
});
} catch (Throwable e) {
listener.error("Failed to push merge to origin repository: " + e.getMessage());
build.setResult(Result.FAILURE);
mergeResult = false;
}
if (!mergeResult) {
pushResult = false;
}
}
if (isPushTags()) {
boolean allTagsResult = true;
for (final TagToPush t : tagsToPush) {
boolean tagResult = true;
if (t.getTagName() == null) {
listener.getLogger().println("No tag to push defined");
tagResult = false;
}
if (t.getTargetRepoName() == null) {
listener.getLogger().println("No target repo to push to defined");
tagResult = false;
}
if (tagResult) {
final String tagName = environment.expand(t.getTagName());
final String targetRepo = environment.expand(t.getTargetRepoName());
try {
tagResult = workingDirectory.act(new FileCallable<Boolean>() {
private static final long serialVersionUID = 1L;
public Boolean invoke(File workspace,
VirtualChannel channel) throws IOException {
IGitAPI git = new GitAPI(gitExe, new FilePath(workspace),
listener, environment);
RemoteConfig remote = gitSCM.getRepositoryByName(targetRepo);
if (remote == null) {
listener.getLogger()
.println("No repository found for target repo name " + targetRepo);
return false;
}
if (t.isCreateTag()) {
if (git.tagExists(tagName)) {
listener.getLogger()
.println("Tag " + tagName
+ " already exists and Create Tag is specified, so failing.");
return false;
}
git.tag(tagName, "Hudson Git plugin tagging with " + tagName);
} else if (!git.tagExists(tagName)) {
listener.getLogger()
.println("Tag " + tagName
+ " does not exist and Create Tag is not specified, so failing.");
return false;
}
listener.getLogger().println("Pushing tag " + tagName + " to repo "
+ targetRepo);
git.push(remote, tagName);
return true;
}
});
} catch (Throwable e) {
listener.error("Failed to push tag " + tagName + " to " + targetRepo
+ ": " + e.getMessage());
build.setResult(Result.FAILURE);
tagResult = false;
}
}
if (!tagResult) {
allTagsResult = false;
}
}
if (!allTagsResult) {
pushResult = false;
}
}
if (isPushBranches()) {
boolean allBranchesResult = true;
for (final BranchToPush b : branchesToPush) {
boolean branchResult = true;
if (b.getBranchName() == null) {
listener.getLogger().println("No branch to push defined");
return false;
}
if (b.getTargetRepoName() == null) {
listener.getLogger().println("No branch repo to push to defined");
return false;
}
final String branchName = environment.expand(b.getBranchName());
final String targetRepo = environment.expand(b.getTargetRepoName());
if (branchResult) {
try {
branchResult = workingDirectory.act(new FileCallable<Boolean>() {
private static final long serialVersionUID = 1L;
public Boolean invoke(File workspace,
VirtualChannel channel) throws IOException {
IGitAPI git = new GitAPI(gitExe, new FilePath(workspace),
listener, environment);
RemoteConfig remote = gitSCM.getRepositoryByName(targetRepo);
if (remote == null) {
listener.getLogger()
.println("No repository found for target repo name " + targetRepo);
return false;
}
listener.getLogger().println("Pushing HEAD to branch " + branchName + " at repo "
+ targetRepo);
git.push(remote, "HEAD:" + branchName);
return true;
}
});
} catch (Throwable e) {
listener.error("Failed to push branch " + branchName + " to "
+ targetRepo + ": " + e.getMessage());
build.setResult(Result.FAILURE);
branchResult = false;
}
}
if (!branchResult) {
allBranchesResult = false;
}
}
if (!allBranchesResult) {
pushResult = false;
}
}
return pushResult;
}
}
/**
* Handles migration from earlier version - if we were pushing merges, we'll be
* instantiated but tagsToPush will be null rather than empty.
*
* @return This.
*/
private Object readResolve() {
// Default unspecified to v0
if (configVersion == null) {
this.configVersion = 0L;
}
if (this.configVersion < 1L && tagsToPush == null) {
this.pushMerge = true;
}
return this;
}
@Extension(ordinal = -1)
public static class DescriptorImpl extends BuildStepDescriptor<Publisher> {
public String getDisplayName() {
return "Git Publisher";
}
@Override
public String getHelpFile() {
return "/plugin/git/gitPublisher.html";
}
/**
* Performs on-the-fly validation on the file mask wildcard.
* <p/>
* I don't think this actually ever gets called, but I'm modernizing it anyway.
*/
public FormValidation doCheck(@AncestorInPath AbstractProject project, @QueryParameter String value)
throws IOException {
return FilePath.validateFileMask(project.getSomeWorkspace(), value);
}
public FormValidation doCheckTagName(@QueryParameter String value) {
return checkFieldNotEmpty(value, "Tag Name");
}
public FormValidation doCheckBranchName(@QueryParameter String value) {
return checkFieldNotEmpty(value, "Branch Name");
}
public boolean isApplicable(Class<? extends AbstractProject> jobType) {
return true;
}
private FormValidation checkFieldNotEmpty(String value, String field) {
value = StringUtils.strip(value);
if (StringUtils.isBlank(value)) {
return FormValidation.error(field + " is required.");
}
return FormValidation.ok();
}
}
public static abstract class PushConfig implements Serializable {
private static final long serialVersionUID = 1L;
private String targetRepoName;
public PushConfig(String targetRepoName) {
this.targetRepoName = targetRepoName;
}
public String getTargetRepoName() {
return targetRepoName;
}
public void setTargetRepoName() {
this.targetRepoName = targetRepoName;
}
}
public static final class BranchToPush extends PushConfig {
private String branchName;
public String getBranchName() {
return branchName;
}
@DataBoundConstructor
public BranchToPush(String targetRepoName, String branchName) {
super(targetRepoName);
this.branchName = branchName;
}
}
public static final class TagToPush extends PushConfig {
private String tagName;
private boolean createTag;
public String getTagName() {
return tagName;
}
public boolean isCreateTag() {
return createTag;
}
@DataBoundConstructor
public TagToPush(String targetRepoName, String tagName, boolean createTag) {
super(targetRepoName);
this.tagName = tagName;
this.createTag = createTag;
}
}
}