about summary refs log tree commit diff
path: root/third_party/gerrit
diff options
context:
space:
mode:
Diffstat (limited to 'third_party/gerrit')
-rw-r--r--third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch25
-rw-r--r--third_party/gerrit/0002-Syntax-highlight-nix.patch24
-rw-r--r--third_party/gerrit/0003-Syntax-highlight-rules.pl.patch46
-rw-r--r--third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch217
-rw-r--r--third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch26
-rw-r--r--third_party/gerrit/0006-Always-use-Google-Fonts.patch28
-rw-r--r--third_party/gerrit/default.nix150
-rw-r--r--third_party/gerrit/detzip.go97
8 files changed, 613 insertions, 0 deletions
diff --git a/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch b/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch
new file mode 100644
index 000000000000..7d197795b725
--- /dev/null
+++ b/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch
@@ -0,0 +1,25 @@
+From 621cadcc1dd71e9397c21cf8cf0f1aae4f6f7057 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Thu, 2 Jul 2020 23:02:09 +0100
+Subject: [PATCH 1/7] Use detzip in download_bower.py
+
+---
+ tools/js/download_bower.py | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
+index d541b565a9..ffdae60f95 100755
+--- a/tools/js/download_bower.py
++++ b/tools/js/download_bower.py
+@@ -110,7 +110,7 @@ def main():
+                 args.b, '--quiet', 'install', '%s#%s' % (args.p, args.v)))
+         bc = os.path.join(cwd, 'bower_components')
+         subprocess.check_call(
+-            ['zip', '-q', '--exclude', '.bower.json', '-r', cached, args.n],
++            ['detzip', '--exclude', '.bower.json', cached, args.n],
+             cwd=bc)
+ 
+         if args.s:
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0002-Syntax-highlight-nix.patch b/third_party/gerrit/0002-Syntax-highlight-nix.patch
new file mode 100644
index 000000000000..256da0a3c930
--- /dev/null
+++ b/third_party/gerrit/0002-Syntax-highlight-nix.patch
@@ -0,0 +1,24 @@
+From 924647c354576ade0dc46fdf30596967f58bb4c6 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Thu, 2 Jul 2020 23:02:32 +0100
+Subject: [PATCH 2/7] Syntax highlight nix
+
+---
+ .../app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts         | 1 +
+ 1 file changed, 1 insertion(+)
+
+diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+index 081d28d749..2762ccc625 100644
+--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
++++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+@@ -99,6 +99,7 @@ const LANGUAGE_MAP = new Map<string, string>([
+   ['text/x-vhdl', 'vhdl'],
+   ['text/x-yaml', 'yaml'],
+   ['text/vbscript', 'vbscript'],
++  ['application/x-mix-transfer', 'nix'],
+ ]);
+ const ASYNC_DELAY = 10;
+ 
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch b/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch
new file mode 100644
index 000000000000..02bb3397eabb
--- /dev/null
+++ b/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch
@@ -0,0 +1,46 @@
+From be348f64eda257ae0af1f89552548d3e8eca3688 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Thu, 2 Jul 2020 23:02:43 +0100
+Subject: [PATCH 3/7] Syntax highlight rules.pl
+
+---
+ .../diff/gr-syntax-layer/gr-syntax-layer.ts         | 13 ++++++++++++-
+ 1 file changed, 12 insertions(+), 1 deletion(-)
+
+diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+index 2762ccc625..598e14589f 100644
+--- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
++++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
+@@ -103,6 +103,10 @@ const LANGUAGE_MAP = new Map<string, string>([
+ ]);
+ const ASYNC_DELAY = 10;
+ 
++const FILENAME_OVERRIDES = new Map<string, string>([
++  ['rules.pl', 'prolog'],
++]);
++
+ const CLASS_SAFELIST = new Set<string>([
+   'gr-diff gr-syntax gr-syntax-attr',
+   'gr-diff gr-syntax gr-syntax-attribute',
+@@ -241,10 +245,17 @@ export class GrSyntaxLayer implements DiffLayer {
+     }
+   }
+ 
++  _basename(filename: string): string {
++    const pieces = filename.split(/\//);
++    return pieces[pieces.length-1];
++  }
++
+   _getLanguage(metaInfo: DiffFileMetaInfo) {
+     // The Gerrit API provides only content-type, but for other users of
+     // gr-diff it may be more convenient to specify the language directly.
+-    return metaInfo.language ?? LANGUAGE_MAP.get(metaInfo.content_type);
++    return metaInfo.language ??
++        FILENAME_OVERRIDES.get(this._basename(metaInfo.name)) ??
++        LANGUAGE_MAP.get(metaInfo.content_type);
+   }
+ 
+   /**
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch b/third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch
new file mode 100644
index 000000000000..8e78e5f535e3
--- /dev/null
+++ b/third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch
@@ -0,0 +1,217 @@
+From 32bf13d8316f93828d2ff47ccfca38d4e7a634b1 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Thu, 2 Jul 2020 23:03:02 +0100
+Subject: [PATCH 4/7] Add titles to CLs over HTTP
+
+---
+ .../gerrit/httpd/raw/IndexHtmlUtil.java       | 13 +++-
+ .../google/gerrit/httpd/raw/IndexServlet.java |  8 ++-
+ .../google/gerrit/httpd/raw/StaticModule.java |  6 +-
+ .../gerrit/httpd/raw/TitleComputer.java       | 67 +++++++++++++++++++
+ .../gerrit/httpd/raw/PolyGerritIndexHtml.soy  |  4 +-
+ 5 files changed, 90 insertions(+), 8 deletions(-)
+ create mode 100644 java/com/google/gerrit/httpd/raw/TitleComputer.java
+
+diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+index 8d52f5ad50..a9cfceb3b6 100644
+--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
++++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+@@ -39,6 +39,7 @@ import java.util.Arrays;
+ import java.util.Collections;
+ import java.util.HashMap;
+ import java.util.Map;
++import java.util.Optional;
+ import java.util.Set;
+ import java.util.function.Function;
+ 
+@@ -60,13 +61,14 @@ public class IndexHtmlUtil {
+       String faviconPath,
+       Map<String, String[]> urlParameterMap,
+       Function<String, SanitizedContent> urlInScriptTagOrdainer,
+-      String requestedURL)
++      String requestedURL,
++      TitleComputer titleComputer)
+       throws URISyntaxException, RestApiException {
+     ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
+     data.putAll(
+             staticTemplateData(
+                 canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
+-        .putAll(dynamicTemplateData(gerritApi, requestedURL));
++        .putAll(dynamicTemplateData(gerritApi, requestedURL, titleComputer));
+     Set<String> enabledExperiments = experimentFeatures.getEnabledExperimentFeatures();
+ 
+     if (!enabledExperiments.isEmpty()) {
+@@ -77,7 +79,9 @@ public class IndexHtmlUtil {
+ 
+   /** Returns dynamic parameters of {@code index.html}. */
+   public static ImmutableMap<String, Object> dynamicTemplateData(
+-      GerritApi gerritApi, String requestedURL) throws RestApiException, URISyntaxException {
++      GerritApi gerritApi,
++      String requestedURL,
++      TitleComputer titleComputer) throws RestApiException, URISyntaxException {
+     ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
+     Map<String, SanitizedContent> initialData = new HashMap<>();
+     Server serverApi = gerritApi.config().server();
+@@ -128,6 +132,9 @@ public class IndexHtmlUtil {
+       // Don't render data
+     }
+ 
++    Optional<String> title = titleComputer.computeTitle(requestedURL);
++    title.ifPresent(s -> data.put("title", s));
++
+     data.put("gerritInitialData", initialData);
+     return data.build();
+   }
+diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
+index 3f2c2028ae..7861c007df 100644
+--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
++++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
+@@ -46,13 +46,15 @@ public class IndexServlet extends HttpServlet {
+   private final ExperimentFeatures experimentFeatures;
+   private final SoySauce soySauce;
+   private final Function<String, SanitizedContent> urlOrdainer;
++  private TitleComputer titleComputer;
+ 
+   IndexServlet(
+       @Nullable String canonicalUrl,
+       @Nullable String cdnPath,
+       @Nullable String faviconPath,
+       GerritApi gerritApi,
+-      ExperimentFeatures experimentFeatures) {
++      ExperimentFeatures experimentFeatures,
++      TitleComputer titleComputer) {
+     this.canonicalUrl = canonicalUrl;
+     this.cdnPath = cdnPath;
+     this.faviconPath = faviconPath;
+@@ -67,6 +69,7 @@ public class IndexServlet extends HttpServlet {
+         (s) ->
+             UnsafeSanitizedContentOrdainer.ordainAsSafe(
+                 s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
++    this.titleComputer = titleComputer;
+   }
+ 
+   @Override
+@@ -85,7 +88,8 @@ public class IndexServlet extends HttpServlet {
+               faviconPath,
+               parameterMap,
+               urlOrdainer,
+-              requestUrl);
++              requestUrl,
++              titleComputer);
+       renderer = soySauce.renderTemplate("com.google.gerrit.httpd.raw.Index").setData(templateData);
+     } catch (URISyntaxException | RestApiException e) {
+       throw new IOException(e);
+diff --git a/java/com/google/gerrit/httpd/raw/StaticModule.java b/java/com/google/gerrit/httpd/raw/StaticModule.java
+index bb1eb92525..6b20c504d2 100644
+--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
++++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
+@@ -224,11 +224,13 @@ public class StaticModule extends ServletModule {
+         @CanonicalWebUrl @Nullable String canonicalUrl,
+         @GerritServerConfig Config cfg,
+         GerritApi gerritApi,
+-        ExperimentFeatures experimentFeatures) {
++        ExperimentFeatures experimentFeatures,
++        TitleComputer titleComputer) {
+       String cdnPath =
+           options.useDevCdn() ? options.devCdn() : cfg.getString("gerrit", null, "cdnPath");
+       String faviconPath = cfg.getString("gerrit", null, "faviconPath");
+-      return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi, experimentFeatures);
++      return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi,
++          experimentFeatures, titleComputer);
+     }
+ 
+     @Provides
+diff --git a/java/com/google/gerrit/httpd/raw/TitleComputer.java b/java/com/google/gerrit/httpd/raw/TitleComputer.java
+new file mode 100644
+index 0000000000..8fd2053ad0
+--- /dev/null
++++ b/java/com/google/gerrit/httpd/raw/TitleComputer.java
+@@ -0,0 +1,67 @@
++package com.google.gerrit.httpd.raw;
++
++import com.google.common.flogger.FluentLogger;
++import com.google.gerrit.entities.Change;
++import com.google.gerrit.extensions.restapi.ResourceConflictException;
++import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
++import com.google.gerrit.server.change.ChangeResource;
++import com.google.gerrit.server.permissions.PermissionBackendException;
++import com.google.gerrit.server.restapi.change.ChangesCollection;
++import com.google.inject.Inject;
++import com.google.inject.Provider;
++import com.google.inject.Singleton;
++
++import java.net.MalformedURLException;
++import java.net.URL;
++import java.util.Optional;
++import java.util.regex.Matcher;
++import java.util.regex.Pattern;
++
++@Singleton
++public class TitleComputer {
++  private static final FluentLogger logger = FluentLogger.forEnclosingClass();
++
++  @Inject
++  public TitleComputer(Provider<ChangesCollection> changes) {
++    this.changes = changes;
++  }
++
++  public Optional<String> computeTitle(String requestedURI) {
++    URL url = null;
++    try {
++      url = new URL(requestedURI);
++    } catch (MalformedURLException e) {
++      logger.atWarning().log("Failed to turn %s into a URL.", requestedURI);
++      return Optional.empty();
++    }
++
++    // Try to turn this into a change.
++    Optional<Change.Id> changeId = tryExtractChange(url.getPath());
++    if (changeId.isPresent()) {
++      return titleFromChangeId(changeId.get());
++    }
++
++    return Optional.empty();
++  }
++
++  private static final Pattern extractChangeIdRegex = Pattern.compile("^/(?:c/.*/\\+/)?(?<changeId>[0-9]+)(?:/[0-9]+)?(?:/.*)?$");
++  private final Provider<ChangesCollection> changes;
++
++  private Optional<Change.Id> tryExtractChange(String path) {
++    Matcher m = extractChangeIdRegex.matcher(path);
++    if (!m.matches()) {
++      return Optional.empty();
++    }
++    return Change.Id.tryParse(m.group("changeId"));
++  }
++
++  private Optional<String> titleFromChangeId(Change.Id changeId) {
++    ChangesCollection changesCollection = changes.get();
++    try {
++      ChangeResource changeResource = changesCollection.parse(changeId);
++      return Optional.of(changeResource.getChange().getSubject());
++    } catch (ResourceConflictException | ResourceNotFoundException | PermissionBackendException e) {
++      return Optional.empty();
++    }
++  }
++}
+diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+index 11717fb8a4..1ae9046360 100644
+--- a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
++++ b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
+@@ -33,10 +33,12 @@
+   {@param? defaultDashboardHex: ?}
+   {@param? dashboardQuery: ?}
+   {@param? userIsAuthenticated: ?}
++  {@param? title: ?}
+   <!DOCTYPE html>{\n}
+   <html lang="en">{\n}
+   <meta charset="utf-8">{\n}
+-  <meta name="description" content="Gerrit Code Review">{\n}
++  {if $title}<title>{$title} · Gerrit Code Review</title>{\n}{/if}
++  <meta name="description" content="{if $title}{$title} · {/if}Gerrit Code Review">{\n}
+   <meta name="referrer" content="never">{\n}
+   <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
+ 
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch b/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
new file mode 100644
index 000000000000..b664ea0ea6b2
--- /dev/null
+++ b/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
@@ -0,0 +1,26 @@
+From bd7db44cabb6de64f03adbaf5e24c73e022a8932 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Sat, 11 Jul 2020 00:45:57 +0000
+Subject: [PATCH 5/7] When using local fonts, always assume Gerrit is mounted
+ at the root.
+
+---
+ polygerrit-ui/app/rollup.config.js | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/polygerrit-ui/app/rollup.config.js b/polygerrit-ui/app/rollup.config.js
+index d93b5eab39..c862c9bbae 100644
+--- a/polygerrit-ui/app/rollup.config.js
++++ b/polygerrit-ui/app/rollup.config.js
+@@ -50,7 +50,7 @@ const importLocalFontMetaUrlResolver = function() {
+     name: 'import-meta-url-resolver',
+     resolveImportMeta: function (property, data) {
+       if(property === 'url' && data.moduleId.endsWith('/@polymer/font-roboto-local/roboto.js')) {
+-        return 'new URL("..", document.baseURI).href';
++        return 'new URL("/", document.baseURI).href';
+       }
+       return null;
+     }
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/0006-Always-use-Google-Fonts.patch b/third_party/gerrit/0006-Always-use-Google-Fonts.patch
new file mode 100644
index 000000000000..5b817d0b55a3
--- /dev/null
+++ b/third_party/gerrit/0006-Always-use-Google-Fonts.patch
@@ -0,0 +1,28 @@
+From d71f51afe12a280b92831070a583b15c8b6bc2f4 Mon Sep 17 00:00:00 2001
+From: Luke Granger-Brown <git@lukegb.com>
+Date: Sat, 11 Jul 2020 00:46:13 +0000
+Subject: [PATCH 6/7] Always use Google Fonts.
+
+We're not a corporate, and we're not behind the GFW. Always use Google Fonts,
+because even though we no longer get the caching benefits (boo, browsers),
+it is still a better geographically-distributed CDN.
+---
+ java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java | 2 +-
+ 1 file changed, 1 insertion(+), 1 deletion(-)
+
+diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+index a9cfceb3b6..9c287c6e45 100644
+--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
++++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
+@@ -184,7 +184,7 @@ public class IndexHtmlUtil {
+     if (urlParameterMap.containsKey("ce")) {
+       data.put("polyfillCE", "true");
+     }
+-    if (urlParameterMap.containsKey("gf")) {
++    if (/* urlParameterMap.containsKey("gf") || */ true) {
+       data.put("useGoogleFonts", "true");
+     }
+ 
+-- 
+2.32.0
+
diff --git a/third_party/gerrit/default.nix b/third_party/gerrit/default.nix
new file mode 100644
index 000000000000..4873cf09b900
--- /dev/null
+++ b/third_party/gerrit/default.nix
@@ -0,0 +1,150 @@
+{ depot, pkgs, ... }:
+
+let
+  detzip = depot.nix.buildGo.program {
+    name = "detzip";
+    srcs = [ ./detzip.go ];
+  };
+  bazelRunScript = pkgs.writeShellScriptBin "bazel-run" ''
+    yarn config set cache-folder "$bazelOut/external/yarn_cache"
+    export HOME="$bazelOut/external/home"
+    mkdir -p "$bazelOut/external/home"
+    exec /bin/bazel "$@"
+  '';
+  bazelTop = pkgs.buildFHSUserEnv {
+    name = "bazel";
+    targetPkgs = pkgs: [
+      (pkgs.bazel.override { enableNixHacks = true; })
+      detzip
+      pkgs.jdk11_headless
+      pkgs.zlib
+      pkgs.python
+      pkgs.curl
+      pkgs.nodejs
+      pkgs.yarn
+      pkgs.git
+      bazelRunScript
+    ];
+    runScript = "/bin/bazel-run";
+  };
+  bazel = bazelTop // { override = x: bazelTop; };
+  version = "3.4.0";
+in
+pkgs.lib.makeOverridable pkgs.buildBazelPackage {
+  pname = "gerrit";
+  inherit version;
+
+  src = pkgs.fetchgit {
+    url = "https://gerrit.googlesource.com/gerrit";
+    rev = "471c1c15a7bc294d10e246df43812942b5ac8a13";
+    branchName = "v${version}";
+    sha256 = "sha256:0ayj0bcsxjln8qydkj9j7yiqibmjgd3bcpqvgsdzdx072wzx01c0";
+    fetchSubmodules = true;
+  };
+
+  patches = [
+    ./0001-Use-detzip-in-download_bower.py.patch
+    ./0002-Syntax-highlight-nix.patch
+    ./0003-Syntax-highlight-rules.pl.patch
+    ./0004-Add-titles-to-CLs-over-HTTP.patch
+    ./0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
+    ./0006-Always-use-Google-Fonts.patch
+  ];
+
+  bazelTarget = "release api-skip-javadoc";
+  inherit bazel;
+
+  bazelFlags = [
+    "--repository_cache="
+    "--disk_cache="
+  ];
+  removeRulesCC = false;
+  fetchConfigured = true;
+
+  fetchAttrs = {
+    sha256 = "sha256:1q4sclf18zzh8hsnccg1y7vqnhgavq62mqp4xx50zxfcnixfkpbx";
+    preBuild = ''
+      rm .bazelversion
+    '';
+
+    installPhase = ''
+      runHook preInstall
+
+      # Remove all built in external workspaces, Bazel will recreate them when building
+      rm -rf $bazelOut/external/{bazel_tools,\@bazel_tools.marker}
+      rm -rf $bazelOut/external/{embedded_jdk,\@embedded_jdk.marker}
+      rm -rf $bazelOut/external/{local_config_cc,\@local_config_cc.marker}
+      rm -rf $bazelOut/external/{local_*,\@local_*.marker}
+
+      # Clear markers
+      find $bazelOut/external -name '@*\.marker' -exec sh -c 'echo > {}' \;
+
+      # Remove all vcs files
+      rm -rf $(find $bazelOut/external -type d -name .git)
+      rm -rf $(find $bazelOut/external -type d -name .svn)
+      rm -rf $(find $bazelOut/external -type d -name .hg)
+
+      # Removing top-level symlinks along with their markers.
+      # This is needed because they sometimes point to temporary paths (?).
+      # For example, in Tensorflow-gpu build:
+      # platforms -> NIX_BUILD_TOP/tmp/install/35282f5123611afa742331368e9ae529/_embedded_binaries/platforms
+      find $bazelOut/external -maxdepth 1 -type l | while read symlink; do
+        name="$(basename "$symlink")"
+        rm -rf "$symlink" "$bazelOut/external/@$name.marker"
+      done
+
+      # Patching symlinks to remove build directory reference
+      find $bazelOut/external -type l | while read symlink; do
+        new_target="$(readlink "$symlink" | sed "s,$NIX_BUILD_TOP,NIX_BUILD_TOP,")"
+        rm "$symlink"
+        ln -sf "$new_target" "$symlink"
+      done
+
+      echo '${bazel.name}' > $bazelOut/external/.nix-bazel-version
+
+      # Gerrit fixups:
+      # Remove polymer-bridges and ba-linkify, they're in-repo
+      rm -rf $bazelOut/external/yarn_cache/v6/npm-polymer-bridges-*
+      rm -rf $bazelOut/external/yarn_cache/v6/npm-ba-linkify-*
+      # Normalize permissions on .yarn-{tarball,metadata} files
+      find $bazelOut/external/yarn_cache \( -name .yarn-tarball.tgz -or -name .yarn-metadata.json \) -exec chmod 644 {} +
+
+      (cd $bazelOut/ && tar czf $out --sort=name --mtime='@1' --owner=0 --group=0 --numeric-owner external/)
+
+      runHook postInstall
+    '';
+  };
+
+  buildAttrs = {
+    preConfigure = ''
+      rm .bazelversion
+    '';
+    installPhase = ''
+      mkdir -p "$out"/webapps/ "$out"/share/api/
+      cp bazel-bin/release.war "$out"/webapps/gerrit-${version}.war
+      unzip bazel-bin/api-skip-javadoc.zip -d "$out"/share/api
+    '';
+
+    nativeBuildInputs = with pkgs; [
+      unzip
+    ];
+  };
+
+  passthru = {
+    # A list of plugins that are part of the gerrit.war file.
+    # Use `java -jar gerrit.war ls | grep -Po '(?<=plugins/)[^.]+' | sed -e 's,^,",' -e 's,$,",' | sort` to generate that list.
+    plugins = [
+      "codemirror-editor"
+      "commit-message-length-validator"
+      "delete-project"
+      "download-commands"
+      "gitiles"
+      "hooks"
+      "plugin-manager"
+      "replication"
+      "reviewnotes"
+      "singleusergroup"
+      "webhooks"
+    ];
+  };
+}
diff --git a/third_party/gerrit/detzip.go b/third_party/gerrit/detzip.go
new file mode 100644
index 000000000000..511c18ecfe17
--- /dev/null
+++ b/third_party/gerrit/detzip.go
@@ -0,0 +1,97 @@
+package main
+
+import (
+	"archive/zip"
+	"flag"
+	"fmt"
+	"io"
+	"log"
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+)
+
+var (
+	exclude = flag.String("exclude", "", "comma-separated list of filenames to exclude (in any directory)")
+)
+
+func init() {
+	flag.Usage = func() {
+		fmt.Fprintf(flag.CommandLine.Output(), "Usage of %s [zip file] [directory]:\n", os.Args[0])
+		flag.PrintDefaults()
+	}
+}
+
+func listToMap(ss []string) map[string]bool {
+	m := make(map[string]bool)
+	for _, s := range ss {
+		m[s] = true
+	}
+	return m
+}
+
+func main() {
+	flag.Parse()
+	if flag.NArg() != 2 {
+		flag.Usage()
+		os.Exit(1)
+	}
+
+	outPath := flag.Arg(0)
+	dirPath := flag.Arg(1)
+
+	excludeFiles := listToMap(strings.Split(*exclude, ","))
+
+	// Aggregate all files first.
+	var files []string
+	filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
+		if err != nil {
+			return err
+		}
+		if info.IsDir() {
+			return nil
+		}
+		if excludeFiles[info.Name()] {
+			return nil
+		}
+		files = append(files, path)
+		return nil
+	})
+
+	// Create zip
+	outW, err := os.Create(outPath)
+	if err != nil {
+		log.Fatalf("Create(%q): %v", outPath, err)
+	}
+
+	zipW := zip.NewWriter(outW)
+
+	// Output files in alphabetical order
+	sort.Strings(files)
+	for _, f := range files {
+		fw, err := zipW.CreateHeader(&zip.FileHeader{
+			Name:   f,
+			Method: zip.Store,
+		})
+		if err != nil {
+			log.Fatalf("creating %q in zip: %v", f, err)
+		}
+
+		ff, err := os.Open(f)
+		if err != nil {
+			log.Fatalf("opening %q: %v", f, err)
+		}
+		if _, err := io.Copy(fw, ff); err != nil {
+			log.Fatalf("copying %q to zip: %v", f, err)
+		}
+		ff.Close()
+	}
+
+	if err := zipW.Close(); err != nil {
+		log.Fatalf("writing ZIP central directory: %v", err)
+	}
+	if err := outW.Close(); err != nil {
+		log.Fatalf("closing ZIP file: %v", err)
+	}
+}