about summary refs log tree commit diff
path: root/third_party
diff options
context:
space:
mode:
Diffstat (limited to 'third_party')
-rw-r--r--third_party/agenix/default.nix2
-rw-r--r--third_party/alsi/OWNERS3
-rw-r--r--third_party/alsi/default.nix25
-rw-r--r--third_party/bat_syntaxes/default.nix4
-rw-r--r--third_party/bufbuild/default.nix29
-rw-r--r--third_party/buzz/default.nix30
-rw-r--r--third_party/cgit/Makefile2
-rw-r--r--third_party/cgit/cgit.c2
-rw-r--r--third_party/cgit/cgit.h28
-rw-r--r--third_party/cgit/parsing.c2
-rw-r--r--third_party/cgit/scan-tree.c2
-rw-r--r--third_party/cgit/shared.c17
-rw-r--r--third_party/cgit/ui-atom.c2
-rw-r--r--third_party/cgit/ui-blame.c12
-rw-r--r--third_party/cgit/ui-blob.c10
-rw-r--r--third_party/cgit/ui-commit.c2
-rw-r--r--third_party/cgit/ui-diff.c14
-rw-r--r--third_party/cgit/ui-log.c6
-rw-r--r--third_party/cgit/ui-patch.c6
-rw-r--r--third_party/cgit/ui-plain.c6
-rw-r--r--third_party/cgit/ui-shared.c10
-rw-r--r--third_party/cgit/ui-snapshot.c14
-rw-r--r--third_party/cgit/ui-stats.c2
-rw-r--r--third_party/cgit/ui-tag.c2
-rw-r--r--third_party/cgit/ui-tree.c8
-rw-r--r--third_party/clj2nix/OWNERS4
-rw-r--r--third_party/ddclient/default.nix12
-rw-r--r--third_party/ddclient/module.nix230
-rw-r--r--third_party/ddclient/pkg.nix45
-rw-r--r--third_party/default.nix4
-rw-r--r--third_party/elmPackages_0_18/default.nix6
-rw-r--r--third_party/exwm/.elpaignore1
-rw-r--r--third_party/exwm/default.nix14
-rw-r--r--third_party/exwm/exwm-background.el199
-rw-r--r--third_party/exwm/exwm-cm.el50
-rw-r--r--third_party/exwm/exwm-config.el2
-rw-r--r--third_party/exwm/exwm-core.el47
-rw-r--r--third_party/exwm/exwm-floating.el23
-rw-r--r--third_party/exwm/exwm-input.el245
-rw-r--r--third_party/exwm/exwm-layout.el63
-rw-r--r--third_party/exwm/exwm-manage.el115
-rw-r--r--third_party/exwm/exwm-randr.el12
-rw-r--r--third_party/exwm/exwm-systemtray.el276
-rw-r--r--third_party/exwm/exwm-workspace.el460
-rw-r--r--third_party/exwm/exwm-xim.el11
-rw-r--r--third_party/exwm/exwm-xsettings.el336
-rw-r--r--third_party/exwm/exwm.el194
-rw-r--r--third_party/geesefs/default.nix25
-rwxr-xr-xthird_party/gerrit-queue/.buildkite/build.sh4
-rw-r--r--third_party/gerrit-queue/.buildkite/pipeline.yml13
-rw-r--r--third_party/gerrit-queue/.gitignore4
-rw-r--r--third_party/gerrit-queue/LICENSE201
-rw-r--r--third_party/gerrit-queue/README.md80
-rw-r--r--third_party/gerrit-queue/default.nix14
-rw-r--r--third_party/gerrit-queue/frontend/frontend.go113
-rw-r--r--third_party/gerrit-queue/frontend/templates/changeset.tmpl.html15
-rw-r--r--third_party/gerrit-queue/frontend/templates/index.tmpl.html76
-rw-r--r--third_party/gerrit-queue/frontend/templates/serie.tmpl.html19
-rw-r--r--third_party/gerrit-queue/gerrit/changeset.go117
-rw-r--r--third_party/gerrit-queue/gerrit/client.go220
-rw-r--r--third_party/gerrit-queue/gerrit/serie.go112
-rw-r--r--third_party/gerrit-queue/gerrit/series.go126
-rw-r--r--third_party/gerrit-queue/go.mod10
-rw-r--r--third_party/gerrit-queue/go.sum69
-rw-r--r--third_party/gerrit-queue/main.go137
-rw-r--r--third_party/gerrit-queue/misc/rotatingloghandler.go34
-rw-r--r--third_party/gerrit-queue/submitqueue/runner.go220
-rw-r--r--third_party/gerrit/0001-Syntax-highlight-nix.patch37
-rw-r--r--third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch25
-rw-r--r--third_party/gerrit/0002-Syntax-highlight-nix.patch24
-rw-r--r--third_party/gerrit/0002-Syntax-highlight-rules.pl.patch37
-rw-r--r--third_party/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch (renamed from third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch)62
-rw-r--r--third_party/gerrit/0003-Syntax-highlight-rules.pl.patch46
-rw-r--r--third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch26
-rw-r--r--third_party/gerrit/0006-Always-use-Google-Fonts.patch28
-rw-r--r--third_party/gerrit/default.nix58
-rw-r--r--third_party/gerrit_plugins/builder.nix20
-rw-r--r--third_party/gerrit_plugins/code-owners/default.nix17
-rw-r--r--third_party/gerrit_plugins/code-owners/using-usernames.patch472
-rw-r--r--third_party/gerrit_plugins/default.nix23
-rw-r--r--third_party/gerrit_plugins/oauth/cas-6x.patch69
-rw-r--r--third_party/gerrit_plugins/oauth/default.nix14
-rw-r--r--third_party/hii/OWNERS4
-rw-r--r--third_party/irccat/default.nix2
-rw-r--r--third_party/josh/0001-josh-proxy-Always-require-authentication-when-pushin.patch43
-rw-r--r--third_party/josh/default.nix38
-rw-r--r--third_party/lisp/OWNERS7
-rw-r--r--third_party/lisp/cl-change-case.nix22
-rw-r--r--third_party/lisp/cl-json.nix4
-rw-r--r--third_party/lisp/cl-ppcre.nix12
-rw-r--r--third_party/lisp/lisp-binary.nix16
-rw-r--r--third_party/lisp/mime4cl/OWNERS4
-rw-r--r--third_party/lisp/mime4cl/README7
-rw-r--r--third_party/lisp/mime4cl/README.md27
-rw-r--r--third_party/lisp/mime4cl/address.lisp34
-rw-r--r--third_party/lisp/mime4cl/default.nix10
-rw-r--r--third_party/lisp/mime4cl/endec.lisp136
-rw-r--r--third_party/lisp/mime4cl/ex-sclf.lisp102
-rw-r--r--third_party/lisp/mime4cl/mime.lisp172
-rw-r--r--third_party/lisp/mime4cl/package.lisp13
-rw-r--r--third_party/lisp/mime4cl/streams.lisp343
-rw-r--r--third_party/lisp/mime4cl/test/endec.lisp26
-rw-r--r--third_party/lisp/mime4cl/test/mime.lisp39
-rw-r--r--third_party/lisp/mime4cl/test/rt.lisp20
-rw-r--r--third_party/lisp/mime4cl/test/samples/sample1.msg (renamed from third_party/lisp/mime4cl/test/sample1.msg)0
-rw-r--r--third_party/lisp/mime4cl/test/temp-file.lisp2
-rw-r--r--third_party/lisp/npg/OWNERS4
-rw-r--r--third_party/lisp/qbase64/coreutils-base64.patch13
-rw-r--r--third_party/lisp/qbase64/default.nix57
-rw-r--r--third_party/lisp/str.nix49
-rw-r--r--third_party/napalm/default.nix7
-rw-r--r--third_party/nixpkgs/default.nix26
-rw-r--r--third_party/overlays/dhall/OWNERS4
-rw-r--r--third_party/overlays/dhall/default.nix18
-rw-r--r--third_party/overlays/ecl-static.nix9
-rw-r--r--third_party/overlays/emacs.nix4
-rw-r--r--third_party/overlays/haskell/OWNERS2
-rw-r--r--third_party/overlays/haskell/default.nix68
-rw-r--r--third_party/overlays/haskell/extra-pkgs/brick-0.73.nix70
-rw-r--r--third_party/overlays/haskell/extra-pkgs/pa-error-tree-0.1.0.0.nix10
-rw-r--r--third_party/overlays/haskell/extra-pkgs/pa-field-parser.nix39
-rw-r--r--third_party/overlays/haskell/extra-pkgs/pa-json.nix43
-rw-r--r--third_party/overlays/haskell/extra-pkgs/pa-label.nix10
-rw-r--r--third_party/overlays/haskell/extra-pkgs/pa-prelude.nix43
-rw-r--r--third_party/overlays/haskell/extra-pkgs/pa-pretty-0.1.1.0.nix29
-rw-r--r--third_party/overlays/haskell/extra-pkgs/pa-run-command-0.1.0.0.nix25
-rw-r--r--third_party/overlays/patches/.skip-tree1
-rw-r--r--third_party/overlays/patches/0001-configure-ac-version.patch13
-rw-r--r--third_party/overlays/patches/buf-tests-dont-use-file-transport.patch64
-rw-r--r--third_party/overlays/patches/cbtemulator-uds.patch140
-rw-r--r--third_party/overlays/patches/clickhouse-support-reading-arrow-LargeListArray.patch106
-rw-r--r--third_party/overlays/patches/crate2nix-run-tests-in-build-source.patch69
-rw-r--r--third_party/overlays/patches/crate2nix-tests-debug.patch12
-rw-r--r--third_party/overlays/patches/evans-add-support-for-unix-domain-sockets.patch39
-rw-r--r--third_party/overlays/patches/tpm2-pkcs11-190-dbupgrade.patch29
-rw-r--r--third_party/overlays/tvl.nix181
-rw-r--r--third_party/prometheus-fail2ban-exporter/default.nix2
-rw-r--r--third_party/public-inbox/0001-feat-always-set-the-List-ID-header-even-in-watch.patch30
-rw-r--r--third_party/public-inbox/default.nix9
-rw-r--r--third_party/rust-crates/OWNERS8
-rw-r--r--third_party/rust-crates/default.nix64
-rw-r--r--third_party/smtprelay/default.nix2
-rw-r--r--third_party/sources/sources.json70
-rw-r--r--third_party/terraform-provider-glesys/default.nix7
144 files changed, 4041 insertions, 3574 deletions
diff --git a/third_party/agenix/default.nix b/third_party/agenix/default.nix
index f80dda512c..1462050ef1 100644
--- a/third_party/agenix/default.nix
+++ b/third_party/agenix/default.nix
@@ -10,4 +10,6 @@ in
 {
   inherit src;
   cli = agenix.agenix;
+
+  meta.ci.targets = [ "cli" ];
 }
diff --git a/third_party/alsi/OWNERS b/third_party/alsi/OWNERS
deleted file mode 100644
index 79a422fcde..0000000000
--- a/third_party/alsi/OWNERS
+++ /dev/null
@@ -1,3 +0,0 @@
-inherit: true
-owners:
- - grfn
diff --git a/third_party/alsi/default.nix b/third_party/alsi/default.nix
deleted file mode 100644
index 8969374176..0000000000
--- a/third_party/alsi/default.nix
+++ /dev/null
@@ -1,25 +0,0 @@
-{ pkgs, ... }:
-
-with pkgs;
-
-stdenv.mkDerivation {
-  name = "alsi";
-  pname = "alsi";
-  version = "0.4.8";
-
-  src = fetchFromGitHub {
-    owner = "trizen";
-    repo = "alsi";
-    rev = "fe2a925caad38d4cc7afe10d74ba60c5db09ee66";
-    sha256 = "060xlalfclrda5f1h3svj4v2gr19mdrsc62vrg7hgii0f3lib7j5";
-  };
-
-  buildInputs = [
-    (perl.withPackages (ps: with ps; [ DataDump ]))
-  ];
-
-  installPhase = ''
-    mkdir -p $out/bin
-    cp alsi $out/bin/alsi
-  '';
-}
diff --git a/third_party/bat_syntaxes/default.nix b/third_party/bat_syntaxes/default.nix
index a48962dd36..15af130916 100644
--- a/third_party/bat_syntaxes/default.nix
+++ b/third_party/bat_syntaxes/default.nix
@@ -7,9 +7,9 @@
 { pkgs, ... }:
 
 let
-  inherit (pkgs) bat runCommandNoCC;
+  inherit (pkgs) bat runCommand;
 in
-runCommandNoCC "bat-syntaxes.bin" { } ''
+runCommand "bat-syntaxes.bin" { } ''
   export HOME=$PWD
   mkdir -p .config/bat/syntaxes
   cp ${./Prolog.sublime-syntax} .config/bat/syntaxes
diff --git a/third_party/bufbuild/default.nix b/third_party/bufbuild/default.nix
deleted file mode 100644
index 12683b1062..0000000000
--- a/third_party/bufbuild/default.nix
+++ /dev/null
@@ -1,29 +0,0 @@
-# buf.build is a Protobuf linter and breaking change detector.
-# Several binaries are produced.
-{ pkgs, lib, ... }:
-
-pkgs.buildGoModule {
-  pname = "buf";
-  version = "v0.20.1";
-  vendorSha256 = "1gg5c7aiqb4w1zxwsraxxpln33xkmkzlp1h69xgi9i08zvrfipqs";
-
-  src = pkgs.fetchFromGitHub {
-    owner = "bufbuild";
-    repo = "buf";
-    rev = "5e8bf4c800de911764ffdf8d2188b7f6f54476e4";
-    sha256 = "1rni5swfnb4sbrd9rls4mc3902xhqrlsja96lfcdfjzx08g6kg20";
-  };
-
-  doCheck = false;
-
-  # TODO(riking): postinstall produce shell completions for bash, fish, zsh
-  # bin/buf bash-completion
-  # bin/buf zsh-completion
-  # # bin/buf manpages # not yet functional
-
-  meta = with lib; {
-    description = "Protobuf linter and breaking change detector";
-    homepage = "https://buf.build/docs/introduction";
-    license = licenses.asl20;
-  };
-}
diff --git a/third_party/buzz/default.nix b/third_party/buzz/default.nix
deleted file mode 100644
index 8b112d9e79..0000000000
--- a/third_party/buzz/default.nix
+++ /dev/null
@@ -1,30 +0,0 @@
-{ depot, pkgs, ... }:
-
-depot.third_party.naersk.buildPackage {
-  src = pkgs.fetchFromGitHub {
-    owner = "jonhoo";
-    repo = "buzz";
-    rev = "02479643ed1b0325050245dbb3b70411b8cffb7a";
-    sha256 = "1spklfv02qlinlail5rmhh1c4926gyrkr2ydd9g6z919rxkl0ywk";
-  };
-
-  buildInputs = with pkgs; [
-    pkgconfig
-    dbus
-    glib
-    openssl
-    cairo
-    pango
-    atk
-    gdk-pixbuf
-    gtk3
-    dbus-glib
-    libappindicator-gtk3
-    llvmPackages.llvm
-    llvmPackages.bintools
-    llvmPackages.clang
-    llvmPackages.libclang
-  ];
-
-  LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib/libclang.so";
-}
diff --git a/third_party/cgit/Makefile b/third_party/cgit/Makefile
index 5967a327bb..1a7f1f6381 100644
--- a/third_party/cgit/Makefile
+++ b/third_party/cgit/Makefile
@@ -14,7 +14,7 @@ htmldir = $(docdir)
 pdfdir = $(docdir)
 mandir = $(prefix)/share/man
 SHA1_HEADER = <openssl/sha.h>
-GIT_VER = 2.36.1
+GIT_VER = 2.41.0
 GIT_URL = https://www.kernel.org/pub/software/scm/git/git-$(GIT_VER).tar.xz
 INSTALL = install
 COPYTREE = cp -r
diff --git a/third_party/cgit/cgit.c b/third_party/cgit/cgit.c
index dd28a79315..40202ead67 100644
--- a/third_party/cgit/cgit.c
+++ b/third_party/cgit/cgit.c
@@ -628,7 +628,7 @@ static int prepare_repo_cmd(int nongit)
 		return 1;
 	}
 
-	if (get_oid(ctx.qry.head, &oid)) {
+	if (repo_get_oid(the_repository, ctx.qry.head, &oid)) {
 		char *old_head = ctx.qry.head;
 		ctx.qry.head = xstrdup(ctx.repo->defbranch);
 		cgit_print_error_page(404, "Not found",
diff --git a/third_party/cgit/cgit.h b/third_party/cgit/cgit.h
index 72fcd8498a..f201f82b85 100644
--- a/third_party/cgit/cgit.h
+++ b/third_party/cgit/cgit.h
@@ -1,25 +1,33 @@
 #ifndef CGIT_H
 #define CGIT_H
 
+#include <stdbool.h>
 
 #include <git-compat-util.h>
-#include <stdbool.h>
 
-#include <cache.h>
+#include <archive.h>
+#include <commit.h>
+#include <diffcore.h>
+#include <diff.h>
+#include <environment.h>
+#include <graph.h>
 #include <grep.h>
+#include <hex.h>
+#include <log-tree.h>
+#include <notes.h>
 #include <object.h>
+#include <object-name.h>
 #include <object-store.h>
-#include <tree.h>
-#include <commit.h>
-#include <tag.h>
-#include <diff.h>
-#include <diffcore.h>
-#include <strvec.h>
+#include <path.h>
 #include <refs.h>
 #include <revision.h>
-#include <log-tree.h>
-#include <archive.h>
+#include <setup.h>
 #include <string-list.h>
+#include <strvec.h>
+#include <tag.h>
+#include <tree.h>
+#include <utf8.h>
+#include <wrapper.h>
 #include <xdiff-interface.h>
 #include <xdiff/xdiff.h>
 #include <utf8.h>
diff --git a/third_party/cgit/parsing.c b/third_party/cgit/parsing.c
index e093aaf701..83d3521e89 100644
--- a/third_party/cgit/parsing.c
+++ b/third_party/cgit/parsing.c
@@ -198,7 +198,7 @@ struct taginfo *cgit_parse_tag(struct tag *tag)
 	const char *p;
 	struct taginfo *ret = NULL;
 
-	data = read_object_file(&tag->object.oid, &type, &size);
+	data = repo_read_object_file(the_repository, &tag->object.oid, &type, &size);
 	if (!data || type != OBJ_TAG)
 		goto cleanup;
 
diff --git a/third_party/cgit/scan-tree.c b/third_party/cgit/scan-tree.c
index 1e3b43debf..aa93665426 100644
--- a/third_party/cgit/scan-tree.c
+++ b/third_party/cgit/scan-tree.c
@@ -54,7 +54,7 @@ static void scan_tree_repo_config(const char *name, const char *value)
 	config_fn(repo, name, value);
 }
 
-static int gitconfig_config(const char *key, const char *value, void *cb)
+static int gitconfig_config(const char *key, const char *value, const struct config_context *, void *cb)
 {
 	const char *name;
 
diff --git a/third_party/cgit/shared.c b/third_party/cgit/shared.c
index 0bceb98912..26b6ddb329 100644
--- a/third_party/cgit/shared.c
+++ b/third_party/cgit/shared.c
@@ -241,7 +241,7 @@ static int load_mmfile(mmfile_t *file, const struct object_id *oid)
 		file->ptr = (char *)"";
 		file->size = 0;
 	} else {
-		file->ptr = read_object_file(oid, &type,
+		file->ptr = repo_read_object_file(the_repository, oid, &type,
 		                           (unsigned long *)&file->size);
 	}
 	return 1;
@@ -343,7 +343,7 @@ void cgit_diff_tree(const struct object_id *old_oid,
 	struct diff_options opt;
 	struct pathspec_item *item;
 
-	diff_setup(&opt);
+	repo_diff_setup(the_repository, &opt);
 	opt.output_format = DIFF_FORMAT_CALLBACK;
 	opt.detect_rename = 1;
 	opt.rename_limit = ctx.cfg.renamelimit;
@@ -539,7 +539,9 @@ char *expand_macros(const char *txt)
 
 char *get_mimetype_for_filename(const char *filename)
 {
-	char *ext, *mimetype, *token, line[1024], *saveptr;
+	char *ext, *mimetype, line[1024];
+	struct string_list list = STRING_LIST_INIT_NODUP;
+	int i;
 	FILE *file;
 	struct string_list_item *mime;
 
@@ -564,13 +566,16 @@ char *get_mimetype_for_filename(const char *filename)
 	while (fgets(line, sizeof(line), file)) {
 		if (!line[0] || line[0] == '#')
 			continue;
-		mimetype = strtok_r(line, " \t\r\n", &saveptr);
-		while ((token = strtok_r(NULL, " \t\r\n", &saveptr))) {
-			if (!strcasecmp(ext, token)) {
+		string_list_split_in_place(&list, line, " \t\r\n", -1);
+		string_list_remove_empty_items(&list, 0);
+		mimetype = list.items[0].string;
+		for (i = 1; i < list.nr; i++) {
+			if (!strcasecmp(ext, list.items[i].string)) {
 				fclose(file);
 				return xstrdup(mimetype);
 			}
 		}
+		string_list_clear(&list, 0);
 	}
 	fclose(file);
 	return NULL;
diff --git a/third_party/cgit/ui-atom.c b/third_party/cgit/ui-atom.c
index 0cf8441d46..fefbc79809 100644
--- a/third_party/cgit/ui-atom.c
+++ b/third_party/cgit/ui-atom.c
@@ -97,7 +97,7 @@ void cgit_print_atom(char *tip, const char *path, int max_count)
 		argv[argc++] = path;
 	}
 
-	init_revisions(&rev, NULL);
+	repo_init_revisions(the_repository, &rev, NULL);
 	rev.abbrev = DEFAULT_ABBREV;
 	rev.commit_format = CMIT_FMT_DEFAULT;
 	rev.verbose_header = 1;
diff --git a/third_party/cgit/ui-blame.c b/third_party/cgit/ui-blame.c
index ca770994a6..6418b24221 100644
--- a/third_party/cgit/ui-blame.c
+++ b/third_party/cgit/ui-blame.c
@@ -49,12 +49,12 @@ static void emit_blame_entry_hash(struct blame_entry *ent)
 
 	char *detail = emit_suspect_detail(suspect);
 	html("<span class='oid'>");
-	cgit_commit_link(find_unique_abbrev(oid, DEFAULT_ABBREV), detail,
+	cgit_commit_link(repo_find_unique_abbrev(the_repository, oid, DEFAULT_ABBREV), detail,
 			 NULL, ctx.qry.head, oid_to_hex(oid), suspect->path);
 	html("</span>");
 	free(detail);
 
-	if (!parse_commit(suspect->commit) && suspect->commit->parents) {
+	if (!repo_parse_commit(the_repository, suspect->commit) && suspect->commit->parents) {
 		struct commit *parent = suspect->commit->parents->item;
 
 		html(" ");
@@ -126,7 +126,7 @@ static void print_object(const struct object_id *oid, const char *path,
 		return;
 	}
 
-	buf = read_object_file(oid, &type, &size);
+	buf = repo_read_object_file(the_repository, oid, &type, &size);
 	if (!buf) {
 		cgit_print_error_page(500, "Internal server error",
 			"Error reading object %s", oid_to_hex(oid));
@@ -135,7 +135,7 @@ static void print_object(const struct object_id *oid, const char *path,
 
 	strvec_push(&rev_argv, "blame");
 	strvec_push(&rev_argv, rev);
-	init_revisions(&revs, NULL);
+	repo_init_revisions(the_repository, &revs, NULL);
 	revs.diffopt.flags.allow_textconv = 1;
 	setup_revisions(rev_argv.nr, rev_argv.v, &revs, NULL);
 	init_scoreboard(&sb);
@@ -287,13 +287,13 @@ void cgit_print_blame(void)
 	if (!rev)
 		rev = ctx.qry.head;
 
-	if (get_oid(rev, &oid)) {
+	if (repo_get_oid(the_repository, rev, &oid)) {
 		cgit_print_error_page(404, "Not found",
 			"Invalid revision name: %s", rev);
 		return;
 	}
 	commit = lookup_commit_reference(the_repository, &oid);
-	if (!commit || parse_commit(commit)) {
+	if (!commit || repo_parse_commit(the_repository, commit)) {
 		cgit_print_error_page(404, "Not found",
 			"Invalid commit reference: %s", rev);
 		return;
diff --git a/third_party/cgit/ui-blob.c b/third_party/cgit/ui-blob.c
index c10ae42ebd..08f94ee97e 100644
--- a/third_party/cgit/ui-blob.c
+++ b/third_party/cgit/ui-blob.c
@@ -52,7 +52,7 @@ int cgit_ref_path_exists(const char *path, const char *ref, int file_only)
 		.file_only = file_only
 	};
 
-	if (get_oid(ref, &oid))
+	if (repo_get_oid(the_repository, ref, &oid))
 		goto done;
 	if (oid_object_info(the_repository, &oid, &size) != OBJ_COMMIT)
 		goto done;
@@ -87,7 +87,7 @@ int cgit_print_file(char *path, const char *head, int file_only)
 		.file_only = file_only
 	};
 
-	if (get_oid(head, &oid))
+	if (repo_get_oid(the_repository, head, &oid))
 		return -1;
 	type = oid_object_info(the_repository, &oid, &size);
 	if (type == OBJ_COMMIT) {
@@ -100,7 +100,7 @@ int cgit_print_file(char *path, const char *head, int file_only)
 	}
 	if (type == OBJ_BAD)
 		return -1;
-	buf = read_object_file(&oid, &type, &size);
+	buf = repo_read_object_file(the_repository, &oid, &type, &size);
 	if (!buf)
 		return -1;
 	buf[size] = '\0';
@@ -138,7 +138,7 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl
 			return;
 		}
 	} else {
-		if (get_oid(head, &oid)) {
+		if (repo_get_oid(the_repository, head, &oid)) {
 			cgit_print_error_page(404, "Not found",
 					"Bad ref: %s", head);
 			return;
@@ -160,7 +160,7 @@ void cgit_print_blob(const char *hex, char *path, const char *head, int file_onl
 		return;
 	}
 
-	buf = read_object_file(&oid, &type, &size);
+	buf = repo_read_object_file(the_repository, &oid, &type, &size);
 	if (!buf) {
 		cgit_print_error_page(500, "Internal server error",
 				"Error reading object %s", hex);
diff --git a/third_party/cgit/ui-commit.c b/third_party/cgit/ui-commit.c
index 72cb98b72a..6517e50cc6 100644
--- a/third_party/cgit/ui-commit.c
+++ b/third_party/cgit/ui-commit.c
@@ -26,7 +26,7 @@ void cgit_print_commit(char *hex, const char *prefix)
 	if (!hex)
 		hex = ctx.qry.head;
 
-	if (get_oid(hex, &oid)) {
+	if (repo_get_oid(the_repository, hex, &oid)) {
 		cgit_print_error_page(400, "Bad request",
 				"Bad object id: %s", hex);
 		return;
diff --git a/third_party/cgit/ui-diff.c b/third_party/cgit/ui-diff.c
index 2a64ae8f14..a82313fc64 100644
--- a/third_party/cgit/ui-diff.c
+++ b/third_party/cgit/ui-diff.c
@@ -259,8 +259,8 @@ static void header(const struct object_id *oid1, char *path1, int mode1,
 		htmlf("deleted file mode %.6o\n", mode1);
 
 	if (!subproject) {
-		abbrev1 = xstrdup(find_unique_abbrev(oid1, DEFAULT_ABBREV));
-		abbrev2 = xstrdup(find_unique_abbrev(oid2, DEFAULT_ABBREV));
+		abbrev1 = xstrdup(repo_find_unique_abbrev(the_repository, oid1, DEFAULT_ABBREV));
+		abbrev2 = xstrdup(repo_find_unique_abbrev(the_repository, oid2, DEFAULT_ABBREV));
 		htmlf("index %s..%s", abbrev1, abbrev2);
 		free(abbrev1);
 		free(abbrev2);
@@ -406,13 +406,13 @@ void cgit_print_diff(const char *new_rev, const char *old_rev,
 
 	if (!new_rev)
 		new_rev = ctx.qry.head;
-	if (get_oid(new_rev, new_rev_oid)) {
+	if (repo_get_oid(the_repository, new_rev, new_rev_oid)) {
 		cgit_print_error_page(404, "Not found",
 			"Bad object name: %s", new_rev);
 		return;
 	}
 	commit = lookup_commit_reference(the_repository, new_rev_oid);
-	if (!commit || parse_commit(commit)) {
+	if (!commit || repo_parse_commit(the_repository, commit)) {
 		cgit_print_error_page(404, "Not found",
 			"Bad commit: %s", oid_to_hex(new_rev_oid));
 		return;
@@ -420,7 +420,7 @@ void cgit_print_diff(const char *new_rev, const char *old_rev,
 	new_tree_oid = get_commit_tree_oid(commit);
 
 	if (old_rev) {
-		if (get_oid(old_rev, old_rev_oid)) {
+		if (repo_get_oid(the_repository, old_rev, old_rev_oid)) {
 			cgit_print_error_page(404, "Not found",
 				"Bad object name: %s", old_rev);
 			return;
@@ -433,7 +433,7 @@ void cgit_print_diff(const char *new_rev, const char *old_rev,
 
 	if (!is_null_oid(old_rev_oid)) {
 		commit2 = lookup_commit_reference(the_repository, old_rev_oid);
-		if (!commit2 || parse_commit(commit2)) {
+		if (!commit2 || repo_parse_commit(the_repository, commit2)) {
 			cgit_print_error_page(404, "Not found",
 				"Bad commit: %s", oid_to_hex(old_rev_oid));
 			return;
@@ -446,7 +446,7 @@ void cgit_print_diff(const char *new_rev, const char *old_rev,
 	if (raw) {
 		struct diff_options diffopt;
 
-		diff_setup(&diffopt);
+		repo_diff_setup(the_repository, &diffopt);
 		diffopt.output_format = DIFF_FORMAT_PATCH;
 		diffopt.flags.recursive = 1;
 		diff_setup_done(&diffopt);
diff --git a/third_party/cgit/ui-log.c b/third_party/cgit/ui-log.c
index 520a7496a8..358cdec4e7 100644
--- a/third_party/cgit/ui-log.c
+++ b/third_party/cgit/ui-log.c
@@ -160,7 +160,7 @@ static int show_commit(struct commit *commit, struct rev_info *revs)
 	/* When we get here we have precisely one parent. */
 	parent = parents->item;
 	/* If we can't parse the commit, let print_commit() report an error. */
-	if (parse_commit(parent))
+	if (repo_parse_commit(the_repository, parent))
 		return 1;
 
 	files = 0;
@@ -345,7 +345,7 @@ static const char *disambiguate_ref(const char *ref, int *must_free_result)
 	struct strbuf longref = STRBUF_INIT;
 
 	strbuf_addf(&longref, "refs/heads/%s", ref);
-	if (get_oid(longref.buf, &oid) == 0) {
+	if (repo_get_oid(the_repository, longref.buf, &oid) == 0) {
 		*must_free_result = 1;
 		return strbuf_detach(&longref, NULL);
 	}
@@ -445,7 +445,7 @@ void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, char *pattern
 	if (path)
 		strvec_push(&rev_argv, path);
 
-	init_revisions(&rev, NULL);
+	repo_init_revisions(the_repository, &rev, NULL);
 	rev.abbrev = DEFAULT_ABBREV;
 	rev.commit_format = CMIT_FMT_DEFAULT;
 	rev.verbose_header = 1;
diff --git a/third_party/cgit/ui-patch.c b/third_party/cgit/ui-patch.c
index 4ac03cbef1..3819a8152a 100644
--- a/third_party/cgit/ui-patch.c
+++ b/third_party/cgit/ui-patch.c
@@ -31,7 +31,7 @@ void cgit_print_patch(const char *new_rev, const char *old_rev,
 	if (!new_rev)
 		new_rev = ctx.qry.head;
 
-	if (get_oid(new_rev, &new_rev_oid)) {
+	if (repo_get_oid(the_repository, new_rev, &new_rev_oid)) {
 		cgit_print_error_page(404, "Not found",
 				"Bad object id: %s", new_rev);
 		return;
@@ -44,7 +44,7 @@ void cgit_print_patch(const char *new_rev, const char *old_rev,
 	}
 
 	if (old_rev) {
-		if (get_oid(old_rev, &old_rev_oid)) {
+		if (repo_get_oid(the_repository, old_rev, &old_rev_oid)) {
 			cgit_print_error_page(404, "Not found",
 					"Bad object id: %s", old_rev);
 			return;
@@ -78,7 +78,7 @@ void cgit_print_patch(const char *new_rev, const char *old_rev,
 			      "%s%n%n%w(0)%b";
 	}
 
-	init_revisions(&rev, NULL);
+	repo_init_revisions(the_repository, &rev, NULL);
 	rev.abbrev = DEFAULT_ABBREV;
 	rev.verbose_header = 1;
 	rev.diff = 1;
diff --git a/third_party/cgit/ui-plain.c b/third_party/cgit/ui-plain.c
index 65a205fad7..a66c5a1de0 100644
--- a/third_party/cgit/ui-plain.c
+++ b/third_party/cgit/ui-plain.c
@@ -28,7 +28,7 @@ static int print_object(const struct object_id *oid, const char *path)
 		return 0;
 	}
 
-	buf = read_object_file(oid, &type, &size);
+	buf = repo_read_object_file(the_repository, oid, &type, &size);
 	if (!buf) {
 		cgit_print_error_page(404, "Not found", "Not found");
 		return 0;
@@ -181,12 +181,12 @@ void cgit_print_plain(void)
 	if (!rev)
 		rev = ctx.qry.head;
 
-	if (get_oid(rev, &oid)) {
+	if (repo_get_oid(the_repository, rev, &oid)) {
 		cgit_print_error_page(404, "Not found", "Not found");
 		return;
 	}
 	commit = lookup_commit_reference(the_repository, &oid);
-	if (!commit || parse_commit(commit)) {
+	if (!commit || repo_parse_commit(the_repository, commit)) {
 		cgit_print_error_page(404, "Not found", "Not found");
 		return;
 	}
diff --git a/third_party/cgit/ui-shared.c b/third_party/cgit/ui-shared.c
index 2e8896c982..d047f9a131 100644
--- a/third_party/cgit/ui-shared.c
+++ b/third_party/cgit/ui-shared.c
@@ -898,7 +898,7 @@ void cgit_add_clone_urls(void (*fn)(const char *))
 static int print_this_commit_option(void)
 {
 	struct object_id oid;
-	if (!ctx.qry.head || get_oid(ctx.qry.head, &oid))
+	if (!ctx.qry.head || repo_get_oid(the_repository, ctx.qry.head, &oid))
 		return 1;
 	html_option(oid_to_hex(&oid), "this commit", ctx.qry.head);
 	return 0;
@@ -1167,11 +1167,11 @@ void cgit_compose_snapshot_prefix(struct strbuf *filename, const char *base,
 	 * name starts with {v,V}[0-9] and the prettify mapping is injective,
 	 * i.e. each stripped tag can be inverted without ambiguities.
 	 */
-	if (get_oid(fmt("refs/tags/%s", ref), &oid) == 0 &&
+	if (repo_get_oid(the_repository, fmt("refs/tags/%s", ref), &oid) == 0 &&
 	    (ref[0] == 'v' || ref[0] == 'V') && isdigit(ref[1]) &&
-	    ((get_oid(fmt("refs/tags/%s", ref + 1), &oid) == 0) +
-	     (get_oid(fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
-	     (get_oid(fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
+	    ((repo_get_oid(the_repository, fmt("refs/tags/%s", ref + 1), &oid) == 0) +
+	     (repo_get_oid(the_repository, fmt("refs/tags/v%s", ref + 1), &oid) == 0) +
+	     (repo_get_oid(the_repository, fmt("refs/tags/V%s", ref + 1), &oid) == 0) == 1))
 		ref++;
 
 	strbuf_addf(filename, "%s-%s", base, ref);
diff --git a/third_party/cgit/ui-snapshot.c b/third_party/cgit/ui-snapshot.c
index 280139355a..992853bcd7 100644
--- a/third_party/cgit/ui-snapshot.c
+++ b/third_party/cgit/ui-snapshot.c
@@ -120,7 +120,7 @@ const struct object_id *cgit_snapshot_get_sig(const char *ref,
 	struct notes_tree *tree;
 	struct object_id oid;
 
-	if (get_oid(ref, &oid))
+	if (repo_get_oid(the_repository, ref, &oid))
 		return NULL;
 
 	tree = &snapshot_sig_notes[f - &cgit_snapshot_formats[0]];
@@ -159,7 +159,7 @@ static int make_snapshot(const struct cgit_snapshot_format *format,
 {
 	struct object_id oid;
 
-	if (get_oid(hex, &oid)) {
+	if (repo_get_oid(the_repository, hex, &oid)) {
 		cgit_print_error_page(404, "Not found",
 				"Bad object id: %s", hex);
 		return 1;
@@ -193,7 +193,7 @@ static int write_sig(const struct cgit_snapshot_format *format,
 		return 0;
 	}
 
-	buf = read_object_file(note, &type, &size);
+	buf = repo_read_object_file(the_repository, note, &type, &size);
 	if (!buf) {
 		cgit_print_error_page(404, "Not found", "Not found");
 		return 0;
@@ -233,7 +233,7 @@ static const char *get_ref_from_filename(const struct cgit_repo *repo,
 	strbuf_addstr(&snapshot, filename);
 	strbuf_setlen(&snapshot, snapshot.len - strlen(format->suffix));
 
-	if (get_oid(snapshot.buf, &oid) == 0)
+	if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
 		goto out;
 
 	reponame = cgit_snapshot_prefix(repo);
@@ -245,15 +245,15 @@ static const char *get_ref_from_filename(const struct cgit_repo *repo,
 		strbuf_splice(&snapshot, 0, new_start - snapshot.buf, "", 0);
 	}
 
-	if (get_oid(snapshot.buf, &oid) == 0)
+	if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
 		goto out;
 
 	strbuf_insert(&snapshot, 0, "v", 1);
-	if (get_oid(snapshot.buf, &oid) == 0)
+	if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
 		goto out;
 
 	strbuf_splice(&snapshot, 0, 1, "V", 1);
-	if (get_oid(snapshot.buf, &oid) == 0)
+	if (repo_get_oid(the_repository, snapshot.buf, &oid) == 0)
 		goto out;
 
 	result = 0;
diff --git a/third_party/cgit/ui-stats.c b/third_party/cgit/ui-stats.c
index 40ed6c21df..9aed4ac3bf 100644
--- a/third_party/cgit/ui-stats.c
+++ b/third_party/cgit/ui-stats.c
@@ -230,7 +230,7 @@ static struct string_list collect_stats(const struct cgit_period *period)
 		argv[4] = ctx.qry.path;
 		argc += 2;
 	}
-	init_revisions(&rev, NULL);
+	repo_init_revisions(the_repository, &rev, NULL);
 	rev.abbrev = DEFAULT_ABBREV;
 	rev.commit_format = CMIT_FMT_DEFAULT;
 	rev.max_parents = 1;
diff --git a/third_party/cgit/ui-tag.c b/third_party/cgit/ui-tag.c
index 7be2808149..be1122b905 100644
--- a/third_party/cgit/ui-tag.c
+++ b/third_party/cgit/ui-tag.c
@@ -48,7 +48,7 @@ void cgit_print_tag(char *revname)
 		revname = ctx.qry.head;
 
 	strbuf_addf(&fullref, "refs/tags/%s", revname);
-	if (get_oid(fullref.buf, &oid)) {
+	if (repo_get_oid(the_repository, fullref.buf, &oid)) {
 		cgit_print_error_page(404, "Not found",
 			"Bad tag reference: %s", revname);
 		goto cleanup;
diff --git a/third_party/cgit/ui-tree.c b/third_party/cgit/ui-tree.c
index 21e0b88448..436b333809 100644
--- a/third_party/cgit/ui-tree.c
+++ b/third_party/cgit/ui-tree.c
@@ -98,7 +98,7 @@ static void print_object(const struct object_id *oid, const char *path, const ch
 		return;
 	}
 
-	buf = read_object_file(oid, &type, &size);
+	buf = repo_read_object_file(the_repository, oid, &type, &size);
 	if (!buf) {
 		cgit_print_error_page(500, "Internal server error",
 			"Error reading object %s", oid_to_hex(oid));
@@ -242,7 +242,7 @@ static int ls_item(const struct object_id *oid, struct strbuf *base,
 	}
 	if (S_ISLNK(mode)) {
 		html(" -> ");
-		buf = read_object_file(oid, &type, &size);
+		buf = repo_read_object_file(the_repository, oid, &type, &size);
 		if (!buf) {
 			htmlf("Error reading object: %s", oid_to_hex(oid));
 			goto cleanup;
@@ -378,13 +378,13 @@ void cgit_print_tree(const char *rev, char *path)
 	if (!rev)
 		rev = ctx.qry.head;
 
-	if (get_oid(rev, &oid)) {
+	if (repo_get_oid(the_repository, rev, &oid)) {
 		cgit_print_error_page(404, "Not found",
 			"Invalid revision name: %s", rev);
 		return;
 	}
 	commit = lookup_commit_reference(the_repository, &oid);
-	if (!commit || parse_commit(commit)) {
+	if (!commit || repo_parse_commit(the_repository, commit)) {
 		cgit_print_error_page(404, "Not found",
 			"Invalid commit reference: %s", rev);
 		return;
diff --git a/third_party/clj2nix/OWNERS b/third_party/clj2nix/OWNERS
index 79a422fcde..b381c4e660 100644
--- a/third_party/clj2nix/OWNERS
+++ b/third_party/clj2nix/OWNERS
@@ -1,3 +1 @@
-inherit: true
-owners:
- - grfn
+aspen
diff --git a/third_party/ddclient/default.nix b/third_party/ddclient/default.nix
new file mode 100644
index 0000000000..28b036ea66
--- /dev/null
+++ b/third_party/ddclient/default.nix
@@ -0,0 +1,12 @@
+# Users of this package & module should replace it with something like
+# inadyn, after https://github.com/NixOS/nixpkgs/issues/242330 is
+# landed.
+#
+# TODO(aspen): replace ddclient with inadyn or something else.
+{ pkgs, ... }:
+
+(pkgs.callPackage ./pkg.nix { }).overrideAttrs (old: {
+  passthru = old.passthru // {
+    module = ./module.nix;
+  };
+})
diff --git a/third_party/ddclient/module.nix b/third_party/ddclient/module.nix
new file mode 100644
index 0000000000..c8d68f9be9
--- /dev/null
+++ b/third_party/ddclient/module.nix
@@ -0,0 +1,230 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: Copyright (c) 2003-2023 The Nixpkgs/NixOS contributors
+{ config, pkgs, lib, ... }:
+
+let
+  cfg = config.services.deprecated-ddclient;
+  boolToStr = bool: if bool then "yes" else "no";
+  dataDir = "/var/lib/ddclient";
+  StateDirectory = builtins.baseNameOf dataDir;
+  RuntimeDirectory = StateDirectory;
+
+  configFile' = pkgs.writeText "ddclient.conf" ''
+    # This file can be used as a template for configFile or is automatically generated by Nix options.
+    cache=${dataDir}/ddclient.cache
+    foreground=YES
+    use=${cfg.use}
+    login=${cfg.username}
+    password=${if cfg.protocol == "nsupdate" then "/run/${RuntimeDirectory}/ddclient.key" else "@password_placeholder@"}
+    protocol=${cfg.protocol}
+    ${lib.optionalString (cfg.script != "") "script=${cfg.script}"}
+    ${lib.optionalString (cfg.server != "") "server=${cfg.server}"}
+    ${lib.optionalString (cfg.zone != "")   "zone=${cfg.zone}"}
+    ssl=${boolToStr cfg.ssl}
+    wildcard=YES
+    quiet=${boolToStr cfg.quiet}
+    verbose=${boolToStr cfg.verbose}
+    ${cfg.extraConfig}
+    ${lib.concatStringsSep "," cfg.domains}
+  '';
+  configFile = if (cfg.configFile != null) then cfg.configFile else configFile';
+
+  preStart = ''
+    install --mode=600 --owner=$USER ${configFile} /run/${RuntimeDirectory}/ddclient.conf
+    ${lib.optionalString (cfg.configFile == null) (if (cfg.protocol == "nsupdate") then ''
+      install --mode=600 --owner=$USER ${cfg.passwordFile} /run/${RuntimeDirectory}/ddclient.key
+    '' else if (cfg.passwordFile != null) then ''
+      "${pkgs.replace-secret}/bin/replace-secret" "@password_placeholder@" "${cfg.passwordFile}" "/run/${RuntimeDirectory}/ddclient.conf"
+    '' else ''
+      sed -i '/^password=@password_placeholder@$/d' /run/${RuntimeDirectory}/ddclient.conf
+    '')}
+  '';
+
+in
+
+with lib;
+
+{
+  ###### interface
+
+  options = {
+
+    services.deprecated-ddclient = with lib.types; {
+
+      enable = mkOption {
+        default = false;
+        type = bool;
+        description = lib.mdDoc ''
+          Whether to synchronise your machine's IP address with a dynamic DNS provider (e.g. dyndns.org).
+        '';
+      };
+
+      package = mkOption {
+        type = package;
+        default = pkgs.ddclient;
+        defaultText = lib.literalExpression "pkgs.ddclient";
+        description = lib.mdDoc ''
+          The ddclient executable package run by the service.
+        '';
+      };
+
+      domains = mkOption {
+        default = [ "" ];
+        type = listOf str;
+        description = lib.mdDoc ''
+          Domain name(s) to synchronize.
+        '';
+      };
+
+      username = mkOption {
+        # For `nsupdate` username contains the path to the nsupdate executable
+        default = lib.optionalString (cfg.protocol == "nsupdate") "${pkgs.bind.dnsutils}/bin/nsupdate";
+        defaultText = "";
+        type = str;
+        description = lib.mdDoc ''
+          User name.
+        '';
+      };
+
+      passwordFile = mkOption {
+        default = null;
+        type = nullOr str;
+        description = lib.mdDoc ''
+          A file containing the password or a TSIG key in named format when using the nsupdate protocol.
+        '';
+      };
+
+      interval = mkOption {
+        default = "10min";
+        type = str;
+        description = lib.mdDoc ''
+          The interval at which to run the check and update.
+          See {command}`man 7 systemd.time` for the format.
+        '';
+      };
+
+      configFile = mkOption {
+        default = null;
+        type = nullOr path;
+        description = lib.mdDoc ''
+          Path to configuration file.
+          When set this overrides the generated configuration from module options.
+        '';
+        example = "/root/nixos/secrets/ddclient.conf";
+      };
+
+      protocol = mkOption {
+        default = "dyndns2";
+        type = str;
+        description = lib.mdDoc ''
+          Protocol to use with dynamic DNS provider (see https://sourceforge.net/p/ddclient/wiki/protocols).
+        '';
+      };
+
+      server = mkOption {
+        default = "";
+        type = str;
+        description = lib.mdDoc ''
+          Server address.
+        '';
+      };
+
+      ssl = mkOption {
+        default = true;
+        type = bool;
+        description = lib.mdDoc ''
+          Whether to use SSL/TLS to connect to dynamic DNS provider.
+        '';
+      };
+
+      quiet = mkOption {
+        default = false;
+        type = bool;
+        description = lib.mdDoc ''
+          Print no messages for unnecessary updates.
+        '';
+      };
+
+      script = mkOption {
+        default = "";
+        type = str;
+        description = lib.mdDoc ''
+          script as required by some providers.
+        '';
+      };
+
+      use = mkOption {
+        default = "web, web=checkip.dyndns.com/, web-skip='Current IP Address: '";
+        type = str;
+        description = lib.mdDoc ''
+          Method to determine the IP address to send to the dynamic DNS provider.
+        '';
+      };
+
+      verbose = mkOption {
+        default = false;
+        type = bool;
+        description = lib.mdDoc ''
+          Print verbose information.
+        '';
+      };
+
+      zone = mkOption {
+        default = "";
+        type = str;
+        description = lib.mdDoc ''
+          zone as required by some providers.
+        '';
+      };
+
+      extraConfig = mkOption {
+        default = "";
+        type = lines;
+        description = lib.mdDoc ''
+          Extra configuration. Contents will be added verbatim to the configuration file.
+          ::: {.note}
+          `daemon` should not be added here because it does not work great with the systemd-timer approach the service uses.
+          :::
+        '';
+      };
+    };
+  };
+
+
+  ###### implementation
+
+  config = mkMerge [
+    (mkIf cfg.enable {
+      systemd.services.ddclient = {
+        description = "Dynamic DNS Client";
+        wantedBy = [ "multi-user.target" ];
+        after = [ "network.target" ];
+        restartTriggers = optional (cfg.configFile != null) cfg.configFile;
+        path = lib.optional (lib.hasPrefix "if," cfg.use) pkgs.iproute2;
+
+        serviceConfig = {
+          DynamicUser = true;
+          RuntimeDirectoryMode = "0700";
+          inherit RuntimeDirectory;
+          inherit StateDirectory;
+          Type = "oneshot";
+          ExecStartPre = "!${pkgs.writeShellScript "ddclient-prestart" preStart}";
+          ExecStart = "${lib.getBin cfg.package}/bin/ddclient -file /run/${RuntimeDirectory}/ddclient.conf";
+        };
+      };
+
+      systemd.timers.ddclient = {
+        description = "Run ddclient";
+        wantedBy = [ "timers.target" ];
+        timerConfig = {
+          OnBootSec = cfg.interval;
+          OnUnitInactiveSec = cfg.interval;
+        };
+      };
+    })
+    {
+      ids.uids.ddclient = 30;
+      ids.gids.ddclient = 30;
+    }
+  ];
+}
diff --git a/third_party/ddclient/pkg.nix b/third_party/ddclient/pkg.nix
new file mode 100644
index 0000000000..586f3891ac
--- /dev/null
+++ b/third_party/ddclient/pkg.nix
@@ -0,0 +1,45 @@
+# SPDX-License-Identifier: MIT
+# SPDX-FileCopyrightText: Copyright (c) 2003-2023 The Nixpkgs/NixOS contributors
+{ lib, fetchFromGitHub, perlPackages, autoreconfHook, iproute2, perl }:
+
+perlPackages.buildPerlPackage rec {
+  pname = "ddclient";
+  version = "3.10.0";
+
+  outputs = [ "out" ];
+
+  src = fetchFromGitHub {
+    owner = "ddclient";
+    repo = "ddclient";
+    rev = "v${version}";
+    sha256 = "sha256-wWUkjXwVNZRJR1rXPn3IkDRi9is9vsRuNC/zq8RpB1E=";
+  };
+
+  postPatch = ''
+    touch Makefile.PL
+  '';
+
+  nativeBuildInputs = [ autoreconfHook ];
+
+  buildInputs = with perlPackages; [ IOSocketINET6 IOSocketSSL JSONPP ];
+
+  installPhase = ''
+    runHook preInstall
+    # patch sheebang ddclient script which only exists after buildPhase
+    preConfigure
+    install -Dm755 ddclient $out/bin/ddclient
+    install -Dm644 -t $out/share/doc/ddclient COP* README.* ChangeLog.md
+    runHook postInstall
+  '';
+
+  # TODO: run upstream tests
+  doCheck = false;
+
+  meta = with lib; {
+    description = "Client for updating dynamic DNS service entries";
+    homepage = "https://ddclient.net/";
+    license = licenses.gpl2Plus;
+    platforms = platforms.linux;
+    maintainers = with maintainers; [ SuperSandro2000 ];
+  };
+}
diff --git a/third_party/default.nix b/third_party/default.nix
index 493301102f..874aecd3e7 100644
--- a/third_party/default.nix
+++ b/third_party/default.nix
@@ -12,7 +12,7 @@
 #    other folders below //third_party, other than the ones mentioned
 #    above.
 
-{ pkgs, depot, ... }:
+{ pkgs, depot, localSystem, ... }:
 
 {
   # Expose a partially applied NixOS, expecting an attribute set with
@@ -27,7 +27,7 @@
   nixos =
     { configuration
     , specialArgs ? { }
-    , system ? builtins.currentSystem
+    , system ? localSystem
     , ...
     }:
     let
diff --git a/third_party/elmPackages_0_18/default.nix b/third_party/elmPackages_0_18/default.nix
index e1e4f6f9c2..0481d0940a 100644
--- a/third_party/elmPackages_0_18/default.nix
+++ b/third_party/elmPackages_0_18/default.nix
@@ -4,6 +4,10 @@
 # Elm 0.19 changed the language & package ecosystem completely,
 # essentially requiring a partial rewrite of all Elm apps. However,
 # //fun/gemma uses Elm 0.18 and I don't have time to rewrite it.
+#
+# Since the ABIs of current glibc and the pinned version have diverged
+# too much, we need to build //fun/gemma completely based on a historical
+# nixpkgs version.
 
 { pkgs, ... }:
 
@@ -14,4 +18,4 @@
     rev = "14f9ee66e63077539252f8b4550049381a082518";
     sha256 = "1wn7nmb1cqfk2j91l3rwc6yhimfkzxprb8wknw5wi57yhq9m6lv1";
   })
-  { }).elmPackages
+{ })
diff --git a/third_party/exwm/.elpaignore b/third_party/exwm/.elpaignore
index b43bf86b50..f0f644ee08 100644
--- a/third_party/exwm/.elpaignore
+++ b/third_party/exwm/.elpaignore
@@ -1 +1,2 @@
+LICENSE
 README.md
diff --git a/third_party/exwm/default.nix b/third_party/exwm/default.nix
new file mode 100644
index 0000000000..6daee882fe
--- /dev/null
+++ b/third_party/exwm/default.nix
@@ -0,0 +1,14 @@
+{ depot, pkgs, lib, ... }:
+
+# special dance for overriding this into the fixed-point of emacs
+# packages, but having it be separately buildable.
+
+pkgs.emacsPackages.callPackage
+  ({ trivialBuild, xelb }: trivialBuild {
+    pname = "depot-exwm";
+    version = "canon";
+    src = ./.;
+
+    packageRequires = [ xelb ];
+  })
+{ }
diff --git a/third_party/exwm/exwm-background.el b/third_party/exwm/exwm-background.el
new file mode 100644
index 0000000000..fa663d8fe6
--- /dev/null
+++ b/third_party/exwm/exwm-background.el
@@ -0,0 +1,199 @@
+;;; exwm-background.el --- X Background Module for EXWM  -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022-2024 Free Software Foundation, Inc.
+
+;; Author: Steven Allen <steven@stebalien.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; This module adds X background color setting support to EXWM.
+
+;; To use this module, load and enable it as follows:
+;;   (require 'exwm-background)
+;;   (exwm-background-enable)
+;;
+;; By default, this will apply the theme's background color.  However, that
+;; color can be customized via the `exwm-background-color' setting.
+
+;;; Code:
+
+(require 'exwm-core)
+
+(defcustom exwm-background-color nil
+  "Background color for Xorg."
+  :type '(choice
+          (color :tag "Background Color")
+          (const :tag "Default" nil))
+  :group 'exwm
+  :initialize #'custom-initialize-default
+  :set (lambda (symbol value)
+         (set-default-toplevel-value symbol value)
+         (exwm-background--update)))
+
+(defconst exwm-background--properties '("_XROOTPMAP_ID" "_XSETROOT_ID" "ESETROOT_PMAP_ID")
+  "The background properties to set.
+We can't need to set these so that compositing window managers
+can correctly display the background color.")
+
+(defvar exwm-background--connection nil
+  "The X connection used for setting the background.
+We use a separate connection as other background-setting tools
+may kill this connection when they replace it.")
+
+(defvar exwm-background--pixmap nil
+  "Cached background pixmap.")
+
+(defvar exwm-background--atoms nil
+  "Cached background atoms.")
+
+(defun exwm-background--update (&rest _)
+  "Update the EXWM background."
+
+  ;; Always reconnect as any tool that sets the background may have disconnected us (to force X to
+  ;; free resources).
+  (exwm-background--connect)
+
+  (let ((gc (xcb:generate-id exwm-background--connection))
+        (color (exwm--color->pixel (or exwm-background-color
+                                       (face-background 'default)))))
+    ;; Fill the pixmap.
+    (xcb:+request exwm-background--connection
+        (make-instance 'xcb:CreateGC
+                       :cid gc :drawable exwm-background--pixmap
+                       :value-mask (logior xcb:GC:Foreground
+                                           xcb:GC:GraphicsExposures)
+                       :foreground color
+                       :graphics-exposures 0))
+
+    (xcb:+request exwm-background--connection
+        (make-instance 'xcb:PolyFillRectangle
+                       :gc gc :drawable exwm-background--pixmap
+                       :rectangles
+                       (list
+                        (make-instance
+                         'xcb:RECTANGLE
+                         :x 0 :y 0 :width 1 :height 1))))
+    (xcb:+request exwm-background--connection (make-instance 'xcb:FreeGC :gc gc)))
+
+  ;; Reapply it to force an update (also clobber anyone else who may have set it).
+  (xcb:+request exwm-background--connection
+      (make-instance 'xcb:ChangeWindowAttributes
+                     :window exwm--root
+                     :value-mask xcb:CW:BackPixmap
+                     :background-pixmap exwm-background--pixmap))
+
+  (let (old)
+    ;; Collect old pixmaps so we can kill other background clients (all the background setting tools
+    ;; seem to do this).
+    (dolist (atom exwm-background--atoms)
+      (when-let* ((reply (xcb:+request-unchecked+reply exwm-background--connection
+                             (make-instance 'xcb:GetProperty
+                                            :delete 0
+                                            :window exwm--root
+                                            :property atom
+                                            :type xcb:Atom:PIXMAP
+                                            :long-offset 0
+                                            :long-length 1)))
+                  (value (vconcat (slot-value reply 'value)))
+                  ((length= value 4))
+                  (pixmap (funcall (if xcb:lsb #'xcb:-unpack-u4-lsb #'xcb:-unpack-u4)
+                                   value 0))
+                  ((not (or (= pixmap exwm-background--pixmap)
+                            (member pixmap old)))))
+        (push pixmap old)))
+
+    ;; Change the background.
+    (dolist (atom exwm-background--atoms)
+      (xcb:+request exwm-background--connection
+          (make-instance 'xcb:ChangeProperty
+                         :window exwm--root
+                         :property atom
+                         :type xcb:Atom:PIXMAP
+                         :format 32
+                         :mode xcb:PropMode:Replace
+                         :data-len 1
+                         :data
+                         (funcall (if xcb:lsb
+                                      #'xcb:-pack-u4-lsb
+                                    #'xcb:-pack-u4)
+                                  exwm-background--pixmap))))
+
+    ;; Kill the old background clients.
+    (dolist (pixmap old)
+      (xcb:+request exwm-background--connection
+          (make-instance 'xcb:KillClient :resource pixmap))))
+
+  (xcb:flush exwm-background--connection))
+
+(defun exwm-background--connected-p ()
+  (and exwm-background--connection
+       (process-live-p (slot-value exwm-background--connection 'process))))
+
+(defun exwm-background--connect ()
+  (unless (exwm-background--connected-p)
+    (setq exwm-background--connection (xcb:connect))
+    ;;prevent query message on exit
+    (set-process-query-on-exit-flag (slot-value exwm-background--connection 'process) nil)
+
+    ;; Intern the background property atoms.
+    (setq exwm-background--atoms
+          (mapcar
+           (lambda (prop) (exwm--intern-atom prop exwm-background--connection))
+           exwm-background--properties))
+
+    ;; Create the pixmap.
+    (setq exwm-background--pixmap (xcb:generate-id exwm-background--connection))
+    (xcb:+request exwm-background--connection
+        (make-instance 'xcb:CreatePixmap
+                       :depth
+                       (slot-value
+                        (xcb:+request-unchecked+reply exwm-background--connection
+                            (make-instance 'xcb:GetGeometry :drawable exwm--root))
+                        'depth)
+                       :pid exwm-background--pixmap
+                       :drawable exwm--root
+                       :width 1 :height 1))))
+
+(defun exwm-background--init ()
+  "Initialize background module."
+  (exwm--log)
+  (add-hook 'enable-theme-functions 'exwm-background--update)
+  (add-hook 'disable-theme-functions 'exwm-background--update)
+  (exwm-background--update))
+
+(defun exwm-background--exit ()
+  "Uninitialize the background module."
+  (exwm--log)
+  (remove-hook 'enable-theme-functions 'exwm-background--update)
+  (remove-hook 'disable-theme-functions 'exwm-background--update)
+  (when (and exwm-background--connection
+             (slot-value exwm-background--connection 'connected))
+    (xcb:disconnect exwm-background--connection))
+  (setq exwm-background--pixmap nil
+        exwm-background--connection nil
+        exwm-background--atoms nil))
+
+(defun exwm-background-enable ()
+  "Enable background support for EXWM."
+  (exwm--log)
+  (add-hook 'exwm-init-hook #'exwm-background--init)
+  (add-hook 'exwm-exit-hook #'exwm-background--exit))
+
+(provide 'exwm-background)
+
+;;; exwm-background.el ends here
diff --git a/third_party/exwm/exwm-cm.el b/third_party/exwm/exwm-cm.el
deleted file mode 100644
index 8a45010030..0000000000
--- a/third_party/exwm/exwm-cm.el
+++ /dev/null
@@ -1,50 +0,0 @@
-;;; exwm-cm.el --- Compositing Manager for EXWM  -*- lexical-binding: t -*-
-
-;; Copyright (C) 2016-2021 Free Software Foundation, Inc.
-
-;; Author: Chris Feng <chris.w.feng@gmail.com>
-
-;; This file is part of GNU Emacs.
-
-;; GNU Emacs is free software: you can redistribute it and/or modify
-;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation, either version 3 of the License, or
-;; (at your option) any later version.
-
-;; GNU Emacs is distributed in the hope that it will be useful,
-;; but WITHOUT ANY WARRANTY; without even the implied warranty of
-;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-;; GNU General Public License for more details.
-
-;; You should have received a copy of the GNU General Public License
-;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
-
-;;; Commentary:
-
-;; This module is obsolete since EXWM now supports third-party compositors.
-
-;;; Code:
-
-(make-obsolete-variable 'exwm-cm-opacity
-                        "This variable should no longer be used." "26")
-
-(defun exwm-cm-set-opacity (&rest _args)
-  (declare (obsolete nil "26")))
-
-(defun exwm-cm-enable ()
-  (declare (obsolete nil "26")))
-
-(defun exwm-cm-start ()
-  (declare (obsolete nil "26")))
-
-(defun exwm-cm-stop ()
-  (declare (obsolete nil "26")))
-
-(defun exwm-cm-toggle ()
-  (declare (obsolete nil "26")))
-
-
-
-(provide 'exwm-cm)
-
-;;; exwm-cm.el ends here
diff --git a/third_party/exwm/exwm-config.el b/third_party/exwm/exwm-config.el
index 9609f4cf3c..a9f21e9c8c 100644
--- a/third_party/exwm/exwm-config.el
+++ b/third_party/exwm/exwm-config.el
@@ -1,6 +1,6 @@
 ;;; exwm-config.el --- Predefined configurations  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
diff --git a/third_party/exwm/exwm-core.el b/third_party/exwm/exwm-core.el
index 995b590dc5..e0d644d941 100644
--- a/third_party/exwm/exwm-core.el
+++ b/third_party/exwm/exwm-core.el
@@ -1,6 +1,6 @@
 ;;; exwm-core.el --- Core definitions  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -33,6 +33,10 @@
 (require 'xcb-ewmh)
 (require 'xcb-debug)
 
+(defgroup exwm-debug nil
+  "Debugging."
+  :group 'exwm)
+
 (defcustom exwm-debug-log-time-function #'exwm-debug-log-uptime
   "Function used for generating timestamps in `exwm-debug' logs.
 
@@ -40,7 +44,6 @@ Here are some predefined candidates:
 `exwm-debug-log-uptime': Display the uptime of this Emacs instance.
 `exwm-debug-log-time': Display time of day.
 `nil': Disable timestamp."
-  :group 'exwm-debug
   :type `(choice (const :tag "Emacs uptime" ,#'exwm-debug-log-uptime)
                  (const :tag "Time of day" ,#'exwm-debug-log-time)
                  (const :tag "Off" nil)
@@ -59,6 +62,9 @@ Here are some predefined candidates:
 
 (defvar exwm--connection nil "X connection.")
 
+(defvar exwm--terminal nil
+  "Terminal corresponding to `exwm--connection'.")
+
 (defvar exwm--wmsn-window nil
   "An X window owning the WM_S0 selection.")
 
@@ -90,10 +96,12 @@ Here are some predefined candidates:
                   (frame-or-index &optional id))
 
 (define-minor-mode exwm-debug
-  "Debug-logging enabled if non-nil"
-  :global t)
+  "Debug-logging enabled if non-nil."
+  :global t
+  :group 'exwm-debug)
 
 (defmacro exwm--debug (&rest forms)
+  "Evaluate FORMS if mode `exwm-debug' is active."
   (when exwm-debug `(progn ,@forms)))
 
 (defmacro exwm--log (&optional format-string &rest objects)
@@ -113,10 +121,12 @@ FORMAT-STRING is a string specifying the message to output, as in
 
 (defsubst exwm--id->buffer (id)
   "X window ID => Emacs buffer."
+  (declare (indent defun))
   (cdr (assoc id exwm--id-buffer-alist)))
 
 (defsubst exwm--buffer->id (buffer)
   "Emacs buffer BUFFER => X window ID."
+  (declare (indent defun))
   (car (rassoc buffer exwm--id-buffer-alist)))
 
 (defun exwm--lock (&rest _args)
@@ -155,9 +165,11 @@ Nil can be passed as placeholder."
                                          (if height xcb:ConfigWindow:Height 0))
                      :x x :y y :width width :height height)))
 
-(defun exwm--intern-atom (atom)
-  "Intern X11 ATOM."
-  (slot-value (xcb:+request-unchecked+reply exwm--connection
+(defun exwm--intern-atom (atom &optional conn)
+  "Intern X11 ATOM.
+If CONN is non-nil, use it instead of the value of the variable
+`exwm--connection'."
+  (slot-value (xcb:+request-unchecked+reply (or conn exwm--connection)
                   (make-instance 'xcb:InternAtom
                                  :only-if-exists 0
                                  :name-len (length atom)
@@ -177,6 +189,12 @@ least SECS seconds later."
                         ,function
                         ,@args))
 
+(defsubst exwm--terminal-p (&optional frame)
+  "Return t when FRAME's terminal is EXWM's terminal.
+If FRAME is null, use selected frame."
+  (declare (indent defun))
+  (eq exwm--terminal (frame-terminal frame)))
+
 (defun exwm--get-client-event-mask ()
   "Return event mask set on all managed windows."
   (logior xcb:EventMask:StructureNotify
@@ -188,14 +206,17 @@ least SECS seconds later."
   "Convert COLOR to PIXEL (index in TrueColor colormap)."
   (when (and color
              (eq (x-display-visual-class) 'true-color))
-    (let ((rgb (x-color-values color)))
-      (logior (lsh (lsh (pop rgb) -8) 16)
-              (lsh (lsh (pop rgb) -8) 8)
-              (lsh (pop rgb) -8)))))
+    (let ((rgb (color-values color)))
+      (logior (ash (ash (pop rgb) -8) 16)
+              (ash (ash (pop rgb) -8) 8)
+              (ash (pop rgb) -8)))))
 
 (defun exwm--get-visual-depth-colormap (conn id)
   "Get visual, depth and colormap from X window ID.
-Return a three element list with the respective results."
+Return a three element list with the respective results.
+
+If CONN is non-nil, use it instead of the value of the variable
+`exwm--connection'."
   (let (ret-visual ret-depth ret-colormap)
     (with-slots (visual colormap)
         (xcb:+request-unchecked+reply conn
@@ -227,7 +248,7 @@ One of `line-mode' or `char-mode'.")
 (defvar-local exwm--geometry nil)
 (defvar-local exwm-class-name nil "Class name in WM_CLASS.")
 (defvar-local exwm-instance-name nil "Instance name in WM_CLASS.")
-(defvar-local exwm-title nil "Window title (either _NET_WM_NAME or WM_NAME)")
+(defvar-local exwm-title nil "Window title (either _NET_WM_NAME or WM_NAME).")
 (defvar-local exwm--title-is-utf8 nil)
 (defvar-local exwm-transient-for nil "WM_TRANSIENT_FOR.")
 (defvar-local exwm--protocols nil)
diff --git a/third_party/exwm/exwm-floating.el b/third_party/exwm/exwm-floating.el
index a9f9315b71..34d06a30db 100644
--- a/third_party/exwm/exwm-floating.el
+++ b/third_party/exwm/exwm-floating.el
@@ -1,6 +1,6 @@
 ;;; exwm-floating.el --- Floating Module for EXWM  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -31,17 +31,16 @@
 
 (defgroup exwm-floating nil
   "Floating."
-  :version "25.3"
   :group 'exwm)
 
 (defcustom exwm-floating-setup-hook nil
-  "Normal hook run when an X window has been made floating, in the
-context of the corresponding buffer."
+  "Normal hook run when an X window has been made floating.
+This hook runs in the context of the corresponding buffer."
   :type 'hook)
 
 (defcustom exwm-floating-exit-hook nil
-  "Normal hook run when an X window has exited floating state, in the
-context of the corresponding buffer."
+  "Normal hook run when an X window has exited floating state.
+This hook runs in the context of the corresponding buffer."
   :type 'hook)
 
 (defcustom exwm-floating-border-color "navy"
@@ -118,13 +117,13 @@ context of the corresponding buffer."
 (defvar exwm-workspace--current)
 (defvar exwm-workspace--frame-y-offset)
 (defvar exwm-workspace--window-y-offset)
-(defvar exwm-workspace--workareas)
 (declare-function exwm-layout--hide "exwm-layout.el" (id))
 (declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id))
 (declare-function exwm-layout--refresh "exwm-layout.el" ())
 (declare-function exwm-layout--show "exwm-layout.el" (id &optional window))
 (declare-function exwm-workspace--position "exwm-workspace.el" (frame))
 (declare-function exwm-workspace--update-offsets "exwm-workspace.el" ())
+(declare-function exwm-workspace--workarea "exwm-workspace.el" (frame))
 
 (defun exwm-floating--set-allowed-actions (id tilling)
   "Set _NET_WM_ALLOWED_ACTIONS."
@@ -161,6 +160,8 @@ context of the corresponding buffer."
                           (get-buffer "*scratch*")))
                   (make-frame
                    `((minibuffer . ,(minibuffer-window exwm--frame))
+                     (tab-bar-lines . 0)
+                     (tab-bar-lines-keep-state . t)
                      (left . ,(* window-min-width -10000))
                      (top . ,(* window-min-height -10000))
                      (width . ,window-min-width)
@@ -184,12 +185,8 @@ context of the corresponding buffer."
     (set-frame-parameter frame 'exwm-container frame-container)
     ;; Fix illegal parameters
     ;; FIXME: check normal hints restrictions
-    (let* ((workarea (elt exwm-workspace--workareas
-                          (exwm-workspace--position original-frame)))
-           (x* (aref workarea 0))
-           (y* (aref workarea 1))
-           (width* (aref workarea 2))
-           (height* (aref workarea 3)))
+    (with-slots ((x* x) (y* y) (width* width) (height* height))
+        (exwm-workspace--workarea original-frame)
       ;; Center floating windows
       (when (and (or (= x 0) (= x x*))
                  (or (= y 0) (= y y*)))
diff --git a/third_party/exwm/exwm-input.el b/third_party/exwm/exwm-input.el
index 50676217f1..f1f035c91a 100644
--- a/third_party/exwm/exwm-input.el
+++ b/third_party/exwm/exwm-input.el
@@ -1,6 +1,6 @@
 ;;; exwm-input.el --- Input Module for EXWM  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -40,14 +40,13 @@
 
 (defgroup exwm-input nil
   "Input."
-  :version "25.3"
   :group 'exwm)
 
 (defcustom exwm-input-prefix-keys
   '(?\C-x ?\C-u ?\C-h ?\M-x ?\M-` ?\M-& ?\M-:)
-  "List of prefix keys EXWM should forward to Emacs when in line-mode.
+  "List of prefix keys EXWM should forward to Emacs when in `line-mode'.
 
-The point is to make keys like 'C-x C-f' forwarded to Emacs in line-mode.
+The point is to make keys like 'C-x C-f' forwarded to Emacs in `line-mode'.
 There is no need to add prefix keys for global/simulation keys or those
 defined in `exwm-mode-map' here."
   :type '(repeat key-sequence)
@@ -87,7 +86,7 @@ defined in `exwm-mode-map' here."
                        value))))
 
 (defcustom exwm-input-line-mode-passthrough nil
-  "Non-nil makes 'line-mode' forward all events to Emacs."
+  "Non-nil makes `line-mode' forward all events to Emacs."
   :type 'boolean)
 
 ;; Input focus update requests should be accumulated for a short time
@@ -102,6 +101,13 @@ defined in `exwm-mode-map' here."
 (defconst exwm-input--update-focus-interval 0.01
   "Time interval (in seconds) for accumulating input focus update requests.")
 
+(defconst exwm-input--passthrough-functions '(read-char
+                                              read-char-exclusive
+                                              read-key-sequence-vector
+                                              read-key-sequence
+                                              read-event)
+  "Low-level read functions that must be exempted from EXWM input handling.")
+
 (defvar exwm-input--during-command nil
   "Indicate whether between `pre-command-hook' and `post-command-hook'.")
 
@@ -115,10 +121,13 @@ defined in `exwm-mode-map' here."
 (defvar exwm-input--local-simulation-keys nil
   "Whether simulation keys are local.")
 
-(defvar exwm-input--simulation-keys nil "Simulation keys in line-mode.")
+(defvar exwm-input--simulation-keys nil "Simulation keys in `line-mode'.")
+
+(defvar exwm-input--skip-buffer-list-update nil
+  "Skip the upcoming `buffer-list-update'.")
 
 (defvar exwm-input--temp-line-mode nil
-  "Non-nil indicates it's in temporary line-mode for char-mode.")
+  "Non-nil indicates it's in temporary line-mode for `char-mode'.")
 
 (defvar exwm-input--timestamp-atom nil)
 
@@ -126,25 +135,15 @@ defined in `exwm-mode-map' here."
 
 (defvar exwm-input--timestamp-window nil)
 
-(defvar exwm-input--update-focus-defer-timer nil "Timer for polling the lock.")
+(defvar exwm-input--update-focus-timer nil
+  "Timer for deferring the update of input focus.")
 
 (defvar exwm-input--update-focus-lock nil
   "Lock for solving input focus update contention.")
 
-(defvar exwm-input--update-focus-timer nil
-  "Timer for deferring the update of input focus.")
-
 (defvar exwm-input--update-focus-window nil "The (Emacs) window to be focused.
-It also helps us discern whether a `buffer-list-update-hook' was caused by a
-different window having been selected.
-
 This value should always be overwritten.")
 
-(defvar exwm-input--update-focus-window-buffer nil
-  "Buffer displayed in `exwm-input--update-focus-window'.
-Helps us discern whether a `buffer-list-update-hook' was caused by the selected
-window switching to a different buffer.")
-
 (defvar exwm-input--echo-area-timer nil "Timer for detecting echo area dirty.")
 
 (defvar exwm-input--event-hook nil
@@ -164,8 +163,6 @@ Current buffer will be the `exwm-mode' buffer when this hook runs.")
 (declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id))
 (declare-function exwm-layout--show "exwm-layout.el" (id &optional window))
 (declare-function exwm-reset "exwm.el" ())
-(declare-function exwm-workspace--client-p "exwm-workspace.el"
-                  (&optional frame))
 (declare-function exwm-workspace--minibuffer-own-frame-p "exwm-workspace.el")
 (declare-function exwm-workspace--workspace-p "exwm-workspace.el" (workspace))
 (declare-function exwm-workspace-switch "exwm-workspace.el"
@@ -301,51 +298,39 @@ ARGS are additional arguments to CALLBACK."
 
 (defun exwm-input--on-buffer-list-update ()
   "Run in `buffer-list-update-hook' to track input focus."
-  ;; `buffer-list-update-hook' is invoked by several functions
-  ;; (`get-buffer-create', `select-window', `with-temp-buffer', etc.), but we
-  ;; just want to notice when a different window has been selected, or when the
-  ;; selected window displays a different buffer, so that we can set the focus
-  ;; to the associated X window (in case of an `exwm-mode' buffer).  In order to
-  ;; differentiate, we keep track of the last selected window and buffer in the
-  ;; `exwm-input--update-focus-window' and
-  ;; `exwm-input--update-focus-window-buffer' variables.
-  (let* ((win (selected-window))
-         (buf (window-buffer win)))
-    (when (and (not (exwm-workspace--client-p))
-               (not (and (eq exwm-input--update-focus-window win)
-                         (eq exwm-input--update-focus-window-buffer buf))))
-      (exwm--log "selected-window=%S current-buffer=%S" win buf)
-      (setq exwm-input--update-focus-window win)
-      (setq exwm-input--update-focus-window-buffer buf)
-      (redirect-frame-focus (selected-frame) nil)
-      (exwm-input--update-focus-defer))))
+  (when (and          ; this hook is called incesantly; place cheap tests on top
+         (not exwm-input--skip-buffer-list-update)
+         (exwm--terminal-p) ; skip other terminals, e.g. TTY client frames
+         (not (frame-parameter nil 'no-accept-focus)))
+    (exwm--log "current-buffer=%S selected-window=%S"
+               (current-buffer) (selected-window))
+    (redirect-frame-focus (selected-frame) nil)
+    (setq exwm-input--update-focus-window (selected-window))
+    (exwm-input--update-focus-defer)))
 
 (defun exwm-input--update-focus-defer ()
-  "Defer updating input focus."
-  (when exwm-input--update-focus-defer-timer
-    (cancel-timer exwm-input--update-focus-defer-timer))
+  "Schedule a deferred update to input focus.
+Instead of immediately focusing the current window, it defers the focus change
+until the selected window stops changing (debouncing input focus updates)."
+  (when exwm-input--update-focus-timer
+    (cancel-timer exwm-input--update-focus-timer))
+  (setq exwm-input--update-focus-timer
+        ;; Attempt to accumulate successive events close enough.
+        (run-with-timer exwm-input--update-focus-interval
+                        nil
+                        #'exwm-input--update-focus-commit)))
+
+(defun exwm-input--update-focus-commit ()
+  "Attempt to update the window focus.
+If we're currently updating the window focus, re-schedule a focus update
+attempt later."
   (if exwm-input--update-focus-lock
-      (setq exwm-input--update-focus-defer-timer
-            (exwm--defer 0 #'exwm-input--update-focus-defer))
-    (setq exwm-input--update-focus-defer-timer nil)
-    (when exwm-input--update-focus-timer
-      (cancel-timer exwm-input--update-focus-timer))
-    (setq exwm-input--update-focus-timer
-          ;; Attempt to accumulate successive events close enough.
-          (run-with-timer exwm-input--update-focus-interval
-                          nil
-                          #'exwm-input--update-focus-commit
-                          exwm-input--update-focus-window))))
-
-(defun exwm-input--update-focus-commit (window)
-  "Commit updating input focus."
-  (setq exwm-input--update-focus-lock t)
-  (unwind-protect
-      (exwm-input--update-focus window)
-    (setq exwm-input--update-focus-lock nil)))
+      (exwm-input--update-focus-defer)
+    (let ((exwm-input--update-focus-lock t))
+      (exwm-input--update-focus exwm-input--update-focus-window))))
 
 (defun exwm-input--update-focus (window)
-  "Update input focus."
+  "Update input focus to WINDOW."
   (when (window-live-p window)
     (exwm--log "focus-window=%s focus-buffer=%s" window (window-buffer window))
     (with-current-buffer (window-buffer window)
@@ -469,9 +454,12 @@ ARGS are additional arguments to CALLBACK."
             (t
              ;; Replay this event by default.
              (setq fake-last-command t)
-             (setq mode xcb:Allow:ReplayPointer))))
-    (when fake-last-command
-      (exwm-input--fake-last-command))
+             (setq mode xcb:Allow:ReplayPointer)))
+      (when fake-last-command
+        (if buffer
+            (with-current-buffer buffer
+              (exwm-input--fake-last-command))
+          (exwm-input--fake-last-command))))
     (xcb:+request exwm--connection
         (make-instance 'xcb:AllowEvents :mode mode :time xcb:Time:CurrentTime))
     (xcb:flush exwm--connection))
@@ -594,18 +582,10 @@ instead."
   (when (called-interactively-p 'any)
     (exwm-input--update-global-prefix-keys)))
 
-;; Putting (t . EVENT) into `unread-command-events' does not really work
-;; as documented for Emacs < 26.2.
-(eval-and-compile
-  (if (or (< emacs-major-version 26)
-          (and (= emacs-major-version 26)
-               (< emacs-minor-version 2)))
-      (defsubst exwm-input--unread-event (event)
-        (setq unread-command-events
-              (append unread-command-events (list event))))
-    (defsubst exwm-input--unread-event (event)
-      (setq unread-command-events
-            (append unread-command-events `((t . ,event)))))))
+(defsubst exwm-input--unread-event (event)
+  (declare (indent defun))
+  (setq unread-command-events
+        (append unread-command-events `((t . ,event)))))
 
 (defun exwm-input--mimic-read-event (event)
   "Process EVENT as if it were returned by `read-event'."
@@ -680,8 +660,26 @@ Current buffer must be an `exwm-mode' buffer."
 (defun exwm-input--fake-last-command ()
   "Fool some packages into thinking there is a change in the buffer."
   (setq last-command #'exwm-input--noop)
-  (run-hooks 'pre-command-hook)
-  (run-hooks 'post-command-hook))
+  ;; The Emacs manual says:
+  ;; > Quitting is suppressed while running pre-command-hook and
+  ;; > post-command-hook. If an error happens while executing one of these
+  ;; > hooks, it does not terminate execution of the hook; instead the error is
+  ;; > silenced and the function in which the error occurred is removed from the
+  ;; > hook.
+  ;; We supress errors but neither continue execution nor we remove from the
+  ;; hook.
+  (condition-case err
+      (run-hooks 'pre-command-hook)
+    ((error)
+     (exwm--log "Error occurred while running pre-command-hook: %s"
+                (error-message-string err))
+     (xcb-debug:backtrace)))
+  (condition-case err
+      (run-hooks 'post-command-hook)
+    ((error)
+     (exwm--log "Error occurred while running post-command-hook: %s"
+                (error-message-string err))
+     (xcb-debug:backtrace))))
 
 (defun exwm-input--on-KeyPress-line-mode (key-press raw-data)
   "Parse X KeyPress event to Emacs key event and then feed the command loop."
@@ -725,7 +723,7 @@ Current buffer must be an `exwm-mode' buffer."
       (xcb:flush exwm--connection))))
 
 (defun exwm-input--on-KeyPress-char-mode (key-press &optional _raw-data)
-  "Handle KeyPress event in char-mode."
+  "Handle KeyPress event in `char-mode'."
   (with-slots (detail state) key-press
     (let ((keysym (xcb:keysyms:keycode->keysym exwm--connection detail state))
           event raw-event)
@@ -748,7 +746,7 @@ Current buffer must be an `exwm-mode' buffer."
 (defun exwm-input--on-ButtonPress-line-mode (buffer button-event)
   "Handle button events in line mode.
 BUFFER is the `exwm-mode' buffer the event was generated
-on. BUTTON-EVENT is the X event converted into an Emacs event.
+on.  BUTTON-EVENT is the X event converted into an Emacs event.
 
 The return value is used as event_mode to release the original
 button event."
@@ -766,7 +764,7 @@ button event."
         xcb:Allow:ReplayPointer))))
 
 (defun exwm-input--on-ButtonPress-char-mode ()
-  "Handle button events in char-mode.
+  "Handle button events in `char-mode'.
 The return value is used as event_mode to release the original
 button event."
   (exwm--log)
@@ -842,7 +840,7 @@ button event."
 
 ;;;###autoload
 (defun exwm-input-grab-keyboard (&optional id)
-  "Switch to line-mode."
+  "Switch to `line-mode'."
   (interactive (list (when (derived-mode-p 'exwm-mode)
                        (exwm--buffer->id (window-buffer)))))
   (when id
@@ -853,7 +851,7 @@ button event."
 
 ;;;###autoload
 (defun exwm-input-release-keyboard (&optional id)
-  "Switch to char-mode."
+  "Switch to `char-mode`."
   (interactive (list (when (derived-mode-p 'exwm-mode)
                        (exwm--buffer->id (window-buffer)))))
   (when id
@@ -864,7 +862,7 @@ button event."
 
 ;;;###autoload
 (defun exwm-input-toggle-keyboard (&optional id)
-  "Toggle between 'line-mode' and 'char-mode'."
+  "Toggle between `line-mode' and `char-mode'."
   (interactive (list (when (derived-mode-p 'exwm-mode)
                        (exwm--buffer->id (window-buffer)))))
   (when id
@@ -971,17 +969,12 @@ multiple keys.  If END-KEY is non-nil, stop sending keys if it's pressed."
                    #'exwm-input-send-simulation-key))))
            exwm-input--simulation-keys))
 
-(defun exwm-input-set-simulation-keys (simulation-keys)
-  "Please customize or set `exwm-input-simulation-keys' instead."
-  (declare (obsolete nil "26"))
-  (exwm-input--set-simulation-keys simulation-keys))
-
 (defcustom exwm-input-simulation-keys nil
   "Simulation keys.
 
 It is an alist of the form (original-key . simulated-key), where both
 original-key and simulated-key are key sequences.  Original-key is what you
-type to an X window in line-mode which then gets translated to simulated-key
+type to an X window in `line-mode' which then gets translated to simulated-key
 by EXWM and forwarded to the X window.
 
 Notes:
@@ -1092,7 +1085,7 @@ where both ORIGINAL-KEY and SIMULATED-KEY are key sequences."
 (defmacro exwm-input-invoke-factory (keys)
   "Make a command that invokes KEYS when called.
 
-One use is to access the keymap bound to KEYS (as prefix keys) in char-mode."
+One use is to access the keymap bound to KEYS (as prefix keys) in `char-mode'."
   (let* ((keys (kbd keys))
          (description (key-description keys)))
     `(defun ,(intern (concat "exwm-input--invoke--" description)) ()
@@ -1116,39 +1109,47 @@ One use is to access the keymap bound to KEYS (as prefix keys) in char-mode."
 
 (defun exwm-input--on-minibuffer-setup ()
   "Run in `minibuffer-setup-hook' to grab keyboard if necessary."
-  (exwm--log)
-  (with-current-buffer
-      (window-buffer (frame-selected-window exwm-workspace--current))
-    (when (and (derived-mode-p 'exwm-mode)
-               (not (exwm-workspace--client-p))
-               (eq exwm--selected-input-mode 'char-mode))
-      (exwm-input--grab-keyboard exwm--id))))
+  (let* ((window (or (minibuffer-selected-window) ; minibuffer-setup-hook
+                     (selected-window)))          ; echo-area-clear-hook
+         (frame (window-frame window)))
+    (when (exwm--terminal-p frame)
+      (with-current-buffer (window-buffer window)
+        (when (and (derived-mode-p 'exwm-mode)
+                   (eq exwm--selected-input-mode 'char-mode))
+          (exwm--log "Grab #x%x window=%s frame=%s" exwm--id window frame)
+          (exwm-input--grab-keyboard exwm--id))))))
 
 (defun exwm-input--on-minibuffer-exit ()
   "Run in `minibuffer-exit-hook' to release keyboard if necessary."
-  (exwm--log)
-  (with-current-buffer
-      (window-buffer (frame-selected-window exwm-workspace--current))
-    (when (and (derived-mode-p 'exwm-mode)
-               (not (exwm-workspace--client-p))
-               (eq exwm--selected-input-mode 'char-mode)
-               (eq exwm--input-mode 'line-mode))
-      (exwm-input--release-keyboard exwm--id))))
+  (let* ((window (or (minibuffer-selected-window) ; minibuffer-setup-hook
+                     (selected-window)))          ; echo-area-clear-hook
+         (frame (window-frame window)))
+    (when (exwm--terminal-p frame)
+      (with-current-buffer (window-buffer window)
+        (when (and (derived-mode-p 'exwm-mode)
+                   (eq exwm--selected-input-mode 'char-mode)
+                   (eq exwm--input-mode 'line-mode))
+          (exwm--log "Release #x%x window=%s frame=%s" exwm--id window frame)
+          (exwm-input--release-keyboard exwm--id))))))
 
 (defun exwm-input--on-echo-area-dirty ()
   "Run when new message arrives to grab keyboard if necessary."
-  (exwm--log)
-  (when (and (not (active-minibuffer-window))
-             (not (exwm-workspace--client-p))
-             cursor-in-echo-area)
+  (when (and cursor-in-echo-area
+             (not (active-minibuffer-window)))
+    (exwm--log)
     (exwm-input--on-minibuffer-setup)))
 
 (defun exwm-input--on-echo-area-clear ()
   "Run in `echo-area-clear-hook' to release keyboard if necessary."
-  (exwm--log)
   (unless (current-message)
+    (exwm--log)
     (exwm-input--on-minibuffer-exit)))
 
+(defun exwm-input--call-with-passthrough (function &rest args)
+  "Bind `exwm-input-line-mode-passthrough' and call FUNCTION with ARGS."
+  (let ((exwm-input-line-mode-passthrough t))
+    (apply function args)))
+
 (defun exwm-input--init ()
   "Initialize the keyboard module."
   (exwm--log)
@@ -1204,7 +1205,10 @@ One use is to access the keymap bound to KEYS (as prefix keys) in char-mode."
         (run-with-idle-timer 0 t #'exwm-input--on-echo-area-dirty))
   (add-hook 'echo-area-clear-hook #'exwm-input--on-echo-area-clear)
   ;; Update focus when buffer list updates
-  (add-hook 'buffer-list-update-hook #'exwm-input--on-buffer-list-update))
+  (add-hook 'buffer-list-update-hook #'exwm-input--on-buffer-list-update)
+
+  (dolist (fun exwm-input--passthrough-functions)
+    (advice-add fun :around #'exwm-input--call-with-passthrough)))
 
 (defun exwm-input--post-init ()
   "The second stage in the initialization of the input module."
@@ -1214,6 +1218,8 @@ One use is to access the keymap bound to KEYS (as prefix keys) in char-mode."
 (defun exwm-input--exit ()
   "Exit the input module."
   (exwm--log)
+  (dolist (fun exwm-input--passthrough-functions)
+    (advice-remove fun #'exwm-input--call-with-passthrough))
   (exwm-input--unset-simulation-keys)
   (remove-hook 'pre-command-hook #'exwm-input--on-pre-command)
   (remove-hook 'post-command-hook #'exwm-input--on-post-command)
@@ -1224,17 +1230,16 @@ One use is to access the keymap bound to KEYS (as prefix keys) in char-mode."
     (setq exwm-input--echo-area-timer nil))
   (remove-hook 'echo-area-clear-hook #'exwm-input--on-echo-area-clear)
   (remove-hook 'buffer-list-update-hook #'exwm-input--on-buffer-list-update)
-  (when exwm-input--update-focus-defer-timer
-    (cancel-timer exwm-input--update-focus-defer-timer))
   (when exwm-input--update-focus-timer
     (cancel-timer exwm-input--update-focus-timer))
   ;; Make input focus working even without a WM.
-  (xcb:+request exwm--connection
-      (make-instance 'xcb:SetInputFocus
-                     :revert-to xcb:InputFocus:PointerRoot
-                     :focus exwm--root
-                     :time xcb:Time:CurrentTime))
-  (xcb:flush exwm--connection))
+  (when (slot-value exwm--connection 'connected)
+    (xcb:+request exwm--connection
+        (make-instance 'xcb:SetInputFocus
+                       :revert-to xcb:InputFocus:PointerRoot
+                       :focus exwm--root
+                       :time xcb:Time:CurrentTime))
+    (xcb:flush exwm--connection)))
 
 
 
diff --git a/third_party/exwm/exwm-layout.el b/third_party/exwm/exwm-layout.el
index 9173a1c049..8649c11ffd 100644
--- a/third_party/exwm/exwm-layout.el
+++ b/third_party/exwm/exwm-layout.el
@@ -1,6 +1,6 @@
 ;;; exwm-layout.el --- Layout Module for EXWM  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -29,7 +29,6 @@
 
 (defgroup exwm-layout nil
   "Layout."
-  :version "25.3"
   :group 'exwm)
 
 (defcustom exwm-layout-auto-iconify t
@@ -57,8 +56,7 @@
 (declare-function exwm-input--grab-keyboard "exwm-input.el")
 (declare-function exwm-input-grab-keyboard "exwm-input.el")
 (declare-function exwm-workspace--active-p "exwm-workspace.el" (frame))
-(declare-function exwm-workspace--client-p "exwm-workspace.el"
-                  (&optional frame))
+(declare-function exwm-workspace--get-geometry "exwm-workspace.el" (frame))
 (declare-function exwm-workspace--minibuffer-own-frame-p "exwm-workspace.el")
 (declare-function exwm-workspace--workspace-p "exwm-workspace.el"
                   (workspace))
@@ -66,7 +64,7 @@
                   (frame-or-index &optional id))
 
 (defun exwm-layout--set-state (id state)
-  "Set WM_STATE."
+  "Set WM_STATE of X window ID to STATE."
   (exwm--log "id=#x%x" id)
   (xcb:+request exwm--connection
       (make-instance 'xcb:icccm:set-WM_STATE
@@ -75,24 +73,28 @@
     (setq exwm-state state)))
 
 (defun exwm-layout--iconic-state-p (&optional id)
+  "Check whether X window ID is in iconic state."
   (= xcb:icccm:WM_STATE:IconicState
      (if id
          (buffer-local-value 'exwm-state (exwm--id->buffer id))
        exwm-state)))
 
-(defun exwm-layout--set-ewmh-state (xwin)
-  "Set _NET_WM_STATE."
-  (with-current-buffer (exwm--id->buffer xwin)
+(defun exwm-layout--set-ewmh-state (id)
+  "Set _NET_WM_STATE of X window ID to the value of variable `exwm--ewmh-state'."
+  (with-current-buffer (exwm--id->buffer id)
     (xcb:+request exwm--connection
         (make-instance 'xcb:ewmh:set-_NET_WM_STATE
                        :window exwm--id
                        :data exwm--ewmh-state))))
 
 (defun exwm-layout--fullscreen-p ()
+  "Check whether current `exwm-mode' buffer is in fullscreen state."
   (when (derived-mode-p 'exwm-mode)
     (memq xcb:Atom:_NET_WM_STATE_FULLSCREEN exwm--ewmh-state)))
 
 (defun exwm-layout--auto-iconify ()
+  "Helper function to iconify unused X windows.
+See variable `exwm-layout-auto-iconify'."
   (when (and exwm-layout-auto-iconify
              (not exwm-transient-for))
     (let ((xwin exwm--id)
@@ -154,7 +156,8 @@
   (with-current-buffer (exwm--id->buffer id)
     (unless (or (exwm-layout--iconic-state-p)
                 (and exwm--floating-frame
-                     (eq 4294967295. exwm--desktop)))
+                     exwm--desktop
+                     (= 4294967295. exwm--desktop)))
       (exwm--log "Hide #x%x" id)
       (when exwm--floating-frame
         (let* ((container (frame-parameter exwm--floating-frame
@@ -212,7 +215,7 @@
 
 ;;;###autoload
 (cl-defun exwm-layout-unset-fullscreen (&optional id)
-  "Restore window from fullscreen state."
+  "Restore X window ID from fullscreen state."
   (interactive)
   (exwm--log "id=#x%x" (or id 0))
   (unless (and (or id (derived-mode-p 'exwm-mode))
@@ -243,7 +246,7 @@
 
 ;;;###autoload
 (cl-defun exwm-layout-toggle-fullscreen (&optional id)
-  "Toggle fullscreen mode."
+  "Toggle fullscreen mode of X window ID."
   (interactive (list (exwm--buffer->id (window-buffer))))
   (exwm--log "id=#x%x" (or id 0))
   (unless (or id (derived-mode-p 'exwm-mode))
@@ -300,7 +303,8 @@ selected by `other-buffer'."
                                               clients clients-floating))))))
 
 (defun exwm-layout--refresh (&optional frame)
-  "Refresh layout."
+  "Refresh layout of FRAME.
+If FRAME is nil, refresh layout of selected frame."
   ;; `window-size-change-functions' sets this argument while
   ;; `window-configuration-change-hook' makes the frame selected.
   (unless frame
@@ -405,22 +409,29 @@ selected by `other-buffer'."
 (defun exwm-layout--on-minibuffer-setup ()
   "Refresh layout when minibuffer grows."
   (exwm--log)
-  (unless (exwm-workspace--client-p)
-    (exwm--defer 0 (lambda ()
-                     (when (< 1 (window-height (minibuffer-window)))
-                       (exwm-layout--refresh))))))
+  ;; Only when active minibuffer's frame is an EXWM frame.
+  (let* ((mini-window (active-minibuffer-window))
+         (frame (window-frame mini-window)))
+    (when (exwm-workspace--workspace-p frame)
+      (exwm--defer 0 (lambda ()
+                       (when (< 1 (window-height mini-window))
+                         (exwm-layout--refresh frame)))))))
 
 (defun exwm-layout--on-echo-area-change (&optional dirty)
-  "Run when message arrives or in `echo-area-clear-hook' to refresh layout."
-  (when (and (current-message)
-             (not (exwm-workspace--client-p))
-             (or (cl-position ?\n (current-message))
-                 (> (length (current-message))
-                    (frame-width exwm-workspace--current))))
-    (exwm--log)
-    (if dirty
-        (exwm-layout--refresh)
-      (exwm--defer 0 #'exwm-layout--refresh))))
+  "Run when message arrives or in `echo-area-clear-hook' to refresh layout.
+If DIRTY is non-nil, refresh layout immediately."
+  (let ((frame (window-frame (active-minibuffer-window)))
+        (msg (current-message)))
+    ;; Check whether the frame where current window's minibuffer resides (not
+    ;; current window's frame for floating windows!) must be adjusted.
+    (when (and msg
+               (exwm-workspace--workspace-p frame)
+               (or (cl-position ?\n msg)
+                   (> (length msg) (frame-width frame))))
+      (exwm--log)
+      (if dirty
+          (exwm-layout--refresh exwm-workspace--current)
+        (exwm--defer 0 #'exwm-layout--refresh exwm-workspace--current)))))
 
 ;;;###autoload
 (defun exwm-layout-enlarge-window (delta &optional horizontal)
diff --git a/third_party/exwm/exwm-manage.el b/third_party/exwm/exwm-manage.el
index e940257fc9..ab66e298ac 100644
--- a/third_party/exwm/exwm-manage.el
+++ b/third_party/exwm/exwm-manage.el
@@ -1,7 +1,7 @@
 ;;; exwm-manage.el --- Window Management Module for  -*- lexical-binding: t -*-
 ;;;                    EXWM
 
-;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -30,12 +30,11 @@
 
 (defgroup exwm-manage nil
   "Manage."
-  :version "25.3"
   :group 'exwm)
 
 (defcustom exwm-manage-finish-hook nil
-  "Normal hook run after a window is just managed, in the context of the
-corresponding buffer."
+  "Normal hook run after a window is just managed.
+This hook runs in the context of the corresponding `exwm-mode' buffer."
   :type 'hook)
 
 (defcustom exwm-manage-force-tiling nil
@@ -151,12 +150,12 @@ want to match against EXWM internal variables such as `exwm-title',
 (defvar exwm-manage--ping-lock nil
   "Non-nil indicates EXWM is pinging a window.")
 
+(defvar exwm-input--skip-buffer-list-update)
 (defvar exwm-input-prefix-keys)
 (defvar exwm-workspace--current)
 (defvar exwm-workspace--id-struts-alist)
 (defvar exwm-workspace--list)
 (defvar exwm-workspace--switch-history-outdated)
-(defvar exwm-workspace--workareas)
 (defvar exwm-workspace-current-index)
 (declare-function exwm--update-class "exwm.el" (id &optional force))
 (declare-function exwm--update-hints "exwm.el" (id &optional force))
@@ -169,17 +168,22 @@ want to match against EXWM internal variables such as `exwm-title',
 (declare-function exwm--update-window-type "exwm.el" (id &optional force))
 (declare-function exwm-floating--set-floating "exwm-floating.el" (id))
 (declare-function exwm-floating--unset-floating "exwm-floating.el" (id))
-(declare-function exwm-input-grab-keyboard "exwm-input.el")
+(declare-function exwm-input-grab-keyboard "exwm-input.el" (&optional id))
+(declare-function exwm-input-release-keyboard "exwm-input.el" (&optional id))
 (declare-function exwm-input-set-local-simulation-keys "exwm-input.el")
 (declare-function exwm-layout--fullscreen-p "exwm-layout.el" ())
 (declare-function exwm-layout--iconic-state-p "exwm-layout.el" (&optional id))
+(declare-function exwm-layout-set-fullscreen "exwm-layout.el" (&optional id))
+(declare-function exwm-workspace--get-geometry "exwm-workspace.el" (frame))
 (declare-function exwm-workspace--position "exwm-workspace.el" (frame))
 (declare-function exwm-workspace--set-fullscreen "exwm-workspace.el" (frame))
 (declare-function exwm-workspace--update-struts "exwm-workspace.el" ())
 (declare-function exwm-workspace--update-workareas "exwm-workspace.el" ())
+(declare-function exwm-workspace--workarea "exwm-workspace.el" (frame))
 
 (defun exwm-manage--update-geometry (id &optional force)
-  "Update window geometry."
+  "Update geometry of X window ID.
+Override current geometry if FORCE is non-nil."
   (exwm--log "id=#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (and exwm--geometry (not force))
@@ -195,7 +199,7 @@ want to match against EXWM internal variables such as `exwm-title',
                                  :height (/ (x-display-pixel-height) 2))))))))
 
 (defun exwm-manage--update-ewmh-state (id)
-  "Update _NET_WM_STATE."
+  "Update _NET_WM_STATE of X window ID."
   (exwm--log "id=#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless exwm--ewmh-state
@@ -206,7 +210,8 @@ want to match against EXWM internal variables such as `exwm-title',
           (setq exwm--ewmh-state (append (slot-value reply 'value) nil)))))))
 
 (defun exwm-manage--update-mwm-hints (id &optional force)
-  "Update _MOTIF_WM_HINTS."
+  "Update _MOTIF_WM_HINTS of X window ID.
+Override current hinds if FORCE is non-nil."
   (exwm--log "id=#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (and (not exwm--mwm-hints-decorations) (not force))
@@ -229,6 +234,23 @@ want to match against EXWM internal variables such as `exwm-title',
                           (elt value 2))) ;MotifWmHints.decorations
               (setq exwm--mwm-hints-decorations nil))))))))
 
+(defun exwm-manage--update-default-directory (id)
+  "Update the `default-directory' of X window ID.
+Sets the `default-directory' of the EXWM buffer associated with X window to
+match its current working directory.
+
+This only works when procfs is mounted, which may not be the case on some BSDs."
+  (with-current-buffer (exwm--id->buffer id)
+    (if-let* ((response (xcb:+request-unchecked+reply exwm--connection
+                            (make-instance 'xcb:ewmh:get-_NET_WM_PID
+                                           :window id)))
+              (pid (slot-value response 'value))
+              (cwd (file-symlink-p (format "/proc/%d/cwd" pid)))
+              ((file-accessible-directory-p cwd)))
+        (setq default-directory (file-name-as-directory cwd))
+      (setq default-directory (expand-file-name "~/")))))
+
+
 (defun exwm-manage--set-client-list ()
   "Set _NET_CLIENT_LIST."
   (exwm--log)
@@ -262,7 +284,8 @@ want to match against EXWM internal variables such as `exwm-title',
         (make-instance 'xcb:ChangeSaveSet
                        :mode xcb:SetMode:Insert
                        :window id))
-    (with-current-buffer (generate-new-buffer "*EXWM*")
+    (with-current-buffer (let ((exwm-input--skip-buffer-list-update t))
+                           (generate-new-buffer "*EXWM*"))
       ;; Keep the oldest X window first.
       (setq exwm--id-buffer-alist
             (nconc exwm--id-buffer-alist `((,id . ,(current-buffer)))))
@@ -324,12 +347,8 @@ want to match against EXWM internal variables such as `exwm-title',
         (with-slots (x y width height) exwm--geometry
           ;; Center window of type _NET_WM_WINDOW_TYPE_SPLASH
           (when (memq xcb:Atom:_NET_WM_WINDOW_TYPE_SPLASH exwm-window-type)
-            (let* ((workarea (elt exwm-workspace--workareas
-                                  (exwm-workspace--position exwm--frame)))
-                   (x* (aref workarea 0))
-                   (y* (aref workarea 1))
-                   (width* (aref workarea 2))
-                   (height* (aref workarea 3)))
+            (with-slots ((x* x) (y* y) (width* width) (height* height))
+                (exwm-workspace--workarea exwm--frame)
               (exwm--set-geometry id
                                   (+ x* (/ (- width* width) 2))
                                   (+ y* (/ (- height* height) 2))
@@ -347,7 +366,8 @@ want to match against EXWM internal variables such as `exwm-title',
                              :stack-mode xcb:StackMode:Below)))
         (xcb:flush exwm--connection)
         (setq exwm--id-buffer-alist (assq-delete-all id exwm--id-buffer-alist))
-        (let ((kill-buffer-query-functions nil))
+        (let ((kill-buffer-query-functions nil)
+              (exwm-input--skip-buffer-list-update t))
           (kill-buffer (current-buffer)))
         (throw 'return 'ignored))
       (let ((index (plist-get exwm--configurations 'workspace)))
@@ -390,29 +410,26 @@ want to match against EXWM internal variables such as `exwm-title',
       (if (plist-get exwm--configurations 'char-mode)
           (exwm-input-release-keyboard id)
         (exwm-input-grab-keyboard id))
-      (let ((simulation-keys (plist-get exwm--configurations 'simulation-keys))
-            (prefix-keys (plist-get exwm--configurations 'prefix-keys)))
-        (with-current-buffer (exwm--id->buffer id)
-          (when simulation-keys
-            (exwm-input-set-local-simulation-keys simulation-keys))
-          (when prefix-keys
-            (setq-local exwm-input-prefix-keys prefix-keys))))
+      (when-let ((simulation-keys (plist-get exwm--configurations 'simulation-keys)))
+        (exwm-input-set-local-simulation-keys simulation-keys))
+      (when-let ((prefix-keys (plist-get exwm--configurations 'prefix-keys)))
+        (setq-local exwm-input-prefix-keys prefix-keys))
       (setq exwm-workspace--switch-history-outdated t)
       (exwm--update-desktop id)
       (exwm-manage--update-ewmh-state id)
-      (with-current-buffer (exwm--id->buffer id)
-        (when (or (plist-get exwm--configurations 'fullscreen)
-                  (exwm-layout--fullscreen-p))
-          (setq exwm--ewmh-state (delq xcb:Atom:_NET_WM_STATE_FULLSCREEN
-                                       exwm--ewmh-state))
-          (exwm-layout-set-fullscreen id))
-        (run-hooks 'exwm-manage-finish-hook)))))
+      (exwm-manage--update-default-directory id)
+      (when (or (plist-get exwm--configurations 'fullscreen)
+                (exwm-layout--fullscreen-p))
+        (setq exwm--ewmh-state (delq xcb:Atom:_NET_WM_STATE_FULLSCREEN
+                                     exwm--ewmh-state))
+        (exwm-layout-set-fullscreen id))
+      (run-hooks 'exwm-manage-finish-hook))))
 
 (defun exwm-manage--unmanage-window (id &optional withdraw-only)
   "Unmanage window ID.
 
 If WITHDRAW-ONLY is non-nil, the X window will be properly placed back to the
-root window.  Set WITHDRAW-ONLY to 'quit if this functions is used when window
+root window.  Set WITHDRAW-ONLY to `quit' if this functions is used when window
 manager is shutting down."
   (let ((buffer (exwm--id->buffer id)))
     (exwm--log "Unmanage #x%x (buffer: %s, widthdraw: %s)"
@@ -427,7 +444,9 @@ manager is shutting down."
       (exwm-workspace--update-workareas)
       (dolist (f exwm-workspace--list)
         (exwm-workspace--set-fullscreen f)))
-    (when (buffer-live-p buffer)
+    (when (and (buffer-live-p buffer)
+               ;; Invoked from `exwm-manage--exit' upon disconnection.
+               (slot-value exwm--connection 'connected))
       (with-current-buffer buffer
         ;; Unmap the X window.
         (xcb:+request exwm--connection
@@ -509,8 +528,11 @@ manager is shutting down."
 
 (defun exwm-manage--kill-buffer-query-function ()
   "Run in `kill-buffer-query-functions'."
-  (exwm--log "id=#x%x; buffer=%s" exwm--id (current-buffer))
+  (exwm--log "id=#x%x; buffer=%s" (or exwm--id 0) (current-buffer))
   (catch 'return
+    (when (or (not exwm--connection)
+              (not (slot-value exwm--connection 'connected)))
+      (throw 'return t))
     (when (or (not exwm--id)
               (xcb:+request-checked+request-check exwm--connection
                   (make-instance 'xcb:ChangeWindowAttributes
@@ -587,7 +609,8 @@ Would you like to kill it? "
         (throw 'return nil)))))
 
 (defun exwm-manage--kill-client (&optional id)
-  "Kill an X client."
+  "Kill X client ID.
+If ID is nil, kill X window corresponding to current buffer."
   (unless id (setq id (exwm--buffer->id (current-buffer))))
   (exwm--log "id=#x%x" id)
   (let* ((response (xcb:+request-unchecked+reply exwm--connection
@@ -605,14 +628,16 @@ Would you like to kill it? "
     (xcb:flush exwm--connection)))
 
 (defun exwm-manage--add-frame (frame)
-  "Run in `after-make-frame-functions'."
+  "Run in `after-make-frame-functions'.
+FRAME is the newly created frame."
   (exwm--log "frame=%s" frame)
   (when (display-graphic-p frame)
     (push (string-to-number (frame-parameter frame 'outer-window-id))
           exwm-manage--frame-outer-id-list)))
 
 (defun exwm-manage--remove-frame (frame)
-  "Run in `delete-frame-functions'."
+  "Run in `delete-frame-functions'.
+FRAME is the frame to be deleted."
   (exwm--log "frame=%s" frame)
   (when (display-graphic-p frame)
     (setq exwm-manage--frame-outer-id-list
@@ -620,7 +645,8 @@ Would you like to kill it? "
                 exwm-manage--frame-outer-id-list))))
 
 (defun exwm-manage--on-ConfigureRequest (data _synthetic)
-  "Handle ConfigureRequest event."
+  "Handle ConfigureRequest event.
+DATA contains unmarshalled ConfigureRequest event data."
   (exwm--log)
   (let ((obj (make-instance 'xcb:ConfigureRequest))
         buffer edges width-delta height-delta)
@@ -710,7 +736,8 @@ border-width: %d; sibling: #x%x; stack-mode: %d"
   (xcb:flush exwm--connection))
 
 (defun exwm-manage--on-MapRequest (data _synthetic)
-  "Handle MapRequest event."
+  "Handle MapRequest event.
+DATA contains unmarshalled MapRequest event data."
   (let ((obj (make-instance 'xcb:MapRequest)))
     (xcb:unmarshal obj data)
     (with-slots (parent window) obj
@@ -730,7 +757,8 @@ border-width: %d; sibling: #x%x; stack-mode: %d"
           (exwm-manage--manage-window window))))))
 
 (defun exwm-manage--on-UnmapNotify (data _synthetic)
-  "Handle UnmapNotify event."
+  "Handle UnmapNotify event.
+DATA contains unmarshalled UnmapNotify event data."
   (let ((obj (make-instance 'xcb:UnmapNotify)))
     (xcb:unmarshal obj data)
     (with-slots (window) obj
@@ -738,7 +766,8 @@ border-width: %d; sibling: #x%x; stack-mode: %d"
       (exwm-manage--unmanage-window window t))))
 
 (defun exwm-manage--on-MapNotify (data _synthetic)
-  "Handle MapNotify event."
+  "Handle MapNotify event.
+DATA contains unmarshalled MapNotify event data."
   (let ((obj (make-instance 'xcb:MapNotify)))
     (xcb:unmarshal obj data)
     (with-slots (window) obj
@@ -763,7 +792,9 @@ border-width: %d; sibling: #x%x; stack-mode: %d"
         (xcb:flush exwm--connection)))))
 
 (defun exwm-manage--on-DestroyNotify (data synthetic)
-  "Handle DestroyNotify event."
+  "Handle DestroyNotify event.
+DATA contains unmarshalled DestroyNotify event data.
+SYNTHETIC indicates whether the event is a synthetic event."
   (unless synthetic
     (exwm--log)
     (let ((obj (make-instance 'xcb:DestroyNotify)))
diff --git a/third_party/exwm/exwm-randr.el b/third_party/exwm/exwm-randr.el
index 68bfdd70e9..7f0e50559b 100644
--- a/third_party/exwm/exwm-randr.el
+++ b/third_party/exwm/exwm-randr.el
@@ -1,6 +1,6 @@
 ;;; exwm-randr.el --- RandR Module for EXWM  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -52,9 +52,10 @@
 (require 'exwm-core)
 (require 'exwm-workspace)
 
+(declare-function x-get-atom-name "C source code" (VALUE &optional FRAME))
+
 (defgroup exwm-randr nil
   "RandR."
-  :version "25.3"
   :group 'exwm)
 
 (defcustom exwm-randr-refresh-hook nil
@@ -89,10 +90,6 @@ corresponding monitors whenever the monitors are active.
   \\='(1 \"HDMI-1\" 3 \"DP-1\")"
   :type '(plist :key-type integer :value-type string))
 
-(with-no-warnings
-  (define-obsolete-variable-alias 'exwm-randr-workspace-output-plist
-    'exwm-randr-workspace-monitor-plist "27.1"))
-
 (defvar exwm-randr--last-timestamp 0 "Used for debouncing events.")
 
 (defvar exwm-randr--prev-screen-change-seqnum nil
@@ -267,9 +264,6 @@ In a mirroring setup some monitors overlap and should be treated as one."
       (xcb:flush exwm--connection)
       (run-hooks 'exwm-randr-refresh-hook))))
 
-(define-obsolete-function-alias 'exwm-randr--refresh #'exwm-randr-refresh
-  "27.1")
-
 (defun exwm-randr--on-ScreenChangeNotify (data _synthetic)
   "Handle `ScreenChangeNotify' event.
 
diff --git a/third_party/exwm/exwm-systemtray.el b/third_party/exwm/exwm-systemtray.el
index 43b3e1eaef..9e57dae4eb 100644
--- a/third_party/exwm/exwm-systemtray.el
+++ b/third_party/exwm/exwm-systemtray.el
@@ -1,7 +1,7 @@
 ;;; exwm-systemtray.el --- System Tray Module for  -*- lexical-binding: t -*-
 ;;;                        EXWM
 
-;; Copyright (C) 2016-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2016-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -30,6 +30,7 @@
 
 ;;; Code:
 
+(require 'xcb-ewmh)
 (require 'xcb-icccm)
 (require 'xcb-xembed)
 (require 'xcb-systemtray)
@@ -37,6 +38,8 @@
 (require 'exwm-core)
 (require 'exwm-workspace)
 
+(declare-function exwm-workspace--workarea "exwm-workspace.el" (frame))
+
 (defclass exwm-systemtray--icon ()
   ((width :initarg :width)
    (height :initarg :height)
@@ -46,7 +49,7 @@
 (defclass xcb:systemtray:-ClientMessage
   (xcb:icccm:--ClientMessage xcb:ClientMessage)
   ((format :initform 32)
-   (type :initform xcb:Atom:MANAGER)
+   (type :initform 'xcb:Atom:MANAGER)
    (time :initarg :time :type xcb:TIMESTAMP)      ;new slot
    (selection :initarg :selection :type xcb:ATOM) ;new slot
    (owner :initarg :owner :type xcb:WINDOW))      ;new slot
@@ -54,7 +57,6 @@
 
 (defgroup exwm-systemtray nil
   "System tray."
-  :version "25.3"
   :group 'exwm)
 
 (defcustom exwm-systemtray-height nil
@@ -67,44 +69,49 @@ You shall use the default value if using auto-hide minibuffer."
   "Gap between icons."
   :type 'integer)
 
+(defvar exwm-systemtray--connection nil "The X connection.")
+
 (defvar exwm-systemtray--embedder-window nil "The embedder window.")
+(defvar exwm-systemtray--embedder-window-depth nil
+  "The embedder window's depth.")
 
-(defcustom exwm-systemtray-background-color nil
+(defcustom exwm-systemtray-background-color 'workspace-background
   "Background color of systemtray.
-
-This should be a color, or nil for transparent background."
-  :type '(choice (const :tag "Transparent" nil)
-                 (color))
+This should be a color, the symbol `workspace-background' for the background
+color of current workspace frame, or the symbol `transparent' for transparent
+background.
+
+Transparent background is not yet supported when Emacs uses 32-bit depth
+visual, as reported by `x-display-planes'.  The X resource \"Emacs.visualClass:
+TrueColor-24\" can be used to force Emacs to use 24-bit depth."
+  :type '(choice (const :tag "Transparent" transparent)
+                 (const :tag "Frame background" workspace-background)
+                 (color :tag "Color"))
   :initialize #'custom-initialize-default
   :set (lambda (symbol value)
+         (when (and (eq value 'transparent)
+                    (not (exwm-systemtray--transparency-supported-p)))
+           (display-warning 'exwm-systemtray
+                            "Transparent background is not supported yet when \
+using 32-bit depth.  Using `workspace-background' instead.")
+           (setq value 'workspace-background))
          (set-default symbol value)
-         ;; Change the background color for embedder.
-         (when (and exwm--connection
+         (when (and exwm-systemtray--connection
                     exwm-systemtray--embedder-window)
-           (let ((background-pixel (exwm--color->pixel value)))
-             (xcb:+request exwm--connection
-                 (make-instance 'xcb:ChangeWindowAttributes
-                                :window exwm-systemtray--embedder-window
-                                :value-mask (logior xcb:CW:BackPixmap
-                                                    (if background-pixel
-                                                        xcb:CW:BackPixel 0))
-                                :background-pixmap
-                                xcb:BackPixmap:ParentRelative
-                                :background-pixel background-pixel))
-             ;; Unmap & map to take effect immediately.
-             (xcb:+request exwm--connection
-                 (make-instance 'xcb:UnmapWindow
-                                :window exwm-systemtray--embedder-window))
-             (xcb:+request exwm--connection
-                 (make-instance 'xcb:MapWindow
-                                :window exwm-systemtray--embedder-window))
-             (xcb:flush exwm--connection)))))
+           ;; Change the background color for embedder.
+           (exwm-systemtray--set-background-color)
+           ;; Unmap & map to take effect immediately.
+           (xcb:+request exwm-systemtray--connection
+                         (make-instance 'xcb:UnmapWindow
+                                        :window exwm-systemtray--embedder-window))
+           (xcb:+request exwm-systemtray--connection
+                         (make-instance 'xcb:MapWindow
+                                        :window exwm-systemtray--embedder-window))
+           (xcb:flush exwm-systemtray--connection))))
 
 ;; GTK icons require at least 16 pixels to show normally.
 (defconst exwm-systemtray--icon-min-size 16 "Minimum icon size.")
 
-(defvar exwm-systemtray--connection nil "The X connection.")
-
 (defvar exwm-systemtray--list nil "The icon list.")
 
 (defvar exwm-systemtray--selection-owner-window nil
@@ -113,7 +120,7 @@ This should be a color, or nil for transparent background."
 (defvar xcb:Atom:_NET_SYSTEM_TRAY_S0)
 
 (defun exwm-systemtray--embed (icon)
-  "Embed an icon."
+  "Embed an ICON."
   (exwm--log "Try to embed #x%x" icon)
   (let ((info (xcb:+request-unchecked+reply exwm-systemtray--connection
                   (make-instance 'xcb:xembed:get-_XEMBED_INFO
@@ -202,7 +209,7 @@ This should be a color, or nil for transparent background."
       (exwm-systemtray--refresh))))
 
 (defun exwm-systemtray--unembed (icon)
-  "Unembed an icon."
+  "Unembed an ICON."
   (exwm--log "Unembed #x%x" icon)
   (xcb:+request exwm-systemtray--connection
       (make-instance 'xcb:UnmapWindow :window icon))
@@ -234,14 +241,13 @@ This should be a color, or nil for transparent background."
         (setq x (+ x (slot-value (cdr pair) 'width)
                    exwm-systemtray-icon-gap))
         (setq map t)))
-    (let ((workarea (elt exwm-workspace--workareas
-                         exwm-workspace-current-index)))
+    (let ((workarea (exwm-workspace--workarea exwm-workspace-current-index)))
       (xcb:+request exwm-systemtray--connection
           (make-instance 'xcb:ConfigureWindow
                          :window exwm-systemtray--embedder-window
                          :value-mask (logior xcb:ConfigWindow:X
                                              xcb:ConfigWindow:Width)
-                         :x (- (aref workarea 2) x)
+                         :x (- (slot-value workarea 'width) x)
                          :width x)))
     (when map
       (xcb:+request exwm-systemtray--connection
@@ -249,8 +255,83 @@ This should be a color, or nil for transparent background."
                          :window exwm-systemtray--embedder-window))))
   (xcb:flush exwm-systemtray--connection))
 
+(defun exwm-systemtray--refresh-background-color (&optional remap)
+  "Refresh background color after theme change or workspace switch.
+If REMAP is not nil, map and unmap the embedder window so that the background is
+redrawn."
+  ;; Only `workspace-background' is dependent on current theme and workspace.
+  (when (eq 'workspace-background exwm-systemtray-background-color)
+    (exwm-systemtray--set-background-color)
+    (when remap
+      (xcb:+request exwm-systemtray--connection
+                    (make-instance 'xcb:UnmapWindow
+                                   :window exwm-systemtray--embedder-window))
+      (xcb:+request exwm-systemtray--connection
+                    (make-instance 'xcb:MapWindow
+                                   :window exwm-systemtray--embedder-window))
+      (xcb:flush exwm-systemtray--connection))))
+
+(defun exwm-systemtray--set-background-color ()
+  "Change the background color of the embedder.
+The color is set according to `exwm-systemtray-background-color'.
+
+Note that this function does not change the current contents of the embedder
+window; unmap & map are necessary for the background color to take effect."
+  (when (and exwm-systemtray--connection
+             exwm-systemtray--embedder-window)
+    (let* ((color (cl-case exwm-systemtray-background-color
+                    ((transparent nil) ; nil means transparent as well
+                     (if (exwm-systemtray--transparency-supported-p)
+                         nil
+                       (message "%s" "[EXWM] system tray does not support \
+`transparent' background; using `workspace-background' instead")
+                       (face-background 'default exwm-workspace--current)))
+                    (workspace-background
+                     (face-background 'default exwm-workspace--current))
+                    (t exwm-systemtray-background-color)))
+           (background-pixel (exwm--color->pixel color)))
+      (xcb:+request exwm-systemtray--connection
+                    (make-instance 'xcb:ChangeWindowAttributes
+                                   :window exwm-systemtray--embedder-window
+                                   ;; Either-or.  A `background-pixel' of nil
+                                   ;; means simulate transparency.  We use
+                                   ;; `xcb:CW:BackPixmap' together with
+                                   ;; `xcb:BackPixmap:ParentRelative' do that,
+                                   ;; but this only works when the parent
+                                   ;; window's visual (Emacs') has the same
+                                   ;; visual depth.
+                                   :value-mask (if background-pixel
+                                                   xcb:CW:BackPixel
+                                                 xcb:CW:BackPixmap)
+                                   ;; Due to the :value-mask above,
+                                   ;; :background-pixmap only takes effect when
+                                   ;; `transparent' is requested and supported
+                                   ;; (visual depth of Emacs and of system tray
+                                   ;; are equal).  Setting
+                                   ;; `xcb:BackPixmap:ParentRelative' when
+                                   ;; that's not the case would produce an
+                                   ;; `xcb:Match' error.
+                                   :background-pixmap xcb:BackPixmap:ParentRelative
+                                   :background-pixel background-pixel)))))
+
+(defun exwm-systemtray--transparency-supported-p ()
+  "Check whether transparent background is supported.
+EXWM system tray supports transparency when the visual depth of the system tray
+window matches that of Emacs.  The visual depth of the system tray window is the
+default visual depth of the display.
+
+Sections \"Visual and background pixmap handling\" and
+\"_NET_SYSTEM_TRAY_VISUAL\" of the System Tray Protocol Specification
+\(https://specifications.freedesktop.org/systemtray-spec/systemtray-spec-latest.html#visuals)
+indicate how to support actual transparency."
+  (let ((planes (x-display-planes)))
+    (if exwm-systemtray--embedder-window-depth
+        (= planes exwm-systemtray--embedder-window-depth)
+      (<= planes 24))))
+
 (defun exwm-systemtray--on-DestroyNotify (data _synthetic)
-  "Unembed icons on DestroyNotify."
+  "Unembed icons on DestroyNotify.
+Argument DATA contains the raw event data."
   (exwm--log)
   (let ((obj (make-instance 'xcb:DestroyNotify)))
     (xcb:unmarshal obj data)
@@ -259,7 +340,8 @@ This should be a color, or nil for transparent background."
         (exwm-systemtray--unembed window)))))
 
 (defun exwm-systemtray--on-ReparentNotify (data _synthetic)
-  "Unembed icons on ReparentNotify."
+  "Unembed icons on ReparentNotify.
+Argument DATA contains the raw event data."
   (exwm--log)
   (let ((obj (make-instance 'xcb:ReparentNotify)))
     (xcb:unmarshal obj data)
@@ -269,7 +351,8 @@ This should be a color, or nil for transparent background."
         (exwm-systemtray--unembed window)))))
 
 (defun exwm-systemtray--on-ResizeRequest (data _synthetic)
-  "Resize the tray icon on ResizeRequest."
+  "Resize the tray icon on ResizeRequest.
+Argument DATA contains the raw event data."
   (exwm--log)
   (let ((obj (make-instance 'xcb:ResizeRequest))
         attr)
@@ -297,7 +380,8 @@ This should be a color, or nil for transparent background."
         (exwm-systemtray--refresh)))))
 
 (defun exwm-systemtray--on-PropertyNotify (data _synthetic)
-  "Map/Unmap the tray icon on PropertyNotify."
+  "Map/Unmap the tray icon on PropertyNotify.
+Argument DATA contains the raw event data."
   (exwm--log)
   (let ((obj (make-instance 'xcb:PropertyNotify))
         attr info visible)
@@ -322,7 +406,8 @@ This should be a color, or nil for transparent background."
           (exwm-systemtray--refresh))))))
 
 (defun exwm-systemtray--on-ClientMessage (data _synthetic)
-  "Handle client messages."
+  "Handle client messages.
+Argument DATA contains the raw event data."
   (let ((obj (make-instance 'xcb:ClientMessage))
         opcode data32)
     (xcb:unmarshal obj data)
@@ -341,7 +426,8 @@ This should be a color, or nil for transparent background."
                (exwm--log "Unknown opcode message: %s" obj)))))))
 
 (defun exwm-systemtray--on-KeyPress (data _synthetic)
-  "Forward all KeyPress events to Emacs frame."
+  "Forward all KeyPress events to Emacs frame.
+Argument DATA contains the raw event data."
   (exwm--log)
   ;; This function is only executed when there's no autohide minibuffer,
   ;; a workspace frame has the input focus and the pointer is over a
@@ -370,13 +456,18 @@ This should be a color, or nil for transparent background."
                                 (frame-parameter exwm-workspace--current
                                                  'window-id))
                        :x 0
-                       :y (- (elt (elt exwm-workspace--workareas
-                                       exwm-workspace-current-index)
-                                  3)
+                       :y (- (slot-value (exwm-workspace--workarea
+                                           exwm-workspace-current-index)
+                                         'height)
                              exwm-workspace--frame-y-offset
                              exwm-systemtray-height))))
+  (exwm-systemtray--refresh-background-color)
   (exwm-systemtray--refresh))
 
+(defun exwm-systemtray--on-theme-change (_theme)
+  "Refresh system tray upon theme change."
+  (exwm-systemtray--refresh-background-color 'remap))
+
 (defun exwm-systemtray--refresh-all ()
   "Reposition/Refresh the system tray."
   (exwm--log)
@@ -386,9 +477,9 @@ This should be a color, or nil for transparent background."
         (make-instance 'xcb:ConfigureWindow
                        :window exwm-systemtray--embedder-window
                        :value-mask xcb:ConfigWindow:Y
-                       :y (- (elt (elt exwm-workspace--workareas
-                                       exwm-workspace-current-index)
-                                  3)
+                       :y (- (slot-value (exwm-workspace--workarea
+                                           exwm-workspace-current-index)
+                                         'height)
                              exwm-workspace--frame-y-offset
                              exwm-systemtray-height))))
   (exwm-systemtray--refresh))
@@ -402,7 +493,8 @@ This should be a color, or nil for transparent background."
   (cl-assert (not exwm-systemtray--embedder-window))
   (unless exwm-systemtray-height
     (setq exwm-systemtray-height (max exwm-systemtray--icon-min-size
-                                      (line-pixel-height))))
+                                      (with-selected-window (minibuffer-window)
+                                        (line-pixel-height)))))
   ;; Create a new connection.
   (setq exwm-systemtray--connection (xcb:connect))
   (set-process-query-on-exit-flag (slot-value exwm-systemtray--connection
@@ -469,8 +561,7 @@ This should be a color, or nil for transparent background."
                        :data xcb:systemtray:ORIENTATION:HORZ)))
   ;; Create the embedder.
   (let ((id (xcb:generate-id exwm-systemtray--connection))
-        (background-pixel (exwm--color->pixel exwm-systemtray-background-color))
-        frame parent depth y)
+        frame parent embedder-depth embedder-visual embedder-colormap y)
     (setq exwm-systemtray--embedder-window id)
     (if (exwm-workspace--minibuffer-own-frame-p)
         (setq frame exwm-workspace--minibuffer
@@ -482,20 +573,26 @@ This should be a color, or nil for transparent background."
       (exwm-workspace--update-offsets)
       (setq frame exwm-workspace--current
             ;; Bottom aligned.
-            y (- (elt (elt exwm-workspace--workareas
-                           exwm-workspace-current-index)
-                      3)
+            y (- (slot-value (exwm-workspace--workarea
+                               exwm-workspace-current-index)
+                             'height)
                  exwm-workspace--frame-y-offset
                  exwm-systemtray-height)))
-    (setq parent (string-to-number (frame-parameter frame 'window-id))
-          depth (slot-value (xcb:+request-unchecked+reply
-                                exwm-systemtray--connection
-                                (make-instance 'xcb:GetGeometry
-                                               :drawable parent))
-                            'depth))
+    (setq parent (string-to-number (frame-parameter frame 'window-id)))
+    ;; Use default depth, visual and colormap (from root window), instead of
+    ;; Emacs frame's.  See Section "Visual and background pixmap handling" in
+    ;; "System Tray Protocol Specification 0.3".
+    (let* ((vdc (exwm--get-visual-depth-colormap exwm-systemtray--connection
+                                                 exwm--root)))
+      (setq embedder-visual (car vdc))
+      (setq embedder-depth (cadr vdc))
+      (setq embedder-colormap (caddr vdc)))
+    ;; Note down the embedder window's depth.  It will be used to check whether
+    ;; we can use xcb:BackPixmap:ParentRelative to emulate transparency.
+    (setq exwm-systemtray--embedder-window-depth embedder-depth)
     (xcb:+request exwm-systemtray--connection
         (make-instance 'xcb:CreateWindow
-                       :depth depth
+                       :depth embedder-depth
                        :wid id
                        :parent parent
                        :x 0
@@ -504,19 +601,29 @@ This should be a color, or nil for transparent background."
                        :height exwm-systemtray-height
                        :border-width 0
                        :class xcb:WindowClass:InputOutput
-                       :visual 0
-                       :value-mask (logior xcb:CW:BackPixmap
-                                           (if background-pixel
-                                               xcb:CW:BackPixel 0)
+                       :visual embedder-visual
+                       :colormap embedder-colormap
+                       :value-mask (logior xcb:CW:BorderPixel
+                                           xcb:CW:Colormap
                                            xcb:CW:EventMask)
-                       :background-pixmap xcb:BackPixmap:ParentRelative
-                       :background-pixel background-pixel
+                       :border-pixel 0
                        :event-mask xcb:EventMask:SubstructureNotify))
+    (exwm-systemtray--set-background-color)
     ;; Set _NET_WM_NAME.
     (xcb:+request exwm-systemtray--connection
         (make-instance 'xcb:ewmh:set-_NET_WM_NAME
                        :window id
-                       :data "EXWM: exwm-systemtray--embedder-window")))
+                       :data "EXWM: exwm-systemtray--embedder-window"))
+    ;; Set _NET_WM_WINDOW_TYPE.
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_WINDOW_TYPE
+                       :window id
+                       :data (vector xcb:Atom:_NET_WM_WINDOW_TYPE_DOCK)))
+    ;; Set _NET_SYSTEM_TRAY_VISUAL.
+    (xcb:+request exwm-systemtray--connection
+        (make-instance 'xcb:xembed:set-_NET_SYSTEM_TRAY_VISUAL
+                       :window exwm-systemtray--selection-owner-window
+                       :data embedder-visual)))
   (xcb:flush exwm-systemtray--connection)
   ;; Attach event listeners.
   (xcb:+event exwm-systemtray--connection 'xcb:DestroyNotify
@@ -536,6 +643,9 @@ This should be a color, or nil for transparent background."
   (add-hook 'exwm-workspace-switch-hook #'exwm-systemtray--on-workspace-switch)
   (add-hook 'exwm-workspace--update-workareas-hook
             #'exwm-systemtray--refresh-all)
+  ;; Add hook to update background colors.
+  (add-hook 'enable-theme-functions #'exwm-systemtray--on-theme-change)
+  (add-hook 'disable-theme-functions #'exwm-systemtray--on-theme-change)
   (add-hook 'menu-bar-mode-hook #'exwm-systemtray--refresh-all)
   (add-hook 'tool-bar-mode-hook #'exwm-systemtray--refresh-all)
   (when (boundp 'exwm-randr-refresh-hook)
@@ -548,27 +658,31 @@ This should be a color, or nil for transparent background."
   "Exit the systemtray module."
   (exwm--log)
   (when exwm-systemtray--connection
-    ;; Hide & reparent out the embedder before disconnection to prevent
-    ;; embedded icons from being reparented to an Emacs frame (which is the
-    ;; parent of the embedder).
-    (xcb:+request exwm-systemtray--connection
-        (make-instance 'xcb:UnmapWindow
-                       :window exwm-systemtray--embedder-window))
-    (xcb:+request exwm-systemtray--connection
-        (make-instance 'xcb:ReparentWindow
-                       :window exwm-systemtray--embedder-window
-                       :parent exwm--root
-                       :x 0
-                       :y 0))
-    (xcb:disconnect exwm-systemtray--connection)
+    (when (slot-value exwm-systemtray--connection 'connected)
+      ;; Hide & reparent out the embedder before disconnection to prevent
+      ;; embedded icons from being reparented to an Emacs frame (which is the
+      ;; parent of the embedder).
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:UnmapWindow
+                         :window exwm-systemtray--embedder-window))
+      (xcb:+request exwm-systemtray--connection
+          (make-instance 'xcb:ReparentWindow
+                         :window exwm-systemtray--embedder-window
+                         :parent exwm--root
+                         :x 0
+                         :y 0))
+      (xcb:disconnect exwm-systemtray--connection))
     (setq exwm-systemtray--connection nil
           exwm-systemtray--list nil
           exwm-systemtray--selection-owner-window nil
-          exwm-systemtray--embedder-window nil)
+          exwm-systemtray--embedder-window nil
+          exwm-systemtray--embedder-window-depth nil)
     (remove-hook 'exwm-workspace-switch-hook
                  #'exwm-systemtray--on-workspace-switch)
     (remove-hook 'exwm-workspace--update-workareas-hook
                  #'exwm-systemtray--refresh-all)
+    (remove-hook 'enable-theme-functions #'exwm-systemtray--on-theme-change)
+    (remove-hook 'disable-theme-functions #'exwm-systemtray--on-theme-change)
     (remove-hook 'menu-bar-mode-hook #'exwm-systemtray--refresh-all)
     (remove-hook 'tool-bar-mode-hook #'exwm-systemtray--refresh-all)
     (when (boundp 'exwm-randr-refresh-hook)
diff --git a/third_party/exwm/exwm-workspace.el b/third_party/exwm/exwm-workspace.el
index fc68e1b070..89be697159 100644
--- a/third_party/exwm/exwm-workspace.el
+++ b/third_party/exwm/exwm-workspace.el
@@ -1,6 +1,6 @@
 ;;; exwm-workspace.el --- Workspace Module for EXWM  -*- lexical-binding: t -*-
 
-;; Copyright (C) 1015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 1015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -31,7 +31,6 @@
 
 (defgroup exwm-workspace nil
   "Workspace."
-  :version "25.3"
   :group 'exwm)
 
 (defcustom exwm-workspace-switch-hook nil
@@ -39,8 +38,8 @@
   :type 'hook)
 
 (defcustom exwm-workspace-list-change-hook nil
-  "Normal hook run when the workspace list is changed (workspace added,
-deleted, moved, etc)."
+  "Normal hook run when the workspace list is changed.
+This happens when a workspace is added, deleted, moved, etc."
   :type 'hook)
 
 (defcustom exwm-workspace-show-all-buffers nil
@@ -74,8 +73,7 @@ A restart is required for this change to take effect."
   :type 'integer)
 
 (defcustom exwm-workspace-switch-create-limit 10
-  "Number of workspaces `exwm-workspace-switch-create' allowed to create
-each time."
+  "Number of workspaces `exwm-workspace-switch-create' is allowed to create."
   :type 'integer)
 
 (defvar exwm-workspace-current-index 0 "Index of current active workspace.")
@@ -85,9 +83,6 @@ each time."
 
 If the minibuffer is detached, this value is 0.")
 
-(defvar exwm-workspace--client nil
-  "The 'client' frame parameter of emacsclient frames.")
-
 (defvar exwm-workspace--create-silently nil
   "When non-nil workspaces are created in the background (not switched to).
 
@@ -153,8 +148,8 @@ Please manually run the hook `exwm-workspace-list-change-hook' afterwards.")
 
 (defsubst exwm-workspace--position (frame)
   "Retrieve index of given FRAME in workspace list.
-
-NIL if FRAME is not a workspace"
+NIL if FRAME is not a workspace."
+  (declare (indent defun))
   (cl-position frame exwm-workspace--list))
 
 (defsubst exwm-workspace--count ()
@@ -163,28 +158,23 @@ NIL if FRAME is not a workspace"
 
 (defsubst exwm-workspace--workspace-p (frame)
   "Return t if FRAME is a workspace."
+  (declare (indent defun))
   (memq frame exwm-workspace--list))
 
-(defvar exwm-workspace--client-p-hash-table
-  (make-hash-table :test 'eq :weakness 'key)
-  "Used to cache the results of calling โ€˜exwm-workspace--client-pโ€™.")
-
-(defsubst exwm-workspace--client-p (&optional frame)
-  "Return non-nil if FRAME is an emacsclient frame."
-  (let* ((frame (or frame (selected-frame)))
-         (cached-value
-          (gethash frame exwm-workspace--client-p-hash-table 'absent)))
-    (if (eq cached-value 'absent)
-        (puthash frame
-                 (or (frame-parameter frame 'client)
-                     (not (display-graphic-p frame)))
-                 exwm-workspace--client-p-hash-table)
-        cached-value)))
+(defsubst exwm-workspace--workarea (frame)
+  "Return workarea corresponding to FRAME.
+FRAME may be either a workspace frame or a workspace position."
+  (declare (indent defun))
+  (elt exwm-workspace--workareas
+       (if (integerp frame)
+           frame
+         (exwm-workspace--position frame))))
 
 (defvar exwm-workspace--switch-map nil
   "Keymap used for interactively selecting workspace.")
 
 (defun exwm-workspace--init-switch-map ()
+  "Initialize variable `exwm-workspace--switch-map'."
   (let ((map (make-sparse-keymap)))
     (define-key map [t] (lambda () (interactive)))
     (define-key map "+" #'exwm-workspace--prompt-add)
@@ -235,7 +225,8 @@ NIL if FRAME is not a workspace"
    (t (user-error "[EXWM] Invalid workspace: %s" frame-or-index))))
 
 (defun exwm-workspace--prompt-for-workspace (&optional prompt)
-  "Prompt for a workspace, returning the workspace frame."
+  "Prompt for a workspace, returning the workspace frame.
+Show PROMPT to the user if non-nil."
   (exwm-workspace--update-switch-history)
   (let* ((current-idx (exwm-workspace--position exwm-workspace--current))
          (history-add-new-input nil)  ;prevent modifying history
@@ -264,7 +255,6 @@ NIL if FRAME is not a workspace"
   (when (and exwm-workspace--prompt-delete-allowed
              (< 1 (exwm-workspace--count)))
     (let ((frame (elt exwm-workspace--list (1- minibuffer-history-position))))
-      (exwm-workspace--get-remove-frame-next-workspace frame)
       (if (eq frame exwm-workspace--current)
           ;; Abort the recursive minibuffer if deleting the current workspace.
           (progn
@@ -351,63 +341,69 @@ NIL if FRAME is not a workspace"
 
 (defun exwm-workspace--update-workareas ()
   "Update `exwm-workspace--workareas'."
-  (let ((root-width (x-display-pixel-width))
-        (root-height (x-display-pixel-height))
-        workareas
-        edge width position
-        delta)
-    ;; Calculate workareas with no struts.
-    (if (frame-parameter (car exwm-workspace--list) 'exwm-geometry)
-        ;; Use the 'exwm-geometry' frame parameter if possible.
-        (dolist (f exwm-workspace--list)
-          (with-slots (x y width height) (frame-parameter f 'exwm-geometry)
-            (setq workareas (append workareas
-                                    (list (vector x y width height))))))
-      ;; Fall back to use the screen size.
-      (let ((workarea (vector 0 0 root-width root-height)))
-        (setq workareas (make-list (exwm-workspace--count) workarea))))
+  (let* ((root-width (x-display-pixel-width))
+         (root-height (x-display-pixel-height))
+         ;; Get workareas prior to struts.
+         (workareas (mapcar
+                     (lambda (frame)
+                       (if-let (rect (frame-parameter frame 'exwm-geometry))
+                           ;; Use the 'exwm-geometry' frame parameter if it
+                           ;; exists.  Make sure to clone it, will be modified
+                           ;; below!
+                           (clone rect)
+                         ;; Fall back to use the screen size.
+                         (make-instance 'xcb:RECTANGLE
+                                        :x 0
+                                        :y 0
+                                        :width root-width
+                                        :height root-height)))
+                     exwm-workspace--list)))
     ;; Exclude areas occupied by struts.
     (dolist (struts exwm-workspace--struts)
-      (setq edge (aref struts 0)
-            width (aref struts 1)
-            position (aref struts 2))
-      (dolist (w workareas)
-        (pcase edge
-          ;; Left and top are always processed first.
-          (`left
-           (setq delta (- (aref w 0) width))
-           (when (and (< delta 0)
-                      (or (not position)
-                          (< (max (aref position 0) (aref w 1))
-                             (min (aref position 1)
-                                  (+ (aref w 1) (aref w 3))))))
-             (cl-incf (aref w 2) delta)
-             (setf (aref w 0) width)))
-          (`right
-           (setq delta (- root-width (aref w 0) (aref w 2) width))
-           (when (and (< delta 0)
-                      (or (not position)
-                          (< (max (aref position 0) (aref w 1))
-                             (min (aref position 1)
-                                  (+ (aref w 1) (aref w 3))))))
-             (cl-incf (aref w 2) delta)))
-          (`top
-           (setq delta (- (aref w 1) width))
-           (when (and (< delta 0)
-                      (or (not position)
-                          (< (max (aref position 0) (aref w 0))
-                             (min (aref position 1)
-                                  (+ (aref w 0) (aref w 2))))))
-             (cl-incf (aref w 3) delta)
-             (setf (aref w 1) width)))
-          (`bottom
-           (setq delta (- root-height (aref w 1) (aref w 3) width))
-           (when (and (< delta 0)
-                      (or (not position)
-                          (< (max (aref position 0) (aref w 0))
-                             (min (aref position 1)
-                                  (+ (aref w 0) (aref w 2))))))
-             (cl-incf (aref w 3) delta))))))
+      (let* ((edge (aref struts 0))
+             (size (aref struts 1))
+             (position (aref struts 2))
+             (beg (and position (aref position 0)))
+             (end (and position (aref position 1)))
+             delta)
+        (dolist (w workareas)
+          (with-slots (x y width height) w
+            (pcase edge
+              ;; Left and top are always processed first.
+              ('left
+               (setq delta (- size x))
+               (when (and (< 0 delta)
+                          (< delta width)
+                          (or (not position)
+                              (< (max beg y)
+                                 (min end (+ y height)))))
+                 (cl-decf width delta)
+                 (setf x size)))
+              ('right
+               (setq delta (- size (- root-width x width)))
+               (when (and (< 0 delta)
+                          (< delta width)
+                          (or (not position)
+                              (< (max beg y)
+                                 (min end (+ y height)))))
+                 (cl-decf width delta)))
+              ('top
+               (setq delta (- size y))
+               (when (and (< 0 delta)
+                          (< delta height)
+                          (or (not position)
+                              (< (max beg x)
+                                 (min end (+ x width)))))
+                 (cl-decf height delta)
+                 (setf y size)))
+              ('bottom
+               (setq delta (- size (- root-height y height)))
+               (when (and (< 0 delta)
+                          (< delta height)
+                          (or (not position)
+                              (< (max beg x)
+                                 (min end (+ x width)))))
+                 (cl-decf height delta))))))))
     ;; Save the result.
     (setq exwm-workspace--workareas workareas)
     (xcb:flush exwm--connection))
@@ -443,8 +439,9 @@ NIL if FRAME is not a workspace"
                   exwm-workspace--window-y-offset (- (elt edges 1) y))))))))
 
 (defun exwm-workspace--set-active (frame active)
-  "Make frame FRAME active on its monitor."
-  (exwm--log "active=%s; frame=%s" frame active)
+  "Make frame FRAME active on its monitor.
+ACTIVE indicates whether to set the frame active or inactive."
+  (exwm--log "active=%s; frame=%s" active frame)
   (set-frame-parameter frame 'exwm-active active)
   (if active
       (exwm-workspace--set-fullscreen frame)
@@ -453,30 +450,25 @@ NIL if FRAME is not a workspace"
   (xcb:flush exwm--connection))
 
 (defun exwm-workspace--active-p (frame)
-  "Return non-nil if FRAME is active"
+  "Return non-nil if FRAME is active."
   (frame-parameter frame 'exwm-active))
 
 (defun exwm-workspace--set-fullscreen (frame)
   "Make frame FRAME fullscreen according to `exwm-workspace--workareas'."
   (exwm--log "frame=%s" frame)
-  (let ((workarea (elt exwm-workspace--workareas
-                       (exwm-workspace--position frame)))
-        (id (frame-parameter frame 'exwm-outer-id))
-        (container (frame-parameter frame 'exwm-container))
-        x y width height)
-    (setq x (aref workarea 0)
-          y (aref workarea 1)
-          width (aref workarea 2)
-          height (aref workarea 3))
-    (exwm--log "x=%s; y=%s; w=%s; h=%s" x y width height)
-    (when (and (eq frame exwm-workspace--current)
-               (exwm-workspace--minibuffer-own-frame-p))
-      (exwm-workspace--resize-minibuffer-frame))
-    (if (exwm-workspace--active-p frame)
-        (exwm--set-geometry container x y width height)
-      (exwm--set-geometry container x y 1 1))
-    (exwm--set-geometry id nil nil width height)
-    (xcb:flush exwm--connection))
+  (let ((id (frame-parameter frame 'exwm-outer-id))
+        (container (frame-parameter frame 'exwm-container)))
+    (with-slots (x y width height)
+        (exwm-workspace--workarea frame)
+      (exwm--log "x=%s; y=%s; w=%s; h=%s" x y width height)
+      (when (and (eq frame exwm-workspace--current)
+                 (exwm-workspace--minibuffer-own-frame-p))
+        (exwm-workspace--resize-minibuffer-frame))
+      (if (exwm-workspace--active-p frame)
+          (exwm--set-geometry container x y width height)
+        (exwm--set-geometry container x y 1 1))
+      (exwm--set-geometry id nil nil width height)
+      (xcb:flush exwm--connection)))
   ;; This is only used for workspace initialization.
   (when exwm-workspace--fullscreen-frame-count
     (cl-incf exwm-workspace--fullscreen-frame-count)))
@@ -484,20 +476,20 @@ NIL if FRAME is not a workspace"
 (defun exwm-workspace--resize-minibuffer-frame ()
   "Resize minibuffer (and its container) to fit the size of workspace."
   (cl-assert (exwm-workspace--minibuffer-own-frame-p))
-  (let ((workarea (elt exwm-workspace--workareas exwm-workspace-current-index))
+  (let ((workarea (exwm-workspace--workarea exwm-workspace-current-index))
         (container (frame-parameter exwm-workspace--minibuffer
                                     'exwm-container))
         y width)
     (setq y (if (eq exwm-workspace-minibuffer-position 'top)
-                (- (aref workarea 1)
+                (- (slot-value workarea 'y)
                    exwm-workspace--attached-minibuffer-height)
               ;; Reset the frame size.
               (set-frame-height exwm-workspace--minibuffer 1)
               (redisplay)               ;FIXME.
-              (+ (aref workarea 1) (aref workarea 3)
+              (+ (slot-value workarea 'y) (slot-value workarea 'height)
                  (- (frame-pixel-height exwm-workspace--minibuffer))
                  exwm-workspace--attached-minibuffer-height))
-          width (aref workarea 2))
+          width (slot-value workarea 'width))
     (xcb:+request exwm--connection
         (make-instance 'xcb:ConfigureWindow
                        :window container
@@ -508,7 +500,7 @@ NIL if FRAME is not a workspace"
                                                xcb:ConfigWindow:Sibling
                                              0)
                                            xcb:ConfigWindow:StackMode)
-                       :x (aref workarea 0)
+                       :x (slot-value workarea 'x)
                        :y y
                        :width width
                        :sibling exwm-manage--desktop
@@ -571,11 +563,13 @@ PREFIX-DIGITS is a list of the digits introduced so far."
 
 ;;;###autoload
 (defun exwm-workspace-switch (frame-or-index &optional force)
-  "Switch to workspace INDEX (0-based).
+  "Switch to workspace FRAME-OR-INDEX (0-based).
 
 Query for the index if not specified when called interactively.  Passing a
 workspace frame as the first option or making use of the rest options are
-for internal use only."
+for internal use only.
+
+When FORCE is true, allow switching to current workspace."
   (interactive
    (list
     (cond
@@ -701,7 +695,7 @@ for internal use only."
 
 ;;;###autoload
 (defun exwm-workspace-switch-create (frame-or-index)
-  "Switch to workspace INDEX or creating it first if it does not exist yet.
+  "Switch to workspace FRAME-OR-INDEX creating it first non-existent.
 
 Passing a workspace frame as the first option is for internal use only."
   (interactive
@@ -830,7 +824,6 @@ INDEX must not exceed the current number of workspaces."
                      (exwm-workspace--workspace-from-frame-or-index
                       frame-or-index)
                    exwm-workspace--current)))
-      (exwm-workspace--get-remove-frame-next-workspace frame)
       (delete-frame frame))))
 
 (defun exwm-workspace--set-desktop (id)
@@ -988,7 +981,7 @@ INDEX must not exceed the current number of workspaces."
 
 ;;;###autoload
 (defun exwm-workspace-switch-to-buffer (buffer-or-name)
-  "Make the current Emacs window display another buffer."
+  "Make selected window display BUFFER-OR-NAME."
   (interactive
    (let ((inhibit-quit t))
      ;; Show all buffers
@@ -1040,7 +1033,7 @@ INDEX must not exceed the current number of workspaces."
         (switch-to-buffer buffer-or-name)))))
 
 (defun exwm-workspace-rename-buffer (newname)
-  "Rename a buffer."
+  "Rename current buffer to NEWNAME."
   (let ((hidden (= ?\s (aref newname 0)))
         (basename (replace-regexp-in-string "<[0-9]+>$" "" newname))
         (counter 1)
@@ -1056,10 +1049,12 @@ INDEX must not exceed the current number of workspaces."
                  buffer-list-update-hook)))
       (rename-buffer (concat (and hidden " ") newname)))))
 
-(defun exwm-workspace--x-create-frame (orig-fun params)
-  "Set override-redirect on the frame created by `x-create-frame'."
+(defun exwm-workspace--x-create-frame (orig-x-create-frame params)
+  "Set override-redirect on the frame created by `x-create-frame'.
+ORIG-X-CREATE-FRAME is the advised function `x-create-frame'.
+PARAMS are the original arguments."
   (exwm--log)
-  (let ((frame (funcall orig-fun params)))
+  (let ((frame (funcall orig-x-create-frame params)))
     (xcb:+request exwm--connection
         (make-instance 'xcb:ChangeWindowAttributes
                        :window (string-to-number
@@ -1078,7 +1073,7 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
 
 ;;;###autoload
 (defun exwm-workspace-attach-minibuffer ()
-  "Attach the minibuffer so that it always shows."
+  "Attach the minibuffer making it always visible."
   (interactive)
   (exwm--log)
   (when (and (exwm-workspace--minibuffer-own-frame-p)
@@ -1130,8 +1125,10 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
       (exwm-workspace-attach-minibuffer))))
 
 (defun exwm-workspace--update-minibuffer-height (&optional echo-area)
-  "Update the minibuffer frame height."
-  (unless (exwm-workspace--client-p)
+  "Update the minibuffer frame height.
+When ECHO-AREA is non-nil, take the size of the echo area into
+account when calculating the height."
+  (when (exwm--terminal-p)
     (let ((height
            (with-current-buffer
                (window-buffer (minibuffer-window exwm-workspace--minibuffer))
@@ -1153,9 +1150,9 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
       (set-frame-height exwm-workspace--minibuffer height))))
 
 (defun exwm-workspace--on-ConfigureNotify (data _synthetic)
-  "Adjust the container to fit the minibuffer frame."
-  (let ((obj (make-instance 'xcb:ConfigureNotify))
-        workarea y)
+  "Adjust the container to fit the minibuffer frame.
+DATA contains unmarshalled ConfigureNotify event data."
+  (let ((obj (make-instance 'xcb:ConfigureNotify)) y)
     (xcb:unmarshal obj data)
     (with-slots (window height) obj
       (when (eq (frame-parameter exwm-workspace--minibuffer 'exwm-outer-id)
@@ -1175,13 +1172,13 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
         (when (/= (exwm-workspace--count) (length exwm-workspace--workareas))
           ;; There is a chance the workareas are not updated timely.
           (exwm-workspace--update-workareas))
-        (setq workarea (elt exwm-workspace--workareas
-                            exwm-workspace-current-index)
-              y (if (eq exwm-workspace-minibuffer-position 'top)
-                    (- (aref workarea 1)
-                       exwm-workspace--attached-minibuffer-height)
-                  (+ (aref workarea 1) (aref workarea 3) (- height)
-                     exwm-workspace--attached-minibuffer-height)))
+        (with-slots ((y* y) (height* height))
+            (exwm-workspace--workarea exwm-workspace-current-index)
+          (setq y (if (eq exwm-workspace-minibuffer-position 'top)
+                      (- y*
+                         exwm-workspace--attached-minibuffer-height)
+                    (+ y* height* (- height)
+                       exwm-workspace--attached-minibuffer-height))))
         (xcb:+request exwm--connection
             (make-instance 'xcb:ConfigureWindow
                            :window (frame-parameter exwm-workspace--minibuffer
@@ -1193,7 +1190,8 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
         (xcb:flush exwm--connection)))))
 
 (defun exwm-workspace--display-buffer (buffer alist)
-  "Display BUFFER as if the current workspace is selected."
+  "Display BUFFER as if the current workspace were selected.
+ALIST is an action alist, as accepted by function `display-buffer'."
   ;; Only when the floating minibuffer frame is selected.
   ;; This also protect this functions from being recursively called.
   (when (eq (selected-frame) exwm-workspace--minibuffer)
@@ -1245,10 +1243,10 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
   (xcb:flush exwm--connection))
 
 (defun exwm-workspace--on-minibuffer-setup ()
-  "Run in minibuffer-setup-hook to show the minibuffer and its container."
+  "Run in `minibuffer-setup-hook' to show the minibuffer and its container."
   (exwm--log)
   (when (and (= 1 (minibuffer-depth))
-             (not (exwm-workspace--client-p)))
+             (exwm--terminal-p))
     (add-hook 'post-command-hook #'exwm-workspace--update-minibuffer-height)
     (exwm-workspace--show-minibuffer))
   ;; FIXME: This is a temporary fix for the *Completions* buffer not
@@ -1267,19 +1265,19 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
         (window-preserve-size window)))))
 
 (defun exwm-workspace--on-minibuffer-exit ()
-  "Run in minibuffer-exit-hook to hide the minibuffer container."
+  "Run in `minibuffer-exit-hook' to hide the minibuffer container."
   (exwm--log)
   (when (and (= 1 (minibuffer-depth))
-             (not (exwm-workspace--client-p)))
+             (exwm--terminal-p))
     (remove-hook 'post-command-hook #'exwm-workspace--update-minibuffer-height)
     (exwm-workspace--hide-minibuffer)))
 
 (defun exwm-workspace--on-echo-area-dirty ()
   "Run when new message arrives to show the echo area and its container."
   (when (and (not (active-minibuffer-window))
-             (not (exwm-workspace--client-p))
              (or (current-message)
-                 cursor-in-echo-area))
+                 cursor-in-echo-area)
+             (exwm--terminal-p))
     (exwm-workspace--update-minibuffer-height t)
     (exwm-workspace--show-minibuffer)
     (unless (or (not exwm-workspace-display-echo-area-timeout)
@@ -1301,8 +1299,8 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
                           #'exwm-workspace--echo-area-maybe-clear))))
 
 (defun exwm-workspace--on-echo-area-clear ()
-  "Run in echo-area-clear-hook to hide echo area container."
-  (unless (exwm-workspace--client-p)
+  "Run in `echo-area-clear-hook' to hide echo area container."
+  (when (exwm--terminal-p)
     (unless (active-minibuffer-window)
       (exwm-workspace--hide-minibuffer))
     (when exwm-workspace--display-echo-area-timer
@@ -1332,8 +1330,6 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
     (set-frame-parameter frame 'exwm-outer-id outer-id)
     (set-frame-parameter frame 'exwm-id window-id)
     (set-frame-parameter frame 'exwm-container container)
-    ;; In case it's created by emacsclient.
-    (set-frame-parameter frame 'client nil)
     ;; Copy RandR frame parameters from the first workspace to
     ;; prevent potential problems.  The values do not matter here as
     ;; they'll be updated by the RandR module later.
@@ -1392,7 +1388,7 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
         (make-instance 'xcb:MapWindow :window container)))
   (xcb:flush exwm--connection)
   ;; Delay making the workspace fullscreen until Emacs becomes idle
-  (exwm--defer 0 #'set-frame-parameter frame 'fullscreen 'fullboth)
+  (exwm--defer 0 #'exwm-workspace--fullscreen-workspace frame)
   ;; Update EWMH properties.
   (exwm-workspace--update-ewmh-props)
   (if exwm-workspace--create-silently
@@ -1403,41 +1399,44 @@ Please check `exwm-workspace--minibuffer-own-frame-p' first."
                frame exwm-workspace-current-index original-index))
     (run-hooks 'exwm-workspace-list-change-hook)))
 
-(defun exwm-workspace--get-remove-frame-next-workspace (frame)
-  "Return the next workspace if workspace FRAME is removed.
-
-All X windows currently on workspace FRAME will be automatically moved to
-the next workspace."
+(defun exwm-workspace--get-next-workspace (frame)
+  "Return the next workspace if workspace FRAME were removed.
+Return nil if FRAME is the only workspace."
   (let* ((index (exwm-workspace--position frame))
          (lastp (= index (1- (exwm-workspace--count))))
          (nextw (elt exwm-workspace--list (+ index (if lastp -1 +1)))))
-    ;; Clients need to be moved to some other workspace before this being
-    ;; removed.
-    (dolist (pair exwm--id-buffer-alist)
-      (with-current-buffer (cdr pair)
-        (when (eq exwm--frame frame)
-          (exwm-workspace-move-window nextw exwm--id))))
-    nextw))
-
-(defun exwm-workspace--remove-frame-as-workspace (frame)
-  "Stop treating frame FRAME as a workspace."
+    (unless (eq frame nextw)
+      nextw)))
+
+(defun exwm-workspace--remove-frame-as-workspace (frame &optional quit)
+  "Stop treating FRAME as a workspace.
+When QUIT is non-nil cleanup avoid communicating with the X server."
   ;; TODO: restore all frame parameters (e.g. exwm-workspace, buffer-predicate,
   ;; etc)
   (exwm--log "Removing frame `%s' as workspace" frame)
-  (let* ((index (exwm-workspace--position frame))
-         (nextw (exwm-workspace--get-remove-frame-next-workspace frame)))
-    ;; Need to remove the workspace from the list in order for
-    ;; the correct calculation of indexes.
-    (setq exwm-workspace--list (delete frame exwm-workspace--list))
-    ;; Update the _NET_WM_DESKTOP property of each X window affected.
-    (dolist (pair exwm--id-buffer-alist)
-      (when (<= (1- index)
-                (exwm-workspace--position (buffer-local-value 'exwm--frame
-                                                              (cdr pair))))
-        (exwm-workspace--set-desktop (car pair))))
-    ;; If the current workspace is deleted, switch to next one.
-    (when (eq frame exwm-workspace--current)
-      (exwm-workspace-switch nextw)))
+  (unless quit
+    (let* ((next-frame (exwm-workspace--get-next-workspace frame))
+           (following-frames (cdr (memq frame exwm-workspace--list))))
+      ;; Need to remove the workspace from the list for the correct calculation of
+      ;; indexes below.
+      (setq exwm-workspace--list (delete frame exwm-workspace--list))
+      ;; Move the windows to the next workspace and switch to it.
+      (unless next-frame
+        ;; The user managed to delete the last workspace, so create a new one.
+        (exwm--log "Last workspace deleted; create a new one")
+        (let ((exwm-workspace--create-silently t))
+          (setq next-frame (make-frame))))
+      (dolist (pair exwm--id-buffer-alist)
+        (let ((other-frame (buffer-local-value 'exwm--frame (cdr pair))))
+          ;; Move X windows to next-frame.
+          (when (eq other-frame frame)
+            (exwm-workspace-move-window next-frame (car pair)))
+          ;; Update the _NET_WM_DESKTOP property of each following X window.
+          (when (memq other-frame following-frames)
+            (exwm-workspace--set-desktop (car pair)))))
+      ;; If the current workspace is deleted, switch to next one.
+      (when (eq frame exwm-workspace--current)
+        (exwm-workspace-switch next-frame))))
   ;; Reparent out the frame.
   (let ((outer-id (frame-parameter frame 'exwm-outer-id)))
     (xcb:+request exwm--connection
@@ -1471,24 +1470,23 @@ the next workspace."
   ;; Update EWMH properties.
   (exwm-workspace--update-ewmh-props)
   ;; Update switch history.
-  (setq exwm-workspace--switch-history-outdated t)
-  (run-hooks 'exwm-workspace-list-change-hook))
+  (unless quit
+    (setq exwm-workspace--switch-history-outdated t)
+    (run-hooks 'exwm-workspace-list-change-hook)))
 
 (defun exwm-workspace--on-delete-frame (frame)
-  "Hook run upon `delete-frame' that tears down FRAME's configuration as a workspace."
+  "Hook run upon `delete-frame' removing FRAME as a workspace."
   (cond
    ((not (exwm-workspace--workspace-p frame))
     (exwm--log "Frame `%s' is not a workspace" frame))
    (t
-    (when (= 1 (exwm-workspace--count))
-      ;; The user managed to delete the last workspace, so create a new one.
-      (exwm--log "Last workspace deleted; create a new one")
-      ;; TODO: this makes sense in the hook.  But we need a function that takes
-      ;; care of converting a workspace into a regular unmanaged frame.
-      (let ((exwm-workspace--create-silently t))
-        (make-frame)))
-    (exwm-workspace--remove-frame-as-workspace frame)
-    (remhash frame exwm-workspace--client-p-hash-table))))
+    (exwm-workspace--remove-frame-as-workspace frame))))
+
+(defun exwm-workspace--fullscreen-workspace (frame)
+  "Make workspace FRAME fullscreen.
+Called from a timer."
+  (when (frame-live-p frame)
+    (set-frame-parameter frame 'fullscreen 'fullboth)))
 
 (defun exwm-workspace--on-after-make-frame (frame)
   "Hook run upon `make-frame' that configures FRAME as a workspace."
@@ -1497,6 +1495,11 @@ the next workspace."
     (exwm--log "Frame `%s' is already a workspace" frame))
    ((not (display-graphic-p frame))
     (exwm--log "Frame `%s' is not graphical" frame))
+   ((not (eq (frame-terminal) exwm--terminal))
+    (exwm--log "Frame `%s' is on a different terminal (%S instead of %S)"
+               frame
+               (frame-terminal frame)
+               exwm--terminal))
    ((not (string-equal
           (replace-regexp-in-string "\\.0$" ""
                                     (slot-value exwm--connection 'display))
@@ -1557,13 +1560,13 @@ applied to all subsequently created X frames."
   (interactive "e"))
 
 (defun exwm-workspace--init-minibuffer-frame ()
+  "Initialize minibuffer-only frame."
   (exwm--log)
   ;; Initialize workspaces without minibuffers.
   (setq exwm-workspace--minibuffer
         (make-frame '((window-system . x) (minibuffer . only)
                       (left . 10000) (right . 10000)
-                      (width . 1) (height . 1)
-                      (client . nil))))
+                      (width . 1) (height . 1))))
   ;; This is the only usable minibuffer frame.
   (setq default-minibuffer-frame exwm-workspace--minibuffer)
   (exwm-workspace--modify-all-x-frames-parameters
@@ -1628,11 +1631,14 @@ applied to all subsequently created X frames."
               :test #'equal))
 
 (defun exwm-workspace--exit-minibuffer-frame ()
+  "Cleanup minibuffer-only frame."
   (exwm--log)
   ;; Only on minibuffer-frame.
   (remove-hook 'minibuffer-setup-hook #'exwm-workspace--on-minibuffer-setup)
   (remove-hook 'minibuffer-exit-hook #'exwm-workspace--on-minibuffer-exit)
   (remove-hook 'echo-area-clear-hook #'exwm-workspace--on-echo-area-clear)
+  (when exwm-workspace--display-echo-area-timer
+    (cancel-timer exwm-workspace--display-echo-area-timer))
   (when exwm-workspace--timer
     (cancel-timer exwm-workspace--timer)
     (setq exwm-workspace--timer nil))
@@ -1640,15 +1646,18 @@ applied to all subsequently created X frames."
         (cl-delete '(exwm-workspace--display-buffer) display-buffer-alist
                    :test #'equal))
   (setq default-minibuffer-frame nil)
-  (let ((id (frame-parameter exwm-workspace--minibuffer 'exwm-outer-id)))
-    (when (and exwm-workspace--minibuffer id)
-      (xcb:+request exwm--connection
-          (make-instance 'xcb:ReparentWindow
-                         :window id
-                         :parent exwm--root
-                         :x 0
-                         :y 0)))
-    (setq exwm-workspace--minibuffer nil)))
+  (when (frame-live-p exwm-workspace--minibuffer) ; might be already dead
+    (let ((id (frame-parameter exwm-workspace--minibuffer 'exwm-outer-id)))
+      (when (and exwm-workspace--minibuffer id
+                 ;; Invoked from `exwm-manage--exit' upon disconnection.
+                 (slot-value exwm--connection 'connected))
+        (xcb:+request exwm--connection
+            (make-instance 'xcb:ReparentWindow
+                           :window id
+                           :parent exwm--root
+                           :x 0
+                           :y 0)))
+      (setq exwm-workspace--minibuffer nil))))
 
 (defun exwm-workspace--init ()
   "Initialize workspace module."
@@ -1666,33 +1675,22 @@ applied to all subsequently created X frames."
           (dolist (i initial-workspaces)
             (unless (frame-parameter i 'window-id)
               (setq initial-workspaces (delq i initial-workspaces))))
-          (setq exwm-workspace--client
-                (frame-parameter (car initial-workspaces) 'client))
           (let ((f (car initial-workspaces)))
             ;; Remove the possible internal border.
-            (set-frame-parameter f 'internal-border-width 0)
-            ;; Prevent user from deleting the first frame by accident.
-            (set-frame-parameter f 'client nil)))
+            (set-frame-parameter f 'internal-border-width 0)))
       (exwm-workspace--init-minibuffer-frame)
       ;; Remove/hide existing frames.
       (dolist (f initial-workspaces)
-        (if (frame-parameter f 'client)
-            (progn
-              (unless exwm-workspace--client
-                (setq exwm-workspace--client (frame-parameter f 'client)))
-              (make-frame-invisible f))
-          (when (eq 'x (framep f))   ;do not delete the initial frame.
-            (delete-frame f))))
+        (when (eq 'x (framep f))        ;do not delete the initial frame.
+          (delete-frame f)))
       ;; Recreate one frame with the external minibuffer set.
-      (setq initial-workspaces (list (make-frame '((window-system . x)
-                                                   (client . nil))))))
+      (setq initial-workspaces (list (make-frame '((window-system . x))))))
     ;; Prevent `other-buffer' from selecting already displayed EXWM buffers.
     (modify-all-frames-parameters
      '((buffer-predicate . exwm-layout--other-buffer-predicate)))
     ;; Create remaining workspaces.
     (dotimes (_ (- exwm-workspace-number (length initial-workspaces)))
-      (nconc initial-workspaces (list (make-frame '((window-system . x)
-                                                    (client . nil))))))
+      (nconc initial-workspaces (list (make-frame '((window-system . x))))))
     ;; Configure workspaces
     (let ((exwm-workspace--create-silently t))
       (dolist (i initial-workspaces)
@@ -1737,36 +1735,26 @@ applied to all subsequently created X frames."
                  #'exwm-workspace--on-echo-area-clear))
   ;; Hide & reparent out all frames (save-set can't be used here since
   ;; X windows will be re-mapped).
+  (when (slot-value exwm--connection 'connected)
+    (dolist (i exwm-workspace--list)
+      (when (frame-live-p i)                    ; might be already dead
+        (exwm-workspace--remove-frame-as-workspace i 'quit)
+        (modify-frame-parameters i '((exwm-selected-window . nil)
+                                     (exwm-urgency . nil)
+                                     (exwm-outer-id . nil)
+                                     (exwm-id . nil)
+                                     (exwm-container . nil)
+                                     ;; (internal-border-width . nil) ; integerp
+                                     (fullscreen . nil)
+                                     (buffer-predicate . nil))))))
+  ;; Don't let dead frames linger.
   (setq exwm-workspace--current nil)
-  (dolist (i exwm-workspace--list)
-    (exwm-workspace--remove-frame-as-workspace i)
-    (modify-frame-parameters i '((exwm-selected-window . nil)
-                                 (exwm-urgency . nil)
-                                 (exwm-outer-id . nil)
-                                 (exwm-id . nil)
-                                 (exwm-container . nil)
-                                 ;; (internal-border-width . nil) ; integerp
-                                 ;; (client . nil)
-                                 (fullscreen . nil)
-                                 (buffer-predicate . nil))))
-  ;; Restore the 'client' frame parameter (before `exwm-exit').
-  (when exwm-workspace--client
-    (dolist (f exwm-workspace--list)
-      (set-frame-parameter f 'client exwm-workspace--client))
-    (when (exwm-workspace--minibuffer-own-frame-p)
-      (set-frame-parameter exwm-workspace--minibuffer 'client
-                           exwm-workspace--client))
-    (setq exwm-workspace--client nil)))
+  (setq exwm-workspace-current-index 0)
+  (setq exwm-workspace--list nil))
 
 (defun exwm-workspace--post-init ()
   "The second stage in the initialization of the workspace module."
   (exwm--log)
-  (when exwm-workspace--client
-    ;; Reset the 'fullscreen' frame parameter to make emacsclinet frames
-    ;; fullscreen (even without the RandR module enabled).
-    (dolist (i exwm-workspace--list)
-      (set-frame-parameter i 'fullscreen nil)
-      (set-frame-parameter i 'fullscreen 'fullboth)))
   ;; Wait until all workspace frames are resized.
   (with-timeout (1)
     (while (< exwm-workspace--fullscreen-frame-count (exwm-workspace--count))
diff --git a/third_party/exwm/exwm-xim.el b/third_party/exwm/exwm-xim.el
index 9589648d22..1f0c9c460b 100644
--- a/third_party/exwm/exwm-xim.el
+++ b/third_party/exwm/exwm-xim.el
@@ -1,6 +1,6 @@
 ;;; exwm-xim.el --- XIM Module for EXWM  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2019-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2019-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
 
@@ -68,7 +68,7 @@
 
 ;;; Code:
 
-(eval-when-compile (require 'cl-lib))
+(require 'cl-lib)
 
 (require 'xcb-keysyms)
 (require 'xcb-xim)
@@ -167,6 +167,7 @@ C,no"
 
 (defun exwm-xim--on-SelectionRequest (data _synthetic)
   "Handle SelectionRequest events on IMS window.
+DATA contains unmarshalled SelectionRequest event data.
 
 Such events would be received when clients query for LOCALES or TRANSPORT."
   (exwm--log)
@@ -754,10 +755,12 @@ Such event would be received when the client window is destroyed."
   ;; Close IMS communication connections.
   (mapc (lambda (i)
           (when (vectorp i)
-            (xcb:disconnect (elt i 0))))
+            (when (slot-value (elt i 0) 'connected)
+              (xcb:disconnect (elt i 0)))))
         exwm-xim--server-client-plist)
   ;; Close the IMS connection.
-  (unless exwm-xim--conn
+  (unless (and exwm-xim--conn
+               (slot-value exwm-xim--conn 'connected))
     (cl-return-from exwm-xim--exit))
   ;; Remove exwm-xim from XIM_SERVERS.
   (let ((reply (xcb:+request-unchecked+reply exwm-xim--conn
diff --git a/third_party/exwm/exwm-xsettings.el b/third_party/exwm/exwm-xsettings.el
new file mode 100644
index 0000000000..99d6b9c4ac
--- /dev/null
+++ b/third_party/exwm/exwm-xsettings.el
@@ -0,0 +1,336 @@
+;;; exwm-xsettings.el --- XSETTINGS Module for EXWM -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022-2024 Free Software Foundation, Inc.
+
+;; Author: Steven Allen <steven@stebalien.com>
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; Implements the XSETTINGS protocol, allowing Emacs to manage the system theme,
+;; fonts, icons, etc.
+;;
+;; This package can be configured as follows:
+;;
+;;   (require 'exwm-xsettings)
+;;   (setq exwm-xsettings-theme '("Adwaita" . "Adwaita-dark") ;; light/dark
+;;         exwm-xsettings `(("Xft/HintStyle" . "hintslight")
+;;                          ("Xft/RGBA" . "rgb")
+;;                          ("Xft/lcdfilter" . "lcddefault")
+;;                          ("Xft/Antialias" . 1)
+;;                          ;; DPI is in 1024ths of an inch, so this is a DPI of
+;;                          ;; 144, equivalent to ;; a scaling factor of 1.5
+;;                          ;; (144 = 1.5 * 96).
+;;                          ("Xft/DPI" . ,(* 144 1024))
+;;                          ("Xft/Hinting" . 1)))
+;;   (exwm-xsettings-enable)
+;;
+;; To modify these settings at runtime, customize them with
+;; `custom-set-variables' or `setopt' (Emacs 29+).  E.g., the following will
+;; immediately change the icon theme to "Papirus" at runtime, even in running
+;; applications:
+;;
+;;   (setopt exwm-xsettings-icon-theme "Papirus")
+
+;;; Code:
+
+(require 'xcb-ewmh)
+(require 'xcb-xsettings)
+(require 'exwm-core)
+
+(defvar exwm-xsettings--connection nil)
+(defvar exwm-xsettings--XSETTINGS_SETTINGS-atom nil)
+(defvar exwm-xsettings--XSETTINGS_S0-atom nil)
+(defvar exwm-xsettings--selection-owner-window nil)
+(defvar exwm-xsettings--serial 0)
+
+(defun exwm-xsettings--rgba-match (_widget value)
+  "Return t if VALUE is a valid RGBA color."
+  (and (numberp value) (<= 0 value 1)))
+
+(defun exwm-xsettings--custom-set (symbol value)
+  "Setter used by `exwm-xsettings' customization options.
+
+SYMBOL is the setting being updated and VALUE is the new value."
+  (set-default-toplevel-value symbol value)
+  (exwm-xsettings--update-settings))
+
+(defgroup exwm-xsettings nil
+  "XSETTINGS."
+  :group 'exwm)
+
+(defcustom exwm-xsettings nil
+  "Alist of custom XSETTINGS.
+These settings take precedence over `exwm-xsettings-theme' and
+`exwm-xsettings-icon-theme'."
+  :type '(alist :key-type (string :tag "Name")
+                :value-type (choice :tag "Value"
+                              (string :tag "String")
+                              (integer :tag "Integer")
+                              (list :tag "Color"
+                                (number :tag "Red"
+                                        :type-error
+                                        "This field should contain a number between 0 and 1."
+                                       :match exwm-xsettings--rgba-match)
+                                (number :tag "Green"
+                                        :type-error
+                                        "This field should contain a number between 0 and 1."
+                                       :match exwm-xsettings--rgba-match)
+                                (number :tag "Blue"
+                                        :type-error
+                                        "This field should contain a number between 0 and 1."
+                                       :match exwm-xsettings--rgba-match)
+                                (number :tag "Alpha"
+                                        :type-error
+                                        "This field should contain a number between 0 and 1."
+                                       :match exwm-xsettings--rgba-match
+                                       :value 1.0))))
+  :initialize #'custom-initialize-default
+  :set #'exwm-xsettings--custom-set)
+
+(defcustom exwm-xsettings-theme nil
+  "The system-wide theme."
+  :type '(choice (string :tag "Theme")
+                 (cons (string :tag "Light Theme")
+                       (string :tag "Dark Theme")))
+  :initialize #'custom-initialize-default
+  :set #'exwm-xsettings--custom-set)
+
+(defcustom exwm-xsettings-icon-theme nil
+  "The system-wide icon theme."
+  :type '(choice (string :tag "Icon Theme")
+                 (cons (string :tag "Light Icon Theme")
+                       (string :tag "Dark Icon Theme")))
+  :initialize #'custom-initialize-default
+  :set #'exwm-xsettings--custom-set)
+
+(defalias 'exwm-xsettings--color-dark-p
+  (if (eval-when-compile (< emacs-major-version 29))
+      ;; Borrowed from Emacs 29.
+      (lambda (rgb)
+        "Whether RGB is more readable against white than black."
+        (unless (<= 0 (apply #'min rgb) (apply #'max rgb) 1)
+          (error "RGB components %S not in [0,1]" rgb))
+        (let* ((r (expt (nth 0 rgb) 2.2))
+               (g (expt (nth 1 rgb) 2.2))
+               (b (expt (nth 2 rgb) 2.2))
+               (y (+ (* r 0.2126) (* g 0.7152) (* b 0.0722))))
+          (< y 0.325)))
+    'color-dark-p))
+
+(defun exwm-xsettings--pick-theme (theme)
+  "Pick a light or dark theme from the given THEME.
+If THEME is a string, it's returned directly.
+If THEME is a cons of (LIGHT . DARK), the appropriate theme is picked based on
+the default face's background color."
+  (pcase theme
+    ((cl-type string) theme)
+    (`(,(cl-type string) . ,(cl-type string))
+     (if (exwm-xsettings--color-dark-p (color-name-to-rgb (face-background 'default)))
+         (cdr theme) (car theme)))
+    (_ (error "Expected theme to be a string or a pair of strings"))))
+
+(defun exwm-xsettings--get-settings ()
+  "Get the current settings.
+Combines `exwm-xsettings', `exwm-xsettings-theme' (if set), and
+`exwm-xsettings-icon-theme' (if set)."
+  (cl-remove-duplicates
+   (append
+    exwm-xsettings
+    (when exwm-xsettings-theme
+      (list (cons "Net/ThemeName" (exwm-xsettings--pick-theme exwm-xsettings-theme))))
+    (when exwm-xsettings-icon-theme
+      (list (cons "Net/IconThemeName" (exwm-xsettings--pick-theme exwm-xsettings-icon-theme)))))
+   :key 'car
+   :test 'string=))
+
+(defun exwm-xsettings--make-settings (settings serial)
+  "Construct a new settings object.
+SETTINGS is an alist of key/value pairs.
+SERIAL is a sequence number."
+  (make-instance 'xcb:xsettings:-Settings
+                 :byte-order (if xcb:lsb 0 1)
+                 :serial serial
+                 :settings-len (length settings)
+                 :settings
+                 (mapcar
+                  (lambda (prop)
+                    (let* ((name (car prop))
+                           (value (cdr prop))
+                           (common (list :name name
+                                         :name-len (length name)
+                                         :last-change-serial serial)))
+                      (pcase value
+                        ((cl-type string)
+                         (apply #'make-instance 'xcb:xsettings:-SETTING_STRING
+                                :value-len (length value)
+                                :value value
+                                common))
+                        ((cl-type integer)
+                         (apply #'make-instance 'xcb:xsettings:-SETTING_INTEGER
+                                :value value common))
+                        ((and (cl-type list) (app length (or 3 4)))
+                         ;; Convert from RGB(A) to 16bit integers.
+                         (setq value (mapcar (lambda (x) (round (* x #xffff))) value))
+                         (apply #'make-instance 'xcb:xsettings:-SETTING_COLOR
+                                :red (pop value)
+                                :green (pop value)
+                                :blue (pop value)
+                                :alpha (or (pop value) #xffff)))
+                        (_ (error "Setting value must be a string, integer, or length 3-4 list")))))
+                  settings)))
+
+(defun exwm-xsettings--update-settings ()
+  "Update the xsettings."
+  (when exwm-xsettings--connection
+    (setq exwm-xsettings--serial (1+ exwm-xsettings--serial))
+    (let* ((settings (exwm-xsettings--get-settings))
+           (bytes (xcb:marshal (exwm-xsettings--make-settings settings exwm-xsettings--serial))))
+      (xcb:+request exwm-xsettings--connection
+          (make-instance 'xcb:ChangeProperty
+                         :mode xcb:PropMode:Replace
+                         :window exwm-xsettings--selection-owner-window
+                         :property exwm-xsettings--XSETTINGS_SETTINGS-atom
+                         :type exwm-xsettings--XSETTINGS_SETTINGS-atom
+                         :format 8
+                         :data-len (length bytes)
+                         :data bytes)))
+    (xcb:flush exwm-xsettings--connection)))
+
+(defun exwm-xsettings--on-theme-change (&rest _)
+  "Called when the Emacs theme is changed."
+  ;; We only bother updating the xsettings if changing the theme could effect
+  ;; the settings.
+  (when (or (consp exwm-xsettings-theme) (consp exwm-xsettings-icon-theme))
+    (exwm-xsettings--update-settings)))
+
+(defun exwm-xsettings--on-SelectionClear (_data _synthetic)
+  "Called when another xsettings daemon takes over."
+  (exwm--log "XSETTINGS manager has been replaced.")
+  (exwm-xsettings--exit))
+
+(cl-defun exwm-xsettings--init ()
+  "Initialize the XSETTINGS module."
+  (exwm--log)
+
+  (cl-assert (not exwm-xsettings--connection))
+
+  ;; Connect
+  (setq exwm-xsettings--connection (xcb:connect))
+  (set-process-query-on-exit-flag (slot-value exwm-xsettings--connection
+                                              'process)
+                                  nil)
+
+  ;; Intern the atoms.
+  (setq exwm-xsettings--XSETTINGS_SETTINGS-atom
+        (exwm--intern-atom "_XSETTINGS_SETTINGS" exwm-xsettings--connection)
+
+        exwm-xsettings--XSETTINGS_S0-atom
+        (exwm--intern-atom "_XSETTINGS_S0" exwm-xsettings--connection))
+
+  ;; Detect running XSETTINGS managers.
+  (with-slots (owner)
+      (xcb:+request-unchecked+reply exwm-xsettings--connection
+          (make-instance 'xcb:GetSelectionOwner
+                         :selection exwm-xsettings--XSETTINGS_S0-atom))
+    (when (/= owner xcb:Window:None)
+      (xcb:disconnect exwm-xsettings--connection)
+      (setq exwm-xsettings--connection nil)
+      (warn "[EXWM] Other XSETTINGS manager detected")
+      (cl-return-from exwm-xsettings--init)))
+
+  (let ((id(xcb:generate-id exwm-xsettings--connection)))
+    (setq exwm-xsettings--selection-owner-window id)
+
+    ;; Create a settings window.
+    (xcb:+request exwm-xsettings--connection
+        (make-instance 'xcb:CreateWindow
+                       :wid id
+                       :parent exwm--root
+                       :class xcb:WindowClass:InputOnly
+                       :x 0
+                       :y 0
+                       :width 1
+                       :height 1
+                       :border-width 0
+                       :depth 0
+                       :visual 0
+                       :value-mask xcb:CW:OverrideRedirect
+                       :override-redirect 1))
+
+    ;; Set _NET_WM_NAME.
+    (xcb:+request exwm-xsettings--connection
+        (make-instance 'xcb:ewmh:set-_NET_WM_NAME
+                       :window id
+                       :data "EXWM: exwm-xsettings--selection-owner-window"))
+
+    ;; Apply the XSETTINGS properties.
+    (exwm-xsettings--update-settings)
+
+    ;; Take ownership and notify.
+    (xcb:+request exwm-xsettings--connection
+        (make-instance 'xcb:SetSelectionOwner
+                       :owner id
+                       :selection exwm-xsettings--XSETTINGS_S0-atom
+                       :time xcb:Time:CurrentTime))
+    (xcb:+request exwm-xsettings--connection
+        (make-instance 'xcb:SendEvent
+                       :propagate 0
+                       :destination exwm--root
+                       :event-mask xcb:EventMask:StructureNotify
+                       :event (xcb:marshal
+                               (make-instance 'xcb:xsettings:-ClientMessage
+                                              :window exwm--root
+                                              :time xcb:Time:CurrentTime
+                                              :selection exwm-xsettings--XSETTINGS_S0-atom
+                                              :owner id)
+                               exwm-xsettings--connection)))
+
+    ;; Detect loss of XSETTINGS ownership.
+    (xcb:+event exwm-xsettings--connection 'xcb:SelectionClear
+                #'exwm-xsettings--on-SelectionClear)
+
+    (xcb:flush exwm-xsettings--connection))
+
+  ;; Update the xsettings if/when the theme changes.
+  (add-hook 'enable-theme-functions #'exwm-xsettings--on-theme-change)
+  (add-hook 'disable-theme-functions #'exwm-xsettings--on-theme-change))
+
+(defun exwm-xsettings--exit ()
+  "Exit the XSETTINGS module."
+  (exwm--log)
+
+  (when exwm-xsettings--connection
+    (remove-hook 'enable-theme-functions #'exwm-xsettings--on-theme-change)
+    (remove-hook 'disable-theme-functions #'exwm-xsettings--on-theme-change)
+
+    (xcb:disconnect exwm-xsettings--connection)
+
+    (setq exwm-xsettings--connection nil
+          exwm-xsettings--XSETTINGS_SETTINGS-atom nil
+          exwm-xsettings--XSETTINGS_S0-atom nil
+          exwm-xsettings--selection-owner-window nil)))
+
+(defun exwm-xsettings-enable ()
+  "Enable xsettings support for EXWM."
+  (exwm--log)
+  (add-hook 'exwm-init-hook #'exwm-xsettings--init)
+  (add-hook 'exwm-exit-hook #'exwm-xsettings--exit))
+
+(provide 'exwm-xsettings)
+
+;;; exwm-xsettings.el ends here
diff --git a/third_party/exwm/exwm.el b/third_party/exwm/exwm.el
index b025f6b49a..c4900eab48 100644
--- a/third_party/exwm/exwm.el
+++ b/third_party/exwm/exwm.el
@@ -1,13 +1,13 @@
 ;;; exwm.el --- Emacs X Window Manager  -*- lexical-binding: t -*-
 
-;; Copyright (C) 2015-2021 Free Software Foundation, Inc.
+;; Copyright (C) 2015-2024 Free Software Foundation, Inc.
 
 ;; Author: Chris Feng <chris.w.feng@gmail.com>
-;; Maintainer: Adriรกn Medraรฑo Calvo <adrian@medranocalvo.com>
-;; Version: 0.26
-;; Package-Requires: ((xelb "0.18"))
+;; Maintainer: Adriรกn Medraรฑo Calvo <adrian@medranocalvo.com>, Steven Allen <steven@stebalien.com>, Daniel Mendler <mail@daniel-mendler.de>
+;; Version: 0.28
+;; Package-Requires: ((emacs "27.1") (xelb "0.18"))
 ;; Keywords: unix
-;; URL: https://github.com/ch11ng/exwm
+;; URL: https://github.com/emacs-exwm/exwm
 
 ;; This file is part of GNU Emacs.
 
@@ -29,14 +29,18 @@
 ;; Overview
 ;; --------
 ;; EXWM (Emacs X Window Manager) is a full-featured tiling X window manager
-;; for Emacs built on top of [XELB](https://github.com/ch11ng/xelb).
+;; for Emacs built on top of [XELB](https://github.com/emacs-exwm/xelb).
 ;; It features:
 ;; + Fully keyboard-driven operations
 ;; + Hybrid layout modes (tiling & stacking)
 ;; + Dynamic workspace support
 ;; + ICCCM/EWMH compliance
-;; + (Optional) RandR (multi-monitor) support
-;; + (Optional) Built-in system tray
+;; Optional features:
+;; + RandR (multi-monitor) support
+;; + System tray
+;; + Input method
+;; + Background setting support
+;; + XSETTINGS server
 
 ;; Installation & configuration
 ;; ----------------------------
@@ -54,7 +58,7 @@
 ;;    xinit -- vt01
 ;;
 ;; You should additionally hide the menu-bar, tool-bar, etc to increase the
-;; usable space.  Please check the wiki (https://github.com/ch11ng/exwm/wiki)
+;; usable space.  Please check the wiki (https://github.com/emacs-exwm/exwm/wiki)
 ;; for more detailed instructions on installation, configuration, usage, etc.
 
 ;; References:
@@ -72,10 +76,11 @@
 (require 'exwm-manage)
 (require 'exwm-input)
 
+(declare-function x-get-atom-name "C source code" (VALUE &optional FRAME))
+
 (defgroup exwm nil
   "Emacs X Window Manager."
   :tag "EXWM"
-  :version "25.3"
   :group 'applications
   :prefix "exwm-")
 
@@ -95,7 +100,10 @@
   "Normal hook run when window title is updated."
   :type 'hook)
 
-(defcustom exwm-blocking-subrs '(x-file-dialog x-popup-dialog x-select-font)
+(defcustom exwm-blocking-subrs
+  ;; `x-file-dialog' and `x-select-font' are missing on some Emacs builds, for
+  ;; example on the X11 Lucid build.
+  '(x-file-dialog x-popup-dialog x-select-font message-box message-or-box)
   "Subrs (primitives) that would normally block EXWM."
   :type '(repeat function))
 
@@ -108,6 +116,10 @@
 (defconst exwm--server-name "server-exwm"
   "Name of the subordinate Emacs server.")
 
+(defvar exwm--server-timeout 1
+  "Number of seconds to wait for the subordinate Emacs server to exit.
+After this time, the server will be killed.")
+
 (defvar exwm--server-process nil "Process of the subordinate Emacs server.")
 
 (defun exwm-reset ()
@@ -127,7 +139,7 @@
   "Restart EXWM."
   (interactive)
   (exwm--log)
-  (when (exwm--confirm-kill-emacs "[EXWM] Restart? " 'no-check)
+  (when (exwm--confirm-kill-emacs "Restart?" 'no-check)
     (let* ((attr (process-attributes (emacs-pid)))
            (args (cdr (assq 'args attr)))
            (ppid (cdr (assq 'ppid attr)))
@@ -153,7 +165,8 @@
         (kill-emacs))))))
 
 (defun exwm--update-desktop (xwin)
-  "Update _NET_WM_DESKTOP."
+  "Update _NET_WM_DESKTOP.
+Argument XWIN contains the X window of the `exwm-mode' buffer."
   (exwm--log "#x%x" xwin)
   (with-current-buffer (exwm--id->buffer xwin)
     (let ((reply (xcb:+request-unchecked+reply exwm--connection
@@ -163,7 +176,7 @@
       (when reply
         (setq desktop (slot-value reply 'value))
         (cond
-         ((eq desktop 4294967295.)
+         ((and desktop (= desktop 4294967295.))
           (unless (or (not exwm--floating-frame)
                       (eq exwm--frame exwm-workspace--current)
                       (and exwm--desktop
@@ -180,7 +193,11 @@
           (exwm-workspace--set-desktop xwin)))))))
 
 (defun exwm--update-window-type (id &optional force)
-  "Update _NET_WM_WINDOW_TYPE."
+  "Update `exwm-window-type' from _NET_WM_WINDOW_TYPE.
+Argument ID contains the X window of the `exwm-mode' buffer.
+
+When FORCE is nil the update only takes place if
+`exwm-window-type' is unset."
   (exwm--log "#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (and exwm-window-type (not force))
@@ -191,7 +208,11 @@
           (setq exwm-window-type (append (slot-value reply 'value) nil)))))))
 
 (defun exwm--update-class (id &optional force)
-  "Update WM_CLASS."
+  "Update `exwm-instance-name' and `exwm-class' from WM_CLASS.
+Argument ID contains the X window of the `exwm-mode' buffer.
+
+When FORCE is nil the update only takes place if any of
+`exwm-instance-name' or `exwm-class' is unset."
   (exwm--log "#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (and exwm-instance-name exwm-class-name (not force))
@@ -204,7 +225,11 @@
             (run-hooks 'exwm-update-class-hook)))))))
 
 (defun exwm--update-utf8-title (id &optional force)
-  "Update _NET_WM_NAME."
+  "Update `exwm-title' from _NET_WM_NAME.
+Argument ID contains the X window of the `exwm-mode' buffer.
+
+When FORCE is nil the update only takes place if `exwm-title' is
+unset."
   (exwm--log "#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (when (or force (not exwm-title))
@@ -217,7 +242,11 @@
             (run-hooks 'exwm-update-title-hook)))))))
 
 (defun exwm--update-ctext-title (id &optional force)
-  "Update WM_NAME."
+  "Update `exwm-title' from WM_NAME.
+Argument ID contains the X window of the `exwm-mode' buffer.
+
+When FORCE is nil the update only takes place if `exwm-title' is
+unset."
   (exwm--log "#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (or exwm--title-is-utf8
@@ -230,13 +259,18 @@
             (run-hooks 'exwm-update-title-hook)))))))
 
 (defun exwm--update-title (id)
-  "Update _NET_WM_NAME or WM_NAME."
+  "Update _NET_WM_NAME or WM_NAME.
+Argument ID contains the X window of the `exwm-mode' buffer."
   (exwm--log "#x%x" id)
   (exwm--update-utf8-title id)
   (exwm--update-ctext-title id))
 
 (defun exwm--update-transient-for (id &optional force)
-  "Update WM_TRANSIENT_FOR."
+  "Update `exwm-transient-for' from WM_TRANSIENT_FOR.
+Argument ID contains the X window of the `exwm-mode' buffer.
+
+When FORCE is nil the update only takes place if `exwm-title' is
+unset."
   (exwm--log "#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (and exwm-transient-for (not force))
@@ -247,7 +281,15 @@
           (setq exwm-transient-for (slot-value reply 'value)))))))
 
 (defun exwm--update-normal-hints (id &optional force)
-  "Update WM_NORMAL_HINTS."
+  "Update normal hints from WM_NORMAL_HINTS.
+Argument ID contains the X window of the `exwm-mode' buffer.
+
+When FORCE is nil the update only takes place all of
+`exwm--normal-hints-x exwm--normal-hints-y',
+`exwm--normal-hints-width exwm--normal-hints-height',
+`exwm--normal-hints-min-width exwm--normal-hints-min-height' and
+`exwm--normal-hints-max-width exwm--normal-hints-max-height' are
+unset."
   (exwm--log "#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (and (not force)
@@ -295,7 +337,11 @@
                           exwm--normal-hints-max-height)))))))))
 
 (defun exwm--update-hints (id &optional force)
-  "Update WM_HINTS."
+  "Update hints from WM_HINTS.
+Argument ID contains the X window of the `exwm-mode' buffer.
+
+When FORCE is nil the update only takes place if both of
+`exwm--hints-input' and `exwm--hints-urgency' are unset."
   (exwm--log "#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (and (not force) exwm--hints-input exwm--hints-urgency)
@@ -317,7 +363,11 @@
               (setq exwm-workspace--switch-history-outdated t))))))))
 
 (defun exwm--update-protocols (id &optional force)
-  "Update WM_PROTOCOLS."
+  "Update `exwm--protocols' from WM_PROTOCOLS.
+Argument ID contains the X window of the `exwm-mode' buffer.
+
+When FORCE is nil the update only takes place if `exwm--protocols'
+is unset."
   (exwm--log "#x%x" id)
   (with-current-buffer (exwm--id->buffer id)
     (unless (and exwm--protocols (not force))
@@ -328,7 +378,7 @@
           (setq exwm--protocols (append (slot-value reply 'value) nil)))))))
 
 (defun exwm--update-struts-legacy (id)
-  "Update _NET_WM_STRUT."
+  "Update struts of X window ID from _NET_WM_STRUT."
   (exwm--log "#x%x" id)
   (let ((pair (assq id exwm-workspace--id-struts-alist))
         reply struts)
@@ -349,7 +399,7 @@
         (exwm-workspace--set-fullscreen f)))))
 
 (defun exwm--update-struts-partial (id)
-  "Update _NET_WM_STRUT_PARTIAL."
+  "Update struts of X window ID from _NET_WM_STRUT_PARTIAL."
   (exwm--log "#x%x" id)
   (let ((reply (xcb:+request-unchecked+reply exwm--connection
                    (make-instance 'xcb:ewmh:get-_NET_WM_STRUT_PARTIAL
@@ -369,13 +419,14 @@
       (exwm-workspace--set-fullscreen f))))
 
 (defun exwm--update-struts (id)
-  "Update _NET_WM_STRUT_PARTIAL or _NET_WM_STRUT."
+  "Update struts of X window ID from _NET_WM_STRUT_PARTIAL or _NET_WM_STRUT."
   (exwm--log "#x%x" id)
   (exwm--update-struts-partial id)
   (exwm--update-struts-legacy id))
 
 (defun exwm--on-PropertyNotify (data _synthetic)
-  "Handle PropertyNotify event."
+  "Handle PropertyNotify event.
+DATA contains unmarshalled PropertyNotify event data."
   (let ((obj (make-instance 'xcb:PropertyNotify))
         atom id buffer)
     (xcb:unmarshal obj data)
@@ -413,15 +464,16 @@
                           atom)))))))
 
 (defun exwm--on-ClientMessage (raw-data _synthetic)
-  "Handle ClientMessage event."
+  "Handle ClientMessage event.
+RAW-DATA contains unmarshalled ClientMessage event data."
   (let ((obj (make-instance 'xcb:ClientMessage))
         type id data)
     (xcb:unmarshal obj raw-data)
     (setq type (slot-value obj 'type)
           id (slot-value obj 'window)
           data (slot-value (slot-value obj 'data) 'data32))
-    (exwm--log "atom=%s(%s)" (x-get-atom-name type exwm-workspace--current)
-               type)
+    (exwm--log "atom=%s(%s) id=#x%x data=%s" (x-get-atom-name type exwm-workspace--current)
+               type (or id 0) data)
     (cond
      ;; _NET_NUMBER_OF_DESKTOPS.
      ((= type xcb:Atom:_NET_NUMBER_OF_DESKTOPS)
@@ -434,7 +486,6 @@
          ((and (> current requested)
                (> current 1))
           (let ((frame (car (last exwm-workspace--list))))
-            (exwm-workspace--get-remove-frame-next-workspace frame)
             (delete-frame frame))))))
      ;; _NET_CURRENT_DESKTOP.
      ((= type xcb:Atom:_NET_CURRENT_DESKTOP)
@@ -443,7 +494,8 @@
      ((= type xcb:Atom:_NET_ACTIVE_WINDOW)
       (let ((buffer (exwm--id->buffer id))
             iconic window)
-        (when (buffer-live-p buffer)
+        (if (buffer-live-p buffer)
+          ;; Either an `exwm-mode' buffer (an X window) or a floating frame.
           (with-current-buffer buffer
             (when (eq exwm--frame exwm-workspace--current)
               (if exwm--floating-frame
@@ -457,7 +509,11 @@
                 (setq window (get-buffer-window nil t))
                 (when (or iconic
                           (not (eq window (selected-window))))
-                  (select-window window))))))))
+                  (select-window window)))))
+          ;; A workspace.
+          (dolist (f exwm-workspace--list)
+            (when (eq id (frame-parameter f 'exwm-outer-id))
+              (x-focus-frame f t))))))
      ;; _NET_CLOSE_WINDOW.
      ((= type xcb:Atom:_NET_CLOSE_WINDOW)
       (let ((buffer (exwm--id->buffer id)))
@@ -594,7 +650,8 @@
                  (x-get-atom-name type exwm-workspace--current) type)))))
 
 (defun exwm--on-SelectionClear (data _synthetic)
-  "Handle SelectionClear events."
+  "Handle SelectionClear events.
+DATA contains unmarshalled SelectionClear event data."
   (exwm--log)
   (let ((obj (make-instance 'xcb:SelectionClear))
         owner selection)
@@ -605,6 +662,17 @@
                (eq selection xcb:Atom:WM_S0))
       (exwm-exit))))
 
+(defun exwm--on-delete-terminal (terminal)
+  "Handle terminal being deleted without Emacs being killed.
+This function is Hooked to `delete-terminal-functions'.
+
+TERMINAL is the terminal being (or that has been) deleted.
+
+This may happen when invoking `save-buffers-kill-terminal' within an emacsclient
+session."
+  (when (eq terminal exwm--terminal)
+    (exwm-exit)))
+
 (defun exwm--init-icccm-ewmh ()
   "Initialize ICCCM/EWMH support."
   (exwm--log)
@@ -825,7 +893,8 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
 
 ;;;###autoload
 (cl-defun exwm-init (&optional frame)
-  "Initialize EXWM."
+  "Initialize EXWM.
+FRAME, if given, indicates the X display EXWM should manage."
   (interactive)
   (exwm--log "%s" frame)
   (if frame
@@ -841,6 +910,7 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
   (condition-case err
       (progn
         (exwm-enable 'undo)               ;never initialize again
+        (setq exwm--terminal (frame-terminal frame))
         (setq exwm--connection (xcb:connect))
         (set-process-query-on-exit-flag (slot-value exwm--connection 'process)
                                         nil) ;prevent query message on exit
@@ -863,6 +933,10 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
         ;; Disable some features not working well with EXWM
         (setq use-dialog-box nil
               confirm-kill-emacs #'exwm--confirm-kill-emacs)
+        (advice-add 'save-buffers-kill-terminal
+                    :before-while #'exwm--confirm-kill-terminal)
+        ;; Clean up if the terminal is deleted.
+        (add-hook 'delete-terminal-functions 'exwm--on-delete-terminal)
         (exwm--lock)
         (exwm--init-icccm-ewmh)
         (exwm-layout--init)
@@ -891,15 +965,17 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
   (run-hooks 'exwm-exit-hook)
   (setq confirm-kill-emacs nil)
   ;; Exit modules.
-  (exwm-input--exit)
-  (exwm-manage--exit)
-  (exwm-workspace--exit)
-  (exwm-floating--exit)
-  (exwm-layout--exit)
   (when exwm--connection
+    (exwm-input--exit)
+    (exwm-manage--exit)
+    (exwm-workspace--exit)
+    (exwm-floating--exit)
+    (exwm-layout--exit)
     (xcb:flush exwm--connection)
     (xcb:disconnect exwm--connection))
-  (setq exwm--connection nil))
+  (setq exwm--connection nil)
+  (setq exwm--terminal nil)
+  (exwm--log "Exited"))
 
 ;;;###autoload
 (defun exwm-enable (&optional undo)
@@ -933,15 +1009,21 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
 (defun exwm--server-stop ()
   "Stop the subordinate Emacs server."
   (exwm--log)
-  (server-force-delete exwm--server-name)
   (when exwm--server-process
+    (when (process-live-p exwm--server-process)
+      (cl-loop
+       initially (signal-process exwm--server-process 'TERM)
+       while     (process-live-p exwm--server-process)
+       repeat    (* 10 exwm--server-timeout)
+       do        (sit-for 0.1)))
     (delete-process exwm--server-process)
     (setq exwm--server-process nil)))
 
-(defun exwm--server-eval-at (&rest args)
-  "Wrapper of `server-eval-at' used to advice subrs."
+(defun exwm--server-eval-at (function &rest args)
+  "Wrapper of `server-eval-at' used to advice subrs.
+FUNCTION is the function to be evaluated, ARGS are the arguments."
   ;; Start the subordinate Emacs server if it's not alive
-  (exwm--log "%s" args)
+  (exwm--log "%s %s" function args)
   (unless (server-running-p exwm--server-name)
     (when exwm--server-process (delete-process exwm--server-process))
     (setq exwm--server-process
@@ -950,7 +1032,7 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
                          (car command-line-args) ;The executable file
                          "-d" (frame-parameter nil 'display)
                          "-Q"
-                         (concat "--daemon=" exwm--server-name)
+                         (concat "--fg-daemon=" exwm--server-name)
                          "--eval"
                          ;; Create an invisible frame
                          "(make-frame '((window-system . x) (visibility)))"))
@@ -959,8 +1041,8 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
   (server-eval-at
    exwm--server-name
    `(progn (select-frame (car (frame-list)))
-           (let ((result ,(nconc (list (make-symbol (subr-name (car args))))
-                                 (cdr args))))
+           (let ((result ,(nconc (list (make-symbol (subr-name function)))
+                                 args)))
              (pcase (type-of result)
                ;; Return the name of a buffer
                (`buffer (buffer-name result))
@@ -978,8 +1060,20 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
                ;; For other types, return the value as-is.
                (t result))))))
 
+(defun exwm--confirm-kill-terminal (&optional _)
+  "Confirm before killing terminal."
+  ;; This is invoked instead of `save-buffers-kill-emacs' (C-x C-c) on client
+  ;; frames.
+  (if (exwm--terminal-p)
+      (exwm--confirm-kill-emacs "Kill terminal?")
+    t))
+
 (defun exwm--confirm-kill-emacs (prompt &optional force)
-  "Confirm before exiting Emacs."
+  "Confirm before exiting Emacs.
+PROMPT a reason to present to the user.
+If FORCE is nil, ask the user for confirmation.
+If FORCE is the symbol `no-check', ask if there are unsaved buffers.
+If FORCE is any other non-nil value, force killing of Emacs."
   (exwm--log)
   (when (cond
          ((and force (not (eq force 'no-check)))
@@ -996,7 +1090,7 @@ manager.  If t, replace it, if nil, abort and ask the user if `ask'."
             (`break (y-or-n-p prompt))
             (x x)))
          (t
-          (yes-or-no-p (format "[EXWM] %d window(s) will be destroyed.  %s"
+          (yes-or-no-p (format "[EXWM] %d X window(s) will be destroyed.  %s"
                                (length exwm--id-buffer-alist) prompt))))
     ;; Run `kill-emacs-hook' (`server-force-stop' excluded) before Emacs
     ;; frames are unmapped so that errors (if any) can be visible.
diff --git a/third_party/geesefs/default.nix b/third_party/geesefs/default.nix
new file mode 100644
index 0000000000..98448bb737
--- /dev/null
+++ b/third_party/geesefs/default.nix
@@ -0,0 +1,25 @@
+# Finally, a good FUSE FS implementation over S3.
+# https://github.com/yandex-cloud/geesefs
+
+{ pkgs, ... }:
+
+pkgs.buildGoModule rec {
+  pname = "geesefs";
+  version = "0.40.1";
+
+  src = pkgs.fetchFromGitHub {
+    owner = "yandex-cloud";
+    repo = "geesefs";
+    rev = "v${version}";
+    hash = "sha256:0ig8h17z8n5j8qb7k2jyh40vv77zazhnz8bxdam9xihxksj8mizp";
+  };
+
+  subPackages = [ "." ];
+  buildInputs = [ pkgs.fuse ];
+  vendorHash = "sha256:11i7cmnlxi00d0csgpv8drfcw0aqshwc4hfs0jw7zwafdhnlyy0j";
+
+  meta = with pkgs.lib; {
+    license = licenses.asl20;
+    maintainers = [ maintainers.tazjin ];
+  };
+}
diff --git a/third_party/gerrit-queue/.buildkite/build.sh b/third_party/gerrit-queue/.buildkite/build.sh
deleted file mode 100755
index 0a218c817e..0000000000
--- a/third_party/gerrit-queue/.buildkite/build.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-export GOPATH=~/go
-go generate
-CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -v -a -ldflags '-extldflags \"-static\"' -o gerrit-queue
diff --git a/third_party/gerrit-queue/.buildkite/pipeline.yml b/third_party/gerrit-queue/.buildkite/pipeline.yml
deleted file mode 100644
index 0885cb9694..0000000000
--- a/third_party/gerrit-queue/.buildkite/pipeline.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-steps:
-  - command: |
-      . /var/lib/buildkite-agent/.nix-profile/etc/profile.d/nix.sh
-      # produces a ./gerrit-queue
-      nix-shell --run ./.buildkite/build.sh
-
-      mkdir -p out
-      mv ./gerrit-queue out/gerrit-queue-$(git describe --tags)
-
-    label: "Build (linux/amd64)"
-    timeout: 30
-    artifact_paths:
-      - "out/*"
diff --git a/third_party/gerrit-queue/.gitignore b/third_party/gerrit-queue/.gitignore
deleted file mode 100644
index f2ec770e3c..0000000000
--- a/third_party/gerrit-queue/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-/.vscode
-/statik
-/.envrc.private
-/gerrit-queue
diff --git a/third_party/gerrit-queue/LICENSE b/third_party/gerrit-queue/LICENSE
deleted file mode 100644
index 261eeb9e9f..0000000000
--- a/third_party/gerrit-queue/LICENSE
+++ /dev/null
@@ -1,201 +0,0 @@
-                                 Apache License
-                           Version 2.0, January 2004
-                        http://www.apache.org/licenses/
-
-   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
-   1. Definitions.
-
-      "License" shall mean the terms and conditions for use, reproduction,
-      and distribution as defined by Sections 1 through 9 of this document.
-
-      "Licensor" shall mean the copyright owner or entity authorized by
-      the copyright owner that is granting the License.
-
-      "Legal Entity" shall mean the union of the acting entity and all
-      other entities that control, are controlled by, or are under common
-      control with that entity. For the purposes of this definition,
-      "control" means (i) the power, direct or indirect, to cause the
-      direction or management of such entity, whether by contract or
-      otherwise, or (ii) ownership of fifty percent (50%) or more of the
-      outstanding shares, or (iii) beneficial ownership of such entity.
-
-      "You" (or "Your") shall mean an individual or Legal Entity
-      exercising permissions granted by this License.
-
-      "Source" form shall mean the preferred form for making modifications,
-      including but not limited to software source code, documentation
-      source, and configuration files.
-
-      "Object" form shall mean any form resulting from mechanical
-      transformation or translation of a Source form, including but
-      not limited to compiled object code, generated documentation,
-      and conversions to other media types.
-
-      "Work" shall mean the work of authorship, whether in Source or
-      Object form, made available under the License, as indicated by a
-      copyright notice that is included in or attached to the work
-      (an example is provided in the Appendix below).
-
-      "Derivative Works" shall mean any work, whether in Source or Object
-      form, that is based on (or derived from) the Work and for which the
-      editorial revisions, annotations, elaborations, or other modifications
-      represent, as a whole, an original work of authorship. For the purposes
-      of this License, Derivative Works shall not include works that remain
-      separable from, or merely link (or bind by name) to the interfaces of,
-      the Work and Derivative Works thereof.
-
-      "Contribution" shall mean any work of authorship, including
-      the original version of the Work and any modifications or additions
-      to that Work or Derivative Works thereof, that is intentionally
-      submitted to Licensor for inclusion in the Work by the copyright owner
-      or by an individual or Legal Entity authorized to submit on behalf of
-      the copyright owner. For the purposes of this definition, "submitted"
-      means any form of electronic, verbal, or written communication sent
-      to the Licensor or its representatives, including but not limited to
-      communication on electronic mailing lists, source code control systems,
-      and issue tracking systems that are managed by, or on behalf of, the
-      Licensor for the purpose of discussing and improving the Work, but
-      excluding communication that is conspicuously marked or otherwise
-      designated in writing by the copyright owner as "Not a Contribution."
-
-      "Contributor" shall mean Licensor and any individual or Legal Entity
-      on behalf of whom a Contribution has been received by Licensor and
-      subsequently incorporated within the Work.
-
-   2. Grant of Copyright License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      copyright license to reproduce, prepare Derivative Works of,
-      publicly display, publicly perform, sublicense, and distribute the
-      Work and such Derivative Works in Source or Object form.
-
-   3. Grant of Patent License. Subject to the terms and conditions of
-      this License, each Contributor hereby grants to You a perpetual,
-      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
-      (except as stated in this section) patent license to make, have made,
-      use, offer to sell, sell, import, and otherwise transfer the Work,
-      where such license applies only to those patent claims licensable
-      by such Contributor that are necessarily infringed by their
-      Contribution(s) alone or by combination of their Contribution(s)
-      with the Work to which such Contribution(s) was submitted. If You
-      institute patent litigation against any entity (including a
-      cross-claim or counterclaim in a lawsuit) alleging that the Work
-      or a Contribution incorporated within the Work constitutes direct
-      or contributory patent infringement, then any patent licenses
-      granted to You under this License for that Work shall terminate
-      as of the date such litigation is filed.
-
-   4. Redistribution. You may reproduce and distribute copies of the
-      Work or Derivative Works thereof in any medium, with or without
-      modifications, and in Source or Object form, provided that You
-      meet the following conditions:
-
-      (a) You must give any other recipients of the Work or
-          Derivative Works a copy of this License; and
-
-      (b) You must cause any modified files to carry prominent notices
-          stating that You changed the files; and
-
-      (c) You must retain, in the Source form of any Derivative Works
-          that You distribute, all copyright, patent, trademark, and
-          attribution notices from the Source form of the Work,
-          excluding those notices that do not pertain to any part of
-          the Derivative Works; and
-
-      (d) If the Work includes a "NOTICE" text file as part of its
-          distribution, then any Derivative Works that You distribute must
-          include a readable copy of the attribution notices contained
-          within such NOTICE file, excluding those notices that do not
-          pertain to any part of the Derivative Works, in at least one
-          of the following places: within a NOTICE text file distributed
-          as part of the Derivative Works; within the Source form or
-          documentation, if provided along with the Derivative Works; or,
-          within a display generated by the Derivative Works, if and
-          wherever such third-party notices normally appear. The contents
-          of the NOTICE file are for informational purposes only and
-          do not modify the License. You may add Your own attribution
-          notices within Derivative Works that You distribute, alongside
-          or as an addendum to the NOTICE text from the Work, provided
-          that such additional attribution notices cannot be construed
-          as modifying the License.
-
-      You may add Your own copyright statement to Your modifications and
-      may provide additional or different license terms and conditions
-      for use, reproduction, or distribution of Your modifications, or
-      for any such Derivative Works as a whole, provided Your use,
-      reproduction, and distribution of the Work otherwise complies with
-      the conditions stated in this License.
-
-   5. Submission of Contributions. Unless You explicitly state otherwise,
-      any Contribution intentionally submitted for inclusion in the Work
-      by You to the Licensor shall be under the terms and conditions of
-      this License, without any additional terms or conditions.
-      Notwithstanding the above, nothing herein shall supersede or modify
-      the terms of any separate license agreement you may have executed
-      with Licensor regarding such Contributions.
-
-   6. Trademarks. This License does not grant permission to use the trade
-      names, trademarks, service marks, or product names of the Licensor,
-      except as required for reasonable and customary use in describing the
-      origin of the Work and reproducing the content of the NOTICE file.
-
-   7. Disclaimer of Warranty. Unless required by applicable law or
-      agreed to in writing, Licensor provides the Work (and each
-      Contributor provides its Contributions) on an "AS IS" BASIS,
-      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
-      implied, including, without limitation, any warranties or conditions
-      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
-      PARTICULAR PURPOSE. You are solely responsible for determining the
-      appropriateness of using or redistributing the Work and assume any
-      risks associated with Your exercise of permissions under this License.
-
-   8. Limitation of Liability. In no event and under no legal theory,
-      whether in tort (including negligence), contract, or otherwise,
-      unless required by applicable law (such as deliberate and grossly
-      negligent acts) or agreed to in writing, shall any Contributor be
-      liable to You for damages, including any direct, indirect, special,
-      incidental, or consequential damages of any character arising as a
-      result of this License or out of the use or inability to use the
-      Work (including but not limited to damages for loss of goodwill,
-      work stoppage, computer failure or malfunction, or any and all
-      other commercial damages or losses), even if such Contributor
-      has been advised of the possibility of such damages.
-
-   9. Accepting Warranty or Additional Liability. While redistributing
-      the Work or Derivative Works thereof, You may choose to offer,
-      and charge a fee for, acceptance of support, warranty, indemnity,
-      or other liability obligations and/or rights consistent with this
-      License. However, in accepting such obligations, You may act only
-      on Your own behalf and on Your sole responsibility, not on behalf
-      of any other Contributor, and only if You agree to indemnify,
-      defend, and hold each Contributor harmless for any liability
-      incurred by, or claims asserted against, such Contributor by reason
-      of your accepting any such warranty or additional liability.
-
-   END OF TERMS AND CONDITIONS
-
-   APPENDIX: How to apply the Apache License to your work.
-
-      To apply the Apache License to your work, attach the following
-      boilerplate notice, with the fields enclosed by brackets "[]"
-      replaced with your own identifying information. (Don't include
-      the brackets!)  The text should be enclosed in the appropriate
-      comment syntax for the file format. We also recommend that a
-      file or class name and description of purpose be included on the
-      same "printed page" as the copyright notice for easier
-      identification within third-party archives.
-
-   Copyright [yyyy] [name of copyright owner]
-
-   Licensed under the Apache License, Version 2.0 (the "License");
-   you may not use this file except in compliance with the License.
-   You may obtain a copy of the License at
-
-       http://www.apache.org/licenses/LICENSE-2.0
-
-   Unless required by applicable law or agreed to in writing, software
-   distributed under the License is distributed on an "AS IS" BASIS,
-   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-   See the License for the specific language governing permissions and
-   limitations under the License.
diff --git a/third_party/gerrit-queue/README.md b/third_party/gerrit-queue/README.md
deleted file mode 100644
index 9ffb81b8d2..0000000000
--- a/third_party/gerrit-queue/README.md
+++ /dev/null
@@ -1,80 +0,0 @@
-# gerrit-queue
-
-This daemon automatically rebases and submits changesets from a Gerrit
-instance, ensuring they still pass CI.
-
-In a usual gerrit setup with a linear master history, different developers
-await CI feedback on a rebased changeset, then one clicks submit, and
-effectively makes everybody else rebase again. `gerrit-queue` is meant to
-remove these races to master.
-
-Developers can set the `Autosubmit` label to `+1` on all changesets in a series,
-and if all preconditions on are met ("submittable" in gerrit speech, this
-usually means passing CI and passing Code Review), `gerrit-queue` takes care of
-rebasing and submitting it to master
-
-## How it works
-Gerrit only knows about Changesets (and some relations to other changesets),
-but usually developers think in terms of multiple changesets.
-
-### Fetching changesets
-`gerrit-queue` fetches all changesets from gerrit, and tries to identify these
-chains of changesets. We call them `Series`. All changesets need to have strict
-parent/child relationships to be detected (so if only half of the stack gets
-rebased by the Gerrit Web interface, these are considered individual series.
-
-Series are sorted by the number of changesets in them. This ensures longer
-series are merged faster, and less rebases are triggered. In the future, this
-might be extended to other metrics.
-
-### Submitting changesets
-The submitqueue has a Trigger() function, which gets periodically executed.
-
-It can keep a reference to one single serie across multiple runs. This is
-necessary if it previously rebased one serie to current HEAD and needs to wait
-some time until CI feedback is there. If it wouldn't keep that state, it would
-pick another series (with +1 from CI) and trigger a rebase on that one, so
-depending on CI run times and trigger intervals, if not keepig this information
-it'd end up rebasing all unrebased changesets on the same HEAD, and then just
-pick one, instead of waiting for the one to finish.
-
-The Trigger() function first instructs the gerrit client to fetch changesets
-and assemble series.
-If there is a `wipSerie` from a previous run, we check if it can still be found
-in the newly assembled list of series (it still needs to contain the same
-number of series. Commit IDs may differ, because the code doesn't reassemble a
-`wipSerie` after scheduling a rebase.
-If the `wipSerie` could be refreshed, we update the pointer with the newly
-assembled series. If we couldn't find it, we drop it.
-
-Now, we enter the main for loop. The first half of the loop checks various
-conditions of the current `wipSerie`, and if successful, does the submit
-("Submit phase"), the second half will pick a suitable new `wipSerie`, and
-potentially do a rebase ("Pick phase").
-
-#### Submit phase
-We check if there is an existing `wipSerie`. If there isn't, we immediately go to
-the "pick" phase.
-
-The `wipSerie` still needs to be rebased on `HEAD` (otherwise, the submit queue
-advanced outside of gerrit), and should not fail CI (logical merge conflict) -
-otherwise we discard it, and continue with the picking phase.
-
-If the `wipSerie` still contains a changeset awaiting CI feedback, we `return`
-from the `Trigger()` function (and go back to sleep).
-
-If the changeset is "submittable" in gerrit speech, and has the necessary
-submit queue tag set, we submit it.
-
-#### Pick phase
-The pick phase finds a new `wipSerie`. It'll first try to find one that already
-is rebased on the current `HEAD` (so the loop can just continue, and the next
-submit phase simply submit), and otherwise fall back to a not-yet-rebased
-serie. Because the rebase mandates waiting for CI, the code `return`s the
-`Trigger()` function, so it'll be called again after waiting some time.
-
-## Compile and Run
-```sh
-go generate
-GERRIT_PASSWORD=mypassword go run main.go --url https://gerrit.mydomain.com --username myuser --project myproject
-```
diff --git a/third_party/gerrit-queue/default.nix b/third_party/gerrit-queue/default.nix
deleted file mode 100644
index 427e312183..0000000000
--- a/third_party/gerrit-queue/default.nix
+++ /dev/null
@@ -1,14 +0,0 @@
-{ pkgs, lib, ... }:
-
-pkgs.buildGoModule {
-  pname = "gerrit-queue";
-  version = "master";
-  vendorSha256 = "0n5h7j416yb2mwic9c3rhqza64jlvl7iw507r9mkw3jadn4whm7a";
-  src = ./.;
-
-  meta = with lib; {
-    description = "Gerrit submit bot";
-    homepage = "https://github.com/tweag/gerrit-queue";
-    license = licenses.asl20;
-  };
-}
diff --git a/third_party/gerrit-queue/frontend/frontend.go b/third_party/gerrit-queue/frontend/frontend.go
deleted file mode 100644
index 2cc65423f0..0000000000
--- a/third_party/gerrit-queue/frontend/frontend.go
+++ /dev/null
@@ -1,113 +0,0 @@
-package frontend
-
-import (
-	"embed"
-	"encoding/json"
-	"fmt"
-	"io/ioutil"
-	"net/http"
-
-	"html/template"
-
-	"github.com/apex/log"
-
-	"github.com/tweag/gerrit-queue/gerrit"
-	"github.com/tweag/gerrit-queue/misc"
-	"github.com/tweag/gerrit-queue/submitqueue"
-)
-
-//go:embed templates
-var templates embed.FS
-
-//loadTemplate loads a list of templates, relative to the templates root, and a
-//FuncMap, and returns a template object
-func loadTemplate(templateNames []string, funcMap template.FuncMap) (*template.Template, error) {
-	if len(templateNames) == 0 {
-		return nil, fmt.Errorf("templateNames can't be empty")
-	}
-	tmpl := template.New(templateNames[0]).Funcs(funcMap)
-
-	for _, templateName := range templateNames {
-		r, err := templates.Open("/" + templateName)
-		if err != nil {
-			return nil, err
-		}
-		defer r.Close()
-		contents, err := ioutil.ReadAll(r)
-		if err != nil {
-			return nil, err
-		}
-		tmpl, err = tmpl.Parse(string(contents))
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return tmpl, nil
-}
-
-// MakeFrontend returns a http.Handler
-func MakeFrontend(rotatingLogHandler *misc.RotatingLogHandler, gerritClient *gerrit.Client, runner *submitqueue.Runner) http.Handler {
-	projectName := gerritClient.GetProjectName()
-	branchName := gerritClient.GetBranchName()
-
-	mux := http.NewServeMux()
-	mux.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) {
-		var wipSerie *gerrit.Serie = nil
-		HEAD := ""
-		currentlyRunning := runner.IsCurrentlyRunning()
-
-		// don't trigger operations requiring a lock
-		if !currentlyRunning {
-			wipSerie = runner.GetWIPSerie()
-			HEAD = gerritClient.GetHEAD()
-		}
-
-		funcMap := template.FuncMap{
-			"changesetURL": func(changeset *gerrit.Changeset) string {
-				return gerritClient.GetChangesetURL(changeset)
-			},
-			"levelToClasses": func(level log.Level) string {
-				switch level {
-				case log.DebugLevel:
-					return "text-muted"
-				case log.InfoLevel:
-					return "text-info"
-				case log.WarnLevel:
-					return "text-warning"
-				case log.ErrorLevel:
-					return "text-danger"
-				case log.FatalLevel:
-					return "text-danger"
-				default:
-					return "text-white"
-				}
-			},
-			"fieldsToJSON": func(fields log.Fields) string {
-				jsonData, _ := json.Marshal(fields)
-				return string(jsonData)
-			},
-		}
-
-		tmpl := template.Must(loadTemplate([]string{
-			"index.tmpl.html",
-			"serie.tmpl.html",
-			"changeset.tmpl.html",
-		}, funcMap))
-
-		tmpl.ExecuteTemplate(w, "index.tmpl.html", map[string]interface{}{
-			// Config
-			"projectName": projectName,
-			"branchName":  branchName,
-
-			// State
-			"currentlyRunning": currentlyRunning,
-			"wipSerie":         wipSerie,
-			"HEAD":             HEAD,
-
-			// History
-			"memory": rotatingLogHandler,
-		})
-	})
-	return mux
-}
diff --git a/third_party/gerrit-queue/frontend/templates/changeset.tmpl.html b/third_party/gerrit-queue/frontend/templates/changeset.tmpl.html
deleted file mode 100644
index 5d3997885c..0000000000
--- a/third_party/gerrit-queue/frontend/templates/changeset.tmpl.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{{ define "changeset" }}
-<tr>
-    <td>{{ .OwnerName }}</td>
-    <td>
-    <strong>{{ .Subject }}</strong> (<a href="{{ changesetURL . }}" target="_blank">#{{ .Number }}</a>)<br />
-    <small><code>{{ .CommitID }}</code></small>
-    </td>
-    <td>
-    <span>
-        {{ if .IsVerified }}<span class="badge badge-success badge-pill">+1 (CI)</span>{{ end }}
-        {{ if .IsCodeReviewed }}<span class="badge badge-info badge-pill">+2 (CR)</span>{{ end }}
-    </span>
-    </td>
-</tr>
-{{ end }}
\ No newline at end of file
diff --git a/third_party/gerrit-queue/frontend/templates/index.tmpl.html b/third_party/gerrit-queue/frontend/templates/index.tmpl.html
deleted file mode 100644
index e04c0a349d..0000000000
--- a/third_party/gerrit-queue/frontend/templates/index.tmpl.html
+++ /dev/null
@@ -1,76 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
-  <title>Gerrit Submit Queue</title>
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js" integrity="sha256-WpOohJOqMqqyKL9FccASB9O0KwACQJpFTUBLTYOVvVU=" crossorigin="anonymous"></script>
-  <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha256-CjSoeELFOcH0/uxWu6mC/Vlrc1AARqbm/jiiImDGV3s=" crossorigin="anonymous"></script>
-  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha256-YLGeXaapI0/5IgZopewRJcFXomhRMlYYjugPLSyNjTY=" crossorigin="anonymous" />
-</head>
-<body>
-  <nav class="navbar sticky-top navbar-expand-sm navbar-dark bg-dark">
-    <div class="container">
-      <a class="navbar-brand" href="#">Gerrit Submit Queue</a>
-      <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
-        <span class="navbar-toggler-icon"></span>
-      </button>
-      <div class="collapse navbar-collapse" id="navbarSupportedContent">
-        <ul class="navbar-nav mr-auto">
-          <li class="nav-item">
-            <a class="nav-link" href="#region-info">Info</a>
-          </li>
-          <li class="nav-item">
-            <a class="nav-link" href="#region-wipserie">WIP Serie</a>
-          </li>
-          <li class="nav-item">
-            <a class="nav-link" href="#region-log">Log</a>
-          </li>
-        </ul>
-      </div>
-    </div>
-  </nav>
-  <div class="container">
-    <h2 id="region-info">Info</h2>
-    <table class="table">
-      <tbody>
-        <tr>
-          <th scope="row">Project Name:</th>
-          <td>{{ .projectName }}</td>
-        </tr>
-        <tr>
-          <th scope="row">Branch Name:</th>
-          <td>{{ .branchName }}</td>
-        </tr>
-        <tr>
-          <th scope="row">Currently running:</th>
-          <td>
-            {{ if .currentlyRunning }}yes{{ else }}no{{ end }}
-          </td>
-        </tr>
-        <tr>
-          <th scope="row">HEAD:</th>
-          <td>
-            {{ if .HEAD }}{{ .HEAD }}{{ else }}-{{ end }}
-          </td>
-        </tr>
-      </tbody>
-    </table>
-
-    <h2 id="region-wipserie">WIP Serie</h2>
-    {{ if .wipSerie }}
-    {{ block "serie" .wipSerie }}{{ end }}
-    {{ else }}
-    - 
-    {{ end }}
-
-    <h2 id="region-log">Log</h2>
-    {{ range $entry := .memory.Entries }}
-    <div class="d-flex flex-row bg-dark {{ levelToClasses $entry.Level }} text-monospace"> 
-      <div class="p-2"><small>{{ $entry.Timestamp.Format "2006-01-02 15:04:05 UTC"}}</small></div>
-      <div class="p-2 flex-grow-1"><small><strong>{{ $entry.Message }}</strong></small></div>
-    </div>
-    <div class="bg-dark {{ levelToClasses $entry.Level }} text-monospace text-break" style="padding-left: 4rem"> 
-    <small>{{ fieldsToJSON $entry.Fields }}</small>
-    </div>
-    {{ end }}
-</body>
-</html>
diff --git a/third_party/gerrit-queue/frontend/templates/serie.tmpl.html b/third_party/gerrit-queue/frontend/templates/serie.tmpl.html
deleted file mode 100644
index 60f0c18113..0000000000
--- a/third_party/gerrit-queue/frontend/templates/serie.tmpl.html
+++ /dev/null
@@ -1,19 +0,0 @@
-{{ define "serie" }}
-<table class="table table-sm table-hover">
-<thead class="thead-light">
-    <tr>
-    <th scope="col">Owner</th>
-    <th scope="col">Changeset</th>
-    <th scope="col">Flags</th>
-    </tr>
-</thead>
-<tbody>
-    <tr>
-        <td colspan="3" class="table-success">Serie with {{ len .ChangeSets }} changes</td>
-    </tr>
-    {{ range $changeset := .ChangeSets }}
-    {{ block "changeset" $changeset }}{{ end }}
-    {{ end }}
-</tbody>
-</table>
-{{ end }}
\ No newline at end of file
diff --git a/third_party/gerrit-queue/gerrit/changeset.go b/third_party/gerrit-queue/gerrit/changeset.go
deleted file mode 100644
index f71032a567..0000000000
--- a/third_party/gerrit-queue/gerrit/changeset.go
+++ /dev/null
@@ -1,117 +0,0 @@
-package gerrit
-
-import (
-	"bytes"
-	"fmt"
-
-	goGerrit "github.com/andygrunwald/go-gerrit"
-	"github.com/apex/log"
-)
-
-// Changeset represents a single changeset
-// Relationships between different changesets are described in Series
-type Changeset struct {
-	changeInfo      *goGerrit.ChangeInfo
-	ChangeID        string
-	Number          int
-	Verified        int
-	CodeReviewed    int
-	Autosubmit      int
-	Submittable     bool
-	CommitID        string
-	ParentCommitIDs []string
-	OwnerName       string
-	Subject         string
-}
-
-// MakeChangeset creates a new Changeset object out of a goGerrit.ChangeInfo object
-func MakeChangeset(changeInfo *goGerrit.ChangeInfo) *Changeset {
-	return &Changeset{
-		changeInfo:      changeInfo,
-		ChangeID:        changeInfo.ChangeID,
-		Number:          changeInfo.Number,
-		Verified:        labelInfoToInt(changeInfo.Labels["Verified"]),
-		CodeReviewed:    labelInfoToInt(changeInfo.Labels["Code-Review"]),
-		Autosubmit:      labelInfoToInt(changeInfo.Labels["Autosubmit"]),
-		Submittable:     changeInfo.Submittable,
-		CommitID:        changeInfo.CurrentRevision, // yes, this IS the commit ID.
-		ParentCommitIDs: getParentCommitIDs(changeInfo),
-		OwnerName:       changeInfo.Owner.Name,
-		Subject:         changeInfo.Subject,
-	}
-}
-
-// IsAutosubmit returns true if the changeset is intended to be
-// automatically submitted by gerrit-queue.
-//
-// This is determined by the Change Owner setting +1 on the
-// "Autosubmit" label.
-func (c *Changeset) IsAutosubmit() bool {
-	return c.Autosubmit == 1
-}
-
-// IsVerified returns true if the changeset passed CI,
-// that's when somebody left the Approved (+1) on the "Verified" label
-func (c *Changeset) IsVerified() bool {
-	return c.Verified == 1
-}
-
-// IsCodeReviewed returns true if the changeset passed code review,
-// that's when somebody left the Recommended (+2) on the "Code-Review" label
-func (c *Changeset) IsCodeReviewed() bool {
-	return c.CodeReviewed == 2
-}
-
-func (c *Changeset) String() string {
-	var b bytes.Buffer
-	b.WriteString("Changeset")
-	b.WriteString(fmt.Sprintf("(commitID: %.7s, author: %s, subject: %s, submittable: %v)",
-		c.CommitID, c.OwnerName, c.Subject, c.Submittable))
-	return b.String()
-}
-
-// FilterChangesets filters a list of Changeset by a given filter function
-func FilterChangesets(changesets []*Changeset, f func(*Changeset) bool) []*Changeset {
-	newChangesets := make([]*Changeset, 0)
-	for _, changeset := range changesets {
-		if f(changeset) {
-			newChangesets = append(newChangesets, changeset)
-		} else {
-			log.WithField("changeset", changeset.String()).Debug("dropped by filter")
-		}
-	}
-	return newChangesets
-}
-
-// labelInfoToInt converts a goGerrit.LabelInfo to -2โ€ฆ+2 int
-func labelInfoToInt(labelInfo goGerrit.LabelInfo) int {
-	if labelInfo.Recommended.AccountID != 0 {
-		return 2
-	}
-	if labelInfo.Approved.AccountID != 0 {
-		return 1
-	}
-	if labelInfo.Disliked.AccountID != 0 {
-		return -1
-	}
-	if labelInfo.Rejected.AccountID != 0 {
-		return -2
-	}
-	return 0
-}
-
-// getParentCommitIDs returns the parent commit IDs of the goGerrit.ChangeInfo
-// There is usually only one parent commit ID, except for merge commits.
-func getParentCommitIDs(changeInfo *goGerrit.ChangeInfo) []string {
-	// obtain the RevisionInfo object
-	revisionInfo := changeInfo.Revisions[changeInfo.CurrentRevision]
-
-	// obtain the Commit object
-	commit := revisionInfo.Commit
-
-	commitIDs := make([]string, len(commit.Parents))
-	for i, commit := range commit.Parents {
-		commitIDs[i] = commit.Commit
-	}
-	return commitIDs
-}
diff --git a/third_party/gerrit-queue/gerrit/client.go b/third_party/gerrit-queue/gerrit/client.go
deleted file mode 100644
index 314f97281c..0000000000
--- a/third_party/gerrit-queue/gerrit/client.go
+++ /dev/null
@@ -1,220 +0,0 @@
-package gerrit
-
-import (
-	"fmt"
-
-	goGerrit "github.com/andygrunwald/go-gerrit"
-	"github.com/apex/log"
-
-	"net/url"
-)
-
-// passed to gerrit when retrieving changesets
-var additionalFields = []string{
-	"LABELS",
-	"CURRENT_REVISION",
-	"CURRENT_COMMIT",
-	"DETAILED_ACCOUNTS",
-	"SUBMITTABLE",
-}
-
-// IClient defines the gerrit.Client interface
-type IClient interface {
-	Refresh() error
-	GetHEAD() string
-	GetBaseURL() string
-	GetChangesetURL(changeset *Changeset) string
-	SubmitChangeset(changeset *Changeset) (*Changeset, error)
-	RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error)
-	ChangesetIsRebasedOnHEAD(changeset *Changeset) bool
-	SerieIsRebasedOnHEAD(serie *Serie) bool
-	FilterSeries(filter func(s *Serie) bool) []*Serie
-	FindSerie(filter func(s *Serie) bool) *Serie
-}
-
-var _ IClient = &Client{}
-
-// Client provides some ways to interact with a gerrit instance
-type Client struct {
-	client      *goGerrit.Client
-	logger      *log.Logger
-	baseURL     string
-	projectName string
-	branchName  string
-	series      []*Serie
-	head        string
-}
-
-// NewClient initializes a new gerrit client
-func NewClient(logger *log.Logger, URL, username, password, projectName, branchName string) (*Client, error) {
-	urlParsed, err := url.Parse(URL)
-	if err != nil {
-		return nil, err
-	}
-	urlParsed.User = url.UserPassword(username, password)
-
-	goGerritClient, err := goGerrit.NewClient(urlParsed.String(), nil)
-	if err != nil {
-		return nil, err
-	}
-	return &Client{
-		client:      goGerritClient,
-		baseURL:     URL,
-		logger:      logger,
-		projectName: projectName,
-		branchName:  branchName,
-	}, nil
-}
-
-// refreshHEAD queries the commit ID of the selected project and branch
-func (c *Client) refreshHEAD() (string, error) {
-	branchInfo, _, err := c.client.Projects.GetBranch(c.projectName, c.branchName)
-	if err != nil {
-		return "", err
-	}
-	return branchInfo.Revision, nil
-}
-
-// GetHEAD returns the internally stored HEAD
-func (c *Client) GetHEAD() string {
-	return c.head
-}
-
-// Refresh causes the client to refresh internal view of gerrit
-func (c *Client) Refresh() error {
-	c.logger.Debug("refreshing from gerrit")
-	HEAD, err := c.refreshHEAD()
-	if err != nil {
-		return err
-	}
-	c.head = HEAD
-
-	var queryString = fmt.Sprintf("status:open project:%s branch:%s", c.projectName, c.branchName)
-	c.logger.Debugf("fetching changesets: %s", queryString)
-	changesets, err := c.fetchChangesets(queryString)
-	if err != nil {
-		return err
-	}
-
-	c.logger.Infof("assembling seriesโ€ฆ")
-	series, err := AssembleSeries(changesets, c.logger)
-	if err != nil {
-		return err
-	}
-	series = SortSeries(series)
-	c.series = series
-	return nil
-}
-
-// fetchChangesets fetches a list of changesets matching a passed query string
-func (c *Client) fetchChangesets(queryString string) (changesets []*Changeset, Error error) {
-	opt := &goGerrit.QueryChangeOptions{}
-	opt.Query = []string{
-		queryString,
-	}
-	opt.AdditionalFields = additionalFields
-	changes, _, err := c.client.Changes.QueryChanges(opt)
-	if err != nil {
-		return nil, err
-	}
-
-	changesets = make([]*Changeset, 0)
-	for _, change := range *changes {
-		changesets = append(changesets, MakeChangeset(&change))
-	}
-
-	return changesets, nil
-}
-
-// fetchChangeset downloads an existing Changeset from gerrit, by its ID
-// Gerrit's API is a bit sparse, and only returns what you explicitly ask it
-// This is used to refresh an existing changeset with more data.
-func (c *Client) fetchChangeset(changeID string) (*Changeset, error) {
-	opt := goGerrit.ChangeOptions{}
-	opt.AdditionalFields = []string{"LABELS", "DETAILED_ACCOUNTS"}
-	changeInfo, _, err := c.client.Changes.GetChange(changeID, &opt)
-	if err != nil {
-		return nil, err
-	}
-	return MakeChangeset(changeInfo), nil
-}
-
-// SubmitChangeset submits a given changeset, and returns a changeset afterwards.
-func (c *Client) SubmitChangeset(changeset *Changeset) (*Changeset, error) {
-	changeInfo, _, err := c.client.Changes.SubmitChange(changeset.ChangeID, &goGerrit.SubmitInput{})
-	if err != nil {
-		return nil, err
-	}
-	c.head = changeInfo.CurrentRevision
-	return c.fetchChangeset(changeInfo.ChangeID)
-}
-
-// RebaseChangeset rebases a given changeset on top of a given ref
-func (c *Client) RebaseChangeset(changeset *Changeset, ref string) (*Changeset, error) {
-	changeInfo, _, err := c.client.Changes.RebaseChange(changeset.ChangeID, &goGerrit.RebaseInput{
-		Base: ref,
-	})
-	if err != nil {
-		return changeset, err
-	}
-	return c.fetchChangeset(changeInfo.ChangeID)
-}
-
-// GetBaseURL returns the gerrit base URL
-func (c *Client) GetBaseURL() string {
-	return c.baseURL
-}
-
-// GetProjectName returns the configured gerrit project name
-func (c *Client) GetProjectName() string {
-	return c.projectName
-}
-
-// GetBranchName returns the configured gerrit branch name
-func (c *Client) GetBranchName() string {
-	return c.branchName
-}
-
-// GetChangesetURL returns the URL to view a given changeset
-func (c *Client) GetChangesetURL(changeset *Changeset) string {
-	return fmt.Sprintf("%s/c/%s/+/%d", c.GetBaseURL(), c.projectName, changeset.Number)
-}
-
-// ChangesetIsRebasedOnHEAD returns true if the changeset is rebased on the current HEAD
-func (c *Client) ChangesetIsRebasedOnHEAD(changeset *Changeset) bool {
-	if len(changeset.ParentCommitIDs) != 1 {
-		return false
-	}
-	return changeset.ParentCommitIDs[0] == c.head
-}
-
-// SerieIsRebasedOnHEAD returns true if the whole series is rebased on the current HEAD
-// this is already the case if the first changeset in the series is rebased on the current HEAD
-func (c *Client) SerieIsRebasedOnHEAD(serie *Serie) bool {
-	// an empty serie should not exist
-	if len(serie.ChangeSets) == 0 {
-		return false
-	}
-	return c.ChangesetIsRebasedOnHEAD(serie.ChangeSets[0])
-}
-
-// FilterSeries returns a subset of all Series, passing the given filter function
-func (c *Client) FilterSeries(filter func(s *Serie) bool) []*Serie {
-	matchedSeries := []*Serie{}
-	for _, serie := range c.series {
-		if filter(serie) {
-			matchedSeries = append(matchedSeries, serie)
-		}
-	}
-	return matchedSeries
-}
-
-// FindSerie returns the first serie that matches the filter, or nil if none was found
-func (c *Client) FindSerie(filter func(s *Serie) bool) *Serie {
-	for _, serie := range c.series {
-		if filter(serie) {
-			return serie
-		}
-	}
-	return nil
-}
diff --git a/third_party/gerrit-queue/gerrit/serie.go b/third_party/gerrit-queue/gerrit/serie.go
deleted file mode 100644
index 788cf46f4e..0000000000
--- a/third_party/gerrit-queue/gerrit/serie.go
+++ /dev/null
@@ -1,112 +0,0 @@
-package gerrit
-
-import (
-	"fmt"
-	"strings"
-
-	"github.com/apex/log"
-)
-
-// Serie represents a list of successive changesets with an unbroken parent -> child relation,
-// starting from the parent.
-type Serie struct {
-	ChangeSets []*Changeset
-}
-
-// GetParentCommitIDs returns the parent commit IDs
-func (s *Serie) GetParentCommitIDs() ([]string, error) {
-	if len(s.ChangeSets) == 0 {
-		return nil, fmt.Errorf("Can't return parent on a serie with zero ChangeSets")
-	}
-	return s.ChangeSets[0].ParentCommitIDs, nil
-}
-
-// GetLeafCommitID returns the commit id of the last commit in ChangeSets
-func (s *Serie) GetLeafCommitID() (string, error) {
-	if len(s.ChangeSets) == 0 {
-		return "", fmt.Errorf("Can't return leaf on a serie with zero ChangeSets")
-	}
-	return s.ChangeSets[len(s.ChangeSets)-1].CommitID, nil
-}
-
-// CheckIntegrity checks that the series contains a properly ordered and connected chain of commits
-func (s *Serie) CheckIntegrity() error {
-	logger := log.WithField("serie", s)
-	// an empty serie is invalid
-	if len(s.ChangeSets) == 0 {
-		return fmt.Errorf("An empty serie is invalid")
-	}
-
-	previousCommitID := ""
-	for i, changeset := range s.ChangeSets {
-		// we can't really check the parent of the first commit
-		// so skip verifying that one
-		logger.WithFields(log.Fields{
-			"changeset":        changeset.String(),
-			"previousCommitID": fmt.Sprintf("%.7s", previousCommitID),
-		}).Debug(" - verifying changeset")
-
-		parentCommitIDs := changeset.ParentCommitIDs
-		if len(parentCommitIDs) == 0 {
-			return fmt.Errorf("Changesets without any parent are not supported")
-		}
-		// we don't check parents of the first changeset in a series
-		if i != 0 {
-			if len(parentCommitIDs) != 1 {
-				return fmt.Errorf("Merge commits in the middle of a series are not supported (only at the beginning)")
-			}
-			if parentCommitIDs[0] != previousCommitID {
-				return fmt.Errorf("changesets parent commit id doesn't match previous commit id")
-			}
-		}
-		// update previous commit id for the next loop iteration
-		previousCommitID = changeset.CommitID
-	}
-	return nil
-}
-
-// FilterAllChangesets applies a filter function on all of the changesets in the series.
-// returns true if it returns true for all changesets, false otherwise
-func (s *Serie) FilterAllChangesets(f func(c *Changeset) bool) bool {
-	for _, changeset := range s.ChangeSets {
-		if f(changeset) == false {
-			return false
-		}
-	}
-	return true
-}
-
-func (s *Serie) String() string {
-	var sb strings.Builder
-	sb.WriteString(fmt.Sprintf("Serie[%d]", len(s.ChangeSets)))
-	if len(s.ChangeSets) == 0 {
-		sb.WriteString("()\n")
-		return sb.String()
-	}
-	parentCommitIDs, err := s.GetParentCommitIDs()
-	if err == nil {
-		if len(parentCommitIDs) == 1 {
-			sb.WriteString(fmt.Sprintf("(parent: %.7s)", parentCommitIDs[0]))
-		} else {
-			sb.WriteString("(merge: ")
-
-			for i, parentCommitID := range parentCommitIDs {
-				sb.WriteString(fmt.Sprintf("%.7s", parentCommitID))
-				if i < len(parentCommitIDs) {
-					sb.WriteString(", ")
-				}
-			}
-
-			sb.WriteString(")")
-
-		}
-	}
-	sb.WriteString(fmt.Sprintf("(%.7s..%.7s)",
-		s.ChangeSets[0].CommitID,
-		s.ChangeSets[len(s.ChangeSets)-1].CommitID))
-	return sb.String()
-}
-
-func shortCommitID(commitID string) string {
-	return commitID[:6]
-}
diff --git a/third_party/gerrit-queue/gerrit/series.go b/third_party/gerrit-queue/gerrit/series.go
deleted file mode 100644
index 295193ee95..0000000000
--- a/third_party/gerrit-queue/gerrit/series.go
+++ /dev/null
@@ -1,126 +0,0 @@
-package gerrit
-
-import (
-	"sort"
-
-	"github.com/apex/log"
-)
-
-// AssembleSeries consumes a list of `Changeset`, and groups them together to series
-//
-// We initially put every Changeset in its own Serie
-//
-// As we have no control over the order of the passed changesets,
-// we maintain a lookup table, mapLeafToSerie,
-// which allows to lookup a serie by its leaf commit id
-// We concat series in a fixpoint approach
-// because both appending and prepending is much more complex.
-// Concatenation moves changesets of the later changeset in the previous one
-// in a cleanup phase, we remove orphaned series (those without any changesets inside)
-// afterwards, we do an integrity check, just to be on the safe side.
-func AssembleSeries(changesets []*Changeset, logger *log.Logger) ([]*Serie, error) {
-	series := make([]*Serie, 0)
-	mapLeafToSerie := make(map[string]*Serie, 0)
-
-	for _, changeset := range changesets {
-		l := logger.WithField("changeset", changeset.String())
-
-		l.Debug("creating initial serie")
-		serie := &Serie{
-			ChangeSets: []*Changeset{changeset},
-		}
-		series = append(series, serie)
-		mapLeafToSerie[changeset.CommitID] = serie
-	}
-
-	// Combine series using a fixpoint approach, with a max iteration count.
-	logger.Debug("glueing together phase")
-	for i := 1; i < 100; i++ {
-		didUpdate := false
-		logger.Debugf("at iteration %d", i)
-		for j, serie := range series {
-			l := logger.WithFields(log.Fields{
-				"i":     i,
-				"j":     j,
-				"serie": serie.String(),
-			})
-			parentCommitIDs, err := serie.GetParentCommitIDs()
-			if err != nil {
-				return series, err
-			}
-			if len(parentCommitIDs) != 1 {
-				// We can't append merge commits to other series
-				l.Infof("No single parent, skipping.")
-				continue
-			}
-			parentCommitID := parentCommitIDs[0]
-			l.Debug("Looking for a predecessor.")
-			// if there's another serie that has this parent as a leaf, glue together
-			if otherSerie, ok := mapLeafToSerie[parentCommitID]; ok {
-				if otherSerie == serie {
-					continue
-				}
-				l = l.WithField("otherSerie", otherSerie)
-
-				myLeafCommitID, err := serie.GetLeafCommitID()
-				if err != nil {
-					return series, err
-				}
-
-				// append our changesets to the other serie
-				l.Debug("Splicing together.")
-				otherSerie.ChangeSets = append(otherSerie.ChangeSets, serie.ChangeSets...)
-
-				delete(mapLeafToSerie, parentCommitID)
-				mapLeafToSerie[myLeafCommitID] = otherSerie
-
-				// orphan our serie
-				serie.ChangeSets = []*Changeset{}
-				// remove the orphaned serie from the lookup table
-				delete(mapLeafToSerie, myLeafCommitID)
-
-				didUpdate = true
-			} else {
-				l.Debug("Not found.")
-			}
-		}
-		series = removeOrphanedSeries(series)
-		if !didUpdate {
-			logger.Infof("converged after %d iterations", i)
-			break
-		}
-	}
-
-	// Check integrity, just to be on the safe side.
-	for _, serie := range series {
-		l := logger.WithField("serie", serie.String())
-		l.Debugf("checking integrity")
-		err := serie.CheckIntegrity()
-		if err != nil {
-			l.Errorf("checking integrity failed: %s", err)
-		}
-	}
-	return series, nil
-}
-
-// removeOrphanedSeries removes all empty series (that contain zero changesets)
-func removeOrphanedSeries(series []*Serie) []*Serie {
-	newSeries := []*Serie{}
-	for _, serie := range series {
-		if len(serie.ChangeSets) != 0 {
-			newSeries = append(newSeries, serie)
-		}
-	}
-	return newSeries
-}
-
-// SortSeries sorts a list of series by the number of changesets in each serie, descending
-func SortSeries(series []*Serie) []*Serie {
-	newSeries := make([]*Serie, len(series))
-	copy(newSeries, series)
-	sort.Slice(newSeries, func(i, j int) bool {
-		// the weight depends on the amount of changesets series changeset size
-		return len(series[i].ChangeSets) > len(series[j].ChangeSets)
-	})
-	return newSeries
-}
diff --git a/third_party/gerrit-queue/go.mod b/third_party/gerrit-queue/go.mod
deleted file mode 100644
index 3929f8cf65..0000000000
--- a/third_party/gerrit-queue/go.mod
+++ /dev/null
@@ -1,10 +0,0 @@
-module github.com/tweag/gerrit-queue
-
-go 1.16
-
-require (
-	github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8
-	github.com/apex/log v1.1.1
-	github.com/google/go-querystring v1.0.0 // indirect
-	github.com/urfave/cli v1.22.1
-)
diff --git a/third_party/gerrit-queue/go.sum b/third_party/gerrit-queue/go.sum
deleted file mode 100644
index d11545a6c9..0000000000
--- a/third_party/gerrit-queue/go.sum
+++ /dev/null
@@ -1,69 +0,0 @@
-github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8 h1:9PvNa6zH6gOW4VVfbAx5rjDLpxunG+RSaXQB+8TEv4w=
-github.com/andygrunwald/go-gerrit v0.0.0-20190825170856-5959a9bf9ff8/go.mod h1:0iuRQp6WJ44ts+iihy5E/WlPqfg5RNeQxOmzRkxCdtk=
-github.com/apex/log v1.1.1 h1:BwhRZ0qbjYtTob0I+2M+smavV0kOC8XgcnGZcyL9liA=
-github.com/apex/log v1.1.1/go.mod h1:Ls949n1HFtXfbDcjiTTFQqkVUrte0puoIBfO3SVgwOA=
-github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
-github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
-github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
-github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
-github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
-github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
-github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
-github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
-github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
-github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
-github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
-github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
-github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
-github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
-github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
-github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
-github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
-github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
-github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
-github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
-github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
-github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
-github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
-github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
-github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
-github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
-github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
-github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY=
-github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
-golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
-golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
-gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/third_party/gerrit-queue/main.go b/third_party/gerrit-queue/main.go
deleted file mode 100644
index eaa792c958..0000000000
--- a/third_party/gerrit-queue/main.go
+++ /dev/null
@@ -1,137 +0,0 @@
-package main
-
-import (
-	"os"
-	"time"
-
-	"net/http"
-
-	"github.com/tweag/gerrit-queue/frontend"
-	"github.com/tweag/gerrit-queue/gerrit"
-	"github.com/tweag/gerrit-queue/misc"
-	"github.com/tweag/gerrit-queue/submitqueue"
-
-	"github.com/urfave/cli"
-
-	"github.com/apex/log"
-	"github.com/apex/log/handlers/multi"
-	"github.com/apex/log/handlers/text"
-)
-
-func main() {
-	var URL, username, password, projectName, branchName string
-	var fetchOnly bool
-	var triggerInterval int
-
-	app := cli.NewApp()
-	app.Name = "gerrit-queue"
-
-	app.Flags = []cli.Flag{
-		cli.StringFlag{
-			Name:        "url",
-			Usage:       "URL to the gerrit instance",
-			EnvVar:      "GERRIT_URL",
-			Destination: &URL,
-			Required:    true,
-		},
-		cli.StringFlag{
-			Name:        "username",
-			Usage:       "Username to use to login to gerrit",
-			EnvVar:      "GERRIT_USERNAME",
-			Destination: &username,
-			Required:    true,
-		},
-		cli.StringFlag{
-			Name:        "password",
-			Usage:       "Password to use to login to gerrit",
-			EnvVar:      "GERRIT_PASSWORD",
-			Destination: &password,
-			Required:    true,
-		},
-		cli.StringFlag{
-			Name:        "project",
-			Usage:       "Gerrit project name to run the submit queue for",
-			EnvVar:      "GERRIT_PROJECT",
-			Destination: &projectName,
-			Required:    true,
-		},
-		cli.StringFlag{
-			Name:        "branch",
-			Usage:       "Destination branch",
-			EnvVar:      "GERRIT_BRANCH",
-			Destination: &branchName,
-			Value:       "master",
-		},
-		cli.IntFlag{
-			Name:        "trigger-interval",
-			Usage:       "How often we should trigger ourselves (interval in seconds)",
-			EnvVar:      "SUBMIT_QUEUE_TRIGGER_INTERVAL",
-			Destination: &triggerInterval,
-			Value:       600,
-		},
-		cli.BoolFlag{
-			Name:        "fetch-only",
-			Usage:       "Only fetch changes and assemble queue, but don't actually write",
-			EnvVar:      "SUBMIT_QUEUE_FETCH_ONLY",
-			Destination: &fetchOnly,
-		},
-	}
-
-	rotatingLogHandler := misc.NewRotatingLogHandler(10000)
-	l := &log.Logger{
-		Handler: multi.New(
-			text.New(os.Stderr),
-			rotatingLogHandler,
-		),
-		Level: log.DebugLevel,
-	}
-
-	app.Action = func(c *cli.Context) error {
-		gerrit, err := gerrit.NewClient(l, URL, username, password, projectName, branchName)
-		if err != nil {
-			return err
-		}
-		log.Infof("Successfully connected to gerrit at %s", URL)
-
-		runner := submitqueue.NewRunner(l, gerrit)
-
-		handler := frontend.MakeFrontend(rotatingLogHandler, gerrit, runner)
-
-		// fetch only on first run
-		err = runner.Trigger(fetchOnly)
-		if err != nil {
-			log.Error(err.Error())
-		}
-
-		// ticker
-		go func() {
-			for {
-				time.Sleep(time.Duration(triggerInterval) * time.Second)
-				err = runner.Trigger(fetchOnly)
-				if err != nil {
-					log.Error(err.Error())
-				}
-			}
-		}()
-
-		server := http.Server{
-			Addr:    ":8080",
-			Handler: handler,
-		}
-
-		server.ListenAndServe()
-		if err != nil {
-			log.Fatalf(err.Error())
-		}
-
-		return nil
-	}
-
-	err := app.Run(os.Args)
-	if err != nil {
-		log.Fatal(err.Error())
-	}
-
-	// TODOS:
-	// - handle event log, either by accepting webhooks, or by streaming events?
-}
diff --git a/third_party/gerrit-queue/misc/rotatingloghandler.go b/third_party/gerrit-queue/misc/rotatingloghandler.go
deleted file mode 100644
index 3d4c5f3a83..0000000000
--- a/third_party/gerrit-queue/misc/rotatingloghandler.go
+++ /dev/null
@@ -1,34 +0,0 @@
-package misc
-
-import (
-	"sync"
-
-	"github.com/apex/log"
-)
-
-// RotatingLogHandler implementation.
-type RotatingLogHandler struct {
-	mu         sync.Mutex
-	Entries    []*log.Entry
-	maxEntries int
-}
-
-// NewRotatingLogHandler creates a new rotating log handler
-func NewRotatingLogHandler(maxEntries int) *RotatingLogHandler {
-	return &RotatingLogHandler{
-		maxEntries: maxEntries,
-	}
-}
-
-// HandleLog implements log.Handler.
-func (h *RotatingLogHandler) HandleLog(e *log.Entry) error {
-	h.mu.Lock()
-	defer h.mu.Unlock()
-	// drop tail if we have more entries than maxEntries
-	if len(h.Entries) > h.maxEntries {
-		h.Entries = append([]*log.Entry{e}, h.Entries[:(h.maxEntries-2)]...)
-	} else {
-		h.Entries = append([]*log.Entry{e}, h.Entries...)
-	}
-	return nil
-}
diff --git a/third_party/gerrit-queue/submitqueue/runner.go b/third_party/gerrit-queue/submitqueue/runner.go
deleted file mode 100644
index 0b4cbcd8dd..0000000000
--- a/third_party/gerrit-queue/submitqueue/runner.go
+++ /dev/null
@@ -1,220 +0,0 @@
-package submitqueue
-
-import (
-	"fmt"
-	"sync"
-
-	"github.com/apex/log"
-
-	"github.com/tweag/gerrit-queue/gerrit"
-)
-
-// Runner is a struct existing across the lifetime of a single run of the submit queue
-// it contains a mutex to avoid being run multiple times.
-// In fact, it even cancels runs while another one is still in progress.
-// It contains a Gerrit object facilitating access, a log object, the configured submit queue tag
-// and a `wipSerie` (only populated if waiting for a rebase)
-type Runner struct {
-	mut              sync.Mutex
-	currentlyRunning bool
-	wipSerie         *gerrit.Serie
-	logger           *log.Logger
-	gerrit           *gerrit.Client
-}
-
-// NewRunner creates a new Runner struct
-func NewRunner(logger *log.Logger, gerrit *gerrit.Client) *Runner {
-	return &Runner{
-		logger: logger,
-		gerrit: gerrit,
-	}
-}
-
-// isAutoSubmittable determines if something could be autosubmitted, potentially requiring a rebase
-// for this, it needs to:
-//  * have the "Autosubmit" label set to +1
-//  * have gerrit's 'submittable' field set to true
-// it doesn't check if the series is rebased on HEAD
-func (r *Runner) isAutoSubmittable(s *gerrit.Serie) bool {
-	for _, c := range s.ChangeSets {
-		if c.Submittable != true || !c.IsAutosubmit() {
-			return false
-		}
-	}
-	return true
-}
-
-// IsCurrentlyRunning returns true if the runner is currently running
-func (r *Runner) IsCurrentlyRunning() bool {
-	return r.currentlyRunning
-}
-
-// GetWIPSerie returns the current wipSerie, if any, nil otherwiese
-// Acquires a lock, so check with IsCurrentlyRunning first
-func (r *Runner) GetWIPSerie() *gerrit.Serie {
-	r.mut.Lock()
-	defer func() {
-		r.mut.Unlock()
-	}()
-	return r.wipSerie
-}
-
-// Trigger gets triggered periodically
-func (r *Runner) Trigger(fetchOnly bool) error {
-	// TODO: If CI fails, remove the auto-submit labels => rules.pl
-	// Only one trigger can run at the same time
-	r.mut.Lock()
-	if r.currentlyRunning {
-		return fmt.Errorf("Already running, skipping")
-	}
-	r.currentlyRunning = true
-	r.mut.Unlock()
-	defer func() {
-		r.mut.Lock()
-		r.currentlyRunning = false
-		r.mut.Unlock()
-	}()
-
-	// Prepare the work by creating a local cache of gerrit state
-	err := r.gerrit.Refresh()
-	if err != nil {
-		return err
-	}
-
-	// early return if we only want to fetch
-	if fetchOnly {
-		return nil
-	}
-
-	if r.wipSerie != nil {
-		// refresh wipSerie with how it looks like in gerrit now
-		wipSerie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool {
-			// the new wipSerie needs to have the same number of changesets
-			if len(r.wipSerie.ChangeSets) != len(s.ChangeSets) {
-				return false
-			}
-			// โ€ฆ and the same ChangeIDs.
-			for idx, c := range s.ChangeSets {
-				if r.wipSerie.ChangeSets[idx].ChangeID != c.ChangeID {
-					return false
-				}
-			}
-			return true
-		})
-		if wipSerie == nil {
-			r.logger.WithField("wipSerie", r.wipSerie).Warn("wipSerie has disappeared")
-			r.wipSerie = nil
-		} else {
-			r.wipSerie = wipSerie
-		}
-	}
-
-	for {
-		// initialize logger
-		r.logger.Info("Running")
-		if r.wipSerie != nil {
-			// if we have a wipSerie
-			l := r.logger.WithField("wipSerie", r.wipSerie)
-			l.Info("Checking wipSerie")
-
-			// discard wipSerie not rebased on HEAD
-			// we rebase them at the end of the loop, so this means master advanced without going through the submit queue
-			if !r.gerrit.SerieIsRebasedOnHEAD(r.wipSerie) {
-				l.Warnf("HEAD has moved to %v while still waiting for wipSerie, discarding it", r.gerrit.GetHEAD())
-				r.wipSerie = nil
-				continue
-			}
-
-			// we now need to check CI feedback:
-			// wipSerie might have failed CI in the meantime
-			for _, c := range r.wipSerie.ChangeSets {
-				if c == nil {
-					l.Error("BUG: changeset is nil")
-					continue
-				}
-				if c.Verified < 0 {
-					l.WithField("failingChangeset", c).Warnf("wipSerie failed CI in the meantime, discarding.")
-					r.wipSerie = nil
-					continue
-				}
-			}
-
-			// it might still be waiting for CI
-			for _, c := range r.wipSerie.ChangeSets {
-				if c == nil {
-					l.Error("BUG: changeset is nil")
-					continue
-				}
-				if c.Verified == 0 {
-					l.WithField("pendingChangeset", c).Warnf("still waiting for CI feedback in wipSerie, going back to sleep.")
-					// break the loop, take a look at it at the next trigger.
-					return nil
-				}
-			}
-
-			// it might be autosubmittable
-			if r.isAutoSubmittable(r.wipSerie) {
-				l.Infof("submitting wipSerie")
-				// if the WIP changeset is ready (auto submittable and rebased on HEAD), submit
-				for _, changeset := range r.wipSerie.ChangeSets {
-					_, err := r.gerrit.SubmitChangeset(changeset)
-					if err != nil {
-						l.WithField("changeset", changeset).Error("error submitting changeset")
-						r.wipSerie = nil
-						return err
-					}
-				}
-				r.wipSerie = nil
-			} else {
-				l.Error("BUG: wipSerie is not autosubmittable")
-				r.wipSerie = nil
-			}
-		}
-
-		r.logger.Info("Looking for series ready to submit")
-		// Find serie, that:
-		//  * has the auto-submit label
-		//  * has +2 review
-		//  * has +1 CI
-		//  * is rebased on master
-		serie := r.gerrit.FindSerie(func(s *gerrit.Serie) bool {
-			return r.isAutoSubmittable(s) && s.ChangeSets[0].ParentCommitIDs[0] == r.gerrit.GetHEAD()
-		})
-		if serie != nil {
-			r.logger.WithField("serie", serie).Info("Found serie to submit without necessary rebase")
-			r.wipSerie = serie
-			continue
-		}
-
-		// Find serie, that:
-		//  * has the auto-submit label
-		//  * has +2 review
-		//  * has +1 CI
-		//  * is NOT rebased on master
-		serie = r.gerrit.FindSerie(r.isAutoSubmittable)
-		if serie == nil {
-			r.logger.Info("no more submittable series found, going back to sleep.")
-			break
-		}
-
-		l := r.logger.WithField("serie", serie)
-		l.Info("found serie, which needs a rebase")
-		// TODO: move into Client.RebaseSeries function
-		head := r.gerrit.GetHEAD()
-		for _, changeset := range serie.ChangeSets {
-			changeset, err := r.gerrit.RebaseChangeset(changeset, head)
-			if err != nil {
-				l.Error(err.Error())
-				return err
-			}
-			head = changeset.CommitID
-		}
-		// we don't need to care about updating the rebased changesets or getting the updated HEAD,
-		// as we'll refetch it on the beginning of the next trigger anyways
-		r.wipSerie = serie
-		break
-	}
-
-	r.logger.Info("Run complete")
-	return nil
-}
diff --git a/third_party/gerrit/0001-Syntax-highlight-nix.patch b/third_party/gerrit/0001-Syntax-highlight-nix.patch
new file mode 100644
index 0000000000..bdc3fd3b55
--- /dev/null
+++ b/third_party/gerrit/0001-Syntax-highlight-nix.patch
@@ -0,0 +1,37 @@
+From 084e4f92fb58f7cd85303ba602fb3c40133c8fcc 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 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<string, string>([
+   ['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/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch b/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch
deleted file mode 100644
index 7d197795b7..0000000000
--- a/third_party/gerrit/0001-Use-detzip-in-download_bower.py.patch
+++ /dev/null
@@ -1,25 +0,0 @@
-From 621cadcc1dd71e9397c21cf8cf0f1aae4f6f7057 Mon Sep 17 00:00:00 2001
-From: Luke Granger-Brown <git@lukegb.com>
-Date: Thu, 2 Jul 2020 23:02:09 +0100
-Subject: [PATCH 1/7] Use detzip in download_bower.py
-
----
- tools/js/download_bower.py | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/tools/js/download_bower.py b/tools/js/download_bower.py
-index d541b565a9..ffdae60f95 100755
---- a/tools/js/download_bower.py
-+++ b/tools/js/download_bower.py
-@@ -110,7 +110,7 @@ def main():
-                 args.b, '--quiet', 'install', '%s#%s' % (args.p, args.v)))
-         bc = os.path.join(cwd, 'bower_components')
-         subprocess.check_call(
--            ['zip', '-q', '--exclude', '.bower.json', '-r', cached, args.n],
-+            ['detzip', '--exclude', '.bower.json', cached, args.n],
-             cwd=bc)
- 
-         if args.s:
--- 
-2.32.0
-
diff --git a/third_party/gerrit/0002-Syntax-highlight-nix.patch b/third_party/gerrit/0002-Syntax-highlight-nix.patch
deleted file mode 100644
index 256da0a3c9..0000000000
--- a/third_party/gerrit/0002-Syntax-highlight-nix.patch
+++ /dev/null
@@ -1,24 +0,0 @@
-From 924647c354576ade0dc46fdf30596967f58bb4c6 Mon Sep 17 00:00:00 2001
-From: Luke Granger-Brown <git@lukegb.com>
-Date: Thu, 2 Jul 2020 23:02:32 +0100
-Subject: [PATCH 2/7] Syntax highlight nix
-
----
- .../app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts         | 1 +
- 1 file changed, 1 insertion(+)
-
-diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
-index 081d28d749..2762ccc625 100644
---- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
-+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
-@@ -99,6 +99,7 @@ const LANGUAGE_MAP = new Map<string, string>([
-   ['text/x-vhdl', 'vhdl'],
-   ['text/x-yaml', 'yaml'],
-   ['text/vbscript', 'vbscript'],
-+  ['application/x-mix-transfer', 'nix'],
- ]);
- const ASYNC_DELAY = 10;
- 
--- 
-2.32.0
-
diff --git a/third_party/gerrit/0002-Syntax-highlight-rules.pl.patch b/third_party/gerrit/0002-Syntax-highlight-rules.pl.patch
new file mode 100644
index 0000000000..4b91e2c354
--- /dev/null
+++ b/third_party/gerrit/0002-Syntax-highlight-rules.pl.patch
@@ -0,0 +1,37 @@
+From aedf8ac8fa5113843bcd83ff85e2d9f3bffdb16c 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 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<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 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/third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch b/third_party/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch
index 8e78e5f535..c4edee3a40 100644
--- a/third_party/gerrit/0004-Add-titles-to-CLs-over-HTTP.patch
+++ b/third_party/gerrit/0003-Add-titles-to-CLs-over-HTTP.patch
@@ -1,30 +1,30 @@
-From 32bf13d8316f93828d2ff47ccfca38d4e7a634b1 Mon Sep 17 00:00:00 2001
+From f49c50ca9a84ca374b7bd91c171bbea0457f2c7a Mon Sep 17 00:00:00 2001
 From: Luke Granger-Brown <git@lukegb.com>
 Date: Thu, 2 Jul 2020 23:03:02 +0100
-Subject: [PATCH 4/7] Add titles to CLs over HTTP
+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 |  6 +-
+ .../google/gerrit/httpd/raw/StaticModule.java |  5 +-
  .../gerrit/httpd/raw/TitleComputer.java       | 67 +++++++++++++++++++
  .../gerrit/httpd/raw/PolyGerritIndexHtml.soy  |  4 +-
- 5 files changed, 90 insertions(+), 8 deletions(-)
+ 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 8d52f5ad50..a9cfceb3b6 100644
+index 72bfe40c3b..439bd73b44 100644
 --- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
 +++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
-@@ -39,6 +39,7 @@ import java.util.Arrays;
- import java.util.Collections;
+@@ -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;
  
-@@ -60,13 +61,14 @@ public class IndexHtmlUtil {
+@@ -62,13 +63,14 @@ public class IndexHtmlUtil {
        String faviconPath,
        Map<String, String[]> urlParameterMap,
        Function<String, SanitizedContent> urlInScriptTagOrdainer,
@@ -38,35 +38,35 @@ index 8d52f5ad50..a9cfceb3b6 100644
                  canonicalURL, cdnPath, faviconPath, urlParameterMap, urlInScriptTagOrdainer))
 -        .putAll(dynamicTemplateData(gerritApi, requestedURL));
 +        .putAll(dynamicTemplateData(gerritApi, requestedURL, titleComputer));
-     Set<String> enabledExperiments = experimentFeatures.getEnabledExperimentFeatures();
- 
-     if (!enabledExperiments.isEmpty()) {
-@@ -77,7 +79,9 @@ public class IndexHtmlUtil {
+     Set<String> 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<String, Object> dynamicTemplateData(
 -      GerritApi gerritApi, String requestedURL) throws RestApiException, URISyntaxException {
-+      GerritApi gerritApi,
-+      String requestedURL,
-+      TitleComputer titleComputer) throws RestApiException, URISyntaxException {
++      GerritApi gerritApi, String requestedURL, TitleComputer titleComputer)
++                throws RestApiException, URISyntaxException {
      ImmutableMap.Builder<String, Object> data = ImmutableMap.builder();
      Map<String, SanitizedContent> initialData = new HashMap<>();
      Server serverApi = gerritApi.config().server();
-@@ -128,6 +132,9 @@ public class IndexHtmlUtil {
-       // Don't render data
+@@ -129,6 +132,10 @@ public class IndexHtmlUtil {
      }
  
+     data.put("gerritInitialData", initialData);
++
 +    Optional<String> title = titleComputer.computeTitle(requestedURL);
 +    title.ifPresent(s -> data.put("title", s));
 +
-     data.put("gerritInitialData", initialData);
      return data.build();
    }
+ 
 diff --git a/java/com/google/gerrit/httpd/raw/IndexServlet.java b/java/com/google/gerrit/httpd/raw/IndexServlet.java
-index 3f2c2028ae..7861c007df 100644
+index fcb821e5ae..e1464b992b 100644
 --- a/java/com/google/gerrit/httpd/raw/IndexServlet.java
 +++ b/java/com/google/gerrit/httpd/raw/IndexServlet.java
-@@ -46,13 +46,15 @@ public class IndexServlet extends HttpServlet {
+@@ -48,13 +48,15 @@ public class IndexServlet extends HttpServlet {
    private final ExperimentFeatures experimentFeatures;
    private final SoySauce soySauce;
    private final Function<String, SanitizedContent> urlOrdainer;
@@ -83,7 +83,7 @@ index 3f2c2028ae..7861c007df 100644
      this.canonicalUrl = canonicalUrl;
      this.cdnPath = cdnPath;
      this.faviconPath = faviconPath;
-@@ -67,6 +69,7 @@ public class IndexServlet extends HttpServlet {
+@@ -69,6 +71,7 @@ public class IndexServlet extends HttpServlet {
          (s) ->
              UnsafeSanitizedContentOrdainer.ordainAsSafe(
                  s, SanitizedContent.ContentKind.TRUSTED_RESOURCE_URI);
@@ -91,33 +91,31 @@ index 3f2c2028ae..7861c007df 100644
    }
  
    @Override
-@@ -85,7 +88,8 @@ public class IndexServlet extends HttpServlet {
+@@ -86,7 +89,8 @@ public class IndexServlet extends HttpServlet {
                faviconPath,
                parameterMap,
                urlOrdainer,
--              requestUrl);
-+              requestUrl,
+-              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 bb1eb92525..6b20c504d2 100644
+index 15dcf42e0e..9f56bf33ce 100644
 --- a/java/com/google/gerrit/httpd/raw/StaticModule.java
 +++ b/java/com/google/gerrit/httpd/raw/StaticModule.java
-@@ -224,11 +224,13 @@ public class StaticModule extends ServletModule {
+@@ -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.useDevCdn() ? options.devCdn() : cfg.getString("gerrit", null, "cdnPath");
+       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);
++      return new IndexServlet(canonicalUrl, cdnPath, faviconPath, gerritApi, experimentFeatures, titleComputer);
      }
  
      @Provides
@@ -195,7 +193,7 @@ index 0000000000..8fd2053ad0
 +  }
 +}
 diff --git a/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy b/resources/com/google/gerrit/httpd/raw/PolyGerritIndexHtml.soy
-index 11717fb8a4..1ae9046360 100644
+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 @@
@@ -213,5 +211,5 @@ index 11717fb8a4..1ae9046360 100644
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0">{\n}
  
 -- 
-2.32.0
+2.37.3
 
diff --git a/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch b/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch
deleted file mode 100644
index 02bb3397ea..0000000000
--- a/third_party/gerrit/0003-Syntax-highlight-rules.pl.patch
+++ /dev/null
@@ -1,46 +0,0 @@
-From be348f64eda257ae0af1f89552548d3e8eca3688 Mon Sep 17 00:00:00 2001
-From: Luke Granger-Brown <git@lukegb.com>
-Date: Thu, 2 Jul 2020 23:02:43 +0100
-Subject: [PATCH 3/7] Syntax highlight rules.pl
-
----
- .../diff/gr-syntax-layer/gr-syntax-layer.ts         | 13 ++++++++++++-
- 1 file changed, 12 insertions(+), 1 deletion(-)
-
-diff --git a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
-index 2762ccc625..598e14589f 100644
---- a/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
-+++ b/polygerrit-ui/app/elements/diff/gr-syntax-layer/gr-syntax-layer.ts
-@@ -103,6 +103,10 @@ const LANGUAGE_MAP = new Map<string, string>([
- ]);
- const ASYNC_DELAY = 10;
- 
-+const FILENAME_OVERRIDES = new Map<string, string>([
-+  ['rules.pl', 'prolog'],
-+]);
-+
- const CLASS_SAFELIST = new Set<string>([
-   'gr-diff gr-syntax gr-syntax-attr',
-   'gr-diff gr-syntax gr-syntax-attribute',
-@@ -241,10 +245,17 @@ export class GrSyntaxLayer implements DiffLayer {
-     }
-   }
- 
-+  _basename(filename: string): string {
-+    const pieces = filename.split(/\//);
-+    return pieces[pieces.length-1];
-+  }
-+
-   _getLanguage(metaInfo: DiffFileMetaInfo) {
-     // The Gerrit API provides only content-type, but for other users of
-     // gr-diff it may be more convenient to specify the language directly.
--    return metaInfo.language ?? LANGUAGE_MAP.get(metaInfo.content_type);
-+    return metaInfo.language ??
-+        FILENAME_OVERRIDES.get(this._basename(metaInfo.name)) ??
-+        LANGUAGE_MAP.get(metaInfo.content_type);
-   }
- 
-   /**
--- 
-2.32.0
-
diff --git a/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch b/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
deleted file mode 100644
index b664ea0ea6..0000000000
--- a/third_party/gerrit/0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
+++ /dev/null
@@ -1,26 +0,0 @@
-From bd7db44cabb6de64f03adbaf5e24c73e022a8932 Mon Sep 17 00:00:00 2001
-From: Luke Granger-Brown <git@lukegb.com>
-Date: Sat, 11 Jul 2020 00:45:57 +0000
-Subject: [PATCH 5/7] When using local fonts, always assume Gerrit is mounted
- at the root.
-
----
- polygerrit-ui/app/rollup.config.js | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/polygerrit-ui/app/rollup.config.js b/polygerrit-ui/app/rollup.config.js
-index d93b5eab39..c862c9bbae 100644
---- a/polygerrit-ui/app/rollup.config.js
-+++ b/polygerrit-ui/app/rollup.config.js
-@@ -50,7 +50,7 @@ const importLocalFontMetaUrlResolver = function() {
-     name: 'import-meta-url-resolver',
-     resolveImportMeta: function (property, data) {
-       if(property === 'url' && data.moduleId.endsWith('/@polymer/font-roboto-local/roboto.js')) {
--        return 'new URL("..", document.baseURI).href';
-+        return 'new URL("/", document.baseURI).href';
-       }
-       return null;
-     }
--- 
-2.32.0
-
diff --git a/third_party/gerrit/0006-Always-use-Google-Fonts.patch b/third_party/gerrit/0006-Always-use-Google-Fonts.patch
deleted file mode 100644
index 5b817d0b55..0000000000
--- a/third_party/gerrit/0006-Always-use-Google-Fonts.patch
+++ /dev/null
@@ -1,28 +0,0 @@
-From d71f51afe12a280b92831070a583b15c8b6bc2f4 Mon Sep 17 00:00:00 2001
-From: Luke Granger-Brown <git@lukegb.com>
-Date: Sat, 11 Jul 2020 00:46:13 +0000
-Subject: [PATCH 6/7] Always use Google Fonts.
-
-We're not a corporate, and we're not behind the GFW. Always use Google Fonts,
-because even though we no longer get the caching benefits (boo, browsers),
-it is still a better geographically-distributed CDN.
----
- java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java | 2 +-
- 1 file changed, 1 insertion(+), 1 deletion(-)
-
-diff --git a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
-index a9cfceb3b6..9c287c6e45 100644
---- a/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
-+++ b/java/com/google/gerrit/httpd/raw/IndexHtmlUtil.java
-@@ -184,7 +184,7 @@ public class IndexHtmlUtil {
-     if (urlParameterMap.containsKey("ce")) {
-       data.put("polyfillCE", "true");
-     }
--    if (urlParameterMap.containsKey("gf")) {
-+    if (/* urlParameterMap.containsKey("gf") || */ true) {
-       data.put("useGoogleFonts", "true");
-     }
- 
--- 
-2.32.0
-
diff --git a/third_party/gerrit/default.nix b/third_party/gerrit/default.nix
index 4873cf09b9..a137946264 100644
--- a/third_party/gerrit/default.nix
+++ b/third_party/gerrit/default.nix
@@ -1,10 +1,6 @@
 { depot, pkgs, ... }:
 
 let
-  detzip = depot.nix.buildGo.program {
-    name = "detzip";
-    srcs = [ ./detzip.go ];
-  };
   bazelRunScript = pkgs.writeShellScriptBin "bazel-run" ''
     yarn config set cache-folder "$bazelOut/external/yarn_cache"
     export HOME="$bazelOut/external/home"
@@ -14,11 +10,10 @@ let
   bazelTop = pkgs.buildFHSUserEnv {
     name = "bazel";
     targetPkgs = pkgs: [
-      (pkgs.bazel.override { enableNixHacks = true; })
-      detzip
-      pkgs.jdk11_headless
+      (pkgs.bazel_5.override { enableNixHacks = true; })
+      pkgs.jdk17_headless
       pkgs.zlib
-      pkgs.python
+      pkgs.python3
       pkgs.curl
       pkgs.nodejs
       pkgs.yarn
@@ -28,7 +23,7 @@ let
     runScript = "/bin/bazel-run";
   };
   bazel = bazelTop // { override = x: bazelTop; };
-  version = "3.4.0";
+  version = "3.9.1";
 in
 pkgs.lib.makeOverridable pkgs.buildBazelPackage {
   pname = "gerrit";
@@ -36,33 +31,31 @@ pkgs.lib.makeOverridable pkgs.buildBazelPackage {
 
   src = pkgs.fetchgit {
     url = "https://gerrit.googlesource.com/gerrit";
-    rev = "471c1c15a7bc294d10e246df43812942b5ac8a13";
+    rev = "620a819cbf3c64fff7a66798822775ad42c91d8e";
     branchName = "v${version}";
-    sha256 = "sha256:0ayj0bcsxjln8qydkj9j7yiqibmjgd3bcpqvgsdzdx072wzx01c0";
+    sha256 = "sha256:1mdxbgnx3mpxand4wq96ic38bb4yh45q271h40jrk7dk23sgmz02";
     fetchSubmodules = true;
   };
 
   patches = [
-    ./0001-Use-detzip-in-download_bower.py.patch
-    ./0002-Syntax-highlight-nix.patch
-    ./0003-Syntax-highlight-rules.pl.patch
-    ./0004-Add-titles-to-CLs-over-HTTP.patch
-    ./0005-When-using-local-fonts-always-assume-Gerrit-is-mount.patch
-    ./0006-Always-use-Google-Fonts.patch
+    ./0001-Syntax-highlight-nix.patch
+    ./0002-Syntax-highlight-rules.pl.patch
+    ./0003-Add-titles-to-CLs-over-HTTP.patch
   ];
 
-  bazelTarget = "release api-skip-javadoc";
+  bazelTargets = [ "release" "api-skip-javadoc" ];
   inherit bazel;
 
   bazelFlags = [
     "--repository_cache="
     "--disk_cache="
   ];
+
   removeRulesCC = false;
   fetchConfigured = true;
 
   fetchAttrs = {
-    sha256 = "sha256:1q4sclf18zzh8hsnccg1y7vqnhgavq62mqp4xx50zxfcnixfkpbx";
+    sha256 = "sha256:119mqli75c9fy05ddrlh2brjxb354yvv1ijjkk1y1yqcaqppwwb8";
     preBuild = ''
       rm .bazelversion
     '';
@@ -87,7 +80,7 @@ pkgs.lib.makeOverridable pkgs.buildBazelPackage {
       # Removing top-level symlinks along with their markers.
       # This is needed because they sometimes point to temporary paths (?).
       # For example, in Tensorflow-gpu build:
-      # platforms -> NIX_BUILD_TOP/tmp/install/35282f5123611afa742331368e9ae529/_embedded_binaries/platforms
+      #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"
@@ -103,13 +96,17 @@ pkgs.lib.makeOverridable pkgs.buildBazelPackage {
       echo '${bazel.name}' > $bazelOut/external/.nix-bazel-version
 
       # Gerrit fixups:
-      # Remove polymer-bridges and ba-linkify, they're in-repo
-      rm -rf $bazelOut/external/yarn_cache/v6/npm-polymer-bridges-*
-      rm -rf $bazelOut/external/yarn_cache/v6/npm-ba-linkify-*
       # Normalize permissions on .yarn-{tarball,metadata} files
-      find $bazelOut/external/yarn_cache \( -name .yarn-tarball.tgz -or -name .yarn-metadata.json \) -exec chmod 644 {} +
+      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/)
+      (cd $bazelOut/ && tar czf $out --sort=name --mtime='@1' --owner=0 --group=0 --numeric-owner external/ _bits/)
 
       runHook postInstall
     '';
@@ -118,6 +115,15 @@ pkgs.lib.makeOverridable pkgs.buildBazelPackage {
   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/
@@ -147,4 +153,6 @@ pkgs.lib.makeOverridable pkgs.buildBazelPackage {
       "webhooks"
     ];
   };
+
+  meta.ci.targets = [ "deps" ];
 }
diff --git a/third_party/gerrit_plugins/builder.nix b/third_party/gerrit_plugins/builder.nix
index 0b6501801c..50a4e78ae7 100644
--- a/third_party/gerrit_plugins/builder.nix
+++ b/third_party/gerrit_plugins/builder.nix
@@ -1,4 +1,4 @@
-{ depot, pkgs, ... }:
+{ depot, lib, pkgs, ... }:
 {
   buildGerritBazelPlugin =
     { name
@@ -8,7 +8,7 @@
         cp -R "${src}" "$out/plugins/${name}"
       ''
     , postPatch ? ""
-    ,
+    , patches ? [ ]
     }: ((depot.third_party.gerrit.override {
       name = "${name}.jar";
 
@@ -18,7 +18,7 @@
         ${overlayPluginCmd}
       '';
 
-      bazelTarget = "//plugins/${name}";
+      bazelTargets = [ "//plugins/${name}" ];
     }).overrideAttrs (super: {
       deps = super.deps.overrideAttrs (superDeps: {
         outputHash = depsOutputHash;
@@ -26,10 +26,14 @@
       installPhase = ''
         cp "bazel-bin/plugins/${name}/${name}.jar" "$out"
       '';
-      postPatch =
-        if super ? postPatch then ''
-          ${super.postPatch}
-          ${postPatch}
-        '' else postPatch;
+      postPatch = ''
+        ${super.postPatch or ""}
+        pushd "plugins/${name}"
+        ${lib.concatMapStringsSep "\n" (patch: ''
+          patch -p1 < ${patch}
+        '') patches}
+        popd
+        ${postPatch}
+      '';
     }));
 }
diff --git a/third_party/gerrit_plugins/code-owners/default.nix b/third_party/gerrit_plugins/code-owners/default.nix
new file mode 100644
index 0000000000..0dc3ef83ae
--- /dev/null
+++ b/third_party/gerrit_plugins/code-owners/default.nix
@@ -0,0 +1,17 @@
+{ depot, pkgs, ... }@args:
+
+let
+  inherit (import ../builder.nix args) buildGerritBazelPlugin;
+in
+buildGerritBazelPlugin rec {
+  name = "code-owners";
+  depsOutputHash = "sha256:0jv62cc1kpgsmwk119i9njmqn6w6k8frlbgcw87y8nfbpprmcf01";
+  src = pkgs.fetchgit {
+    url = "https://gerrit.googlesource.com/plugins/code-owners";
+    rev = "e654ae5bda2085bce9a99942bec440e004a114f3";
+    sha256 = "sha256:14d3x3iqskgw16pvyaa0swh252agj84p9pzlf24l8lgx9d7y4biz";
+  };
+  patches = [
+    ./using-usernames.patch
+  ];
+}
diff --git a/third_party/gerrit_plugins/code-owners/using-usernames.patch b/third_party/gerrit_plugins/code-owners/using-usernames.patch
new file mode 100644
index 0000000000..25079ae136
--- /dev/null
+++ b/third_party/gerrit_plugins/code-owners/using-usernames.patch
@@ -0,0 +1,472 @@
+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(
diff --git a/third_party/gerrit_plugins/default.nix b/third_party/gerrit_plugins/default.nix
deleted file mode 100644
index c08afb0f77..0000000000
--- a/third_party/gerrit_plugins/default.nix
+++ /dev/null
@@ -1,23 +0,0 @@
-{ depot, pkgs, ... }@args:
-
-let
-  inherit (import ./builder.nix args) buildGerritBazelPlugin;
-in
-depot.nix.readTree.drvTargets {
-  # https://gerrit.googlesource.com/plugins/owners
-  owners = buildGerritBazelPlugin rec {
-    name = "owners";
-    depsOutputHash = "sha256:00nbqwr83wsqa6l67bv4ywv9795l1ibl0yv1kq5q811syrvk2xiz";
-    src = pkgs.fetchgit {
-      url = "https://gerrit.googlesource.com/plugins/owners";
-      rev = "99a9ab585532d172d141b4641dfc70081513dfc2";
-      sha256 = "sha256:1xn9qb7q94jxfx7yq0zjqjm16gfyzzif13sak9x6j4f9r68frcd4";
-    };
-    overlayPluginCmd = ''
-      chmod +w "$out" "$out/plugins/external_plugin_deps.bzl"
-      cp -R "${src}/owners" "$out/plugins/owners"
-      cp "${src}/external_plugin_deps.bzl" "$out/plugins/external_plugin_deps.bzl"
-      cp -R "${src}/owners-common" "$out/owners-common"
-    '';
-  };
-}
diff --git a/third_party/gerrit_plugins/oauth/cas-6x.patch b/third_party/gerrit_plugins/oauth/cas-6x.patch
deleted file mode 100644
index 7494298b3f..0000000000
--- a/third_party/gerrit_plugins/oauth/cas-6x.patch
+++ /dev/null
@@ -1,69 +0,0 @@
-diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
-index 450549f..27310cd 100644
---- a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
-+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasApi.java
-@@ -15,7 +15,7 @@
- package com.googlesource.gerrit.plugins.oauth;
- 
- import com.github.scribejava.core.builder.api.DefaultApi20;
--import com.github.scribejava.core.extractors.OAuth2AccessTokenExtractor;
-+import com.github.scribejava.core.extractors.OAuth2AccessTokenJsonExtractor;
- import com.github.scribejava.core.extractors.TokenExtractor;
- import com.github.scribejava.core.model.OAuth2AccessToken;
- import com.github.scribejava.core.oauth2.bearersignature.BearerSignature;
-@@ -47,6 +47,6 @@ public class CasApi extends DefaultApi20 {
- 
-   @Override
-   public TokenExtractor<OAuth2AccessToken> getAccessTokenExtractor() {
--    return OAuth2AccessTokenExtractor.instance();
-+    return OAuth2AccessTokenJsonExtractor.instance();
-   }
- }
-diff --git a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
-index 5f3e4a1..fc5bc50 100644
---- a/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
-+++ b/src/main/java/com/googlesource/gerrit/plugins/oauth/CasOAuthService.java
-@@ -106,36 +106,14 @@ class CasOAuthService implements OAuthServiceProvider {
-         throw new IOException(String.format("CAS response missing id: %s", response.getBody()));
-       }
- 
--      JsonElement attrListJson = jsonObject.get("attributes");
--      if (attrListJson == null) {
--        throw new IOException(
--            String.format("CAS response missing attributes: %s", response.getBody()));
--      }
--
-       String email = null, name = null, login = null;
--      if (attrListJson.isJsonArray()) {
--        // It is possible for CAS to be configured to not return any attributes (email, name,
--        // login),
--        // in which case,
--        // CAS returns an empty JSON object "attributes":{}, rather than "null" or an empty JSON
--        // array
--        // "attributes": []
--
--        JsonArray attrJson = attrListJson.getAsJsonArray();
--        for (JsonElement elem : attrJson) {
--          if (elem == null || !elem.isJsonObject()) {
--            throw new IOException(String.format("Invalid JSON '%s': not a JSON Object", elem));
--          }
--          JsonObject obj = elem.getAsJsonObject();
--
--          String property = getStringElement(obj, "email");
--          if (property != null) email = property;
--          property = getStringElement(obj, "name");
--          if (property != null) name = property;
--          property = getStringElement(obj, "login");
--          if (property != null) login = property;
--        }
--      }
-+
-+      String property = getStringElement(jsonObject, "mail");
-+      if (property != null) email = property;
-+      property = getStringElement(jsonObject, "displayName");
-+      if (property != null) name = property;
-+      property = getStringElement(jsonObject, "uid");
-+      if (property != null) login = property;
- 
-       return new OAuthUserInfo(
-           CAS_PROVIDER_PREFIX + id.getAsString(),
diff --git a/third_party/gerrit_plugins/oauth/default.nix b/third_party/gerrit_plugins/oauth/default.nix
index 01748ba842..e7626fa88c 100644
--- a/third_party/gerrit_plugins/oauth/default.nix
+++ b/third_party/gerrit_plugins/oauth/default.nix
@@ -5,23 +5,15 @@ let
 in
 buildGerritBazelPlugin rec {
   name = "oauth";
-  depsOutputHash = "sha256:0j86amkw54y177s522hc988hqg034fsrkywbsb9a7h14zwcqbran";
+  depsOutputHash = "sha256:01z7rn8hnms3cp7mq27yk063lpy4pmqwpfrcc3cfl0r43k889zz3";
   src = pkgs.fetchgit {
     url = "https://gerrit.googlesource.com/plugins/oauth";
-    rev = "4aa7322db5ec221b2419e12a9ec7af5b8c66659c";
-    sha256 = "1szra3pjl0axf4a7k96flpk7rhfvp37rdxay4gbglh939gzbba88";
+    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"
   '';
-
-  # The code in the OAuth repo expects CAS to return oauth2 access tokens as urlencoded.
-  # Our version of CAS returns them as JSON instead.
-  postPatch = ''
-    pushd plugins/oauth
-    patch -p1 <${./cas-6x.patch}
-    popd
-  '';
 }
diff --git a/third_party/hii/OWNERS b/third_party/hii/OWNERS
index a742d0d22b..a640227914 100644
--- a/third_party/hii/OWNERS
+++ b/third_party/hii/OWNERS
@@ -1,3 +1 @@
-inherited: true
-owners:
-  - Profpatsch
+Profpatsch
diff --git a/third_party/irccat/default.nix b/third_party/irccat/default.nix
index 3181fd2067..c5d7a5f6df 100644
--- a/third_party/irccat/default.nix
+++ b/third_party/irccat/default.nix
@@ -5,7 +5,7 @@ pkgs.buildGoModule rec {
   pname = "irccat";
   version = "20201108";
   meta.license = lib.licenses.gpl3;
-  vendorSha256 = "06a985y4alw1rsghgmhfyczns6klz7bbkfn5mnqc9fdfclgg4s3r";
+  vendorHash = "sha256:06a985y4alw1rsghgmhfyczns6klz7bbkfn5mnqc9fdfclgg4s3r";
 
   src = pkgs.fetchFromGitHub {
     owner = "irccloud";
diff --git a/third_party/josh/0001-josh-proxy-Always-require-authentication-when-pushin.patch b/third_party/josh/0001-josh-proxy-Always-require-authentication-when-pushin.patch
deleted file mode 100644
index d3a2c0e998..0000000000
--- a/third_party/josh/0001-josh-proxy-Always-require-authentication-when-pushin.patch
+++ /dev/null
@@ -1,43 +0,0 @@
-From a82ccf1fab187969544b638f6977d698a55dbb2f Mon Sep 17 00:00:00 2001
-From: Vincent Ambo <mail@tazj.in>
-Date: Fri, 11 Feb 2022 13:14:02 +0300
-Subject: [PATCH] josh-proxy: Always require authentication when pushing
-
-This supports the use-case where josh serves a public repo without
-auth, but requires auth for pushing back.
----
- josh-proxy/src/auth.rs           | 4 ++--
- josh-proxy/src/bin/josh-proxy.rs | 2 +-
- 2 files changed, 3 insertions(+), 3 deletions(-)
-
-diff --git a/josh-proxy/src/auth.rs b/josh-proxy/src/auth.rs
-index 96a8241..0a007f3 100644
---- a/josh-proxy/src/auth.rs
-+++ b/josh-proxy/src/auth.rs
-@@ -54,8 +54,8 @@ impl Handle {
-     }
- }
- 
--pub async fn check_auth(url: &str, auth: &Handle, required: bool) -> josh::JoshResult<bool> {
--    if required && auth.hash.is_empty() {
-+pub async fn check_auth(url: &str, pathinfo: &str, auth: &Handle, required: bool) -> josh::JoshResult<bool> {
-+    if auth.hash.is_empty() && (required || pathinfo == "/git-receive-pack") {
-         return Ok(false);
-     }
- 
-diff --git a/josh-proxy/src/bin/josh-proxy.rs b/josh-proxy/src/bin/josh-proxy.rs
-index 700f2da..a96da1c 100644
---- a/josh-proxy/src/bin/josh-proxy.rs
-+++ b/josh-proxy/src/bin/josh-proxy.rs
-@@ -449,7 +449,7 @@ async fn call_service(
-     ]
-     .join("");
- 
--    if !josh_proxy::auth::check_auth(&remote_url, &auth, ARGS.is_present("require-auth"))
-+    if !josh_proxy::auth::check_auth(&remote_url, &parsed_url.pathinfo, &auth, ARGS.is_present("require-auth"))
-         .in_current_span()
-         .await?
-     {
--- 
-2.34.1
-
diff --git a/third_party/josh/default.nix b/third_party/josh/default.nix
deleted file mode 100644
index c82f91f80c..0000000000
--- a/third_party/josh/default.nix
+++ /dev/null
@@ -1,38 +0,0 @@
-# https://github.com/esrlabs/josh
-{ depot, pkgs, ... }:
-
-let
-  src = pkgs.fetchFromGitHub {
-    owner = "esrlabs";
-    repo = "josh";
-    rev = "effe6290559136faba5591a115e56c2b30210329";
-    hash = "sha256:0kam9rqjk96brvh15wj3h3vm2sqnr5pckz91az2ida5617d5gp9v";
-  };
-in
-depot.third_party.naersk.buildPackage {
-  inherit src;
-
-  buildInputs = with pkgs; [
-    libgit2
-    openssl
-    pkgconfig
-  ];
-
-  cargoBuildOptions = x: x ++ [
-    "-p"
-    "josh"
-    "-p"
-    "josh-proxy"
-    "-p"
-    "josh-ui"
-  ];
-
-  overrideMain = x: {
-    patches = [ ./0001-josh-proxy-Always-require-authentication-when-pushin.patch ];
-
-    nativeBuildInputs = (x.nativeBuildInputs or [ ]) ++ [ pkgs.makeWrapper ];
-    postInstall = ''
-      wrapProgram $out/bin/josh-proxy --prefix PATH : "${pkgs.git}/bin"
-    '';
-  };
-}
diff --git a/third_party/lisp/OWNERS b/third_party/lisp/OWNERS
index 2d7f7e237b..6536baf505 100644
--- a/third_party/lisp/OWNERS
+++ b/third_party/lisp/OWNERS
@@ -1,5 +1,2 @@
-# -*- mode: yaml; -*-
-inherited: true
-owners:
-  - eta
-  - grfn
+eta
+aspen
diff --git a/third_party/lisp/cl-change-case.nix b/third_party/lisp/cl-change-case.nix
new file mode 100644
index 0000000000..b66368a9b6
--- /dev/null
+++ b/third_party/lisp/cl-change-case.nix
@@ -0,0 +1,22 @@
+{ depot, pkgs, ... }:
+
+let src = with pkgs; srcOnly lispPackages.cl-change-case;
+in depot.nix.buildLisp.library {
+  name = "cl-change-case";
+
+  deps = with depot.third_party.lisp; [ cl-ppcre cl-ppcre.unicode ];
+
+  srcs = [ (src + "/src/cl-change-case.lisp") ];
+
+  tests = {
+    name = "cl-change-case-tests";
+    srcs = [ (src + "/t/cl-change-case.lisp") ];
+    deps = [
+      depot.third_party.lisp.fiveam
+    ];
+
+    expression = ''
+      (5am:run! :cl-change-case)
+    '';
+  };
+}
diff --git a/third_party/lisp/cl-json.nix b/third_party/lisp/cl-json.nix
index 8dbb1567ac..6b82fac772 100644
--- a/third_party/lisp/cl-json.nix
+++ b/third_party/lisp/cl-json.nix
@@ -8,8 +8,8 @@ let
   src = pkgs.fetchFromGitHub {
     owner = "sternenseemann";
     repo = "cl-json";
-    rev = "479685029c511cb2011f2f2a99ca6c63aa2e4865";
-    sha256 = "1663xlzb0wj6kd0wy2cmhafrwip7vy0wlfckc519aj9j18aak5ja";
+    rev = "c059bec94e28a11102a994d6949e2e52764f21fd";
+    sha256 = "0l07syw1b1x2zi8kj4iph3rf6vi6c16b7fk69iv7x27wrdsr1qwj";
   };
 
   getSrcs = subdir: map (f: src + ("/" + subdir + "/" + f));
diff --git a/third_party/lisp/cl-ppcre.nix b/third_party/lisp/cl-ppcre.nix
index 561e306191..7cb99db639 100644
--- a/third_party/lisp/cl-ppcre.nix
+++ b/third_party/lisp/cl-ppcre.nix
@@ -24,4 +24,16 @@ in depot.nix.buildLisp.library {
     "scanner.lisp"
     "api.lisp"
   ];
+
+  passthru = {
+    unicode = depot.nix.buildLisp.library {
+      name = "cl-ppcre-unicode";
+      deps = with depot.third_party.lisp; [ cl-ppcre cl-unicode ];
+
+      srcs = map (f: src + ("/cl-ppcre-unicode/" + f)) [
+        "packages.lisp"
+        "resolver.lisp"
+      ];
+    };
+  };
 }
diff --git a/third_party/lisp/lisp-binary.nix b/third_party/lisp/lisp-binary.nix
index 8deba4546f..296112cc9e 100644
--- a/third_party/lisp/lisp-binary.nix
+++ b/third_party/lisp/lisp-binary.nix
@@ -2,22 +2,18 @@
 { depot, pkgs, ... }:
 
 let
-  src = pkgs.fetchFromGitHub {
-    owner = "j3pic";
-    repo = "lisp-binary";
-    rev = "052df578900dea59bf951e0a6749281fa73432e4";
-    sha256 = "1i1s5g01aimfq6lndcl1pnw7ly5hdh0wmjp2dj9cjjwbkz9lnwcf";
-  };
+  src = pkgs.srcOnly pkgs.lispPackages.lisp-binary;
 in
 depot.nix.buildLisp.library {
   name = "lisp-binary";
 
   deps = with depot.third_party.lisp; [
+    alexandria
     cffi
-    quasiquote_2
-    moptilities
-    flexi-streams
     closer-mop
+    flexi-streams
+    moptilities
+    quasiquote_2
   ];
 
   srcs = map (f: src + ("/" + f)) [
@@ -32,6 +28,6 @@ depot.nix.buildLisp.library {
   ];
 
   brokenOn = [
-    "ecl" # dynamic cffi
+    "ecl" # TODO(sterni): disable conditionally cffi for ECL
   ];
 }
diff --git a/third_party/lisp/mime4cl/OWNERS b/third_party/lisp/mime4cl/OWNERS
index f16dd105d7..2e95807063 100644
--- a/third_party/lisp/mime4cl/OWNERS
+++ b/third_party/lisp/mime4cl/OWNERS
@@ -1,3 +1 @@
-inherited: true
-owners:
-  - sterni
+sterni
diff --git a/third_party/lisp/mime4cl/README b/third_party/lisp/mime4cl/README
deleted file mode 100644
index 73f0efbda9..0000000000
--- a/third_party/lisp/mime4cl/README
+++ /dev/null
@@ -1,7 +0,0 @@
-MIME4CL is a Common Lisp library for dealing with MIME messages.
-It has originally been written by Walter C. Pelissero and vendored
-into depot as upstream has become inactive and provides no repo
-of any kind. Upstream and depot version may diverge.
-
-Upstream Website: http://wcp.sdf-eu.org/software/#mime4cl
-Vendored Tarball: http://wcp.sdf-eu.org/software/mime4cl-20150207T211851.tbz
diff --git a/third_party/lisp/mime4cl/README.md b/third_party/lisp/mime4cl/README.md
new file mode 100644
index 0000000000..2704d481ed
--- /dev/null
+++ b/third_party/lisp/mime4cl/README.md
@@ -0,0 +1,27 @@
+# mime4cl
+
+`MIME4CL` is a Common Lisp library for dealing with MIME messages. It was
+originally been written by Walter C. Pelissero and vendored into depot
+([mime4cl-20150207T211851.tbz](http://wcp.sdf-eu.org/software/mime4cl-20150207T211851.tbz)
+to be exact) as upstream has become inactive. Its [original
+website](http://wcp.sdf-eu.org/software/#mime4cl) can still be accessed.
+
+The depot version has since diverged from upstream. Main aims were to improve
+performance and reduce code size by relying on third party libraries like
+flexi-streams. It is planned to improve encoding handling in the long term.
+Currently, the library is being worked on intermittently and not very well
+testedโ€”**it may not work as expected**.
+
+## Differences from the original version
+
+* `//nix/buildLisp` is used as the build system. ASDF is currently untested and
+  may be broken.
+
+* The dependency on [sclf](http://wcp.sdf-eu.org/software/#sclf) has been
+  eliminated by inlining the relevant parts.
+
+* `MY-STRING-INPUT-STREAM`, `DELIMITED-INPUT-STREAM`,
+  `CHARACTER-INPUT-ADAPTER-STREAM`, `BINARY-INPUT-ADAPTER-STREAM` etc. have been
+  replaced by (thin wrappers around) flexi-streams. In addition to improved
+  handling of encodings, this allows using `READ-SEQUENCE` via the gray stream
+  interface.
diff --git a/third_party/lisp/mime4cl/address.lisp b/third_party/lisp/mime4cl/address.lisp
index 944156916c..42688a595b 100644
--- a/third_party/lisp/mime4cl/address.lisp
+++ b/third_party/lisp/mime4cl/address.lisp
@@ -1,7 +1,7 @@
 ;;;  address.lisp --- e-mail address parser
 
 ;;;  Copyright (C) 2007, 2008, 2009 by Walter C. Pelissero
-;;;  Copyright (C) 2022 The TVL Authors
+;;;  Copyright (C) 2022-2023 The TVL Authors
 
 ;;;  Author: Walter C. Pelissero <walter@pelissero.de>
 ;;;  Project: mime4cl
@@ -219,14 +219,14 @@
   (not (find c " ()\"[]@.<>:;,")))
 
 (defun read-atext (first-character cursor)
-  (be string (with-output-to-string (out)
-               (write-char first-character out)
-               (loop
-                  for c = (read-char (cursor-stream cursor) nil)
-                  while (and c (atom-component-p c))
-                  do (write-char c out)
-                  finally (when c
-                            (unread-char c (cursor-stream cursor)))))
+  (let ((string (with-output-to-string (out)
+                  (write-char first-character out)
+                  (loop
+                    for c = (read-char (cursor-stream cursor) nil)
+                    while (and c (atom-component-p c))
+                    do (write-char c out)
+                    finally (when c
+                              (unread-char c (cursor-stream cursor)))))))
     (make-token :type 'atext
                 :value string
                 :position (incf (cursor-position cursor)))))
@@ -236,7 +236,7 @@
            (make-token :type 'keyword
                        :value (string c)
                        :position (incf (cursor-position cursor)))))
-    (be in (cursor-stream cursor)
+    (let ((in (cursor-stream cursor)))
       (loop
          for c = (read-char in nil)
          while c
@@ -259,7 +259,7 @@
   "Return the list of tokens produced by a lexical analysis of
 STRING.  These are the tokens that would be seen by the parser."
   (with-input-from-string (stream string)
-    (be cursor (make-cursor :stream stream)
+    (let ((cursor (make-cursor :stream stream)))
       (loop
          for tokens = (read-next-tokens cursor)
          until (endp tokens)
@@ -282,19 +282,19 @@ addresses only."
 MAILBOX-GROUPs.  If STRING is unparsable return NIL.  If
 NO-GROUPS is true, return a flat list of mailboxes throwing away
 the group containers, if any."
-  (be grammar (force define-grammar)
+  (let ((grammar (force define-grammar)))
     (with-input-from-string (stream string)
-      (be* cursor (make-cursor :stream stream)
-           mailboxes (ignore-errors	; ignore parsing errors
-                       (parse grammar 'address-list cursor))
+      (let* ((cursor (make-cursor :stream stream))
+             (mailboxes (ignore-errors  ; ignore parsing errors
+                         (parse grammar 'address-list cursor))))
         (if no-groups
             (mailboxes-only mailboxes)
             mailboxes)))))
 
 (defun debug-addresses (string)
   "More or less like PARSE-ADDRESSES, but don't ignore parsing errors."
-  (be grammar (force define-grammar)
+  (let ((grammar (force define-grammar)))
     (with-input-from-string (stream string)
-      (be cursor (make-cursor :stream stream)
+      (let ((cursor (make-cursor :stream stream)))
         (parse grammar 'address-list cursor)))))
 
diff --git a/third_party/lisp/mime4cl/default.nix b/third_party/lisp/mime4cl/default.nix
index 349ef397f7..af015a257b 100644
--- a/third_party/lisp/mime4cl/default.nix
+++ b/third_party/lisp/mime4cl/default.nix
@@ -6,9 +6,11 @@ depot.nix.buildLisp.library {
   name = "mime4cl";
 
   deps = [
-    depot.third_party.lisp.babel
+    depot.third_party.lisp.flexi-streams
     depot.third_party.lisp.npg
     depot.third_party.lisp.trivial-gray-streams
+    depot.third_party.lisp.qbase64
+    { sbcl = depot.nix.buildLisp.bundled "sb-posix"; }
   ];
 
   srcs = [
@@ -29,10 +31,8 @@ depot.nix.buildLisp.library {
       (pkgs.writeText "nix-samples.lisp" ''
         (in-package :mime4cl-tests)
 
-        ;; missing from the tarball completely
-        (defvar *samples-directory* (pathname "/this/does/not/exist"))
-        ;; override auto discovery which doesn't work in store
-        (defvar *sample1-file* (pathname "${./test/sample1.msg}"))
+        ;; override auto discovery which doesn't work in the nix store
+        (defvar *samples-directory* (pathname "${./test/samples}/"))
       '')
       ./test/temp-file.lisp
       ./test/endec.lisp
diff --git a/third_party/lisp/mime4cl/endec.lisp b/third_party/lisp/mime4cl/endec.lisp
index 020c212e5e..2e282c2378 100644
--- a/third_party/lisp/mime4cl/endec.lisp
+++ b/third_party/lisp/mime4cl/endec.lisp
@@ -1,6 +1,7 @@
 ;;;  endec.lisp --- encoder/decoder functions
 
 ;;;  Copyright (C) 2005-2008, 2010 by Walter C. Pelissero
+;;;  Copyright (C) 2023 by The TVL Authors
 
 ;;;  Author: Walter C. Pelissero <walter@pelissero.de>
 ;;;  Project: mime4cl
@@ -21,19 +22,21 @@
 
 (in-package :mime4cl)
 
+(defun redirect-stream (in out &key (buffer-size 4096))
+  "Consume input stream IN and write all its content to output stream OUT.
+The streams' element types need to match."
+  (let ((buf (make-array buffer-size :element-type (stream-element-type in))))
+    (loop for pos = (read-sequence buf in)
+          while (> pos 0)
+          do (write-sequence buf out :end pos))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 ;; Thank you SBCL for rendering constants totally useless!
 (defparameter +base64-encode-table+
   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=")
 
-(defparameter +base64-decode-table+
-  (let ((da (make-array 256 :element-type '(unsigned-byte 8) :initial-element 65)))
-    (dotimes (i 64)
-      (setf (aref da (char-code (char +base64-encode-table+ i))) i))
-    da))
-
-(declaim (type (simple-array (unsigned-byte 8)) +base64-decode-table+)
-         (type simple-string +base64-encode-table+))
+(declaim (type simple-string +base64-encode-table+))
 
 (defvar *base64-line-length* 76
   "Maximum length of the encoded base64 line.  NIL means it can
@@ -161,7 +164,7 @@ It should expect a character as its only argument."))
        for byte = (decoder-read-byte decoder)
        unless byte
        do (return-from decoder-read-line nil)
-       do (be c (code-char byte)
+       do (let ((c (code-char byte)))
             (cond ((char= c #\return)
                    ;; skip the newline
                    (decoder-read-byte decoder)
@@ -198,7 +201,7 @@ value."
              (save (c)
                (saveb (char-code c)))
              (push-next ()
-               (be c (funcall input-function)
+               (let ((c (funcall input-function)))
                  (declare (type (or null character) c))
                  (cond ((not c))
                        ((or (char= c #\space)
@@ -206,7 +209,7 @@ value."
                         (save c)
                         (push-next))
                        ((char= c #\=)
-                        (be c1 (funcall input-function)
+                        (let ((c1 (funcall input-function)))
                           (cond ((not c1)
                                  (save #\=))
                                 ((char= c1 #\return)
@@ -221,7 +224,7 @@ value."
                                  (push-next))
                                 (t
                                  ;; hexadecimal sequence: get the 2nd digit
-                                 (be c2 (funcall input-function)
+                                 (let ((c2 (funcall input-function)))
                                    (if c2
                                        (aif (parse-hex c1 c2)
                                             (saveb it)
@@ -271,10 +274,10 @@ binary output OUT the decoded stream of bytes."
 (defmacro make-stream-to-sequence-decoder (decoder-class input-form &key parser-errors)
   "Decode the character stream STREAM and return a sequence of bytes."
   (with-gensyms (output-sequence)
-    `(be ,output-sequence (make-array 0
-                                      :element-type '(unsigned-byte 8)
-                                      :fill-pointer 0
-                                      :adjustable t)
+    `(let ((,output-sequence (make-array 0
+                                         :element-type '(unsigned-byte 8)
+                                         :fill-pointer 0
+                                         :adjustable t)))
        (make-decoder-loop ,decoder-class ,input-form
                           (vector-push-extend byte ,output-sequence)
                           :parser-errors ,parser-errors)
@@ -377,7 +380,7 @@ characters quoted printables encoded."
 (defun encode-quoted-printable-sequence-to-stream (sequence stream &key (start 0) (end (length sequence)))
   "Encode the sequence of bytes SEQUENCE and write to STREAM a
 quoted printable sequence of characters."
-  (be i start
+  (let ((i start))
     (make-encoder-loop quoted-printable-encoder
      (when (< i end)
        (prog1 (elt sequence i)
@@ -470,7 +473,7 @@ character stream."
 (defun encode-base64-sequence-to-stream (sequence stream &key (start 0) (end (length sequence)))
   "Encode the sequence of bytes SEQUENCE and write to STREAM the
 Base64 character sequence."
-  (be i start
+  (let ((i start))
     (make-encoder-loop base64-encoder
                        (when (< i end)
                          (prog1 (elt sequence i)
@@ -483,60 +486,34 @@ return it."
   (with-output-to-string (out)
     (encode-base64-sequence-to-stream sequence out :start start :end end)))
 
-(defclass base64-decoder (parsing-decoder)
-  ((bitstore :initform 0
-             :type fixnum)
-   (bytecount :initform 0 :type fixnum))
-  (:documentation
-   "Class for Base64 decoder input streams."))
-
-(defmethod decoder-read-byte ((decoder base64-decoder))
-  (declare (optimize (speed 3) (safety 0) (debug 0)))
-  (with-slots (bitstore bytecount input-function) decoder
-    (declare (type fixnum bitstore bytecount)
-             (type function input-function))
-    (labels ((in6 ()
-               (loop
-                  for c = (funcall input-function)
-                  when (or (not c) (char= #\= c))
-                  do (return-from decoder-read-byte nil)
-                  do (be sextet (aref +base64-decode-table+ (char-code c))
-                       (unless (= sextet 65) ; ignore unrecognised characters
-                         (return sextet)))))
-             (push6 (sextet)
-               (declare (type fixnum sextet))
-               (setf bitstore
-                     (logior sextet (the fixnum (ash bitstore 6))))))
-      (case bytecount
-        (0
-         (setf bitstore (in6))
-         (push6 (in6))
-         (setf bytecount 1)
-         (ash bitstore -4))
-        (1
-         (push6 (in6))
-         (setf bytecount 2)
-         (logand #xFF (ash bitstore -2)))
-        (2
-         (push6 (in6))
-         (setf bytecount 0)
-         (logand #xFF bitstore))))))
-
 (defun decode-base64-stream (in out &key parser-errors)
   "Read from IN a stream of characters Base64 encoded and write
 to OUT a stream of decoded bytes."
-  (make-decoder-loop base64-decoder
-                     (read-byte in nil) (write-byte byte out)
-                     :parser-errors parser-errors))
+  ;; parser-errors are ignored for base64
+  (declare (ignore parser-errors))
+  (redirect-stream (make-instance 'qbase64:decode-stream
+                                  :underlying-stream in)
+                   out))
 
 (defun decode-base64-stream-to-sequence (stream &key parser-errors)
-  (make-stream-to-sequence-decoder base64-decoder
-                                   (read-char stream nil)
-                                   :parser-errors parser-errors))
-
-(defun decode-base64-string (string &key (start 0) (end (length string)) parser-errors)
-  (with-input-from-string (in string :start start :end end)
-    (decode-base64-stream-to-sequence in :parser-errors parser-errors)))
+  "Read Base64 characters from STREAM and return result of decoding them as a
+binary sequence."
+  ;; parser-errors are ignored for base64
+  (declare (ignore parser-errors))
+  (let* ((buffered-size 4096)
+         (dstream (make-instance 'qbase64:decode-stream
+                                 :underlying-stream stream))
+         (output-seq (make-array buffered-size
+                                 :element-type '(unsigned-byte 8)
+                                 :adjustable t)))
+    (loop for cap = (array-dimension output-seq 0)
+          for pos = (read-sequence output-seq dstream :start (or pos 0))
+          if (>= pos cap)
+            do (adjust-array output-seq (+ cap buffered-size))
+          else
+            do (progn
+                 (adjust-array output-seq pos)
+                 (return output-seq)))))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
@@ -547,25 +524,14 @@ to OUT a stream of decoded bytes."
      while c
      do (write-byte (char-code c) out)))
 
-(defun decode-stream (in out encoding &key parser-errors-p)
-  (gcase (encoding string-equal)
-    (:quoted-printable
-     (decode-quoted-printable-stream in out
-                                     :parser-errors parser-errors-p))
-    (:base64
-     (decode-base64-stream in out
-                           :parser-errors parser-errors-p))
-    (otherwise
-     (dump-stream-binary in out))))
-
 (defun decode-string (string encoding &key parser-errors-p)
   (gcase (encoding string-equal)
     (:quoted-printable
      (decode-quoted-printable-string string
                                      :parser-errors parser-errors-p))
     (:base64
-     (decode-base64-string string
-                           :parser-errors parser-errors-p))
+     ;; parser-errors-p is unused in base64
+     (qbase64:decode-string string))
     (otherwise
      (map '(vector (unsigned-byte 8)) #'char-code string))))
 
@@ -649,7 +615,7 @@ method of RFC2047 and return a sequence of bytes."
 bytes."
   (gcase (encoding string-equal)
     ("Q" (decode-quoted-printable-RFC2047-string string :start start :end end))
-    ("B" (decode-base64-string string :start start :end end))
+    ("B" (qbase64:decode-string (subseq string start end)))
     (t string)))
 
 (defun parse-RFC2047-text (text)
@@ -684,13 +650,13 @@ sequence, a charset string indicating the original coding."
 
 (defun decode-RFC2047 (text)
   "Decode TEXT into a fully decoded string. Whenever a non ASCII part is
-  encountered, try to decode it using babel, otherwise signal an error."
+  encountered, try to decode it using flexi-streams, otherwise signal an error."
   (flet ((decode-part (part)
            (etypecase part
-             (cons (babel:octets-to-string
+             (cons (flexi-streams:octets-to-string
                     (car part)
-                    :encoding (babel-encodings:get-character-encoding
-                               (intern (string-upcase (cdr part)) 'keyword))))
+                    :external-format (flexi-streams:make-external-format
+                                      (intern (string-upcase (cdr part)) 'keyword))))
              (string part))))
     (apply #'concatenate
            (cons 'string
diff --git a/third_party/lisp/mime4cl/ex-sclf.lisp b/third_party/lisp/mime4cl/ex-sclf.lisp
index 8a288cced8..1719732fb3 100644
--- a/third_party/lisp/mime4cl/ex-sclf.lisp
+++ b/third_party/lisp/mime4cl/ex-sclf.lisp
@@ -1,7 +1,7 @@
 ;;; ex-sclf.lisp --- subset of sclf used by mime4cl
 
 ;;;  Copyright (C) 2005-2010 by Walter C. Pelissero
-;;;  Copyright (C) 2022 The TVL Authors
+;;;  Copyright (C) 2022-2023 The TVL Authors
 
 ;;;  Author: sternenseemann <sternenseemann@systemli.org>
 ;;;  Project: mime4cl
@@ -32,10 +32,9 @@
 
 (defpackage :mime4cl-ex-sclf
   (:use :common-lisp)
-  (:export
-   #:be
-   #:be*
+  (:import-from :sb-posix :stat :stat-size)
 
+  (:export
    #:aif
    #:awhen
    #:aand
@@ -65,8 +64,6 @@
    #:save-file-excursion
    #:read-file
 
-   #:unix-file-stat
-   #:unix-stat
    #:file-size
 
    #:promise
@@ -94,38 +91,16 @@ See also LET-GENSYMS."
 
 ;; CONTROL FLOW
 
-(defmacro be (&rest bindings-and-body)
-  "Less-parenthetic let."
-  (let ((bindings
-         (loop
-            while (and (symbolp (car bindings-and-body))
-                       (cdr bindings-and-body))
-            collect (list (pop bindings-and-body)
-                          (pop bindings-and-body)))))
-    `(let ,bindings
-       ,@bindings-and-body)))
-
-(defmacro be* (&rest bindings-and-body)
-  "Less-parenthetic let*."
-  (let ((bindings
-         (loop
-            while (and (symbolp (car bindings-and-body))
-                       (cdr bindings-and-body))
-            collect (list (pop bindings-and-body)
-                          (pop bindings-and-body)))))
-    `(let* ,bindings
-       ,@bindings-and-body)))
-
 (defmacro aif (test then &optional else)
-  `(be it ,test
-       (if it
-           ,then
-           ,else)))
+  `(let ((it ,test))
+     (if it
+         ,then
+         ,else)))
 
 (defmacro awhen (test &body then)
-  `(be it ,test
-       (when it
-         ,@then)))
+  `(let ((it ,test))
+     (when it
+       ,@then)))
 
 (defmacro aand (&rest args)
   (cond ((null args) t)
@@ -136,7 +111,7 @@ See also LET-GENSYMS."
   "Generic CASE macro.  Match VALUE to CASES as if by the normal CASE
 but use TEST as the comparison function, which defaults to EQUALP."
   (with-gensyms (val)
-    `(be ,val ,value
+    `(let ((,val ,value))
        ,(cons 'cond
               (mapcar #'(lambda (case-desc)
                           (destructuring-bind (vals &rest forms) case-desc
@@ -163,10 +138,10 @@ Accept any argument accepted by the POSITION function."
   "Split SEQUENCE at occurence of any element from BAG.
 Contiguous occurences of elements from BAG are considered atomic;
 so no empty sequence is returned."
-  (be len (length sequence)
+  (let ((len (length sequence)))
     (labels ((split-from (start)
                (unless (>= start len)
-                 (be sep (position-any bag sequence :start start :key key)
+                 (let ((sep (position-any bag sequence :start start :key key)))
                    (cond ((not sep)
                           (list (subseq sequence start)))
                          ((> sep start)
@@ -198,7 +173,7 @@ SKIP-EMPTY is true then filter out the empty substrings.  If ESCAPE is
 not nil then split at SEPARATOR only if it's not preceded by ESCAPE."
   (declare (type string string) (type character separator))
   (labels ((next-separator (beg)
-             (be pos (position separator string :start beg)
+             (let ((pos (position separator string :start beg)))
                (if (and escape
                         pos
                         (plusp pos)
@@ -235,7 +210,7 @@ nothing) between them."
           list))
 
 (defun string-starts-with (prefix string &optional (compare #'string=))
-  (be prefix-length (length prefix)
+  (let ((prefix-length (length prefix)))
     (and (>= (length string) prefix-length)
          (funcall compare prefix string :end2 prefix-length))))
 
@@ -275,7 +250,7 @@ nothing) between them."
 before FORMS.  Optionally POSITION can be set to the starting offset."
   (unless position
     (setf position (gensym)))
-  `(be ,position (file-position ,stream)
+  `(let ((,position (file-position ,stream)))
      (unwind-protect (progn ,@forms)
        (file-position ,stream ,position))))
 
@@ -288,7 +263,7 @@ ELEMENT-TYPE."
                       :if-does-not-exist (unless (eq :value if-does-not-exist)
                                            :error))
     (if in
-        (be seq (make-array (file-length in) :element-type element-type)
+        (let ((seq (make-array (file-length in) :element-type element-type)))
           (read-sequence seq in)
           seq)
         default)))
@@ -300,51 +275,12 @@ ELEMENT-TYPE."
   #-sbcl (let (#+cmu (lisp::*ignore-wildcards* t))
            (namestring pathname)))
 
-(defstruct (unix-file-stat (:conc-name stat-))
-  device
-  inode
-  links
-  atime
-  mtime
-  ctime
-  size
-  blksize
-  blocks
-  uid
-  gid
-  mode)
-
-(defun unix-stat (pathname)
-  ;; this could be different depending on the unix systems
-  (multiple-value-bind (ok? device inode mode links uid gid rdev
-                            size atime mtime ctime
-                            blksize blocks)
-      (#+cmu unix:unix-lstat
-       #+sbcl sb-unix:unix-lstat
-       ;; TODO(sterni): ECL, CCL
-       (if (stringp pathname)
-           pathname
-           (native-namestring pathname)))
-    (declare (ignore rdev))
-    (when ok?
-      (make-unix-file-stat :device device
-                           :inode inode
-                           :links links
-                           :atime atime
-                           :mtime mtime
-                           :ctime ctime
-                           :size size
-                           :blksize blksize
-                           :blocks blocks
-                           :uid uid
-                           :gid gid
-                           :mode mode))))
-
 ;; FILE-LENGTH is a bit idiosyncratic in this respect.  Besides, Unix
 ;; allows to get to know the file size without being able to open a
 ;; file; just ask politely.
 (defun file-size (pathname)
-  (stat-size (unix-stat pathname)))
+  #+sbcl (stat-size (unix-stat pathname))
+  #-sbcl (error "nyi"))
 
 ;; LAZY
 
diff --git a/third_party/lisp/mime4cl/mime.lisp b/third_party/lisp/mime4cl/mime.lisp
index eec7f87dfa..3cdac4b26b 100644
--- a/third_party/lisp/mime4cl/mime.lisp
+++ b/third_party/lisp/mime4cl/mime.lisp
@@ -1,7 +1,7 @@
 ;;;  mime4cl.lisp --- MIME primitives for Common Lisp
 
 ;;;  Copyright (C) 2005-2008, 2010 by Walter C. Pelissero
-;;;  Copyright (C) 2021 by the TVL Authors
+;;;  Copyright (C) 2021-2023 by the TVL Authors
 
 ;;;  Author: Walter C. Pelissero <walter@pelissero.de>
 ;;;  Project: mime4cl
@@ -183,14 +183,11 @@
                :test #'string=)
        (mime= (mime-body part1) (mime-body part2))))
 
-(defun mime-body-stream (mime-part &key (binary t))
-  (make-instance (if binary
-                     'binary-input-adapter-stream
-                     'character-input-adapter-stream)
-                 :source (mime-body mime-part)))
+(defun mime-body-stream (mime-part)
+  (make-input-adapter (mime-body mime-part)))
 
 (defun mime-body-length (mime-part)
-  (be body (mime-body mime-part)
+  (let ((body (mime-body mime-part)))
     ;; here the stream type is missing on purpose, because we may not
     ;; be able to size the length of a stream
     (etypecase body
@@ -207,8 +204,8 @@
             while byte
             count byte))))))
 
-(defmacro with-input-from-mime-body-stream ((stream part &key (binary t)) &body forms)
-  `(with-open-stream (,stream (mime-body-stream ,part :binary ,binary))
+(defmacro with-input-from-mime-body-stream ((stream part) &body forms)
+  `(with-open-stream (,stream (mime-body-stream ,part))
      ,@forms))
 
 (defmethod mime= ((part1 mime-bodily-part) (part2 mime-bodily-part))
@@ -302,12 +299,13 @@ semi-colons not within strings or comments."
 (defun parse-parameter (string)
   "Given a string like \"foo=bar\" return a pair (\"foo\" .
 \"bar\").  Return NIL if string is not parsable."
-  (be equal-position (position #\= string)
+  ;; TODO(sterni): when-let
+  (let ((equal-position (position #\= string)))
     (when equal-position
-      (be key (subseq string  0 equal-position)
+      (let ((key (subseq string  0 equal-position)))
         (if (= equal-position (1- (length string)))
             (cons key "")
-            (be value (string-trim-whitespace (subseq string (1+ equal-position)))
+            (let ((value (string-trim-whitespace (subseq string (1+ equal-position)))))
               (cons key
                     (if (and (> (length value) 1)
                              (char= #\" (elt value 0)))
@@ -316,8 +314,8 @@ semi-colons not within strings or comments."
                         ;; reader
                         (or (ignore-errors (read-from-string value))
                             (subseq value 1))
-                        (be end (or (position-if #'whitespace-p value)
-                                    (length value))
+                        (let ((end (or (position-if #'whitespace-p value)
+                                       (length value))))
                           (subseq value 0 end))))))))))
 
 (defun parse-content-type (string)
@@ -340,7 +338,7 @@ Example: (\"text\" \"plain\" ((\"charset\" . \"us-ascii\")...))."
 list.  The first element is the layout, the other elements are
 the optional parameters alist.
 Example: (\"inline\" (\"filename\" . \"doggy.jpg\"))."
-  (be parts (split-header-parts string)
+  (let ((parts (split-header-parts string)))
     (cons (car parts) (mapcan #'(lambda (parameter-string)
                                   (awhen (parse-parameter parameter-string)
                                     (list it)))
@@ -350,7 +348,7 @@ Example: (\"inline\" (\"filename\" . \"doggy.jpg\"))."
   "Parse STRING which should be a valid RFC822 message header and
 return two values: a string of the header name and a string of
 the header value."
-  (be colon (position #\: string)
+  (let ((colon (position #\: string)))
     (when colon
       (values (string-trim-whitespace (subseq string 0 colon))
               (string-trim-whitespace (subseq string (1+ colon)))))))
@@ -419,34 +417,6 @@ each (non-boundary) line or END-PART-FUNCTION at each PART-BOUNDARY."
          do (last-part)
          do (process-line line)))))
 
-;; This awkward handling of newlines is due to RFC2046: "The CRLF
-;; preceding the boundary delimiter line is conceptually attached to
-;; the boundary so that it is possible to have a part that does not
-;; end with a CRLF (line break).  Body parts that must be considered
-;; to end with line breaks, therefore, must have two CRLFs preceding
-;; the boundary delimiter line, the first of which is part of the
-;; preceding body part, and the second of which is part of the
-;; encapsulation boundary".
-(defun split-multipart-parts (body-stream part-boundary)
-  "Read from BODY-STREAM and split MIME parts separated by
-PART-BOUNDARY.  Return a list of strings."
-  (let ((part (make-string-output-stream))
-        (parts '())
-        (beginning-of-part-p t))
-    (flet ((output-line (line)
-             (if beginning-of-part-p
-                 (setf beginning-of-part-p nil)
-                 (terpri part))
-             (write-string line part))
-           (end-part ()
-             (setf beginning-of-part-p t)
-             (push (get-output-stream-string part) parts)))
-      (do-multipart-parts body-stream part-boundary #'output-line #'end-part)
-      (close part)
-      ;; the first part is empty or contains all the junk
-      ;; to the first boundary
-      (cdr (nreverse parts)))))
-
 (defun index-multipart-parts (body-stream part-boundary)
   "Read from BODY-STREAM and return the file offset of the MIME parts
 separated by PART-BOUNDARY."
@@ -531,9 +501,9 @@ separated by PART-BOUNDARY."
   (encode-mime-body (mime-body part) stream))
 
 (defmethod encode-mime-body ((part mime-multipart) stream)
-  (be boundary (or (get-mime-type-parameter part :boundary)
-                   (setf (get-mime-type-parameter part :boundary)
-                         (choose-boundary (mime-parts part))))
+  (let ((boundary (or (get-mime-type-parameter part :boundary)
+                      (setf (get-mime-type-parameter part :boundary)
+                            (choose-boundary (mime-parts part))))))
     (dolist (p (mime-parts part))
       (format stream "~%--~A~%" boundary)
       (encode-mime-part p stream))
@@ -588,7 +558,7 @@ found in STREAM."
   ;; continuation line of a header we don't want to a header we want
   (loop
      with headers = '() and skip-header = nil
-     for line = (be line (read-line stream nil)
+     for line = (let ((line (read-line stream nil)))
                   ;; skip the Unix "From " header if present
                   (if (string-starts-with "From " line)
                       (read-line stream nil)
@@ -641,19 +611,19 @@ found in STREAM."
 
 (defgeneric decode-mime-body (part input-stream))
 
-(defmethod decode-mime-body ((part mime-part) (stream delimited-input-stream))
- (be base (base-stream stream)
-   (if *lazy-mime-decode*
-       (setf (mime-body part)
-             (make-file-portion :data (etypecase base
-                                        (my-string-input-stream
-                                         (stream-string base))
-                                        (file-stream
-                                         (pathname base)))
-                                :encoding (mime-encoding part)
-                                :start (file-position stream)
-                                :end (stream-end stream)))
-       (call-next-method))))
+(defmethod decode-mime-body ((part mime-part) (stream flexi-stream))
+  (let ((base (flexi-stream-root-stream stream)))
+    (if *lazy-mime-decode*
+        (setf (mime-body part)
+              (make-file-portion :data (etypecase base
+                                         (vector-stream
+                                          (flexi-streams::vector-stream-vector base))
+                                         (file-stream
+                                          (pathname base)))
+                                 :encoding (mime-encoding part)
+                                 :start (flexi-stream-position stream)
+                                 :end (flexi-stream-bound stream)))
+        (call-next-method))))
 
 (defmethod decode-mime-body ((part mime-part) (stream file-stream))
   (if *lazy-mime-decode*
@@ -663,12 +633,12 @@ found in STREAM."
                                :start (file-position stream)))
       (call-next-method)))
 
-(defmethod decode-mime-body ((part mime-part) (stream my-string-input-stream))
+(defmethod decode-mime-body ((part mime-part) (stream vector-stream))
   (if *lazy-mime-decode*
       (setf (mime-body part)
-            (make-file-portion :data (stream-string stream)
+            (make-file-portion :data (flexi-streams::vector-stream-vector stream)
                                :encoding (mime-encoding part)
-                               :start (file-position stream)))
+                               :start (flexi-streams::vector-stream-index stream)))
       (call-next-method)))
 
 (defmethod decode-mime-body ((part mime-part) stream)
@@ -679,19 +649,18 @@ found in STREAM."
   "Decode STREAM according to PART characteristics and return a
 list of MIME parts."
   (save-file-excursion (stream)
-    (be offsets (index-multipart-parts stream (get-mime-type-parameter part :boundary))
+    (let ((offsets (index-multipart-parts stream (get-mime-type-parameter part :boundary))))
       (setf (mime-parts part)
             (mapcar #'(lambda (p)
                         (destructuring-bind (start . end) p
-                          (be *default-type* (if (eq :digest (mime-subtype part))
-                                                 '("message" "rfc822" ())
-                                                 '("text" "plain" (("charset" . "us-ascii"))))
-                              in (make-instance 'delimited-input-stream
-                                                :stream stream
-                                                :dont-close t
-                                                :start start
-                                                :end end)
-                              (read-mime-part in))))
+                          (let ((*default-type* (if (eq :digest (mime-subtype part))
+                                                    '("message" "rfc822" ())
+                                                    '("text" "plain" (("charset" . "us-ascii")))))
+                                (in (make-positioned-flexi-input-stream stream
+                                                                        :position start
+                                                                        :bound end
+                                                                        :ignore-close t)))
+                            (read-mime-part in))))
                     offsets)))))
 
 (defmethod decode-mime-body ((part mime-message) stream)
@@ -713,11 +682,11 @@ Return STRING itself if STRING is an unkown encoding."
        string))
 
 (defun header (name headers)
-  (be elt (assoc name headers :test #'string-equal)
+  (let ((elt (assoc name headers :test #'string-equal)))
     (values (cdr elt) (car elt))))
 
 (defun (setf header) (value name headers)
-  (be entry (assoc name headers :test #'string-equal)
+  (let ((entry (assoc name headers :test #'string-equal)))
     (unless entry
       (error "missing header ~A can't be set" name))
     (setf (cdr entry) value)))
@@ -729,7 +698,7 @@ guessed from the headers, use the *DEFAULT-TYPE*."
   (flet ((hdr (what)
            (header what headers)))
     (destructuring-bind (type subtype parms)
-        (or 
+        (or
          (aand (hdr :content-type)
                (parse-content-type it))
          *default-type*)
@@ -755,16 +724,16 @@ guessed from the headers, use the *DEFAULT-TYPE*."
 
 (defun read-mime-part (stream)
   "Read mime part from STREAM.  Return a MIME-PART object."
-  (be headers (read-rfc822-headers stream
-                                   '(:mime-version :content-transfer-encoding :content-type
-                                     :content-disposition :content-description :content-id))
+  (let ((headers (read-rfc822-headers stream
+                                      '(:mime-version :content-transfer-encoding :content-type
+                                        :content-disposition :content-description :content-id))))
     (make-mime-part headers stream)))
 
 (defun read-mime-message (stream)
   "Main function to read a MIME message from a stream.  It
 returns a MIME-MESSAGE object."
-  (be headers (read-rfc822-headers stream)
-      *default-type* '("text" "plain" (("charset" . "us-ascii")))
+  (let ((headers (read-rfc822-headers stream))
+        (*default-type* '("text" "plain" (("charset" . "us-ascii")))))
     (flet ((hdr (what)
              (header what headers)))
       (destructuring-bind (type subtype parms)
@@ -782,17 +751,21 @@ returns a MIME-MESSAGE object."
   msg)
 
 (defmethod mime-message ((msg string))
-  (with-open-stream (in (make-instance 'my-string-input-stream :string msg))
-    (read-mime-message in)))
+  (mime-message (flexi-streams:string-to-octets msg)))
 
-(defmethod mime-message ((msg stream))
-  (read-mime-message msg))
+(defmethod mime-message ((msg vector))
+  (with-input-from-sequence (in msg)
+    (mime-message in)))
 
 (defmethod mime-message ((msg pathname))
-  (let (#+sbcl(sb-impl::*default-external-format* :latin-1)
-        #+sbcl(sb-alien::*default-c-string-external-format* :latin-1))
-    (with-open-file (in msg)
-      (read-mime-message in))))
+  (with-open-file (in msg :element-type '(unsigned-byte 8))
+    (mime-message in)))
+
+(defmethod mime-message ((msg flexi-stream))
+  (read-mime-message msg))
+
+(defmethod mime-message ((msg stream))
+  (read-mime-message (make-flexi-stream msg)))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
@@ -815,15 +788,16 @@ returns a MIME-MESSAGE object."
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
 (defmethod make-encoded-body-stream ((part mime-bodily-part))
-  (be body (mime-body part)
+  (let ((body (mime-body part)))
     (make-instance (case (mime-encoding part)
                      (:base64
                       'base64-encoder-input-stream)
                      (:quoted-printable
                       'quoted-printable-encoder-input-stream)
-                     (t
+                     (otherwise
                       '8bit-encoder-input-stream))
-                   :stream (make-instance 'binary-input-adapter-stream :source body))))
+                   :underlying-stream
+                   (make-input-adapter body))))
 
 (defun choose-boundary (parts &optional default)
   (labels ((match-in-parts (boundary parts)
@@ -855,7 +829,7 @@ returns a MIME-MESSAGE object."
 
 ;; fall back method
 (defmethod mime-part-size ((part mime-part))
-  (be body (mime-body part)
+  (let ((body (mime-body part)))
     (typecase body
       (pathname
        (file-size body))
@@ -882,7 +856,7 @@ returns a MIME-MESSAGE object."
   (case (mime-subtype part)
     (:alternative
      ;; try to choose something simple to print or the first thing
-     (be parts (mime-parts part)
+     (let ((parts (mime-parts part)))
        (print-mime-part (or (find-if #'(lambda (part)
                                          (and (eq (class-of part) (find-class 'mime-text))
                                               (eq (mime-subtype part) :plain)))
@@ -896,7 +870,7 @@ returns a MIME-MESSAGE object."
 ;; because we don't know which one we should use.  Messages written in
 ;; anything but ASCII will likely be unreadable -wcp11/10/07.
 (defmethod print-mime-part ((part mime-text) (out stream))
-  (be body (mime-body part)
+  (let ((body (mime-body part)))
     (etypecase body
       (string
        (write-string body out))
@@ -950,8 +924,8 @@ second in MIME."))
 (defmethod find-mime-part-by-path ((part mime-multipart) path)
   (if (null path)
       part
-      (be parts (mime-parts part)
-          part-number (car path)
+      (let ((parts (mime-parts part))
+            (part-number (car path)))
         (if (<= 1 part-number (length parts))
             (find-mime-part-by-path (nth (1- (car path)) (mime-parts part)) (cdr path))
             (error "~S has just ~D subparts, but part ~D was requested (parts are enumerated base 1)."
@@ -979,7 +953,7 @@ is a string."))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(defmethod find-mime-text-part (msg)
+(defgeneric find-mime-text-part (msg)
   (:documentation
    "Return message if it is a text message or first text part.
    If no suitable text part is found, return NIL."))
diff --git a/third_party/lisp/mime4cl/package.lisp b/third_party/lisp/mime4cl/package.lisp
index 31cd85d54e..94b9e6b390 100644
--- a/third_party/lisp/mime4cl/package.lisp
+++ b/third_party/lisp/mime4cl/package.lisp
@@ -23,9 +23,7 @@
 
 (defpackage :mime4cl
   (:nicknames :mime)
-  (:use :common-lisp :npg :mime4cl-ex-sclf :trivial-gray-streams)
-  (:import-from :babel :octets-to-string)
-  (:import-from :babel-encodings :get-character-encoding)
+  (:use :common-lisp :npg :mime4cl-ex-sclf :trivial-gray-streams :flexi-streams)
   (:export #:*lazy-mime-decode*
            #:print-mime-part
            #:read-mime-message
@@ -68,11 +66,10 @@
            #:decode-quoted-printable-string
            #:encode-quoted-printable-stream
            #:encode-quoted-printable-sequence
-           #:decode-base64-stream
-           #:decode-base64-string
            #:encode-base64-stream
            #:encode-base64-sequence
            #:parse-RFC2047-text
+           #:decode-RFC2047
            #:parse-RFC822-header
            #:read-RFC822-headers
            #:time-RFC822-string
@@ -85,7 +82,6 @@
            #:with-input-from-mime-body-stream
            ;; endec.lisp
            #:base64-encoder
-           #:base64-decoder
            #:null-encoder
            #:null-decoder
            #:byte-encoder
@@ -101,4 +97,7 @@
            ;; address.lisp
            #:parse-addresses #:mailboxes-only
            #:mailbox #:mbx-description #:mbx-user #:mbx-host #:mbx-domain #:mbx-domain-name #:mbx-address
-           #:mailbox-group #:mbxg-name #:mbxg-mailboxes))
+           #:mailbox-group #:mbxg-name #:mbxg-mailboxes
+           ;; streams.lisp
+           #:redirect-stream
+           ))
diff --git a/third_party/lisp/mime4cl/streams.lisp b/third_party/lisp/mime4cl/streams.lisp
index dcac6ac341..71a32d84e4 100644
--- a/third_party/lisp/mime4cl/streams.lisp
+++ b/third_party/lisp/mime4cl/streams.lisp
@@ -1,7 +1,7 @@
 ;;; streams.lisp --- En/De-coding Streams
 
 ;;; Copyright (C) 2012 by Walter C. Pelissero
-;;; Copyright (C) 2021-2022 by the TVL Authors
+;;; Copyright (C) 2021-2023 by the TVL Authors
 
 ;;; Author: Walter C. Pelissero <walter@pelissero.de>
 ;;; Project: mime4cl
@@ -21,9 +21,17 @@
 
 (in-package :mime4cl)
 
+(defun flexi-stream-root-stream (stream)
+  "Return the non FLEXI-STREAM stream a given chain of FLEXI-STREAMs is based on."
+  (if (typep stream 'flexi-stream)
+      (flexi-stream-root-stream (flexi-stream-stream stream))
+      stream))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
 (defclass coder-stream-mixin ()
   ((real-stream :type stream
-                :initarg :stream
+                :initarg :underlying-stream
                 :reader real-stream)
    (dont-close :initform nil
                :initarg :dont-close)))
@@ -39,9 +47,12 @@
 (defclass coder-output-stream-mixin (fundamental-binary-output-stream coder-stream-mixin)
   ())
 
+;; TODO(sterni): temporary, ugly measure to make flexi-streams happy
+(defmethod stream-element-type ((stream coder-input-stream-mixin))
+  (declare (ignore stream))
+  '(unsigned-byte 8))
 
 (defclass quoted-printable-decoder-stream (coder-input-stream-mixin quoted-printable-decoder) ())
-(defclass base64-decoder-stream (coder-input-stream-mixin base64-decoder) ())
 (defclass 8bit-decoder-stream (coder-input-stream-mixin 8bit-decoder) ())
 
 (defclass quoted-printable-encoder-stream (coder-output-stream-mixin quoted-printable-encoder) ())
@@ -52,7 +63,7 @@
 
 (defmethod initialize-instance :after ((stream coder-stream-mixin) &key &allow-other-keys)
   (unless (slot-boundp stream 'real-stream)
-    (error "REAL-STREAM is unbound.  Must provide a :STREAM argument.")))
+    (error "REAL-STREAM is unbound.  Must provide a :UNDERLYING-STREAM argument.")))
 
 (defmethod initialize-instance ((stream coder-output-stream-mixin) &key &allow-other-keys)
   (call-next-method)
@@ -119,7 +130,7 @@ in a stream of character."))
   (with-slots (encoder buffer-queue real-stream) stream
     (loop
        while (queue-empty-p buffer-queue)
-       do (be byte (read-byte real-stream nil)
+       do (let ((byte (read-byte real-stream nil)))
             (if byte
                 (encoder-write-byte encoder byte)
                 (progn
@@ -136,220 +147,128 @@ in a stream of character."))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
-(defclass input-adapter-stream ()
-  ((source :initarg :source)
-   (real-stream)
-   (input-function)))
-
-(defclass binary-input-adapter-stream (fundamental-binary-input-stream input-adapter-stream) ())
-
-(defclass character-input-adapter-stream (fundamental-character-input-stream input-adapter-stream) ())
-
-(defmethod stream-element-type ((stream binary-input-adapter-stream))
-  '(unsigned-byte 8))
-
-(defmethod initialize-instance ((stream input-adapter-stream) &key &allow-other-keys)
-  (call-next-method)
-  (assert (slot-boundp stream 'source)))
-
-(defmethod initialize-instance ((stream binary-input-adapter-stream) &key &allow-other-keys)
-  (call-next-method)
-  ;; REAL-STREAM slot is set only if we are going to close it later on
-  (with-slots (source real-stream input-function) stream
-    (etypecase source
-      (string
-       (setf real-stream (make-string-input-stream source)
-             input-function #'(lambda ()
-                                (awhen (read-char real-stream nil)
-                                  (char-code it)))))
-      ((vector (unsigned-byte 8))
-       (be i 0
-         (setf input-function #'(lambda ()
-                                  (when (< i (length source))
-                                    (prog1 (aref source i)
-                                      (incf i)))))))
-      (stream
-       (assert (input-stream-p source))
-       (setf input-function (if (subtypep (stream-element-type source) 'character)
-                                #'(lambda ()
-                                    (awhen (read-char source nil)
-                                      (char-code it)))
-                                #'(lambda ()
-                                    (read-byte source nil)))))
-      (pathname
-       (setf real-stream (open source :element-type '(unsigned-byte 8))
-             input-function #'(lambda ()
-                                (read-byte real-stream nil))))
-      (file-portion
-       (setf real-stream (open-decoded-file-portion source)
-             input-function #'(lambda ()
-                                (read-byte real-stream nil)))))))
-
-(defmethod initialize-instance ((stream character-input-adapter-stream) &key &allow-other-keys)
-  (call-next-method)
-  ;; REAL-STREAM slot is set only if we are going to close later on
-  (with-slots (source real-stream input-function) stream
-    (etypecase source
-      (string
-       (setf real-stream (make-string-input-stream source)
-             input-function #'(lambda ()
-                                (read-char real-stream nil))))
-      ((vector (unsigned-byte 8))
-       (be i 0
-         (setf input-function #'(lambda ()
-                                  (when (< i (length source))
-                                    (prog1 (code-char (aref source i))
-                                      (incf i)))))))
-      (stream
-       (assert (input-stream-p source))
-       (setf input-function (if (subtypep (stream-element-type source) 'character)
-                                #'(lambda ()
-                                    (read-char source nil))
-                                #'(lambda ()
-                                    (awhen (read-byte source nil)
-                                      (code-char it))))))
-      (pathname
-       (setf real-stream (open source :element-type 'character)
-             input-function #'(lambda ()
-                                (read-char real-stream nil))))
-      (file-portion
-       (setf real-stream (open-decoded-file-portion source)
-             input-function #'(lambda ()
-                                (awhen (read-byte real-stream nil)
-                                  (code-char it))))))))
-
-(defmethod close ((stream input-adapter-stream) &key abort)
-  (when (slot-boundp stream 'real-stream)
-    (with-slots (real-stream) stream
-      (close real-stream :abort abort))))
-
-(defmethod stream-read-byte ((stream binary-input-adapter-stream))
-  (with-slots (input-function) stream
-    (or (funcall input-function)
-        :eof)))
-
-(defmethod stream-read-char ((stream character-input-adapter-stream))
-  (with-slots (input-function) stream
-    (or (funcall input-function)
-        :eof)))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-(defclass delimited-input-stream (fundamental-character-input-stream coder-stream-mixin)
-  ((start-offset :initarg :start
-                 :initform 0
-                 :reader stream-start
-                 :type integer)
-   (end-offset :initarg :end
-               :initform nil
-               :reader stream-end
-               :type (or null integer))
-   (current-offset :type integer)))
-
-(defmethod print-object ((object delimited-input-stream) stream)
-  (if *print-readably*
-      (call-next-method)
-      (with-slots (start-offset end-offset) object
-        (print-unreadable-object (object stream :type t :identity t)
-          (format stream "start=~A end=~A" start-offset end-offset)))))
-
-(defun base-stream (stream)
-  (if (typep stream 'delimited-input-stream)
-      (base-stream (real-stream stream))
-      stream))
-
-(defmethod initialize-instance ((stream delimited-input-stream) &key &allow-other-keys)
-  (call-next-method)
-  (unless (slot-boundp stream 'real-stream)
-    (error "REAL-STREAM is unbound.  Must provide a :STREAM argument."))
-  (with-slots (start-offset) stream
-    (file-position stream start-offset)))
-
-(defmethod (setf stream-file-position) (newval (stream delimited-input-stream))
-  (with-slots (current-offset real-stream) stream
-    (setf current-offset newval)
-    (call-next-method)))
-
-(defmethod stream-file-position ((stream delimited-input-stream))
-  (slot-value stream 'current-offset))
-
-;; Calling file-position with SBCL on every read is quite expensive, since
-;; it will invoke lseek each time. This is so expensive that it's faster to
-;; /compute/ the amount the stream got advanced by.
-;; file-position's behavior however, is quite flexible and it behaves differently
-;; not only for different implementation, but also different streams in SBCL.
-;; Thus, we should ideally go back to file-position and try to reduce the amount
-;; of calls by using read-sequence.
-;; TODO(sterni): make decoders use read-sequence and drop offset tracking code
-(macrolet ((def-stream-read (name read-fun update-offset-form)
-             `(defmethod ,name ((stream delimited-input-stream))
-               (with-slots (real-stream end-offset current-offset) stream
-                 (let ((el (if (or (not end-offset)
-                                   (< current-offset end-offset))
-                               (or (,read-fun real-stream nil)
-                                   :eof)
-                               :eof)))
-                   (setf current-offset ,update-offset-form)
-                   el)))))
-
-  ;; Assume we are using an encoding where < 128 is one byte, in all other cases
-  ;; it's hard to guess how much file-position will increase
-  (def-stream-read stream-read-char read-char
-    (if (or (eq el :eof) (< (char-code el) 128))
-        (1+ current-offset)
-        (file-position real-stream)))
-
-  (def-stream-read stream-read-byte read-byte (1+ current-offset)))
-
-;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
-
-(defclass my-string-input-stream (fundamental-character-input-stream coder-stream-mixin)
-  ((string :initarg :string
-           :reader stream-string)))
-
-(defmethod initialize-instance ((stream my-string-input-stream) &key &allow-other-keys)
+(defun make-custom-flexi-stream (class stream other-args)
+  (apply #'make-instance
+         class
+         :stream stream
+         (mapcar (lambda (x)
+                   ;; make-flexi-stream has a discrepancy between :initarg of
+                   ;; make-instance and its &key which we mirror here.
+                   (if (eq x :external-format) :flexi-stream-external-format x))
+                 other-args)))
+
+(defclass adapter-flexi-input-stream (flexi-input-stream)
+  ((ignore-close
+    :initform nil
+    :initarg :ignore-close
+    :documentation
+    "If T, calling CLOSE on the stream does nothing.
+If NIL, the underlying stream is closed."))
+  (:documentation "FLEXI-STREAM that does not close the underlying stream on
+CLOSE if :IGNORE-CLOSE is T."))
+
+(defmethod close ((stream adapter-flexi-input-stream) &key abort)
+  (declare (ignore abort))
+  (with-slots (ignore-close) stream
+    (unless ignore-close
+      (call-next-method))))
+
+(defun make-input-adapter (source)
+  (etypecase source
+    ;; If it's already a stream, we need to make sure it's not closed by the adapter
+    (stream
+     (assert (input-stream-p source))
+     (if (and (typep source 'adapter-flexi-input-stream)
+              (slot-value source 'ignore-close))
+         source ; already ignores CLOSE
+         (make-adapter-flexi-input-stream source :ignore-close t)))
+    ;; TODO(sterni): is this necessary? (maybe with (not *lazy-mime-decode*)?)
+    (string
+     (make-input-adapter (string-to-octets source)))
+    ((vector (unsigned-byte 8))
+     (make-in-memory-input-stream source))
+    (pathname
+     (make-flexi-stream (open source :element-type '(unsigned-byte 8))))
+    (file-portion
+     (open-decoded-file-portion source))))
+
+(defun make-adapter-flexi-input-stream (stream &rest args)
+  "Create a ADAPTER-FLEXI-INPUT-STREAM. Accepts the same keyword arguments as
+MAKE-FLEXI-STREAM as well as :IGNORE-CLOSE. If T, the underlying stream is not
+closed."
+  (make-custom-flexi-stream 'adapter-flexi-input-stream stream args))
+
+(defclass positioned-flexi-input-stream (adapter-flexi-input-stream)
+  ()
+  (:documentation
+   "FLEXI-INPUT-STREAM that automatically advances the underlying :STREAM to
+the location given by :POSITION. This uses FILE-POSITION internally, so it'll
+only works if the underlying stream position is tracked in bytes. Note that
+the underlying stream is still advanced, so having multiple instances of
+POSITIONED-FLEXI-INPUT-STREAM based with the same underlying stream won't work
+reliably.
+Also supports :IGNORE-CLOSE of ADAPTER-FLEXI-INPUT-STREAM."))
+
+(defmethod initialize-instance ((stream positioned-flexi-input-stream)
+                                &key &allow-other-keys)
   (call-next-method)
-  (assert (slot-boundp stream 'string))
-  (with-slots (string real-stream) stream
-    (setf real-stream (make-string-input-stream string))))
-
-(defmethod stream-read-char ((stream my-string-input-stream))
-  (with-slots (real-stream) stream
-    (or (read-char real-stream nil)
-        :eof)))
+  ;; The :POSITION initarg is only informational for flexi-streams: It assumes
+  ;; it is were the stream it got is already at and continuously updates it
+  ;; for querying (via FLEXI-STREAM-POSITION) and bound checking.
+  ;; Since we have streams that are not positioned correctly, we need to do this
+  ;; here using FILE-POSITION. Note that assumes the underlying implementation
+  ;; uses bytes for FILE-POSITION which is not guaranteed (probably some streams
+  ;; even in SBCL don't).
+  (file-position (flexi-stream-stream stream) (flexi-stream-position stream)))
+
+(defun make-positioned-flexi-input-stream (stream &rest args)
+  "Create a POSITIONED-FLEXI-INPUT-STREAM. Accepts the same keyword arguments as
+MAKE-FLEXI-STREAM as well as :IGNORE-CLOSE. Causes the FILE-POSITION of STREAM to
+be modified to match the :POSITION argument."
+  (make-custom-flexi-stream 'positioned-flexi-input-stream stream args))
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
 
+;; TODO(sterni): test correct behavior with END NIL
 (defstruct file-portion
-  data					;  string or a pathname
+  data                                  ; string or a pathname
   encoding
   start
   end)
 
-(defun open-file-portion (file-portion)
-  (be data (file-portion-data file-portion)
-    (etypecase data
-      (pathname
-       (be stream (open data)
-         (make-instance 'delimited-input-stream
-                        :stream stream
-                        :start (file-portion-start file-portion)
-                        :end (file-portion-end file-portion))))
-      (string
-       (make-instance 'delimited-input-stream
-                      :stream (make-string-input-stream data)
-                      :start (file-portion-start file-portion)
-                      :end (file-portion-end file-portion)))
-      (stream
-       (make-instance 'delimited-input-stream
-                      :stream data
-                      :dont-close t
-                      :start (file-portion-start file-portion)
-                      :end (file-portion-end file-portion))))))
-
 (defun open-decoded-file-portion (file-portion)
-  (make-instance (case (file-portion-encoding file-portion)
-                   (:quoted-printable 'quoted-printable-decoder-stream)
-                   (:base64 'base64-decoder-stream)
-                   (t '8bit-decoder-stream))
-                 :stream (open-file-portion file-portion)))
+  (with-slots (data encoding start end)
+      file-portion
+    (let* ((binary-stream
+             (etypecase data
+               (pathname
+                (open data :element-type '(unsigned-byte 8)))
+               ((vector (unsigned-byte 8))
+                (flexi-streams:make-in-memory-input-stream data))
+               (stream
+                ;; TODO(sterni): assert that bytes/flexi-stream
+                data)))
+           (params (ccase encoding
+                     ((:quoted-printable :base64) '(:external-format :us-ascii))
+                     (:8bit '(:element-type (unsigned-byte 8)))
+                     (:7bit '(:external-format :us-ascii))))
+           (portion-stream (apply #'make-positioned-flexi-input-stream
+                                  binary-stream
+                                  :position start
+                                  :bound end
+                                  ;; if data is a stream we can't have a
+                                  ;; FILE-PORTION without modifying it when
+                                  ;; reading etc. The least we can do, though,
+                                  ;; is forgo destroying it.
+                                  :ignore-close (typep data 'stream)
+                                  params))
+           (needs-decoder-stream (member encoding '(:quoted-printable
+                                                    :base64))))
+
+      (if needs-decoder-stream
+          (make-instance
+           (ccase encoding
+             (:quoted-printable 'quoted-printable-decoder-stream)
+             (:base64 'qbase64:decode-stream))
+           :underlying-stream portion-stream)
+          portion-stream))))
diff --git a/third_party/lisp/mime4cl/test/endec.lisp b/third_party/lisp/mime4cl/test/endec.lisp
index 4ff89d5eac..6b22b3f6a2 100644
--- a/third_party/lisp/mime4cl/test/endec.lisp
+++ b/third_party/lisp/mime4cl/test/endec.lisp
@@ -103,13 +103,12 @@ line")
 
 (deftest base64.3
     (map 'string #'code-char
-         (decode-base64-string "U29tZSByYW5kb20gc3RyaW5nLg=="))
+         (qbase64:decode-string "U29tZSByYW5kb20gc3RyaW5nLg=="))
   "Some random string.")
 
 (deftest base64.4
     (map 'string #'code-char
-         (decode-base64-string "some rubbish U29tZSByYW5kb20gc3RyaW5nLg== more rubbish"
-                               :start 13 :end 41))
+         (qbase64:decode-string "U29tZSByYW5kb20gc3RyaW5nLg=="))
   "Some random string.")
 
 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -118,6 +117,26 @@ line")
     (parse-RFC2047-text "foo bar")
   ("foo bar"))
 
+;; from RFC2047 section 8
+(deftest RFC2047.2
+    (decode-RFC2047 "=?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>")
+  "Keith Moore <moore@cs.utk.edu>")
+
+;; from RFC2047 section 8
+(deftest RFC2047.3
+    (decode-RFC2047 "=?ISO-8859-1?Q?Olle_J=E4rnefors?=")
+  "Olle Jรคrnefors")
+
+;; from RFC2047 section 8
+(deftest RFC2047.4
+    (decode-RFC2047 "Nathaniel Borenstein <nsb@thumper.bellcore.com> (=?iso-8859-8?b?7eXs+SDv4SDp7Oj08A==?=)")
+  "Nathaniel Borenstein <nsb@thumper.bellcore.com> (ืื•ืœืฉ ืŸื‘ ื™ืœื˜ืคื )")
+
+;; from RFC2047 section 8
+(deftest RFC2047.5
+  (decode-RFC2047 "=?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>")
+  "Keld Jรธrn Simonsen <keld@dkuug.dk>")
+
 (defun perftest-encoder (encoder-class &optional (megs 100))
   (declare (optimize (speed 3) (debug 0) (safety 0))
            (type fixnum megs))
@@ -145,7 +164,6 @@ line")
         (let* ((meg (* 1024 1024))
                (buffer (make-sequence '(vector (unsigned-byte 8)) meg))
                (encoder-class (ecase decoder-class
-                                (mime4cl:base64-decoder 'mime4cl:base64-encoder)
                                 (mime4cl:quoted-printable-decoder 'mime4cl:quoted-printable-encoder)))
                (encoder (make-instance encoder-class
                                        :output-function #'(lambda (c)
diff --git a/third_party/lisp/mime4cl/test/mime.lisp b/third_party/lisp/mime4cl/test/mime.lisp
index 8d93978599..dbd1dd996d 100644
--- a/third_party/lisp/mime4cl/test/mime.lisp
+++ b/third_party/lisp/mime4cl/test/mime.lisp
@@ -1,7 +1,7 @@
 ;;; mime.lisp --- MIME regression tests
 
 ;;; Copyright (C) 2012 by Walter C. Pelissero
-;;; Copyright (C) 2021-2022 by the TVL Authors
+;;; Copyright (C) 2021-2023 by the TVL Authors
 
 ;;; Author: Walter C. Pelissero <walter@pelissero.de>
 ;;; Project: mime4cl
@@ -27,28 +27,15 @@
                          *load-pathname*
                          #P"")))
 
-(defvar *sample1-file* (make-pathname :defaults #.(or *compile-file-pathname*
-                                                      *load-pathname*)
-                                      :name "sample1"
-                                      :type "msg"))
-
-(deftest mime.1
-    (let* ((orig (mime-message *sample1-file*))
-           (dup (mime-message (with-output-to-string (out) (encode-mime-part orig out)))))
-      (mime= orig dup))
-  t)
-
-(deftest mime.2
-    (loop
-       for f in (directory (make-pathname :defaults *samples-directory*
-                                          :name :wild
-                                          :type "txt"))
-       do
-         (format t "~A:~%" f)
-         (finish-output)
-         (let* ((orig (mime-message f))
-                (dup (mime-message (with-output-to-string (out) (encode-mime-part orig out)))))
-           (unless (mime= orig dup)
-             (return nil)))
-       finally (return t))
-  t)
+(loop
+ for f in (directory (make-pathname :defaults *samples-directory*
+                                    :name :wild
+                                    :type "msg"))
+ for i from 1
+ do
+ (add-test (intern (format nil "MIME.~A" i))
+           `(let* ((orig (mime-message ,f))
+                   (dup (mime-message
+                         (with-output-to-string (out) (encode-mime-part orig out)))))
+              (mime= orig dup))
+           t))
diff --git a/third_party/lisp/mime4cl/test/rt.lisp b/third_party/lisp/mime4cl/test/rt.lisp
index 06160debbe..3f3aa5c56c 100644
--- a/third_party/lisp/mime4cl/test/rt.lisp
+++ b/third_party/lisp/mime4cl/test/rt.lisp
@@ -1,5 +1,6 @@
 #|----------------------------------------------------------------------------|
  | Copyright 1990 by the Massachusetts Institute of Technology, Cambridge MA. |
+ | Copyright 2023 by the TVL Authors                                          |
  |                                                                            |
  | Permission  to  use,  copy, modify, and distribute this software  and  its |
  | documentation for any purpose  and without fee is hereby granted, provided |
@@ -20,10 +21,10 @@
  |----------------------------------------------------------------------------|#
 
 (defpackage #:regression-test
-  (:nicknames #:rtest #-lispworks #:rt) 
+  (:nicknames #:rtest #-lispworks #:rt)
   (:use #:cl)
   (:export #:*do-tests-when-defined* #:*test* #:continue-testing
-           #:deftest #:do-test #:do-tests #:get-test #:pending-tests
+           #:deftest #:add-test #:do-test #:do-tests #:get-test #:pending-tests
            #:rem-all-tests #:rem-test)
   (:documentation "The MIT regression tester with pfdietz's modifications"))
 
@@ -86,25 +87,28 @@
 (defmacro deftest (name form &rest values)
   `(add-entry '(t ,name ,form .,values)))
 
+(defun add-test (name form &rest values)
+  (funcall #'add-entry (append (list 't name form) values)))
+
 (defun add-entry (entry)
   (setq entry (copy-list entry))
   (do ((l *entries* (cdr l))) (nil)
     (when (null (cdr l))
       (setf (cdr l) (list entry))
       (return nil))
-    (when (equal (name (cadr l)) 
+    (when (equal (name (cadr l))
                  (name entry))
       (setf (cadr l) entry)
       (report-error nil
-        "Redefining test ~:@(~S~)"
-        (name entry))
+                    "Redefining test ~:@(~S~)"
+                    (name entry))
       (return nil)))
   (when *do-tests-when-defined*
     (do-entry entry))
   (setq *test* (name entry)))
 
 (defun report-error (error? &rest args)
-  (cond (*debug* 
+  (cond (*debug*
          (apply #'format t args)
          (if error? (throw '*debug* nil)))
         (error? (apply #'error args))
@@ -184,7 +188,7 @@
       (setf (pend entry)
             (or aborted
                 (not (equalp-with-case r (vals entry)))))
-      
+
       (when (pend entry)
         (let ((*print-circle* *print-circle-on-failure*))
           (format s "~&Test ~:@(~S~) failed~
@@ -210,7 +214,7 @@
     (setf (pend entry) t))
   (if (streamp out)
       (do-entries out)
-      (with-open-file 
+      (with-open-file
           (stream out :direction :output)
         (do-entries stream))))
 
diff --git a/third_party/lisp/mime4cl/test/sample1.msg b/third_party/lisp/mime4cl/test/samples/sample1.msg
index 662a9fab34..662a9fab34 100644
--- a/third_party/lisp/mime4cl/test/sample1.msg
+++ b/third_party/lisp/mime4cl/test/samples/sample1.msg
diff --git a/third_party/lisp/mime4cl/test/temp-file.lisp b/third_party/lisp/mime4cl/test/temp-file.lisp
index 3e6765806c..554f35844b 100644
--- a/third_party/lisp/mime4cl/test/temp-file.lisp
+++ b/third_party/lisp/mime4cl/test/temp-file.lisp
@@ -63,7 +63,7 @@ file, otherwise *TMP-FILE-DEFAULTS* is used."
   "Execute BODY within a dynamic extent where STREAM is bound to
 a STREAM open on a unique temporary file name.  OPEN-TEMP-ARGS are
 passed verbatim to OPEN-TEMP-FILE."
-  `(be ,stream (open-temp-file ,@open-temp-args)
+  `(let ((,stream (open-temp-file ,@open-temp-args)))
      (unwind-protect
           (progn ,@body)
        (close ,stream)
diff --git a/third_party/lisp/npg/OWNERS b/third_party/lisp/npg/OWNERS
index f16dd105d7..2e95807063 100644
--- a/third_party/lisp/npg/OWNERS
+++ b/third_party/lisp/npg/OWNERS
@@ -1,3 +1 @@
-inherited: true
-owners:
-  - sterni
+sterni
diff --git a/third_party/lisp/qbase64/coreutils-base64.patch b/third_party/lisp/qbase64/coreutils-base64.patch
new file mode 100644
index 0000000000..5a2f2a9f08
--- /dev/null
+++ b/third_party/lisp/qbase64/coreutils-base64.patch
@@ -0,0 +1,13 @@
+diff --git a/qbase64-test.lisp b/qbase64-test.lisp
+index 310fdf3..b92abb5 100644
+--- a/qbase64-test.lisp
++++ b/qbase64-test.lisp
+@@ -14,7 +14,7 @@
+       (with-open-temporary-file (tmp :direction :output :element-type '(unsigned-byte 8))
+         (write-sequence bytes tmp)
+         (force-output tmp)
+-        (let* ((encoded (uiop:run-program `("base64" "-b" ,(format nil "~A" linebreak) "-i" ,(namestring tmp)) :output (if (zerop linebreak) '(:string :stripped t) :string)))
++        (let* ((encoded (uiop:run-program `("base64" "-w" ,(format nil "~A" linebreak) ,(namestring tmp)) :output (if (zerop linebreak) '(:string :stripped t) :string) :error-output *error-output*))
+                (length (length encoded)))
+           (cond ((and (> length 1)
+                       (string= (subseq encoded (- length 2))
diff --git a/third_party/lisp/qbase64/default.nix b/third_party/lisp/qbase64/default.nix
new file mode 100644
index 0000000000..40a93e04f0
--- /dev/null
+++ b/third_party/lisp/qbase64/default.nix
@@ -0,0 +1,57 @@
+{ depot, pkgs, ... }:
+
+let
+  src = pkgs.applyPatches {
+    src = pkgs.fetchFromGitHub {
+      owner = "chaitanyagupta";
+      repo = "qbase64";
+      rev = "4ac193ed6b35a867ca453ed74acc128c9a077407";
+      sha256 = "06daqqfdd51wkx0pyxgz7zq4ibzsqsgn3qs04jabx67gyybgnmjm";
+    };
+
+    patches = [
+      # qbase64 expects macOS base64
+      ./coreutils-base64.patch
+    ];
+  };
+
+  getSrcs = builtins.map (p: "${src}/${p}");
+
+in
+
+depot.nix.buildLisp.library {
+  name = "qbase64";
+
+  srcs = getSrcs [
+    "package.lisp"
+    "utils.lisp"
+    "stream-utils.lisp"
+    "qbase64.lisp"
+  ];
+
+  deps = [
+    depot.third_party.lisp.trivial-gray-streams
+    depot.third_party.lisp.metabang-bind
+  ];
+
+  tests = {
+    name = "qbase64-tests";
+
+    srcs = getSrcs [
+      "qbase64-test.lisp"
+    ];
+
+    deps = [
+      {
+        sbcl = depot.nix.buildLisp.bundled "uiop";
+        default = depot.nix.buildLisp.bundled "asdf";
+      }
+      depot.third_party.lisp.fiveam
+      depot.third_party.lisp.cl-fad
+    ];
+
+    expression = ''
+      (fiveam:run! '(qbase64-test::encoder 'qbase64-test::decoder))
+    '';
+  };
+}
diff --git a/third_party/lisp/str.nix b/third_party/lisp/str.nix
new file mode 100644
index 0000000000..556f9cc307
--- /dev/null
+++ b/third_party/lisp/str.nix
@@ -0,0 +1,49 @@
+{ depot, pkgs, ... }:
+
+let
+  inherit (depot.nix) buildLisp;
+  src = with pkgs; srcOnly lispPackages.str;
+in
+buildLisp.library {
+  name = "str";
+
+  deps = with depot.third_party.lisp; [
+    {
+      sbcl = buildLisp.bundled "uiop";
+      default = buildLisp.bundled "asdf";
+    }
+    cl-ppcre
+    cl-ppcre.unicode
+    cl-change-case
+  ];
+
+  srcs = [
+    (pkgs.runCommand "str.lisp" { } ''
+      substitute ${src}/str.lisp $out \
+        --replace-fail \
+          '(asdf:component-version (asdf:find-system "str"))' \
+          '"${pkgs.lispPackages.str.meta.version}"'
+    '')
+  ];
+
+  brokenOn = [
+    "ccl" # In REPLACE-USING: Shouldn't assign to variable I
+  ];
+
+  tests = {
+    name = "str-test";
+    srcs = [ (src + "/test/test-str.lisp") ];
+    deps = [
+      {
+        sbcl = depot.nix.buildLisp.bundled "uiop";
+        default = depot.nix.buildLisp.bundled "asdf";
+      }
+      depot.third_party.lisp.prove
+      depot.third_party.lisp.fiveam
+    ];
+
+    expression = ''
+      (fiveam:run! 'str::test-str)
+    '';
+  };
+}
diff --git a/third_party/napalm/default.nix b/third_party/napalm/default.nix
new file mode 100644
index 0000000000..e85c360ba9
--- /dev/null
+++ b/third_party/napalm/default.nix
@@ -0,0 +1,7 @@
+{ depot, pkgs, ... }:
+
+pkgs.callPackage depot.third_party.sources.napalm { } // {
+  meta.ci.targets = [
+    "napalm-registry"
+  ];
+}
diff --git a/third_party/nixpkgs/default.nix b/third_party/nixpkgs/default.nix
index dc3a59d435..b79a963e5c 100644
--- a/third_party/nixpkgs/default.nix
+++ b/third_party/nixpkgs/default.nix
@@ -11,7 +11,11 @@
 { depot ? { }
 , externalArgs ? { }
 , depotOverlays ? true
-, localSystem ? builtins.currentSystem
+, localSystem ? externalArgs.localSystem or builtins.currentSystem
+, crossSystem ? externalArgs.crossSystem or localSystem
+  # additional overlays to be applied.
+  # Useful when calling this file in a view exported from depot.
+, additionalOverlays ? [ ]
 , ...
 }:
 
@@ -24,10 +28,14 @@ let
       (if externalArgs ? nixpkgsConfig then externalArgs.nixpkgsConfig else { })
       // {
         allowUnfree = true;
+        allowUnfreeRedistributable = true;
         allowBroken = true;
+        # Forbids our meta.ci attribute
+        # https://github.com/NixOS/nixpkgs/pull/191171#issuecomment-1260650771
+        checkMeta = false;
       };
 
-    inherit localSystem;
+    inherit localSystem crossSystem;
   };
 
   # import the nixos-unstable package set, or optionally use the
@@ -44,16 +52,7 @@ let
   # Overlay for packages that should come from the stable channel
   # instead (e.g. because something is broken in unstable).
   # Use `stableNixpkgs` from above.
-  stableOverlay = _unstableSelf: _unstableSuper: {
-    inherit (stableNixpkgs)
-      # bat syntaxes changed with syntect 5.0, but cheddar is still on 4.x
-      # TODO(tazjin): upgrade cheddar to syntect 5.0
-      bat
-
-      # ntfy does not build on unstable as of 2022-08-02
-      ntfy
-      ;
-  };
+  stableOverlay = _unstableSelf: unstableSuper: { };
 
   # Overlay to expose the nixpkgs commits we are using to other Nix code.
   commitsOverlay = _: _: {
@@ -69,10 +68,9 @@ import nixpkgsSrc (commonNixpkgsArgs // {
     stableOverlay
   ] ++ (if depotOverlays then [
     depot.third_party.overlays.haskell
-    depot.third_party.overlays.emacs
     depot.third_party.overlays.tvl
     depot.third_party.overlays.ecl-static
     depot.third_party.overlays.dhall
     (import depot.third_party.sources.rust-overlay)
-  ] else [ ]);
+  ] else [ ] ++ additionalOverlays);
 })
diff --git a/third_party/overlays/dhall/OWNERS b/third_party/overlays/dhall/OWNERS
index a742d0d22b..a640227914 100644
--- a/third_party/overlays/dhall/OWNERS
+++ b/third_party/overlays/dhall/OWNERS
@@ -1,3 +1 @@
-inherited: true
-owners:
-  - Profpatsch
+Profpatsch
diff --git a/third_party/overlays/dhall/default.nix b/third_party/overlays/dhall/default.nix
index d944905e8d..4625035999 100644
--- a/third_party/overlays/dhall/default.nix
+++ b/third_party/overlays/dhall/default.nix
@@ -3,15 +3,6 @@
 self: super:
 
 let
-  dhall-source = subdir: pkg: super.haskell.lib.overrideSrc pkg {
-    src = "${super.fetchFromGitHub {
-      owner = "Profpatsch";
-      repo = "dhall-haskell";
-      # https://github.com/dhall-lang/dhall-haskell/pull/2426
-      rev = "82123817316192d39f9a3e68b8ce9c9cff0a48ed";
-      sha256 = "sha256-gbHoUKIdLPIttqeV471jsT8OJz6uiI6LpHOwtLbBGHY=";
-    }}/${subdir}";
-  };
 
   # binary releases of dhall tools, since the build in nixpkgs is
   # broken most of the time. The binaries are also fully static
@@ -25,13 +16,8 @@ let
       { pkgs = self; };
 in
 {
-  # TODO: this is to fix a bug in dhall-nix
-  haskellPackages = super.haskellPackages.override {
-    overrides = hsSelf: hsSuper: {
-      dhall = dhall-source "dhall" hsSuper.dhall;
-      dhall-nix = dhall-source "dhall-nix" hsSuper.dhall-nix;
-    };
-  };
+  # ATTN: see the haskell overlay for some overrides we need.
+
   # dhall = easy-dhall-nix.dhall-simple;
   # dhall-nix = easy-dhall-nix.dhall-nix-simple;
   dhall-bash = easy-dhall-nix.dhall-bash-simple;
diff --git a/third_party/overlays/ecl-static.nix b/third_party/overlays/ecl-static.nix
index 66579c33ab..d81075bdee 100644
--- a/third_party/overlays/ecl-static.nix
+++ b/third_party/overlays/ecl-static.nix
@@ -20,15 +20,6 @@ self: super:
   ecl-static = (super.pkgsMusl.ecl.override {
     inherit (self.pkgsStatic) gmp libffi boehmgc;
   }).overrideAttrs (drv: rec {
-    # version must not be changed as it indicates where to find the bundled libs,
-    # using ecl HEAD is necessary for us since it includes multiple fixes to do
-    # with bytecode compilation and allows to concatenate fasc files again.
-    src = self.fetchFromGitLab {
-      owner = "embeddable-common-lisp";
-      repo = "ecl";
-      rev = "1c989247c1b0bf1d38a76aec30b9ca5e41afe1e3";
-      sha256 = "0bzjqw6m1kk5z5b81yizic347k931msp5lf78x65dcw3fqfwv3xn";
-    };
     configureFlags = drv.configureFlags ++ [
       "--disable-shared"
       "--with-dffi=no" # will fail at runtime anyways if statically linked
diff --git a/third_party/overlays/emacs.nix b/third_party/overlays/emacs.nix
deleted file mode 100644
index 341feb5015..0000000000
--- a/third_party/overlays/emacs.nix
+++ /dev/null
@@ -1,4 +0,0 @@
-# Emacs overlay from https://github.com/nix-community/emacs-overlay
-{ depot, ... }:
-
-import depot.third_party.sources.emacs-overlay
diff --git a/third_party/overlays/haskell/OWNERS b/third_party/overlays/haskell/OWNERS
new file mode 100644
index 0000000000..5f87d2f271
--- /dev/null
+++ b/third_party/overlays/haskell/OWNERS
@@ -0,0 +1,2 @@
+Profpatsch
+sterni
diff --git a/third_party/overlays/haskell/default.nix b/third_party/overlays/haskell/default.nix
index 0ed0196a28..dc1201ec43 100644
--- a/third_party/overlays/haskell/default.nix
+++ b/third_party/overlays/haskell/default.nix
@@ -7,25 +7,69 @@
 self: super: # overlay parameters for the nixpkgs overlay
 
 let
-  overrides = hsSelf: hsSuper: with self.haskell.lib.compose; {
-    # No overrides for the default package set necessary at the moment
-  };
+  haskellLib = self.haskell.lib.compose;
 in
 {
   haskellPackages = super.haskellPackages.override {
-    inherit overrides;
+    overrides = hsSelf: hsSuper: {
+      punycode = haskellLib.appendPatch
+        (self.fetchpatch {
+          name = "punycode-mtl-2.3.patch";
+          url = "https://github.com/litherum/punycode/pull/5/commits/41e55c8b7cef14563e6d04a7190dbabff5a77886.patch";
+          sha256 = "03kgmy4z36jv16ffp5jrig2gr8ydc8cl1iscc7difisaq88mxvqc";
+        })
+        hsSuper.punycode;
+
+      # Build with deprecated ansi-wl-pprint is broken now, use HEAD which switched to
+      # prettyprinter
+      tmp-postgres = haskellLib.overrideSrc
+        {
+          version = "unstable-2023-08-08";
+          src = self.fetchFromGitHub {
+            owner = "jfischoff";
+            repo = "tmp-postgres";
+            rev = "7f2467a6d6d5f6db7eed59919a6773fe006cf22b";
+            sha256 = "0l1gdx5s8ximgawd3yzfy47pv5pgwqmjqp8hx5rbrq68vr04wkbl";
+          };
+        }
+        (hsSuper.tmp-postgres.override {
+          ansi-wl-pprint = hsSelf.prettyprinter;
+        });
+
+      ihp-hsx = lib.pipe hsSuper.ihp-hsx [
+        (haskellLib.overrideSrc {
+          version = "unstable-2023-03-28";
+          src = "${self.fetchFromGitHub {
+            owner = "digitallyinduced";
+            repo = "ihp";
+            rev = "ab4ecd05f4e7b6b3c4b74b82d39fc6c5cc48766b";
+            sha256 = "1fj5q9lygnmvqqv2fwqdj12sv63gkdfv5ha6fi190sv07dp9n9an";
+          }}/ihp-hsx";
+        })
+        haskellLib.doJailbreak
+      ];
+
+      pa-prelude = hsSelf.callPackage ./extra-pkgs/pa-prelude.nix { };
+      pa-error-tree = hsSelf.callPackage ./extra-pkgs/pa-error-tree-0.1.0.0.nix { };
+      pa-field-parser = hsSelf.callPackage ./extra-pkgs/pa-field-parser.nix { };
+      pa-label = hsSelf.callPackage ./extra-pkgs/pa-label.nix { };
+      pa-pretty = hsSelf.callPackage ./extra-pkgs/pa-pretty-0.1.1.0.nix { };
+      pa-json = hsSelf.callPackage ./extra-pkgs/pa-json.nix { };
+      pa-run-command = hsSelf.callPackage ./extra-pkgs/pa-run-command-0.1.0.0.nix { };
+    };
   };
 
   haskell = lib.recursiveUpdate super.haskell {
     packages.ghc8107 = super.haskell.packages.ghc8107.override {
-      overrides = lib.composeExtensions overrides (
-        hsSelf: hsSuper: with self.haskell.lib.compose; {
-          # TODO(sterni): TODO(grfn): patch xanthous to work with random-fu 0.3.*,
-          # so we can use GHC 9.0.2 and benefit from upstream binary cache.
-          random-fu = hsSelf.callPackage ./extra-pkgs/random-fu-0.2.nix { };
-          rvar = hsSelf.callPackage ./extra-pkgs/rvar-0.2.nix { };
-        }
-      );
+      overrides = hsSelf: hsSuper: {
+        # TODO(sterni): TODO(grfn): patch xanthous to work with random-fu 0.3.*,
+        # so we can use GHC 9.0.2 and benefit from upstream binary cache.
+        random-fu = hsSelf.callPackage ./extra-pkgs/random-fu-0.2.nix { };
+        rvar = hsSelf.callPackage ./extra-pkgs/rvar-0.2.nix { };
+
+        # TODO(grfn): port to brick 1.4 (EventM gains an additional type argument in 1.0)
+        brick = hsSelf.callPackage ./extra-pkgs/brick-0.73.nix { };
+      };
     };
   };
 }
diff --git a/third_party/overlays/haskell/extra-pkgs/brick-0.73.nix b/third_party/overlays/haskell/extra-pkgs/brick-0.73.nix
new file mode 100644
index 0000000000..c5e2883c75
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/brick-0.73.nix
@@ -0,0 +1,70 @@
+{ mkDerivation
+, base
+, bytestring
+, config-ini
+, containers
+, contravariant
+, data-clist
+, deepseq
+, directory
+, dlist
+, exceptions
+, filepath
+, lib
+, microlens
+, microlens-mtl
+, microlens-th
+, QuickCheck
+, stm
+, template-haskell
+, text
+, text-zipper
+, transformers
+, unix
+, vector
+, vty
+, word-wrap
+}:
+mkDerivation {
+  pname = "brick";
+  version = "0.73";
+  sha256 = "741c8d0717f0ab5addd5d3acc88cb36d645a0c73907bde509b2fd9d9bc02039c";
+  isLibrary = true;
+  isExecutable = true;
+  libraryHaskellDepends = [
+    base
+    bytestring
+    config-ini
+    containers
+    contravariant
+    data-clist
+    deepseq
+    directory
+    dlist
+    exceptions
+    filepath
+    microlens
+    microlens-mtl
+    microlens-th
+    stm
+    template-haskell
+    text
+    text-zipper
+    transformers
+    unix
+    vector
+    vty
+    word-wrap
+  ];
+  testHaskellDepends = [
+    base
+    containers
+    microlens
+    QuickCheck
+    vector
+    vty
+  ];
+  homepage = "https://github.com/jtdaugherty/brick/";
+  description = "A declarative terminal user interface library";
+  license = lib.licenses.bsd3;
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/pa-error-tree-0.1.0.0.nix b/third_party/overlays/haskell/extra-pkgs/pa-error-tree-0.1.0.0.nix
new file mode 100644
index 0000000000..a38cd4efaa
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/pa-error-tree-0.1.0.0.nix
@@ -0,0 +1,10 @@
+{ mkDerivation, base, containers, lib, pa-prelude }:
+mkDerivation {
+  pname = "pa-error-tree";
+  version = "0.1.0.0";
+  sha256 = "f82d3d905e8d9f0d31c81f31c424b9a95c65a8925517ccac92134f410cf8d639";
+  libraryHaskellDepends = [ base containers pa-prelude ];
+  homepage = "https://github.com/possehl-analytics/pa-hackage";
+  description = "Collect a tree of errors and pretty-print";
+  license = lib.licenses.bsd3;
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/pa-field-parser.nix b/third_party/overlays/haskell/extra-pkgs/pa-field-parser.nix
new file mode 100644
index 0000000000..a3c146ee09
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/pa-field-parser.nix
@@ -0,0 +1,39 @@
+{ mkDerivation
+, aeson
+, aeson-better-errors
+, attoparsec
+, base
+, case-insensitive
+, containers
+, lib
+, pa-error-tree
+, pa-prelude
+, scientific
+, semigroupoids
+, template-haskell
+, text
+, time
+}:
+mkDerivation {
+  pname = "pa-field-parser";
+  version = "0.3.0.0";
+  sha256 = "528c2b6bf5ad6454861b059c7eb6924f4c32bcb5b8faa4c2389d9ddfd92fcd57";
+  libraryHaskellDepends = [
+    aeson
+    aeson-better-errors
+    attoparsec
+    base
+    case-insensitive
+    containers
+    pa-error-tree
+    pa-prelude
+    scientific
+    semigroupoids
+    template-haskell
+    text
+    time
+  ];
+  homepage = "https://github.com/possehl-analytics/pa-hackage";
+  description = "โ€œVerticalโ€ parsing of values";
+  license = lib.licenses.bsd3;
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/pa-json.nix b/third_party/overlays/haskell/extra-pkgs/pa-json.nix
new file mode 100644
index 0000000000..8ce838b22c
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/pa-json.nix
@@ -0,0 +1,43 @@
+{ mkDerivation
+, aeson
+, aeson-better-errors
+, aeson-pretty
+, base
+, base64-bytestring
+, bytestring
+, containers
+, lib
+, pa-error-tree
+, pa-field-parser
+, pa-label
+, pa-prelude
+, scientific
+, text
+, time
+, vector
+}:
+mkDerivation {
+  pname = "pa-json";
+  version = "0.3.0.0";
+  sha256 = "45e79765e57e21400f3f3b1e86094473fac61d298618d7e34f6cad4988d8923b";
+  libraryHaskellDepends = [
+    aeson
+    aeson-better-errors
+    aeson-pretty
+    base
+    base64-bytestring
+    bytestring
+    containers
+    pa-error-tree
+    pa-field-parser
+    pa-label
+    pa-prelude
+    scientific
+    text
+    time
+    vector
+  ];
+  homepage = "https://github.com/possehl-analytics/pa-hackage";
+  description = "Our JSON parsers/encoders";
+  license = lib.licenses.bsd3;
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/pa-label.nix b/third_party/overlays/haskell/extra-pkgs/pa-label.nix
new file mode 100644
index 0000000000..7cfa257c81
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/pa-label.nix
@@ -0,0 +1,10 @@
+{ mkDerivation, base, lib }:
+mkDerivation {
+  pname = "pa-label";
+  version = "0.1.1.0";
+  sha256 = "b40183900c045641c0632ed8e53a326c0c0e9c2806568613c03b3131d9016183";
+  libraryHaskellDepends = [ base ];
+  homepage = "https://github.com/possehl-analytics/pa-hackage";
+  description = "Labels, and labelled tuples and enums (GHC >9.2)";
+  license = lib.licenses.bsd3;
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/pa-prelude.nix b/third_party/overlays/haskell/extra-pkgs/pa-prelude.nix
new file mode 100644
index 0000000000..17e1996ab6
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/pa-prelude.nix
@@ -0,0 +1,43 @@
+{ mkDerivation
+, base
+, bytestring
+, containers
+, error
+, exceptions
+, lib
+, mtl
+, profunctors
+, PyF
+, scientific
+, semigroupoids
+, template-haskell
+, text
+, these
+, validation-selective
+, vector
+}:
+mkDerivation {
+  pname = "pa-prelude";
+  version = "0.2.0.0";
+  sha256 = "68015f7c19e9c618fc04e2516baccfce52af24efb9ca1480162c9ea0aef7f301";
+  libraryHaskellDepends = [
+    base
+    bytestring
+    containers
+    error
+    exceptions
+    mtl
+    profunctors
+    PyF
+    scientific
+    semigroupoids
+    template-haskell
+    text
+    these
+    validation-selective
+    vector
+  ];
+  homepage = "https://github.com/possehl-analytics/pa-hackage";
+  description = "The Possehl Analytics Prelude";
+  license = lib.licenses.bsd3;
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/pa-pretty-0.1.1.0.nix b/third_party/overlays/haskell/extra-pkgs/pa-pretty-0.1.1.0.nix
new file mode 100644
index 0000000000..d6dadef849
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/pa-pretty-0.1.1.0.nix
@@ -0,0 +1,29 @@
+{ mkDerivation
+, aeson
+, aeson-pretty
+, ansi-terminal
+, base
+, hscolour
+, lib
+, nicify-lib
+, pa-prelude
+, text
+}:
+mkDerivation {
+  pname = "pa-pretty";
+  version = "0.1.1.0";
+  sha256 = "da925a7cf2ac49c5769d7ebd08c2599b537efe45b3d506bf4d7c8673633ef6c9";
+  libraryHaskellDepends = [
+    aeson
+    aeson-pretty
+    ansi-terminal
+    base
+    hscolour
+    nicify-lib
+    pa-prelude
+    text
+  ];
+  homepage = "https://github.com/possehl-analytics/pa-hackage";
+  description = "Some pretty-printing helpers";
+  license = lib.licenses.bsd3;
+}
diff --git a/third_party/overlays/haskell/extra-pkgs/pa-run-command-0.1.0.0.nix b/third_party/overlays/haskell/extra-pkgs/pa-run-command-0.1.0.0.nix
new file mode 100644
index 0000000000..b12eb5efbf
--- /dev/null
+++ b/third_party/overlays/haskell/extra-pkgs/pa-run-command-0.1.0.0.nix
@@ -0,0 +1,25 @@
+{ mkDerivation
+, base
+, bytestring
+, lib
+, monad-logger
+, pa-prelude
+, text
+, typed-process
+}:
+mkDerivation {
+  pname = "pa-run-command";
+  version = "0.1.0.0";
+  sha256 = "37837e0cddedc9b615063f0357115739c53b5dcb8af82ce86a95a3a5c88c29a3";
+  libraryHaskellDepends = [
+    base
+    bytestring
+    monad-logger
+    pa-prelude
+    text
+    typed-process
+  ];
+  homepage = "https://github.com/possehl-analytics/pa-hackage";
+  description = "Helper functions for spawning subprocesses";
+  license = lib.licenses.bsd3;
+}
diff --git a/third_party/overlays/patches/.skip-tree b/third_party/overlays/patches/.skip-tree
new file mode 100644
index 0000000000..86eae51a6d
--- /dev/null
+++ b/third_party/overlays/patches/.skip-tree
@@ -0,0 +1 @@
+No readTree-compatible files.
diff --git a/third_party/overlays/patches/0001-configure-ac-version.patch b/third_party/overlays/patches/0001-configure-ac-version.patch
new file mode 100644
index 0000000000..fa2575cb93
--- /dev/null
+++ b/third_party/overlays/patches/0001-configure-ac-version.patch
@@ -0,0 +1,13 @@
+diff --git a/configure.ac b/configure.ac
+index e861e42..018c19c 100644
+--- a/configure.ac
++++ b/configure.ac
+@@ -26,7 +26,7 @@
+ #;**********************************************************************;
+ 
+ AC_INIT([tpm2-pkcs11],
+-  [m4_esyscmd_s([git describe --tags --always --dirty])],
++  [git-@VERSION@],
+   [https://github.com/tpm2-software/tpm2-pkcs11/issues],
+   [],
+   [https://github.com/tpm2-software/tpm2-pkcs11])
diff --git a/third_party/overlays/patches/buf-tests-dont-use-file-transport.patch b/third_party/overlays/patches/buf-tests-dont-use-file-transport.patch
new file mode 100644
index 0000000000..34be80eb36
--- /dev/null
+++ b/third_party/overlays/patches/buf-tests-dont-use-file-transport.patch
@@ -0,0 +1,64 @@
+commit e9219b88de5ed37af337ee2d2e71e7ec7c0aad1b
+Author: Robbert van Ginkel <rvanginkel@buf.build>
+Date:   Thu Oct 20 16:43:28 2022 -0400
+
+    Fix git unit test by using fake git server rather than file:// (#1518)
+    
+    More recent versions of git fix a CVE by disabling some usage of the
+    `file://` transport, see
+    https://github.blog/2022-10-18-git-security-vulnerabilities-announced/#cve-2022-39253.
+    We were using this transport in tests.
+    
+    Instead, use https://git-scm.com/docs/git-http-backend to serve up this
+    repository locally so we don't have to use the file protocol. This
+    should be a more accurate tests, since we mostly expect submodules to
+    come from servers.
+
+diff --git a/.golangci.yml b/.golangci.yml
+index 318d1171..865e03e7 100644
+--- a/.golangci.yml
++++ b/.golangci.yml
+@@ -136,3 +136,8 @@ issues:
+     - linters:
+         - containedctx
+       path: private/bufpkg/bufmodule/bufmoduleprotocompile
++      # We should be able to use net/http/cgi in a unit test, in addition the CVE mentions only versions of go < 1.6.3 are affected.
++    - linters:
++        - gosec
++      path: private/pkg/git/git_test.go
++      text: "G504:"
+diff --git a/private/pkg/git/git_test.go b/private/pkg/git/git_test.go
+index 7b77b6cd..7132054e 100644
+--- a/private/pkg/git/git_test.go
++++ b/private/pkg/git/git_test.go
+@@ -17,6 +17,8 @@ package git
+ import (
+ 	"context"
+ 	"errors"
++	"net/http/cgi"
++	"net/http/httptest"
+ 	"os"
+ 	"os/exec"
+ 	"path/filepath"
+@@ -213,6 +215,21 @@ func createGitDirs(
+ 	runCommand(ctx, t, container, runner, "git", "-C", submodulePath, "add", "test.proto")
+ 	runCommand(ctx, t, container, runner, "git", "-C", submodulePath, "commit", "-m", "commit 0")
+ 
++	gitExecPath, err := command.RunStdout(ctx, container, runner, "git", "--exec-path")
++	require.NoError(t, err)
++	t.Log(filepath.Join(string(gitExecPath), "git-http-backend"))
++	// https://git-scm.com/docs/git-http-backend#_description
++	f, err := os.Create(filepath.Join(submodulePath, ".git", "git-daemon-export-ok"))
++	require.NoError(t, err)
++	require.NoError(t, f.Close())
++	server := httptest.NewServer(&cgi.Handler{
++		Path: filepath.Join(strings.TrimSpace(string(gitExecPath)), "git-http-backend"),
++		Dir:  submodulePath,
++		Env:  []string{"GIT_PROJECT_ROOT=" + submodulePath},
++	})
++	t.Cleanup(server.Close)
++	submodulePath = server.URL
++
+ 	originPath := filepath.Join(tmpDir, "origin")
+ 	require.NoError(t, os.MkdirAll(originPath, 0777))
+ 	runCommand(ctx, t, container, runner, "git", "-C", originPath, "init")
diff --git a/third_party/overlays/patches/cbtemulator-uds.patch b/third_party/overlays/patches/cbtemulator-uds.patch
new file mode 100644
index 0000000000..a19255306f
--- /dev/null
+++ b/third_party/overlays/patches/cbtemulator-uds.patch
@@ -0,0 +1,140 @@
+commit 1397e10225d8c6fd079a86fccd58fb5d0f4200bc
+Author: Florian Klink <flokli@flokli.de>
+Date:   Fri Mar 29 10:06:34 2024 +0100
+
+    feat(bigtable/emulator): allow listening on Unix Domain Sockets
+    
+    cbtemulator listening on unix domain sockets is much easier than trying
+    to allocate free TCP ports, especially if many cbtemulators are run at
+    the same time in integration tests.
+    
+    This adds an additional flag, address, which has priority if it's set,
+    rather than host:port.
+    
+    `NewServer` already takes a `laddr string`, so we simply check for it to
+    contain slashes, and if so, listen on unix, rather than TCP.
+
+diff --git a/bigtable/bttest/inmem.go b/bigtable/bttest/inmem.go
+index 556abc2a85..33e4bf2667 100644
+--- a/bttest/inmem.go
++++ b/bttest/inmem.go
+@@ -40,6 +40,7 @@ import (
+ 	"math"
+ 	"math/rand"
+ 	"net"
++	"os"
+ 	"regexp"
+ 	"sort"
+ 	"strings"
+@@ -106,7 +107,15 @@ type server struct {
+ // The Server will be listening for gRPC connections, without TLS,
+ // on the provided address. The resolved address is named by the Addr field.
+ func NewServer(laddr string, opt ...grpc.ServerOption) (*Server, error) {
+-	l, err := net.Listen("tcp", laddr)
++	var l net.Listener
++	var err error
++
++	// If the address contains slashes, listen on a unix domain socket instead.
++	if strings.Contains(laddr, "/") {
++		l, err = net.Listen("unix", laddr)
++	} else {
++		l, err = net.Listen("tcp", laddr)
++	}
+ 	if err != nil {
+ 		return nil, err
+ 	}
+diff --git a/bigtable/cmd/emulator/cbtemulator.go b/bigtable/cmd/emulator/cbtemulator.go
+index 144c09ffb1..deaf69b717 100644
+--- a/cmd/emulator/cbtemulator.go
++++ b/cmd/emulator/cbtemulator.go
+@@ -27,8 +27,9 @@ import (
+ )
+ 
+ var (
+-	host = flag.String("host", "localhost", "the address to bind to on the local machine")
+-	port = flag.Int("port", 9000, "the port number to bind to on the local machine")
++	host    = flag.String("host", "localhost", "the address to bind to on the local machine")
++	port    = flag.Int("port", 9000, "the port number to bind to on the local machine")
++	address = flag.String("address", "", "address:port number or unix socket path to listen on. Has priority over host/port")
+ )
+ 
+ const (
+@@ -42,7 +43,15 @@ func main() {
+ 		grpc.MaxRecvMsgSize(maxMsgSize),
+ 		grpc.MaxSendMsgSize(maxMsgSize),
+ 	}
+-	srv, err := bttest.NewServer(fmt.Sprintf("%s:%d", *host, *port), opts...)
++
++	var laddr string
++	if *address != "" {
++		laddr = *address
++	} else {
++		laddr = fmt.Sprintf("%s:%d", *host, *port)
++	}
++
++	srv, err := bttest.NewServer(laddr, opts...)
+ 	if err != nil {
+ 		log.Fatalf("failed to start emulator: %v", err)
+ 	}
+commit ce16f843d6c93159d86b3807c6d9ff66e43aac67
+Author: Florian Klink <flokli@flokli.de>
+Date:   Fri Mar 29 11:53:15 2024 +0100
+
+    feat(bigtable): clean up unix socket on close
+    
+    Call srv.Close when receiving an interrupt, and delete the unix domain
+    socket in that function.
+
+diff --git a/bigtable/bttest/inmem.go b/bigtable/bttest/inmem.go
+index 33e4bf2667..0dc96024b1 100644
+--- a/bttest/inmem.go
++++ b/bttest/inmem.go
+@@ -148,6 +148,11 @@ func (s *Server) Close() {
+ 
+ 	s.srv.Stop()
+ 	s.l.Close()
++
++	// clean up unix socket
++	if strings.Contains(s.Addr, "/") {
++		_ = os.Remove(s.Addr)
++	}
+ }
+ 
+ func (s *server) CreateTable(ctx context.Context, req *btapb.CreateTableRequest) (*btapb.Table, error) {
+diff --git a/bigtable/cmd/emulator/cbtemulator.go b/bigtable/cmd/emulator/cbtemulator.go
+index deaf69b717..5a9e8f7a8c 100644
+--- a/cmd/emulator/cbtemulator.go
++++ b/cmd/emulator/cbtemulator.go
+@@ -18,9 +18,12 @@ cbtemulator launches the in-memory Cloud Bigtable server on the given address.
+ package main
+ 
+ import (
++	"context"
+ 	"flag"
+ 	"fmt"
+ 	"log"
++	"os"
++	"os/signal"
+ 
+ 	"cloud.google.com/go/bigtable/bttest"
+ 	"google.golang.org/grpc"
+@@ -51,11 +54,18 @@ func main() {
+ 		laddr = fmt.Sprintf("%s:%d", *host, *port)
+ 	}
+ 
++	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
++	defer stop()
++
+ 	srv, err := bttest.NewServer(laddr, opts...)
+ 	if err != nil {
+ 		log.Fatalf("failed to start emulator: %v", err)
+ 	}
+ 
+ 	fmt.Printf("Cloud Bigtable emulator running on %s\n", srv.Addr)
+-	select {}
++	select {
++	case <-ctx.Done():
++		srv.Close()
++		stop()
++	}
+ }
diff --git a/third_party/overlays/patches/clickhouse-support-reading-arrow-LargeListArray.patch b/third_party/overlays/patches/clickhouse-support-reading-arrow-LargeListArray.patch
new file mode 100644
index 0000000000..9e79aa7267
--- /dev/null
+++ b/third_party/overlays/patches/clickhouse-support-reading-arrow-LargeListArray.patch
@@ -0,0 +1,106 @@
+From cdea2e8ad98995202ce81c9c030f2ae64d73b05a Mon Sep 17 00:00:00 2001
+From: edef <edef@edef.eu>
+Date: Mon, 30 Oct 2023 08:08:10 +0000
+Subject: [PATCH] Support reading arrow::LargeListArray
+
+---
+ .../Formats/Impl/ArrowColumnToCHColumn.cpp    | 33 +++++++++++++++----
+ 1 file changed, 26 insertions(+), 7 deletions(-)
+
+diff --git a/src/Processors/Formats/Impl/ArrowColumnToCHColumn.cpp b/src/Processors/Formats/Impl/ArrowColumnToCHColumn.cpp
+index 6f9d49498f2..b93846cd4eb 100644
+--- a/src/Processors/Formats/Impl/ArrowColumnToCHColumn.cpp
++++ b/src/Processors/Formats/Impl/ArrowColumnToCHColumn.cpp
+@@ -436,6 +436,22 @@ static ColumnPtr readByteMapFromArrowColumn(std::shared_ptr<arrow::ChunkedArray>
+     return nullmap_column;
+ }
+ 
++template <typename T>
++struct ArrowOffsetArray;
++
++template <>
++struct ArrowOffsetArray<arrow::ListArray>
++{
++    using type = arrow::Int32Array;
++};
++
++template <>
++struct ArrowOffsetArray<arrow::LargeListArray>
++{
++    using type = arrow::Int64Array;
++};
++
++template <typename ArrowListArray>
+ static ColumnPtr readOffsetsFromArrowListColumn(std::shared_ptr<arrow::ChunkedArray> & arrow_column)
+ {
+     auto offsets_column = ColumnUInt64::create();
+@@ -444,9 +460,9 @@ static ColumnPtr readOffsetsFromArrowListColumn(std::shared_ptr<arrow::ChunkedAr
+ 
+     for (int chunk_i = 0, num_chunks = arrow_column->num_chunks(); chunk_i < num_chunks; ++chunk_i)
+     {
+-        arrow::ListArray & list_chunk = dynamic_cast<arrow::ListArray &>(*(arrow_column->chunk(chunk_i)));
++        ArrowListArray & list_chunk = dynamic_cast<ArrowListArray &>(*(arrow_column->chunk(chunk_i)));
+         auto arrow_offsets_array = list_chunk.offsets();
+-        auto & arrow_offsets = dynamic_cast<arrow::Int32Array &>(*arrow_offsets_array);
++        auto & arrow_offsets = dynamic_cast<ArrowOffsetArray<ArrowListArray>::type &>(*arrow_offsets_array);
+ 
+         /*
+          * CH uses element size as "offsets", while arrow uses actual offsets as offsets.
+@@ -602,13 +618,14 @@ static ColumnPtr readColumnWithIndexesData(std::shared_ptr<arrow::ChunkedArray>
+     }
+ }
+ 
++template <typename ArrowListArray>
+ static std::shared_ptr<arrow::ChunkedArray> getNestedArrowColumn(std::shared_ptr<arrow::ChunkedArray> & arrow_column)
+ {
+     arrow::ArrayVector array_vector;
+     array_vector.reserve(arrow_column->num_chunks());
+     for (int chunk_i = 0, num_chunks = arrow_column->num_chunks(); chunk_i < num_chunks; ++chunk_i)
+     {
+-        arrow::ListArray & list_chunk = dynamic_cast<arrow::ListArray &>(*(arrow_column->chunk(chunk_i)));
++        ArrowListArray & list_chunk = dynamic_cast<ArrowListArray &>(*(arrow_column->chunk(chunk_i)));
+ 
+         /*
+          * It seems like arrow::ListArray::values() (nested column data) might or might not be shared across chunks.
+@@ -819,12 +836,12 @@ static ColumnWithTypeAndName readColumnFromArrowColumn(
+                     key_type_hint = map_type_hint->getKeyType();
+                 }
+             }
+-            auto arrow_nested_column = getNestedArrowColumn(arrow_column);
++            auto arrow_nested_column = getNestedArrowColumn<arrow::ListArray>(arrow_column);
+             auto nested_column = readColumnFromArrowColumn(arrow_nested_column, column_name, format_name, false, dictionary_infos, allow_null_type, skip_columns_with_unsupported_types, skipped, date_time_overflow_behavior, nested_type_hint, true);
+             if (skipped)
+                 return {};
+ 
+-            auto offsets_column = readOffsetsFromArrowListColumn(arrow_column);
++            auto offsets_column = readOffsetsFromArrowListColumn<arrow::ListArray>(arrow_column);
+ 
+             const auto * tuple_column = assert_cast<const ColumnTuple *>(nested_column.column.get());
+             const auto * tuple_type = assert_cast<const DataTypeTuple *>(nested_column.type.get());
+@@ -846,7 +863,9 @@ static ColumnWithTypeAndName readColumnFromArrowColumn(
+             return {std::move(map_column), std::move(map_type), column_name};
+         }
+         case arrow::Type::LIST:
++        case arrow::Type::LARGE_LIST:
+         {
++            bool is_large = arrow_column->type()->id() == arrow::Type::LARGE_LIST;
+             DataTypePtr nested_type_hint;
+             if (type_hint)
+             {
+@@ -854,11 +873,11 @@ static ColumnWithTypeAndName readColumnFromArrowColumn(
+                 if (array_type_hint)
+                     nested_type_hint = array_type_hint->getNestedType();
+             }
+-            auto arrow_nested_column = getNestedArrowColumn(arrow_column);
++            auto arrow_nested_column = is_large ? getNestedArrowColumn<arrow::LargeListArray>(arrow_column) : getNestedArrowColumn<arrow::ListArray>(arrow_column);
+             auto nested_column = readColumnFromArrowColumn(arrow_nested_column, column_name, format_name, false, dictionary_infos, allow_null_type, skip_columns_with_unsupported_types, skipped, date_time_overflow_behavior, nested_type_hint);
+             if (skipped)
+                 return {};
+-            auto offsets_column = readOffsetsFromArrowListColumn(arrow_column);
++            auto offsets_column = is_large ? readOffsetsFromArrowListColumn<arrow::LargeListArray>(arrow_column) : readOffsetsFromArrowListColumn<arrow::ListArray>(arrow_column);
+             auto array_column = ColumnArray::create(nested_column.column, offsets_column);
+             auto array_type = std::make_shared<DataTypeArray>(nested_column.type);
+             return {std::move(array_column), std::move(array_type), column_name};
+-- 
+2.42.0
+
diff --git a/third_party/overlays/patches/crate2nix-run-tests-in-build-source.patch b/third_party/overlays/patches/crate2nix-run-tests-in-build-source.patch
new file mode 100644
index 0000000000..52793270e6
--- /dev/null
+++ b/third_party/overlays/patches/crate2nix-run-tests-in-build-source.patch
@@ -0,0 +1,69 @@
+From 7cf084f73f7d15fe0538a625182fa7179c083b3d Mon Sep 17 00:00:00 2001
+From: Raito Bezarius <masterancpp@gmail.com>
+Date: Tue, 16 Jan 2024 02:10:48 +0100
+Subject: [PATCH] fix(template): run tests in `/build/source` instead `/build`
+
+Previously, the source tree was located inline in `/build` during tests, this was a mistake
+because the crates more than often are built in `/build/source` as per the `sourceRoot` system.
+
+This can cause issues with test binaries hardcoding `/build/source/...` as their choice for doing things,
+causing them to be confused in the test phase which is relocated without rewriting the paths inside test binaries.
+
+We fix that by relocating ourselves in the right hierarchy.
+
+This is a "simple" fix in the sense that more edge cases could exist but they are hard to reason about
+because they would be crates using custom `sourceRoot`, i.e. having `crate.sourceRoot` set and then it becomes
+a bit hard to reproduce the hierarchy, you need to analyze whether the path is absolute or relative,
+
+If it's relative, you can just reuse it and reproduce that specific hierarchy.
+If it's absolute, you need to cut the "absolute" meaningless part, e.g. `$NIX_BUILD_TOP/` and proceed like
+it's a relative path IMHO.
+---
+ crate2nix/Cargo.nix                                  | 10 ++++++++++
+ crate2nix/templates/nix/crate2nix/default.nix        | 10 ++++++++++
+
+diff --git a/Cargo.nix b/Cargo.nix
+index 6ef7a49..172ff34 100644
+--- a/Cargo.nix
++++ b/Cargo.nix
+@@ -2889,6 +2889,16 @@ rec {
+           # recreate a file hierarchy as when running tests with cargo
+ 
+           # the source for test data
++          # It's necessary to locate the source in $NIX_BUILD_TOP/source/
++          # instead of $NIX_BUILD_TOP/
++          # because we compiled those test binaries in the former and not the latter.
++          # So all paths will expect source tree to be there and not in the build top directly.
++          # For example: $NIX_BUILD_TOP := /build in general, if you ask yourself.
++          # TODO(raitobezarius): I believe there could be more edge cases if `crate.sourceRoot`
++          # do exist but it's very hard to reason about them, so let's wait until the first bug report.
++          mkdir -p source/
++          cd source/
++
+           ${pkgs.buildPackages.xorg.lndir}/bin/lndir ${crate.src}
+ 
+           # build outputs
+diff --git a/crate2nix/templates/nix/crate2nix/default.nix b/crate2nix/templates/nix/crate2nix/default.nix
+index e4fc2e9..dfb14c4 100644
+--- a/templates/nix/crate2nix/default.nix
++++ b/templates/nix/crate2nix/default.nix
+@@ -135,6 +135,16 @@ rec {
+           # recreate a file hierarchy as when running tests with cargo
+ 
+           # the source for test data
++          # It's necessary to locate the source in $NIX_BUILD_TOP/source/
++          # instead of $NIX_BUILD_TOP/
++          # because we compiled those test binaries in the former and not the latter.
++          # So all paths will expect source tree to be there and not in the build top directly.
++          # For example: $NIX_BUILD_TOP := /build in general, if you ask yourself.
++          # TODO(raitobezarius): I believe there could be more edge cases if `crate.sourceRoot`
++          # do exist but it's very hard to reason about them, so let's wait until the first bug report.
++          mkdir -p source/
++          cd source/
++
+           ${pkgs.buildPackages.xorg.lndir}/bin/lndir ${crate.src}
+ 
+           # build outputs
+-- 
+2.43.0
+
diff --git a/third_party/overlays/patches/crate2nix-tests-debug.patch b/third_party/overlays/patches/crate2nix-tests-debug.patch
new file mode 100644
index 0000000000..384178c805
--- /dev/null
+++ b/third_party/overlays/patches/crate2nix-tests-debug.patch
@@ -0,0 +1,12 @@
+diff --git a/templates/nix/crate2nix/default.nix b/templates/nix/crate2nix/default.nix
+index 4eefda8..d064118 100644
+--- a/templates/nix/crate2nix/default.nix
++++ b/templates/nix/crate2nix/default.nix
+@@ -111,6 +111,7 @@ rec {
+             (
+               _: {
+                 buildTests = true;
++                release = false;
+               }
+             );
+           # If the user hasn't set any pre/post commands, we don't want to
diff --git a/third_party/overlays/patches/evans-add-support-for-unix-domain-sockets.patch b/third_party/overlays/patches/evans-add-support-for-unix-domain-sockets.patch
new file mode 100644
index 0000000000..c66528f538
--- /dev/null
+++ b/third_party/overlays/patches/evans-add-support-for-unix-domain-sockets.patch
@@ -0,0 +1,39 @@
+From 55d7e7af7c56f678eb817059417241bb61ee5181 Mon Sep 17 00:00:00 2001
+From: Florian Klink <flokli@flokli.de>
+Date: Sun, 8 Oct 2023 11:00:27 +0200
+Subject: [PATCH] add support for unix domain sockets
+
+grpc.NewClient already supports connecting to unix domain sockets, and
+accepts a string anyways.
+
+As a quick fix, detect the `address` starting with `unix://` and don't
+add the port.
+
+In the long term, we might want to deprecate `host` and `port` cmdline
+args in favor of a single `address` arg.
+---
+ mode/common.go | 8 +++++++-
+ 1 file changed, 7 insertions(+), 1 deletion(-)
+
+diff --git a/mode/common.go b/mode/common.go
+index dfc7839..55f1e36 100644
+--- a/mode/common.go
++++ b/mode/common.go
+@@ -13,7 +13,13 @@ import (
+ )
+ 
+ func newGRPCClient(cfg *config.Config) (grpc.Client, error) {
+-	addr := fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port)
++	addr := cfg.Server.Host
++
++	// as long as the address doesn't start with unix, also add the port.
++	if !strings.HasPrefix(cfg.Server.Host, "unix://") {
++		addr = fmt.Sprintf("%s:%s", cfg.Server.Host, cfg.Server.Port)
++	}
++
+ 	if cfg.Request.Web {
+ 		//TODO: remove second arg
+ 		return grpc.NewWebClient(addr, cfg.Server.Reflection, false, "", "", "", grpc.Headers(cfg.Request.Header)), nil
+-- 
+2.42.0
+
diff --git a/third_party/overlays/patches/tpm2-pkcs11-190-dbupgrade.patch b/third_party/overlays/patches/tpm2-pkcs11-190-dbupgrade.patch
new file mode 100644
index 0000000000..f831c11a80
--- /dev/null
+++ b/third_party/overlays/patches/tpm2-pkcs11-190-dbupgrade.patch
@@ -0,0 +1,29 @@
+From 987323794148a6ff5ce3d02eef8cfeb46bee1761 Mon Sep 17 00:00:00 2001
+From: Anton <tracefinder@gmail.com>
+Date: Tue, 7 Nov 2023 12:02:15 +0300
+Subject: [PATCH] Skip null attribute during DB update
+
+Signed-off-by: Anton <tracefinder@gmail.com>
+---
+ src/lib/db.c | 8 +++++---
+ 1 file changed, 5 insertions(+), 3 deletions(-)
+
+diff --git a/src/lib/db.c b/src/lib/db.c
+index b4bbd1bf..74c5a7b4 100644
+--- a/src/lib/db.c
++++ b/src/lib/db.c
+@@ -2169,9 +2169,11 @@ static CK_RV dbup_handler_from_7_to_8(sqlite3 *updb) {
+ 
+         /* for each tobject */
+         CK_ATTRIBUTE_PTR a = attr_get_attribute_by_type(tobj->attrs, CKA_ALLOWED_MECHANISMS);
+-        CK_BYTE type = type_from_ptr(a->pValue, a->ulValueLen);
+-        if (type != TYPE_BYTE_INT_SEQ) {
+-            rv = _db_update_tobject_attrs(updb, tobj->id, tobj->attrs);
++        if (a) {
++            CK_BYTE type = type_from_ptr(a->pValue, a->ulValueLen);
++            if (type != TYPE_BYTE_INT_SEQ) {
++                rv = _db_update_tobject_attrs(updb, tobj->id, tobj->attrs);
++            }
+         }
+ 
+         tobject_free(tobj);
diff --git a/third_party/overlays/tvl.nix b/third_party/overlays/tvl.nix
index 9bb88dc2be..23f56e2f98 100644
--- a/third_party/overlays/tvl.nix
+++ b/third_party/overlays/tvl.nix
@@ -1,45 +1,46 @@
 # This overlay is used to make TVL-specific modifications in the
 # nixpkgs tree, where required.
-{ depot, ... }:
+{ lib, depot, localSystem, ... }:
 
 self: super:
-let
-  # Rollback Nix to a stable version (2.3) with backports for
-  # build-user problems applied.
-  nixSrc =
-    let
-      # branch 2.3-backport-await-users
-      rev = "4510dbc8a6802902cbab6444134659548fffb9b0";
-    in
-    self.fetchFromGitHub
-      {
-        owner = "tvlfyi";
-        repo = "nix";
-        inherit rev;
-        hash = "sha256:0vg2xzwc8q1sw20b26qbyd4flnws8668yhi1cg2h6z3jb3wamhr5";
-      } // { revCount = 0; shortRev = builtins.substring 0 7 rev; };
-in
-{
-  nix = (import "${nixSrc}/release.nix" {
-    nix = nixSrc;
-    nixpkgs = super.path;
-    systems = [ builtins.currentSystem ];
-  }).build."${builtins.currentSystem}";
-
-  clang-tools_11 = self.clang-tools.override {
-    llvmPackages = self.llvmPackages_11;
+depot.nix.readTree.drvTargets {
+  nix_2_3 = (super.nix_2_3.override {
+    # flaky tests, long painful build, see https://github.com/NixOS/nixpkgs/pull/266443
+    withAWS = false;
+  });
+  nix = self.nix_2_3 // {
+    # avoid duplicate pipeline step
+    meta = self.nix_2_3.meta or { } // {
+      ci = self.nix_2_3.meta.ci or { } // {
+        skip = true;
+      };
+    };
   };
+  nix_latest = super.nix.override ({
+    # flaky tests, long painful build, see https://github.com/NixOS/nixpkgs/pull/266443
+    withAWS = false;
+  });
 
-  # stdenv which uses clang, lld and libc++; full is a slight exaggeration,
-  # we for example don't use LLVM's libunwind
-  fullLlvm11Stdenv = self.overrideCC self.stdenv
-    (self.llvmPackages_11.libcxxStdenv.cc.override {
-      inherit (self.llvmPackages_11) bintools;
-    });
+  # To match telega in emacs-overlay or wherever
+  tdlib = super.tdlib.overrideAttrs (_: {
+    version = "1.8.24";
+    src = self.fetchFromGitHub {
+      owner = "tdlib";
+      repo = "td";
+      rev = "d79bd4b69403868897496da39b773ab25c69f6af";
+      sha256 = "0bc5akzw12qwj45rzqkrhw65qlrn9q8pzmvc5aiqv4bvhkb1ghl0";
+    };
+  });
+
+  home-manager = super.home-manager.overrideAttrs (_: {
+    src = depot.third_party.sources.home-manager;
+    version = "git-"
+      + builtins.substring 0 7 depot.third_party.sources.home-manager.rev;
+  });
 
   # Add our Emacs packages to the fixpoint
   emacsPackagesFor = emacs: (
-    (super.emacsPackagesFor emacs).overrideScope' (eself: esuper: {
+    (super.emacsPackagesFor emacs).overrideScope (eself: esuper: {
       tvlPackages = depot.tools.emacs-pkgs // depot.third_party.emacs;
 
       # Use the notmuch from nixpkgs instead of from the Emacs
@@ -47,9 +48,7 @@ in
       notmuch = super.notmuch.emacs;
 
       # Build EXWM with the depot sources instead.
-      exwm = esuper.exwm.overrideAttrs (_: {
-        src = depot.path.origSrc + "/third_party/exwm";
-      });
+      depotExwm = eself.callPackage depot.third_party.exwm.override { };
 
       # Workaround for magit checking the git version at load time
       magit = esuper.magit.overrideAttrs (_: {
@@ -57,21 +56,21 @@ in
           self.git
         ];
       });
-    })
-  );
 
-  # Upgrade to match telega in emacs-overlay
-  # TODO(tazjin): ugrade tdlib (+ telega?!) in nixpkgs
-  tdlib = assert super.tdlib.version == "1.8.3";
-    super.tdlib.overrideAttrs (old: {
-      version = "1.8.4";
-      src = self.fetchFromGitHub {
-        owner = "tdlib";
-        repo = "td";
-        rev = "7eabd8ca60de025e45e99d4e5edd39f4ebd9467e";
-        sha256 = "1chs0ibghjj275v9arsn3k68ppblpm7ysqk0za9kya5vdnldlld5";
+      # Pin xelb to a newer one until the new maintainers do a release.
+      xelb = eself.trivialBuild {
+        pname = "xelb";
+        version = "0.19-dev"; # invented version, last actual release was 0.18
+
+        src = self.fetchFromGitHub {
+          owner = "emacs-exwm";
+          repo = "xelb";
+          rev = "86089eba2de6c818bfa2fac075cb7ad876262798";
+          sha256 = "1mmlrd2zpcwiv8gh10y7lrpflnbmsycdascrxjr3bfcwa8yx7901";
+        };
       };
-    });
+    })
+  );
 
   # dottime support for notmuch
   notmuch = super.notmuch.overrideAttrs (old: {
@@ -82,14 +81,7 @@ in
 
   # nix-serve does not work with nix 2.4
   # https://github.com/edolstra/nix-serve/issues/28
-  nix-serve = super.nix-serve.override { nix = super.nix_2_3; };
-
-  # Workaround for srcOnly with separateDebugInfo until
-  # https://github.com/NixOS/nixpkgs/pull/179170 is merged.
-  srcOnly = args: (super.srcOnly args).overrideAttrs (_: {
-    outputs = [ "out" ];
-    separateDebugInfo = false;
-  });
+  nix-serve = super.nix-serve.override { nix = self.nix_2_3; };
 
   # Avoid builds of mkShell derivations in CI.
   mkShell = super.lib.makeOverridable (args: (super.mkShell args).overrideAttrs (_: {
@@ -98,25 +90,66 @@ in
     };
   }));
 
-  # upgrade home-manager until the service-generation fix has landed upstream
-  # https://github.com/nix-community/home-manager/issues/2846
-  home-manager = super.home-manager.overrideAttrs (old: rec {
-    version = assert super.home-manager.version == "2021-12-25"; "2022-04-08";
-    src = self.fetchFromGitHub {
-      owner = "nix-community";
-      repo = "home-manager";
-      rev = "f911ebbec927e8e9b582f2e32e2b35f730074cfc";
-      sha256 = "07qa2qkbjczj3d0m03jpw85hfj35cbjm48xhifz3viy4khjw88vl";
-    };
+  # https://github.com/googleapis/google-cloud-go/pull/9665
+  cbtemulator = super.cbtemulator.overrideAttrs (old: {
+    patches = old.patches or [ ] ++ [
+      ./patches/cbtemulator-uds.patch
+    ];
   });
 
-  python38 = super.python38.override {
-    packageOverrides = pySelf: pySuper: {
-      backports-zoneinfo = pySuper.backports-zoneinfo.overridePythonAttrs (_: {
-        # Outdated test-data, see https://github.com/pganssle/zoneinfo/pull/115
-        # Can be dropped when https://github.com/NixOS/nixpkgs/pull/170450 lands.
-        doCheck = false;
-      });
+  crate2nix = super.rustPlatform.buildRustPackage rec {
+    pname = "crate2nix";
+    version = "0.13.0";
+
+    src = super.fetchFromGitHub {
+      owner = "nix-community";
+      repo = "crate2nix";
+      rev = "ceb06eb7e76afb9e01a5f069aae136f97df72730";
+      hash = "sha256-JTMe8GViCQt51WUiaaoIPmWtwEeeYrl6pBxo2DNuKig=";
     };
+
+    patches = [
+      ./patches/crate2nix-tests-debug.patch
+      ./patches/crate2nix-run-tests-in-build-source.patch
+    ];
+
+    sourceRoot = "${src.name}/crate2nix";
+
+    cargoHash = "sha256-dhlSXY1CJE+JJt+6Y7W1MVMz36nwr6ny543py1TcjyY=";
+
+    nativeBuildInputs = [ super.makeWrapper ];
+
+    # Tests use nix(1), which tries (and fails) to set up /nix/var inside the
+    # sandbox
+    doCheck = false;
+
+    postFixup = ''
+      wrapProgram $out/bin/crate2nix \
+          --suffix PATH ":" ${lib.makeBinPath (with self; [ cargo nix_latest nix-prefetch-git ])}
+
+      rm -rf $out/lib $out/bin/crate2nix.d
+      mkdir -p \
+        $out/share/bash-completion/completions \
+        $out/share/zsh/vendor-completions
+      $out/bin/crate2nix completions -s 'bash' -o $out/share/bash-completion/completions
+      $out/bin/crate2nix completions -s 'zsh' -o $out/share/zsh/vendor-completions
+    '';
   };
+
+  evans = super.evans.overrideAttrs (old: {
+    patches = old.patches or [ ] ++ [
+      # add support for unix domain sockets
+      # https://github.com/ktr0731/evans/pull/680
+      ./patches/evans-add-support-for-unix-domain-sockets.patch
+    ];
+  });
+
+  # Imports a patch that fixes usage of this package on versions
+  # >=1.9. The patch has been proposed upstream, but so far with no
+  # reactions from the maintainer:
+  #
+  # https://github.com/tpm2-software/tpm2-pkcs11/pull/849
+  tpm2-pkcs11 = super.tpm2-pkcs11.overrideAttrs (old: {
+    patches = (old.patches or [ ]) ++ [ ./patches/tpm2-pkcs11-190-dbupgrade.patch ];
+  });
 }
diff --git a/third_party/prometheus-fail2ban-exporter/default.nix b/third_party/prometheus-fail2ban-exporter/default.nix
index 818839e48c..42ba0a14db 100644
--- a/third_party/prometheus-fail2ban-exporter/default.nix
+++ b/third_party/prometheus-fail2ban-exporter/default.nix
@@ -7,7 +7,7 @@ let
   };
 
   python = pkgs.python3.withPackages (p: [
-    p.prometheus_client
+    p.prometheus-client
   ]);
 
 in
diff --git a/third_party/public-inbox/0001-feat-always-set-the-List-ID-header-even-in-watch.patch b/third_party/public-inbox/0001-feat-always-set-the-List-ID-header-even-in-watch.patch
new file mode 100644
index 0000000000..bff2d4c200
--- /dev/null
+++ b/third_party/public-inbox/0001-feat-always-set-the-List-ID-header-even-in-watch.patch
@@ -0,0 +1,30 @@
+From 1719e904acf19499209b16a8a008f55390a7b5e2 Mon Sep 17 00:00:00 2001
+From: Vincent Ambo <mail@tazj.in>
+Date: Sun, 29 Jan 2023 13:36:12 +0300
+Subject: [PATCH] feat: always set the List-ID header even in -watch
+
+Without bothering to figure out exactly how this code path is usually
+triggered, always set a list ID when ingesting new emails in
+public-inbox-watch.
+---
+ lib/PublicInbox/Watch.pm | 4 ++++
+ 1 file changed, 4 insertions(+)
+
+diff --git a/lib/PublicInbox/Watch.pm b/lib/PublicInbox/Watch.pm
+index 3f6fe21..147971c 100644
+--- a/lib/PublicInbox/Watch.pm
++++ b/lib/PublicInbox/Watch.pm
+@@ -188,6 +188,10 @@ sub _remove_spam {
+ sub import_eml ($$$) {
+ 	my ($self, $ibx, $eml) = @_;
+ 
++        # TVL-specific: always set the list-id header, regardless of
++        # any of the other logic below.
++        PublicInbox::MDA->set_list_headers($eml, $ibx);
++
+ 	# any header match means it's eligible for the inbox:
+ 	if (my $watch_hdrs = $ibx->{-watchheaders}) {
+ 		my $ok;
+-- 
+2.39.0
+
diff --git a/third_party/public-inbox/default.nix b/third_party/public-inbox/default.nix
new file mode 100644
index 0000000000..1a4b196f94
--- /dev/null
+++ b/third_party/public-inbox/default.nix
@@ -0,0 +1,9 @@
+{ pkgs, ... }:
+
+pkgs.public-inbox.overrideAttrs (old: {
+  patches = (old.patches or [ ]) ++ [
+    ./0001-feat-always-set-the-List-ID-header-even-in-watch.patch
+  ];
+
+  doCheck = false; # too slow, and nixpkgs already runs them
+})
diff --git a/third_party/rust-crates/OWNERS b/third_party/rust-crates/OWNERS
index e978e46150..d2bb7704b6 100644
--- a/third_party/rust-crates/OWNERS
+++ b/third_party/rust-crates/OWNERS
@@ -1,5 +1,3 @@
-inherited: true
-owners:
-  - sterni
-  - Profpatsch
-  - zseri
+fogti
+sterni
+Profpatsch
diff --git a/third_party/rust-crates/default.nix b/third_party/rust-crates/default.nix
index 17e06a7049..697e47cdde 100644
--- a/third_party/rust-crates/default.nix
+++ b/third_party/rust-crates/default.nix
@@ -1,9 +1,11 @@
 { depot, pkgs, ... }:
 
 # TVL tool rust crate dependencies, where tools like carnix are not used.
-# Intended for manual updates, which keeps us honest with what we pull into our closure.
+# Intended for manual updates, which makes sure we never actually update.
 
 let
+  inherit (pkgs) fetchpatch;
+
   buildRustCrate =
     attrs@{ edition ? "2018"
     , pname
@@ -16,8 +18,7 @@ let
         ;
     });
 in
-
-rec {
+depot.nix.readTree.drvTargets rec{
   cfg-if = buildRustCrate {
     pname = "cfg-if";
     version = "1.0.0";
@@ -75,15 +76,15 @@ rec {
 
   libc = buildRustCrate {
     pname = "libc";
-    version = "0.2.82";
+    version = "0.2.153";
     edition = "2015";
-    sha256 = "02zgn6c0xwh331hky417lbr29kmvrw3ylxs8822syyhjfjqszvsx";
+    sha256 = "1xz1nz9k0vrv7lbir7ma0q4ii9cp3c0s9fbxp6268film2wrxs19";
   };
 
   bitflags = buildRustCrate {
     pname = "bitflags";
-    version = "1.2.1";
-    sha256 = "0b77awhpn7yaqjjibm69ginfn996azx5vkzfjj39g3wbsqs7mkxg";
+    version = "2.4.2";
+    sha256 = "1p370m8qh3clk33rqmyglcphlsq0gpf69j22d61fy4kkmrfn8hbd";
   };
 
   inotify-sys = buildRustCrate {
@@ -95,9 +96,18 @@ rec {
 
   inotify = buildRustCrate {
     pname = "inotify";
-    version = "0.9.2";
+    version = "0.10.2";
+    patches = [
+      # Unreleased compat patch for bitflags >= 2
+      (fetchpatch {
+        name = "inotify-bitflags-2.patch";
+        url = "https://github.com/hannobraun/inotify-rs/commit/f4765593894ef0b36d39739cf3349485ca88b1ce.patch";
+        sha256 = "107r9jai0jdr0hybsvbjyjn23vyk2lp1l1pmznb7jp38my0grh4b";
+        excludes = [ "Cargo.toml" ];
+      })
+    ];
     dependencies = [ bitflags libc inotify-sys ];
-    sha256 = "0fcknyvknglwwk1pdzdlb4m0ry2dym1yx8r5prf2v00pxnjk0hv2";
+    sha256 = "0lqwk7yf6bzc2jzj5iji2p3f29zdpllqd207vgg7jswmg2gqnlqc";
   };
 
   httparse = buildRustCrate {
@@ -108,7 +118,7 @@ rec {
   };
 
   version-check = buildRustCrate {
-    pname = "version-check";
+    pname = "version_check";
     version = "0.9.2";
     edition = "2015";
     sha256 = "1vwvc1mzwv8ana9jv8z933p2xzgj1533qwwl5zr8mi89azyhq21v";
@@ -168,11 +178,11 @@ rec {
 
   chrono = buildRustCrate {
     pname = "chrono";
-    version = "0.4.19";
-    edition = "2015";
+    version = "0.4.22";
+    edition = "2018";
     dependencies = [ num-traits num-integer ];
     features = [ "alloc" "std" ];
-    sha256 = "0cjf5dnfbk99607vz6n5r6bhwykcypq5psihvk845sxrhnzadsar";
+    sha256 = "01vbn93ba1q2afq10qis41j847damk5ifgn1all337mcscl345fn";
   };
 
   imap-proto = buildRustCrate {
@@ -205,9 +215,9 @@ rec {
 
   epoll = buildRustCrate {
     pname = "epoll";
-    version = "4.3.1";
+    version = "4.3.3";
     dependencies = [ bitflags libc ];
-    sha256 = "0dgmgdmrfbjkpxn1w3xmmwsm2a623a9qdwn90s8yl78n4a36kbh9";
+    sha256 = "1wc8dsd0dhqgskmkwd82fzqsy2hg0wm3833jxhzxkrwcip25yr3a";
   };
 
   serde = buildRustCrate {
@@ -279,14 +289,12 @@ rec {
   pkg-config = buildRustCrate {
     pname = "pkg-config";
     version = "0.3.19";
-    crateName = "pkg_config";
     sha256 = "1kd047p8jv6mhmfzddjvfa2nwkfrb3l1wml6lfm51n1cr06cc9lz";
   };
 
   libz-sys = buildRustCrate {
     pname = "libz-sys";
     version = "1.1.2";
-    crateName = "libz-sys";
     sha256 = "1y7v6bkwr4b6yaf951p1ns7mx47b29ziwdd5wziaic14gs1gwq30";
     buildDependencies = [
       cc
@@ -296,9 +304,8 @@ rec {
 
   libgit2-sys = buildRustCrate {
     pname = "libgit2-sys";
-    version = "0.12.26+1.3.0";
-    crateName = "libgit2_sys";
-    sha256 = "15zg0yy7lk7464yf9i1kxh4gaxdyb8m96ayb7vkjgmz1s2rgq7s2";
+    version = "0.16.2+1.7.2";
+    sha256 = "0bs446idbmg8s13jvb0ck6qmrskcdn2mp3d4mn9ggxbmiw4ryd3g";
     dependencies = [
       libc
       libz-sys
@@ -313,20 +320,19 @@ rec {
       cc
       pkg-config
     ];
+    env.LIBGIT2_NO_VENDOR = "1";
   };
 
   matches = buildRustCrate {
     pname = "matches";
     version = "0.1.8";
-    crateName = "matches";
     sha256 = "03hl636fg6xggy0a26200xs74amk3k9n0908rga2szn68agyz3cv";
     libPath = "lib.rs";
   };
 
   percent-encoding = buildRustCrate {
-    pname = "percent_encoding";
+    pname = "percent-encoding";
     version = "2.1.0";
-    crateName = "percent_encoding";
     sha256 = "0i838f2nr81585ckmfymf8l1x1vdmx6n8xqvli0lgcy60yl2axy3";
     libPath = "lib.rs";
   };
@@ -334,7 +340,6 @@ rec {
   form_urlencoded = buildRustCrate {
     pname = "form_urlencoded";
     version = "1.0.1";
-    crateName = "form_urlencoded";
     sha256 = "0rhv2hfrzk2smdh27walkm66zlvccnnwrbd47fmf8jh6m420dhj8";
     dependencies = [
       matches
@@ -345,14 +350,12 @@ rec {
   tinyvec_macros = buildRustCrate {
     pname = "tinyvec_macros";
     version = "0.1.0";
-    crateName = "tinyvec-macros";
     sha256 = "0aim73hyq5g8b2hs9gjq2sv0xm4xzfbwp5fdyg1frljqzkapq682";
   };
 
   tinyvec = buildRustCrate {
     pname = "tinyvec";
     version = "1.2.0";
-    crateName = "tinyvec";
     sha256 = "1c95nma20kiyrjwfsk7hzd5ir6yy4bm63fmfbfb4dm9ahnlvdp3y";
     features = [ "alloc" ];
     dependencies = [
@@ -363,7 +366,6 @@ rec {
   unicode-normalization = buildRustCrate {
     pname = "unicode-normalization";
     version = "0.1.17";
-    crateName = "unicode_normalization";
     sha256 = "0w4s0avzlf7pzcclhhih93aap613398sshm6jrxcwq0f9lhis11c";
     dependencies = [
       tinyvec
@@ -373,7 +375,6 @@ rec {
   unicode-bidi = buildRustCrate {
     pname = "unicode-bidi";
     version = "0.3.5";
-    crateName = "unicode_bidi";
     sha256 = "193jzlxj1dfcms2381lyd45zh4ywlicj9lzcfpid1zbkmfarymkz";
     dependencies = [
       matches
@@ -383,7 +384,6 @@ rec {
   idna = buildRustCrate {
     pname = "idna";
     version = "0.2.3";
-    crateName = "idna";
     sha256 = "0hwypd0fpym9lmd4bbqpwyr5lhrlvmvzhi1vy9asc5wxwkzrh299";
     dependencies = [
       matches
@@ -395,7 +395,6 @@ rec {
   url = buildRustCrate {
     pname = "url";
     version = "2.2.1";
-    crateName = "url";
     sha256 = "1ci1djafh83qhpzbmxnr9w5gcrjs3ghf8rrxdy4vklqyji6fvn5v";
     dependencies = [
       form_urlencoded
@@ -409,9 +408,8 @@ rec {
   git2 = buildRustCrate {
     pname = "git2";
     edition = "2018";
-    version = "0.13.25";
-    crateName = "git2";
-    sha256 = "181mw4kxsqrwpib9kf25fykc48wxhjla37vzis4j0b0w0yhyaqi3";
+    version = "0.18.1";
+    sha256 = "1d1wm8cn37svyxgvzfapwilkkc9d2x7fcrgciwn8b2pv9aqz102k";
     dependencies = [
       bitflags
       libc
diff --git a/third_party/smtprelay/default.nix b/third_party/smtprelay/default.nix
index b2d730a667..1a68245e92 100644
--- a/third_party/smtprelay/default.nix
+++ b/third_party/smtprelay/default.nix
@@ -4,7 +4,7 @@
 pkgs.buildGoModule rec {
   pname = "smtprelay";
   version = "1.7.0";
-  vendorSha256 = "00nb81hdg5pv5l0q7w5lv08dv4v72vml7jha351frani0gpg27pn";
+  vendorHash = "sha256:00nb81hdg5pv5l0q7w5lv08dv4v72vml7jha351frani0gpg27pn";
 
   src = pkgs.fetchFromGitHub {
     owner = "decke";
diff --git a/third_party/sources/sources.json b/third_party/sources/sources.json
index 875fedaa41..109451ff51 100644
--- a/third_party/sources/sources.json
+++ b/third_party/sources/sources.json
@@ -5,22 +5,22 @@
         "homepage": "https://matrix.to/#/#agenix:nixos.org",
         "owner": "ryantm",
         "repo": "agenix",
-        "rev": "7e5e58b98c3dcbf497543ff6f22591552ebfe65b",
-        "sha256": "1cfdd2ja56g8clllygf91il7dignr90ij1bl29g3kl7dl977dhl4",
+        "rev": "0.15.0",
+        "sha256": "01dhrghwa7zw93cybvx4gnrskqk97b004nfxgsys0736823956la",
         "type": "tarball",
-        "url": "https://github.com/ryantm/agenix/archive/7e5e58b98c3dcbf497543ff6f22591552ebfe65b.tar.gz",
+        "url": "https://github.com/ryantm/agenix/archive/0.15.0.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     },
-    "emacs-overlay": {
+    "home-manager": {
         "branch": "master",
-        "description": "Bleeding edge emacs overlay [maintainer=@adisbladis] ",
-        "homepage": "",
+        "description": "Manage a user environment using Nix  [maintainer=@rycee] ",
+        "homepage": "https://nix-community.github.io/home-manager/",
         "owner": "nix-community",
-        "repo": "emacs-overlay",
-        "rev": "d73ef414f3b07c4e675e9582c4f181580853e53b",
-        "sha256": "0vk902q7vyss206pzkfi2ii91cfdhs0nmp3n0m40yixlq956a8nb",
+        "repo": "home-manager",
+        "rev": "c1609d584a6b5e9e6a02010f51bd368cb4782f8e",
+        "sha256": "112r86p3iah1xahwlp82yd3gvh10wkf271za5h7v3jsqv08c6gkr",
         "type": "tarball",
-        "url": "https://github.com/nix-community/emacs-overlay/archive/d73ef414f3b07c4e675e9582c4f181580853e53b.tar.gz",
+        "url": "https://github.com/nix-community/home-manager/archive/c1609d584a6b5e9e6a02010f51bd368cb4782f8e.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     },
     "impermanence": {
@@ -29,10 +29,10 @@
         "homepage": "",
         "owner": "nix-community",
         "repo": "impermanence",
-        "rev": "2f39baeb7d039fda5fc8225111bb79474138e6f4",
-        "sha256": "0x7mwbqj1h3rym93hy1knxd33dzspmy5i7y1k930vg85yp3a1y8q",
+        "rev": "a33ef102a02ce77d3e39c25197664b7a636f9c30",
+        "sha256": "1mig6ns8l5iynsm6pfbnx2b9hmr592s1kqbw6gq1n25czdlcniam",
         "type": "tarball",
-        "url": "https://github.com/nix-community/impermanence/archive/2f39baeb7d039fda5fc8225111bb79474138e6f4.tar.gz",
+        "url": "https://github.com/nix-community/impermanence/archive/a33ef102a02ce77d3e39c25197664b7a636f9c30.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     },
     "naersk": {
@@ -41,10 +41,22 @@
         "homepage": "",
         "owner": "nmattia",
         "repo": "naersk",
-        "rev": "c6a45e4277fa58abd524681466d3450f896dc094",
-        "sha256": "1wm5gvlp3jdbg4ycg6wp7dzxhk0ik91pk7smxz78wqlghi4h121d",
+        "rev": "c5037590290c6c7dae2e42e7da1e247e54ed2d49",
+        "sha256": "1ql5ziwfrpmc8cxhgflmdy2z06z4dsdfzjwb2vv9bag6a2chrvq8",
+        "type": "tarball",
+        "url": "https://github.com/nmattia/naersk/archive/c5037590290c6c7dae2e42e7da1e247e54ed2d49.tar.gz",
+        "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
+    },
+    "napalm": {
+        "branch": "master",
+        "description": "Support for building npm packages in Nix and lightweight npm registry [maintainer @nmattia]",
+        "homepage": "",
+        "owner": "nix-community",
+        "repo": "napalm",
+        "rev": "edcb26c266ca37c9521f6a97f33234633cbec186",
+        "sha256": "0ai1ax380nnpz0mbgbc5vdzafyjilcmdj7kgv087x2vagpprb4yy",
         "type": "tarball",
-        "url": "https://github.com/nmattia/naersk/archive/c6a45e4277fa58abd524681466d3450f896dc094.tar.gz",
+        "url": "https://github.com/nix-community/napalm/archive/edcb26c266ca37c9521f6a97f33234633cbec186.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     },
     "nixpkgs": {
@@ -53,22 +65,22 @@
         "homepage": "",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "c4a0efdd5a728e20791b8d8d2f26f90ac228ee8d",
-        "sha256": "0rg066r8hx882hlhi4yvz6d8nyww7cqbjknyrsk0w44jj2jzaidg",
+        "rev": "7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856",
+        "sha256": "0ijqx995jw9i16f28whyjdll9b0nydmyl4n91bci2cgryxms7f8f",
         "type": "tarball",
-        "url": "https://github.com/NixOS/nixpkgs/archive/c4a0efdd5a728e20791b8d8d2f26f90ac228ee8d.tar.gz",
+        "url": "https://github.com/NixOS/nixpkgs/archive/7bb2ccd8cdc44c91edba16c48d2c8f331fb3d856.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     },
     "nixpkgs-stable": {
-        "branch": "nixos-21.11",
+        "branch": "nixos-23.11",
         "description": "Nix Packages collection",
         "homepage": "",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "eabc38219184cc3e04a974fe31857d8e0eac098d",
-        "sha256": "04ffwp2gzq0hhz7siskw6qh9ys8ragp7285vi1zh8xjksxn1msc5",
+        "rev": "dd37924974b9202f8226ed5d74a252a9785aedf8",
+        "sha256": "1nxd4dqci8rs94a7cypx30axgj778p2wydkx16q298n29crkflbw",
         "type": "tarball",
-        "url": "https://github.com/NixOS/nixpkgs/archive/eabc38219184cc3e04a974fe31857d8e0eac098d.tar.gz",
+        "url": "https://github.com/NixOS/nixpkgs/archive/dd37924974b9202f8226ed5d74a252a9785aedf8.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     },
     "rust-overlay": {
@@ -77,10 +89,10 @@
         "homepage": "",
         "owner": "oxalica",
         "repo": "rust-overlay",
-        "rev": "18354cce8137aaef0d505d6f677e9bbdd542020d",
-        "sha256": "1dwy1mlinqavgs99ilhwjq6p5dlra3a2s0mzqdnwwy04w2s9kzxs",
+        "rev": "2a42c742ab04b61d9b2f1edf392842cf9f27ebfd",
+        "sha256": "1wpkca75ysb2ssycc0dshd1m76q8iqhzrrbr6xmfmkkcj1p333nk",
         "type": "tarball",
-        "url": "https://github.com/oxalica/rust-overlay/archive/18354cce8137aaef0d505d6f677e9bbdd542020d.tar.gz",
+        "url": "https://github.com/oxalica/rust-overlay/archive/2a42c742ab04b61d9b2f1edf392842cf9f27ebfd.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     },
     "rustsec-advisory-db": {
@@ -89,10 +101,10 @@
         "homepage": "https://rustsec.org",
         "owner": "RustSec",
         "repo": "advisory-db",
-        "rev": "9739cb7f1ea9173c5ed46f4e52c988a33031dfa2",
-        "sha256": "02dafylgckvdagd19gff5yi8x50vpa158mvmjarh8mmwm46v8ryw",
+        "rev": "35e7459a331d3e0c585e56dabd03006b9b354088",
+        "sha256": "1j8c0vzwg6b9lxmdy2a40pvwsy2kncv455spbjbxsj10p2vmy5fl",
         "type": "tarball",
-        "url": "https://github.com/RustSec/advisory-db/archive/9739cb7f1ea9173c5ed46f4e52c988a33031dfa2.tar.gz",
+        "url": "https://github.com/RustSec/advisory-db/archive/35e7459a331d3e0c585e56dabd03006b9b354088.tar.gz",
         "url_template": "https://github.com/<owner>/<repo>/archive/<rev>.tar.gz"
     }
 }
diff --git a/third_party/terraform-provider-glesys/default.nix b/third_party/terraform-provider-glesys/default.nix
index 85a9fd6480..2eea7e1905 100644
--- a/third_party/terraform-provider-glesys/default.nix
+++ b/third_party/terraform-provider-glesys/default.nix
@@ -4,14 +4,15 @@
 { pkgs, ... }:
 
 pkgs.terraform-providers.mkProvider rec {
-  version = "0.3.2";
+  version = "0.9.0";
+  spdx = "MPL-2.0";
 
   owner = "glesys";
   repo = "terraform-provider-glesys";
   rev = "v${version}";
-  sha256 = "1hlqa4f9d44hq614ff8ivg8a6fwg48jwz11zsrlghjzky82cfraq";
+  hash = "sha256:0n2wb1gl0agc9agqlmhg4mh9kyfhw4zvrryyl8wfxlp1hkr0wz9y";
 
-  vendorSha256 = "0g5g69absf0vmin0ff0anrxcgfq0bzx4iz3qci90p9xkvyph4nlw";
+  vendorHash = "sha256:13wdx7q5rsyjrm6cn030m5hgcvx0m17dhr16wmbfv71pmsszfdjm";
 
   # This provider is not officially published in the TF registry, so
   # we're giving it a fake source here.