提交 2f5ca1a7 编写于 作者: A Andrew Bayer 提交者: GitHub

Merge pull request #2730 from abayer/jenkins-24141

[JENKINS-24141] Pull ChangeLogSet-related logic out of AbstractBuild
......@@ -30,6 +30,7 @@ import hudson.EnvVars;
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.File;
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 static java.util.logging.Level.WARNING;
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 @@ import org.kohsuke.accmod.restrictions.DoNotUse;
* @author Kohsuke Kawaguchi
* @see AbstractProject
*/
public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends AbstractBuild<P,R>> extends Run<P,R> implements Queue.Executable, LazyBuildMixIn.LazyLoadingRun<P,R> {
public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends AbstractBuild<P,R>> extends Run<P,R> implements Queue.Executable, LazyBuildMixIn.LazyLoadingRun<P,R>, RunWithSCM<P,R> {
/**
* Set if we want the blame information to flow from upstream to downstream build.
......@@ -321,80 +319,38 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
return getParent().getScm().getModuleRoots(ws, this);
}
/**
* List of users who committed a change since the last non-broken build till now.
*
* <p>
* 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<User> getCulprits() {
if (culprits==null) {
Set<User> r = new HashSet<User>();
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<String> getCulpritIds() {
return culprits;
}
if (upstreamCulprits) {
// If we have dependencies since the last successful build, add their authors to our list
if (getPreviousNotFailedBuild() != null) {
Map <AbstractProject,DependencyChange> 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<User> calculateCulprits() {
Set<User> c = RunWithSCM.super.calculateCulprits();
AbstractBuild<P,R> p = getPreviousCompletedBuild();
if (upstreamCulprits) {
// If we have dependencies since the last successful build, add their authors to our list
if (p.getPreviousNotFailedBuild() != null) {
Map<AbstractProject, AbstractBuild.DependencyChange> 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<User>() {
public Iterator<User> iterator() {
return new AdaptedIterator<String,User>(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 abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
* @return never null.
*/
@Exported
public ChangeLogSet<? extends Entry> getChangeSet() {
@Nonnull public ChangeLogSet<? extends Entry> getChangeSet() {
synchronized (changeSetLock) {
if (scm==null) {
scm = NullChangeLogParser.INSTANCE;
......@@ -887,10 +843,10 @@ public abstract class AbstractBuild<P extends AbstractProject<P,R>,R extends Abs
return cs;
}
@Restricted(DoNotUse.class) // for project-changes.jelly
public List<ChangeLogSet<? extends ChangeLogSet.Entry>> getChangeSets() {
@Override
@Nonnull public List<ChangeLogSet<? extends ChangeLogSet.Entry>> getChangeSets() {
ChangeLogSet<? extends Entry> cs = getChangeSet();
return cs.isEmptySet() ? Collections.<ChangeLogSet<? extends ChangeLogSet.Entry>>emptyList() : Collections.<ChangeLogSet<? extends ChangeLogSet.Entry>>singletonList(cs);
return cs.isEmptySet() ? Collections.emptyList() : Collections.singletonList(cs);
}
/**
......
......@@ -34,7 +34,6 @@ import hudson.CopyOnWrite;
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.CauseOfBlockage;
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.File;
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.annotation.Nonnull;
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 abstract class AbstractProject<P extends AbstractProject<P,R>,R extends A
}
/**
* 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<FeedItem> entries = new ArrayList<FeedItem>();
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<FeedItem>() {
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.
*
......
......@@ -28,6 +28,7 @@ import hudson.BulkChange;
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.Range;
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.BuildDiscarder;
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 abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, R
}
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<FeedItem> entries = new ArrayList<FeedItem>();
String scmDisplayName = "";
if (this instanceof SCMTriggerItem) {
SCMTriggerItem scmItem = (SCMTriggerItem) this;
List<String> 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<? extends ChangeLogSet.Entry> 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<FeedItem>() {
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
......
......@@ -59,9 +59,11 @@ import hudson.widgets.Widget;
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.Level;
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 abstract class View extends AbstractModelObject implements AccessControll
*/
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 abstract class View extends AbstractModelObject implements AccessControll
private boolean isRelevant(Collection<Label> labels, Computer computer) {
Node node = computer.getNode();
if (node == null) return false;
if (labels.contains(null) && node.getMode() == Mode.NORMAL) return true;
if (labels.contains(null) && node.getMode() == Node.Mode.NORMAL) return true;
for (Label l : labels)
if (l != null && l.contains(node))
......@@ -615,12 +618,12 @@ public abstract class View extends AbstractModelObject implements AccessControll
/**
* Which project did this user commit? Can be null.
*/
private AbstractProject project;
private Job<?,?> project;
/** @see UserAvatarResolver */
String avatar;
UserInfo(User user, AbstractProject p, Calendar lastChange) {
UserInfo(User user, Job<?,?> p, Calendar lastChange) {
this.user = user;
this.project = p;
this.lastChange = lastChange;
......@@ -636,8 +639,13 @@ public abstract class View extends AbstractModelObject implements AccessControll
return lastChange;
}
@Exported
@Deprecated
public AbstractProject getProject() {
return project instanceof AbstractProject ? (AbstractProject)project : null;
}
@Exported(name="project")
public Job<?,?> getJob() {
return project;
}
......@@ -720,20 +728,23 @@ public abstract class View extends AbstractModelObject implements AccessControll
private Map<User,UserInfo> getUserInfo(Collection<? extends Item> items) {
Map<User,UserInfo> users = new HashMap<User,UserInfo>();
for (Item item : items) {
for (Job job : item.getAllJobs()) {
if (job instanceof AbstractProject) {
AbstractProject<?,?> p = (AbstractProject) job;
for (AbstractBuild<?,?> build : p.getBuilds()) {
for (Entry entry : build.getChangeSet()) {
User user = entry.getAuthor();
UserInfo info = users.get(user);
if(info==null)
users.put(user,new UserInfo(user,p,build.getTimestamp()));
else
if(info.getLastChange().before(build.getTimestamp())) {
info.project = p;
info.lastChange = build.getTimestamp();
for (Job<?, ?> job : item.getAllJobs()) {
RunList<? extends Run<?, ?>> runs = job.getBuilds();
for (Run<?, ?> r : runs) {
if (r instanceof RunWithSCM) {
RunWithSCM<?,?> runWithSCM = (RunWithSCM<?,?>) r;
for (ChangeLogSet<? extends Entry> c : runWithSCM.getChangeSets()) {
for (Entry entry : c) {
User user = entry.getAuthor();
UserInfo info = users.get(user);
if (info == null)
users.put(user, new UserInfo(user, job, r.getTimestamp()));
else if (info.getLastChange().before(r.getTimestamp())) {
info.project = job;
info.lastChange = r.getTimestamp();
}
}
}
}
......@@ -761,13 +772,17 @@ public abstract class View extends AbstractModelObject implements AccessControll
public static boolean isApplicable(Collection<? extends Item> items) {
for (Item item : items) {
for (Job job : item.getAllJobs()) {
if (job instanceof AbstractProject) {
AbstractProject<?,?> p = (AbstractProject) job;
for (AbstractBuild<?,?> build : p.getBuilds()) {
for (Entry entry : build.getChangeSet()) {
User user = entry.getAuthor();
if(user!=null)
return true;
RunList<? extends Run<?, ?>> runs = job.getBuilds();
for (Run<?,?> r : runs) {
if (r instanceof RunWithSCM) {
RunWithSCM<?,?> runWithSCM = (RunWithSCM<?,?>) r;
for (ChangeLogSet<? extends Entry> c : runWithSCM.getChangeSets()) {
for (Entry entry : c) {
User user = entry.getAuthor();
if (user != null)
return true;
}
}
}
}
......@@ -813,38 +828,42 @@ public abstract class View extends AbstractModelObject implements AccessControll
int itemCount = 0;
for (Item item : items) {
for (Job<?,?> job : item.getAllJobs()) {
if (job instanceof AbstractProject) {
AbstractProject<?,?> p = (AbstractProject) job;
RunList<? extends AbstractBuild<?,?>> builds = p.getBuilds();
int buildCount = 0;
for (AbstractBuild<?,?> build : builds) {
if (canceled()) {
return;
}
for (ChangeLogSet.Entry entry : build.getChangeSet()) {
RunList<? extends Run<?, ?>> builds = job.getBuilds();
int buildCount = 0;
for (Run<?, ?> r : builds) {
if (canceled()) {
return;
}
if (!(r instanceof RunWithSCM)) {
continue;
}
RunWithSCM<?, ?> runWithSCM = (RunWithSCM<?, ?>) r;
for (ChangeLogSet<? extends ChangeLogSet.Entry> c : runWithSCM.getChangeSets()) {
for (ChangeLogSet.Entry entry : c) {
User user = entry.getAuthor();
UserInfo info = users.get(user);
if (info == null) {
UserInfo userInfo = new UserInfo(user, p, build.getTimestamp());
UserInfo userInfo = new UserInfo(user, job, r.getTimestamp());
userInfo.avatar = UserAvatarResolver.resolveOrNull(user, iconSize);
synchronized (this) {
users.put(user, userInfo);
modified.add(user);
}
} else if (info.getLastChange().before(build.getTimestamp())) {
} else if (info.getLastChange().before(r.getTimestamp())) {
synchronized (this) {
info.project = p;
info.lastChange = build.getTimestamp();
info.project = job;
info.lastChange = r.getTimestamp();
modified.add(user);
}
}
}
// TODO consider also adding the user of the UserCause when applicable
buildCount++;
// TODO this defeats lazy-loading. Should rather do a breadth-first search, as in hudson.plugins.view.dashboard.builds.LatestBuilds
// (though currently there is no quick implementation of RunMap.size() ~ idOnDisk.size(), which would be needed for proper progress)
progress((itemCount + 1.0 * buildCount / builds.size()) / (items.size() + 1));
}
// TODO consider also adding the user of the UserCause when applicable
buildCount++;
// TODO this defeats lazy-loading. Should rather do a breadth-first search, as in hudson.plugins.view.dashboard.builds.LatestBuilds
// (though currently there is no quick implementation of RunMap.size() ~ idOnDisk.size(), which would be needed for proper progress)
progress((itemCount + 1.0 * buildCount / builds.size()) / (items.size() + 1));
}
}
itemCount++;
......@@ -884,7 +903,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
accumulate("avatar", i.avatar != null ? i.avatar : Stapler.getCurrentRequest().getContextPath() + Functions.getResourcePath() + "/images/" + iconSize + "/user.png").
accumulate("timeSortKey", i.getTimeSortKey()).
accumulate("lastChangeTimeString", i.getLastChangeTimeString());
AbstractProject<?,?> p = i.getProject();
Job<?,?> p = i.getJob();
if (p != null) {
entry.accumulate("projectUrl", p.getUrl()).accumulate("projectFullDisplayName", p.getFullDisplayName());
}
......@@ -1205,8 +1224,8 @@ public abstract class View extends AbstractModelObject implements AccessControll
save();
}
public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {
ContextMenu m = new ContextMenu();
public ModelObjectWithContextMenu.ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) throws Exception {
ModelObjectWithContextMenu.ContextMenu m = new ModelObjectWithContextMenu.ContextMenu();
for (TopLevelItem i : getItems())
m.add(i.getShortUrl(),i.getDisplayName());
return m;
......@@ -1288,7 +1307,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
|| requestContentType.startsWith("text/xml"));
String name = req.getParameter("name");
checkGoodName(name);
Jenkins.checkGoodName(name);
if(owner.getView(name)!=null)
throw new Failure(Messages.Hudson_ViewAlreadyExists(name));
......@@ -1350,7 +1369,7 @@ public abstract class View extends AbstractModelObject implements AccessControll
try (InputStream in = new BufferedInputStream(xml)) {
View v = (View) Jenkins.XSTREAM.fromXML(in);
if (name != null) v.name = name;
checkGoodName(v.name);
Jenkins.checkGoodName(v.name);
return v;
} catch(StreamException|ConversionException|Error e) {// mostly reflection errors
throw new IOException("Unable to read",e);
......
......@@ -522,13 +522,24 @@ public abstract class SCM implements Describable<SCM>, ExtensionPoint {
* (for example, SVN revision number.)
*
* <p>
* 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<String,String> env) {
if (build instanceof AbstractBuild) {
buildEnvVars((AbstractBuild)build, env);
}
}
@Deprecated
public void buildEnvVars(AbstractBuild<?,?> build, Map<String, String> env) {
if (Util.isOverridden(SCM.class, getClass(), "buildEnvironment", Run.class, Map.class)) {
buildEnvironment(build, env);
}
// default implementation is noop.
}
......
/*
* 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<JobT extends Job<JobT, RunT>,
RunT extends Run<JobT, RunT> & RunWithSCM<JobT,RunT>> {
/**
* Gets all {@link ChangeLogSet}s currently associated with this item.
*
* @return A possibly empty list of {@link ChangeLogSet}s.
*/
@Nonnull
List<ChangeLogSet<? extends ChangeLogSet.Entry>> 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<String> 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.
*
* <p>
* 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<User> getCulprits() {
if (shouldCalculateCulprits()) {
return calculateCulprits();
}
return new AbstractSet<User>() {
private Set<String> culpritIds = ImmutableSet.copyOf(getCulpritIds());
public Iterator<User> iterator() {
return new AdaptedIterator<String,User>(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<User> calculateCulprits() {
Set<User> 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<? extends ChangeLogSet.Entry> 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<? extends ChangeLogSet.Entry> 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;
}
}
......@@ -34,8 +34,12 @@ import java.util.Arrays;
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 class AbstractBuildTest {
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 class AbstractBuildTest {
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
......
......@@ -224,7 +224,7 @@ public class LogRotatorTest {
}
}
static class StallBuilder extends TestBuilder {
public static class StallBuilder extends TestBuilder {
private int syncBuildNumber;
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册