feat(*): initialize new Snix infrastructure

Co-Authored-By: edef <edef@edef.eu>
Co-Authored-by: Ryan Lahfa <raito@lix.systems>
Change-Id: Ica1cda177a236814de900f50a8a61d288f58f519
This commit is contained in:
Florian Klink 2025-01-06 01:06:47 +01:00
parent 067eff3427
commit a52ea3675c
124 changed files with 27723 additions and 1631 deletions

View file

@ -0,0 +1,4 @@
{ depot, ... }: {
package = import ./package.nix;
module = import ./module.nix;
}

View file

@ -0,0 +1,53 @@
{ config
, lib
, pkgs
, ...
}:
let
cfg = config.services.alertmanager-irc-relay;
yaml = pkgs.formats.yaml { };
configFile = yaml.generate "config.yaml" cfg.settings;
inherit (lib) mkEnableOption mkIf types mkOption mapAttrs mkPackageOption;
in
{
options.services.alertmanager-irc-relay = {
enable = mkEnableOption "Alertmanager IRC relay";
package = mkPackageOption pkgs "alertmanager-irc-relay" { };
settings = mkOption {
type = types.attrsOf yaml.type;
};
environmentFiles = mkOption {
type = types.listOf types.path;
};
};
config = mkIf cfg.enable {
systemd.services.alertmanager-irc-relay = {
description = "Alertmanager IRC Relay Service";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${lib.getExe cfg.package} --config ${configFile}";
Restart = "always";
DynamicUser = true;
ProtectSystem = "strict";
ProtectHome = true;
NoNewPrivileges = true;
EnvironmentFile = cfg.environmentFiles;
};
};
services.alertmanager-irc-relay.settings = mapAttrs (_: lib.mkDefault) {
http_host = "localhost";
http_port = 8000;
msg_once_per_alert_group = true;
use_privmsg = false;
msg_template = "Alert {{ .Labels.alertname }} on {{ .Labels.instance }} is {{ .Status }}";
alert_buffer_size = 2048;
};
};
}

View file

@ -0,0 +1,29 @@
{ lib
, buildGoModule
, fetchFromGitHub
,
}:
buildGoModule rec {
pname = "alertmanager-irc-relay";
version = "0.5.1";
src = fetchFromGitHub {
owner = "google";
repo = "alertmanager-irc-relay";
rev = "v${version}";
hash = "sha256-Rl7o2QPa/IU1snlx/LiJxQok9pnkw9XANnJsu41vNlY=";
};
vendorHash = "sha256-KX+TR0n14+95lldF+0KUo5DbqOKpUDaZNuKMBf0KHFQ=";
ldflags = [ "-s" "-w" ];
meta = {
description = "Send Prometheus Alerts to IRC using Webhooks";
homepage = "https://github.com/google/alertmanager-irc-relay";
license = lib.licenses.asl20;
maintainers = with lib.maintainers; [ raitobezarius ];
mainProgram = "alertmanager-irc-relay";
};
}

View file

@ -51,6 +51,7 @@
{
inherit (eval) pkgs config options;
system = eval.config.system.build.toplevel;
vm = vmConfig.system.build.vm;
# NOTE: This doesn't work because of missing NixOS modules.
# vm = vmConfig.system.build.vm;
};
}

9
third_party/disko/default.nix vendored Normal file
View file

@ -0,0 +1,9 @@
# This file imports the pinned disko.
{ depot ? { }
, ...
}:
{
src = depot.third_party.sources.disko;
}

View file

@ -1,37 +0,0 @@
From 216843cff4a8e41ad9887118751a412c1a22ce72 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 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 50742903de..d1e89920cc 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
@@ -98,6 +98,7 @@ const LANGUAGE_MAP = new Map<string, string>([
['text/x-vhdl', 'vhdl'],
['text/x-yaml', 'yaml'],
['text/vbscript', 'vbscript'],
+ ['text/x-nix', 'nix'],
]);
const CLASS_PREFIX = '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 642ef474a5..97f1ff835b 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -154,6 +154,7 @@ msgenny = text/x-msgenny
mts = application/typescript
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.45.1

View file

@ -1,37 +0,0 @@
From 63f1ff6ea749ae2af29a53463bca81bc3f4bf25b 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 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 d1e89920cc..5d62af1c64 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
@@ -72,6 +72,7 @@ const LANGUAGE_MAP = new Map<string, string>([
['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 97f1ff835b..85d630340f 100644
--- a/resources/com/google/gerrit/server/mime/mime-types.properties
+++ b/resources/com/google/gerrit/server/mime/mime-types.properties
@@ -208,6 +208,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.45.1

View file

@ -1,214 +0,0 @@
From ca2df6d7f53441d443d42908e30bf60fbfc15392 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 3/3] Add titles to CLs over HTTP
---
.../gerrit/httpd/raw/IndexHtmlUtil.java | 12 +++-
.../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, 88 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 a92dd18f04..f87c46d321 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;
@@ -63,13 +64,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, canonicalURL));
+ .putAll(dynamicTemplateData(gerritApi, requestedURL, canonicalURL, titleComputer));
Set<String> enabledExperiments = new HashSet<>();
enabledExperiments.addAll(experimentFeatures.getEnabledExperimentFeatures());
// Add all experiments enabled through url
@@ -82,7 +84,7 @@ public class IndexHtmlUtil {
/** Returns dynamic parameters of {@code index.html}. */
public static ImmutableMap<String, Object> dynamicTemplateData(
- GerritApi gerritApi, String requestedURL, String canonicalURL)
+ GerritApi gerritApi, String requestedURL, String canonicalURL, TitleComputer titleComputer)
throws RestApiException, URISyntaxException {
ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
Map<String, SanitizedContent> initialData = new HashMap<>();
@@ -141,6 +143,10 @@ public class IndexHtmlUtil {
}
data.put("gerritInitialData", initialData);
+
+ Optional<String> 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<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;
@@ -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 b00294f73e..f1c1aae12c 100644
--- a/java/com/google/gerrit/httpd/raw/StaticModule.java
+++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
@@ -224,10 +224,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().orElseGet(() -> 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 5ff1822cd9..81c3cdf0e1 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.45.1

View file

@ -1,11 +0,0 @@
# Not using common --repository_cache because Gerrit's bazelrc overrides this...
build --repository_cache=repository-cache
build --action_env=SSL_CERT_FILE
build --action_env=GERRIT_CACHE_HOME
build --tool_java_runtime_version=local_jdk --java_runtime_version=local_jdk
build --workspace_status_command="cat .version"
# Disable errorprone
build --javacopt="-XepDisableAllChecks"
sync --repository_cache=repository-cache

View file

@ -1,105 +0,0 @@
{ depot, pkgs, ... }:
let
inherit (depot.nix) buildBazelPackageNG;
inherit (buildBazelPackageNG) bazelRulesJavaHook bazelRulesNodeJS5Hook;
in
pkgs.lib.makeOverridable depot.nix.buildBazelPackageNG rec {
pname = "gerrit";
version = "3.10.0";
bazel = pkgs.bazel_7;
src = (pkgs.fetchgit {
url = "https://gerrit.googlesource.com/gerrit";
rev = "v${version}";
fetchSubmodules = true;
deepClone = true;
hash = "sha256-FpKuzityHuHNYBIOL8YUjCLlkuVBfxjvHECb26NsZNE=";
}).overrideAttrs (_: {
env.NIX_PREFETCH_GIT_CHECKOUT_HOOK = ''
pushd "$dir" >/dev/null
${pkgs.python3}/bin/python tools/workspace_status_release.py | sort > .version
popd >/dev/null
# delete all the .git; we can't do this using fetchgit if deepClone is on,
# but our mischief has already been achieved by the python command above :)
find "$dir" -name .git -print0 | xargs -0 rm -rf
'';
});
depsHash = "sha256:114k8ck7056c415qncqmykwqrgprmxnaw3pdv50758mrgw7zdkpm";
patches = [
./0001-Syntax-highlight-nix.patch
./0002-Syntax-highlight-rules.pl.patch
./0003-Add-titles-to-CLs-over-HTTP.patch
];
nativeBuildInputs = with pkgs; [
bazelRulesJavaHook
bazelRulesNodeJS5Hook
curl
jdk
python3
unzip
];
prePatch = ''
rm .bazelversion
ln -sf ${./bazelrc} user.bazelrc
ln -sf ${./workspace_overrides.bzl} workspace_overrides.bzl
substituteInPlace WORKSPACE \
--replace-fail 'load("@io_bazel_rules_webtesting//web:repositories.bzl"' 'load("//:workspace_overrides.bzl"' \
--replace-fail 'load("@io_bazel_rules_webtesting//web/versioned:browsers-0.3.3.bzl"' 'load("//:workspace_overrides.bzl"'
patchShebangs Documentation/replace_macros.py
'';
postPatch = ''
sed -Ei 's,^(STABLE_BUILD_GERRIT_LABEL.*)$,\1-dirty-nix,' .version
'';
preBuild = ''
export GERRIT_CACHE_HOME=$HOME/gerrit-cache
'';
extraCacheInstall = ''
cp -R $GERRIT_CACHE_HOME $out/gerrit-cache
'';
extraBuildSetup = ''
ln -sf $cache/gerrit-cache $GERRIT_CACHE_HOME
'';
extraBuildInstall = ''
mkdir -p "$out"/share/api/
unzip bazel-bin/api-skip-javadoc.zip -d "$out"/share/api
'';
bazelTargets = {
"//:release" = "$out/webapps/gerrit-${version}.war";
"//:api-skip-javadoc" = null;
};
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"
];
};
meta.ci.targets = [ "cache" ];
}

View file

@ -1,5 +0,0 @@
def web_test_repositories():
pass
def browser_repositories(*args, **kwargs):
pass

View file

@ -1,42 +0,0 @@
{ depot, lib, pkgs, ... }:
{
buildGerritBazelPlugin =
{ name
, version
, src
, depsHash ? null
, overlayPluginCmd ? ''
cp -R "${src}" "$out/plugins/${name}"
echo "STABLE_BUILD_${lib.toUpper name}_LABEL v${version}-nix${if patches != [] then "-dirty" else ""}" >> $out/.version
''
, postOverlayPlugin ? ""
, postPatch ? ""
, patches ? [ ]
}: ((depot.third_party.gerrit.override (old: {
name = "${name}.jar";
src = pkgs.runCommandLocal "${name}-src" { } ''
cp -R "${depot.third_party.gerrit.src}" "$out"
chmod -R +w "$out"
${overlayPluginCmd}
${postOverlayPlugin}
'';
depsHash = (if depsHash != null then depsHash else old.depsHash);
bazelTargets = {
"//plugins/${name}" = "$out";
};
extraBuildInstall = "";
})).overrideAttrs (super: {
postPatch = ''
${super.postPatch or ""}
pushd "plugins/${name}"
${lib.concatMapStringsSep "\n" (patch: ''
patch -p1 < ${patch}
'') patches}
popd
${postPatch}
'';
}));
}

View file

@ -1,17 +0,0 @@
{ depot, pkgs, ... }@args:
let
inherit (import ../builder.nix args) buildGerritBazelPlugin;
in
buildGerritBazelPlugin rec {
name = "code-owners";
version = "7de40d8";
src = pkgs.fetchgit {
url = "https://gerrit.googlesource.com/plugins/code-owners";
rev = "7de40d8b30e55eb64316b6fc3d0d00da9caddade";
hash = "sha256-0sLwUcG9RN1o9vZGW8ErwL7UgJapgYoo8XMGsWLO25Q=";
};
patches = [
./using-usernames.patch
];
}

View file

@ -1,472 +0,0 @@
commit 29ace6c38ac513f7ec56ca425230d5712c081043
Author: Luke Granger-Brown <git@lukegb.com>
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> 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<String> messageBuilder = ImmutableList.builder();
AtomicBoolean ownedByAllUsers = new AtomicBoolean(false);
@@ -405,9 +429,53 @@ public class CodeOwnerResolver {
ImmutableMultimap<CodeOwnerReference, CodeOwnerAnnotation> annotations) {
requireNonNull(codeOwnerReferences, "codeOwnerReferences");
+ ImmutableSet<String> 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<String, CodeOwner> 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<CodeOwner, String> usersToGroups =
+ resolvedGroups.inverse();
+
ImmutableSet<String> 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<String, CodeOwner> 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<Pair<String, CodeOwner>> resolvedGroupsCodeOwnersStream =
+ resolvedGroups.entries().stream().map(e -> Pair.of(e.getKey(), e.getValue()));
+ Streams.concat(Streams.concat(newlyResolvedCodeOwnersStream, cachedCodeOwnersStream), resolvedGroupsCodeOwnersStream)
.forEach(
p -> {
ImmutableSet.Builder<CodeOwnerAnnotation> 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<String, Collection<ExternalId>> lookupExternalIds(
ImmutableList.Builder<String> messages, ImmutableSet<String> emails) {
+ String[] actualEmails = emails.stream()
+ .filter(email -> email.contains("@"))
+ .toArray(String[]::new);
+ ImmutableSet<String> usernames = emails.stream()
+ .filter(email -> !email.contains("@"))
+ .collect(ImmutableSet.toImmutableSet());
try {
- ImmutableMap<String, Collection<ExternalId>> extIdsByEmail =
- externalIdCache.byEmails(emails.toArray(new String[0])).asMap();
+ ImmutableMap<String, Collection<ExternalId>> extIds =
+ new ImmutableMap.Builder<String, Collection<ExternalId>>()
+ .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> 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<CodeOwner> 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<CodeOwner> result =
@@ -127,6 +142,18 @@ public class CodeOwnerResolverTest extends AbstractCodeOwnersTest {
CodeOwnerResolver.ALL_USERS_WILDCARD));
}
+ @Test
+ public void cannotResolveCodeOwnerReferenceForGroup() throws Exception {
+ OptionalResultWithMessages<CodeOwner> 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(

View file

@ -1,18 +0,0 @@
{ depot, pkgs, ... }@args:
let
inherit (import ../builder.nix args) buildGerritBazelPlugin;
in
buildGerritBazelPlugin rec {
name = "oauth";
version = "982316";
src = pkgs.fetchgit {
url = "https://gerrit.googlesource.com/plugins/oauth";
rev = "98231604d60788bb43490f1a301d792817ac8008";
hash = "sha256-AuVO1Yys8BYqGHZI/adszCUg0JM2v4Td4fe26LdOPLM=";
};
depsHash = "sha256:10py3vq9sfq5j4gjrlxff01vp346jbcygry06x4zc26xgnf4pa9r";
postOverlayPlugin = ''
cp "${src}/external_plugin_deps.bzl" "$out/plugins/external_plugin_deps.bzl"
'';
}

View file

@ -0,0 +1,28 @@
From 0edb45d9dc9a2688c8b66a21df2a555f864fb81c Mon Sep 17 00:00:00 2001
From: Jade Lovelace <software@lfcode.ca>
Date: Thu, 16 May 2024 18:15:54 -0700
Subject: [PATCH 1/2] lix: Make a Code Review (Gerrit) tab
---
templates/repo/header.tmpl | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/templates/repo/header.tmpl b/templates/repo/header.tmpl
index 21017415c1..c5a36b2203 100644
--- a/templates/repo/header.tmpl
+++ b/templates/repo/header.tmpl
@@ -99,6 +99,11 @@
</a>
{{end}}
+ {{if and (eq .Repository.Name "snix") (eq .Repository.OwnerName "snix")}}
+ <a class="item" href="https://cl.snix.dev/q/project:snix">
+ {{svg "octicon-code-review"}} Code Review (Gerrit)
+ </a>
+ {{end}}
{{if .Permission.CanRead $.UnitTypeExternalTracker}}
<a class="{{if .PageIsIssueList}}active {{end}}item" href="{{.RepoExternalIssuesLink}}" target="_blank" rel="noopener noreferrer">
{{svg "octicon-link-external"}} {{ctx.Locale.Tr "repo.issues"}}
--
2.45.1

View file

@ -0,0 +1,113 @@
From c560d6e110377fb6f42e841a0bd35f80c0f2eb29 Mon Sep 17 00:00:00 2001
From: Jade Lovelace <software@lfcode.ca>
Date: Thu, 16 May 2024 19:56:25 -0700
Subject: [PATCH 2/2] lix: link gerrit cl/ and change-ids
This code is a bit dubious but you know, golang
Co-authored-by: Ryan Lahfa <raito@lix.systems>
---
modules/markup/html.go | 59 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 59 insertions(+)
diff --git a/modules/markup/html.go b/modules/markup/html.go
index 2e65827bf7..2d474077a5 100644
--- a/modules/markup/html.go
+++ b/modules/markup/html.go
@@ -50,6 +50,20 @@ var (
// so that abbreviated hash links can be used as well. This matches git and GitHub usability.
hashCurrentPattern = regexp.MustCompile(`(?:\s|^|\(|\[)([0-9a-f]{7,64})(?:\s|$|\)|\]|[.,:](\s|$))`)
+ // Gerrit change ID pattern
+ changeIdPattern = GerritPattern{
+ urlMatch: 1,
+ displayMatch: 1,
+ pattern: regexp.MustCompile(`(?:\s|^|\(|\[)(I[0-9a-f]{8,40})(?:\s|$|\)|\]|[.,](\s|$))`),
+ }
+
+ // Gerrit cl/ pattern
+ clSlashPattern = GerritPattern{
+ urlMatch: 2,
+ displayMatch: 1,
+ pattern: regexp.MustCompile(`(?:^|[\s\]\[(){},.;:!?])(cl/([0-9]{0,6}))(?:$|[\s\]\[(){},.;:!?])`),
+ }
+
// shortLinkPattern matches short but difficult to parse [[name|link|arg=test]] syntax
shortLinkPattern = regexp.MustCompile(`\[\[(.*?)\]\](\w*)`)
@@ -77,6 +91,14 @@ var (
InlineCodeBlockRegex = regexp.MustCompile("`[^`]+`")
)
+type GerritPattern struct {
+ // The match group to put in the URL
+ urlMatch int
+ // The match group to render as the link
+ displayMatch int
+ pattern *regexp.Regexp
+}
+
// CSS class for action keywords (e.g. "closes: #1")
const keywordClass = "issue-keyword"
@@ -152,6 +174,7 @@ var defaultProcessors = []processor{
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
+ gerritCLPatternProcessor,
emailAddressProcessor,
emojiProcessor,
emojiShortCodeProcessor,
@@ -179,6 +202,7 @@ var commitMessageProcessors = []processor{
issueIndexPatternProcessor,
commitCrossReferencePatternProcessor,
hashCurrentPatternProcessor,
+ gerritCLPatternProcessor,
emailAddressProcessor,
emojiProcessor,
emojiShortCodeProcessor,
@@ -788,6 +812,41 @@ func shortLinkProcessor(ctx *RenderContext, node *html.Node) {
}
}
+func gerritCLPatternProcessor(ctx *RenderContext, node *html.Node) {
+ if ctx.Metas == nil || node == nil {
+ return
+ }
+
+ pats := []*GerritPattern{
+ &changeIdPattern,
+ &clSlashPattern,
+ }
+
+ next := node.NextSibling
+ for node != nil && node != next {
+ for _, pat := range pats {
+ m := pat.pattern.FindStringSubmatchIndex(node.Data)
+ if m == nil {
+ continue
+ }
+
+ linkPart := node.Data[m[pat.urlMatch*2]:m[pat.urlMatch*2+1]]
+ linkNamePart := node.Data[m[pat.displayMatch*2]:m[pat.displayMatch*2+1]]
+ link := util.URLJoin("https://cl.snix.dev/q", linkPart)
+ replaceContent(node, m[pat.displayMatch*2], m[pat.displayMatch*2+1], createCodeLink(link, linkNamePart, ""))
+ }
+
+ // FIXME: I don't understand how any of this stuff was supposed to work to
+ // begin with, since it seems like it returns if it doesn't just match in
+ // the first node???!
+ if node.NextSibling == nil {
+ return
+ }
+
+ node = node.NextSibling.NextSibling
+ }
+}
+
func fullIssuePatternProcessor(ctx *RenderContext, node *html.Node) {
if ctx.Metas == nil {
return
--
2.47.0

View file

@ -0,0 +1,79 @@
From 616ea9925fb7a28f1594694665ca89462bcbd461 Mon Sep 17 00:00:00 2001
From: Jade Lovelace <software@lfcode.ca>
Date: Tue, 19 Mar 2024 18:42:13 -0700
Subject: [PATCH] Add a dont_notify option to the issue create api
yolo
---
modules/structs/issue.go | 1 +
routers/api/v1/repo/issue.go | 2 +-
routers/web/repo/issue.go | 2 +-
services/issue/issue.go | 6 +++++-
4 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/modules/structs/issue.go b/modules/structs/issue.go
index e2b49e94c5..09d239cdb7 100644
--- a/modules/structs/issue.go
+++ b/modules/structs/issue.go
@@ -97,6 +97,7 @@ type CreateIssueOption struct {
// list of label ids
Labels []int64 `json:"labels"`
Closed bool `json:"closed"`
+ DontNotify bool `json:"dont_notify"`
}
// EditIssueOption options for editing an issue
diff --git a/routers/api/v1/repo/issue.go b/routers/api/v1/repo/issue.go
index 0d304dd66d..d9431ef201 100644
--- a/routers/api/v1/repo/issue.go
+++ b/routers/api/v1/repo/issue.go
@@ -708,7 +708,7 @@ func CreateIssue(ctx *context.APIContext) {
form.Labels = make([]int64, 0)
}
- if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs); err != nil {
+ if err := issue_service.NewIssue(ctx, ctx.Repo.Repository, issue, form.Labels, nil, assigneeIDs, !form.DontNotify); err != nil {
if errors.Is(err, user_model.ErrBlockedByUser) {
ctx.Error(http.StatusForbidden, "BlockedByUser", err)
return
diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go
index afa6160d42..dbc500819e 100644
--- a/routers/web/repo/issue.go
+++ b/routers/web/repo/issue.go
@@ -1249,7 +1249,7 @@ func NewIssuePost(ctx *context.Context) {
Ref: form.Ref,
}
- if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs); err != nil {
+ if err := issue_service.NewIssue(ctx, repo, issue, labelIDs, attachments, assigneeIDs, true); err != nil {
if errors.Is(err, user_model.ErrBlockedByUser) {
ctx.RenderWithErr(ctx.Tr("repo.issues.blocked_by_user"), tplIssueNew, form)
return
diff --git a/services/issue/issue.go b/services/issue/issue.go
index 5e726176d0..cdb2f69828 100644
--- a/services/issue/issue.go
+++ b/services/issue/issue.go
@@ -24,7 +24,7 @@ import (
)
// NewIssue creates new issue with labels for repository.
-func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64) error {
+func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_model.Issue, labelIDs []int64, uuids []string, assigneeIDs []int64, notify bool) error {
// Check if the user is not blocked by the repo's owner.
if user_model.IsBlocked(ctx, repo.OwnerID, issue.PosterID) {
return user_model.ErrBlockedByUser
@@ -45,7 +45,11 @@ func NewIssue(ctx context.Context, repo *repo_model.Repository, issue *issues_mo
return err
}
+ if !notify {
+ return nil
+ }
notify_service.NewIssue(ctx, issue, mentions)
+
if len(issue.Labels) > 0 {
notify_service.IssueChangeLabels(ctx, issue.Poster, issue, issue.Labels, nil)
}
--
2.44.0

18
third_party/lix_forgejo/default.nix vendored Normal file
View file

@ -0,0 +1,18 @@
# Patches that the Lix core team developed for git.lix.systems
# Re-applied for git.snix.dev
{ ... }: {
patches = {
# Show a link to upstream for a nixos/nix repository.
upstream_link = ./upstream-link.patch;
# Make it possible not to be notified upon issue creation.
# Minimize noise.
api_dont_notify = ./api-dont-notify.patch;
# Sign in redirection to the OAuth 2 handler.
signin_redirect = ./signin-redirect.patch;
# Series to make Forgejo more Gerrit compatible.
forgejo_is_now_gerrit_native = ./0001-lix-Make-a-Code-Review-Gerrit-tab.patch;
# Modified for our Gerrit instance.
forgejo_knows_about_gerrit = ./0002-lix-link-gerrit-cl-and-change-ids.patch;
};
custom_emojis = ./emojis.txt;
}

1
third_party/lix_forgejo/emojis.txt vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,30 @@
From efba969f2ea6505417546e5f628066e3f10817d5 Mon Sep 17 00:00:00 2001
From: Puck Meerburg <puck@puckipedia.com>
Date: Wed, 8 May 2024 19:28:34 +0000
Subject: [PATCH] Auto-redirect to oauth2 if a provider is available.
---
routers/web/auth/auth.go | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/routers/web/auth/auth.go b/routers/web/auth/auth.go
index 50fd760d80..c6cb5c9075 100644
--- a/routers/web/auth/auth.go
+++ b/routers/web/auth/auth.go
@@ -186,6 +186,13 @@ func SignIn(ctx *context.Context) {
ctx.ServerError("UserSignIn", err)
return
}
+
+ if len(oauth2Providers) > 0 {
+ provider := oauth2Providers[0]
+ ctx.Redirect(setting.AppSubURL + "/user/oauth2/" + provider.DisplayName())
+ return
+ }
+
ctx.Data["OAuth2Providers"] = oauth2Providers
ctx.Data["Title"] = ctx.Tr("sign_in")
ctx.Data["SignInLink"] = setting.AppSubURL + "/user/login"
--
2.44.0

View file

@ -0,0 +1,14 @@
diff --git a/templates/repo/issue/view_title.tmpl b/templates/repo/issue/view_title.tmpl
index c1dd265..9a573ce 100644
--- a/templates/repo/issue/view_title.tmpl
+++ b/templates/repo/issue/view_title.tmpl
@@ -16,6 +16,9 @@
{{if and (or .HasIssuesOrPullsWritePermission .IsIssuePoster) (not .Repository.IsArchived)}}
<button id="edit-title" class="ui small basic button edit-button not-in-edit{{if .Issue.IsPull}} gt-mr-0{{end}}">{{.locale.Tr "repo.issues.edit"}}</button>
{{end}}
+ {{if and (eq .Repository.Name "nix") (eq .Repository.OwnerName "NixOS")}}
+ <a role="button" class="ui small basic button" href="https://github.com/NixOS/nix/issues/{{.Issue.Index}}">{{svg "octicon-mark-github"}} Open upstream</a>
+ {{end}}
{{if not .Issue.IsPull}}
<a role="button" class="ui small green button new-issue-button gt-mr-0" href="{{.RepoLink}}/issues/new{{if .NewIssueChooseTemplate}}/choose{{end}}">{{.locale.Tr "repo.issues.new"}}</a>
{{end}}

12
third_party/nix-gerrit/default.nix vendored Normal file
View file

@ -0,0 +1,12 @@
# This file imports the pinned nix-gerrit.
{ depot ? { }
, ...
}:
let
nix-gerrit-src = depot.third_party.sources.nix-gerrit;
in
import nix-gerrit-src {
pkgs = depot.third_party.nixpkgs;
}

View file

@ -8,6 +8,10 @@
self: super:
depot.nix.readTree.drvTargets {
# Not available in nixpkgs.
# Raito: If you want this to disappear, please send the PR yourself.
alertmanager-irc-relay = super.callPackage depot.third_party.alertmanager-irc-relay.package { };
# Avoid builds of mkShell derivations in CI.
mkShell = super.lib.makeOverridable (
args:

View file

@ -23,6 +23,18 @@
"url": "https://github.com/mlochbaum/bqn-libs/archive/405644fe7aeca7cd31304c05b95de1a6c197fff6.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"disko": {
"branch": "master",
"description": "Declarative disk partitioning and formatting using nix [maintainers=@Lassulus @Enzime @iFreilicht]",
"homepage": "",
"owner": "nix-community",
"repo": "disko",
"rev": "84a5b93637cc16cbfcc61b6e1684d626df61eb21",
"sha256": "0pg2gmr63wa8ywkwvmkwir38yp2hqmz2mya71rlzzkdxwyf7bnyr",
"type": "tarball",
"url": "https://github.com/nix-community/disko/archive/84a5b93637cc16cbfcc61b6e1684d626df61eb21.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"gitignore.nix": {
"branch": "master",
"description": "Nix functions for filtering local git sources",
@ -60,6 +72,12 @@
"url": "https://github.com/nix-community/napalm/archive/e1babff744cd278b56abe8478008b4a9e23036cf.tar.gz",
"url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
},
"nix-gerrit": {
"branch": "main",
"repo": "https://git.lix.systems/the-distro/nix-gerrit.git",
"rev": "86d04ec2e075c8c548acc6cb03eba08d8931c681",
"type": "git"
},
"nixpkgs": {
"branch": "nixos-unstable",
"description": "Nix Packages collection",