diff --git a/.github/renovate.json b/.github/renovate.json index 90c5a18d9f6d..cfd588a66b8d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -71,16 +71,6 @@ "org.fusesource.jansi:jansi" ] }, - { - "description": "Depends on commons-lang3 which is in progress for removal from core. See: https://issues.jenkins.io/browse/JENKINS-73355", - "matchManagers": [ - "maven" - ], - "enabled": false, - "matchPackageNames": [ - "org.apache.commons:commons-compress" - ] - }, { "description": "Contains incompatible API changes and needs compatibility work. See: https://github.com/jenkinsci/jenkins/pull/4224", "matchManagers": [ diff --git a/.github/workflows/publish-release-artifact.yml b/.github/workflows/publish-release-artifact.yml index 3bd728e9632b..e7a8904e78dc 100644 --- a/.github/workflows/publish-release-artifact.yml +++ b/.github/workflows/publish-release-artifact.yml @@ -73,7 +73,7 @@ jobs: wget -q https://get.jenkins.io/${REPO}/${PROJECT_VERSION}/${FILE_NAME} - name: Upload Release Asset id: upload-war - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -108,7 +108,7 @@ jobs: - name: Upload Release Asset id: upload-deb if: always() - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -144,7 +144,7 @@ jobs: - name: Upload Release Asset id: upload-rpm if: always() - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -180,7 +180,7 @@ jobs: - name: Upload Release Asset id: upload-msi if: always() - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: @@ -216,7 +216,7 @@ jobs: - name: Upload Release Asset id: upload-suse-rpm if: always() - uses: softprops/action-gh-release@01570a1f39cb168c169c802c3bceb9e93fb10974 # v2.1.0 + uses: softprops/action-gh-release@7b4da11513bf3f43f9999e90eabced41ab8bb048 # v2.2.0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/ath.sh b/ath.sh index e25bf4b945e8..3f6bca7f61bf 100644 --- a/ath.sh +++ b/ath.sh @@ -6,7 +6,7 @@ set -o xtrace cd "$(dirname "$0")" # https://github.com/jenkinsci/acceptance-test-harness/releases -export ATH_VERSION=6081.v29b_ce3c2771c +export ATH_VERSION=6107.v8c73fa_b_8f784 if [[ $# -eq 0 ]]; then export JDK=17 diff --git a/bom/pom.xml b/bom/pom.xml index 61de025b3e9b..a4c2dfa36e7d 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -62,7 +62,7 @@ THE SOFTWARE. org.springframework spring-framework-bom - 6.2.0 + 6.2.1 pom import @@ -70,7 +70,7 @@ THE SOFTWARE. org.springframework.security spring-security-bom - 6.4.1 + 6.4.2 pom import @@ -88,7 +88,7 @@ THE SOFTWARE. com.google.guava guava - 33.3.1-jre + 33.4.0-jre @@ -191,11 +191,6 @@ THE SOFTWARE. ant 1.10.15 - - org.apache.commons - commons-compress - 1.26.1 - org.apache.commons commons-fileupload2 diff --git a/core/pom.xml b/core/pom.xml index 9bff5e5ad0b2..0b9cb30d8763 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -285,17 +285,6 @@ THE SOFTWARE. org.apache.ant ant - - org.apache.commons - commons-compress - - - - org.apache.commons - commons-lang3 - - - org.apache.commons commons-fileupload2-core diff --git a/core/src/main/java/hudson/Functions.java b/core/src/main/java/hudson/Functions.java index 150b1b658c16..67801475233d 100644 --- a/core/src/main/java/hudson/Functions.java +++ b/core/src/main/java/hudson/Functions.java @@ -62,6 +62,7 @@ import hudson.model.View; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; +import hudson.search.SearchFactory; import hudson.search.SearchableModelObject; import hudson.security.ACL; import hudson.security.AccessControlled; @@ -124,6 +125,7 @@ import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.DecimalFormat; +import java.text.MessageFormat; import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; @@ -2576,4 +2578,34 @@ static String guessIcon(String iconGuess, String rootURL) { public static String generateItemId() { return String.valueOf(Math.floor(Math.random() * 3000)); } + + @Restricted(NoExternalUse.class) + public static ExtensionList getSearchFactories() { + return SearchFactory.all(); + } + + /** + * @param keyboardShortcut the shortcut to be translated + * @return the translated shortcut, e.g. CMD+K to ⌘+K for macOS, CTRL+K for Windows + */ + @Restricted(NoExternalUse.class) + public static String translateModifierKeysForUsersPlatform(String keyboardShortcut) { + StaplerRequest2 currentRequest = Stapler.getCurrentRequest2(); + currentRequest.getWebApp().getDispatchValidator().allowDispatch(currentRequest, Stapler.getCurrentResponse2()); + String userAgent = currentRequest.getHeader("User-Agent"); + + List platformsThatUseCommand = List.of("MAC", "IPHONE", "IPAD"); + boolean useCmdKey = platformsThatUseCommand.stream().anyMatch(e -> userAgent.toUpperCase().contains(e)); + + return keyboardShortcut.replace("CMD", useCmdKey ? "⌘" : "CTRL"); + } + + @Restricted(NoExternalUse.class) + public static String formatMessage(String format, Object args) { + if (format == null) { + return args.toString(); + } + + return MessageFormat.format(format, args); + } } 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/hudson/model/Job.java b/core/src/main/java/hudson/model/Job.java index d22c25e98e3d..1bbf3dad1a24 100644 --- a/core/src/main/java/hudson/model/Job.java +++ b/core/src/main/java/hudson/model/Job.java @@ -519,6 +519,11 @@ public boolean supportsLogRotator() { return true; } + @Override + public String getSearchIcon() { + return "symbol-status-" + this.getIconColor().getIconName(); + } + @Override protected SearchIndexBuilder makeSearchIndex() { return super.makeSearchIndex().add(new SearchIndex() { @@ -541,7 +546,7 @@ public void find(String token, List result) { public void suggest(String token, List result) { find(token, result); } - }).add("configure", "config", "configure"); + }); } @Override @@ -853,7 +858,7 @@ public RunT getBuildForCLI(@Argument(required = true, metaVar = "BUILD#", usage } /** - * Gets the youngest build #m that satisfies {@code n<=m}. + * Gets the oldest build #m that satisfies {@code m ≥ n}. * * This is useful when you'd like to fetch a build but the exact build might * be already gone (deleted, rotated, etc.) @@ -868,7 +873,7 @@ public RunT getNearestBuild(int n) { } /** - * Gets the latest build #m that satisfies {@code m<=n}. + * Gets the newest build #m that satisfies {@code m ≤ n}. * * This is useful when you'd like to fetch a build but the exact build might * be already gone (deleted, rotated, etc.) @@ -977,7 +982,7 @@ public File getBuildDir() { protected abstract void removeRun(RunT run); /** - * Returns the last build. + * Returns the newest build. * @see LazyBuildMixIn#getLastBuild */ @Exported diff --git a/core/src/main/java/hudson/model/User.java b/core/src/main/java/hudson/model/User.java index 685a80e540a9..d40664ca2c7a 100644 --- a/core/src/main/java/hudson/model/User.java +++ b/core/src/main/java/hudson/model/User.java @@ -44,6 +44,7 @@ import hudson.security.AccessControlled; import hudson.security.SecurityRealm; import hudson.security.UserMayOrMayNotExistException2; +import hudson.tasks.UserAvatarResolver; import hudson.util.FormValidation; import hudson.util.RunList; import hudson.util.XStream2; @@ -77,6 +78,7 @@ import jenkins.util.SystemProperties; import org.jenkinsci.Symbol; import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.Beta; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.StaplerProxy; import org.kohsuke.stapler.StaplerRequest2; @@ -277,6 +279,11 @@ public String getId() { return "/user/" + Util.rawEncode(idStrategy().keyFor(id)); } + @Override + public String getSearchIcon() { + return UserAvatarResolver.resolve(this, "48x48"); + } + /** * The URL of the user page. */ @@ -673,9 +680,9 @@ public void doSubmitDescription(StaplerRequest2 req, StaplerResponse2 rsp) throw } /** - * To be called from {@link Jenkins#reload} only. + * Called from {@link Jenkins#reload}. */ - @Restricted(NoExternalUse.class) + @Restricted(Beta.class) public static void reload() throws IOException { UserIdMapper.getInstance().reload(); AllUsers.reload(); diff --git a/core/src/main/java/hudson/model/View.java b/core/src/main/java/hudson/model/View.java index b17077cde44d..722254572c0c 100644 --- a/core/src/main/java/hudson/model/View.java +++ b/core/src/main/java/hudson/model/View.java @@ -560,6 +560,11 @@ public String getSearchUrl() { return getUrl(); } + @Override + public String getSearchIcon() { + return "symbol-jobs"; + } + /** * Returns the transient {@link Action}s associated with the top page. * diff --git a/core/src/main/java/hudson/search/Search.java b/core/src/main/java/hudson/search/Search.java index 7773d9e9d696..97d15b65c2df 100644 --- a/core/src/main/java/hudson/search/Search.java +++ b/core/src/main/java/hudson/search/Search.java @@ -46,6 +46,8 @@ import jenkins.security.stapler.StaplerNotDispatchable; import jenkins.util.MemoryReductionUtil; import jenkins.util.SystemProperties; +import org.jenkins.ui.symbol.Symbol; +import org.jenkins.ui.symbol.SymbolRequest; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.Ancestor; @@ -56,6 +58,7 @@ import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.StaplerResponse2; import org.kohsuke.stapler.export.DataWriter; +import org.kohsuke.stapler.export.ExportConfig; import org.kohsuke.stapler.export.Exported; import org.kohsuke.stapler.export.ExportedBean; import org.kohsuke.stapler.export.Flavor; @@ -157,10 +160,23 @@ public void doSuggestOpenSearch(StaplerRequest2 req, StaplerResponse2 rsp, @Quer */ public void doSuggest(StaplerRequest2 req, StaplerResponse2 rsp, @QueryParameter String query) throws IOException, ServletException { Result r = new Result(); - for (SuggestedItem item : getSuggestions(req, query)) - r.suggestions.add(new Item(item.getPath())); + for (SuggestedItem curItem : getSuggestions(req, query)) { + String iconName = curItem.item.getSearchIcon(); - rsp.serveExposedBean(req, r, Flavor.JSON); + if (iconName == null || + (!iconName.startsWith("symbol-") && !iconName.startsWith("http")) + ) { + iconName = "symbol-search"; + } + + if (iconName.startsWith("symbol")) { + r.suggestions.add(new Item(curItem.getPath(), curItem.getUrl(), + Symbol.get(new SymbolRequest.Builder().withRaw(iconName).build()))); + } else { + r.suggestions.add(new Item(curItem.getPath(), curItem.getUrl(), iconName, "image")); + } + } + rsp.serveExposedBean(req, r, new ExportConfig()); } /** @@ -252,12 +268,48 @@ public static class Result { @ExportedBean(defaultVisibility = 999) public static class Item { + @Exported @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "read by Stapler") public String name; + private final String url; + + private final String type; + + private final String icon; + public Item(String name) { + this(name, null, null); + } + + public Item(String name, String url, String icon) { + this.name = name; + this.url = url; + this.icon = icon; + this.type = "symbol"; + } + + public Item(String name, String url, String icon, String type) { this.name = name; + this.url = url; + this.icon = icon; + this.type = type; + } + + @Exported + public String getUrl() { + return url; + } + + @Exported + public String getIcon() { + return icon; + } + + @Exported + public String getType() { + return type; } } diff --git a/core/src/main/java/hudson/search/SearchItem.java b/core/src/main/java/hudson/search/SearchItem.java index 02b2921b9741..e64efb0257ed 100644 --- a/core/src/main/java/hudson/search/SearchItem.java +++ b/core/src/main/java/hudson/search/SearchItem.java @@ -25,6 +25,7 @@ package hudson.search; import hudson.model.Build; +import org.jenkins.ui.icon.IconSpec; /** * Represents an item reachable from {@link SearchIndex}. @@ -54,6 +55,14 @@ public interface SearchItem { String getSearchUrl(); + default String getSearchIcon() { + if (this instanceof IconSpec) { + return ((IconSpec) this).getIconClassName(); + } + + return "symbol-search"; + } + /** * Returns the {@link SearchIndex} to further search sub items inside this item. * diff --git a/core/src/main/java/hudson/search/SuggestedItem.java b/core/src/main/java/hudson/search/SuggestedItem.java index 9ba455270e58..6911d002fe50 100644 --- a/core/src/main/java/hudson/search/SuggestedItem.java +++ b/core/src/main/java/hudson/search/SuggestedItem.java @@ -62,7 +62,7 @@ private void getPath(StringBuilder buf) { buf.append(item.getSearchName()); else { parent.getPath(buf); - buf.append(' ').append(item.getSearchName()); + buf.append(" » ").append(item.getSearchName()); } } diff --git a/core/src/main/java/hudson/tools/ZipExtractionInstaller.java b/core/src/main/java/hudson/tools/ZipExtractionInstaller.java index 1e1c171f6a95..4646980ea8d6 100644 --- a/core/src/main/java/hudson/tools/ZipExtractionInstaller.java +++ b/core/src/main/java/hudson/tools/ZipExtractionInstaller.java @@ -43,6 +43,8 @@ import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Path; import jenkins.MasterToSlaveFileCallable; import jenkins.model.Jenkins; import org.jenkinsci.Symbol; @@ -117,6 +119,14 @@ public FormValidation doCheckUrl(@QueryParameter String value) throws Interrupte } catch (URISyntaxException e) { return FormValidation.error(e, Messages.ZipExtractionInstaller_malformed_url()); } + if (uri.getScheme() != null && !uri.getScheme().startsWith("http")) { + try { + Path.of(uri); + return FormValidation.ok(); + } catch (FileSystemNotFoundException | IllegalArgumentException e) { + return FormValidation.error(e, Messages.ZipExtractionInstaller_malformed_url()); + } + } HttpClient httpClient = ProxyConfiguration.newHttpClient(); HttpRequest httpRequest; try { diff --git a/core/src/main/java/jenkins/model/IComputer.java b/core/src/main/java/jenkins/model/IComputer.java index 37fe0af98535..e00ee78bd30f 100644 --- a/core/src/main/java/jenkins/model/IComputer.java +++ b/core/src/main/java/jenkins/model/IComputer.java @@ -35,6 +35,7 @@ import jenkins.agents.IOfflineCause; import org.jenkins.ui.icon.Icon; import org.jenkins.ui.icon.IconSet; +import org.jenkins.ui.icon.IconSpec; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.Beta; @@ -44,7 +45,7 @@ * @since 2.480 */ @Restricted(Beta.class) -public interface IComputer extends AccessControlled { +public interface IComputer extends AccessControlled, IconSpec { /** * Returns {@link Node#getNodeName() the name of the node}. */ diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index bedbea0af7c4..66adc45f82e8 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -147,6 +147,7 @@ import hudson.scm.RepositoryBrowser; import hudson.scm.SCM; import hudson.search.CollectionSearchIndex; +import hudson.search.SearchIndex; import hudson.search.SearchIndexBuilder; import hudson.search.SearchItem; import hudson.security.ACL; @@ -2345,11 +2346,29 @@ public String getSearchUrl() { @Override public SearchIndexBuilder makeSearchIndex() { SearchIndexBuilder builder = super.makeSearchIndex(); - if (hasPermission(ADMINISTER)) { - builder.add("configure", "config", "configure") - .add("manage") - .add("log"); - } + + this.actions.stream().filter(e -> e.getIconFileName() != null).forEach(action -> builder.add(new SearchItem() { + @Override + public String getSearchName() { + return action.getDisplayName(); + } + + @Override + public String getSearchUrl() { + return action.getUrlName(); + } + + @Override + public String getSearchIcon() { + return action.getIconFileName(); + } + + @Override + public SearchIndex getSearchIndex() { + return SearchIndex.EMPTY; + } + })); + builder.add(new CollectionSearchIndex() { @Override protected SearchItem get(String key) { return getItemByFullName(key, TopLevelItem.class); } @@ -3299,11 +3318,19 @@ public void load() throws IOException { if (cfg.exists()) { // reset some data that may not exist in the disk file // so that we can take a proper compensation action later. + String originalPrimaryView = primaryView; + List originalViews = new ArrayList<>(views); primaryView = null; views.clear(); - - // load from disk - cfg.unmarshal(Jenkins.this); + try { + // load from disk + cfg.unmarshal(Jenkins.this); + } catch (IOException | RuntimeException x) { + primaryView = originalPrimaryView; + views.clear(); + views.addAll(originalViews); + throw x; + } } // initialize views by inserting the default view if necessary // this is both for clean Jenkins and for backward compatibility. 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/model/experimentalflags/RemoveYuiUserExperimentalFlag.java b/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java index e8f8dcc31775..418f9c4b5406 100644 --- a/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java +++ b/core/src/main/java/jenkins/model/experimentalflags/RemoveYuiUserExperimentalFlag.java @@ -24,8 +24,10 @@ package jenkins.model.experimentalflags; +import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; +import jenkins.util.SystemProperties; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -46,4 +48,10 @@ public String getDisplayName() { public String getShortDescription() { return "Remove YUI from all Jenkins UI pages. This will break anything that depends on YUI"; } + + @NonNull + @Override + public Boolean getDefaultValue() { + return SystemProperties.getBoolean(RemoveYuiUserExperimentalFlag.class.getName() + ".defaultValue", true); + } } diff --git a/core/src/main/java/jenkins/telemetry/Telemetry.java b/core/src/main/java/jenkins/telemetry/Telemetry.java index 1f60e319f97d..de081c16954b 100644 --- a/core/src/main/java/jenkins/telemetry/Telemetry.java +++ b/core/src/main/java/jenkins/telemetry/Telemetry.java @@ -130,6 +130,11 @@ public static ExtensionList all() { return ExtensionList.lookup(Telemetry.class); } + @Restricted(NoExternalUse.class) // called by jelly + public static boolean isAnyTrialActive() { + return all().stream().anyMatch(Telemetry::isActivePeriod); + } + /** * @since 2.147 * @return whether to collect telemetry 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/core/src/main/java/org/jenkins/ui/icon/IconSpec.java b/core/src/main/java/org/jenkins/ui/icon/IconSpec.java index c44e577e1ce0..f1bf535b6ca6 100644 --- a/core/src/main/java/org/jenkins/ui/icon/IconSpec.java +++ b/core/src/main/java/org/jenkins/ui/icon/IconSpec.java @@ -27,8 +27,7 @@ /** * Icon Specification. *
- * Plugin extension points that implement/extend Action/ManagementLink should - * also implement this interface. + * If your class provides an icon spec you should implement this interface. * * @author tom.fennelly@gmail.com * @since 2.0 diff --git a/core/src/main/resources/hudson/model/UsageStatistics/global.groovy b/core/src/main/resources/hudson/model/UsageStatistics/global.groovy index 5506a37a67d5..7254eb283a60 100644 --- a/core/src/main/resources/hudson/model/UsageStatistics/global.groovy +++ b/core/src/main/resources/hudson/model/UsageStatistics/global.groovy @@ -8,7 +8,7 @@ def f=namespace(lib.FormTagLib) f.section(title: _("Usage Statistics")) { if (UsageStatistics.DISABLED) { - span(class: "jenkins-not-applicable") { + div(class: "jenkins-not-applicable jenkins-description") { raw(_("disabledBySystemProperty")) } } else if (FIPS140.useCompliantAlgorithms()) { diff --git a/core/src/main/resources/hudson/model/UsageStatistics/help-usageStatisticsCollected.jelly b/core/src/main/resources/hudson/model/UsageStatistics/help-usageStatisticsCollected.jelly index a1bef14d4e22..2d01661a175b 100644 --- a/core/src/main/resources/hudson/model/UsageStatistics/help-usageStatisticsCollected.jelly +++ b/core/src/main/resources/hudson/model/UsageStatistics/help-usageStatisticsCollected.jelly @@ -1,14 +1,14 @@ -
+

For any project, it's critical to know how the software is used, but tracking usage data is inherently difficult in open-source projects. Anonymous usage statistics address this need. When enabled, Jenkins periodically sends information to the Jenkins project. The Jenkins project uses this information to set development priorities. -

+

-

General usage statistics

+

General usage statistics

Jenkins reports the following general usage statistics:

    @@ -24,7 +24,7 @@

-

Telemetry collection

+

Telemetry collection

@@ -38,24 +38,34 @@ Each trial has a specific purpose and a defined end date, after which collection stops, independent of the installed versions of Jenkins or plugins. Once a trial is complete, the trial results may be aggregated and shared with the developer community.

-

- The following trials defined on this instance are active now or in the future: -

+ + -
- - -
${collector.displayName}
-
- -

- Start date: ${collector.start}
- End date: ${collector.end} -

-
-
-
-
+ + +

${%There are currently no active trials.}

+
+ +

+ The following trials defined on this instance are active now or in the future: +

+
+ + +
${collector.displayName}
+
+ +

+ Start date: ${collector.start} +
+ End date: ${collector.end} +

+
+
+
+
+
+
diff --git a/core/src/main/resources/jenkins/model/Jenkins/sidepanel.jelly b/core/src/main/resources/jenkins/model/Jenkins/sidepanel.jelly index 9ddff73647d6..e413712d6e7c 100644 --- a/core/src/main/resources/jenkins/model/Jenkins/sidepanel.jelly +++ b/core/src/main/resources/jenkins/model/Jenkins/sidepanel.jelly @@ -23,4 +23,9 @@ THE SOFTWARE. --> - \ No newline at end of file + + + + + + diff --git a/core/src/main/resources/jenkins/views/JenkinsHeader/search-box.js b/core/src/main/resources/jenkins/views/JenkinsHeader/search-box.js deleted file mode 100644 index 91e805c4ceaf..000000000000 --- a/core/src/main/resources/jenkins/views/JenkinsHeader/search-box.js +++ /dev/null @@ -1,6 +0,0 @@ -(function () { - var element = document.getElementById("search-box-completion"); - if (element) { - createSearchBox(element.getAttribute("data-search-url")); - } -})(); diff --git a/core/src/main/resources/lib/form/toggleSwitch.jelly b/core/src/main/resources/lib/form/toggleSwitch.jelly index 2a2da3e94eb4..b0c9fcf5f835 100644 --- a/core/src/main/resources/lib/form/toggleSwitch.jelly +++ b/core/src/main/resources/lib/form/toggleSwitch.jelly @@ -76,19 +76,19 @@ THE SOFTWARE. disabled="${readOnlyMode ? 'true' : null}" checked="${value ? 'true' : null}"/> - - ${description} + + ${attrs.description} diff --git a/core/src/main/resources/lib/layout/command-palette.jelly b/core/src/main/resources/lib/layout/command-palette.jelly new file mode 100644 index 000000000000..3d454f88a8c1 --- /dev/null +++ b/core/src/main/resources/lib/layout/command-palette.jelly @@ -0,0 +1,47 @@ + + + + + + The command palette overlay + + +
+ + +
+
+ +
+
+
+
+
+
+ diff --git a/core/src/main/resources/lib/layout/header/searchbox.jelly b/core/src/main/resources/lib/layout/header/searchbox.jelly index 916f5ad49c22..033dc2ab0407 100644 --- a/core/src/main/resources/lib/layout/header/searchbox.jelly +++ b/core/src/main/resources/lib/layout/header/searchbox.jelly @@ -1,25 +1,27 @@ - -