diff --git a/bom/pom.xml b/bom/pom.xml index dc442e708a6f..cfabffc73d90 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -70,7 +70,7 @@ THE SOFTWARE. org.springframework.security spring-security-bom - 6.4.1 + 6.4.2 pom import diff --git a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java index 72da0c7514c1..242975e2b13b 100644 --- a/core/src/main/java/hudson/model/DirectoryBrowserSupport.java +++ b/core/src/main/java/hudson/model/DirectoryBrowserSupport.java @@ -58,6 +58,8 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import jenkins.model.Jenkins; import jenkins.security.MasterToSlaveCallable; import jenkins.security.ResourceDomainConfiguration; @@ -65,8 +67,6 @@ import jenkins.util.SystemProperties; import jenkins.util.VirtualFile; import org.apache.commons.io.IOUtils; -import org.apache.tools.zip.ZipEntry; -import org.apache.tools.zip.ZipOutputStream; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.HttpResponse; @@ -530,8 +530,8 @@ private static String createBackRef(int times) { private static void zip(StaplerResponse2 rsp, VirtualFile root, VirtualFile dir, String glob) throws IOException, InterruptedException { OutputStream outputStream = rsp.getOutputStream(); - try (ZipOutputStream zos = new ZipOutputStream(outputStream)) { - zos.setEncoding(Charset.defaultCharset().displayName()); // TODO JENKINS-20663 make this overridable via query parameter + // TODO JENKINS-20663 make encoding overridable via query parameter + try (ZipOutputStream zos = new ZipOutputStream(outputStream, Charset.defaultCharset())) { // TODO consider using run(Callable) here if (glob.isEmpty()) { diff --git a/core/src/main/java/jenkins/model/PeepholePermalink.java b/core/src/main/java/jenkins/model/PeepholePermalink.java index 62185f39f1ae..de6844acca55 100644 --- a/core/src/main/java/jenkins/model/PeepholePermalink.java +++ b/core/src/main/java/jenkins/model/PeepholePermalink.java @@ -4,6 +4,8 @@ import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.Extension; +import hudson.ExtensionList; +import hudson.ExtensionPoint; import hudson.Util; import hudson.model.Job; import hudson.model.PermalinkProjectAction.Permalink; @@ -14,6 +16,7 @@ import hudson.util.AtomicFileWriter; import java.io.File; import java.io.IOException; +import java.io.Serializable; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.HashMap; @@ -24,6 +27,7 @@ import java.util.logging.Logger; import java.util.stream.Stream; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; import org.kohsuke.accmod.restrictions.NoExternalUse; /** @@ -63,14 +67,6 @@ */ public abstract class PeepholePermalink extends Permalink implements Predicate> { - /** - * JENKINS-22822: avoids rereading caches. - * Top map keys are {@code builds} directories. - * Inner maps are from permalink name to build number. - * Synchronization is first on the outer map, then on the inner. - */ - private static final Map> caches = new HashMap<>(); - /** * Checks if the given build satisfies the peep-hole criteria. * @@ -94,115 +90,216 @@ protected File getPermalinkFile(Job job) { */ @Override public Run resolve(Job job) { - Map cache = cacheFor(job.getBuildDir()); - int n; - synchronized (cache) { - n = cache.getOrDefault(getId(), 0); + return ExtensionList.lookupFirst(Cache.class).get(job, getId()).resolve(this, job, getId()); + } + + /** + * Start from the build 'b' and locate the build that matches the criteria going back in time + */ + @CheckForNull + private Run find(@CheckForNull Run b) { + while (b != null && !apply(b)) { + b = b.getPreviousBuild(); } - if (n == RESOLVES_TO_NONE) { - return null; + return b; + } + + /** + * Remembers the value 'n' in the cache for future {@link #resolve(Job)}. + */ + protected void updateCache(@NonNull Job job, @CheckForNull Run b) { + ExtensionList.lookupFirst(Cache.class).put(job, getId(), b != null ? new Cache.Some(b.getNumber()) : Cache.NONE); + } + + /** + * Persistable cache of peephole permalink targets. + */ + @Restricted(Beta.class) + public interface Cache extends ExtensionPoint { + + /** Cacheable target of a permalink. */ + sealed interface PermalinkTarget extends Serializable { + + /** + * Implementation of {@link #resolve(Job)}. + * This may update the cache if it was missing or found to be invalid. + */ + @Restricted(NoExternalUse.class) + @CheckForNull + Run resolve(@NonNull PeepholePermalink pp, @NonNull Job job, @NonNull String id); + + /** + * Partial implementation of {@link #resolve(PeepholePermalink, Job, String)} when searching. + * @param b if set, the newest build to even consider when searching + */ + @Restricted(NoExternalUse.class) + @CheckForNull + default Run search(@NonNull PeepholePermalink pp, @NonNull Job job, @NonNull String id, @CheckForNull Run b) { + if (b == null) { + // no cache + b = job.getLastBuild(); + } + // start from the build 'b' and locate the build that matches the criteria going back in time + b = pp.find(b); + pp.updateCache(job, b); + return b; + } + } - Run b; - if (n > 0) { - b = job.getBuildByNumber(n); - if (b != null && apply(b)) { - return b; // found it (in the most efficient way possible) + + /** + * The cache entry for this target is missing. + */ + record Unknown() implements PermalinkTarget { + @Override + public Run resolve(PeepholePermalink pp, Job job, String id) { + return search(pp, job, id, null); } - } else { - b = null; } - // the cache is stale. start the search - if (b == null) { - b = job.getNearestOldBuild(n); + Unknown UNKNOWN = new Unknown(); + + /** + * The cache entry for this target is present. + */ + sealed interface Known extends PermalinkTarget {} + + /** There is known to be no matching build. */ + record None() implements Known { + @Override + public Run resolve(PeepholePermalink pp, Job job, String id) { + return null; + } } - if (b == null) { - // no cache - b = job.getLastBuild(); + /** Singleton of {@link None}. */ + None NONE = new None(); + + /** A matching build, indicated by {@link Run#getNumber}. */ + record Some(int number) implements Known { + @Override + public Run resolve(PeepholePermalink pp, Job job, String id) { + Run b = job.getBuildByNumber(number); + if (b != null && pp.apply(b)) { + return b; // found it (in the most efficient way possible) + } + // the cache is stale. start the search + if (b == null) { + b = job.getNearestOldBuild(number); + } + return search(pp, job, id, b); + } } - // start from the build 'b' and locate the build that matches the criteria going back in time - b = find(b); + /** + * Looks for any existing cache hit. + * @param id {@link #getId} + * @return {@link Some} or {@link #NONE} or {@link #UNKNOWN} + */ + @NonNull PermalinkTarget get(@NonNull Job job, @NonNull String id); - updateCache(job, b); - return b; + /** + * Updates the cache. + * Note that this may be called not just when a build completes or is deleted + * (meaning that the logical value of the cache has changed), + * but also when {@link #resolve} has failed to find a cached value + * (or determined that a previously cached value is in fact invalid). + * @param id {@link #getId} + * @param target {@link Some} or {@link #NONE} + */ + void put(@NonNull Job job, @NonNull String id, @NonNull Known target); } /** - * Start from the build 'b' and locate the build that matches the criteria going back in time + * Default cache based on a {@code permalinks} file in the build directory. + * There is one line per cached permalink, in the format {@code lastStableBuild 123} + * or (for a negative cache) {@code lastFailedBuild -1}. */ - private Run find(Run b) { - //noinspection StatementWithEmptyBody - for ( ; b != null && !apply(b); b = b.getPreviousBuild()) - ; - return b; - } + @Restricted(NoExternalUse.class) + @Extension(ordinal = -1000) + public static final class DefaultCache implements Cache { + + /** + * JENKINS-22822: avoids rereading caches. + * Top map keys are {@code builds} directories. + * Inner maps are from permalink name to target. + * Synchronization is first on the outer map, then on the inner. + */ + private final Map> caches = new HashMap<>(); - private static @NonNull Map cacheFor(@NonNull File buildDir) { - synchronized (caches) { - Map cache = caches.get(buildDir); - if (cache == null) { - cache = load(buildDir); - caches.put(buildDir, cache); + @Override + public PermalinkTarget get(Job job, String id) { + var cache = cacheFor(job.getBuildDir()); + synchronized (cache) { + var cached = cache.get(id); + return cached != null ? cached : UNKNOWN; } - return cache; } - } - private static @NonNull Map load(@NonNull File buildDir) { - Map cache = new TreeMap<>(); - File storage = storageFor(buildDir); - if (storage.isFile()) { - try (Stream lines = Files.lines(storage.toPath(), StandardCharsets.UTF_8)) { - lines.forEach(line -> { - int idx = line.indexOf(' '); - if (idx == -1) { - return; - } + @Override + public void put(Job job, String id, Known target) { + File buildDir = job.getBuildDir(); + var cache = cacheFor(buildDir); + synchronized (cache) { + cache.put(id, target); + File storage = storageFor(buildDir); + LOGGER.fine(() -> "saving to " + storage + ": " + cache); + try (AtomicFileWriter cw = new AtomicFileWriter(storage)) { try { - cache.put(line.substring(0, idx), Integer.parseInt(line.substring(idx + 1))); - } catch (NumberFormatException x) { - LOGGER.log(Level.WARNING, "failed to read " + storage, x); + for (var entry : cache.entrySet()) { + cw.write(entry.getKey()); + cw.write(' '); + cw.write(Integer.toString(entry.getValue() instanceof Cache.Some some ? some.number : -1)); + cw.write('\n'); + } + cw.commit(); + } finally { + cw.abort(); } - }); - } catch (IOException x) { - LOGGER.log(Level.WARNING, "failed to read " + storage, x); + } catch (IOException x) { + LOGGER.log(Level.WARNING, "failed to update " + storage, x); + } } - LOGGER.fine(() -> "loading from " + storage + ": " + cache); } - return cache; - } - static @NonNull File storageFor(@NonNull File buildDir) { - return new File(buildDir, "permalinks"); - } + private @NonNull Map cacheFor(@NonNull File buildDir) { + synchronized (caches) { + var cache = caches.get(buildDir); + if (cache == null) { + cache = load(buildDir); + caches.put(buildDir, cache); + } + return cache; + } + } - /** - * Remembers the value 'n' in the cache for future {@link #resolve(Job)}. - */ - protected void updateCache(@NonNull Job job, @CheckForNull Run b) { - File buildDir = job.getBuildDir(); - Map cache = cacheFor(buildDir); - synchronized (cache) { - cache.put(getId(), b == null ? RESOLVES_TO_NONE : b.getNumber()); + private static @NonNull Map load(@NonNull File buildDir) { + Map cache = new TreeMap<>(); File storage = storageFor(buildDir); - LOGGER.fine(() -> "saving to " + storage + ": " + cache); - try (AtomicFileWriter cw = new AtomicFileWriter(storage)) { - try { - for (Map.Entry entry : cache.entrySet()) { - cw.write(entry.getKey()); - cw.write(' '); - cw.write(Integer.toString(entry.getValue())); - cw.write('\n'); - } - cw.commit(); - } finally { - cw.abort(); + if (storage.isFile()) { + try (Stream lines = Files.lines(storage.toPath(), StandardCharsets.UTF_8)) { + lines.forEach(line -> { + int idx = line.indexOf(' '); + if (idx == -1) { + return; + } + try { + int number = Integer.parseInt(line.substring(idx + 1)); + cache.put(line.substring(0, idx), number == -1 ? Cache.NONE : new Cache.Some(number)); + } catch (NumberFormatException x) { + LOGGER.log(Level.WARNING, "failed to read " + storage, x); + } + }); + } catch (IOException x) { + LOGGER.log(Level.WARNING, "failed to read " + storage, x); } - } catch (IOException x) { - LOGGER.log(Level.WARNING, "failed to update " + storage, x); + LOGGER.fine(() -> "loading from " + storage + ": " + cache); } + return cache; + } + + static @NonNull File storageFor(@NonNull File buildDir) { + return new File(buildDir, "permalinks"); } } @@ -380,7 +477,5 @@ public boolean apply(Run run) { @Restricted(NoExternalUse.class) public static void initialized() {} - private static final int RESOLVES_TO_NONE = -1; - private static final Logger LOGGER = Logger.getLogger(PeepholePermalink.class.getName()); } diff --git a/core/src/main/java/jenkins/util/VirtualFile.java b/core/src/main/java/jenkins/util/VirtualFile.java index 6c754a50bbc4..36279a8ec6fa 100644 --- a/core/src/main/java/jenkins/util/VirtualFile.java +++ b/core/src/main/java/jenkins/util/VirtualFile.java @@ -60,6 +60,8 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; import jenkins.MasterToSlaveFileCallable; import jenkins.model.ArtifactManager; import jenkins.security.MasterToSlaveCallable; @@ -68,8 +70,6 @@ import org.apache.tools.ant.types.selectors.SelectorUtils; import org.apache.tools.ant.types.selectors.TokenizedPath; import org.apache.tools.ant.types.selectors.TokenizedPattern; -import org.apache.tools.zip.ZipEntry; -import org.apache.tools.zip.ZipOutputStream; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -375,8 +375,8 @@ public int zip(OutputStream outputStream, String includes, String excludes, bool } Collection files = list(includes, excludes, useDefaultExcludes, openOptions); - try (ZipOutputStream zos = new ZipOutputStream(outputStream)) { - zos.setEncoding(Charset.defaultCharset().displayName()); // TODO JENKINS-20663 make this overridable via query parameter + // TODO JENKINS-20663 make encoding overridable via query parameter + try (ZipOutputStream zos = new ZipOutputStream(outputStream, Charset.defaultCharset())) { for (String relativePath : files) { VirtualFile virtualFile = this.child(relativePath); diff --git a/package.json b/package.json index 1989b499283c..5fa4e3c1b4ce 100644 --- a/package.json +++ b/package.json @@ -39,10 +39,10 @@ "mini-css-extract-plugin": "2.9.2", "postcss": "8.4.49", "postcss-loader": "8.1.1", - "postcss-preset-env": "10.1.1", + "postcss-preset-env": "10.1.2", "postcss-scss": "4.0.9", "prettier": "3.4.2", - "sass": "1.82.0", + "sass": "1.83.0", "sass-loader": "16.0.4", "style-loader": "4.0.0", "stylelint": "16.11.0", @@ -54,7 +54,7 @@ }, "dependencies": { "handlebars": "4.7.8", - "hotkeys-js": "3.12.2", + "hotkeys-js": "3.13.9", "jquery": "3.7.1", "lodash": "4.17.21", "sortablejs": "1.15.6", diff --git a/pom.xml b/pom.xml index 926fb78b6de1..a59462a515d9 100644 --- a/pom.xml +++ b/pom.xml @@ -97,7 +97,7 @@ THE SOFTWARE. 1.30 false - 8.2 + 8.4 20.18.1 diff --git a/src/main/js/components/command-palette/index.js b/src/main/js/components/command-palette/index.js index a0935fd7a18c..c369b5ab4318 100644 --- a/src/main/js/components/command-palette/index.js +++ b/src/main/js/components/command-palette/index.js @@ -33,6 +33,8 @@ function init() { searchResultsContainer, () => searchResults.querySelectorAll("a"), hoverClass, + () => {}, + () => commandPalette.open, ); // Events diff --git a/src/main/js/util/keyboard.js b/src/main/js/util/keyboard.js index 88d677530416..11c414b6a5da 100644 --- a/src/main/js/util/keyboard.js +++ b/src/main/js/util/keyboard.js @@ -81,7 +81,7 @@ export default function makeKeyboardNavigable( } function scrollAndSelect(selectedItem, selectedClass, items) { - if (selectedItem !== null) { + if (selectedItem) { if (!isInViewport(selectedItem)) { selectedItem.scrollIntoView(false); } diff --git a/test/pom.xml b/test/pom.xml index ceab77c44894..4a43825f1605 100644 --- a/test/pom.xml +++ b/test/pom.xml @@ -218,7 +218,7 @@ THE SOFTWARE. org.jenkins-ci.plugins cloudbees-folder - 6.973.vc9b_85a_61e4fc + 6.975.v4161e479479f test diff --git a/test/src/test/java/hudson/model/AbstractProjectTest.java b/test/src/test/java/hudson/model/AbstractProjectTest.java index 04db7ea8f4fb..b8a9d8e1ea6a 100644 --- a/test/src/test/java/hudson/model/AbstractProjectTest.java +++ b/test/src/test/java/hudson/model/AbstractProjectTest.java @@ -664,6 +664,8 @@ private void testAutoCompleteResponse(JSONObject responseBody, String... project JSONObject o = new JSONObject(); o.put("name", p); o.put("url", JSONObject.fromObject(null)); + o.put("icon", JSONObject.fromObject(null)); + o.put("type", "symbol"); expected.add(o); } assertThat(suggestions.containsAll(expected), is(true)); diff --git a/test/src/test/java/jenkins/model/PeepholePermalinkTest.java b/test/src/test/java/jenkins/model/PeepholePermalinkTest.java index 8d06ab980173..2a4e05854962 100644 --- a/test/src/test/java/jenkins/model/PeepholePermalinkTest.java +++ b/test/src/test/java/jenkins/model/PeepholePermalinkTest.java @@ -82,7 +82,7 @@ public void basics() throws Exception { } private void assertStorage(String id, Job job, Run build) throws Exception { - assertThat(Files.readAllLines(PeepholePermalink.storageFor(job.getBuildDir()).toPath(), StandardCharsets.UTF_8), + assertThat(Files.readAllLines(PeepholePermalink.DefaultCache.storageFor(job.getBuildDir()).toPath(), StandardCharsets.UTF_8), hasItem(id + " " + (build == null ? -1 : build.getNumber()))); } diff --git a/test/src/test/java/jenkins/model/lazy/LazyBuildMixInTest.java b/test/src/test/java/jenkins/model/lazy/LazyBuildMixInTest.java index 6b2ef4e6cf41..53f0603dd0b1 100644 --- a/test/src/test/java/jenkins/model/lazy/LazyBuildMixInTest.java +++ b/test/src/test/java/jenkins/model/lazy/LazyBuildMixInTest.java @@ -24,6 +24,9 @@ package jenkins.model.lazy; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; @@ -87,6 +90,40 @@ public class LazyBuildMixInTest { assertEquals(b3, b1a.getNextBuild()); } + @Test public void numericLookups() throws Exception { + var p = r.createFreeStyleProject(); + var b1 = r.buildAndAssertSuccess(p); + var b2 = r.buildAndAssertSuccess(p); + var b3 = r.buildAndAssertSuccess(p); + var b4 = r.buildAndAssertSuccess(p); + var b5 = r.buildAndAssertSuccess(p); + var b6 = r.buildAndAssertSuccess(p); + b1.delete(); + b3.delete(); + b6.delete(); + // leaving 2, 4, 5 + assertThat(p.getFirstBuild(), is(b2)); + assertThat(p.getLastBuild(), is(b5)); + assertThat(p.getNearestBuild(-1), is(b2)); + assertThat(p.getNearestBuild(0), is(b2)); + assertThat(p.getNearestBuild(1), is(b2)); + assertThat(p.getNearestBuild(2), is(b2)); + assertThat(p.getNearestBuild(3), is(b4)); + assertThat(p.getNearestBuild(4), is(b4)); + assertThat(p.getNearestBuild(5), is(b5)); + assertThat(p.getNearestBuild(6), nullValue()); + assertThat(p.getNearestBuild(7), nullValue()); + assertThat(p.getNearestOldBuild(-1), nullValue()); + assertThat(p.getNearestOldBuild(0), nullValue()); + assertThat(p.getNearestOldBuild(1), nullValue()); + assertThat(p.getNearestOldBuild(2), is(b2)); + assertThat(p.getNearestOldBuild(3), is(b2)); + assertThat(p.getNearestOldBuild(4), is(b4)); + assertThat(p.getNearestOldBuild(5), is(b5)); + assertThat(p.getNearestOldBuild(6), is(b5)); + assertThat(p.getNearestOldBuild(7), is(b5)); + } + @Issue("JENKINS-20662") @Test public void newRunningBuildRelationFromPrevious() throws Exception { FreeStyleProject p = r.createFreeStyleProject(); diff --git a/war/pom.xml b/war/pom.xml index 75f7f4e7c16c..8b6e5411c4b3 100644 --- a/war/pom.xml +++ b/war/pom.xml @@ -645,7 +645,7 @@ THE SOFTWARE. org.eclipse.jetty.ee9 jetty-ee9-maven-plugin - 12.0.14 + 12.0.16