diff --git a/common/base-server.nix b/common/base-server.nix index 498b9e6..b93a24d 100644 --- a/common/base-server.nix +++ b/common/base-server.nix @@ -1,4 +1,6 @@ { lib, pkgs, ... }: { + nixpkgs.overlays = import ../overlays; + nix.package = pkgs.lix; services.openssh.enable = lib.mkForce true; diff --git a/flake.nix b/flake.nix index 4623b9d..632745e 100644 --- a/flake.nix +++ b/flake.nix @@ -48,6 +48,17 @@ ./hosts/meta01.nixpkgs.lahfa.xyz ]; }; + + gerrit01 = { + imports = [ + inputs.agenix.nixosModules.default + inputs.hydra.nixosModules.hydra + + ./services + ./common + ./hosts/cl.nixpkgs.lahfa.xyz + ]; + }; }; }; } diff --git a/hosts/cl.nixpkgs.lahfa.xyz/default.nix b/hosts/cl.nixpkgs.lahfa.xyz/default.nix new file mode 100755 index 0000000..a0f9f85 --- /dev/null +++ b/hosts/cl.nixpkgs.lahfa.xyz/default.nix @@ -0,0 +1,45 @@ +{ + config, + lib, + pkgs, + ... +}: +{ + networking.hostName = "gerrit01"; + # TODO: make it the default + networking.domain = "infra.forkos.org"; + + time.timeZone = "Europe/Paris"; + + bagel.sysadmin.enable = true; + # Gerrit is proxied. + bagel.raito.v6-proxy-awareness.enable = true; + bagel.hardware.raito-vm = { + enable = true; + networking = { + nat-lan-mac = "bc:24:11:f7:29:6c"; + wan = { + address = "2001:bc8:38ee:100:1000::10/64"; + mac = "bc:24:11:4a:9d:32"; + }; + }; + }; + + fileSystems."/gerrit-data" = { + device = "/dev/disk/by-uuid/d1062305-0dea-4740-9a27-b6b1691862a4"; + fsType = "ext4"; + }; + + bagel.services.gerrit = { + enable = true; + domains = [ + "cl.forkos.org" + ]; + data = "/gerrit-data"; + }; + + i18n.defaultLocale = "fr_FR.UTF-8"; + + system.stateVersion = "24.05"; + deployment.targetHost = "gerrit01.infra.forkos.org"; +} diff --git a/overlays/default.nix b/overlays/default.nix new file mode 100644 index 0000000..1ccdeda --- /dev/null +++ b/overlays/default.nix @@ -0,0 +1,3 @@ +[ + (import ./gerrit.nix) +] diff --git a/overlays/gerrit.nix b/overlays/gerrit.nix new file mode 100644 index 0000000..f5d6176 --- /dev/null +++ b/overlays/gerrit.nix @@ -0,0 +1,11 @@ +self: super: { + buildGerrit = self.callPackage ../pkgs/gerrit { }; + gerrit = self.buildGerrit { }; + buildGerritBazelPlugin = self.callPackage ../pkgs/gerrit_plugins/builder.nix { + inherit (self) buildGerrit; + }; + gerritPlugins = { + code-owners = self.callPackage ../pkgs/gerrit_plugins/code-owners { }; + oauth = self.callPackage ../pkgs/gerrit_plugins/oauth { }; + }; +} diff --git a/pkgs/gerrit/0001-Syntax-highlight-nix.patch b/pkgs/gerrit/0001-Syntax-highlight-nix.patch new file mode 100644 index 0000000..bdc3fd3 --- /dev/null +++ b/pkgs/gerrit/0001-Syntax-highlight-nix.patch @@ -0,0 +1,37 @@ +From 084e4f92fb58f7cd85303ba602fb3c40133c8fcc Mon Sep 17 00:00:00 2001 +From: Luke Granger-Brown +Date: Thu, 2 Jul 2020 23:02:32 +0100 +Subject: [PATCH 1/3] Syntax highlight nix + +--- + .../app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts | 1 + + resources/com/google/gerrit/server/mime/mime-types.properties | 1 + + 2 files changed, 2 insertions(+) + +diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts +index a9f88bdd81..385249f280 100644 +--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts ++++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts +@@ -93,6 +93,7 @@ const LANGUAGE_MAP = new Map([ + ['text/x-vhdl', 'vhdl'], + ['text/x-yaml', 'yaml'], + ['text/vbscript', 'vbscript'], ++ ['text/x-nix', 'nix'], + ]); + + const CLASS_PREFIX = 'gr-diff gr-syntax gr-syntax-'; +diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties +index 2f9561ba2e..739818ec05 100644 +--- a/resources/com/google/gerrit/server/mime/mime-types.properties ++++ b/resources/com/google/gerrit/server/mime/mime-types.properties +@@ -149,6 +149,7 @@ mscin = text/x-mscgen + msgenny = text/x-msgenny + nb = text/x-mathematica + nginx.conf = text/x-nginx-conf ++nix = text/x-nix + nsh = text/x-nsis + nsi = text/x-nsis + nt = text/n-triples +-- +2.37.3 + diff --git a/pkgs/gerrit/0002-Syntax-highlight-rules.pl.patch b/pkgs/gerrit/0002-Syntax-highlight-rules.pl.patch new file mode 100644 index 0000000..4b91e2c --- /dev/null +++ b/pkgs/gerrit/0002-Syntax-highlight-rules.pl.patch @@ -0,0 +1,37 @@ +From aedf8ac8fa5113843bcd83ff85e2d9f3bffdb16c Mon Sep 17 00:00:00 2001 +From: Luke Granger-Brown +Date: Thu, 2 Jul 2020 23:02:43 +0100 +Subject: [PATCH 2/3] Syntax highlight rules.pl + +--- + .../app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts | 1 + + resources/com/google/gerrit/server/mime/mime-types.properties | 1 + + 2 files changed, 2 insertions(+) + +diff --git a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts +index 385249f280..7cb3068494 100644 +--- a/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts ++++ b/polygerrit-ui/app/embed/diff/gr-syntax-layer/gr-syntax-layer-worker.ts +@@ -68,6 +68,7 @@ const LANGUAGE_MAP = new Map([ + ['text/x-perl', 'perl'], + ['text/x-pgsql', 'pgsql'], // postgresql + ['text/x-php', 'php'], ++ ['text/x-prolog', 'prolog'], + ['text/x-properties', 'properties'], + ['text/x-protobuf', 'protobuf'], + ['text/x-puppet', 'puppet'], +diff --git a/resources/com/google/gerrit/server/mime/mime-types.properties b/resources/com/google/gerrit/server/mime/mime-types.properties +index 739818ec05..58eb727bf9 100644 +--- a/resources/com/google/gerrit/server/mime/mime-types.properties ++++ b/resources/com/google/gerrit/server/mime/mime-types.properties +@@ -200,6 +200,7 @@ rq = application/sparql-query + rs = text/x-rustsrc + rss = application/xml + rst = text/x-rst ++rules.pl = text/x-prolog + README.md = text/x-gfm + s = text/x-gas + sas = text/x-sas +-- +2.37.3 + diff --git a/pkgs/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch b/pkgs/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch new file mode 100644 index 0000000..c4edee3 --- /dev/null +++ b/pkgs/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch @@ -0,0 +1,215 @@ +From f49c50ca9a84ca374b7bd91c171bbea0457f2c7a Mon Sep 17 00:00:00 2001 +From: Luke Granger-Brown +Date: Thu, 2 Jul 2020 23:03:02 +0100 +Subject: [PATCH 3/3] 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 | 5 +- + .../gerrit/httpd/raw/TitleComputer.java | 67 +++++++++++++++++++ + .../gerrit/httpd/raw/PolyGerritIndexHtml.soy | 4 +- + 5 files changed, 89 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 72bfe40c3b..439bd73b44 100644 +--- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java ++++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java +@@ -41,6 +41,7 @@ import java.util.Collections; + import java.util.HashMap; + import java.util.HashSet; + import java.util.Map; ++import java.util.Optional; + import java.util.Set; + import java.util.function.Function; + +@@ -62,13 +63,14 @@ public class IndexHtmlUtil { + String faviconPath, + Map urlParameterMap, + Function urlInScriptTagOrdainer, +- String requestedURL) ++ String requestedURL, ++ TitleComputer titleComputer) + throws URISyntaxException, RestApiException { + ImmutableMap.Builder data = ImmutableMap.builder(); + data.putAll( + staticTemplateData( + canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer)) +- .putAll(dynamicTemplateData(gerritApi, requestedURL)); ++ .putAll(dynamicTemplateData(gerritApi, requestedURL, titleComputer)); + Set enabledExperiments = new HashSet<>(); + enabledExperiments.addAll(experimentFeatures.getEnabledExperimentFeatures()); + // Add all experiments enabled through url +@@ -81,7 +83,8 @@ public class IndexHtmlUtil { + + /** Returns dynamic parameters of {@code index.html}. */ + public static ImmutableMap dynamicTemplateData( +- GerritApi gerritApi, String requestedURL) throws RestApiException, URISyntaxException { ++ GerritApi gerritApi, String requestedURL, TitleComputer titleComputer) ++ throws RestApiException, URISyntaxException { + ImmutableMap.Builder data = ImmutableMap.builder(); + Map initialData = new HashMap<>(); + Server serverApi = gerritApi.config().server(); +@@ -129,6 +132,10 @@ public class IndexHtmlUtil { + } + + data.put("gerritInitialData", initialData); ++ ++ Optional title = titleComputer.computeTitle(requestedURL); ++ title.ifPresent(s -> data.put("title", s)); ++ + return data.build(); + } + +diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java +index fcb821e5ae..e1464b992b 100644 +--- a/java/com/google/gerrit/httpd/raw/IndexServlet.java ++++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java +@@ -48,13 +48,15 @@ public class IndexServlet extends HttpServlet { + private final ExperimentFeatures experimentFeatures; + private final SoySauce soySauce; + private final Function 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; +@@ -69,6 +71,7 @@ public class IndexServlet extends HttpServlet { + (s) -> + UnsafeSanitizedContentOrdainer.ordainAsSafe( + s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI); ++ this.titleComputer = titleComputer; + } + + @Override +@@ -86,7 +89,8 @@ public class IndexServlet extends HttpServlet { + faviconPath, + parameterMap, + urlOrdainer, +- getRequestUrl(req)); ++ getRequestUrl(req), ++ 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 15dcf42e0e..9f56bf33ce 100644 +--- a/java/com/google/gerrit/httpd/raw/StaticModule.java ++++ b/java/com/google/gerrit/httpd/raw/StaticModule.java +@@ -241,10 +241,11 @@ public class StaticModule extends ServletModule { + @CanonicalWebUrl @Nullable String canonicalUrl, + @GerritServerConfig Config cfg, + GerritApi gerritApi, +- ExperimentFeatures experimentFeatures) { ++ ExperimentFeatures experimentFeatures, ++ TitleComputer titleComputer) { + String cdnPath = options.devCdn().orElse(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 changes) { ++ this.changes = changes; ++ } ++ ++ public Optional 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 changeId = tryExtractChange(url.getPath()); ++ if (changeId.isPresent()) { ++ return titleFromChangeId(changeId.get()); ++ } ++ ++ return Optional.empty(); ++ } ++ ++ private static final Pattern extractChangeIdRegex = Pattern.compile("^/(?:c/.*/\\+/)?(?[0-9]+)(?:/[0-9]+)?(?:/.*)?$"); ++ private final Provider changes; ++ ++ private Optional tryExtractChange(String path) { ++ Matcher m = extractChangeIdRegex.matcher(path); ++ if (!m.matches()) { ++ return Optional.empty(); ++ } ++ return Change.Id.tryParse(m.group("changeId")); ++ } ++ ++ private Optional 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 dbfef44dfe..347ee75aab 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: ?} + {\n} + {\n} + {\n} +- {\n} ++ {if $title}{$title} · Gerrit Code Review{\n}{/if} ++ {\n} + {\n} + {\n} + +-- +2.37.3 + diff --git a/pkgs/gerrit/default.nix b/pkgs/gerrit/default.nix new file mode 100644 index 0000000..3fc9862 --- /dev/null +++ b/pkgs/gerrit/default.nix @@ -0,0 +1,152 @@ +{ buildFHSUserEnv, writeShellScriptBin, buildBazelPackage, fetchgit, unzip }: +{ name ? "gerrit-${version}", version ? "3.9.1", src ? (fetchgit { + url = "https://gerrit.googlesource.com/gerrit"; + rev = "620a819cbf3c64fff7a66798822775ad42c91d8e"; + branchName = "v${version}"; + sha256 = "sha256:1mdxbgnx3mpxand4wq96ic38bb4yh45q271h40jrk7dk23sgmz02"; + fetchSubmodules = true; + }), bazelTargets ? [ "release" "api-skip-javadoc" ] +}: +let + bazelRunScript = 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 = buildFHSUserEnv { + name = "bazel"; + targetPkgs = pkgs: [ + (pkgs.bazel_5.override { enableNixHacks = true; }) + pkgs.jdk17_headless + pkgs.zlib + pkgs.python3 + pkgs.curl + pkgs.nodejs + pkgs.yarn + pkgs.git + bazelRunScript + ]; + runScript = "/bin/bazel-run"; + }; + bazel = bazelTop // { override = x: bazelTop; }; +in +buildBazelPackage { + inherit name version src; + + patches = [ + ./0001-Syntax-highlight-nix.patch + ./0002-Syntax-highlight-rules.pl.patch + ./0003-Add-titles-to-CLs-over-HTTP.patch + ]; + + inherit bazel bazelTargets; + + bazelFlags = [ + "--repository_cache=" + "--disk_cache=" + ]; + + removeRulesCC = false; + fetchConfigured = true; + + fetchAttrs = { + sha256 = "sha256-rsYQR6/RO5NM3/fnB3lEmbz876B59QWxWpE3M/Z4rK4="; + 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: + #sha256:06bmzbcb9717s4b016kcbn8nr9pgaz04i8bnzg7ybkbdwpl8vxvv"; 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: + # Normalize permissions on .yarn-{tarball,metadata} files + test -d $bazelOut/external/yarn_cache && find $bazelOut/external/yarn_cache \( -name .yarn-tarball.tgz -or -name .yarn-metadata.json \) -exec chmod 644 {} + + + mkdir $bazelOut/_bits/ + find . -name node_modules -prune -print | while read d; do + echo "$d" "$(dirname $d)" + mkdir -p $bazelOut/_bits/$(dirname $d) + cp -R "$d" "$bazelOut/_bits/$(dirname $d)/node_modules" + done + + (cd $bazelOut/ && tar czf $out --sort=name --mtime='@1' --owner=0 --group=0 --numeric-owner external/ _bits/) + + runHook postInstall + ''; + }; + + buildAttrs = { + preConfigure = '' + rm .bazelversion + + [ "$(ls -A $bazelOut/_bits)" ] && cp -R $bazelOut/_bits/* ./ || true + ''; + postPatch = '' + # Disable all errorprone checks, since we might be using a different version. + sed -i \ + -e '/-Xep:/d' \ + -e '/-XepExcludedPaths:/a "-XepDisableAllChecks",' \ + tools/BUILD + ''; + 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 = [ + 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/pkgs/gerrit/detzip.go b/pkgs/gerrit/detzip.go new file mode 100644 index 0000000..511c18e --- /dev/null +++ b/pkgs/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) + } +} diff --git a/pkgs/gerrit_plugins/builder.nix b/pkgs/gerrit_plugins/builder.nix new file mode 100644 index 0000000..299b208 --- /dev/null +++ b/pkgs/gerrit_plugins/builder.nix @@ -0,0 +1,36 @@ +{ buildGerrit, gerrit, runCommandLocal, lib }: + { name + , src + , depsOutputHash + , overlayPluginCmd ? '' + cp -R "${src}" "$out/plugins/${name}" + '' + , postPatch ? "" + , patches ? [ ] +}: (buildGerrit { + name = "${name}.jar"; + + src = runCommandLocal "${name}-src" { } '' + cp -R "${gerrit.src}" "$out" + chmod +w "$out/plugins" + ${overlayPluginCmd} + ''; + + bazelTargets = [ "//plugins/${name}" ]; +}).overrideAttrs (super: { + deps = super.deps.overrideAttrs (superDeps: { + outputHash = depsOutputHash; + }); + installPhase = '' + cp "bazel-bin/plugins/${name}/${name}.jar" "$out" + ''; + postPatch = '' + ${super.postPatch or ""} + pushd "plugins/${name}" + ${lib.concatMapStringsSep "\n" (patch: '' + patch -p1 < ${patch} + '') patches} + popd + ${postPatch} + ''; + }) diff --git a/pkgs/gerrit_plugins/code-owners/default.nix b/pkgs/gerrit_plugins/code-owners/default.nix new file mode 100644 index 0000000..a54131c --- /dev/null +++ b/pkgs/gerrit_plugins/code-owners/default.nix @@ -0,0 +1,14 @@ +{ fetchgit, buildGerritBazelPlugin, lib }: + +buildGerritBazelPlugin { + name = "code-owners"; + depsOutputHash = "sha256-Ee2n7R/vi91drR+dNYB0QnGiiqcmz9/pynHhV9yDxdE="; + src = fetchgit { + url = "https://gerrit.googlesource.com/plugins/code-owners"; + rev = "e654ae5bda2085bce9a99942bec440e004a114f3"; + sha256 = "sha256:14d3x3iqskgw16pvyaa0swh252agj84p9pzlf24l8lgx9d7y4biz"; + }; + patches = [ + ./using-usernames.patch + ]; +} diff --git a/pkgs/gerrit_plugins/code-owners/using-usernames.patch b/pkgs/gerrit_plugins/code-owners/using-usernames.patch new file mode 100644 index 0000000..25079ae --- /dev/null +++ b/pkgs/gerrit_plugins/code-owners/using-usernames.patch @@ -0,0 +1,472 @@ +commit 29ace6c38ac513f7ec56ca425230d5712c081043 +Author: Luke Granger-Brown +Date: Wed Sep 21 03:15:38 2022 +0100 + + Add support for usernames and groups + + Change-Id: I3ba8527f66216d08e555a6ac4451fe0d1e090de5 + +diff --git a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java +index 70009591..6dc596c9 100644 +--- a/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java ++++ b/java/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolver.java +@@ -17,6 +17,8 @@ package com.google.gerrit.plugins.codeowners.backend; + import static com.google.common.base.Preconditions.checkState; + import static com.google.common.collect.ImmutableMap.toImmutableMap; + import static com.google.common.collect.ImmutableSet.toImmutableSet; ++import static com.google.common.collect.ImmutableSetMultimap.flatteningToImmutableSetMultimap; ++import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap; + import static com.google.gerrit.plugins.codeowners.backend.CodeOwnersInternalServerErrorException.newInternalServerError; + import static java.util.Objects.requireNonNull; + +@@ -25,6 +27,7 @@ import com.google.common.collect.ImmutableList; + import com.google.common.collect.ImmutableMap; + import com.google.common.collect.ImmutableMultimap; + import com.google.common.collect.ImmutableSet; ++import com.google.common.collect.ImmutableSetMultimap; + import com.google.common.collect.Iterables; + import com.google.common.collect.Streams; + import com.google.common.flogger.FluentLogger; +@@ -33,17 +36,24 @@ import com.google.gerrit.entities.Project; + import com.google.gerrit.metrics.Timer0; + import com.google.gerrit.plugins.codeowners.backend.config.CodeOwnersPluginConfiguration; + import com.google.gerrit.plugins.codeowners.metrics.CodeOwnerMetrics; ++import com.google.gerrit.server.AnonymousUser; + import com.google.gerrit.server.CurrentUser; + import com.google.gerrit.server.IdentifiedUser; + import com.google.gerrit.server.account.AccountCache; + import com.google.gerrit.server.account.AccountControl; + import com.google.gerrit.server.account.AccountState; ++import com.google.gerrit.server.account.GroupBackend; ++import com.google.gerrit.server.account.GroupBackends; ++import com.google.gerrit.server.account.InternalGroupBackend; + import com.google.gerrit.server.account.externalids.ExternalId; + import com.google.gerrit.server.account.externalids.ExternalIdCache; + import com.google.gerrit.server.permissions.GlobalPermission; + import com.google.gerrit.server.permissions.PermissionBackend; + import com.google.gerrit.server.permissions.PermissionBackendException; ++import com.google.gerrit.server.util.RequestContext; ++import com.google.gerrit.server.util.ThreadLocalRequestContext; + import com.google.inject.Inject; ++import com.google.inject.OutOfScopeException; + import com.google.inject.Provider; + import java.io.IOException; + import java.nio.file.Path; +@@ -102,6 +112,8 @@ public class CodeOwnerResolver { + + @VisibleForTesting public static final String ALL_USERS_WILDCARD = "*"; + ++ public static final String GROUP_PREFIX = "group:"; ++ + private final CodeOwnersPluginConfiguration codeOwnersPluginConfiguration; + private final PermissionBackend permissionBackend; + private final Provider currentUser; +@@ -112,6 +124,8 @@ public class CodeOwnerResolver { + private final CodeOwnerMetrics codeOwnerMetrics; + private final UnresolvedImportFormatter unresolvedImportFormatter; + private final TransientCodeOwnerCache transientCodeOwnerCache; ++ private final InternalGroupBackend groupBackend; ++ private final ThreadLocalRequestContext context; + + // Enforce visibility by default. + private boolean enforceVisibility = true; +@@ -132,7 +146,9 @@ public class CodeOwnerResolver { + PathCodeOwners.Factory pathCodeOwnersFactory, + CodeOwnerMetrics codeOwnerMetrics, + UnresolvedImportFormatter unresolvedImportFormatter, +- TransientCodeOwnerCache transientCodeOwnerCache) { ++ TransientCodeOwnerCache transientCodeOwnerCache, ++ InternalGroupBackend groupBackend, ++ ThreadLocalRequestContext context) { + this.codeOwnersPluginConfiguration = codeOwnersPluginConfiguration; + this.permissionBackend = permissionBackend; + this.currentUser = currentUser; +@@ -143,6 +159,8 @@ public class CodeOwnerResolver { + this.codeOwnerMetrics = codeOwnerMetrics; + this.unresolvedImportFormatter = unresolvedImportFormatter; + this.transientCodeOwnerCache = transientCodeOwnerCache; ++ this.groupBackend = groupBackend; ++ this.context = context; + } + + /** +@@ -361,6 +379,12 @@ public class CodeOwnerResolver { + "cannot resolve code owner email %s: no account with this email exists", + CodeOwnerResolver.ALL_USERS_WILDCARD)); + } ++ if (codeOwnerReference.email().startsWith(GROUP_PREFIX)) { ++ return OptionalResultWithMessages.createEmpty( ++ String.format( ++ "cannot resolve code owner email %s: this is a group", ++ codeOwnerReference.email())); ++ } + + ImmutableList.Builder messageBuilder = ImmutableList.builder(); + AtomicBoolean ownedByAllUsers = new AtomicBoolean(false); +@@ -405,9 +429,53 @@ public class CodeOwnerResolver { + ImmutableMultimap annotations) { + requireNonNull(codeOwnerReferences, "codeOwnerReferences"); + ++ ImmutableSet groupsToResolve = ++ codeOwnerReferences.stream() ++ .map(CodeOwnerReference::email) ++ .filter(ref -> ref.startsWith(GROUP_PREFIX)) ++ .map(ref -> ref.substring(GROUP_PREFIX.length())) ++ .collect(toImmutableSet()); ++ ++ // When we call GroupBackends.findExactSuggestion we need to ensure that we ++ // have a user in context. This is because the suggestion backend is ++ // likely to want to try to check that we can actually see the group it's ++ // returning (which we also check for explicitly, because I have trust ++ // issues). ++ RequestContext oldCtx = context.getContext(); ++ // Check if we have a user in the context at all... ++ try { ++ oldCtx.getUser(); ++ } catch (OutOfScopeException | NullPointerException e) { ++ // Nope. ++ RequestContext newCtx = () -> { ++ return new AnonymousUser(); ++ }; ++ context.setContext(newCtx); ++ } ++ ImmutableSetMultimap resolvedGroups = null; ++ try { ++ resolvedGroups = ++ groupsToResolve.stream() ++ .map(groupName -> GroupBackends.findExactSuggestion(groupBackend, groupName)) ++ .filter(groupRef -> groupRef != null) ++ .filter(groupRef -> groupBackend.isVisibleToAll(groupRef.getUUID())) ++ .map(groupRef -> groupBackend.get(groupRef.getUUID())) ++ .collect(flatteningToImmutableSetMultimap( ++ groupRef -> GROUP_PREFIX + groupRef.getName(), ++ groupRef -> accountCache ++ .get(ImmutableSet.copyOf(groupRef.getMembers())) ++ .values().stream() ++ .map(accountState -> CodeOwner.create(accountState.account().id())))); ++ } finally { ++ context.setContext(oldCtx); ++ } ++ ImmutableSetMultimap usersToGroups = ++ resolvedGroups.inverse(); ++ + ImmutableSet emailsToResolve = + codeOwnerReferences.stream() + .map(CodeOwnerReference::email) ++ .filter(ref -> !ref.startsWith(GROUP_PREFIX)) + .filter(filterOutAllUsersWildCard(ownedByAllUsers)) + .collect(toImmutableSet()); + +@@ -442,7 +510,8 @@ public class CodeOwnerResolver { + ImmutableMap codeOwnersByEmail = + accountsByEmail.map(mapToCodeOwner()).collect(toImmutableMap(Pair::key, Pair::value)); + +- if (codeOwnersByEmail.keySet().size() < emailsToResolve.size()) { ++ if (codeOwnersByEmail.keySet().size() < emailsToResolve.size() || ++ resolvedGroups.keySet().size() < groupsToResolve.size()) { + hasUnresolvedCodeOwners.set(true); + } + +@@ -456,7 +525,9 @@ public class CodeOwnerResolver { + cachedCodeOwnersByEmail.entrySet().stream() + .filter(e -> e.getValue().isPresent()) + .map(e -> Pair.of(e.getKey(), e.getValue().get())); +- Streams.concat(newlyResolvedCodeOwnersStream, cachedCodeOwnersStream) ++ Stream> resolvedGroupsCodeOwnersStream = ++ resolvedGroups.entries().stream().map(e -> Pair.of(e.getKey(), e.getValue())); ++ Streams.concat(Streams.concat(newlyResolvedCodeOwnersStream, cachedCodeOwnersStream), resolvedGroupsCodeOwnersStream) + .forEach( + p -> { + ImmutableSet.Builder annotationBuilder = ImmutableSet.builder(); +@@ -467,6 +538,12 @@ public class CodeOwnerResolver { + annotationBuilder.addAll( + annotations.get(CodeOwnerReference.create(ALL_USERS_WILDCARD))); + ++ // annotations for the groups this user is in apply as well ++ for (String group : usersToGroups.get(p.value())) { ++ annotationBuilder.addAll( ++ annotations.get(CodeOwnerReference.create(group))); ++ } ++ + if (!codeOwnersWithAnnotations.containsKey(p.value())) { + codeOwnersWithAnnotations.put(p.value(), new HashSet<>()); + } +@@ -570,7 +647,7 @@ public class CodeOwnerResolver { + } + + messages.add(String.format("email %s has no domain", email)); +- return false; ++ return true; // TVL: we allow domain-less strings which are treated as usernames. + } + + /** +@@ -585,11 +662,29 @@ public class CodeOwnerResolver { + */ + private ImmutableMap> lookupExternalIds( + ImmutableList.Builder messages, ImmutableSet emails) { ++ String[] actualEmails = emails.stream() ++ .filter(email -> email.contains("@")) ++ .toArray(String[]::new); ++ ImmutableSet usernames = emails.stream() ++ .filter(email -> !email.contains("@")) ++ .collect(ImmutableSet.toImmutableSet()); + try { +- ImmutableMap> extIdsByEmail = +- externalIdCache.byEmails(emails.toArray(new String[0])).asMap(); ++ ImmutableMap> extIds = ++ new ImmutableMap.Builder>() ++ .putAll(externalIdCache.byEmails(actualEmails).asMap()) ++ .putAll(externalIdCache.allByAccount().entries().stream() ++ .map(entry -> entry.getValue()) ++ .filter(externalId -> ++ externalId.key().scheme() != null && ++ externalId.key().isScheme(ExternalId.SCHEME_USERNAME) && ++ usernames.contains(externalId.key().id())) ++ .collect(toImmutableSetMultimap( ++ externalId -> externalId.key().id(), ++ externalId -> externalId)) ++ .asMap()) ++ .build(); + emails.stream() +- .filter(email -> !extIdsByEmail.containsKey(email)) ++ .filter(email -> !extIds.containsKey(email)) + .forEach( + email -> { + transientCodeOwnerCache.cacheNonResolvable(email); +@@ -598,7 +693,7 @@ public class CodeOwnerResolver { + "cannot resolve code owner email %s: no account with this email exists", + email)); + }); +- return extIdsByEmail; ++ return extIds; + } catch (IOException e) { + throw newInternalServerError( + String.format("cannot resolve code owner emails: %s", emails), e); +@@ -815,6 +910,15 @@ public class CodeOwnerResolver { + user != null ? user.getLoggableName() : currentUser.get().getLoggableName())); + return true; + } ++ if (!email.contains("@")) { ++ // the email is the username of the account, or a group, or something else. ++ messages.add( ++ String.format( ++ "account %s is visible to user %s", ++ accountState.account().id(), ++ user != null ? user.getLoggableName() : currentUser.get().getLoggableName())); ++ return true; ++ } + + if (user != null) { + if (user.hasEmailAddress(email)) { +diff --git a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java +index 5f350998..7977ba55 100644 +--- a/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java ++++ b/java/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParser.java +@@ -149,7 +149,8 @@ public class FindOwnersCodeOwnerConfigParser implements CodeOwnerConfigParser { + private static final String EOL = "[\\s]*(#.*)?$"; // end-of-line + private static final String GLOB = "[^\\s,=]+"; // a file glob + +- private static final String EMAIL_OR_STAR = "([^\\s<>@,]+@[^\\s<>@#,]+|\\*)"; ++ // Also allows usernames, and group:$GROUP_NAME. ++ private static final String EMAIL_OR_STAR = "([^\\s<>@,]+@[^\\s<>@#,]+?|\\*|[a-zA-Z0-9_\\-]+|group:[a-zA-Z0-9_\\-]+)"; + private static final String EMAIL_LIST = + "(" + EMAIL_OR_STAR + "(" + COMMA + EMAIL_OR_STAR + ")*)"; + +diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java +index 7ec92959..59cf7e05 100644 +--- a/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java ++++ b/javatests/com/google/gerrit/plugins/codeowners/backend/AbstractFileBasedCodeOwnerBackendTest.java +@@ -424,7 +424,7 @@ public abstract class AbstractFileBasedCodeOwnerBackendTest extends AbstractCode + .commit() + .parent(head) + .message("Add invalid test code owner config") +- .add(JgitPath.of(codeOwnerConfigKey.filePath(getFileName())).get(), "INVALID")); ++ .add(JgitPath.of(codeOwnerConfigKey.filePath(getFileName())).get(), "INVALID!")); + } + + // Try to update the code owner config. +diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java +index 6171aca9..37699012 100644 +--- a/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java ++++ b/javatests/com/google/gerrit/plugins/codeowners/backend/CodeOwnerResolverTest.java +@@ -24,8 +24,10 @@ import com.google.gerrit.acceptance.TestAccount; + import com.google.gerrit.acceptance.TestMetricMaker; + import com.google.gerrit.acceptance.config.GerritConfig; + import com.google.gerrit.acceptance.testsuite.account.AccountOperations; ++import com.google.gerrit.acceptance.testsuite.group.GroupOperations; + import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; + import com.google.gerrit.entities.Account; ++import com.google.gerrit.entities.AccountGroup; + import com.google.gerrit.plugins.codeowners.acceptance.AbstractCodeOwnersTest; + import com.google.gerrit.server.ServerInitiated; + import com.google.gerrit.server.account.AccountsUpdate; +@@ -51,6 +53,7 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest { + @Inject private RequestScopeOperations requestScopeOperations; + @Inject @ServerInitiated private Provider accountsUpdate; + @Inject private AccountOperations accountOperations; ++ @Inject private GroupOperations groupOperations; + @Inject private ExternalIdNotes.Factory externalIdNotesFactory; + @Inject private TestMetricMaker testMetricMaker; + @Inject private ExternalIdFactory externalIdFactory; +@@ -112,6 +115,18 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest { + .contains(String.format("account %s is visible to user %s", admin.id(), admin.username())); + } + ++ @Test ++ public void resolveCodeOwnerReferenceForUsername() throws Exception { ++ OptionalResultWithMessages result = ++ codeOwnerResolverProvider ++ .get() ++ .resolveWithMessages(CodeOwnerReference.create(admin.username())); ++ assertThat(result.get()).hasAccountIdThat().isEqualTo(admin.id()); ++ assertThat(result) ++ .hasMessagesThat() ++ .contains(String.format("account %s is visible to user %s", admin.id(), admin.username())); ++ } ++ + @Test + public void cannotResolveCodeOwnerReferenceForStarAsEmail() throws Exception { + OptionalResultWithMessages result = +@@ -127,6 +142,18 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest { + CodeOwnerResolver.ALL_USERS_WILDCARD)); + } + ++ @Test ++ public void cannotResolveCodeOwnerReferenceForGroup() throws Exception { ++ OptionalResultWithMessages result = ++ codeOwnerResolverProvider ++ .get() ++ .resolveWithMessages(CodeOwnerReference.create("group:Administrators")); ++ assertThat(result).isEmpty(); ++ assertThat(result) ++ .hasMessagesThat() ++ .contains("cannot resolve code owner email group:Administrators: this is a group"); ++ } ++ + @Test + public void resolveCodeOwnerReferenceForAmbiguousEmailIfOtherAccountIsInactive() + throws Exception { +@@ -397,6 +424,64 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest { + assertThat(result.hasUnresolvedCodeOwners()).isFalse(); + } + ++ @Test ++ public void resolvePathCodeOwnersWhenNonVisibleGroupIsUsed() throws Exception { ++ CodeOwnerConfig codeOwnerConfig = ++ CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION) ++ .addCodeOwnerSet( ++ CodeOwnerSet.createWithoutPathExpressions("group:Administrators")) ++ .build(); ++ ++ CodeOwnerResolverResult result = ++ codeOwnerResolverProvider ++ .get() ++ .resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md")); ++ assertThat(result.codeOwnersAccountIds()).isEmpty(); ++ assertThat(result.ownedByAllUsers()).isFalse(); ++ assertThat(result.hasUnresolvedCodeOwners()).isTrue(); ++ } ++ ++ @Test ++ public void resolvePathCodeOwnersWhenVisibleGroupIsUsed() throws Exception { ++ AccountGroup.UUID createdGroupUUID = groupOperations ++ .newGroup() ++ .name("VisibleGroup") ++ .visibleToAll(true) ++ .addMember(admin.id()) ++ .create(); ++ ++ CodeOwnerConfig codeOwnerConfig = ++ CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION) ++ .addCodeOwnerSet( ++ CodeOwnerSet.createWithoutPathExpressions("group:VisibleGroup")) ++ .build(); ++ ++ CodeOwnerResolverResult result = ++ codeOwnerResolverProvider ++ .get() ++ .resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md")); ++ assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id()); ++ assertThat(result.ownedByAllUsers()).isFalse(); ++ assertThat(result.hasUnresolvedCodeOwners()).isFalse(); ++ } ++ ++ @Test ++ public void resolvePathCodeOwnersWhenUsernameIsUsed() throws Exception { ++ CodeOwnerConfig codeOwnerConfig = ++ CodeOwnerConfig.builder(CodeOwnerConfig.Key.create(project, "master", "/"), TEST_REVISION) ++ .addCodeOwnerSet( ++ CodeOwnerSet.createWithoutPathExpressions(admin.username())) ++ .build(); ++ ++ CodeOwnerResolverResult result = ++ codeOwnerResolverProvider ++ .get() ++ .resolvePathCodeOwners(codeOwnerConfig, Paths.get("/README.md")); ++ assertThat(result.codeOwnersAccountIds()).containsExactly(admin.id()); ++ assertThat(result.ownedByAllUsers()).isFalse(); ++ assertThat(result.hasUnresolvedCodeOwners()).isFalse(); ++ } ++ + @Test + public void resolvePathCodeOwnersNonResolvableCodeOwnersAreFilteredOut() throws Exception { + CodeOwnerConfig codeOwnerConfig = +@@ -655,7 +740,7 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest { + "domain example.com of email foo@example.org@example.com is allowed"); + assertIsEmailDomainAllowed( + "foo@example.org", false, "domain example.org of email foo@example.org is not allowed"); +- assertIsEmailDomainAllowed("foo", false, "email foo has no domain"); ++ assertIsEmailDomainAllowed("foo", true, "email foo has no domain"); + assertIsEmailDomainAllowed( + "foo@example.com@example.org", + false, +diff --git a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java +index 260e635e..7aab99d0 100644 +--- a/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java ++++ b/javatests/com/google/gerrit/plugins/codeowners/backend/findowners/FindOwnersCodeOwnerConfigParserTest.java +@@ -158,16 +158,42 @@ public class FindOwnersCodeOwnerConfigParserTest extends AbstractCodeOwnerConfig + codeOwnerConfigParser.parse( + TEST_REVISION, + CodeOwnerConfig.Key.create(project, "master", "/"), +- getCodeOwnerConfig(EMAIL_1, "INVALID", "NOT_AN_EMAIL", EMAIL_2))); ++ getCodeOwnerConfig(EMAIL_1, "INVALID!", "NOT!AN_EMAIL", EMAIL_2))); + assertThat(exception.getFullMessage(FindOwnersBackend.CODE_OWNER_CONFIG_FILE_NAME)) + .isEqualTo( + String.format( + "invalid code owner config file '/OWNERS' (project = %s, branch = master):\n" +- + " invalid line: INVALID\n" +- + " invalid line: NOT_AN_EMAIL", ++ + " invalid line: INVALID!\n" ++ + " invalid line: NOT!AN_EMAIL", + project)); + } + ++ @Test ++ public void codeOwnerConfigWithUsernames() throws Exception { ++ assertParseAndFormat( ++ getCodeOwnerConfig(EMAIL_1, "USERNAME", EMAIL_2), ++ codeOwnerConfig -> ++ assertThat(codeOwnerConfig) ++ .hasCodeOwnerSetsThat() ++ .onlyElement() ++ .hasCodeOwnersEmailsThat() ++ .containsExactly(EMAIL_1, "USERNAME", EMAIL_2), ++ getCodeOwnerConfig(EMAIL_1, "USERNAME", EMAIL_2)); ++ } ++ ++ @Test ++ public void codeOwnerConfigWithGroups() throws Exception { ++ assertParseAndFormat( ++ getCodeOwnerConfig(EMAIL_1, "group:tvl-employees", EMAIL_2), ++ codeOwnerConfig -> ++ assertThat(codeOwnerConfig) ++ .hasCodeOwnerSetsThat() ++ .onlyElement() ++ .hasCodeOwnersEmailsThat() ++ .containsExactly(EMAIL_1, "group:tvl-employees", EMAIL_2), ++ getCodeOwnerConfig(EMAIL_1, "group:tvl-employees", EMAIL_2)); ++ } ++ + @Test + public void codeOwnerConfigWithComment() throws Exception { + assertParseAndFormat( diff --git a/pkgs/gerrit_plugins/oauth/default.nix b/pkgs/gerrit_plugins/oauth/default.nix new file mode 100644 index 0000000..a52b1d5 --- /dev/null +++ b/pkgs/gerrit_plugins/oauth/default.nix @@ -0,0 +1,16 @@ +{ buildGerritBazelPlugin, fetchgit }: + +buildGerritBazelPlugin rec { + name = "oauth"; + depsOutputHash = "sha256-4/+E0BwkA+rYYCy7y3G9xF86DJj+CFzPZUNXC5HN5wc="; + src = fetchgit { + url = "https://gerrit.googlesource.com/plugins/oauth"; + rev = "b27cf3ea820eec2ddd22d217fc839261692ccdb0"; + sha256 = "1m654ibgzprrhcl0wpzqrmq8drpgx6rzlw0ha16l1fi2zv5idkk2"; + }; + overlayPluginCmd = '' + chmod +w "$out" "$out/plugins/external_plugin_deps.bzl" + cp -R "${src}" "$out/plugins/${name}" + cp "${src}/external_plugin_deps.bzl" "$out/plugins/external_plugin_deps.bzl" + ''; +} diff --git a/services/default.nix b/services/default.nix index 3a44ef3..1e4488a 100644 --- a/services/default.nix +++ b/services/default.nix @@ -3,5 +3,6 @@ ./hydra ./postgres ./netbox + ./gerrit ]; } diff --git a/services/gerrit/default.nix b/services/gerrit/default.nix new file mode 100644 index 0000000..e314710 --- /dev/null +++ b/services/gerrit/default.nix @@ -0,0 +1,185 @@ +# Gerrit configuration for the Nixpkgs monorepo +# Inspired from TVL configuration. +{ pkgs, config, lib, ... }: + +let + inherit (lib) mkEnableOption mkIf mkOption types; + cfgGerrit = config.services.gerrit; + cfg = config.bagel.services.gerrit; +in +{ + options.bagel.services.gerrit = { + enable = mkEnableOption "Gerrit"; + domains = mkOption { + type = types.listOf types.str; + description = "List of domains that Gerrit will answer to"; + }; + data = mkOption { + type = types.path; + default = "/var/lib/gerrit"; + description = "Root of data directory for the Gerrit"; + }; + }; + + imports = [ + ./www.nix + ]; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = [ 29418 ]; + + fileSystems."/var/lib/gerrit" = mkIf (cfg.data != "/var/lib/gerrit") { + device = cfg.data; + options = [ "bind" ]; + }; + + users.users.git = { + isSystemUser = true; + group = "git"; + }; + users.groups.git = {}; + + services.gerrit = { + enable = true; + listenAddress = "[::]:4778"; # 4778 - grrt + serverId = "9e5216ad-038d-4d74-a4e8-716515834a94"; + + builtinPlugins = [ + "gitiles" + "codemirror-editor" + "reviewnotes" + "download-commands" + "hooks" + "replication" + "webhooks" + ]; + + plugins = with pkgs.gerritPlugins; [ + oauth + ]; + + package = pkgs.gerrit; + + jvmHeapLimit = "32g"; + + # In some NixOS channel bump, the default version of OpenJDK has + # changed to one that is incompatible with our current version of + # Gerrit. + # + # TODO(tazjin): Update Gerrit and remove this when possible. + jvmPackage = pkgs.openjdk17_headless; + + settings = { + sshd.threads = 64; + sshd.batchThreads = 8; + gc.interval = "1 day"; + database.poolLimit = "250"; + database.poolMaxIdle = 16; + http.maxThreads = 100; + # core.packedGitLimit = "4g"; + # core.packedGitWindowSize = "16k"; + # core.packedGitOpenFiles = "4096"; + receive.timeout = "4min"; + # pack.threads = "8"; + log.jsonLogging = true; + log.textLogging = false; + sshd.advertisedAddress = "cl.nixpkgs.lahfa.xyz:29418"; + cache.web_sessions.maxAge = "3 months"; + plugins.allowRemoteAdmin = false; + change.enableAttentionSet = true; + change.enableAssignee = false; + + # Configures gerrit for being reverse-proxied by nginx as per + # https://gerrit-review.googlesource.com/Documentation/config-reverseproxy.html + gerrit = { + canonicalWebUrl = "https://cl.nixpkgs.lahfa.xyz"; + docUrl = "/Documentation"; + }; + + httpd.listenUrl = "proxy-https://${cfgGerrit.listenAddress}"; + + download.command = [ + "checkout" + "cherry_pick" + "format_patch" + "pull" + ]; + + # Auto-link other CLs + commentlink.gerrit = { + match = "cl/(\\d+)"; + link = "https://cl.nixpkgs.lahfa.xyz/$1"; + }; + + # Configures integration with Keycloak, which then integrates with a + # variety of backends. + auth.type = "OAUTH"; + plugin.gerrit-oauth-provider-keycloak-oauth = { + root-url = "https://identity.lix.systems"; + realm = "lix-project"; + client-id = "raito-gerrit-testing"; + # client-secret is set in /var/lib/gerrit/etc/secure.config. + }; + + plugin.code-owners = { + # A Code-Review +2 vote is required from a code owner. + requiredApproval = "Code-Review+2"; + # The OWNERS check can be overriden using an Owners-Override vote. + overrideApproval = "Owners-Override+1"; + # People implicitly approve their own changes automatically. + enableImplicitApprovals = "TRUE"; + }; + + # Allow users to add additional email addresses to their accounts. + oauth.allowRegisterNewEmail = true; + + # Use Gerrit's built-in HTTP passwords, rather than trying to use the + # password against the backing OAuth provider. + auth.gitBasicAuthPolicy = "HTTP"; + + # Email sending (emails are relayed via the tazj.in domain's + # GSuite currently). + # + # Note that sendemail.smtpPass is stored in + # $site_path/etc/secure.config and is *not* controlled by Nix. + # + # Receiving email is not currently supported. + sendemail.enable = false; + #sendemail = { + # enable = false; + # html = false; + # connectTimeout = "10sec"; + # from = "TVL Code Review "; + # includeDiff = true; + # smtpEncryption = "none"; + # smtpServer = "localhost"; + # smtpServerPort = 2525; + #}; + }; + + # Replication of the depot repository to secondary machines, for + # serving cgit/josh. + #replicationSettings = { + # gerrit.replicateOnStartup = true; + + # remote.sanduny = { + # url = "depot@sanduny.tvl.su:/var/lib/depot"; + # projects = "depot"; + # }; + #}; + }; + + systemd.services.gerrit = { + serviceConfig = { + # There seems to be no easy way to get `DynamicUser` to play + # well with other services (e.g. by using SupplementaryGroups, + # which seem to have no effect) so we force the DynamicUser + # setting for the Gerrit service to be disabled and reuse the + # existing 'git' user. + DynamicUser = lib.mkForce false; + User = "git"; + Group = "git"; + }; + }; + }; +} diff --git a/services/gerrit/www.nix b/services/gerrit/www.nix new file mode 100644 index 0000000..9976774 --- /dev/null +++ b/services/gerrit/www.nix @@ -0,0 +1,37 @@ +{ config, lib, ... }: +let + inherit (lib) mkIf; + cfg = config.bagel.services.gerrit; +in + { + config = mkIf cfg.enable { + services.nginx = { + enable = true; + enableReload = true; + appendHttpConfig = '' + add_header Permissions-Policy "interest-cohort=()"; + ''; + }; + services.nginx.virtualHosts.gerrit = { + serverName = builtins.head cfg.domains; + serverAliases = builtins.tail cfg.domains; + enableACME = true; + forceSSL = true; + + extraConfig = '' + location / { + proxy_pass http://localhost:4778; + proxy_set_header X-Forwarded-For $remote_addr; + # The :443 suffix is a workaround for https://b.tvl.fyi/issues/88. + proxy_set_header Host $host:443; + } + + location = /robots.txt { + return 200 'User-agent: *\nAllow: /'; + } + ''; + }; + + networking.firewall.allowedTCPPorts = [ 443 80 ]; + }; +}