diff --git a/core/src/main/java/hudson/model/AbstractBuild.java b/core/src/main/java/hudson/model/AbstractBuild.java index 16f35eee8561..168505f29a2f 100644 --- a/core/src/main/java/hudson/model/AbstractBuild.java +++ b/core/src/main/java/hudson/model/AbstractBuild.java @@ -30,6 +30,7 @@ import hudson.FilePath; import hudson.Functions; import hudson.Launcher; +import jenkins.scm.RunWithSCM; import jenkins.util.SystemProperties; import hudson.console.ModelHyperlinkNote; import hudson.model.Fingerprint.BuildPtr; @@ -70,7 +71,6 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.lang.ref.WeakReference; -import java.util.AbstractSet; import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; @@ -91,8 +91,6 @@ import jenkins.model.lazy.BuildReference; import jenkins.model.lazy.LazyBuildMixIn; -import org.kohsuke.accmod.Restricted; -import org.kohsuke.accmod.restrictions.DoNotUse; /** * Base implementation of {@link Run}s that build software. @@ -102,7 +100,7 @@ * @author Kohsuke Kawaguchi * @see AbstractProject */ -public abstract class AbstractBuild

,R extends AbstractBuild> extends Run implements Queue.Executable, LazyBuildMixIn.LazyLoadingRun { +public abstract class AbstractBuild

,R extends AbstractBuild> extends Run implements Queue.Executable, LazyBuildMixIn.LazyLoadingRun, RunWithSCM { /** * Set if we want the blame information to flow from upstream to downstream build. @@ -321,80 +319,38 @@ public FilePath[] getModuleRoots() { return getParent().getScm().getModuleRoots(ws, this); } - /** - * List of users who committed a change since the last non-broken build till now. - * - *

- * This list at least always include people who made changes in this build, but - * if the previous build was a failure it also includes the culprit list from there. - * - * @return - * can be empty but never null. - */ - @Exported - public Set getCulprits() { - if (culprits==null) { - Set r = new HashSet(); - R p = getPreviousCompletedBuild(); - if (p !=null && isBuilding()) { - Result pr = p.getResult(); - if (pr!=null && pr.isWorseThan(Result.SUCCESS)) { - // we are still building, so this is just the current latest information, - // but we seems to be failing so far, so inherit culprits from the previous build. - // isBuilding() check is to avoid recursion when loading data from old Hudson, which doesn't record - // this information - r.addAll(p.getCulprits()); - } - } - for (Entry e : getChangeSet()) - r.add(e.getAuthor()); + @Override + @CheckForNull public Set getCulpritIds() { + return culprits; + } - if (upstreamCulprits) { - // If we have dependencies since the last successful build, add their authors to our list - if (getPreviousNotFailedBuild() != null) { - Map depmap = getDependencyChanges(getPreviousSuccessfulBuild()); - for (DependencyChange dep : depmap.values()) { - for (AbstractBuild b : dep.getBuilds()) { - for (Entry entry : b.getChangeSet()) { - r.add(entry.getAuthor()); - } + @Override + public boolean shouldCalculateCulprits() { + return getCulpritIds() == null; + } + + @Override + @Nonnull + public Set calculateCulprits() { + Set c = RunWithSCM.super.calculateCulprits(); + + AbstractBuild p = getPreviousCompletedBuild(); + if (upstreamCulprits) { + // If we have dependencies since the last successful build, add their authors to our list + if (p.getPreviousNotFailedBuild() != null) { + Map depmap = + p.getDependencyChanges(p.getPreviousSuccessfulBuild()); + for (AbstractBuild.DependencyChange dep : depmap.values()) { + for (AbstractBuild b : dep.getBuilds()) { + for (ChangeLogSet.Entry entry : b.getChangeSet()) { + c.add(entry.getAuthor()); } } } } - - return r; } - return new AbstractSet() { - public Iterator iterator() { - return new AdaptedIterator(culprits.iterator()) { - protected User adapt(String id) { - return User.get(id); - } - }; - } - - public int size() { - return culprits.size(); - } - }; - } - - /** - * Returns true if this user has made a commit to this build. - * - * @since 1.191 - */ - public boolean hasParticipant(User user) { - for (ChangeLogSet.Entry e : getChangeSet()) - try{ - if (e.getAuthor()==user) - return true; - } catch (RuntimeException re) { - LOGGER.log(Level.INFO, "Failed to determine author of changelog " + e.getCommitId() + "for " + getParent().getDisplayName() + ", " + getDisplayName(), re); - } - return false; + return c; } /** @@ -863,7 +819,7 @@ public Collection getBuildFingerprints() { * @return never null. */ @Exported - public ChangeLogSet getChangeSet() { + @Nonnull public ChangeLogSet getChangeSet() { synchronized (changeSetLock) { if (scm==null) { scm = NullChangeLogParser.INSTANCE; @@ -887,10 +843,10 @@ public ChangeLogSet getChangeSet() { return cs; } - @Restricted(DoNotUse.class) // for project-changes.jelly - public List> getChangeSets() { + @Override + @Nonnull public List> getChangeSets() { ChangeLogSet cs = getChangeSet(); - return cs.isEmptySet() ? Collections.>emptyList() : Collections.>singletonList(cs); + return cs.isEmptySet() ? Collections.emptyList() : Collections.singletonList(cs); } /** diff --git a/core/src/main/java/hudson/model/AbstractProject.java b/core/src/main/java/hudson/model/AbstractProject.java index 995b584cb8b6..7e003bfca930 100644 --- a/core/src/main/java/hudson/model/AbstractProject.java +++ b/core/src/main/java/hudson/model/AbstractProject.java @@ -34,7 +34,6 @@ import hudson.EnvVars; import hudson.ExtensionList; import hudson.ExtensionPoint; -import hudson.FeedAdapter; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; @@ -55,8 +54,6 @@ import hudson.model.queue.QueueTaskFuture; import hudson.model.queue.SubTask; import hudson.model.queue.SubTaskContributor; -import hudson.scm.ChangeLogSet; -import hudson.scm.ChangeLogSet.Entry; import hudson.scm.NullSCM; import hudson.scm.PollingResult; @@ -87,7 +84,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; @@ -107,7 +103,6 @@ import javax.servlet.ServletException; import jenkins.model.BlockedBecauseOfBuildInProgress; import jenkins.model.Jenkins; -import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.ParameterizedJobMixIn; import jenkins.model.Uptime; import jenkins.model.lazy.LazyBuildMixIn; @@ -1974,66 +1969,6 @@ public HttpResponse doEnable() throws IOException, ServletException { } - /** - * RSS feed for changes in this project. - */ - public void doRssChangelog( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { - class FeedItem { - ChangeLogSet.Entry e; - int idx; - - public FeedItem(Entry e, int idx) { - this.e = e; - this.idx = idx; - } - - AbstractBuild getBuild() { - return e.getParent().build; - } - } - - List entries = new ArrayList(); - - for(R r=getLastBuild(); r!=null; r=r.getPreviousBuild()) { - int idx=0; - for( ChangeLogSet.Entry e : r.getChangeSet()) - entries.add(new FeedItem(e,idx++)); - } - - RSS.forwardToRss( - getDisplayName()+' '+getScm().getDescriptor().getDisplayName()+" changes", - getUrl()+"changes", - entries, new FeedAdapter() { - public String getEntryTitle(FeedItem item) { - return "#"+item.getBuild().number+' '+item.e.getMsg()+" ("+item.e.getAuthor()+")"; - } - - public String getEntryUrl(FeedItem item) { - return item.getBuild().getUrl()+"changes#detail"+item.idx; - } - - public String getEntryID(FeedItem item) { - return getEntryUrl(item); - } - - public String getEntryDescription(FeedItem item) { - StringBuilder buf = new StringBuilder(); - for(String path : item.e.getAffectedPaths()) - buf.append(path).append('\n'); - return buf.toString(); - } - - public Calendar getEntryTimestamp(FeedItem item) { - return item.getBuild().getTimestamp(); - } - - public String getEntryAuthor(FeedItem entry) { - return JenkinsLocationConfiguration.get().getAdminAddress(); - } - }, - req, rsp ); - } - /** * {@link AbstractProject} subtypes should implement this base class as a descriptor. * diff --git a/core/src/main/java/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index c41ff5596698..0575724d9971 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -28,6 +28,7 @@ import hudson.EnvVars; import hudson.Extension; import hudson.ExtensionPoint; +import hudson.FeedAdapter; import hudson.PermalinkList; import hudson.Util; import hudson.cli.declarative.CLIResolver; @@ -36,6 +37,8 @@ import hudson.model.Fingerprint.RangeSet; import hudson.model.PermalinkProjectAction.Permalink; import hudson.model.listeners.ItemListener; +import hudson.scm.ChangeLogSet; +import hudson.scm.SCM; import hudson.search.QuickSilver; import hudson.search.SearchIndex; import hudson.search.SearchIndexBuilder; @@ -83,12 +86,14 @@ import jenkins.model.BuildDiscarderProperty; import jenkins.model.DirectlyModifiableTopLevelItemGroup; import jenkins.model.Jenkins; +import jenkins.model.JenkinsLocationConfiguration; import jenkins.model.ModelObjectWithChildren; import jenkins.model.ProjectNamingStrategy; import jenkins.model.RunIdMigrator; import jenkins.model.lazy.LazyBuildMixIn; +import jenkins.scm.RunWithSCM; import jenkins.security.HexStringConfidentialKey; -import jenkins.util.io.OnMaster; +import jenkins.triggers.SCMTriggerItem; import net.sf.json.JSONException; import net.sf.json.JSONObject; import org.apache.commons.io.FileUtils; @@ -1042,7 +1047,84 @@ public PermalinkList getPermalinks() { } return permalinks; } - + + /** + * RSS feed for changes in this project. + * + * @since TODO + */ + public void doRssChangelog(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { + class FeedItem { + ChangeLogSet.Entry e; + int idx; + + public FeedItem(ChangeLogSet.Entry e, int idx) { + this.e = e; + this.idx = idx; + } + + Run getBuild() { + return e.getParent().build; + } + } + + List entries = new ArrayList(); + String scmDisplayName = ""; + if (this instanceof SCMTriggerItem) { + SCMTriggerItem scmItem = (SCMTriggerItem) this; + List scmNames = new ArrayList<>(); + for (SCM s : scmItem.getSCMs()) { + scmNames.add(s.getDescriptor().getDisplayName()); + } + scmDisplayName = " " + Util.join(scmNames, ", "); + } + + for (RunT r = getLastBuild(); r != null; r = r.getPreviousBuild()) { + int idx = 0; + if (r instanceof RunWithSCM) { + for (ChangeLogSet c : ((RunWithSCM) r).getChangeSets()) { + for (ChangeLogSet.Entry e : c) { + entries.add(new FeedItem(e, idx++)); + } + } + } + } + RSS.forwardToRss( + getDisplayName() + scmDisplayName + " changes", + getUrl() + "changes", + entries, new FeedAdapter() { + public String getEntryTitle(FeedItem item) { + return "#" + item.getBuild().number + ' ' + item.e.getMsg() + " (" + item.e.getAuthor() + ")"; + } + + public String getEntryUrl(FeedItem item) { + return item.getBuild().getUrl() + "changes#detail" + item.idx; + } + + public String getEntryID(FeedItem item) { + return getEntryUrl(item); + } + + public String getEntryDescription(FeedItem item) { + StringBuilder buf = new StringBuilder(); + for (String path : item.e.getAffectedPaths()) + buf.append(path).append('\n'); + return buf.toString(); + } + + public Calendar getEntryTimestamp(FeedItem item) { + return item.getBuild().getTimestamp(); + } + + public String getEntryAuthor(FeedItem entry) { + return JenkinsLocationConfiguration.get().getAdminAddress(); + } + }, + req, rsp); + } + + + @Override public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception { // not sure what would be really useful here. This needs more thoughts. // for the time being, I'm starting with permalinks diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java index 4be622f2a34d..db669cdb2a5d 100644 --- a/core/src/main/java/hudson/model/View.java +++ b/core/src/main/java/hudson/model/View.java @@ -59,9 +59,11 @@ import javax.annotation.Nonnull; import jenkins.model.Jenkins; import jenkins.model.ModelObjectWithChildren; +import jenkins.model.ModelObjectWithContextMenu; import jenkins.model.item_category.Categories; import jenkins.model.item_category.Category; import jenkins.model.item_category.ItemCategory; +import jenkins.scm.RunWithSCM; import jenkins.util.ProgressiveRendering; import jenkins.util.xml.XMLUtils; @@ -114,7 +116,8 @@ import java.util.logging.Logger; import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST; -import static jenkins.model.Jenkins.*; +import static jenkins.scm.RunWithSCM.*; + import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.QueryParameter; @@ -249,7 +252,7 @@ public String getViewName() { */ public void rename(String newName) throws Failure, FormException { if(name.equals(newName)) return; // noop - checkGoodName(newName); + Jenkins.checkGoodName(newName); if(owner.getView(newName)!=null) throw new FormException(Messages.Hudson_ViewAlreadyExists(newName),"name"); String oldName = name; @@ -461,7 +464,7 @@ public List getComputers() { private boolean isRelevant(Collection

- * This method is invoked whenever someone does {@link AbstractBuild#getEnvironment(TaskListener)}, which - * can be before/after your checkout method is invoked. So if you are going to provide information about - * check out (like SVN revision number that was checked out), be prepared for the possibility that the - * check out hasn't happened yet. + * This method is invoked whenever someone does {@link AbstractBuild#getEnvironment(TaskListener)}, via + * {@link #buildEnvVars(AbstractBuild, Map)}, which can be before/after your checkout method is invoked. So if you + * are going to provide information about check out (like SVN revision number that was checked out), be prepared + * for the possibility that the check out hasn't happened yet. + * + * @since FIXME */ - // TODO is an equivalent for Run needed? + public void buildEnvironment(@Nonnull Run build, @Nonnull Map env) { + if (build instanceof AbstractBuild) { + buildEnvVars((AbstractBuild)build, env); + } + } + + @Deprecated public void buildEnvVars(AbstractBuild build, Map env) { + if (Util.isOverridden(SCM.class, getClass(), "buildEnvironment", Run.class, Map.class)) { + buildEnvironment(build, env); + } // default implementation is noop. } diff --git a/core/src/main/java/jenkins/scm/RunWithSCM.java b/core/src/main/java/jenkins/scm/RunWithSCM.java new file mode 100644 index 000000000000..551716503cf9 --- /dev/null +++ b/core/src/main/java/jenkins/scm/RunWithSCM.java @@ -0,0 +1,160 @@ +/* + * The MIT License + * + * Copyright 2017 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 jenkins.scm; + +import com.google.common.collect.ImmutableSet; +import hudson.model.Job; +import hudson.model.Queue; +import hudson.model.Result; +import hudson.model.Run; +import hudson.model.User; +import hudson.scm.ChangeLogSet; +import hudson.scm.SCM; +import hudson.util.AdaptedIterator; +import org.kohsuke.stapler.export.Exported; + +import javax.annotation.CheckForNull; +import javax.annotation.Nonnull; +import java.util.AbstractSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Allows a {@link Run} to provide {@link SCM}-related methods, such as providing changesets and culprits. + * + * @since FIXME + */ +public interface RunWithSCM, + RunT extends Run & RunWithSCM> { + + /** + * Gets all {@link ChangeLogSet}s currently associated with this item. + * + * @return A possibly empty list of {@link ChangeLogSet}s. + */ + @Nonnull + List> getChangeSets(); + + /** + * Gets the ids for all {@link User}s included in {@link #getChangeSets()} for this item. + * + * @return A set of user IDs, or null if this was the first time the method was called or the build is still running + * for a {@link RunWithSCM} instance with no culprits. + */ + @CheckForNull + Set getCulpritIds(); + + /** + * Determines whether culprits should be recalcuated or the existing {@link #getCulpritIds()} should be used instead. + * + * @return True if culprits should be recalcuated, false otherwise. + */ + boolean shouldCalculateCulprits(); + + /** + * List of users who committed a change since the last non-broken build till now. + * + *

+ * This list at least always include people who made changes in this build, but + * if the previous build was a failure it also includes the culprit list from there. + * + * @return + * can be empty but never null. + */ + @Exported + @Nonnull default Set getCulprits() { + if (shouldCalculateCulprits()) { + return calculateCulprits(); + } + + return new AbstractSet() { + private Set culpritIds = ImmutableSet.copyOf(getCulpritIds()); + + public Iterator iterator() { + return new AdaptedIterator(culpritIds.iterator()) { + protected User adapt(String id) { + return User.get(id); + } + }; + } + + public int size() { + return culpritIds.size(); + } + }; + } + + /** + * Method used for actually calculating the culprits from scratch. Called by {@link #getCulprits()} and + * overrides of {@link #getCulprits()}. Does not persist culprits information. + * + * @return a non-null {@link Set} of {@link User}s associated with this item. + */ + @SuppressWarnings("unchecked") + @Nonnull + default Set calculateCulprits() { + Set r = new HashSet<>(); + RunT p = ((RunT)this).getPreviousCompletedBuild(); + if (p != null) { + Result pr = p.getResult(); + if (pr != null && pr.isWorseThan(Result.SUCCESS)) { + // we are still building, so this is just the current latest information, + // but we seems to be failing so far, so inherit culprits from the previous build. + r.addAll(p.getCulprits()); + } + } + for (ChangeLogSet c : getChangeSets()) { + for (ChangeLogSet.Entry e : c) { + r.add(e.getAuthor()); + } + } + + return r; + } + + /** + * Returns true if this user has made a commit to this build. + */ + @SuppressWarnings("unchecked") + default boolean hasParticipant(User user) { + for (ChangeLogSet c : getChangeSets()) { + for (ChangeLogSet.Entry e : c) { + try { + if (e.getAuthor() == user) { + return true; + } + } catch (RuntimeException re) { + Logger LOGGER = Logger.getLogger(RunWithSCM.class.getName()); + LOGGER.log(Level.INFO, "Failed to determine author of changelog " + e.getCommitId() + "for " + ((RunT) this).getParent().getDisplayName() + ", " + ((RunT) this).getDisplayName(), re); + } + } + } + return false; + } +} diff --git a/test/src/test/java/hudson/model/AbstractBuildTest.java b/test/src/test/java/hudson/model/AbstractBuildTest.java index 55a9326b9c3b..4d96f971fa5a 100644 --- a/test/src/test/java/hudson/model/AbstractBuildTest.java +++ b/test/src/test/java/hudson/model/AbstractBuildTest.java @@ -34,8 +34,12 @@ import java.util.Collections; import java.util.Set; import java.util.TreeSet; +import java.util.concurrent.TimeUnit; + import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; + +import hudson.tasks.LogRotatorTest; import org.junit.Rule; import org.junit.Test; import org.jvnet.hudson.test.CaptureEnvironmentBuilder; @@ -132,6 +136,8 @@ public void culprits() throws Exception { FakeChangeLogSCM scm = new FakeChangeLogSCM(); p.setScm(scm); + LogRotatorTest.StallBuilder sync = new LogRotatorTest.StallBuilder(); + // 1st build, successful, no culprits scm.addChange().withAuthor("alice"); FreeStyleBuild b = j.buildAndAssertSuccess(p); @@ -144,8 +150,15 @@ public void culprits() throws Exception { assertCulprits(b, "bob"); // 3rd build. bob continues to be in culprit + p.getBuildersList().add(sync); scm.addChange().withAuthor("charlie"); - b = j.assertBuildStatus(Result.FAILURE, p.scheduleBuild2(0).get()); + b = p.scheduleBuild2(0).waitForStart(); + sync.waitFor(b.getNumber(), 1, TimeUnit.SECONDS); + + // Verify that we can get culprits while running. + assertCulprits(b, "bob", "charlie"); + sync.release(b.getNumber()); + j.assertBuildStatus(Result.FAILURE, j.waitForCompletion(b)); assertCulprits(b, "bob", "charlie"); // 4th build, unstable. culprit list should continue diff --git a/test/src/test/java/hudson/tasks/LogRotatorTest.java b/test/src/test/java/hudson/tasks/LogRotatorTest.java index d45259e23c3c..1990b3bc826d 100644 --- a/test/src/test/java/hudson/tasks/LogRotatorTest.java +++ b/test/src/test/java/hudson/tasks/LogRotatorTest.java @@ -224,7 +224,7 @@ public Descriptor getDescriptor() { } } - static class StallBuilder extends TestBuilder { + public static class StallBuilder extends TestBuilder { private int syncBuildNumber;