diff a/bot/build.gradle b/bot/build.gradle
--- a/bot/build.gradle
+++ b/bot/build.gradle
@@ -29,10 +29,11 @@
     }
 }
 
 dependencies {
     implementation project(':host')
+    implementation project(':network')
     implementation project(':vcs')
     implementation project(':json')
     implementation project(':census')
 }
 
diff a/bot/src/main/java/module-info.java b/bot/src/main/java/module-info.java
--- a/bot/src/main/java/module-info.java
+++ b/bot/src/main/java/module-info.java
@@ -22,10 +22,11 @@
  */
 module org.openjdk.skara.bot {
     requires transitive org.openjdk.skara.host;
     requires transitive org.openjdk.skara.json;
     requires transitive org.openjdk.skara.census;
+    requires org.openjdk.skara.network;
     requires org.openjdk.skara.vcs;
     requires java.logging;
     requires jdk.httpserver;
 
     exports org.openjdk.skara.bot;
diff a/bot/src/main/java/org/openjdk/skara/bot/BotRunnerConfiguration.java b/bot/src/main/java/org/openjdk/skara/bot/BotRunnerConfiguration.java
--- a/bot/src/main/java/org/openjdk/skara/bot/BotRunnerConfiguration.java
+++ b/bot/src/main/java/org/openjdk/skara/bot/BotRunnerConfiguration.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.bot;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.json.JSONObject;
 import org.openjdk.skara.vcs.VCS;
 
 import java.io.*;
 import java.net.URI;
@@ -36,23 +36,25 @@
 import java.util.regex.Pattern;
 
 public class BotRunnerConfiguration {
     private final Logger log;
     private final JSONObject config;
-    private final Map<String, Host> hosts;
+    private final Map<String, RepositoryHost> repositoryHosts;
+    private final Map<String, IssueHost> issueHosts;
     private final Map<String, HostedRepository> repositories;
 
     private BotRunnerConfiguration(JSONObject config, Path cwd) throws ConfigurationError {
         this.config = config;
         log = Logger.getLogger("org.openjdk.skara.bot");
 
-        hosts = parseHosts(config, cwd);
+        repositoryHosts = parseRepositoryHosts(config, cwd);
+        issueHosts = parseIssueHosts(config, cwd);
         repositories = parseRepositories(config);
     }
 
-    private Map<String, Host> parseHosts(JSONObject config, Path cwd) throws ConfigurationError {
-        Map<String, Host> ret = new HashMap<>();
+    private Map<String, RepositoryHost> parseRepositoryHosts(JSONObject config, Path cwd) throws ConfigurationError {
+        Map<String, RepositoryHost> ret = new HashMap<>();
 
         if (!config.contains("hosts")) {
             return ret;
         }
 
@@ -84,11 +86,27 @@
                                                                        github.get("app").get("installation").asString()));
                 } else {
                     var pat = new PersonalAccessToken(github.get("username").asString(), github.get("pat").asString());
                     ret.put(entry.name(), HostFactory.createGitHubHost(uri, pat));
                 }
-            } else if (entry.value().contains("jira")) {
+            } else {
+                throw new ConfigurationError("Host " + entry.name());
+            }
+        }
+
+        return ret;
+    }
+
+    private Map<String, IssueHost> parseIssueHosts(JSONObject config, Path cwd) throws ConfigurationError {
+        Map<String, IssueHost> ret = new HashMap<>();
+
+        if (!config.contains("hosts")) {
+            return ret;
+        }
+
+        for (var entry : config.get("hosts").fields()) {
+            if (entry.value().contains("jira")) {
                 var jira = entry.value().get("jira");
                 var uri = URIBuilder.base(jira.get("url").asString()).build();
                 ret.put(entry.name(), HostFactory.createJiraHost(uri, null));
             } else {
                 throw new ConfigurationError("Host " + entry.name());
@@ -105,15 +123,15 @@
             return ret;
         }
 
         for (var entry : config.get("repositories").fields()) {
             var hostName = entry.value().get("host").asString();
-            if (!hosts.containsKey(hostName)) {
+            if (!repositoryHosts.containsKey(hostName)) {
                 throw new ConfigurationError("Repository " + entry.name() + " uses undefined host '" + hostName + "'");
             }
-            var host = hosts.get(hostName);
-            var repo = host.getRepository(entry.value().get("repository").asString());
+            var host = repositoryHosts.get(hostName);
+            var repo = host.repository(entry.value().get("repository").asString());
             ret.put(entry.name(), repo);
         }
 
         return ret;
     }
@@ -131,40 +149,40 @@
             entry = entry.substring(0, refSeparatorIndex);
         }
         var hostSeparatorIndex = entry.indexOf('/');
         if (hostSeparatorIndex >= 0) {
             var hostName = entry.substring(0, hostSeparatorIndex);
-            var host = hosts.get(hostName);
-            if (!hosts.containsKey(hostName)) {
+            var host = repositoryHosts.get(hostName);
+            if (!repositoryHosts.containsKey(hostName)) {
                 throw new ConfigurationError("Repository entry " + entry + " uses undefined host '" + hostName + "'");
             }
             var repositoryName = entry.substring(hostSeparatorIndex + 1);
-            ret.repository = host.getRepository(repositoryName);
+            ret.repository = host.repository(repositoryName);
         } else {
             if (!repositories.containsKey(entry)) {
                 throw new ConfigurationError("Repository " + entry + " is not defined!");
             }
             ret.repository = repositories.get(entry);
         }
 
         if (ret.ref == null) {
-            ret.ref = ret.repository.getRepositoryType() == VCS.GIT ? "master" : "default";
+            ret.ref = ret.repository.repositoryType() == VCS.GIT ? "master" : "default";
         }
 
         return ret;
     }
 
     private IssueProject parseIssueProjectEntry(String entry) throws ConfigurationError {
         var hostSeparatorIndex = entry.indexOf('/');
         if (hostSeparatorIndex >= 0) {
             var hostName = entry.substring(0, hostSeparatorIndex);
-            var host = hosts.get(hostName);
-            if (!hosts.containsKey(hostName)) {
+            var host = issueHosts.get(hostName);
+            if (!issueHosts.containsKey(hostName)) {
                 throw new ConfigurationError("Issue project entry " + entry + " uses undefined host '" + hostName + "'");
             }
             var issueProjectName = entry.substring(hostSeparatorIndex + 1);
-            return host.getIssueProject(issueProjectName);
+            return host.project(issueProjectName);
         } else {
             throw new ConfigurationError("Malformed issue project entry");
         }
     }
 
diff a/bots/cli/build.gradle b/bots/cli/build.gradle
--- a/bots/cli/build.gradle
+++ b/bots/cli/build.gradle
@@ -47,10 +47,11 @@
     implementation project(':bots:submit')
     implementation project(':bots:forward')
     implementation project(':vcs')
     implementation project(':jcheck')
     implementation project(':host')
+    implementation project(':network')
     implementation project(':bot')
     implementation project(':census')
     implementation project(':json')
     implementation project(':ini')
     implementation project(':process')
diff a/bots/cli/src/main/java/module-info.java b/bots/cli/src/main/java/module-info.java
--- a/bots/cli/src/main/java/module-info.java
+++ b/bots/cli/src/main/java/module-info.java
@@ -28,10 +28,11 @@
     requires org.openjdk.skara.census;
     requires org.openjdk.skara.json;
     requires org.openjdk.skara.args;
     requires org.openjdk.skara.process;
     requires org.openjdk.skara.proxy;
+    requires org.openjdk.skara.network;
 
     requires java.sql;
 
     exports org.openjdk.skara.bots.cli;
 }
diff a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java
--- a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java
+++ b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLauncher.java
@@ -22,11 +22,11 @@
  */
 package org.openjdk.skara.bots.cli;
 
 import org.openjdk.skara.args.*;
 import org.openjdk.skara.bot.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.json.*;
 import org.openjdk.skara.proxy.HttpProxy;
 
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
diff a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java
--- a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java
+++ b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotLogstashHandler.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.bots.cli;
 
 import org.openjdk.skara.bot.BotTaskAggregationHandler;
-import org.openjdk.skara.host.network.RestRequest;
+import org.openjdk.skara.network.RestRequest;
 import org.openjdk.skara.json.JSON;
 
 import java.io.*;
 import java.net.URI;
 import java.time.*;
diff a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotSlackHandler.java b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotSlackHandler.java
--- a/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotSlackHandler.java
+++ b/bots/cli/src/main/java/org/openjdk/skara/bots/cli/BotSlackHandler.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.bots.cli;
 
 import org.openjdk.skara.bot.BotTaskAggregationHandler;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.JSON;
 
 import java.net.URI;
 import java.time.*;
 import java.util.*;
diff a/bots/cli/src/test/java/org/openjdk/skara/bots/cli/RestReceiver.java b/bots/cli/src/test/java/org/openjdk/skara/bots/cli/RestReceiver.java
--- a/bots/cli/src/test/java/org/openjdk/skara/bots/cli/RestReceiver.java
+++ b/bots/cli/src/test/java/org/openjdk/skara/bots/cli/RestReceiver.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.bots.cli;
 
 import com.sun.net.httpserver.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.json.*;
 
 import java.io.*;
 import java.net.*;
 import java.nio.charset.StandardCharsets;
diff a/bots/forward/src/main/java/org/openjdk/skara/bots/forward/ForwardBot.java b/bots/forward/src/main/java/org/openjdk/skara/bots/forward/ForwardBot.java
--- a/bots/forward/src/main/java/org/openjdk/skara/bots/forward/ForwardBot.java
+++ b/bots/forward/src/main/java/org/openjdk/skara/bots/forward/ForwardBot.java
@@ -59,46 +59,46 @@
     public boolean concurrentWith(WorkItem other) {
         if (!(other instanceof ForwardBot)) {
             return true;
         }
         var otherBot = (ForwardBot) other;
-        return !toHostedRepo.getName().equals(otherBot.toHostedRepo.getName());
+        return !toHostedRepo.name().equals(otherBot.toHostedRepo.name());
     }
 
     @Override
     public void run(Path scratchPath) {
         try {
             var sanitizedUrl =
-                URLEncoder.encode(toHostedRepo.getWebUrl().toString(), StandardCharsets.UTF_8);
+                URLEncoder.encode(toHostedRepo.webUrl().toString(), StandardCharsets.UTF_8);
             var toDir = storage.resolve(sanitizedUrl);
             Repository toLocalRepo = null;
             if (!Files.exists(toDir)) {
-                log.info("Cloning " + toHostedRepo.getName());
+                log.info("Cloning " + toHostedRepo.name());
                 Files.createDirectories(toDir);
-                toLocalRepo = Repository.clone(toHostedRepo.getUrl(), toDir, true);
+                toLocalRepo = Repository.clone(toHostedRepo.url(), toDir, true);
             } else {
-                log.info("Found existing scratch directory for " + toHostedRepo.getName());
+                log.info("Found existing scratch directory for " + toHostedRepo.name());
                 toLocalRepo = Repository.get(toDir).orElseThrow(() -> {
                         return new RuntimeException("Repository in " + toDir + " has vanished");
                 });
             }
 
-            log.info("Fetching " + fromHostedRepo.getName() + ":" + fromBranch.name() +
+            log.info("Fetching " + fromHostedRepo.name() + ":" + fromBranch.name() +
                      " to " + toBranch.name());
-            var fetchHead = toLocalRepo.fetch(fromHostedRepo.getUrl(),
+            var fetchHead = toLocalRepo.fetch(fromHostedRepo.url(),
                                               fromBranch.name() + ":" + toBranch.name());
-            log.info("Pushing " + toBranch.name() + " to " + toHostedRepo.getName());
-            toLocalRepo.push(fetchHead, toHostedRepo.getUrl(), toBranch.name(), false);
+            log.info("Pushing " + toBranch.name() + " to " + toHostedRepo.name());
+            toLocalRepo.push(fetchHead, toHostedRepo.url(), toBranch.name(), false);
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
 
     @Override
     public String toString() {
-        return "FowardBot@(" + fromHostedRepo.getName() + ":" + fromBranch.name() +
-                           "-> " + toHostedRepo.getName() + ":" + toBranch.name() + ")";
+        return "FowardBot@(" + fromHostedRepo.name() + ":" + fromBranch.name() +
+                           "-> " + toHostedRepo.name() + ":" + toBranch.name() + ")";
     }
 
     @Override
     public List<WorkItem> getPeriodicItems() {
         return List.of(this);
diff a/bots/forward/src/main/java/org/openjdk/skara/bots/forward/ForwardBotFactory.java b/bots/forward/src/main/java/org/openjdk/skara/bots/forward/ForwardBotFactory.java
--- a/bots/forward/src/main/java/org/openjdk/skara/bots/forward/ForwardBotFactory.java
+++ b/bots/forward/src/main/java/org/openjdk/skara/bots/forward/ForwardBotFactory.java
@@ -26,11 +26,10 @@
 import org.openjdk.skara.vcs.Branch;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.file.Files;
-import java.nio.file.Path;
 import java.util.*;
 import java.util.logging.Logger;
 
 public class ForwardBotFactory implements BotFactory {
     private final Logger log = Logger.getLogger("org.openjdk.skara.bots");;
@@ -61,12 +60,12 @@
             var toRepo = configuration.repository(to[0]);
             var toBranch = new Branch(to[1]);
 
             var bot = new ForwardBot(storage, fromRepo, fromBranch, toRepo, toBranch);
             log.info("Setting up forwarding from " +
-                     fromRepo.getName() + ":" + fromBranch.name() +
-                     "to " + toRepo.getName() + ":" + toBranch.name());
+                     fromRepo.name() + ":" + fromBranch.name() +
+                     "to " + toRepo.name() + ":" + toBranch.name());
             ret.add(bot);
         }
 
         return ret;
     }
diff a/bots/forward/src/test/java/org/openjdk/skara/bots/forward/ForwardBotTests.java b/bots/forward/src/test/java/org/openjdk/skara/bots/forward/ForwardBotTests.java
--- a/bots/forward/src/test/java/org/openjdk/skara/bots/forward/ForwardBotTests.java
+++ b/bots/forward/src/test/java/org/openjdk/skara/bots/forward/ForwardBotTests.java
@@ -39,11 +39,11 @@
     private static final Branch master = new Branch("master");
 
     @Test
     void mirrorMasterBranches(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -76,11 +76,11 @@
     }
 
     @Test
     void mirrorDifferentBranches(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
diff a/bots/hgbridge/build.gradle b/bots/hgbridge/build.gradle
--- a/bots/hgbridge/build.gradle
+++ b/bots/hgbridge/build.gradle
@@ -35,8 +35,9 @@
     implementation project(':vcs')
     implementation project(':host')
     implementation project(':census')
     implementation project(':process')
     implementation project(':json')
+    implementation project(':network')
 
     testImplementation project(':test')
 }
diff a/bots/hgbridge/src/main/java/module-info.java b/bots/hgbridge/src/main/java/module-info.java
--- a/bots/hgbridge/src/main/java/module-info.java
+++ b/bots/hgbridge/src/main/java/module-info.java
@@ -21,10 +21,11 @@
  * questions.
  */
 module org.openjdk.skara.bots.hgbridge {
     requires org.openjdk.skara.vcs;
     requires org.openjdk.skara.host;
+    requires org.openjdk.skara.network;
     requires org.openjdk.skara.census;
     requires org.openjdk.skara.bot;
     requires org.openjdk.skara.process;
     requires java.logging;
 
diff a/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/ExporterConfig.java b/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/ExporterConfig.java
--- a/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/ExporterConfig.java
+++ b/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/ExporterConfig.java
@@ -166,11 +166,11 @@
         }
         return ret;
     }
 
     public Converter resolve(Path scratchPath) throws IOException {
-        var localRepo = Repository.materialize(scratchPath, configurationRepo.getUrl(), configurationRef);
+        var localRepo = Repository.materialize(scratchPath, configurationRepo.url(), configurationRef);
 
         var replacements = parseMap(localRepo.root(), replacementsFile,
                                     field -> new Hash(field.name()),
                                     field -> field.value().stream()
                                                   .map(JSONValue::asString).collect(Collectors.toList()));
diff a/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/JBridgeBot.java b/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/JBridgeBot.java
--- a/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/JBridgeBot.java
+++ b/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/JBridgeBot.java
@@ -61,11 +61,11 @@
             return true;
         }
     }
 
     private void pushMarks(Path markSource, String destName, Path markScratchPath) throws IOException {
-        var marksRepo = Repository.materialize(markScratchPath, exporterConfig.marksRepo().getUrl(), exporterConfig.marksRef());
+        var marksRepo = Repository.materialize(markScratchPath, exporterConfig.marksRepo().url(), exporterConfig.marksRef());
 
         // We should never change existing marks
         var markDest = markScratchPath.resolve(destName);
         var updated = Files.readString(markSource);
         if (Files.exists(markDest)) {
@@ -85,11 +85,11 @@
         }
 
         Files.writeString(markDest, updated, StandardCharsets.UTF_8);
         marksRepo.add(markDest);
         var hash = marksRepo.commit("Updated marks", exporterConfig.marksAuthorName(), exporterConfig.marksAuthorEmail());
-        marksRepo.push(hash, exporterConfig.marksRepo().getUrl(), exporterConfig.marksRef());
+        marksRepo.push(hash, exporterConfig.marksRepo().url(), exporterConfig.marksRef());
     }
 
     @Override
     public void run(Path scratchPath) {
         log.fine("Running export for " + exporterConfig.source().toString());
@@ -116,25 +116,25 @@
                 }
             }
 
             IOException lastException = null;
             for (var destination : exporterConfig.destinations()) {
-                var markerBase = destination.getUrl().getHost() + "/" + destination.getName();
+                var markerBase = destination.url().getHost() + "/" + destination.name();
                 var successfulPushMarker = storage.resolve(URLEncoder.encode(markerBase, StandardCharsets.UTF_8) + ".success.txt");
                 if (exported.isPresent() || !successfulPushMarker.toFile().isFile()) {
                     var repo = exported.orElse(Exporter.current(storage).orElseThrow());
                     try {
                         Files.deleteIfExists(successfulPushMarker);
-                        repo.pushAll(destination.getUrl());
+                        repo.pushAll(destination.url());
                         storage.resolve(successfulPushMarker).toFile().createNewFile();
                     } catch (IOException e) {
-                        log.severe("Failed to push to " + destination.getUrl());
+                        log.severe("Failed to push to " + destination.url());
                         log.throwing("JBridgeBot", "run", e);
                         lastException = e;
                     }
                 } else {
-                    log.fine("No changes detected in " + exporterConfig.source() + " - skipping push to " + destination.getName());
+                    log.fine("No changes detected in " + exporterConfig.source() + " - skipping push to " + destination.name());
                 }
             }
             if (lastException != null) {
                 throw new UncheckedIOException(lastException);
             }
diff a/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/JBridgeBotFactory.java b/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/JBridgeBotFactory.java
--- a/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/JBridgeBotFactory.java
+++ b/bots/hgbridge/src/main/java/org/openjdk/skara/bots/hgbridge/JBridgeBotFactory.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.bots.hgbridge;
 
 import org.openjdk.skara.bot.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.json.*;
 
 import java.util.*;
 import java.util.stream.Collectors;
 
diff a/bots/hgbridge/src/test/java/org/openjdk/skara/bots/hgbridge/BridgeBotTests.java b/bots/hgbridge/src/test/java/org/openjdk/skara/bots/hgbridge/BridgeBotTests.java
--- a/bots/hgbridge/src/test/java/org/openjdk/skara/bots/hgbridge/BridgeBotTests.java
+++ b/bots/hgbridge/src/test/java/org/openjdk/skara/bots/hgbridge/BridgeBotTests.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.bots.hgbridge;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.process.Process;
 import org.openjdk.skara.test.*;
 import org.openjdk.skara.vcs.Tag;
 import org.openjdk.skara.vcs.*;
 import org.openjdk.skara.vcs.openjdk.convert.*;
@@ -65,11 +65,11 @@
 
         TestExporterConfig(URI source, HostedRepository destination, Path marksRepoPath) throws IOException {
             this.source(source);
             this.destinations(List.of(destination));
 
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
             var marksLocalRepo = Repository.init(marksRepoPath.resolve("marks.git"), VCS.GIT);
 
             var initialFile = marksLocalRepo.root().resolve("init.txt");
             if (!Files.exists(initialFile)) {
                 Files.writeString(initialFile, "Hello", StandardCharsets.UTF_8);
@@ -168,11 +168,11 @@
             var bridge = new JBridgeBot(config, storageFolder.path());
 
             runHgCommand(localHgRepo, "strip", "-r", "bd7a3ed1210f");
             TestBotRunner.runPeriodicItems(bridge);
 
-            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
 
             // Only a subset of known tags should be present
             var localGitTags = getTagNames(localGitRepo);
             assertEquals(getTagNames(localHgRepo), localGitTags);
             assertTrue(localGitTags.contains("jtreg4.1-b02"));
@@ -181,26 +181,26 @@
             // Import more revisions into the local hg repository and export again
             localHgRepo.fetch(source, "default");
             TestBotRunner.runPeriodicItems(bridge);
 
             // There should now be more tags present
-            Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
             localGitTags = getTagNames(localGitRepo);
             assertEquals(getTagNames(localHgRepo), localGitTags);
             assertTrue(localGitTags.contains("jtreg4.1-b02"));
             assertTrue(localGitTags.contains("jtreg4.1-b05"));
 
             // Export it again with different storage to force an export from scratch
             bridge = new JBridgeBot(config, storageFolder2.path());
             TestBotRunner.runPeriodicItems(bridge);
-            Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
             var newLocalGitTags = getTagNames(localGitRepo);
             assertEquals(localGitTags, newLocalGitTags);
 
             // Export it once more when nothing has changed
             TestBotRunner.runPeriodicItems(bridge);
-            Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
             newLocalGitTags = getTagNames(localGitRepo);
             assertEquals(localGitTags, newLocalGitTags);
         }
     }
 
@@ -216,11 +216,11 @@
             var config = new TestExporterConfig(source, destinationRepo, marksFolder.path());
             var bridge = new JBridgeBot(config, storageFolder.path());
             TestBotRunner.runPeriodicItems(bridge);
 
             // Materialize it and ensure that it contains a known commit
-            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
             var localGitCommits = getCommitHashes(localGitRepo);
             assertTrue(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
 
             // Now corrupt the .hg folder in the permanent storage
             Files.walk(storageFolder.path())
@@ -234,11 +234,11 @@
                      }
                  });
 
             // Now export it again - should still be intact
             TestBotRunner.runPeriodicItems(bridge);
-            Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
             localGitCommits = getCommitHashes(localGitRepo);
             assertTrue(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
         }
     }
 
@@ -261,11 +261,11 @@
             config = new TestExporterConfig(source, destinationRepo, marksFolder.path());
             var goodBridge = new JBridgeBot(config, storageFolder2.path());
             TestBotRunner.runPeriodicItems(goodBridge);
 
             // Verify that it now contains a known commit
-            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
             var localGitCommits = getCommitHashes(localGitRepo);
             assertTrue(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
         }
     }
 
@@ -284,29 +284,29 @@
             // Export an hg repository as is
             var bridge = new JBridgeBot(config, storageFolder.path());
             TestBotRunner.runPeriodicItems(bridge);
 
             // Materialize it and ensure that it contains a known commit
-            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
             var localGitCommits = getCommitHashes(localGitRepo);
             assertTrue(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
 
             // Push something else to overwrite it (but retain the lock)
-            var localRepo = CheckableRepository.init(gitFolder2.path(), destinationRepo.getRepositoryType());
+            var localRepo = CheckableRepository.init(gitFolder2.path(), destinationRepo.repositoryType());
             credentials.commitLock(localRepo);
-            localRepo.pushAll(destinationRepo.getUrl());
+            localRepo.pushAll(destinationRepo.url());
 
             // Materialize it again and ensure that the known commit is now gone
-            localGitRepo = Repository.materialize(gitFolder3.path(), destinationRepo.getUrl(), "master");
+            localGitRepo = Repository.materialize(gitFolder3.path(), destinationRepo.url(), "master");
             localGitCommits = getCommitHashes(localGitRepo);
             assertFalse(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
 
             // Now run the exporter again - nothing should happen
             TestBotRunner.runPeriodicItems(bridge);
 
             // Materialize it yet again and ensure that the known commit is still gone
-            localGitRepo = Repository.materialize(gitFolder4.path(), destinationRepo.getUrl(), "master");
+            localGitRepo = Repository.materialize(gitFolder4.path(), destinationRepo.url(), "master");
             localGitCommits = getCommitHashes(localGitRepo);
             assertFalse(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
         }
     }
 
@@ -326,28 +326,28 @@
             // Export an hg repository as is
             var bridge = new JBridgeBot(config, storageFolder.path());
             TestBotRunner.runPeriodicItems(bridge);
 
             // Materialize it and ensure that it contains a known commit
-            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.getUrl(), "master");
+            var localGitRepo = Repository.materialize(gitFolder.path(), destinationRepo.url(), "master");
             var localGitCommits = getCommitHashes(localGitRepo);
             assertTrue(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
 
             // Push something else to overwrite it
-            var localRepo = CheckableRepository.init(gitFolder2.path(), destinationRepo.getRepositoryType());
-            localRepo.pushAll(destinationRepo.getUrl());
+            var localRepo = CheckableRepository.init(gitFolder2.path(), destinationRepo.repositoryType());
+            localRepo.pushAll(destinationRepo.url());
 
             // Materialize it again and ensure that the known commit is now gone
-            localGitRepo = Repository.materialize(gitFolder3.path(), destinationRepo.getUrl(), "master");
+            localGitRepo = Repository.materialize(gitFolder3.path(), destinationRepo.url(), "master");
             localGitCommits = getCommitHashes(localGitRepo);
             assertFalse(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
 
             // Now run the exporter again - nothing should happen
             TestBotRunner.runPeriodicItems(bridge);
 
             // Materialize it yet again and ensure that the known commit is still gone
-            localGitRepo = Repository.materialize(gitFolder4.path(), destinationRepo.getUrl(), "master");
+            localGitRepo = Repository.materialize(gitFolder4.path(), destinationRepo.url(), "master");
             localGitCommits = getCommitHashes(localGitRepo);
             assertFalse(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
 
             // Remove the successful push markers
             Files.walk(storageFolder.path())
@@ -363,11 +363,11 @@
 
             // Now run the exporter again - it should do the push again
             TestBotRunner.runPeriodicItems(bridge);
 
             // Materialize it and ensure that the known commit is back
-            localGitRepo = Repository.materialize(gitFolder5.path(), destinationRepo.getUrl(), "master");
+            localGitRepo = Repository.materialize(gitFolder5.path(), destinationRepo.url(), "master");
             localGitCommits = getCommitHashes(localGitRepo);
             assertTrue(localGitCommits.contains("9cb6a5b843c0e9f6d45273a1a6f5c98979ab0766"));
         }
     }
 
diff a/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBot.java b/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBot.java
--- a/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBot.java
+++ b/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBot.java
@@ -57,56 +57,56 @@
     public boolean concurrentWith(WorkItem other) {
         if (!(other instanceof MergeBot)) {
             return true;
         }
         var otherBot = (MergeBot) other;
-        return !to.getName().equals(otherBot.to.getName());
+        return !to.name().equals(otherBot.to.name());
     }
 
     @Override
     public void run(Path scratchPath) {
         try {
             var sanitizedUrl =
-                URLEncoder.encode(to.getWebUrl().toString(), StandardCharsets.UTF_8);
+                URLEncoder.encode(to.webUrl().toString(), StandardCharsets.UTF_8);
             var dir = storage.resolve(sanitizedUrl);
             Repository repo = null;
             if (!Files.exists(dir)) {
-                log.info("Cloning " + to.getName());
+                log.info("Cloning " + to.name());
                 Files.createDirectories(dir);
-                repo = Repository.clone(to.getUrl(), dir);
+                repo = Repository.clone(to.url(), dir);
             } else {
-                log.info("Found existing scratch directory for " + to.getName());
+                log.info("Found existing scratch directory for " + to.name());
                 repo = Repository.get(dir).orElseThrow(() -> {
                         return new RuntimeException("Repository in " + dir + " has vanished");
                 });
             }
 
             repo.fetchAll();
             var originToBranch = new Branch("origin/" + toBranch.name());
 
             // Check if pull request already created
-            var title = "Cannot automatically merge " + from.getName() + ":" + fromBranch.name();
+            var title = "Cannot automatically merge " + from.name() + ":" + fromBranch.name();
             var marker = "<!-- MERGE CONFLICTS -->";
-            for (var pr : to.getPullRequests()) {
-                if (pr.getTitle().equals(title) &&
-                    pr.getBody().startsWith(marker) &&
-                    to.host().getCurrentUserDetails().equals(pr.getAuthor())) {
-                    var lines = pr.getBody().split("\n");
+            for (var pr : to.pullRequests()) {
+                if (pr.title().equals(title) &&
+                    pr.body().startsWith(marker) &&
+                    to.host().currentUser().equals(pr.author())) {
+                    var lines = pr.body().split("\n");
                     var head = new Hash(lines[1].substring(5, 45));
                     if (repo.contains(originToBranch, head)) {
-                        log.info("Closing resolved merge conflict PR " + pr.getId());
+                        log.info("Closing resolved merge conflict PR " + pr.id());
                         pr.addComment("Merge conflicts have been resolved, closing this PR");
                         pr.setState(PullRequest.State.CLOSED);
                     } else {
                         log.info("Outstanding unresolved merge already present");
                         return;
                     }
                 }
             }
 
-            log.info("Fetching " + from.getName() + ":" + fromBranch.name());
-            var fetchHead = repo.fetch(from.getUrl(), fromBranch.name());
+            log.info("Fetching " + from.name() + ":" + fromBranch.name());
+            var fetchHead = repo.fetch(from.url(), fromBranch.name());
             var head = repo.resolve(toBranch.name()).orElseThrow(() ->
                     new IOException("Could not resolve branch " + toBranch.name())
             );
             if (repo.contains(originToBranch, fetchHead)) {
                 log.info("Nothing to merge");
@@ -127,11 +127,11 @@
             if (error == null) {
                 log.info("Pushing successful merge");
                 if (!isAncestor) {
                     repo.commit("Merge", "duke", "duke@openjdk.org");
                 }
-                repo.push(toBranch, to.getUrl().toString(), false);
+                repo.push(toBranch, to.url().toString(), false);
             } else {
                 log.info("Got error: " + error.getMessage());
                 log.info("Aborting unsuccesful merge");
                 repo.abortMerge();
 
@@ -140,23 +140,23 @@
                 var commits = repo.commits(mergeBase.hex() + ".." + fetchHead.hex(), true).asList();
 
                 var message = new ArrayList<String>();
                 message.add(marker);
                 message.add("<!-- " + fetchHead.hex() + " -->");
-                message.add("The following commits from `" + from.getName() + ":" + fromBranch.name() +
+                message.add("The following commits from `" + from.name() + ":" + fromBranch.name() +
                             "` could *not* be automatically merged into `" + toBranch.name() + "`:");
                 message.add("");
                 for (var commit : commits) {
                     message.add("- " + commit.hash().abbreviate() + ": " + commit.message().get(0));
                 }
                 message.add("");
                 message.add("To manually resolve these merge conflicts, please create a personal fork of " +
-                            to.getWebUrl() + " and execute the following commands:");
+                            to.webUrl() + " and execute the following commands:");
                 message.add("");
                 message.add("```bash");
                 message.add("$ git checkout " + toBranch.name());
-                message.add("$ git pull " + from.getWebUrl() + " " + fromBranch.name());
+                message.add("$ git pull " + from.webUrl() + " " + fromBranch.name());
                 message.add("```");
                 message.add("");
                 message.add("When you have resolved the conflicts resulting from the above commands, run:");
                 message.add("");
                 message.add("```bash");
@@ -179,12 +179,12 @@
         }
     }
 
     @Override
     public String toString() {
-        return "MergeBot@(" + from.getName() + ":" + fromBranch.name() + "-> "
-                            + to.getName() + ":" + toBranch.name() + ")";
+        return "MergeBot@(" + from.name() + ":" + fromBranch.name() + "-> "
+                            + to.name() + ":" + toBranch.name() + ")";
     }
 
     @Override
     public List<WorkItem> getPeriodicItems() {
         return List.of(this);
diff a/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java b/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java
--- a/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java
+++ b/bots/merge/src/main/java/org/openjdk/skara/bots/merge/MergeBotFactory.java
@@ -54,12 +54,12 @@
             var fromBranch = new Branch(configuration.repositoryRef(repo.get("from").asString()));
 
             var toRepo = configuration.repository(repo.get("to").asString());
             var toBranch = new Branch(configuration.repositoryRef(repo.get("to").asString()));
 
-            log.info("Setting up merging from " + fromRepo.getName() + ":" + fromBranch.name() +
-                     " to " + toRepo.getName() + ":" + toBranch.name());
+            log.info("Setting up merging from " + fromRepo.name() + ":" + fromBranch.name() +
+                     " to " + toRepo.name() + ":" + toBranch.name());
             bots.add(new MergeBot(storage, fromRepo, fromBranch, toRepo, toBranch));
         }
         return bots;
     }
 }
diff a/bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java b/bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java
--- a/bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java
+++ b/bots/merge/src/test/java/org/openjdk/skara/bots/merge/MergeBotTests.java
@@ -40,11 +40,11 @@
 
 class MergeBotTests {
     @Test
     void mergeMasterBranch(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -100,18 +100,18 @@
             assertTrue(merge.isMerge());
             assertEquals(List.of("Merge"), merge.message());
             assertEquals("duke", merge.author().name());
             assertEquals("duke@openjdk.org", merge.author().email());
 
-            assertEquals(0, toHostedRepo.getPullRequests().size());
+            assertEquals(0, toHostedRepo.pullRequests().size());
         }
     }
 
     @Test
     void failingMergeTest(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -159,21 +159,21 @@
             assertEquals(2, toCommits.size());
             var toHashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
             assertTrue(toHashes.contains(toHashA));
             assertTrue(toHashes.contains(toHashB));
 
-            var pullRequests = toHostedRepo.getPullRequests();
+            var pullRequests = toHostedRepo.pullRequests();
             assertEquals(1, pullRequests.size());
             var pr = pullRequests.get(0);
-            assertEquals("Cannot automatically merge test:master", pr.getTitle());
+            assertEquals("Cannot automatically merge test:master", pr.title());
         }
     }
 
     @Test
     void failingMergeShouldResultInOnlyOnePR(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -222,21 +222,21 @@
             assertEquals(2, toCommits.size());
             var toHashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
             assertTrue(toHashes.contains(toHashA));
             assertTrue(toHashes.contains(toHashB));
 
-            var pullRequests = toHostedRepo.getPullRequests();
+            var pullRequests = toHostedRepo.pullRequests();
             assertEquals(1, pullRequests.size());
             var pr = pullRequests.get(0);
-            assertEquals("Cannot automatically merge test:master", pr.getTitle());
+            assertEquals("Cannot automatically merge test:master", pr.title());
         }
     }
 
     @Test
     void resolvedMergeConflictShouldResultInClosedPR(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory(false)) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -285,32 +285,32 @@
             assertEquals(2, toCommits.size());
             var toHashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
             assertTrue(toHashes.contains(toHashA));
             assertTrue(toHashes.contains(toHashB));
 
-            var pullRequests = toHostedRepo.getPullRequests();
+            var pullRequests = toHostedRepo.pullRequests();
             assertEquals(1, pullRequests.size());
             var pr = pullRequests.get(0);
-            assertEquals("Cannot automatically merge test:master", pr.getTitle());
+            assertEquals("Cannot automatically merge test:master", pr.title());
 
-            var fetchHead = toLocalRepo.fetch(fromHostedRepo.getWebUrl(), "master");
+            var fetchHead = toLocalRepo.fetch(fromHostedRepo.webUrl(), "master");
             toLocalRepo.merge(fetchHead, "ours");
             toLocalRepo.commit("Merge", "duke", "duke@openjdk.org", now);
 
             toCommits = toLocalRepo.commits().asList();
             assertEquals(4, toCommits.size());
 
             TestBotRunner.runPeriodicItems(bot);
-            pullRequests = toHostedRepo.getPullRequests();
+            pullRequests = toHostedRepo.pullRequests();
             assertEquals(0, pullRequests.size());
         }
     }
 
     @Test
     void resolvedMergeConflictAndThenNewConflict(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory(false)) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -359,24 +359,24 @@
             assertEquals(2, toCommits.size());
             var toHashes = toCommits.stream().map(Commit::hash).collect(Collectors.toSet());
             assertTrue(toHashes.contains(toHashA));
             assertTrue(toHashes.contains(toHashB));
 
-            var pullRequests = toHostedRepo.getPullRequests();
+            var pullRequests = toHostedRepo.pullRequests();
             assertEquals(1, pullRequests.size());
             var pr = pullRequests.get(0);
-            assertEquals("Cannot automatically merge test:master", pr.getTitle());
+            assertEquals("Cannot automatically merge test:master", pr.title());
 
-            var fetchHead = toLocalRepo.fetch(fromHostedRepo.getWebUrl(), "master");
+            var fetchHead = toLocalRepo.fetch(fromHostedRepo.webUrl(), "master");
             toLocalRepo.merge(fetchHead, "ours");
             toLocalRepo.commit("Merge", "duke", "duke@openjdk.org", now);
 
             toCommits = toLocalRepo.commits().asList();
             assertEquals(4, toCommits.size());
 
             TestBotRunner.runPeriodicItems(bot);
-            pullRequests = toHostedRepo.getPullRequests();
+            pullRequests = toHostedRepo.pullRequests();
             assertEquals(0, pullRequests.size());
 
             var fromFileC = fromDir.resolve("c.txt");
             Files.writeString(fromFileC, "Hello C1\n");
             fromLocalRepo.add(fromFileC);
@@ -386,11 +386,11 @@
             Files.writeString(toFileC, "Hello C2\n");
             toLocalRepo.add(toFileC);
             toLocalRepo.commit("Adding c2", "duke", "duke@openjdk.org", now);
 
             TestBotRunner.runPeriodicItems(bot);
-            pullRequests = toHostedRepo.getPullRequests();
+            pullRequests = toHostedRepo.pullRequests();
             assertEquals(1, pullRequests.size());
-            assertEquals("Cannot automatically merge test:master", pr.getTitle());
+            assertEquals("Cannot automatically merge test:master", pr.title());
         }
     }
 }
diff a/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java b/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java
--- a/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java
+++ b/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBot.java
@@ -51,43 +51,43 @@
     public boolean concurrentWith(WorkItem other) {
         if (!(other instanceof MirrorBot)) {
             return true;
         }
         var otherBot = (MirrorBot) other;
-        return !from.getName().equals(otherBot.from.getName());
+        return !from.name().equals(otherBot.from.name());
     }
 
     @Override
     public void run(Path scratchPath) {
         try {
             var sanitizedUrl =
-                URLEncoder.encode(from.getWebUrl().toString(), StandardCharsets.UTF_8);
+                URLEncoder.encode(from.webUrl().toString(), StandardCharsets.UTF_8);
             var dir = storage.resolve(sanitizedUrl);
             Repository repo = null;
             if (!Files.exists(dir)) {
-                log.info("Cloning " + from.getName());
+                log.info("Cloning " + from.name());
                 Files.createDirectories(dir);
-                repo = Repository.mirror(from.getUrl(), dir);
+                repo = Repository.mirror(from.url(), dir);
             } else {
-                log.info("Found existing scratch directory for " + from.getName());
+                log.info("Found existing scratch directory for " + from.name());
                 repo = Repository.get(dir).orElseThrow(() -> {
                         return new RuntimeException("Repository in " + dir + " has vanished");
                 });
             }
 
-            log.info("Pulling " + from.getName());
+            log.info("Pulling " + from.name());
             repo.fetchAll();
-            log.info("Pushing to " + to.getName());
-            repo.pushAll(to.getUrl());
+            log.info("Pushing to " + to.name());
+            repo.pushAll(to.url());
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
 
     @Override
     public String toString() {
-        return "MirrorBot@(" + from.getName() + "-> " + to.getName() + ")";
+        return "MirrorBot@(" + from.name() + "-> " + to.name() + ")";
     }
 
     @Override
     public List<WorkItem> getPeriodicItems() {
         return List.of(this);
diff a/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java b/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java
--- a/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java
+++ b/bots/mirror/src/main/java/org/openjdk/skara/bots/mirror/MirrorBotFactory.java
@@ -53,11 +53,11 @@
             var fromRepo = configuration.repository(fromName);
 
             var toName = repo.get("to").asString();
             var toRepo = configuration.repository(toName);
 
-            log.info("Setting up mirroring from " + fromRepo.getName() + "to " + toRepo.getName());
+            log.info("Setting up mirroring from " + fromRepo.name() + "to " + toRepo.name());
             bots.add(new MirrorBot(storage, fromRepo, toRepo));
         }
         return bots;
     }
 }
diff a/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java b/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java
--- a/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java
+++ b/bots/mirror/src/test/java/org/openjdk/skara/bots/mirror/MirrorBotTests.java
@@ -38,11 +38,11 @@
 
 class MirrorBotTests {
     @Test
     void mirrorMasterBranch(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -75,11 +75,11 @@
     }
 
     @Test
     void mirrorMultipleBranches(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -121,11 +121,11 @@
     }
 
     @Test
     void mirrorMultipleTags(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
@@ -166,11 +166,11 @@
     }
 
     @Test
     void mirrorRemovingBranch(TestInfo testInfo) throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var fromLocalRepo = Repository.init(fromDir, VCS.GIT);
             var fromHostedRepo = new TestHostedRepository(host, "test", fromLocalRepo);
 
diff a/bots/mlbridge/build.gradle b/bots/mlbridge/build.gradle
--- a/bots/mlbridge/build.gradle
+++ b/bots/mlbridge/build.gradle
@@ -32,10 +32,11 @@
 
 dependencies {
     implementation project(':bot')
     implementation project(':mailinglist')
     implementation project(':host')
+    implementation project(':network')
     implementation project(':census')
     implementation project(':vcs')
     implementation project(':jcheck')
     implementation project(':json')
     implementation project(':email')
diff a/bots/mlbridge/src/main/java/module-info.java b/bots/mlbridge/src/main/java/module-info.java
--- a/bots/mlbridge/src/main/java/module-info.java
+++ b/bots/mlbridge/src/main/java/module-info.java
@@ -28,9 +28,10 @@
     requires org.openjdk.skara.vcs;
     requires org.openjdk.skara.jcheck;
     requires org.openjdk.skara.json;
     requires org.openjdk.skara.email;
     requires org.openjdk.skara.webrev;
+    requires org.openjdk.skara.network;
     requires java.logging;
 
     provides org.openjdk.skara.bot.BotFactory with org.openjdk.skara.bots.mlbridge.MailingListBridgeBotFactory;
 }
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveMessages.java
@@ -47,26 +47,26 @@
                      .map(line -> line.length() > 0 ? line.charAt(0) == '>' ? ">" + line : "> " + line : "> ")
                      .collect(Collectors.joining("\n"));
     }
 
     private static String replyFooter(PullRequestInstance prInstance) {
-        return "PR: " + prInstance.pr().getWebUrl();
+        return "PR: " + prInstance.pr().webUrl();
     }
 
     // When changing this, ensure that the PR pattern in the notifier still matches
     static String composeConversation(PullRequestInstance prInstance, URI webrev) {
         var commitMessages = prInstance.formatCommitMessages(prInstance.baseHash(), prInstance.headHash(), ArchiveMessages::formatCommit);
-        var filteredBody = filterComments(prInstance.pr().getBody());
+        var filteredBody = filterComments(prInstance.pr().body());
         if (filteredBody.isEmpty()) {
-            filteredBody = prInstance.pr().getTitle().strip();
+            filteredBody = prInstance.pr().title().strip();
         }
         var issueString = prInstance.issueUrl().map(url -> "  Issue: " + url + "\n").orElse("");
         return filteredBody + "\n\n" +
                 infoSeparator + "\n\n" +
                 "Commits:\n" +
                 commitMessages + "\n\n" +
-                "Changes: " + prInstance.pr().getChangeUrl() + "\n" +
+                "Changes: " + prInstance.pr().changeUrl() + "\n" +
                 " Webrev: " + webrev.toString() + "\n" +
                 issueString +
                 "  Stats: " + prInstance.stats(prInstance.baseHash(), prInstance.headHash()) + "\n" +
                 "  Patch: " + prInstance.diffUrl() + "\n" +
                 "  Fetch: " + prInstance.fetchCommand() + "\n\n" +
@@ -78,11 +78,11 @@
         var issueString = prInstance.issueUrl().map(url -> "  Issue: " + url + "\n").orElse("");
         return "The pull request has been updated with a complete new set of changes (possibly due to a rebase).\n\n" +
                 infoSeparator + "\n\n" +
                 "Commits:\n" +
                 commitMessages + "\n\n" +
-                "Changes: " + prInstance.pr().getChangeUrl() + "\n" +
+                "Changes: " + prInstance.pr().changeUrl() + "\n" +
                 " Webrev: " + fullWebrev.toString() + "\n" +
                 issueString +
                 "  Stats: " + prInstance.stats(prInstance.baseHash(), prInstance.headHash()) + "\n" +
                 "  Patch: " + prInstance.diffUrl() + "\n" +
                 "  Fetch: " + prInstance.fetchCommand() + "\n\n" +
@@ -94,12 +94,12 @@
         return "The pull request has been updated with additional changes.\n\n" +
                 infoSeparator + "\n\n" +
                 "Added commits:\n" +
                 newCommitMessages + "\n\n" +
                 "Changes:\n" +
-                "  - all: " + prInstance.pr().getChangeUrl() + "\n" +
-                "  - new: " + prInstance.pr().getChangeUrl(lastHead) + "\n\n" +
+                "  - all: " + prInstance.pr().changeUrl() + "\n" +
+                "  - new: " + prInstance.pr().changeUrl(lastHead) + "\n\n" +
                 "Webrevs:\n" +
                 " - full: " + fullWebrev.toString() + "\n" +
                 " - incr: " + incrementalWebrev.toString() + "\n\n" +
                 issueString +
                 "  Stats: " + prInstance.stats(lastHead, prInstance.headHash()) + "\n" +
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveWorkItem.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveWorkItem.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveWorkItem.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ArchiveWorkItem.java
@@ -50,33 +50,33 @@
         this.exceptionConsumer = exceptionConsumer;
     }
 
     @Override
     public String toString() {
-        return "ArchiveWorkItem@" + bot.codeRepo().getName() + "#" + pr.getId();
+        return "ArchiveWorkItem@" + bot.codeRepo().name() + "#" + pr.id();
     }
 
     @Override
     public boolean concurrentWith(WorkItem other) {
         if (!(other instanceof ArchiveWorkItem)) {
             return true;
         }
         ArchiveWorkItem otherItem = (ArchiveWorkItem)other;
-        if (!pr.getId().equals(otherItem.pr.getId())) {
+        if (!pr.id().equals(otherItem.pr.id())) {
             return true;
         }
-        if (!bot.codeRepo().getName().equals(otherItem.bot.codeRepo().getName())) {
+        if (!bot.codeRepo().name().equals(otherItem.bot.codeRepo().name())) {
             return true;
         }
         return false;
     }
 
     private void pushMbox(Repository localRepo, String message) {
         try {
             localRepo.add(localRepo.root().resolve("."));
             var hash = localRepo.commit(message, bot.emailAddress().fullName().orElseThrow(), bot.emailAddress().address());
-            localRepo.push(hash, bot.archiveRepo().getUrl(), "master");
+            localRepo.push(hash, bot.archiveRepo().url(), "master");
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
 
@@ -108,20 +108,20 @@
         return Optional.empty();
     }
 
     private Repository materializeArchive(Path scratchPath) {
         try {
-            return Repository.materialize(scratchPath, bot.archiveRepo().getUrl(), pr.getTargetRef());
+            return Repository.materialize(scratchPath, bot.archiveRepo().url(), pr.targetRef());
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
 
     private final static Pattern commandPattern = Pattern.compile("^/.*$");
 
-    private boolean ignoreComment(HostUserDetails author, String body) {
-        if (pr.repository().host().getCurrentUserDetails().equals(author)) {
+    private boolean ignoreComment(HostUser author, String body) {
+        if (pr.repository().host().currentUser().equals(author)) {
             return true;
         }
         if (bot.ignoredUsers().contains(author.userName())) {
             return true;
         }
@@ -142,22 +142,22 @@
     private static final String webrevHeaderMarker = "<!-- mlbridge webrev header -->";
     private static final String webrevListMarker = "<!-- mlbridge webrev list -->";
 
     private void updateWebrevComment(List<Comment> comments, int index, URI fullWebrev, URI incWebrev) {
         var existing = comments.stream()
-                               .filter(comment -> comment.author().equals(pr.repository().host().getCurrentUserDetails()))
+                               .filter(comment -> comment.author().equals(pr.repository().host().currentUser()))
                                .filter(comment -> comment.body().contains(webrevCommentMarker))
                                .findAny();
         var comment = webrevCommentMarker + "\n";
         comment += webrevHeaderMarker + "\n";
         comment += "### Webrevs" + "\n";
         comment += webrevListMarker + "\n";
         comment += " * " + String.format("%02d", index) + ": [Full](" + fullWebrev.toString() + ")";
         if (incWebrev != null) {
             comment += " - [Incremental](" + incWebrev.toString() + ")";
         }
-        comment += " (" + pr.getHeadHash() + ")\n";
+        comment += " (" + pr.headHash() + ")\n";
 
         if (existing.isPresent()) {
             if (existing.get().body().contains(fullWebrev.toString())) {
                 log.fine("Webrev link already posted - skipping update");
                 return;
@@ -186,28 +186,28 @@
 
     @Override
     public void run(Path scratchPath) {
         var path = scratchPath.resolve("mlbridge");
         var archiveRepo = materializeArchive(path);
-        var mboxBasePath = path.resolve(bot.codeRepo().getName());
+        var mboxBasePath = path.resolve(bot.codeRepo().name());
         var mbox = MailingListServerFactory.createMboxFileServer(mboxBasePath);
-        var reviewArchiveList = mbox.getList(pr.getId());
+        var reviewArchiveList = mbox.getList(pr.id());
         var sentMails = parseArchive(reviewArchiveList);
 
         // First determine if this PR should be inspected further or not
         if (sentMails.isEmpty()) {
-            var labels = new HashSet<>(pr.getLabels());
+            var labels = new HashSet<>(pr.labels());
             for (var readyLabel : bot.readyLabels()) {
                 if (!labels.contains(readyLabel)) {
                     log.fine("PR is not yet ready - missing label '" + readyLabel + "'");
                     return;
                 }
             }
         }
 
         // Also inspect comments before making the first post
-        var comments = pr.getComments();
+        var comments = pr.comments();
         if (sentMails.isEmpty()) {
             for (var readyComment : bot.readyComments().entrySet()) {
                 var commentFound = false;
                 for (var comment : comments) {
                     if (comment.author().userName().equals(readyComment.getKey())) {
@@ -247,12 +247,12 @@
             updateWebrevComment(comments, 0, webrev, null);
         } else {
             var latestHead = reviewArchive.latestHead();
 
             // Check if the head has changed
-            if (!pr.getHeadHash().equals(latestHead)) {
-                log.fine("Head hash change detected: current: " + pr.getHeadHash() + " - last: " + latestHead);
+            if (!pr.headHash().equals(latestHead)) {
+                log.fine("Head hash change detected: current: " + pr.headHash() + " - last: " + latestHead);
 
                 var latestBase = reviewArchive.latestBase();
                 if (!prInstance.baseHash().equals(latestBase)) {
                     // FIXME: Could try harder to make an incremental
                     var fullWebrev = bot.webrevStorage().createAndArchive(prInstance, webrevPath, prInstance.baseHash(),
@@ -278,20 +278,20 @@
             }
             reviewArchive.addComment(comment);
         }
 
         // File specific comments
-        var reviewComments = pr.getReviewComments();
+        var reviewComments = pr.reviewComments();
         for (var reviewComment : reviewComments) {
             if (ignoreComment(reviewComment.author(), reviewComment.body())) {
                 continue;
             }
             reviewArchive.addReviewComment(reviewComment);
         }
 
         // Review comments
-        var reviews = pr.getReviews();
+        var reviews = pr.reviews();
         for (var review : reviews) {
             if (ignoreComment(review.reviewer(), review.body().orElse(""))) {
                 continue;
             }
             reviewArchive.addReview(review);
@@ -302,11 +302,11 @@
             return;
         }
 
         // Push all new mails to the archive repository
         newMails.forEach(reviewArchiveList::post);
-        pushMbox(archiveRepo, "Adding comments for PR " + bot.codeRepo().getName() + "/" + pr.getId());
+        pushMbox(archiveRepo, "Adding comments for PR " + bot.codeRepo().name() + "/" + pr.id());
 
         // Finally post all new mails to the actual list
         for (var newMail : newMails) {
             var filteredHeaders = newMail.headers().stream()
                                          .filter(header -> !header.startsWith("PR-"))
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/CensusInstance.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/CensusInstance.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/CensusInstance.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/CensusInstance.java
@@ -47,11 +47,11 @@
         this.namespace = namespace;
     }
 
     private static Repository initialize(HostedRepository repo, String ref, Path folder) {
         try {
-            return Repository.materialize(folder, repo.getUrl(), ref);
+            return Repository.materialize(folder, repo.url(), ref);
         } catch (IOException e) {
             throw new RuntimeException("Failed to retrieve census to " + folder, e);
         }
     }
 
@@ -74,32 +74,32 @@
 
         return namespace;
     }
 
     private static JCheckConfiguration configuration(HostedRepository remoteRepo, String ref) {
-        var confFile = remoteRepo.getFileContents(".jcheck/conf", ref);
+        var confFile = remoteRepo.fileContents(".jcheck/conf", ref);
         return JCheckConfiguration.parse(confFile.lines().collect(Collectors.toList()));
     }
 
     static CensusInstance create(HostedRepository censusRepo, String censusRef, Path folder, PullRequest pr) {
-        var repoName = censusRepo.getUrl().getHost() + "/" + censusRepo.getName();
+        var repoName = censusRepo.url().getHost() + "/" + censusRepo.name();
         var repoFolder = folder.resolve(URLEncoder.encode(repoName, StandardCharsets.UTF_8));
         try {
             var localRepo = Repository.get(repoFolder)
                                       .or(() -> Optional.of(initialize(censusRepo, censusRef, repoFolder)))
                                       .orElseThrow();
-            var hash = localRepo.fetch(censusRepo.getUrl(), censusRef);
+            var hash = localRepo.fetch(censusRepo.url(), censusRef);
             localRepo.checkout(hash, true);
         } catch (IOException e) {
             initialize(censusRepo, censusRef, repoFolder);
         }
 
         try {
-            var configuration = configuration(pr.repository(), pr.getTargetRef());
+            var configuration = configuration(pr.repository(), pr.targetRef());
             var census = Census.parse(repoFolder);
             var project = project(configuration, census);
-            var namespace = namespace(census, pr.repository().getNamespace());
+            var namespace = namespace(census, pr.repository().namespace());
             return new CensusInstance(census, configuration, project, namespace);
         } catch (IOException e) {
             throw new UncheckedIOException("Cannot parse census at " + repoFolder, e);
         }
     }
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/CommentPosterWorkItem.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/CommentPosterWorkItem.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/CommentPosterWorkItem.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/CommentPosterWorkItem.java
@@ -71,15 +71,15 @@
         pr.addComment(body);
     }
 
     @Override
     public void run(Path scratchPath) {
-        var comments = pr.getComments();
+        var comments = pr.comments();
 
         var alreadyBridged = new HashSet<EmailAddress>();
         for (var comment : comments) {
-            if (!comment.author().equals(pr.repository().host().getCurrentUserDetails())) {
+            if (!comment.author().equals(pr.repository().host().currentUser())) {
                 continue;
             }
             var matcher = bridgedMailId.matcher(comment.body());
             if (!matcher.find()) {
                 continue;
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListArchiveReaderBot.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListArchiveReaderBot.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListArchiveReaderBot.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListArchiveReaderBot.java
@@ -99,11 +99,11 @@
         for (var newMessage : newMessages) {
             parsedEmailIds.add(newMessage.id());
         }
 
         var pr = parsedConversations.get(conversation.first().id());
-        var bridgeIdPattern = Pattern.compile("^[^.]+\\.[^.]+@" + pr.repository().getUrl().getHost() + "$");
+        var bridgeIdPattern = Pattern.compile("^[^.]+\\.[^.]+@" + pr.repository().url().getHost() + "$");
 
         // Filter out already bridged comments
         var bridgeCandidates = newMessages.stream()
                 .filter(email -> !bridgeIdPattern.matcher(email.id().address()).matches())
                 .collect(Collectors.toList());
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBot.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBot.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBot.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBot.java
@@ -145,11 +145,11 @@
 
     @Override
     public List<WorkItem> getPeriodicItems() {
         List<WorkItem> ret = new LinkedList<>();
 
-        for (var pr : codeRepo.getPullRequests()) {
+        for (var pr : codeRepo.pullRequests()) {
             if (updateCache.needsUpdate(pr)) {
                 ret.add(new ArchiveWorkItem(pr, this, e -> updateCache.invalidate(pr)));
             }
         }
 
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBotFactory.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBotFactory.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBotFactory.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBotFactory.java
@@ -23,11 +23,11 @@
 package org.openjdk.skara.bots.mlbridge;
 
 import org.openjdk.skara.bot.*;
 import org.openjdk.skara.email.EmailAddress;
 import org.openjdk.skara.host.HostedRepository;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.json.*;
 import org.openjdk.skara.mailinglist.MailingListServerFactory;
 
 import java.nio.file.Path;
 import java.time.Duration;
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/PullRequestInstance.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/PullRequestInstance.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/PullRequestInstance.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/PullRequestInstance.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.bots.mlbridge;
 
 import org.openjdk.skara.host.PullRequest;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.vcs.*;
 import org.openjdk.skara.vcs.openjdk.Issue;
 
 import java.io.*;
 import java.net.URI;
@@ -48,13 +48,13 @@
         this.projectPrefix = projectPrefix;
 
         // Materialize the PR's target ref
         try {
             var repository = pr.repository();
-            localRepo = Repository.materialize(localRepoPath, repository.getUrl(), pr.getTargetRef());
-            targetHash = localRepo.fetch(repository.getUrl(), pr.getTargetRef());
-            headHash = localRepo.fetch(repository.getUrl(), pr.getHeadHash().hex());
+            localRepo = Repository.materialize(localRepoPath, repository.url(), pr.targetRef());
+            targetHash = localRepo.fetch(repository.url(), pr.targetRef());
+            headHash = localRepo.fetch(repository.url(), pr.headHash().hex());
             baseHash = localRepo.mergeBase(targetHash, headHash);
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
@@ -70,16 +70,16 @@
     Hash headHash() {
         return this.headHash;
     }
 
     String diffUrl() {
-        return pr.getWebUrl() + ".diff";
+        return pr.webUrl() + ".diff";
     }
 
     String fetchCommand() {
-        var repoUrl = pr.repository().getWebUrl();
-        return "git fetch " + repoUrl + " " + pr.getSourceRef() + ":pull/" + pr.getId();
+        var repoUrl = pr.repository().webUrl();
+        return "git fetch " + repoUrl + " " + pr.sourceRef() + ":pull/" + pr.id();
     }
 
     String stats(Hash base, Hash head) {
         try {
             var diff = localRepo.diff(base, head);
@@ -100,11 +100,11 @@
             throw new UncheckedIOException(e);
         }
     }
 
     Optional<String> issueUrl() {
-        var issue = Issue.fromString(pr.getTitle());
+        var issue = Issue.fromString(pr.title());
         return issue.map(value -> URIBuilder.base(issueTracker).appendPath(projectPrefix + "-" + value.id()).build().toString());
     }
 
     @FunctionalInterface
     interface CommitFormatter {
@@ -120,11 +120,11 @@
             throw new UncheckedIOException(e);
         }
     }
 
     String id() {
-        return pr.getId();
+        return pr.id();
     }
 
     PullRequest pr() {
         return pr;
     }
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ReviewArchive.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ReviewArchive.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ReviewArchive.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/ReviewArchive.java
@@ -20,11 +20,11 @@
     private final List<Email> generated = new ArrayList<>();
     private final Map<String, Email> generatedIds = new HashMap<>();
     private final List<Hash> reportedHeads;
     private final List<Hash> reportedBases;
 
-    private EmailAddress getAuthorAddress(HostUserDetails originalAuthor) {
+    private EmailAddress getAuthorAddress(HostUser originalAuthor) {
         var contributor = censusInstance.namespace().get(originalAuthor.id());
         if (contributor == null) {
             return EmailAddress.from(originalAuthor.fullName(),
                                      censusInstance.namespace().name() + "+" +
                                              originalAuthor.id() + "+" + originalAuthor.userName() + "@" +
@@ -35,17 +35,17 @@
         }
     }
 
     private EmailAddress getUniqueMessageId(String identifier) {
         try {
-            var prSpecific = prInstance.pr().repository().getName().replace("/", ".") + "." + prInstance.id();
+            var prSpecific = prInstance.pr().repository().name().replace("/", ".") + "." + prInstance.id();
             var digest = MessageDigest.getInstance("SHA-256");
             digest.update(prSpecific.getBytes(StandardCharsets.UTF_8));
             digest.update(identifier.getBytes(StandardCharsets.UTF_8));
             var encodedCommon = Base64.getUrlEncoder().encodeToString(digest.digest());
 
-            return EmailAddress.from(encodedCommon + "." + UUID.randomUUID() + "@" + prInstance.pr().repository().getUrl().getHost());
+            return EmailAddress.from(encodedCommon + "." + UUID.randomUUID() + "@" + prInstance.pr().repository().url().getHost());
         } catch (NoSuchAlgorithmException e) {
             throw new RuntimeException("Cannot find SHA-256");
         }
     }
 
@@ -172,13 +172,13 @@
     }
 
     void create(URI webrev) {
         var body = ArchiveMessages.composeConversation(prInstance, webrev);
         var id = getMessageId();
-        var email = Email.create("RFR: " + prInstance.pr().getTitle(), body)
+        var email = Email.create("RFR: " + prInstance.pr().title(), body)
                          .sender(sender)
-                         .author(getAuthorAddress(prInstance.pr().getAuthor()))
+                         .author(getAuthorAddress(prInstance.pr().author()))
                          .id(id)
                          .header("PR-Head-Hash", prInstance.headHash().hex())
                          .header("PR-Base-Hash", prInstance.baseHash().hex())
                          .build();
         generated.add(email);
@@ -191,13 +191,13 @@
 
     void addFull(URI webrev) {
         var body = ArchiveMessages.composeRebaseComment(prInstance, webrev);
         var id = getMessageId(prInstance.headHash());
         var parent = topEmail();
-        var email = Email.reply(parent, "Re: " + latestHeadPrefix() + " RFR: " + prInstance.pr().getTitle(), body)
+        var email = Email.reply(parent, "Re: " + latestHeadPrefix() + " RFR: " + prInstance.pr().title(), body)
                          .sender(sender)
-                         .author(getAuthorAddress(prInstance.pr().getAuthor()))
+                         .author(getAuthorAddress(prInstance.pr().author()))
                          .recipient(parent.author())
                          .id(id)
                          .header("PR-Head-Hash", prInstance.headHash().hex())
                          .header("PR-Base-Hash", prInstance.baseHash().hex())
                          .header("PR-Sequence", Integer.toString(existing.size() + generated.size()))
@@ -208,24 +208,24 @@
 
     void addIncremental(URI fullWebrev, URI incrementalWebrev) {
         var body = ArchiveMessages.composeIncrementalComment(latestHead(), prInstance, fullWebrev, incrementalWebrev);
         var id = getMessageId(prInstance.headHash());
         var parent = topEmail();
-        var email = Email.reply(parent, "Re: " + latestHeadPrefix() + " RFR: " + prInstance.pr().getTitle(), body)
+        var email = Email.reply(parent, "Re: " + latestHeadPrefix() + " RFR: " + prInstance.pr().title(), body)
                          .sender(sender)
-                         .author(getAuthorAddress(prInstance.pr().getAuthor()))
+                         .author(getAuthorAddress(prInstance.pr().author()))
                          .recipient(parent.author())
                          .id(id)
                          .header("PR-Head-Hash", prInstance.headHash().hex())
                          .header("PR-Base-Hash", prInstance.baseHash().hex())
                          .header("PR-Sequence", Integer.toString(existing.size() + generated.size()))
                          .build();
         generated.add(email);
         generatedIds.put(getStableMessageId(id), email);
     }
 
-    private Optional<Email> findCollapsable(Email parent, HostUserDetails author, String subject) {
+    private Optional<Email> findCollapsable(Email parent, HostUser author, String subject) {
         var parentId = getStableMessageId(parent.id());
 
         // Is it a self-reply?
         if (parent.author().equals(getAuthorAddress(author)) && generatedIds.containsKey(parentId)) {
             // But avoid extending top-level parents
@@ -253,11 +253,11 @@
         }
 
         return Optional.empty();
     }
 
-    private void addReplyCommon(Email parent, HostUserDetails author, String subject, String body, EmailAddress id) {
+    private void addReplyCommon(Email parent, HostUser author, String subject, String body, EmailAddress id) {
         if (!subject.startsWith("Re: ")) {
             subject = "Re: " + subject;
         }
 
         // Collapse self-replies and replies-to-same that have been created in this run
@@ -300,11 +300,11 @@
         if (existingIds.containsKey(getStableMessageId(id))) {
             return;
         }
 
         var parent = latestGeneralComment();
-        addReplyCommon(parent, comment.author(), "Re: RFR: " + prInstance.pr().getTitle(), comment.body(), id);
+        addReplyCommon(parent, comment.author(), "Re: RFR: " + prInstance.pr().title(), comment.body(), id);
     }
 
     private String projectRole(Contributor contributor) {
         var version = censusInstance.configuration().census().version();
         if (censusInstance.project().isLead(contributor.username(), version)) {
@@ -333,11 +333,11 @@
         var subject = parent.subject();
 
         // Approvals by Reviewers get special treatment - post these as top-level comments
         if (review.verdict() == Review.Verdict.APPROVED && isReviewer) {
             parent = topEmail();
-            subject = "Re: [Approved] " + "RFR: " + prInstance.pr().getTitle();
+            subject = "Re: [Approved] " + "RFR: " + prInstance.pr().title();
         }
 
         var userName = contributor != null ? contributor.username() : review.reviewer().userName() + "@" + censusInstance.namespace().name();
         var userRole = contributor != null ? projectRole(contributor) : "none";
         var replyBody = ArchiveMessages.reviewCommentBody(review.body().orElse(""), review.verdict(), userName, userRole);
@@ -354,11 +354,11 @@
         var parent = parentForReviewComment(reviewComment);
         var body = new StringBuilder();
 
         // Add some context to the first post
         if (reviewComment.parent().isEmpty()) {
-            var contents = prInstance.pr().repository().getFileContents(reviewComment.path(), reviewComment.hash().hex()).lines().collect(Collectors.toList());
+            var contents = prInstance.pr().repository().fileContents(reviewComment.path(), reviewComment.hash().hex()).lines().collect(Collectors.toList());
 
             body.append(reviewComment.path()).append(" line ").append(reviewComment.line()).append(":\n\n");
             for (int i = Math.max(0, reviewComment.line() - 2); i < Math.min(contents.size(), reviewComment.line() + 1); ++i) {
                 body.append("> ").append(i + 1).append(": ").append(contents.get(i)).append("\n");
             }
diff a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/WebrevStorage.java b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/WebrevStorage.java
--- a/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/WebrevStorage.java
+++ b/bots/mlbridge/src/main/java/org/openjdk/skara/bots/mlbridge/WebrevStorage.java
@@ -22,11 +22,11 @@
  */
 package org.openjdk.skara.bots.mlbridge;
 
 import org.openjdk.skara.email.EmailAddress;
 import org.openjdk.skara.host.HostedRepository;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.vcs.*;
 import org.openjdk.skara.webrev.Webrev;
 
 import java.io.*;
 import java.net.URI;
@@ -88,11 +88,11 @@
                 } catch (IOException e) {
                     // If the commit fails, it probably means that we're resuming a partially completed previous update
                     // where some of the files have already been committed. Ignore it and continue.
                     continue;
                 }
-                localStorage.push(hash, storage.getUrl(), storageRef);
+                localStorage.push(hash, storage.url(), storageRef);
             }
         }
     }
 
     private static void clearDirectory(Path directory) {
@@ -105,11 +105,11 @@
         }
     }
 
     URI createAndArchive(PullRequestInstance prInstance, Path scratchPath, Hash base, Hash head, String identifier) {
         try {
-            var localStorage = Repository.materialize(scratchPath, storage.getUrl(), storageRef);
+            var localStorage = Repository.materialize(scratchPath, storage.url(), storageRef);
             var relativeFolder = baseFolder.resolve(String.format("%s/webrev.%s", prInstance.id(), identifier));
             var outputFolder = scratchPath.resolve(relativeFolder);
             // If a previous operation was interrupted there may be content here already - overwrite if so
             if (Files.exists(outputFolder)) {
                 clearDirectory(outputFolder);
diff a/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/MailingListArchiveReaderBotTests.java b/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/MailingListArchiveReaderBotTests.java
--- a/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/MailingListArchiveReaderBotTests.java
+++ b/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/MailingListArchiveReaderBotTests.java
@@ -22,11 +22,11 @@
  */
 package org.openjdk.skara.bots.mlbridge;
 
 import org.openjdk.skara.email.*;
 import org.openjdk.skara.host.PullRequest;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.mailinglist.*;
 import org.openjdk.skara.test.*;
 
 import org.junit.jupiter.api.*;
 
@@ -41,11 +41,11 @@
     private void addReply(Conversation conversation, MailingList mailingList, PullRequest pr) {
         var first = conversation.first();
 
         var reply = "Looks good";
         var references = first.id().toString();
-        var email = Email.create(EmailAddress.from("Commenter", "<c@test.test>"), "Re: RFR: " + pr.getTitle(), reply)
+        var email = Email.create(EmailAddress.from("Commenter", "<c@test.test>"), "Re: RFR: " + pr.title(), reply)
                          .recipient(first.author())
                          .id(EmailAddress.from(UUID.randomUUID() + "@id.id"))
                          .header("In-Reply-To", first.id().toString())
                          .header("References", references)
                          .build();
@@ -60,15 +60,15 @@
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var ignored = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress,
-                                                 Set.of(ignored.host().getCurrentUserDetails().userName()),
+                                                 Set.of(ignored.host().currentUser().userName()),
                                                  Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
                                                  URIBuilder.base("http://www.test.test/").build(),
                                                  Set.of(), Map.of(),
@@ -80,19 +80,19 @@
                                                                              Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var readerBot = new MailingListArchiveReaderBot(from, Set.of(mailmanList), Set.of(archive));
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo, "A simple change",
                                                                "Change msg\n\nWith several lines");
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This should now be ready");
 
             // Run an archive pass
             TestBotRunner.runPeriodicItems(mlBot);
@@ -111,11 +111,11 @@
             // Another archive reader pass - has to be done twice
             TestBotRunner.runPeriodicItems(readerBot);
             TestBotRunner.runPeriodicItems(readerBot);
 
             // The bridge should now have processed the reply
-            var updated = pr.getComments();
+            var updated = pr.comments();
             assertEquals(2, updated.size());
         }
     }
 
     @Test
@@ -126,15 +126,15 @@
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var ignored = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress,
-                                                 Set.of(ignored.host().getCurrentUserDetails().userName()),
+                                                 Set.of(ignored.host().currentUser().userName()),
                                                  Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
                                                  URIBuilder.base("http://www.test.test/").build(),
                                                  Set.of(), Map.of(),
@@ -146,19 +146,19 @@
                                                                              Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var readerBot = new MailingListArchiveReaderBot(from, Set.of(mailmanList), Set.of(archive));
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo, "A simple change",
                                                                "Change msg\n\nWith several lines");
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This should now be ready");
 
             // Run an archive pass
             TestBotRunner.runPeriodicItems(mlBot);
@@ -173,18 +173,18 @@
             // Another archive reader pass - has to be done twice
             TestBotRunner.runPeriodicItems(readerBot);
             TestBotRunner.runPeriodicItems(readerBot);
 
             // The bridge should now have processed the reply
-            var updated = pr.getComments();
+            var updated = pr.comments();
             assertEquals(2, updated.size());
 
             var newReaderBot = new MailingListArchiveReaderBot(from, Set.of(mailmanList), Set.of(archive));
             TestBotRunner.runPeriodicItems(newReaderBot);
             TestBotRunner.runPeriodicItems(newReaderBot);
 
             // The new bridge should not have made duplicate posts
-            var notUpdated = pr.getComments();
+            var notUpdated = pr.comments();
             assertEquals(2, notUpdated.size());
         }
     }
 }
diff a/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBotTests.java b/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBotTests.java
--- a/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBotTests.java
+++ b/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/MailingListBridgeBotTests.java
@@ -22,11 +22,11 @@
  */
 package org.openjdk.skara.bots.mlbridge;
 
 import org.openjdk.skara.email.EmailAddress;
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.mailinglist.MailingListServerFactory;
 import org.openjdk.skara.test.*;
 import org.openjdk.skara.vcs.Repository;
 
 import org.junit.jupiter.api.*;
@@ -91,12 +91,12 @@
     private long countSubstrings(String string, String substring) {
         return Pattern.compile(substring).matcher(string).results().count();
     }
 
     private String noreplyAddress(HostedRepository repository) {
-        return "test+" + repository.host().getCurrentUserDetails().id() + "+" +
-                repository.host().getCurrentUserDetails().userName() +
+        return "test+" + repository.host().currentUser().id() + "+" +
+                repository.host().currentUser().userName() +
                 "@openjdk.java.net";
     }
 
     @Test
     void simpleArchive(TestInfo testInfo) throws IOException {
@@ -108,74 +108,74 @@
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var ignored = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master", listAddress,
-                                                 Set.of(ignored.host().getCurrentUserDetails().userName()),
+                                                 Set.of(ignored.host().currentUser().userName()),
                                                  Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
                                                  URIBuilder.base("http://www.test.test/").build(),
-                                                 Set.of("rfr"), Map.of(ignored.host().getCurrentUserDetails().userName(),
+                                                 Set.of("rfr"), Map.of(ignored.host().currentUser().userName(),
                                                                        Pattern.compile("ready")),
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of("Extra1", "val1", "Extra2", "val2"),
                                                  Duration.ZERO);
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo, "A simple change",
                                                                "Change msg\n\nWith several lines");
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "1234: This is a pull request");
             pr.setBody("This should not be ready");
 
             // Run an archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // A PR that isn't ready for review should not be archived
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertFalse(archiveContains(archiveFolder.path(), "This is a pull request"));
 
             // Flag it as ready for review
             pr.setBody("This should now be ready");
             pr.addLabel("rfr");
 
             // Run another archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // But it should still not be archived
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertFalse(archiveContains(archiveFolder.path(), "This is a pull request"));
 
             // Now post a general comment - not a ready marker
-            var ignoredPr = ignored.getPullRequest(pr.getId());
+            var ignoredPr = ignored.pullRequest(pr.id());
             ignoredPr.addComment("hello there");
 
             // Run another archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // It should still not be archived
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertFalse(archiveContains(archiveFolder.path(), "This is a pull request"));
 
             // Now post a ready comment
             ignoredPr.addComment("ready");
 
             // Run another archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should now contain an entry
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "This is a pull request"));
             assertTrue(archiveContains(archiveFolder.path(), "This should now be ready"));
             assertTrue(archiveContains(archiveFolder.path(), "Patch:"));
             assertTrue(archiveContains(archiveFolder.path(), "Changes:"));
             assertTrue(archiveContains(archiveFolder.path(), "Webrev:"));
@@ -193,22 +193,22 @@
             var mailmanList = mailmanServer.getList(listAddress.address());
             var conversations = mailmanList.conversations(Duration.ofDays(1));
             assertEquals(1, conversations.size());
             var mail = conversations.get(0).first();
             assertEquals("RFR: 1234: This is a pull request", mail.subject());
-            assertEquals(pr.getAuthor().fullName(), mail.author().fullName().orElseThrow());
+            assertEquals(pr.author().fullName(), mail.author().fullName().orElseThrow());
             assertEquals(noreplyAddress(archive), mail.author().address());
             assertEquals(from, mail.sender());
             assertEquals("val1", mail.headerValue("Extra1"));
             assertEquals("val2", mail.headerValue("Extra2"));
 
             // And there should be a webrev
-            Repository.materialize(webrevFolder.path(), archive.getUrl(), "webrev");
+            Repository.materialize(webrevFolder.path(), archive.url(), "webrev");
             assertTrue(webrevContains(webrevFolder.path(), "1 lines changed"));
-            var comments = pr.getComments();
+            var comments = pr.comments();
             var webrevComments = comments.stream()
-                                         .filter(comment -> comment.author().equals(author.host().getCurrentUserDetails()))
+                                         .filter(comment -> comment.author().equals(author.host().currentUser()))
                                          .filter(comment -> comment.body().contains("webrev"))
                                          .filter(comment -> comment.body().contains(editHash.hex()))
                                          .collect(Collectors.toList());
             assertEquals(1, webrevComments.size());
 
@@ -220,11 +220,11 @@
 
             // Run another archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should now contain the comment, but not the ignored one
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "This is a comment"));
             assertTrue(archiveContains(archiveFolder.path(), "> This should now be ready"));
             assertFalse(archiveContains(archiveFolder.path(), "Don't mind me"));
 
             listServer.processIncoming();
@@ -238,11 +238,11 @@
 
             // Run another archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should contain the additional comment
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "This is another comment"));
             assertTrue(archiveContains(archiveFolder.path(), ">> This should now be ready"));
 
             listServer.processIncoming();
             conversations = mailmanList.conversations(Duration.ofDays(1));
@@ -265,51 +265,51 @@
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var ignored = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master", listAddress,
-                                                 Set.of(ignored.host().getCurrentUserDetails().userName()),
+                                                 Set.of(ignored.host().currentUser().userName()),
                                                  Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
                                                  URIBuilder.base("http://www.test.test/").build(),
                                                  Set.of(), Map.of(),
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // And make a file specific comment
             var currentMaster = localRepo.resolve("master").orElseThrow();
             var comment = pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review comment");
 
             // Add one from an ignored user as well
-            var ignoredPr = ignored.getPullRequest(pr.getId());
+            var ignoredPr = ignored.pullRequest(pr.id());
             ignoredPr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Don't mind me");
 
             // Process comments
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // The archive should now contain an entry
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "This is a pull request"));
             assertTrue(archiveContains(archiveFolder.path(), "This is now ready"));
             assertTrue(archiveContains(archiveFolder.path(), "Review comment"));
             assertTrue(archiveContains(archiveFolder.path(), "> This is now ready"));
             assertTrue(archiveContains(archiveFolder.path(), reviewFile.toString()));
@@ -327,11 +327,11 @@
             pr.addReviewCommentReply(comment, "This is a review reply");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // The archive should contain the additional comment (but no quoted footers)
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "This is a review reply"));
             assertTrue(archiveContains(archiveFolder.path(), ">> This is now ready"));
             assertFalse(archiveContains(archiveFolder.path(), "^> PR:"));
 
             // As well as the mailing list
@@ -353,11 +353,11 @@
              var listServer = new TestMailmanServer()) {
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress, Set.of(), Set.of(),
                                                  listServer.getArchive(),
                                                  listServer.getSMTP(),
@@ -367,18 +367,18 @@
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
             pr.addComment("Avoid combining");
 
             TestBotRunner.runPeriodicItems(mlBot);
@@ -392,11 +392,11 @@
             pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Final review comment");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // The archive should contain a combined entry
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertEquals(2, archiveContainsCount(archiveFolder.path(), "^On.*wrote:"));
 
             // As well as the mailing list
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
@@ -421,11 +421,11 @@
             pr.addReviewCommentReply(first, "I agree");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // The archive should contain a new entry
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertEquals(3, archiveContainsCount(archiveFolder.path(), "^On.*wrote:"));
 
             // The combined review comments should only appear unquoted once
             assertEquals(1, archiveContainsCount(archiveFolder.path(), "^Another review comment"));
         }
@@ -440,12 +440,12 @@
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id())
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addReviewer(reviewer.host().currentUser().id())
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress, Set.of(), Set.of(),
                                                  listServer.getArchive(),
                                                  listServer.getSMTP(),
@@ -455,25 +455,25 @@
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // Make a file specific comment
-            var reviewPr = reviewer.getPullRequest(pr.getId());
+            var reviewPr = reviewer.pullRequest(pr.id());
             var comment1 = reviewPr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review comment");
             pr.addReviewCommentReply(comment1, "I agree");
             reviewPr.addReviewCommentReply(comment1, "Great");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
@@ -497,11 +497,11 @@
             listServer.processIncoming();
             listServer.processIncoming();
             listServer.processIncoming();
 
             // Sanity check the archive
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertEquals(9, archiveContainsCount(archiveFolder.path(), "^On.*wrote:"));
 
             // File specific comments should appear before the approval
             var archiveText = archiveContents(archiveFolder.path()).orElseThrow();
             assertTrue(archiveText.indexOf("Looks fine") > archiveText.indexOf("You are welcome"));
@@ -523,11 +523,11 @@
             assertTrue(thread1.body().contains("Review comment\n\n"), thread1.body());
             assertFalse(thread1.body().contains("Another review comment"), thread1.body());
             var thread1reply1 = conversations.get(0).replies(thread1).get(0);
             assertTrue(thread1reply1.body().contains("I agree"));
             assertEquals(noreplyAddress(archive), thread1reply1.author().address());
-            assertEquals(archive.host().getCurrentUserDetails().fullName(), thread1reply1.author().fullName().orElseThrow());
+            assertEquals(archive.host().currentUser().fullName(), thread1reply1.author().fullName().orElseThrow());
             var thread1reply2 = conversations.get(0).replies(thread1reply1).get(0);
             assertTrue(thread1reply2.body().contains("Great"));
             assertEquals("integrationreviewer1@openjdk.java.net", thread1reply2.author().address());
             assertEquals("Generated Reviewer 1", thread1reply2.author().fullName().orElseThrow());
 
@@ -558,11 +558,11 @@
              var listServer = new TestMailmanServer()) {
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress, Set.of(), Set.of(),
                                                  listServer.getArchive(),
                                                  listServer.getSMTP(),
@@ -572,18 +572,18 @@
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Line 1\nLine 2\nLine 3\nLine 4");
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
@@ -592,11 +592,11 @@
 
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // The archive should only contain context around line 2
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "^> 2: Line 1$"));
             assertTrue(archiveContains(archiveFolder.path(), "^> 3: Line 2$"));
             assertFalse(archiveContains(archiveFolder.path(), "^> 4: Line 3$"));
         }
     }
@@ -609,11 +609,11 @@
              var listServer = new TestMailmanServer()) {
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress, Set.of(), Set.of(),
                                                  listServer.getArchive(),
                                                  listServer.getSMTP(),
@@ -623,30 +623,30 @@
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
             var initialHash = CheckableRepository.appendAndCommit(localRepo,
                                                                   "Line 0.1\nLine 0.2\nLine 0.3\nLine 0.4\n" +
                                                                           "Line 1\nLine 2\nLine 3\nLine 4\n" +
                                                                           "Line 5\nLine 6\nLine 7\nLine 8\n" +
                                                                           "Line 8.1\nLine 8.2\nLine 8.3\nLine 8.4\n" +
                                                                           "Line 9\nLine 10\nLine 11\nLine 12\n" +
                                                                           "Line 13\nLine 14\nLine 15\nLine 16\n");
-            localRepo.push(initialHash, author.getUrl(), "master");
+            localRepo.push(initialHash, author.url(), "master");
 
             // Make a change with a corresponding PR
             var current = Files.readString(localRepo.root().resolve(reviewFile), StandardCharsets.UTF_8);
             var updated = current.replaceAll("Line 2", "Line 2 edit\nLine 2.5");
             updated = updated.replaceAll("Line 13", "Line 12.5\nLine 13 edit");
             Files.writeString(localRepo.root().resolve(reviewFile), updated, StandardCharsets.UTF_8);
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
@@ -656,11 +656,11 @@
 
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // The archive should only contain context around line 2 and 20
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "reviewfile.txt line 7"));
             assertTrue(archiveContains(archiveFolder.path(), "^> 6: Line 1$"));
             assertTrue(archiveContains(archiveFolder.path(), "^> 7: Line 2 edit$"));
             assertFalse(archiveContains(archiveFolder.path(), "Line 3"));
 
@@ -679,11 +679,11 @@
              var listServer = new TestMailmanServer()) {
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress, Set.of(), Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
@@ -692,18 +692,18 @@
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready\n<!-- this is a comment -->\nAnd this is not\n" +
                                "<!-- Anything below this marker will be hidden -->\nStatus stuff");
 
             // Make a bunch of comments
@@ -714,11 +714,11 @@
 
             // Run an archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should not contain the comment
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "This is now ready"));
             assertFalse(archiveContains(archiveFolder.path(), "this is a comment"));
             assertFalse(archiveContains(archiveFolder.path(), "Status stuff"));
             assertTrue(archiveContains(archiveFolder.path(), "And this is not"));
             assertFalse(archiveContains(archiveFolder.path(), "<!--"));
@@ -738,11 +738,11 @@
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var commenter = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress, Set.of(), Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
@@ -751,57 +751,57 @@
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
 
             // Run an archive pass
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             var nextHash = CheckableRepository.appendAndCommit(localRepo, "Yet one more line", "Fixing");
-            localRepo.push(nextHash, author.getUrl(), "edit");
+            localRepo.push(nextHash, author.url(), "edit");
 
             // Make sure that the push registered
-            var lastHeadHash = pr.getHeadHash();
+            var lastHeadHash = pr.headHash();
             var refreshCount = 0;
             do {
-                pr = author.getPullRequest(pr.getId());
+                pr = author.pullRequest(pr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (pr.getHeadHash().equals(lastHeadHash));
+            } while (pr.headHash().equals(lastHeadHash));
 
             // Run another archive pass
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // The archive should reference the updated push
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "additional changes"));
-            assertTrue(archiveContains(archiveFolder.path(), "full.*/" + pr.getId() + "/webrev.01"));
-            assertTrue(archiveContains(archiveFolder.path(), "inc.*/" + pr.getId() + "/webrev.00-01"));
+            assertTrue(archiveContains(archiveFolder.path(), "full.*/" + pr.id() + "/webrev.01"));
+            assertTrue(archiveContains(archiveFolder.path(), "inc.*/" + pr.id() + "/webrev.00-01"));
             assertTrue(archiveContains(archiveFolder.path(), "Patch"));
             assertTrue(archiveContains(archiveFolder.path(), "Fetch"));
             assertTrue(archiveContains(archiveFolder.path(), "Fixing"));
 
             // The webrev comment should be updated
-            var comments = pr.getComments();
+            var comments = pr.comments();
             var webrevComments = comments.stream()
-                                         .filter(comment -> comment.author().equals(author.host().getCurrentUserDetails()))
+                                         .filter(comment -> comment.author().equals(author.host().currentUser()))
                                          .filter(comment -> comment.body().contains("webrev"))
                                          .filter(comment -> comment.body().contains(nextHash.hex()))
                                          .filter(comment -> comment.body().contains(editHash.hex()))
                                          .collect(Collectors.toList());
             assertEquals(1, webrevComments.size());
@@ -815,29 +815,29 @@
                 assertEquals(noreplyAddress(archive), newMail.author().address());
                 assertEquals(from, newMail.sender());
             }
 
             // Add a comment
-            var commenterPr = commenter.getPullRequest(pr.getId());
+            var commenterPr = commenter.pullRequest(pr.id());
             commenterPr.addReviewComment(masterHash, nextHash, reviewFile.toString(), 2, "Review comment");
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // Ensure that additional updates are only reported once
             for (int i = 0; i < 3; ++i) {
                 var anotherHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "Fixing");
-                localRepo.push(anotherHash, author.getUrl(), "edit");
+                localRepo.push(anotherHash, author.url(), "edit");
 
                 // Make sure that the push registered
-                lastHeadHash = pr.getHeadHash();
+                lastHeadHash = pr.headHash();
                 refreshCount = 0;
                 do {
-                    pr = author.getPullRequest(pr.getId());
+                    pr = author.pullRequest(pr.id());
                     if (refreshCount++ > 100) {
                         fail("The PR did not update after the new push");
                     }
-                } while (pr.getHeadHash().equals(lastHeadHash));
+                } while (pr.headHash().equals(lastHeadHash));
 
                 TestBotRunner.runPeriodicItems(mlBot);
                 TestBotRunner.runPeriodicItems(mlBot);
                 listServer.processIncoming();
             }
@@ -859,11 +859,11 @@
              var listServer = new TestMailmanServer()) {
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var sender = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(sender, author, archive, censusBuilder.build(), "master",
                                                  listAddress, Set.of(), Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
@@ -872,57 +872,57 @@
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path().resolve("first"), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path().resolve("first"), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo, "A line", "Original msg");
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
 
             // Run an archive pass
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
-            var newLocalRepo = Repository.materialize(tempFolder.path().resolve("second"), author.getUrl(), "master");
+            var newLocalRepo = Repository.materialize(tempFolder.path().resolve("second"), author.url(), "master");
             var newEditHash = CheckableRepository.appendAndCommit(newLocalRepo, "Another line", "Replaced msg");
-            newLocalRepo.push(newEditHash, author.getUrl(), "edit", true);
+            newLocalRepo.push(newEditHash, author.url(), "edit", true);
 
             // Make sure that the push registered
-            var lastHeadHash = pr.getHeadHash();
+            var lastHeadHash = pr.headHash();
             var refreshCount = 0;
             do {
-                pr = author.getPullRequest(pr.getId());
+                pr = author.pullRequest(pr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (pr.getHeadHash().equals(lastHeadHash));
+            } while (pr.headHash().equals(lastHeadHash));
 
             // Run another archive pass
             TestBotRunner.runPeriodicItems(mlBot);
             listServer.processIncoming();
 
             // The archive should reference the rebased push
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "complete new set of changes"));
-            assertTrue(archiveContains(archiveFolder.path(), pr.getId() + "/webrev.01"));
+            assertTrue(archiveContains(archiveFolder.path(), pr.id() + "/webrev.01"));
             assertFalse(archiveContains(archiveFolder.path(), "Incremental"));
             assertTrue(archiveContains(archiveFolder.path(), "Patch"));
             assertTrue(archiveContains(archiveFolder.path(), "Fetch"));
             assertTrue(archiveContains(archiveFolder.path(), "Original msg"));
             assertTrue(archiveContains(archiveFolder.path(), "Replaced msg"));
 
             // The webrev comment should be updated
-            var comments = pr.getComments();
+            var comments = pr.comments();
             var webrevComments = comments.stream()
-                                         .filter(comment -> comment.author().equals(author.host().getCurrentUserDetails()))
+                                         .filter(comment -> comment.author().equals(author.host().currentUser()))
                                          .filter(comment -> comment.body().contains("webrev"))
                                          .filter(comment -> comment.body().contains(newEditHash.hex()))
                                          .collect(Collectors.toList());
             assertEquals(1, webrevComments.size());
 
@@ -950,65 +950,65 @@
             var author = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var ignored = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress,
-                                                 Set.of(ignored.host().getCurrentUserDetails().userName()),
+                                                 Set.of(ignored.host().currentUser().userName()),
                                                  Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
                                                  URIBuilder.base("http://www.test.test/").build(),
                                                  Set.of(), Map.of(),
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo, "A simple change",
                                                                "Change msg\n\nWith several lines");
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
 
             // Flag it as ready for review
             pr.setBody("This should now be ready");
 
             // Run an archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should now contain an entry
-            var archiveRepo = Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            var archiveRepo = Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), editHash.abbreviate()));
 
             // And there should be a webrev comment
-            var comments = pr.getComments();
+            var comments = pr.comments();
             var webrevComments = comments.stream()
-                                         .filter(comment -> comment.author().equals(author.host().getCurrentUserDetails()))
+                                         .filter(comment -> comment.author().equals(author.host().currentUser()))
                                          .filter(comment -> comment.body().contains("webrev"))
                                          .filter(comment -> comment.body().contains(editHash.hex()))
                                          .collect(Collectors.toList());
             assertEquals(1, webrevComments.size());
             assertEquals(1, countSubstrings(webrevComments.get(0).body(), "webrev.00"));
 
             // Pretend the archive didn't work out
-            archiveRepo.push(masterHash, archive.getUrl(), "master", true);
+            archiveRepo.push(masterHash, archive.url(), "master", true);
 
             // Run another archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The webrev comment should not contain duplicate entries
-            comments = pr.getComments();
+            comments = pr.comments();
             webrevComments = comments.stream()
-                                         .filter(comment -> comment.author().equals(author.host().getCurrentUserDetails()))
+                                         .filter(comment -> comment.author().equals(author.host().currentUser()))
                                          .filter(comment -> comment.body().contains("webrev"))
                                          .filter(comment -> comment.body().contains(editHash.hex()))
                                          .collect(Collectors.toList());
             assertEquals(1, webrevComments.size());
             assertEquals(1, countSubstrings(webrevComments.get(0).body(), "webrev.00"));
@@ -1025,12 +1025,12 @@
             var archive = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var from = EmailAddress.from("test", "test@test.mail");
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id())
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addReviewer(reviewer.host().currentUser().id())
+                                           .addAuthor(author.host().currentUser().id());
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress, Set.of(), Set.of(),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
                                                  URIBuilder.base("http://www.test.test/").build(),
@@ -1038,33 +1038,33 @@
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
 
             // Run an archive pass
             TestBotRunner.runPeriodicItems(mlBot);
 
             // First unapprove it
-            var reviewedPr = reviewer.getPullRequest(pr.getId());
+            var reviewedPr = reviewer.pullRequest(pr.id());
             reviewedPr.addReview(Review.Verdict.DISAPPROVED, "Reason 1");
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should contain a note
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertEquals(1, archiveContainsCount(archiveFolder.path(), "Disapproved by "));
             assertEquals(1, archiveContainsCount(archiveFolder.path(), " by integrationreviewer1"));
             if (author.host().supportsReviewBody()) {
                 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Reason 1"));
             }
@@ -1074,11 +1074,11 @@
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should contain another note
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertEquals(1, archiveContainsCount(archiveFolder.path(), "Approved by "));
             if (author.host().supportsReviewBody()) {
                 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Reason 2"));
             }
             assertEquals(1, archiveContainsCount(archiveFolder.path(), "Re: \\[Approved\\] RFR:"));
@@ -1088,11 +1088,11 @@
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should contain another note
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertEquals(2, archiveContainsCount(archiveFolder.path(), "Disapproved by "));
             if (author.host().supportsReviewBody()) {
                 assertEquals(1, archiveContainsCount(archiveFolder.path(), "Reason 3"));
             }
         }
@@ -1107,50 +1107,50 @@
             var author = credentials.getHostedRepository();
             var ignored = credentials.getHostedRepository();
             var archive = credentials.getHostedRepository();
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var from = EmailAddress.from("test", "test@test.mail");
             var mlBot = new MailingListBridgeBot(from, author, archive, censusBuilder.build(), "master",
                                                  listAddress,
-                                                 Set.of(ignored.host().getCurrentUserDetails().userName()),
+                                                 Set.of(ignored.host().currentUser().userName()),
                                                  Set.of(Pattern.compile("ignore this comment", Pattern.MULTILINE | Pattern.DOTALL)),
                                                  listServer.getArchive(), listServer.getSMTP(),
                                                  archive, "webrev", Path.of("test"),
                                                  URIBuilder.base("http://www.test.test/").build(),
                                                  Set.of(), Map.of(),
                                                  URIBuilder.base("http://issues.test/browse/").build(),
                                                  Map.of(), Duration.ZERO);
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.setBody("This is now ready");
 
             // Make a bunch of comments
             pr.addComment("Plain comment");
             pr.addComment("ignore this comment");
             pr.addComment("I think it is time to\nignore this comment!");
             pr.addReviewComment(masterHash, editHash, reviewFile.toString(), 2, "Review ignore this comment");
 
-            var ignoredPR = ignored.getPullRequest(pr.getId());
+            var ignoredPR = ignored.pullRequest(pr.id());
             ignoredPR.addComment("Don't mind me");
 
             TestBotRunner.runPeriodicItems(mlBot);
             TestBotRunner.runPeriodicItems(mlBot);
 
             // The archive should not contain the ignored comments
-            Repository.materialize(archiveFolder.path(), archive.getUrl(), "master");
+            Repository.materialize(archiveFolder.path(), archive.url(), "master");
             assertTrue(archiveContains(archiveFolder.path(), "This is now ready"));
             assertFalse(archiveContains(archiveFolder.path(), "ignore this comment"));
             assertFalse(archiveContains(archiveFolder.path(), "it is time to"));
             assertFalse(archiveContains(archiveFolder.path(), "Don't mind me"));
             assertFalse(archiveContains(archiveFolder.path(), "Review ignore"));
diff a/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/WebrevStorageTests.java b/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/WebrevStorageTests.java
--- a/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/WebrevStorageTests.java
+++ b/bots/mlbridge/src/test/java/org/openjdk/skara/bots/mlbridge/WebrevStorageTests.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.bots.mlbridge;
 
 import org.openjdk.skara.email.EmailAddress;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.test.*;
 import org.openjdk.skara.vcs.Repository;
 
 import org.junit.jupiter.api.*;
 
@@ -43,18 +43,18 @@
             var archive = credentials.getHostedRepository();
 
             // Populate the projects repository
             var reviewFile = Path.of("reviewfile.txt");
             var repoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(repoFolder, author.getRepositoryType(), reviewFile);
+            var localRepo = CheckableRepository.init(repoFolder, author.repositoryType(), reviewFile);
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
-            localRepo.push(masterHash, archive.getUrl(), "webrev", true);
+            localRepo.push(masterHash, author.url(), "master", true);
+            localRepo.push(masterHash, archive.url(), "webrev", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(archive, "master", "edit", "This is a pull request");
             pr.addLabel("rfr");
             pr.setBody("This is now ready");
 
             var from = EmailAddress.from("test", "test@test.mail");
@@ -65,15 +65,15 @@
             var prInstance = new PullRequestInstance(prFolder, pr, URIBuilder.base("http://issues.test/browse/").build(), "TEST");
             var scratchFolder = tempFolder.path().resolve("scratch");
             storage.createAndArchive(prInstance, scratchFolder, masterHash, editHash, "00");
 
             // Update the local repository and check that the webrev has been generated
-            Repository.materialize(repoFolder, archive.getUrl(), "webrev");
-            assertTrue(Files.exists(repoFolder.resolve("test/" + pr.getId() + "/webrev.00/index.html")));
+            Repository.materialize(repoFolder, archive.url(), "webrev");
+            assertTrue(Files.exists(repoFolder.resolve("test/" + pr.id() + "/webrev.00/index.html")));
 
             // Create it again - it will overwrite the previous one
             storage.createAndArchive(prInstance, scratchFolder, masterHash, editHash, "00");
-            Repository.materialize(repoFolder, archive.getUrl(), "webrev");
-            assertTrue(Files.exists(repoFolder.resolve("test/" + pr.getId() + "/webrev.00/index.html")));
+            Repository.materialize(repoFolder, archive.url(), "webrev");
+            assertTrue(Files.exists(repoFolder.resolve("test/" + pr.id() + "/webrev.00/index.html")));
         }
     }
 }
diff a/bots/notify/build.gradle b/bots/notify/build.gradle
--- a/bots/notify/build.gradle
+++ b/bots/notify/build.gradle
@@ -30,10 +30,11 @@
     }
 }
 
 dependencies {
     implementation project(':host')
+    implementation project(':network')
     implementation project(':bot')
     implementation project(':json')
     implementation project(':census')
     implementation project(':vcs')
     implementation project(':jcheck')
diff a/bots/notify/src/main/java/module-info.java b/bots/notify/src/main/java/module-info.java
--- a/bots/notify/src/main/java/module-info.java
+++ b/bots/notify/src/main/java/module-info.java
@@ -25,9 +25,10 @@
     requires org.openjdk.skara.vcs;
     requires org.openjdk.skara.jcheck;
     requires org.openjdk.skara.email;
     requires org.openjdk.skara.storage;
     requires org.openjdk.skara.mailinglist;
+    requires org.openjdk.skara.network;
     requires java.logging;
 
     provides org.openjdk.skara.bot.BotFactory with org.openjdk.skara.bots.notify.JNotifyBotFactory;
 }
diff a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JNotifyBot.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JNotifyBot.java
--- a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JNotifyBot.java
+++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JNotifyBot.java
@@ -59,11 +59,11 @@
     public boolean concurrentWith(WorkItem other) {
         if (!(other instanceof JNotifyBot)) {
             return true;
         }
         JNotifyBot otherItem = (JNotifyBot) other;
-        if (!repository.getName().equals(otherItem.repository.getName())) {
+        if (!repository.name().equals(otherItem.repository.name())) {
             return true;
         }
         return false;
     }
 
@@ -209,16 +209,16 @@
         return repo;
     }
 
     @Override
     public void run(Path scratchPath) {
-        var sanitizedUrl = URLEncoder.encode(repository.getWebUrl().toString() + "v2", StandardCharsets.UTF_8);
+        var sanitizedUrl = URLEncoder.encode(repository.webUrl().toString() + "v2", StandardCharsets.UTF_8);
         var path = storagePath.resolve(sanitizedUrl);
         var historyPath = scratchPath.resolve("notify").resolve("history");
 
         try {
-            var localRepo = fetchAll(path, repository.getUrl());
+            var localRepo = fetchAll(path, repository.url());
             var history = UpdateHistory.create(tagStorageBuilder, historyPath.resolve("tags"), branchStorageBuilder, historyPath.resolve("branches"));
             handleTags(localRepo, history);
 
             var knownRefs = localRepo.remoteBranches("origin")
                                      .stream()
@@ -240,11 +240,11 @@
         }
     }
 
     @Override
     public String toString() {
-        return "JNotifyBot@" + repository.getName();
+        return "JNotifyBot@" + repository.name();
     }
 
     @Override
     public List<WorkItem> getPeriodicItems() {
         return List.of(this);
diff a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JNotifyBotFactory.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JNotifyBotFactory.java
--- a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JNotifyBotFactory.java
+++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JNotifyBotFactory.java
@@ -22,11 +22,11 @@
  */
 package org.openjdk.skara.bots.notify;
 
 import org.openjdk.skara.bot.*;
 import org.openjdk.skara.email.EmailAddress;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.json.JSONObject;
 import org.openjdk.skara.mailinglist.MailingListServerFactory;
 import org.openjdk.skara.storage.StorageBuilder;
 import org.openjdk.skara.vcs.Tag;
 
diff a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JsonUpdater.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JsonUpdater.java
--- a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JsonUpdater.java
+++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/JsonUpdater.java
@@ -42,11 +42,11 @@
         this.defaultBuild = defaultBuild;
     }
 
     private JSONObject commitToChanges(HostedRepository repository, Commit commit, String build) {
         var ret = JSON.object();
-        ret.put("url",  repository.getWebUrl(commit.hash()).toString()); //FIXME
+        ret.put("url",  repository.webUrl(commit.hash()).toString()); //FIXME
         ret.put("version", version);
         ret.put("build", build);
 
         var parsedMessage = CommitMessageParsers.v1.parse(commit);
         var issueIds = JSON.array();
@@ -75,22 +75,22 @@
         return ret;
     }
 
     @Override
     public void handleCommits(HostedRepository repository, List<Commit> commits, Branch branch) {
-        try (var writer = new JsonUpdateWriter(path, repository.getName())) {
+        try (var writer = new JsonUpdateWriter(path, repository.name())) {
             for (var commit : commits) {
                 var json = commitToChanges(repository, commit, defaultBuild);
                 writer.write(json);
             }
         }
     }
 
     @Override
     public void handleTagCommits(HostedRepository repository, List<Commit> commits, OpenJDKTag tag) {
         var build = String.format("b%02d", tag.buildNum());
-        try (var writer = new JsonUpdateWriter(path, repository.getName())) {
+        try (var writer = new JsonUpdateWriter(path, repository.name())) {
             var issues = new ArrayList<Issue>();
             for (var commit : commits) {
                 var parsedMessage = CommitMessageParsers.v1.parse(commit);
                 issues.addAll(parsedMessage.issues());
             }
diff a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/MailingListUpdater.java b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/MailingListUpdater.java
--- a/bots/notify/src/main/java/org/openjdk/skara/bots/notify/MailingListUpdater.java
+++ b/bots/notify/src/main/java/org/openjdk/skara/bots/notify/MailingListUpdater.java
@@ -83,11 +83,11 @@
         printer.println("Author:    " + commit.author().name() + " <" + commit.author().email() + ">");
         if (!commit.author().equals(commit.committer())) {
             printer.println("Committer: " + commit.committer().name() + " <" + commit.committer().email() + ">");
         }
         printer.println("Date:      " + commit.date().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss +0000")));
-        printer.println("URL:       " + repository.getWebUrl(commit.hash()));
+        printer.println("URL:       " + repository.webUrl(commit.hash()));
         printer.println();
         printer.println(String.join("\n", commit.message()));
         printer.println();
 
         for (var diff : commit.parentDiffs()) {
@@ -104,13 +104,13 @@
         return EmailAddress.from(commit.committer().name(), commit.committer().email());
     }
 
     private String commitsToSubject(HostedRepository repository, List<Commit> commits, Branch branch) {
         var subject = new StringBuilder();
-        subject.append(repository.getRepositoryType().shortName());
+        subject.append(repository.repositoryType().shortName());
         subject.append(": ");
-        subject.append(repository.getName());
+        subject.append(repository.name());
         subject.append(": ");
         if (includeBranch) {
             subject.append(branch.name());
             subject.append(": ");
         }
@@ -122,13 +122,13 @@
         }
         return subject.toString();
     }
 
     private String tagToSubject(HostedRepository repository, Hash hash, OpenJDKTag tag) {
-        return repository.getRepositoryType().shortName() +
+        return repository.repositoryType().shortName() +
                 ": " +
-                repository.getName() +
+                repository.name() +
                 ": Added tag " +
                 tag.tag() +
                 " for changeset " +
                 hash.abbreviate();
     }
@@ -148,11 +148,11 @@
                 ret.add(commit);
                 continue;
             }
 
             var candidate = candidates.get(0);
-            var prLink = candidate.getWebUrl();
+            var prLink = candidate.webUrl();
             var prLinkPattern = Pattern.compile("^(?:PR: )?" + Pattern.quote(prLink.toString()), Pattern.MULTILINE);
 
             var rfrCandidates = rfrs.stream()
                                     .filter(email -> prLinkPattern.matcher(email.body()).find())
                                     .collect(Collectors.toList());
@@ -246,13 +246,13 @@
         list.post(email);
     }
 
     private String newBranchSubject(HostedRepository repository, List<Commit> commits, Branch parent, Branch branch) {
         var subject = new StringBuilder();
-        subject.append(repository.getRepositoryType().shortName());
+        subject.append(repository.repositoryType().shortName());
         subject.append(": ");
-        subject.append(repository.getName());
+        subject.append(repository.name());
         subject.append(": created branch ");
         subject.append(branch);
         subject.append(" based on the branch ");
         subject.append(parent);
         subject.append(" containing ");
diff a/bots/notify/src/test/java/org/openjdk/skara/bots/notify/UpdateHistoryTests.java b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/UpdateHistoryTests.java
--- a/bots/notify/src/test/java/org/openjdk/skara/bots/notify/UpdateHistoryTests.java
+++ b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/UpdateHistoryTests.java
@@ -37,16 +37,16 @@
 import static org.junit.jupiter.api.Assertions.*;
 
 class UpdateHistoryTests {
     private String resetHostedRepository(HostedRepository repository) throws IOException {
         var folder = Files.createTempDirectory("updatehistory");
-        var localRepository = Repository.init(folder, repository.getRepositoryType());
+        var localRepository = Repository.init(folder, repository.repositoryType());
         var firstFile = folder.resolve("first.txt");
         Files.writeString(firstFile, "First file to commit");
         localRepository.add(firstFile);
         var firstCommit = localRepository.commit("First commit", "Duke", "duke@openjdk.java.net");
-        localRepository.push(firstCommit, repository.getUrl(), localRepository.defaultBranch().toString(), true);
+        localRepository.push(firstCommit, repository.url(), localRepository.defaultBranch().toString(), true);
         return localRepository.defaultBranch().toString();
     }
 
     private UpdateHistory createHistory(HostedRepository repository, String ref) throws IOException {
         var folder = Files.createTempDirectory("updatehistory");
diff a/bots/notify/src/test/java/org/openjdk/skara/bots/notify/UpdaterTests.java b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/UpdaterTests.java
--- a/bots/notify/src/test/java/org/openjdk/skara/bots/notify/UpdaterTests.java
+++ b/bots/notify/src/test/java/org/openjdk/skara/bots/notify/UpdaterTests.java
@@ -64,13 +64,13 @@
     void testJsonUpdaterBranch(TestInfo testInfo) throws IOException {
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var localRepoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(localRepoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, repo.repositoryType());
             credentials.commitLock(localRepo);
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var tagStorage = createTagStorage(repo);
             var branchStorage = createBranchStorage(repo);
             var jsonFolder = tempFolder.path().resolve("json");
             Files.createDirectory(jsonFolder);
@@ -81,18 +81,18 @@
 
             TestBotRunner.runPeriodicItems(notifyBot);
             assertEquals(List.of(), findJsonFiles(jsonFolder, ""));
 
             var editHash = CheckableRepository.appendAndCommit(localRepo, "One more line", "12345678: Fixes");
-            localRepo.push(editHash, repo.getUrl(), "master");
+            localRepo.push(editHash, repo.url(), "master");
             TestBotRunner.runPeriodicItems(notifyBot);
             var jsonFiles = findJsonFiles(jsonFolder, "");
             assertEquals(1, jsonFiles.size());
             var jsonData = Files.readString(jsonFiles.get(0), StandardCharsets.UTF_8);
             var json = JSON.parse(jsonData);
             assertEquals(1, json.asArray().size());
-            assertEquals(repo.getWebUrl(editHash).toString(), json.asArray().get(0).get("url").asString());
+            assertEquals(repo.webUrl(editHash).toString(), json.asArray().get(0).get("url").asString());
             assertEquals(List.of("12345678"), json.asArray().get(0).get("issue").asArray().stream()
                                                   .map(JSONValue::asString)
                                                   .collect(Collectors.toList()));
         }
     }
@@ -101,15 +101,15 @@
     void testJsonUpdaterTag(TestInfo testInfo) throws IOException {
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var localRepoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(localRepoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, repo.repositoryType());
             credentials.commitLock(localRepo);
             var masterHash = localRepo.resolve("master").orElseThrow();
             localRepo.tag(masterHash, "jdk-12+1", "Added tag 1", "Duke", "duke@openjdk.java.net");
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var tagStorage = createTagStorage(repo);
             var branchStorage = createBranchStorage(repo);
             var jsonFolder = tempFolder.path().resolve("json");
             Files.createDirectory(jsonFolder);
@@ -120,15 +120,15 @@
 
             TestBotRunner.runPeriodicItems(notifyBot);
             assertEquals(List.of(), findJsonFiles(jsonFolder, ""));
 
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.fetch(repo.getUrl(), "history:history");
+            localRepo.fetch(repo.url(), "history:history");
             localRepo.tag(editHash, "jdk-12+2", "Added tag 2", "Duke", "duke@openjdk.java.net");
             var editHash2 = CheckableRepository.appendAndCommit(localRepo, "Another line", "34567890: Even more fixes");
             localRepo.tag(editHash2, "jdk-12+4", "Added tag 3", "Duke", "duke@openjdk.java.net");
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             TestBotRunner.runPeriodicItems(notifyBot);
             var jsonFiles = findJsonFiles(jsonFolder, "");
             assertEquals(3, jsonFiles.size());
 
@@ -139,16 +139,16 @@
                 if (json.asArray().get(0).contains("date")) {
                     assertEquals(2, json.asArray().size());
                     assertEquals(List.of("23456789"), json.asArray().get(0).get("issue").asArray().stream()
                                                           .map(JSONValue::asString)
                                                           .collect(Collectors.toList()));
-                    assertEquals(repo.getWebUrl(editHash).toString(), json.asArray().get(0).get("url").asString());
+                    assertEquals(repo.webUrl(editHash).toString(), json.asArray().get(0).get("url").asString());
                     assertEquals("team", json.asArray().get(0).get("build").asString());
                     assertEquals(List.of("34567890"), json.asArray().get(1).get("issue").asArray().stream()
                                                           .map(JSONValue::asString)
                                                           .collect(Collectors.toList()));
-                    assertEquals(repo.getWebUrl(editHash2).toString(), json.asArray().get(1).get("url").asString());
+                    assertEquals(repo.webUrl(editHash2).toString(), json.asArray().get(1).get("url").asString());
                     assertEquals("team", json.asArray().get(1).get("build").asString());
                 } else {
                     assertEquals(1, json.asArray().size());
                     if (json.asArray().get(0).get("build").asString().equals("b02")) {
                         assertEquals(List.of("23456789"), json.asArray().get(0).get("issue").asArray().stream()
@@ -170,14 +170,14 @@
         try (var listServer = new TestMailmanServer();
              var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var repoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(repoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             credentials.commitLock(localRepo);
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var tagStorage = createTagStorage(repo);
@@ -192,11 +192,11 @@
             // No mail should be sent on the first run as there is no history
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.push(editHash, repo.getUrl(), "master");
+            localRepo.push(editHash, repo.url(), "master");
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
 
             var conversations = mailmanList.conversations(Duration.ofDays(1));
             var email = conversations.get(0).first();
@@ -221,14 +221,14 @@
         try (var listServer = new TestMailmanServer();
              var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var repoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(repoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             credentials.commitLock(localRepo);
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var tagStorage = createTagStorage(repo);
@@ -244,14 +244,14 @@
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             var editHash1 = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes",
                                                                 "first_author", "first@author.example.com");
-            localRepo.push(editHash1, repo.getUrl(), "master");
+            localRepo.push(editHash1, repo.url(), "master");
             var editHash2 = CheckableRepository.appendAndCommit(localRepo, "Yet another line", "3456789A: Even more fixes",
                                                                 "another_author", "another@author.example.com");
-            localRepo.push(editHash2, repo.getUrl(), "master");
+            localRepo.push(editHash2, repo.url(), "master");
 
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
 
             var conversations = mailmanList.conversations(Duration.ofDays(1));
@@ -274,14 +274,14 @@
         try (var listServer = new TestMailmanServer();
              var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var repoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(repoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             credentials.commitLock(localRepo);
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var tagStorage = createTagStorage(repo);
@@ -298,11 +298,11 @@
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes",
                                                                "author", "author@test.test",
                                                                "committer", "committer@test.test");
-            localRepo.push(editHash, repo.getUrl(), "master");
+            localRepo.push(editHash, repo.url(), "master");
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
 
             var conversations = mailmanList.conversations(Duration.ofDays(1));
             var email = conversations.get(0).first();
@@ -322,15 +322,15 @@
         try (var listServer = new TestMailmanServer();
              var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var repoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(repoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             credentials.commitLock(localRepo);
             var branch = localRepo.branch(masterHash, "another");
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var tagStorage = createTagStorage(repo);
@@ -346,13 +346,13 @@
             // No mail should be sent on the first run as there is no history
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             var editHash1 = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.push(editHash1, repo.getUrl(), "master");
+            localRepo.push(editHash1, repo.url(), "master");
             var editHash2 = CheckableRepository.appendAndCommit(localRepo, "Yet another line", "3456789A: Even more fixes");
-            localRepo.push(editHash2, repo.getUrl(), "master");
+            localRepo.push(editHash2, repo.url(), "master");
 
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
 
             var conversations = mailmanList.conversations(Duration.ofDays(1));
@@ -369,11 +369,11 @@
             assertFalse(email.body().contains(masterHash.abbreviate()));
             assertFalse(email.body().contains("456789AB: Yet more fixes"));
 
             localRepo.checkout(branch, true);
             var editHash3 = CheckableRepository.appendAndCommit(localRepo, "Another branch", "456789AB: Yet more fixes");
-            localRepo.push(editHash3, repo.getUrl(), "another");
+            localRepo.push(editHash3, repo.url(), "another");
 
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
 
             conversations = mailmanList.conversations(Duration.ofDays(1));
@@ -395,14 +395,14 @@
         try (var listServer = new TestMailmanServer();
              var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var repoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(repoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             credentials.commitLock(localRepo);
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var tagStorage = createTagStorage(repo);
@@ -418,32 +418,32 @@
             // No mail should be sent on the first run as there is no history
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.push(editHash, repo.getUrl(), "edit");
+            localRepo.push(editHash, repo.url(), "edit");
             var pr = credentials.createPullRequest(repo, "master", "edit", "RFR: My PR");
 
             // Create a potentially conflicting one
             var otherHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.push(otherHash, repo.getUrl(), "other");
+            localRepo.push(otherHash, repo.url(), "other");
             var otherPr = credentials.createPullRequest(repo, "master", "other", "RFR: My other PR");
 
             // PR hasn't been integrated yet, so there should be no mail
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             // Simulate an RFR email
-            var rfr = Email.create(sender, "RFR: My PR", "PR: " + pr.getWebUrl().toString())
+            var rfr = Email.create(sender, "RFR: My PR", "PR: " + pr.webUrl().toString())
                     .recipient(listAddress)
                     .build();
             mailmanList.post(rfr);
             listServer.processIncoming();
 
             // And an integration
             pr.addComment("Pushed as commit " + editHash.hex() + ".");
-            localRepo.push(editHash, repo.getUrl(), "master");
+            localRepo.push(editHash, repo.url(), "master");
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
 
             var conversations = mailmanList.conversations(Duration.ofDays(1));
             assertEquals(1, conversations.size());
@@ -460,11 +460,11 @@
             assertFalse(email.body().contains(masterHash.abbreviate()));
             assertTrue(email.hasHeader("extra1"));
             assertEquals("value1", email.headerValue("extra1"));
 
             // Now push the other one without a matching PR - PR_ONLY will not generate a mail
-            localRepo.push(otherHash, repo.getUrl(), "master");
+            localRepo.push(otherHash, repo.url(), "master");
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofSeconds(1)));
         }
     }
 
@@ -473,14 +473,14 @@
         try (var listServer = new TestMailmanServer();
              var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var repoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(repoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             credentials.commitLock(localRepo);
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var tagStorage = createTagStorage(repo);
@@ -495,36 +495,36 @@
             // No mail should be sent on the first run as there is no history
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.push(editHash, repo.getUrl(), "edit");
+            localRepo.push(editHash, repo.url(), "edit");
             var pr = credentials.createPullRequest(repo, "master", "edit", "RFR: My PR");
 
             // Create a potentially conflicting one
             var otherHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.push(otherHash, repo.getUrl(), "other");
+            localRepo.push(otherHash, repo.url(), "other");
             var otherPr = credentials.createPullRequest(repo, "master", "other", "RFR: My other PR");
 
             // PR hasn't been integrated yet, so there should be no mail
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             // Simulate an RFR email
-            var rfr = Email.create("RFR: My PR", "PR:\n" + pr.getWebUrl().toString())
+            var rfr = Email.create("RFR: My PR", "PR:\n" + pr.webUrl().toString())
                            .author(EmailAddress.from("duke", "duke@duke.duke"))
                            .recipient(listAddress)
                            .build();
             mailmanList.post(rfr);
             listServer.processIncoming();
 
             // And an integration
             pr.addComment("Pushed as commit " + editHash.hex() + ".");
-            localRepo.push(editHash, repo.getUrl(), "master");
+            localRepo.push(editHash, repo.url(), "master");
 
             // Push the other one without a matching PR
-            localRepo.push(otherHash, repo.getUrl(), "master");
+            localRepo.push(otherHash, repo.url(), "master");
 
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
             listServer.processIncoming();
 
@@ -559,15 +559,15 @@
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory();
              var listServer = new TestMailmanServer()) {
             var repo = credentials.getHostedRepository();
             var localRepoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(localRepoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, repo.repositoryType());
             credentials.commitLock(localRepo);
             var masterHash = localRepo.resolve("master").orElseThrow();
             localRepo.tag(masterHash, "jdk-12+1", "Added tag 1", "Duke", "duke@openjdk.java.net");
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var tagStorage = createTagStorage(repo);
@@ -585,20 +585,20 @@
             // No mail should be sent on the first run as there is no history
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.fetch(repo.getUrl(), "history:history");
+            localRepo.fetch(repo.url(), "history:history");
             localRepo.tag(editHash, "jdk-12+2", "Added tag 2", "Duke", "duke@openjdk.java.net");
             CheckableRepository.appendAndCommit(localRepo, "Another line 1", "34567890: Even more fixes");
             CheckableRepository.appendAndCommit(localRepo, "Another line 2", "45678901: Yet even more fixes");
             var editHash2 = CheckableRepository.appendAndCommit(localRepo, "Another line 3", "56789012: Still even more fixes");
             localRepo.tag(editHash2, "jdk-12+4", "Added tag 3", "Duke", "duke@openjdk.java.net");
             CheckableRepository.appendAndCommit(localRepo, "Another line 4", "67890123: Brand new fixes");
             var editHash3 = CheckableRepository.appendAndCommit(localRepo, "Another line 5", "78901234: More brand new fixes");
             localRepo.tag(editHash3, "jdk-13+0", "Added tag 4", "Duke", "duke@openjdk.java.net");
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
             listServer.processIncoming();
             listServer.processIncoming();
@@ -652,14 +652,14 @@
         try (var listServer = new TestMailmanServer();
              var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var repo = credentials.getHostedRepository();
             var repoFolder = tempFolder.path().resolve("repo");
-            var localRepo = CheckableRepository.init(repoFolder, repo.getRepositoryType());
+            var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             credentials.commitLock(localRepo);
-            localRepo.pushAll(repo.getUrl());
+            localRepo.pushAll(repo.url());
 
             var listAddress = EmailAddress.parse(listServer.createList("test"));
             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
             var mailmanList = mailmanServer.getList(listAddress.address());
             var tagStorage = createTagStorage(repo);
@@ -675,11 +675,11 @@
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
             CheckableRepository.appendAndCommit(localRepo, "Another line", "12345678: Some fixes");
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
-            localRepo.push(editHash, repo.getUrl(), "newbranch1");
+            localRepo.push(editHash, repo.url(), "newbranch1");
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
 
             var conversations = mailmanList.conversations(Duration.ofDays(1));
             var email = conversations.get(0).first();
@@ -694,11 +694,11 @@
             assertEquals("value2", email.headerValue("extra2"));
 
             TestBotRunner.runPeriodicItems(notifyBot);
             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
 
-            localRepo.push(editHash, repo.getUrl(), "newbranch2");
+            localRepo.push(editHash, repo.url(), "newbranch2");
             TestBotRunner.runPeriodicItems(notifyBot);
             listServer.processIncoming();
 
             var newConversation = mailmanList.conversations(Duration.ofDays(1)).stream()
                                              .filter(c -> !c.equals(conversations.get(0)))
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/AllowCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/AllowCommand.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/AllowCommand.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/AllowCommand.java
@@ -29,11 +29,11 @@
 import java.util.List;
 
 public class AllowCommand implements CommandHandler {
     @Override
     public void handle(PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List<Comment> allComments, PrintWriter reply) {
-        var botUser = pr.repository().host().getCurrentUserDetails();
+        var botUser = pr.repository().host().currentUser();
         var vetoers = Veto.vetoers(botUser, allComments);
 
         if (!vetoers.contains(comment.author().id())) {
             reply.println("You have not rejected this change");
             return;
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CensusInstance.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CensusInstance.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CensusInstance.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CensusInstance.java
@@ -47,11 +47,11 @@
         this.namespace = namespace;
     }
 
     private static Repository initialize(HostedRepository repo, String ref, Path folder) {
         try {
-            return Repository.materialize(folder, repo.getUrl(), ref);
+            return Repository.materialize(folder, repo.url(), ref);
         } catch (IOException e) {
             throw new RuntimeException("Failed to retrieve census to " + folder, e);
         }
     }
 
@@ -74,32 +74,32 @@
 
         return namespace;
     }
 
     private static JCheckConfiguration configuration(HostedRepository remoteRepo, String ref) {
-        var confFile = remoteRepo.getFileContents(".jcheck/conf", ref);
+        var confFile = remoteRepo.fileContents(".jcheck/conf", ref);
         return JCheckConfiguration.parse(confFile.lines().collect(Collectors.toList()));
     }
 
     static CensusInstance create(HostedRepository censusRepo, String censusRef, Path folder, PullRequest pr) {
-        var repoName = censusRepo.getUrl().getHost() + "/" + censusRepo.getName();
+        var repoName = censusRepo.url().getHost() + "/" + censusRepo.name();
         var repoFolder = folder.resolve(URLEncoder.encode(repoName, StandardCharsets.UTF_8));
         try {
             var localRepo = Repository.get(repoFolder)
                                       .or(() -> Optional.of(initialize(censusRepo, censusRef, repoFolder)))
                                       .orElseThrow();
-            var hash = localRepo.fetch(censusRepo.getUrl(), censusRef);
+            var hash = localRepo.fetch(censusRepo.url(), censusRef);
             localRepo.checkout(hash, true);
         } catch (IOException e) {
             initialize(censusRepo, censusRef, repoFolder);
         }
 
         try {
-            var configuration = configuration(pr.repository(), pr.getTargetRef());
+            var configuration = configuration(pr.repository(), pr.targetRef());
             var census = Census.parse(repoFolder);
             var project = project(configuration, census);
-            var namespace = namespace(census, pr.repository().getNamespace());
+            var namespace = namespace(census, pr.repository().namespace());
             return new CensusInstance(census, configuration, project, namespace);
         } catch (IOException e) {
             throw new UncheckedIOException("Cannot parse census at " + repoFolder, e);
         }
     }
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckRun.java
@@ -73,11 +73,11 @@
         run.checkStatus();
     }
 
     // For unknown contributors, check that all commits have the same name and email
     private boolean checkCommitAuthor(List<Commit> commits) throws IOException {
-        var author = censusInstance.namespace().get(pr.getAuthor().id());
+        var author = censusInstance.namespace().get(pr.author().id());
         if (author != null) {
             return true;
         }
 
         var names = new HashSet<String>();
@@ -90,19 +90,19 @@
 
         return ((names.size() == 1) && emails.size() == 1);
     }
 
     private Optional<String> mergeSourceRepository() {
-        var repoMatcher = mergeSourcePattern.matcher(pr.getTitle());
+        var repoMatcher = mergeSourcePattern.matcher(pr.title());
         if (!repoMatcher.matches()) {
             return Optional.empty();
         }
         return Optional.of(repoMatcher.group(1));
     }
 
     private Optional<String> mergeSourceBranch() {
-        var branchMatcher = mergeSourcePattern.matcher(pr.getTitle());
+        var branchMatcher = mergeSourcePattern.matcher(pr.title());
         if (!branchMatcher.matches()) {
             return Optional.empty();
         }
         var mergeSourceBranch = branchMatcher.group(2);
         return Optional.of(mergeSourceBranch);
@@ -111,21 +111,21 @@
     // Additional bot-specific checks that are not handled by JCheck
     private List<String> botSpecificChecks() throws IOException {
         var ret = new ArrayList<String>();
 
         var baseHash = prInstance.baseHash();
-        var headHash = pr.getHeadHash();
+        var headHash = pr.headHash();
         var commits = prInstance.localRepo().commits(baseHash + ".." + headHash).asList();
 
         if (!checkCommitAuthor(commits)) {
             var error = "For contributors who are not existing OpenJDK Authors, commit attribution will be taken from " +
                     "the commits in the PR. However, the commits in this PR have inconsistent user names and/or " +
                     "email addresses. Please amend the commits.";
             ret.add(error);
         }
 
-        if (pr.getTitle().startsWith("Merge")) {
+        if (pr.title().startsWith("Merge")) {
             if (commits.size() < 2) {
                 ret.add("A Merge PR must contain at least two commits that are not already present in the target.");
             } else {
                 if (!commits.get(0).isMerge()) {
                     ret.add("The top commit must be a merge commit.");
@@ -133,13 +133,13 @@
 
                 var sourceRepo = mergeSourceRepository();
                 var sourceBranch = mergeSourceBranch();
                 if (sourceBranch.isPresent() && sourceRepo.isPresent()) {
                     try {
-                        var mergeSourceRepo = pr.repository().host().getRepository(sourceRepo.get());
+                        var mergeSourceRepo = pr.repository().host().repository(sourceRepo.get());
                         try {
-                            var sourceHash = prInstance.localRepo().fetch(mergeSourceRepo.getUrl(), sourceBranch.get());
+                            var sourceHash = prInstance.localRepo().fetch(mergeSourceRepo.url(), sourceBranch.get());
                             if (!prInstance.localRepo().isAncestor(commits.get(1).hash(), sourceHash)) {
                                 ret.add("The merge contains commits that are not ancestors of the source");
                             }
                         } catch (IOException e) {
                             ret.add("Could not fetch branch `" + sourceBranch.get() + "` from project `" +
@@ -220,11 +220,11 @@
         } else {
             return "no project role";
         }
     }
 
-    private String formatReviewer(HostUserDetails reviewer) {
+    private String formatReviewer(HostUser reviewer) {
         var namespace = censusInstance.namespace();
         var contributor = namespace.get(reviewer.id());
         if (contributor == null) {
             return reviewer.userName() + " (no known " + namespace.name() + " user name / role)";
         } else {
@@ -243,11 +243,11 @@
     private Optional<String> getReviewersList(List<Review> reviews) {
         var reviewers = reviews.stream()
                                .filter(review -> review.verdict() == Review.Verdict.APPROVED)
                                .map(review -> {
                                    var entry = " * " + formatReviewer(review.reviewer());
-                                   if (!review.hash().equals(pr.getHeadHash())) {
+                                   if (!review.hash().equals(pr.headHash())) {
                                        entry += " **Note!** Review applies to " + review.hash();
                                    }
                                    return entry;
                                })
                                .collect(Collectors.joining("\n"));
@@ -261,21 +261,21 @@
     private String getStatusMessage(List<Review> reviews, PullRequestCheckIssueVisitor visitor) {
         var progressBody = new StringBuilder();
         progressBody.append("## Progress\n");
         progressBody.append(getChecksList(visitor));
 
-        var issue = Issue.fromString(pr.getTitle());
+        var issue = Issue.fromString(pr.title());
         if (issueProject != null && issue.isPresent()) {
             progressBody.append("\n\n## Issue\n");
-            var iss = issueProject.getIssue(issue.get().id());
+            var iss = issueProject.issue(issue.get().id());
             if (iss.isPresent()) {
                 progressBody.append("[");
-                progressBody.append(iss.get().getId());
+                progressBody.append(iss.get().id());
                 progressBody.append("](");
-                progressBody.append(iss.get().getWebUrl());
+                progressBody.append(iss.get().webUrl());
                 progressBody.append("): ");
-                progressBody.append(iss.get().getTitle());
+                progressBody.append(iss.get().title());
                 progressBody.append("\n");
             } else {
                 progressBody.append("⚠️ Failed to retrieve information on issue `");
                 progressBody.append(issue.get().toString());
                 progressBody.append("`.\n");
@@ -289,11 +289,11 @@
 
         return progressBody.toString();
     }
 
     private String updateStatusMessage(String message) {
-        var description = pr.getBody();
+        var description = pr.body();
         var markerIndex = description.lastIndexOf(progressMarker);
 
         if (markerIndex >= 0 && description.substring(markerIndex).equals(message)) {
             log.info("Progress already up to date");
             return description;
@@ -331,21 +331,21 @@
             pr.addComment(body);
         }
     }
 
     private Optional<Comment> findComment(List<Comment> comments, String marker) {
-        var self = pr.repository().host().getCurrentUserDetails();
+        var self = pr.repository().host().currentUser();
         return comments.stream()
                        .filter(comment -> comment.author().equals(self))
                        .filter(comment -> comment.body().contains(marker))
                        .findAny();
     }
 
     private String getMergeReadyComment(String commitMessage, List<Review> reviews, boolean rebasePossible) {
         var message = new StringBuilder();
         message.append("@");
-        message.append(pr.getAuthor().userName());
+        message.append(pr.author().userName());
         message.append(" This change can now be integrated. The commit message will be:\n");
         message.append("```\n");
         message.append(commitMessage);
         message.append("\n```\n");
 
@@ -362,33 +362,33 @@
                 message.append("have been ");
                 message.append(divergingCommits.size());
                 message.append(" commits ");
             }
             message.append("pushed to the `");
-            message.append(pr.getTargetRef());
+            message.append(pr.targetRef());
             message.append("` branch:\n");
             var commitList = divergingCommits.stream()
                     .map(commit -> " * " + commit.hash().hex() + ": " + commit.message().get(0))
                     .collect(Collectors.joining("\n"));
             message.append(commitList);
             message.append("\n\n");
             if (rebasePossible) {
                 message.append("Since there are no conflicts, your changes will automatically be rebased on top of the ");
                 message.append("above commits when integrating. If you prefer to do this manually, please merge `");
-                message.append(pr.getTargetRef());
+                message.append(pr.targetRef());
                 message.append("` into your branch first.\n");
             } else {
                 message.append("Your changes cannot be rebased automatically without conflicts, so you will need to ");
                 message.append("merge `");
-                message.append(pr.getTargetRef());
+                message.append(pr.targetRef());
                 message.append("` into your branch before integrating.\n");
             }
         }
 
-        if (!ProjectPermissions.mayCommit(censusInstance, pr.getAuthor())) {
+        if (!ProjectPermissions.mayCommit(censusInstance, pr.author())) {
             message.append("\n");
-            var contributor = censusInstance.namespace().get(pr.getAuthor().id());
+            var contributor = censusInstance.namespace().get(pr.author().id());
             if (contributor == null) {
                 message.append("As you are not a known OpenJDK [Author](http://openjdk.java.net/bylaws#author), ");
             } else {
                 message.append("As you do not have Committer status in this project, ");
             }
@@ -422,11 +422,11 @@
     }
 
     private String getMergeNoLongerReadyComment() {
         var message = new StringBuilder();
         message.append("@");
-        message.append(pr.getAuthor().userName());
+        message.append(pr.author().userName());
         message.append(" This change is no longer ready for integration - check the PR body for details.\n");
         message.append(mergeReadyMarker);
         return message.toString();
     }
 
@@ -443,11 +443,11 @@
             existing.ifPresent(comment -> pr.updateComment(comment.id(), getMergeNoLongerReadyComment()));
         }
     }
 
     private void checkStatus() {
-        var checkBuilder = CheckBuilder.create("jcheck", pr.getHeadHash());
+        var checkBuilder = CheckBuilder.create("jcheck", pr.headHash());
         checkBuilder.title("Required");
         var censusDomain = censusInstance.configuration().census().domain();
 
         try {
             // Post check in-progress
@@ -495,25 +495,25 @@
                 newLabels.remove("outdated");
             }
 
             // Ensure that the ready for sponsor label is up to date
             newLabels.remove("sponsor");
-            var readyHash = ReadyForSponsorTracker.latestReadyForSponsor(pr.repository().host().getCurrentUserDetails(), comments);
+            var readyHash = ReadyForSponsorTracker.latestReadyForSponsor(pr.repository().host().currentUser(), comments);
             if (readyHash.isPresent() && readyForIntegration) {
                 var acceptedHash = readyHash.get();
-                if (pr.getHeadHash().equals(acceptedHash)) {
+                if (pr.headHash().equals(acceptedHash)) {
                     newLabels.add("sponsor");
                 }
             }
 
             // Calculate current metadata to avoid unnecessary future checks
-            var metadata = workItem.getMetadata(pr.getTitle(), updatedBody, pr.getComments(), activeReviews, newLabels, censusInstance, pr.getTargetHash());
+            var metadata = workItem.getMetadata(pr.title(), updatedBody, pr.comments(), activeReviews, newLabels, censusInstance, pr.targetHash());
             checkBuilder.metadata(metadata);
         } catch (Exception e) {
             log.throwing("CommitChecker", "checkStatus", e);
             newLabels.remove("ready");
-            var metadata = workItem.getMetadata(pr.getTitle(), pr.getBody(), pr.getComments(), activeReviews, newLabels, censusInstance, pr.getTargetHash());
+            var metadata = workItem.getMetadata(pr.title(), pr.body(), pr.comments(), activeReviews, newLabels, censusInstance, pr.targetHash());
             checkBuilder.metadata(metadata);
             checkBuilder.title("Exception occurred during jcheck");
             checkBuilder.summary(e.getMessage());
             checkBuilder.complete(false);
         }
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckWorkItem.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckWorkItem.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckWorkItem.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CheckWorkItem.java
@@ -52,11 +52,11 @@
         this.censusRef = censusRef;
         this.blockingLabels = blockingLabels;
         this.issueProject = issueProject;
     }
 
-    private String encodeReviewer(HostUserDetails reviewer, CensusInstance censusInstance) {
+    private String encodeReviewer(HostUser reviewer, CensusInstance censusInstance) {
         var census = censusInstance.census();
         var project = censusInstance.project();
         var namespace = censusInstance.namespace();
         var contributor = namespace.get(reviewer.id());
         if (contributor == null) {
@@ -77,11 +77,11 @@
                                         .filter(review -> review.verdict() == Review.Verdict.APPROVED)
                                         .map(review -> encodeReviewer(review.reviewer(), censusInstance) + review.hash().hex())
                                         .sorted()
                                         .collect(Collectors.joining());
             var commentString = comments.stream()
-                                        .filter(comment -> comment.author().id().equals(pr.repository().host().getCurrentUserDetails().id()))
+                                        .filter(comment -> comment.author().id().equals(pr.repository().host().currentUser().id()))
                                         .flatMap(comment -> comment.body().lines())
                                         .filter(line -> metadataComments.matcher(line).find())
                                         .collect(Collectors.joining());
             var labelString = labels.stream()
                                     .sorted()
@@ -99,14 +99,14 @@
             throw new RuntimeException("Cannot find SHA-256");
         }
     }
 
     private boolean currentCheckValid(CensusInstance censusInstance, List<Comment> comments, List<Review> reviews, Set<String> labels) {
-        var hash = pr.getHeadHash();
-        var targetHash = pr.getTargetHash();
-        var metadata = getMetadata(pr.getTitle(), pr.getBody(),comments, reviews, labels, censusInstance, targetHash);
-        var currentChecks = pr.getChecks(hash);
+        var hash = pr.headHash();
+        var targetHash = pr.targetHash();
+        var metadata = getMetadata(pr.title(), pr.body(), comments, reviews, labels, censusInstance, targetHash);
+        var currentChecks = pr.checks(hash);
 
         if (currentChecks.containsKey("jcheck")) {
             var check = currentChecks.get("jcheck");
             // Check if the currently running check seems stale - perhaps the checker failed to complete
             if (check.completedAt().isEmpty()) {
@@ -133,20 +133,20 @@
         return false;
     }
 
     @Override
     public String toString() {
-        return "CheckWorkItem@" + pr.repository().getName() + "#" + pr.getId();
+        return "CheckWorkItem@" + pr.repository().name() + "#" + pr.id();
     }
 
     @Override
     public void run(Path scratchPath) {
         // First determine if the current state of the PR has already been checked
         var census = CensusInstance.create(censusRepo, censusRef, scratchPath.resolve("census"), pr);
-        var comments = pr.getComments();
-        var allReviews = pr.getReviews();
-        var labels = new HashSet<>(pr.getLabels());
+        var comments = pr.comments();
+        var allReviews = pr.reviews();
+        var labels = new HashSet<>(pr.labels());
 
         // Filter out the active reviews
         var activeReviews = PullRequestInstance.filterActiveReviews(allReviews);
         if (!currentCheckValid(census, comments, activeReviews, labels)) {
             if (labels.contains("integrated")) {
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandWorkItem.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandWorkItem.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandWorkItem.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/CommandWorkItem.java
@@ -80,11 +80,11 @@
             HelpCommand.external = external;
         }
     }
 
     private List<AbstractMap.SimpleEntry<String, Comment>> findCommandComments(List<Comment> comments) {
-        var self = pr.repository().host().getCurrentUserDetails();
+        var self = pr.repository().host().currentUser();
         var handled = comments.stream()
                               .filter(comment -> comment.author().equals(self))
                               .map(comment -> commandReplyPattern.matcher(comment.body()))
                               .filter(Matcher::find)
                               .map(matcher -> matcher.group(1))
@@ -134,16 +134,16 @@
 
     @Override
     public void run(Path scratchPath) {
         log.info("Looking for merge commands");
 
-        if (pr.getLabels().contains("integrated")) {
+        if (pr.labels().contains("integrated")) {
             log.info("Skip checking for commands in integrated PR");
             return;
         }
 
-        var comments = pr.getComments();
+        var comments = pr.comments();
         var unprocessedCommands = findCommandComments(comments);
         if (unprocessedCommands.isEmpty()) {
             log.fine("No new merge commands found, stopping further processing");
             return;
         }
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ContributorCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ContributorCommand.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ContributorCommand.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ContributorCommand.java
@@ -33,12 +33,12 @@
 public class ContributorCommand implements CommandHandler {
     private static final Pattern commandPattern = Pattern.compile("^(add|remove)\\s+(.*?\\s+<\\S+>)$");
 
     @Override
     public void handle(PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List<Comment> allComments, PrintWriter reply) {
-        if (!comment.author().equals(pr.getAuthor())) {
-            reply.println("Only the author (@" + pr.getAuthor().userName() + ") is allowed to issue the `contributor` command.");
+        if (!comment.author().equals(pr.author())) {
+            reply.println("Only the author (@" + pr.author().userName() + ") is allowed to issue the `contributor` command.");
             return;
         }
 
         var matcher = commandPattern.matcher(args);
         if (!matcher.matches()) {
@@ -51,11 +51,11 @@
             case "add":
                 reply.println(Contributors.addContributorMarker(contributor));
                 reply.println("Contributor `" + contributor.toString() + "` successfully added.");
                 break;
             case "remove":
-                var existing = new HashSet<>(Contributors.contributors(pr.repository().host().getCurrentUserDetails(), allComments));
+                var existing = new HashSet<>(Contributors.contributors(pr.repository().host().currentUser(), allComments));
                 if (existing.contains(contributor)) {
                     reply.println(Contributors.removeContributorMarker(contributor));
                     reply.println("Contributor `" + contributor.toString() + "` successfully removed.");
                 } else {
                     reply.println("Contributor `" + contributor.toString() + "` was not found.");
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Contributors.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Contributors.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Contributors.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Contributors.java
@@ -40,11 +40,11 @@
 
     static String removeContributorMarker(EmailAddress contributor) {
         return String.format(removeMarker, contributor.toString());
     }
 
-    static List<EmailAddress> contributors(HostUserDetails botUser, List<Comment> comments) {
+    static List<EmailAddress> contributors(HostUser botUser, List<Comment> comments) {
         var contributorActions = comments.stream()
                                          .filter(comment -> comment.author().equals(botUser))
                                          .map(comment -> markerPattern.matcher(comment.body()))
                                          .filter(Matcher::find)
                                          .collect(Collectors.toList());
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/IntegrateCommand.java
@@ -47,35 +47,35 @@
                 return Optional.of(inProgress);
             } else {
                 return Optional.of(failure);
             }
         }
-        return Optional.of(String.format(outdated, pr.getHeadHash()));
+        return Optional.of(String.format(outdated, pr.headHash()));
     }
 
     @Override
     public void handle(PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List<Comment> allComments, PrintWriter reply) {
-        if (!comment.author().equals(pr.getAuthor())) {
-            reply.println("Only the author (@" + pr.getAuthor().userName() + ") is allowed to issue the `integrate` command.");
+        if (!comment.author().equals(pr.author())) {
+            reply.println("Only the author (@" + pr.author().userName() + ") is allowed to issue the `integrate` command.");
             return;
         }
 
-        var problem = checkProblem(pr.getChecks(pr.getHeadHash()), "jcheck", pr);
+        var problem = checkProblem(pr.checks(pr.headHash()), "jcheck", pr);
         if (problem.isPresent()) {
             reply.print("Your merge request cannot be fulfilled at this time, as ");
             reply.println(problem.get());
             return;
         }
 
-        if (pr.getLabels().contains("rejected")) {
+        if (pr.labels().contains("rejected")) {
             reply.println("The change is currently blocked from integration by a rejection.");
             return;
         }
 
         // Run a final jcheck to ensure the change has been properly reviewed
         try {
-            var sanitizedUrl = URLEncoder.encode(pr.repository().getWebUrl().toString(), StandardCharsets.UTF_8);
+            var sanitizedUrl = URLEncoder.encode(pr.repository().webUrl().toString(), StandardCharsets.UTF_8);
             var path = scratchPath.resolve("pr.integrate").resolve(sanitizedUrl);
 
             var prInstance = new PullRequestInstance(path, pr);
             var localHash = prInstance.commit(censusInstance.namespace(), censusInstance.configuration().census().domain(), null);
             var rebaseMessage = new StringWriter();
@@ -99,29 +99,29 @@
                       .forEach(reply::println);
                 return;
             }
 
             // Finally check if the author is allowed to perform the actual push
-            if (!pr.getTitle().startsWith("Merge")) {
-                if (!ProjectPermissions.mayCommit(censusInstance, pr.getAuthor())) {
-                    reply.println(ReadyForSponsorTracker.addIntegrationMarker(pr.getHeadHash()));
-                    reply.println("Your change (at version " + pr.getHeadHash() + ") is now ready to be sponsored by a Committer.");
+            if (!pr.title().startsWith("Merge")) {
+                if (!ProjectPermissions.mayCommit(censusInstance, pr.author())) {
+                    reply.println(ReadyForSponsorTracker.addIntegrationMarker(pr.headHash()));
+                    reply.println("Your change (at version " + pr.headHash() + ") is now ready to be sponsored by a Committer.");
                     pr.addLabel("sponsor");
                     return;
                 }
             } else {
-                if (!ProjectPermissions.mayCommit(censusInstance, pr.getAuthor())) {
+                if (!ProjectPermissions.mayCommit(censusInstance, pr.author())) {
                     reply.println("Merges require Committer status.");
                     return;
                 }
             }
 
             // Rebase and push it!
-            if (!localHash.equals(pr.getTargetHash())) {
+            if (!localHash.equals(pr.targetHash())) {
                 reply.println(rebaseMessage.toString());
                 reply.println("Pushed as commit " + rebasedHash.get().hex() + ".");
-                prInstance.localRepo().push(rebasedHash.get(), pr.repository().getUrl(), pr.getTargetRef());
+                prInstance.localRepo().push(rebasedHash.get(), pr.repository().url(), pr.targetRef());
                 pr.setState(PullRequest.State.CLOSED);
                 pr.addLabel("integrated");
                 pr.removeLabel("ready");
             } else {
                 reply.print("Warning! Your commit did not result in any changes! ");
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/LabelerWorkItem.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/LabelerWorkItem.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/LabelerWorkItem.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/LabelerWorkItem.java
@@ -43,11 +43,11 @@
         this.currentLabels = currentLabels;
     }
 
     @Override
     public String toString() {
-        return "LabelerWorkItem@" + pr.repository().getName() + "#" + pr.getId();
+        return "LabelerWorkItem@" + pr.repository().name() + "#" + pr.id();
     }
 
     private Set<String> getLabels(PullRequestInstance prInstance) throws IOException {
         var labels = new HashSet<String>();
         var files = prInstance.changedFiles();
@@ -65,19 +65,19 @@
         return labels;
     }
 
     @Override
     public void run(Path scratchPath) {
-        if (currentLabels.containsKey(pr.getHeadHash())) {
+        if (currentLabels.containsKey(pr.headHash())) {
             return;
         }
         try {
             var prInstance = new PullRequestInstance(scratchPath.resolve("labeler"), pr);
             var newLabels = getLabels(prInstance);
-            var currentLabels = pr.getLabels().stream()
-                    .filter(labelPatterns::containsKey)
-                    .collect(Collectors.toSet());
+            var currentLabels = pr.labels().stream()
+                                  .filter(labelPatterns::containsKey)
+                                  .collect(Collectors.toSet());
 
             // Add all labels not already set
             newLabels.stream()
                      .filter(label -> !currentLabels.contains(label))
                      .forEach(pr::addLabel);
@@ -85,11 +85,11 @@
             // Remove set labels no longer present
             currentLabels.stream()
                          .filter(label -> !newLabels.contains(label))
                          .forEach(pr::removeLabel);
 
-            this.currentLabels.put(pr.getHeadHash(), Boolean.TRUE);
+            this.currentLabels.put(pr.headHash(), Boolean.TRUE);
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
 }
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ProjectPermissions.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ProjectPermissions.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ProjectPermissions.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ProjectPermissions.java
@@ -20,14 +20,14 @@
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 package org.openjdk.skara.bots.pr;
 
-import org.openjdk.skara.host.HostUserDetails;
+import org.openjdk.skara.host.HostUser;
 
 class ProjectPermissions {
-    static boolean mayCommit(CensusInstance censusInstance, HostUserDetails user) {
+    static boolean mayCommit(CensusInstance censusInstance, HostUser user) {
         var census = censusInstance.census();
         var project = censusInstance.project();
         var namespace = censusInstance.namespace();
         int version = census.version().format();
 
@@ -38,11 +38,11 @@
         return project.isCommitter(contributor.username(), version) ||
                 project.isReviewer(contributor.username(), version) ||
                 project.isLead(contributor.username(), version);
     }
 
-    static boolean mayReview(CensusInstance censusInstance, HostUserDetails user) {
+    static boolean mayReview(CensusInstance censusInstance, HostUser user) {
         var census = censusInstance.census();
         var project = censusInstance.project();
         var namespace = censusInstance.namespace();
         int version = census.version().format();
 
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBot.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBot.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBot.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestBot.java
@@ -72,19 +72,19 @@
     PullRequestBot(HostedRepository repo, HostedRepository censusRepo, String censusRef) {
         this(repo, censusRepo, censusRef, Map.of(), Map.of(), Map.of(), Set.of(), Map.of(), null);
     }
 
     private boolean isReady(PullRequest pr) {
-        var labels = new HashSet<>(pr.getLabels());
+        var labels = new HashSet<>(pr.labels());
         for (var readyLabel : readyLabels) {
             if (!labels.contains(readyLabel)) {
                 log.fine("PR is not yet ready - missing label '" + readyLabel + "'");
                 return false;
             }
         }
 
-        var comments = pr.getComments();
+        var comments = pr.comments();
         for (var readyComment : readyComments.entrySet()) {
             var commentFound = false;
             for (var comment : comments) {
                 if (comment.author().userName().equals(readyComment.getKey())) {
                     var matcher = readyComment.getValue().matcher(comment.body());
@@ -121,11 +121,11 @@
         return ret;
     }
 
     @Override
     public List<WorkItem> getPeriodicItems() {
-        return getWorkItems(remoteRepo.getPullRequests());
+        return getWorkItems(remoteRepo.pullRequests());
     }
 
     @Override
     public List<WorkItem> processWebHook(JSONValue body) {
         var webHook = remoteRepo.parseWebHook(body);
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestInstance.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestInstance.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestInstance.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestInstance.java
@@ -44,24 +44,24 @@
     PullRequestInstance(Path localRepoPath, PullRequest pr) throws IOException  {
         this.pr = pr;
         var repository = pr.repository();
 
         // Materialize the PR's target ref
-        localRepo = Repository.materialize(localRepoPath, repository.getUrl(), pr.getTargetRef());
-        targetHash = localRepo.fetch(repository.getUrl(), pr.getTargetRef());
-        headHash = localRepo.fetch(repository.getUrl(), pr.getHeadHash().hex());
+        localRepo = Repository.materialize(localRepoPath, repository.url(), pr.targetRef());
+        targetHash = localRepo.fetch(repository.url(), pr.targetRef());
+        headHash = localRepo.fetch(repository.url(), pr.headHash().hex());
         baseHash = localRepo.mergeBase(targetHash, headHash);
     }
 
     /**
      * The Review list is in chronological order, the latest one from a particular reviewer is the
      * one that is "active".
      * @param allReviews
      * @return
      */
     static List<Review> filterActiveReviews(List<Review> allReviews) {
-        var reviewPerUser = new LinkedHashMap<HostUserDetails, Review>();
+        var reviewPerUser = new LinkedHashMap<HostUser, Review>();
         for (var review : allReviews) {
             reviewPerUser.put(review.reviewer(), review);
         }
         return new ArrayList<>(reviewPerUser.values());
     }
@@ -73,19 +73,19 @@
                           .map(namespace::get)
                           .filter(Objects::nonNull)
                           .map(Contributor::username)
                           .collect(Collectors.toList());
 
-        var comments = pr.getComments();
-        var additionalContributors = Contributors.contributors(pr.repository().host().getCurrentUserDetails(),
+        var comments = pr.comments();
+        var additionalContributors = Contributors.contributors(pr.repository().host().currentUser(),
                                                                comments).stream()
                                                  .map(email -> Author.fromString(email.toString()))
                                                  .collect(Collectors.toList());
 
-        var summary = Summary.summary(pr.repository().host().getCurrentUserDetails(), comments);
-        var issue = Issue.fromString(pr.getTitle());
-        var commitMessageBuilder = issue.map(CommitMessage::title).orElseGet(() -> CommitMessage.title(isMerge ? "Merge" : pr.getTitle()));
+        var summary = Summary.summary(pr.repository().host().currentUser(), comments);
+        var issue = Issue.fromString(pr.title());
+        var commitMessageBuilder = issue.map(CommitMessage::title).orElseGet(() -> CommitMessage.title(isMerge ? "Merge" : pr.title()));
         commitMessageBuilder.contributors(additionalContributors)
                                          .reviewers(reviewers);
         summary.ifPresent(commitMessageBuilder::summary);
 
         return String.join("\n", commitMessageBuilder.format(CommitMessageFormatters.v1));
@@ -95,11 +95,11 @@
         localRepo.checkout(baseHash, true);
         localRepo.squash(headHash);
 
         Author committer;
         Author author;
-        var contributor = namespace.get(pr.getAuthor().id());
+        var contributor = namespace.get(pr.author().id());
 
         if (contributor == null) {
             // Use the information contained in the head commit - jcheck has verified that it contains sane values
             var headCommit = localRepo.commits(headHash.hex() + "^.." + headHash.hex()).asList().get(0);
             author = headCommit.author();
@@ -119,11 +119,11 @@
     }
 
     private Hash commitMerge(List<Review> activeReviews, Namespace namespace, String censusDomain) throws IOException {
         localRepo.checkout(headHash, true);
 
-        var contributor = namespace.get(pr.getAuthor().id());
+        var contributor = namespace.get(pr.author().id());
         if (contributor == null) {
             throw new RuntimeException("Merges can only be performed by Committers");
         }
 
         var author = new Author(contributor.fullName().orElseThrow(), contributor.username() + "@" + censusDomain);
@@ -131,12 +131,12 @@
         var commitMessage = commitMessage(activeReviews, namespace, true);
         return localRepo.amend(commitMessage, author.name(), author.email(), author.name(), author.email());
     }
 
     Hash commit(Namespace namespace, String censusDomain, String sponsorId) throws IOException {
-        var activeReviews = filterActiveReviews(pr.getReviews());
-        if (!pr.getTitle().startsWith("Merge")) {
+        var activeReviews = filterActiveReviews(pr.reviews());
+        if (!pr.title().startsWith("Merge")) {
             return commitSquashed(activeReviews, namespace, censusDomain, sponsorId);
         } else {
             return commitMerge(activeReviews, namespace, censusDomain);
         }
     }
@@ -151,11 +151,11 @@
 
     Optional<Hash> rebase(Hash commitHash, PrintWriter reply) {
         var divergingCommits = divergingCommits();
         if (divergingCommits.size() > 0) {
             reply.print("The following commits have been pushed to ");
-            reply.print(pr.getTargetRef());
+            reply.print(pr.targetRef());
             reply.println(" since your change was applied:");
             divergingCommits.forEach(c -> reply.println(" * " + c.hash().hex() + ": " + c.message().get(0)));
 
             try {
                 var commit = localRepo.lookup(commitHash).orElseThrow();
@@ -165,11 +165,11 @@
                 var hash = localRepo.head();
                 return Optional.of(hash);
             } catch (IOException e) {
                 reply.println();
                 reply.print("It was not possible to rebase your changes automatically. Please merge `");
-                reply.print(pr.getTargetRef());
+                reply.print(pr.targetRef());
                 reply.println("` into your branch and try again.");
                 try {
                     localRepo.checkout(commitHash, true);
                 } catch (IOException e2) {
                     throw new UncheckedIOException(e2);
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestWorkItem.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestWorkItem.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestWorkItem.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/PullRequestWorkItem.java
@@ -40,14 +40,14 @@
     public final boolean concurrentWith(WorkItem other) {
         if (!(other instanceof PullRequestWorkItem)) {
             return true;
         }
         PullRequestWorkItem otherItem = (PullRequestWorkItem)other;
-        if (!pr.getId().equals(otherItem.pr.getId())) {
+        if (!pr.id().equals(otherItem.pr.id())) {
             return true;
         }
-        if (!pr.repository().getName().equals(otherItem.pr.repository().getName())) {
+        if (!pr.repository().name().equals(otherItem.pr.repository().name())) {
             return true;
         }
         return false;
     }
 
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ReadyForSponsorTracker.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ReadyForSponsorTracker.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ReadyForSponsorTracker.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/ReadyForSponsorTracker.java
@@ -35,11 +35,11 @@
 
     static String addIntegrationMarker(Hash hash) {
         return String.format(marker, hash.hex());
     }
 
-    static Optional<Hash> latestReadyForSponsor(HostUserDetails botUser, List<Comment> comments) {
+    static Optional<Hash> latestReadyForSponsor(HostUser botUser, List<Comment> comments) {
         var ready = comments.stream()
                                          .filter(comment -> comment.author().equals(botUser))
                                          .map(comment -> markerPattern.matcher(comment.body()))
                                          .filter(Matcher::find)
                             .map(matcher -> matcher.group(1))
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/RejectCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/RejectCommand.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/RejectCommand.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/RejectCommand.java
@@ -29,29 +29,29 @@
 import java.util.List;
 
 public class RejectCommand implements CommandHandler {
     @Override
     public void handle(PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List<Comment> allComments, PrintWriter reply) {
-        if (pr.getAuthor().equals(comment.author())) {
+        if (pr.author().equals(comment.author())) {
             reply.println("You can't reject your own changes.");
             return;
         }
         if (!ProjectPermissions.mayReview(censusInstance, comment.author())) {
             reply.println("Only [Reviewers](http://openjdk.java.net/bylaws#reviewer) are allowed to reject changes.");
             return;
         }
 
-        var botUser = pr.repository().host().getCurrentUserDetails();
+        var botUser = pr.repository().host().currentUser();
         var vetoers = Veto.vetoers(botUser, allComments);
 
         if (vetoers.contains(comment.author().id())) {
             reply.println("You have already rejected this change.");
             return;
         }
 
         // Notify the author as well
-        reply.print("@" + pr.getAuthor().userName() + " ");
+        reply.print("@" + pr.author().userName() + " ");
 
         reply.println("This change cannot be integrated while the rejection is in place. To lift the rejection, ");
         reply.println("issue an allow command: `/allow`");
 
         reply.println(Veto.addVeto(comment.author()));
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SponsorCommand.java
@@ -34,43 +34,43 @@
 public class SponsorCommand implements CommandHandler {
     private final Logger log = Logger.getLogger("org.openjdk.skara.bots.pr");
 
     @Override
     public void handle(PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List<Comment> allComments, PrintWriter reply) {
-        if (ProjectPermissions.mayCommit(censusInstance, pr.getAuthor())) {
+        if (ProjectPermissions.mayCommit(censusInstance, pr.author())) {
             reply.println("This change does not need sponsoring - the author is allowed to integrate it.");
             return;
         }
         if (!ProjectPermissions.mayCommit(censusInstance, comment.author())) {
             reply.println("Only [Committers](http://openjdk.java.net/bylaws#committer) are allowed to sponsor changes.");
             return;
         }
 
-        var readyHash = ReadyForSponsorTracker.latestReadyForSponsor(pr.repository().host().getCurrentUserDetails(), allComments);
+        var readyHash = ReadyForSponsorTracker.latestReadyForSponsor(pr.repository().host().currentUser(), allComments);
         if (readyHash.isEmpty()) {
-            reply.println("The change author (@" + pr.getAuthor().userName() + ") must issue an `integrate` command before the integration can be sponsored.");
+            reply.println("The change author (@" + pr.author().userName() + ") must issue an `integrate` command before the integration can be sponsored.");
             return;
         }
 
         var acceptedHash = readyHash.get();
-        if (!pr.getHeadHash().equals(acceptedHash)) {
-            reply.print("The PR has been updated since the change author (@" + pr.getAuthor().userName() + ") ");
+        if (!pr.headHash().equals(acceptedHash)) {
+            reply.print("The PR has been updated since the change author (@" + pr.author().userName() + ") ");
             reply.println("issued the `integrate` command - the author must perform this command again.");
             return;
         }
 
-        if (pr.getLabels().contains("rejected")) {
+        if (pr.labels().contains("rejected")) {
             reply.println("The change is currently blocked from integration by a rejection.");
             return;
         }
 
         // Notify the author as well
-        reply.print("@" + pr.getAuthor().userName() + " ");
+        reply.print("@" + pr.author().userName() + " ");
 
         // Execute merge
         try {
-            var sanitizedUrl = URLEncoder.encode(pr.repository().getWebUrl().toString(), StandardCharsets.UTF_8);
+            var sanitizedUrl = URLEncoder.encode(pr.repository().webUrl().toString(), StandardCharsets.UTF_8);
             var path = scratchPath.resolve("pr.sponsor").resolve(sanitizedUrl);
 
             var prInstance = new PullRequestInstance(path, pr);
             var localHash = prInstance.commit(censusInstance.namespace(), censusInstance.configuration().census().domain(),
                                          comment.author().id());
@@ -94,14 +94,14 @@
                       .map(line -> " * " + line)
                       .forEach(reply::println);
                 return;
             }
 
-            if (!localHash.equals(pr.getTargetHash())) {
+            if (!localHash.equals(pr.targetHash())) {
                 reply.println(rebaseMessage.toString());
                 reply.println("Pushed as commit " + rebasedHash.get().hex() + ".");
-                prInstance.localRepo().push(rebasedHash.get(), pr.repository().getUrl(), pr.getTargetRef());
+                prInstance.localRepo().push(rebasedHash.get(), pr.repository().url(), pr.targetRef());
                 pr.setState(PullRequest.State.CLOSED);
                 pr.addLabel("integrated");
                 pr.removeLabel("sponsor");
                 pr.removeLabel("ready");
             } else {
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Summary.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Summary.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Summary.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Summary.java
@@ -36,11 +36,11 @@
     static String setSummaryMarker(String summary) {
         var encodedSummary = Base64.getEncoder().encodeToString(summary.getBytes(StandardCharsets.UTF_8));
         return String.format(summaryMarker, encodedSummary);
     }
 
-    static Optional<String> summary(HostUserDetails botUser, List<Comment> comments) {
+    static Optional<String> summary(HostUser botUser, List<Comment> comments) {
         var summaryActions = comments.stream()
                                          .filter(comment -> comment.author().equals(botUser))
                                          .map(comment -> markerPattern.matcher(comment.body()))
                                          .filter(Matcher::find)
                                          .collect(Collectors.toList());
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SummaryCommand.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SummaryCommand.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SummaryCommand.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/SummaryCommand.java
@@ -29,16 +29,16 @@
 import java.util.List;
 
 public class SummaryCommand implements CommandHandler {
     @Override
     public void handle(PullRequest pr, CensusInstance censusInstance, Path scratchPath, String args, Comment comment, List<Comment> allComments, PrintWriter reply) {
-        if (!comment.author().equals(pr.getAuthor())) {
-            reply.println("Only the author (@" + pr.getAuthor().userName() + ") is allowed to issue the `summary` command.");
+        if (!comment.author().equals(pr.author())) {
+            reply.println("Only the author (@" + pr.author().userName() + ") is allowed to issue the `summary` command.");
             return;
         }
 
-        var currentSummary = Summary.summary(pr.repository().host().getCurrentUserDetails(), allComments);
+        var currentSummary = Summary.summary(pr.repository().host().currentUser(), allComments);
 
         if (args.isBlank()) {
             if (currentSummary.isPresent()) {
                 reply.println("Removing existing summary");
                 reply.println(Summary.setSummaryMarker(""));
diff a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Veto.java b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Veto.java
--- a/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Veto.java
+++ b/bots/pr/src/main/java/org/openjdk/skara/bots/pr/Veto.java
@@ -32,19 +32,19 @@
     private final static String vetoReplyMarker = "<!-- Veto marker (%s) -->";
     private final static Pattern vetoReplyPattern = Pattern.compile("<!-- Veto marker \\((\\S+)\\) -->");
     private final static String approvalReplyMarker = "<!-- Approval marker (%s) -->";
     private final static Pattern approvalReplyPattern = Pattern.compile("<!-- Approval marker \\((\\S+)\\) -->");
 
-    static String addVeto(HostUserDetails vetoer) {
+    static String addVeto(HostUser vetoer) {
         return String.format(vetoReplyMarker, vetoer.id());
     }
 
-    static String removeVeto(HostUserDetails vetoer) {
+    static String removeVeto(HostUser vetoer) {
         return String.format(approvalReplyMarker, vetoer.id());
     }
 
-    static Set<String> vetoers(HostUserDetails botUser, List<Comment> allComments) {
+    static Set<String> vetoers(HostUser botUser, List<Comment> allComments) {
         var vetoers = new HashSet<String>();
         var botComments = allComments.stream()
                 .filter(comment -> comment.author().equals((botUser)))
                 .collect(Collectors.toList());
 
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CheckTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CheckTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CheckTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CheckTests.java
@@ -42,53 +42,53 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check succeeded
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
 
             // The PR should now be ready for review
-            assertTrue(pr.getLabels().contains("rfr"));
-            assertFalse(pr.getLabels().contains("ready"));
+            assertTrue(pr.labels().contains("rfr"));
+            assertFalse(pr.labels().contains("ready"));
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The check should now be successful
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
 
             // The PR should not be flagged as ready for review, at it is already reviewed
-            assertFalse(pr.getLabels().contains("rfr"));
-            assertTrue(pr.getLabels().contains("ready"));
+            assertFalse(pr.labels().contains("rfr"));
+            assertTrue(pr.labels().contains("ready"));
         }
     }
 
     @Test
     void whitespaceIssue(TestInfo testInfo) throws IOException {
@@ -97,68 +97,68 @@
 
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo, "A line with a trailing whitespace   ");
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The PR should not be flagged as ready for review
-            assertFalse(pr.getLabels().contains("rfr"));
+            assertFalse(pr.labels().contains("rfr"));
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check failed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.FAILURE, check.status());
 
             // The PR should not still not be flagged as ready for review
-            assertFalse(pr.getLabels().contains("rfr"));
+            assertFalse(pr.labels().contains("rfr"));
 
             // Remove the trailing whitespace in a new commit
             editHash = CheckableRepository.replaceAndCommit(localRepo, "A line without a trailing whitespace");
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
 
             // Make sure that the push registered
-            var lastHeadHash = pr.getHeadHash();
+            var lastHeadHash = pr.headHash();
             var refreshCount = 0;
             do {
-                pr = author.getPullRequest(pr.getId());
+                pr = author.pullRequest(pr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (pr.getHeadHash().equals(lastHeadHash));
+            } while (pr.headHash().equals(lastHeadHash));
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The PR should not be flagged as ready for review, at it is already reviewed
-            assertFalse(pr.getLabels().contains("rfr"));
+            assertFalse(pr.labels().contains("rfr"));
 
             // The check should now be successful
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
         }
     }
@@ -171,80 +171,80 @@
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
             var commenter = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id())
-                                           .addReviewer(commenter.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id())
+                                           .addReviewer(commenter.host().currentUser().id());
 
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var authorPr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Let the status bot inspect the PR
             TestBotRunner.runPeriodicItems(checkBot);
-            assertFalse(authorPr.getBody().contains("Approvers"));
+            assertFalse(authorPr.body().contains("Approvers"));
 
             // Approve it
-            var reviewerPr = reviewer.getPullRequest(authorPr.getId());
+            var reviewerPr = reviewer.pullRequest(authorPr.id());
             reviewerPr.addReview(Review.Verdict.APPROVED, "Approved");
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Refresh the PR and check that it has been approved
-            authorPr = author.getPullRequest(authorPr.getId());
-            assertTrue(authorPr.getBody().contains("Approvers"));
+            authorPr = author.pullRequest(authorPr.id());
+            assertTrue(authorPr.body().contains("Approvers"));
 
             // Update the file after approval
             editHash = CheckableRepository.appendAndCommit(localRepo, "Now I've gone and changed it");
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
 
             // Make sure that the push registered
-            var lastHeadHash = authorPr.getHeadHash();
+            var lastHeadHash = authorPr.headHash();
             var refreshCount = 0;
             do {
-                authorPr = author.getPullRequest(authorPr.getId());
+                authorPr = author.pullRequest(authorPr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (authorPr.getHeadHash().equals(lastHeadHash));
+            } while (authorPr.headHash().equals(lastHeadHash));
 
             // Check that the review is flagged as stale
             TestBotRunner.runPeriodicItems(checkBot);
-            authorPr = author.getPullRequest(authorPr.getId());
-            assertTrue(authorPr.getBody().contains("Note"));
+            authorPr = author.pullRequest(authorPr.id());
+            assertTrue(authorPr.body().contains("Note"));
 
             // Now we can approve it again
             reviewerPr.addReview(Review.Verdict.APPROVED, "Approved");
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Refresh the PR and check that it has been approved (once) and is no longer stale
-            authorPr = author.getPullRequest(authorPr.getId());
-            assertTrue(authorPr.getBody().contains("Approvers"));
-            assertEquals(1, authorPr.getBody().split("Generated Reviewer", -1).length - 1);
-            assertTrue(authorPr.getReviews().size() >= 1);
-            assertFalse(authorPr.getBody().contains("Note"));
+            authorPr = author.pullRequest(authorPr.id());
+            assertTrue(authorPr.body().contains("Approvers"));
+            assertEquals(1, authorPr.body().split("Generated Reviewer", -1).length - 1);
+            assertTrue(authorPr.reviews().size() >= 1);
+            assertFalse(authorPr.body().contains("Note"));
 
             // Add a review with disapproval
-            var commenterPr = commenter.getPullRequest(authorPr.getId());
+            var commenterPr = commenter.pullRequest(authorPr.id());
             commenterPr.addReview(Review.Verdict.DISAPPROVED, "Disapproved");
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Refresh the PR and check that it still only approved once (but two reviews) and is no longer stale
-            authorPr = author.getPullRequest(authorPr.getId());
-            assertTrue(authorPr.getBody().contains("Approvers"));
-            assertEquals(1, authorPr.getBody().split("Generated Reviewer", -1).length - 1);
-            assertTrue(authorPr.getReviews().size() >= 2);
-            assertFalse(authorPr.getBody().contains("Note"));
+            authorPr = author.pullRequest(authorPr.id());
+            assertTrue(authorPr.body().contains("Approvers"));
+            assertEquals(1, authorPr.body().split("Generated Reviewer", -1).length - 1);
+            assertTrue(authorPr.reviews().size() >= 2);
+            assertFalse(authorPr.body().contains("Note"));
         }
     }
 
     @Test
     void selfReview(TestInfo testInfo) throws IOException {
@@ -252,38 +252,38 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(author.host().getCurrentUserDetails().id());
+                                           .addReviewer(author.host().currentUser().id());
 
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var authorPr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Let the status bot inspect the PR
             TestBotRunner.runPeriodicItems(checkBot);
-            assertFalse(authorPr.getBody().contains("Approvers"));
+            assertFalse(authorPr.body().contains("Approvers"));
 
             // Approve it
             authorPr.addReview(Review.Verdict.APPROVED, "Approved");
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Refresh the PR and check that it has been approved
-            authorPr = author.getPullRequest(authorPr.getId());
-            assertTrue(authorPr.getBody().contains("Approvers"));
+            authorPr = author.pullRequest(authorPr.id());
+            assertTrue(authorPr.body().contains("Approvers"));
 
             // Verify that the check failed
-            var checks = authorPr.getChecks(editHash);
+            var checks = authorPr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.FAILURE, check.status());
         }
     }
@@ -294,50 +294,50 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make two changes with different authors
             CheckableRepository.appendAndCommit(localRepo, "First edit", "Edit by number 1",
                                                 "number1", "number1@none.none");
             var editHash = CheckableRepository.appendAndCommit(localRepo, "Second edit", "Edit by number 2",
                                                                "number2", "number2@none.none");
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check failed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.FAILURE, check.status());
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The check should still be failing
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("jcheck");
             assertEquals(CheckStatus.FAILURE, check.status());
 
             // The PR should not be flagged as ready for review, as multiple committers is a problem
-            assertFalse(pr.getLabels().contains("rfr"));
+            assertFalse(pr.labels().contains("rfr"));
         }
     }
 
     @Test
     void updatedContentFailsCheck(TestInfo testInfo) throws IOException {
@@ -345,76 +345,76 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check passed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
 
             // The PR should now be ready for review
-            assertTrue(pr.getLabels().contains("rfr"));
-            assertFalse(pr.getLabels().contains("ready"));
+            assertTrue(pr.labels().contains("rfr"));
+            assertFalse(pr.labels().contains("ready"));
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The check should now be successful
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
 
             // The PR should not be flagged as ready for review, at it is already reviewed
-            assertFalse(pr.getLabels().contains("rfr"));
-            assertTrue(pr.getLabels().contains("ready"));
+            assertFalse(pr.labels().contains("rfr"));
+            assertTrue(pr.labels().contains("ready"));
 
             var addedHash = CheckableRepository.appendAndCommit(localRepo, "trailing whitespace   ");
-            localRepo.push(addedHash, author.getUrl(), "edit");
+            localRepo.push(addedHash, author.url(), "edit");
 
             // Make sure that the push registered
-            var lastHeadHash = pr.getHeadHash();
+            var lastHeadHash = pr.headHash();
             var refreshCount = 0;
             do {
-                pr = author.getPullRequest(pr.getId());
+                pr = author.pullRequest(pr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (pr.getHeadHash().equals(lastHeadHash));
+            } while (pr.headHash().equals(lastHeadHash));
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The PR is now neither ready for review nor integration
-            assertFalse(pr.getLabels().contains("rfr"));
-            assertFalse(pr.getLabels().contains("ready"));
+            assertFalse(pr.labels().contains("rfr"));
+            assertFalse(pr.labels().contains("ready"));
 
             // The check should now be failing
-            checks = pr.getChecks(addedHash);
+            checks = pr.checks(addedHash);
             assertEquals(1, checks.size());
             check = checks.get("jcheck");
             assertEquals(CheckStatus.FAILURE, check.status());
         }
     }
@@ -428,59 +428,59 @@
 
             // This test is only relevant on hosts not supporting proper review comment bodies
             assumeTrue(!author.host().supportsReviewBody());
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
-            var comments = pr.getComments();
+            var comments = pr.comments();
             var commentCount = comments.size();
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
 
             // There should now be two additional comments
-            comments = pr.getComments();
+            comments = pr.comments();
             assertEquals(commentCount + 2, comments.size());
             var comment = comments.get(commentCount);
-            assertTrue(comment.body().contains(reviewer.host().getCurrentUserDetails().userName()));
+            assertTrue(comment.body().contains(reviewer.host().currentUser().userName()));
             assertTrue(comment.body().contains("approved"));
 
             // Drop the review
             approvalPr.addReview(Review.Verdict.NONE, "Unreviewed");
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
 
             // There should now be yet another comment
-            comments = pr.getComments();
+            comments = pr.comments();
             assertEquals(commentCount + 3, comments.size());
             comment = comments.get(commentCount + 2);
-            assertTrue(comment.body().contains(reviewer.host().getCurrentUserDetails().userName()));
+            assertTrue(comment.body().contains(reviewer.host().currentUser().userName()));
             assertTrue(comment.body().contains("comment"));
 
             // No changes should not generate additional comments
             TestBotRunner.runPeriodicItems(checkBot);
-            comments = pr.getComments();
+            comments = pr.comments();
             assertEquals(commentCount + 3, comments.size());
         }
     }
 
     @Test
@@ -490,27 +490,27 @@
              var pushedFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Get all messages up to date
             TestBotRunner.runPeriodicItems(mergeBot);
 
@@ -518,17 +518,17 @@
             localRepo.checkout(masterHash, true);
             var unrelated = localRepo.root().resolve("unrelated.txt");
             Files.writeString(unrelated, "Hello");
             localRepo.add(unrelated);
             var unrelatedHash = localRepo.commit("Unrelated", "X", "x@y.z");
-            localRepo.push(unrelatedHash, author.getUrl(), "master");
+            localRepo.push(unrelatedHash, author.url(), "master");
 
             // Let the bot see the changes
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an ok message
-            var updated = pr.getComments().stream()
+            var updated = pr.comments().stream()
                             .filter(comment -> comment.body().contains("there has been 1 commit"))
                             .filter(comment -> comment.body().contains("please merge"))
                             .count();
             assertEquals(1, updated);
         }
@@ -541,70 +541,70 @@
              var pushedFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Get all messages up to date
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push something conflicting to master
             localRepo.checkout(masterHash, true);
             var conflictingHash = CheckableRepository.appendAndCommit(localRepo, "This looks like a conflict");
-            localRepo.push(conflictingHash, author.getUrl(), "master");
+            localRepo.push(conflictingHash, author.url(), "master");
 
             // Let the bot see the changes
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with that there is a conflict
-            var updated = pr.getComments().stream()
+            var updated = pr.comments().stream()
                             .filter(comment -> comment.body().contains("there has been 1 commit"))
                             .filter(comment -> comment.body().contains("cannot be rebased automatically"))
                             .count();
             assertEquals(1, updated);
 
             // The PR should be flagged as outdated
-            assertTrue(pr.getLabels().contains("outdated"));
+            assertTrue(pr.labels().contains("outdated"));
 
             // But it should still pass jcheck
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
 
             // Restore the master branch
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Let the bot see the changes
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should no longer detect a conflict
-            updated = pr.getComments().stream()
-                            .filter(comment -> comment.body().contains("change can now be integrated"))
-                            .count();
+            updated = pr.comments().stream()
+                        .filter(comment -> comment.body().contains("change can now be integrated"))
+                        .count();
             assertEquals(1, updated);
 
             // The PR should not be flagged as outdated
-            assertFalse(pr.getLabels().contains("outdated"));
+            assertFalse(pr.labels().contains("outdated"));
         }
     }
 
     @Test
     void blockingLabel(TestInfo testInfo) throws IOException {
@@ -612,48 +612,48 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master", Map.of(), Map.of(),
                                               Map.of("block", "Test Blocker"), Set.of(), Map.of());
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
             pr.addLabel("block");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check failed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.FAILURE, check.status());
             assertTrue(check.summary().orElseThrow().contains("Test Blocker"));
 
             // The PR should not yet be ready for review
-            assertTrue(pr.getLabels().contains("block"));
-            assertFalse(pr.getLabels().contains("rfr"));
-            assertFalse(pr.getLabels().contains("ready"));
+            assertTrue(pr.labels().contains("block"));
+            assertFalse(pr.labels().contains("rfr"));
+            assertFalse(pr.labels().contains("ready"));
 
             // Check the status again
             pr.removeLabel("block");
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The PR should now be ready for review
-            assertTrue(pr.getLabels().contains("rfr"));
-            assertFalse(pr.getLabels().contains("ready"));
+            assertTrue(pr.labels().contains("rfr"));
+            assertFalse(pr.labels().contains("ready"));
         }
     }
 
     @Test
     void missingReadyLabel(TestInfo testInfo) throws IOException {
@@ -661,41 +661,41 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master", Map.of(), Map.of(),
                                               Map.of(), Set.of("good-to-go"), Map.of());
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that no checks have been run
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(0, checks.size());
 
             // The PR should not yet be ready for review
-            assertFalse(pr.getLabels().contains("rfr"));
+            assertFalse(pr.labels().contains("rfr"));
 
             // Check the status again
             pr.addLabel("good-to-go");
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The PR should now be ready for review
-            assertTrue(pr.getLabels().contains("rfr"));
+            assertTrue(pr.labels().contains("rfr"));
         }
     }
 
     @Test
     void missingReadyComment(TestInfo testInfo) throws IOException {
@@ -703,42 +703,42 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master", Map.of(), Map.of(),
-                                              Map.of(), Set.of(), Map.of(reviewer.host().getCurrentUserDetails().userName(), Pattern.compile("proceed")));
+                                              Map.of(), Set.of(), Map.of(reviewer.host().currentUser().userName(), Pattern.compile("proceed")));
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that no checks have been run
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(0, checks.size());
 
             // The PR should not yet be ready for review
-            assertFalse(pr.getLabels().contains("rfr"));
+            assertFalse(pr.labels().contains("rfr"));
 
             // Check the status again
-            var reviewerPr = reviewer.getPullRequest(pr.getId());
+            var reviewerPr = reviewer.pullRequest(pr.id());
             reviewerPr.addComment("proceed");
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The PR should now be ready for review
-            assertTrue(pr.getLabels().contains("rfr"));
+            assertTrue(pr.labels().contains("rfr"));
         }
     }
 
     @Test
     void issueIssue(TestInfo testInfo) throws IOException {
@@ -746,31 +746,31 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master", Map.of(), Map.of(),
                                               Map.of(), Set.of(), Map.of());
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), Path.of("appendable.txt"),
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), Path.of("appendable.txt"),
                                                      Set.of("issues"));
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check failed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.FAILURE, check.status());
 
             // Add an issue to the title
@@ -778,11 +778,11 @@
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The check should now be successful
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
         }
     }
@@ -794,81 +794,81 @@
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
             var issues = credentials.getIssueProject();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master", Map.of(), Map.of(),
                                               Map.of(), Set.of(), Map.of(), issues);
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), Path.of("appendable.txt"),
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), Path.of("appendable.txt"),
                                                      Set.of("issues"));
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             var issue1 = issues.createIssue("My first issue", List.of("Hello"));
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", issue1.getId() + ": This is a pull request");
+            localRepo.push(editHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", issue1.id() + ": This is a pull request");
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The check should be successful
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
 
             // And the body should contain the issue title
-            assertTrue(pr.getBody().contains("My first issue"));
+            assertTrue(pr.body().contains("My first issue"));
 
             // Change the issue
             var issue2 = issues.createIssue("My second issue", List.of("Body"));
-            pr.setTitle(issue2.getId() + ": This is a pull request");
+            pr.setTitle(issue2.id() + ": This is a pull request");
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
 
             // The body should contain the updated issue title
-            assertFalse(pr.getBody().contains("My first issue"));
-            assertTrue(pr.getBody().contains("My second issue"));
+            assertFalse(pr.body().contains("My first issue"));
+            assertTrue(pr.body().contains("My second issue"));
 
             // Use an invalid issue key
-            var issueKey = issue1.getId().replace("TEST", "BADPROJECT");
+            var issueKey = issue1.id().replace("TEST", "BADPROJECT");
             pr.setTitle(issueKey + ": This is a pull request");
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
-            assertFalse(pr.getBody().contains("My first issue"));
-            assertFalse(pr.getBody().contains("My second issue"));
-            assertTrue(pr.getBody().contains("Failed to retrieve"));
+            assertFalse(pr.body().contains("My first issue"));
+            assertFalse(pr.body().contains("My second issue"));
+            assertTrue(pr.body().contains("Failed to retrieve"));
 
             // Now drop the issue key
-            issueKey = issue1.getId().replace("TEST-", "");
+            issueKey = issue1.id().replace("TEST-", "");
             pr.setTitle(issueKey + ": This is a pull request");
 
             // The body should now contain the updated issue title
             TestBotRunner.runPeriodicItems(checkBot);
-            assertTrue(pr.getBody().contains("My first issue"));
-            assertFalse(pr.getBody().contains("My second issue"));
+            assertTrue(pr.body().contains("My first issue"));
+            assertFalse(pr.body().contains("My second issue"));
 
             // Now enter an invalid issue id
             pr.setTitle("2384848: This is a pull request");
 
             // Check the status again
             TestBotRunner.runPeriodicItems(checkBot);
-            assertFalse(pr.getBody().contains("My first issue"));
-            assertFalse(pr.getBody().contains("My second issue"));
-            assertTrue(pr.getBody().contains("Failed to retrieve"));
+            assertFalse(pr.body().contains("My first issue"));
+            assertFalse(pr.body().contains("My second issue"));
+            assertTrue(pr.body().contains("Failed to retrieve"));
 
             // The check should still be successful though
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
         }
     }
@@ -878,42 +878,42 @@
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Verify no checks exists
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(0, checks.size());
 
             // Create a check that is running
             var original = CheckBuilder.create("jcheck", editHash)
                                        .title("jcheck title")
                                        .summary("jcheck summary")
                                        .build();
             pr.createCheck(original);
 
             // Verify check is created
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var retrieved = checks.get("jcheck");
             assertEquals("jcheck title", retrieved.title().get());
             assertEquals("jcheck summary", retrieved.summary().get());
             assertEquals(CheckStatus.IN_PROGRESS, retrieved.status());
 
             // Cancel the check
             var cancelled = CheckBuilder.from(retrieved).cancel().build();
             pr.updateCheck(cancelled);
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             retrieved = checks.get("jcheck");
             assertEquals("jcheck title", retrieved.title().get());
             assertEquals("jcheck summary", retrieved.summary().get());
             assertEquals(CheckStatus.CANCELLED, retrieved.status());
@@ -926,47 +926,47 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Enable a new check in the target branch
             localRepo.checkout(masterHash, true);
-            CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), Path.of("appendable.txt"),
+            CheckableRepository.init(tempFolder.path(), author.repositoryType(), Path.of("appendable.txt"),
                                      Set.of("author", "reviewers", "whitespace", "issues"));
             var headHash = localRepo.resolve("HEAD").orElseThrow();
-            localRepo.push(headHash, author.getUrl(), "master", true);
+            localRepo.push(headHash, author.url(), "master", true);
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check failed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertTrue(check.summary().orElseThrow().contains("commit message does not reference any issue"));
             assertEquals(CheckStatus.FAILURE, check.status());
 
             // Adjust the title to conform and check the status again
             pr.setTitle("12345: This is a pull request");
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check passed
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
         }
     }
@@ -977,35 +977,35 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var checkBot = new PullRequestBot(author, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit",
                                                    "This is a pull request", true);
 
             // Check the status
             TestBotRunner.runPeriodicItems(checkBot);
 
             // Verify that the check succeeded
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("jcheck");
             assertEquals(CheckStatus.SUCCESS, check.status());
 
             // The PR should still not be ready for review as it is a draft
-            assertFalse(pr.getLabels().contains("rfr"));
-            assertFalse(pr.getLabels().contains("ready"));
+            assertFalse(pr.labels().contains("rfr"));
+            assertFalse(pr.labels().contains("ready"));
         }
     }
 }
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommandTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommandTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommandTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/CommandTests.java
@@ -38,30 +38,30 @@
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue an invalid command
             pr.addComment("/howdy");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("Unknown command"))
                           .filter(comment -> comment.body().contains("help"))
                           .count();
             assertEquals(1, error);
         }
@@ -73,30 +73,30 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue an invalid command
             pr.addComment("/help");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with some help
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("Available commands"))
                           .filter(comment -> comment.body().contains("help"))
                           .filter(comment -> comment.body().contains("integrate"))
                           .filter(comment -> comment.body().contains("sponsor"))
                           .count();
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/ContributorTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/ContributorTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/ContributorTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/ContributorTests.java
@@ -40,24 +40,24 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id())
-                                           .addCommitter(author.host().getCurrentUserDetails().id());
+                                           .addReviewer(integrator.host().currentUser().id())
+                                           .addCommitter(author.host().currentUser().id());
             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue an invalid command
             pr.addComment("/contributor hello");
             TestBotRunner.runPeriodicItems(prBot);
@@ -89,40 +89,40 @@
             // Now add someone back again
             pr.addComment("/contributor add Test Person <test@test.test>");
             TestBotRunner.runPeriodicItems(prBot);
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
             TestBotRunner.runPeriodicItems(prBot);
             TestBotRunner.runPeriodicItems(prBot);
 
             // The commit message preview should contain the contributor
-            var creditLine = pr.getComments().stream()
+            var creditLine = pr.comments().stream()
                                .flatMap(comment -> comment.body().lines())
                                .filter(line -> line.contains("Test Person <test@test.test>"))
                                .filter(line -> line.contains("Co-authored-by"))
                                .findAny()
                                .orElseThrow();
             assertEquals("Co-authored-by: Test Person <test@test.test>", creditLine);
 
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("change can now be integrated"))
                            .count();
             assertEquals(1, pushed);
 
             // Add a second person
             pr.addComment("/contributor add Another Person <another@test.test>");
             TestBotRunner.runPeriodicItems(prBot);
             TestBotRunner.runPeriodicItems(prBot);
 
-            creditLine = pr.getComments().stream()
-                               .flatMap(comment -> comment.body().lines())
-                               .filter(line -> line.contains("Another Person <another@test.test>"))
-                               .filter(line -> line.contains("Co-authored-by"))
-                               .findAny()
-                               .orElseThrow();
+            creditLine = pr.comments().stream()
+                           .flatMap(comment -> comment.body().lines())
+                           .filter(line -> line.contains("Another Person <another@test.test>"))
+                           .filter(line -> line.contains("Co-authored-by"))
+                           .findAny()
+                           .orElseThrow();
             assertEquals("Co-authored-by: Another Person <another@test.test>", creditLine);
 
             // Integrate
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(prBot);
@@ -130,11 +130,11 @@
             // The bot should reply with an ok message
             assertLastCommentContains(pr,"Pushed as commit");
 
             // The change should now be present on the master branch
             var pushedFolder = tempFolder.path().resolve("pushed");
-            var pushedRepo = Repository.materialize(pushedFolder, author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedFolder, author.url(), "master");
             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
 
             var headHash = pushedRepo.resolve("HEAD").orElseThrow();
             var headCommit = pushedRepo.commits(headHash.hex() + "^.." + headHash.hex()).asList().get(0);
 
@@ -154,31 +154,31 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var external = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue a contributor command not as the PR author
-            var externalPr = external.getPullRequest(pr.getId());
+            var externalPr = external.pullRequest(pr.id());
             externalPr.addComment("/contributor add Test Person <test@test.test>");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("Only the author"))
                           .count();
             assertEquals(1, error);
         }
     }
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IntegrateTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IntegrateTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IntegrateTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/IntegrateTests.java
@@ -44,58 +44,58 @@
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Attempt a merge (the bot should only process the first one)
             pr.addComment("/integrate");
             pr.addComment("/integrate");
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an ok message
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
 
             // The change should now be present on the master branch
-            var pushedRepo = Repository.materialize(pushedFolder.path(), author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedFolder.path(), author.url(), "master");
             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
 
             var headHash = pushedRepo.resolve("HEAD").orElseThrow();
             var headCommit = pushedRepo.commits(headHash.hex() + "^.." + headHash.hex()).asList().get(0);
 
             // Author and committer should be the same
             assertEquals("Generated Committer 1", headCommit.author().name());
             assertEquals("integrationcommitter1@openjdk.java.net", headCommit.author().email());
             assertEquals("Generated Committer 1", headCommit.committer().name());
             assertEquals("integrationcommitter1@openjdk.java.net", headCommit.committer().email());
-            assertTrue(pr.getLabels().contains("integrated"));
+            assertTrue(pr.labels().contains("integrated"));
 
             // Ready label should have been removed
-            assertFalse(pr.getLabels().contains("ready"));
+            assertFalse(pr.labels().contains("ready"));
         }
     }
 
     @Test
     void reviewersRetained(TestInfo testInfo) throws IOException {
@@ -105,44 +105,44 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var committer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addCommitter(committer.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addCommitter(committer.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Review it twice
-            var integratorPr = integrator.getPullRequest(pr.getId());
-            var committerPr = committer.getPullRequest(pr.getId());
+            var integratorPr = integrator.pullRequest(pr.id());
+            var committerPr = committer.pullRequest(pr.id());
             integratorPr.addReview(Review.Verdict.APPROVED, "Approved");
             committerPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Attempt a merge
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an ok message
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
 
             // The change should now be present on the master branch
-            var pushedRepo = Repository.materialize(pushedFolder.path(), author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedFolder.path(), author.url(), "master");
             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
             var headCommit = pushedRepo.commits("HEAD").asList().get(0);
             assertTrue(String.join("", headCommit.message())
                              .matches(".*Reviewed-by: integrationreviewer3, integrationcommitter2$"),
                        String.join("", headCommit.message()));
@@ -154,30 +154,30 @@
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Attempt a merge, do not run the check from the bot
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot, item -> item instanceof CheckWorkItem);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("merge request cannot be fulfilled at this time"))
                           .filter(comment -> comment.body().contains("status check"))
                           .filter(comment -> comment.body().contains("has not been performed on commit"))
                           .count();
             assertEquals(1, error);
@@ -189,72 +189,72 @@
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository - but without any checks enabled
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), Path.of("appendable.txt"),
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType(), Path.of("appendable.txt"),
                                                      Set.of());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Now enable checks
             localRepo.checkout(masterHash, true);
-            CheckableRepository.init(tempFolder.path(), author.getRepositoryType(), Path.of("appendable.txt"),
+            CheckableRepository.init(tempFolder.path(), author.repositoryType(), Path.of("appendable.txt"),
                                      Set.of("author", "reviewers", "whitespace"));
             var updatedHash = localRepo.resolve("HEAD").orElseThrow();
-            localRepo.push(updatedHash, author.getUrl(), "master", true);
+            localRepo.push(updatedHash, author.url(), "master", true);
 
             // Attempt a merge
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("merge request cannot be fulfilled at this time"))
                           .filter(comment -> comment.body().contains("failed the final jcheck"))
                           .count();
-            assertEquals(1, error, pr.getComments().stream().map(Comment::body).collect(Collectors.joining("\n---\n")));
+            assertEquals(1, error, pr.comments().stream().map(Comment::body).collect(Collectors.joining("\n---\n")));
         }
     }
 
     @Test
     void failedCheck(TestInfo testInfo) throws IOException {
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo, "trailing whitespace   ");
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Attempt a merge
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("merge request cannot be fulfilled at this time"))
                           .filter(comment -> comment.body().contains("status check"))
                           .filter(comment -> comment.body().contains("did not complete successfully"))
                           .count();
             assertEquals(1, error);
@@ -266,40 +266,40 @@
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Flag it as checked
             var check = CheckBuilder.create("testcheck", editHash);
             pr.createCheck(check.build());
             check.complete(true);
             pr.updateCheck(check.build());
 
             // Now push another change
             var updatedHash = CheckableRepository.appendAndCommit(localRepo, "Yet another line");
-            localRepo.push(updatedHash, author.getUrl(), "edit", true);
+            localRepo.push(updatedHash, author.url(), "edit", true);
 
             // Attempt a merge - avoid running checks from the bot
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot, item -> item instanceof CheckWorkItem);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("merge request cannot be fulfilled at this time"))
                           .filter(comment -> comment.body().contains("status check"))
                           .filter(comment -> comment.body().contains("has not been performed on commit"))
                           .count();
             assertEquals(1, error);
@@ -312,79 +312,79 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot see it (a few times)
             TestBotRunner.runPeriodicItems(mergeBot);
             TestBotRunner.runPeriodicItems(mergeBot);
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an instructional message (and only one)
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("change can now be integrated"))
                            .filter(comment -> comment.body().contains("Reviewed-by: integrationreviewer3"))
                            .count();
             assertEquals(1, pushed);
 
             // Ensure that the bot doesn't pick up on commands in the instructional message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("Only the author"))
                           .count();
             assertEquals(0, error);
 
             // Drop the approval
             approvalPr.addReview(Review.Verdict.DISAPPROVED, "Disapproved");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The instructional message should have been updated
-            pushed = pr.getComments().stream()
+            pushed = pr.comments().stream()
                        .filter(comment -> comment.body().contains("no longer ready for integration"))
                        .count();
             assertEquals(1, pushed);
 
             // Restore the approval
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The instructional message should have been updated
-            pushed = pr.getComments().stream()
-                           .filter(comment -> comment.body().contains("change can now be integrated"))
-                           .filter(comment -> comment.body().contains("Reviewed-by: integrationreviewer3"))
-                           .count();
+            pushed = pr.comments().stream()
+                       .filter(comment -> comment.body().contains("change can now be integrated"))
+                       .filter(comment -> comment.body().contains("Reviewed-by: integrationreviewer3"))
+                       .count();
             assertEquals(1, pushed);
 
             // Approve it as yet another user
-            var reviewerPr = reviewer.getPullRequest(pr.getId());
+            var reviewerPr = reviewer.pullRequest(pr.id());
             reviewerPr.addReview(Review.Verdict.APPROVED, "Approved");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The instructional message should have been updated
-            pushed = pr.getComments().stream()
-                           .filter(comment -> comment.body().contains("change can now be integrated"))
-                           .filter(comment -> comment.body().contains("Reviewed-by: integrationreviewer3, integrationreviewer2"))
-                           .count();
+            pushed = pr.comments().stream()
+                       .filter(comment -> comment.body().contains("change can now be integrated"))
+                       .filter(comment -> comment.body().contains("Reviewed-by: integrationreviewer3, integrationreviewer2"))
+                       .count();
             assertEquals(1, pushed);
         }
     }
 
     @Test
@@ -394,31 +394,31 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var external = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue a merge command not as the PR author
-            var externalPr = external.getPullRequest(pr.getId());
+            var externalPr = external.pullRequest(pr.id());
             externalPr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("Only the author"))
                           .count();
             assertEquals(1, error);
         }
     }
@@ -430,52 +430,52 @@
              var pushedFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Push something unrelated to master
             localRepo.checkout(masterHash, true);
             var unrelated = localRepo.root().resolve("unrelated.txt");
             Files.writeString(unrelated, "Hello");
             localRepo.add(unrelated);
             var unrelatedHash = localRepo.commit("Unrelated", "X", "x@y.z");
-            localRepo.push(unrelatedHash, author.getUrl(), "master");
+            localRepo.push(unrelatedHash, author.url(), "master");
 
             // Attempt a merge (the bot should only process the first one)
             pr.addComment("/integrate");
             pr.addComment("/integrate");
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an ok message
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .filter(comment -> comment.body().contains("commit was automatically rebased without conflicts"))
                            .count();
             assertEquals(1, pushed);
 
             // The change should now be present on the master branch
-            var pushedRepo = Repository.materialize(pushedFolder.path(), author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedFolder.path(), author.url(), "master");
             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
         }
     }
 
     @Test
@@ -484,47 +484,47 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check it
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Pre-push to cause a failure
-            localRepo.push(editHash, author.getUrl(), "master");
+            localRepo.push(editHash, author.url(), "master");
 
             // Attempt a merge (without triggering another check)
             pr.addComment("/integrate");
             assertThrows(RuntimeException.class, () -> TestBotRunner.runPeriodicItems(mergeBot, wi -> wi instanceof CheckWorkItem));
 
             // Restore the master branch
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // The bot should now retry
             TestBotRunner.runPeriodicItems(mergeBot, wi -> wi instanceof CheckWorkItem);
 
             // The bot should reply with an ok message
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
         }
     }
@@ -535,40 +535,40 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Push something conflicting to master
             localRepo.checkout(masterHash, true);
             var conflictingHash = CheckableRepository.appendAndCommit(localRepo, "This looks like a conflict");
-            localRepo.push(conflictingHash, author.getUrl(), "master");
+            localRepo.push(conflictingHash, author.url(), "master");
 
             // Attempt an integration
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("It was not possible to rebase your changes automatically."))
                           .filter(comment -> comment.body().contains("Please merge `master`"))
                           .count();
             assertEquals(1, error);
         }
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/LabelerTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/LabelerTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/LabelerTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/LabelerTests.java
@@ -42,84 +42,84 @@
             var reviewer = credentials.getHostedRepository();
 
             var labelPatterns = Map.of("test1", List.of(Pattern.compile("a.txt")),
                                        "test2", List.of(Pattern.compile("b.txt")));
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var labelBot = new PullRequestBot(author, censusBuilder.build(), "master", labelPatterns, Map.of(), Map.of(), Set.of(), Map.of());
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path();
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Check the status - only the rfr label should be set
             TestBotRunner.runPeriodicItems(labelBot);
-            assertEquals(Set.of("rfr"), new HashSet<>(pr.getLabels()));
+            assertEquals(Set.of("rfr"), new HashSet<>(pr.labels()));
 
             var fileA = localRepoFolder.resolve("a.txt");
             Files.writeString(fileA, "Hello");
             localRepo.add(fileA);
             var hashA = localRepo.commit("test1", "test", "test@test");
-            localRepo.push(hashA, author.getUrl(), "edit");
+            localRepo.push(hashA, author.url(), "edit");
 
             // Make sure that the push registered
-            var lastHeadHash = pr.getHeadHash();
+            var lastHeadHash = pr.headHash();
             var refreshCount = 0;
             do {
-                pr = author.getPullRequest(pr.getId());
+                pr = author.pullRequest(pr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (pr.getHeadHash().equals(lastHeadHash));
+            } while (pr.headHash().equals(lastHeadHash));
 
             // Check the status - there should now be a test1 label
             TestBotRunner.runPeriodicItems(labelBot);
-            assertEquals(Set.of("rfr", "test1"), new HashSet<>(pr.getLabels()));
+            assertEquals(Set.of("rfr", "test1"), new HashSet<>(pr.labels()));
 
             var fileB = localRepoFolder.resolve("b.txt");
             Files.writeString(fileB, "Hello");
             localRepo.add(fileB);
             var hashB = localRepo.commit("test2", "test", "test@test");
-            localRepo.push(hashB, author.getUrl(), "edit");
+            localRepo.push(hashB, author.url(), "edit");
 
             // Make sure that the push registered
-            lastHeadHash = pr.getHeadHash();
+            lastHeadHash = pr.headHash();
             refreshCount = 0;
             do {
-                pr = author.getPullRequest(pr.getId());
+                pr = author.pullRequest(pr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (pr.getHeadHash().equals(lastHeadHash));
+            } while (pr.headHash().equals(lastHeadHash));
 
             // Check the status - there should now be a test2 label
             TestBotRunner.runPeriodicItems(labelBot);
-            assertEquals(Set.of("rfr", "test1", "test2"), new HashSet<>(pr.getLabels()));
+            assertEquals(Set.of("rfr", "test1", "test2"), new HashSet<>(pr.labels()));
 
             localRepo.remove(fileA);
             var hashNoA = localRepo.commit("test2", "test", "test@test");
-            localRepo.push(hashNoA, author.getUrl(), "edit");
+            localRepo.push(hashNoA, author.url(), "edit");
 
             // Make sure that the push registered
-            lastHeadHash = pr.getHeadHash();
+            lastHeadHash = pr.headHash();
             refreshCount = 0;
             do {
-                pr = author.getPullRequest(pr.getId());
+                pr = author.pullRequest(pr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (pr.getHeadHash().equals(lastHeadHash));
+            } while (pr.headHash().equals(lastHeadHash));
 
             // Check the status - the test1 label should be gone
             TestBotRunner.runPeriodicItems(labelBot);
-            assertEquals(Set.of("rfr", "test2"), new HashSet<>(pr.getLabels()));
+            assertEquals(Set.of("rfr", "test2"), new HashSet<>(pr.labels()));
         }
     }
 }
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/MergeTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/MergeTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/MergeTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/MergeTests.java
@@ -45,61 +45,61 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make more changes in another branch
             var otherHash1 = CheckableRepository.appendAndCommit(localRepo, "First change in other",
                                                                 "First other\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(otherHash1, author.getUrl(), "other", true);
+            localRepo.push(otherHash1, author.url(), "other", true);
             var otherHash2 = CheckableRepository.appendAndCommit(localRepo, "Second change in other",
                                                                 "Second other\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(otherHash2, author.getUrl(), "other");
+            localRepo.push(otherHash2, author.url(), "other");
 
             // Go back to the original master
             localRepo.checkout(masterHash, true);
 
             // Make a change with a corresponding PR
             var unrelated = Files.writeString(localRepo.root().resolve("unrelated.txt"), "Unrelated", StandardCharsets.UTF_8);
             localRepo.add(unrelated);
             localRepo.commit("Unrelated", "some", "some@one");
             localRepo.merge(otherHash2);
             var mergeHash = localRepo.commit("Merge commit", "some", "some@one");
-            localRepo.push(mergeHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.getName() + ":other");
+            localRepo.push(mergeHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.name() + ":other");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check the status
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push it
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an ok message
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
 
             // The change should now be present on the master branch
             var pushedRepoFolder = tempFolder.path().resolve("pushedrepo");
-            var pushedRepo = Repository.materialize(pushedRepoFolder, author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedRepoFolder, author.url(), "master");
             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
 
             // The commits from the "other" branch should be preserved and not squashed (but not the merge commit)
             var headHash = pushedRepo.resolve("HEAD").orElseThrow();
             Set<Hash> commits;
@@ -127,71 +127,71 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make more changes in another branch
             var otherHash1 = CheckableRepository.appendAndCommit(localRepo, "First change in other",
                                                                  "First other\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(otherHash1, author.getUrl(), "other", true);
+            localRepo.push(otherHash1, author.url(), "other", true);
             var otherHash2 = CheckableRepository.appendAndCommit(localRepo, "Second change in other",
                                                                  "Second other\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(otherHash2, author.getUrl(), "other");
+            localRepo.push(otherHash2, author.url(), "other");
 
             // Go back to the original master
             localRepo.checkout(masterHash, true);
 
             // Make a change with a corresponding PR
             var unrelated = Files.writeString(localRepo.root().resolve("unrelated.txt"), "Unrelated", StandardCharsets.UTF_8);
             localRepo.add(unrelated);
             var updatedMaster = localRepo.commit("Unrelated", "some", "some@one");
             localRepo.merge(otherHash2);
             var mergeHash = localRepo.commit("Merge commit", "some", "some@one");
-            localRepo.push(mergeHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.getName() + ":other");
+            localRepo.push(mergeHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.name() + ":other");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check the status
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push something new to master
             localRepo.checkout(updatedMaster, true);
             var newMaster = Files.writeString(localRepo.root().resolve("newmaster.txt"), "New on master", StandardCharsets.UTF_8);
             localRepo.add(newMaster);
             var newMasterHash = localRepo.commit("New commit on master", "some", "some@one");
-            localRepo.push(newMasterHash, author.getUrl(), "master");
+            localRepo.push(newMasterHash, author.url(), "master");
 
             // Let the bot notice
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push it
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an ok message
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
-            assertEquals(1, pushed, () -> pr.getComments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
+            assertEquals(1, pushed, () -> pr.comments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
 
             // The change should now be present on the master branch
             var pushedRepoFolder = tempFolder.path().resolve("pushedrepo");
-            var pushedRepo = Repository.materialize(pushedRepoFolder, author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedRepoFolder, author.url(), "master");
             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
 
             // The commits from the "other" branch should be preserved and not squashed (but not the merge commit)
             var headHash = pushedRepo.resolve("HEAD").orElseThrow();
             Set<Hash> commits;
@@ -219,54 +219,54 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change in another branch that will not pass jcheck
             var otherHash = CheckableRepository.appendAndCommit(localRepo, "Change in other with trails..   ",
                                                                 "Bad change in other");
-            localRepo.push(otherHash, author.getUrl(), "other", true);
+            localRepo.push(otherHash, author.url(), "other", true);
 
             // Go back to the original master
             localRepo.checkout(masterHash, true);
 
             // Make a change with a corresponding PR
             var unrelated = Files.writeString(localRepo.root().resolve("unrelated.txt"), "Unrelated", StandardCharsets.UTF_8);
             localRepo.add(unrelated);
             localRepo.commit("Unrelated", "some", "some@one");
             localRepo.merge(otherHash);
             var mergeHash = localRepo.commit("Merge commit", "some", "some@one");
-            localRepo.push(mergeHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.getName() + ":other");
+            localRepo.push(mergeHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.name() + ":other");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check the status
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push it
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with a failure message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("did not complete successfully"))
                           .count();
-            assertEquals(1, error, () -> pr.getComments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
+            assertEquals(1, error, () -> pr.comments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
         }
     }
 
     @Test
     void invalidSourceRepo(TestInfo testInfo) throws IOException {
@@ -274,57 +274,57 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change in another branch
             var otherHash = CheckableRepository.appendAndCommit(localRepo, "Change in other",
                                                                 "Other\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(otherHash, author.getUrl(), "other", true);
+            localRepo.push(otherHash, author.url(), "other", true);
 
             // Go back to the original master
             localRepo.checkout(masterHash, true);
 
             // Make a change with a corresponding PR
             var unrelated = Files.writeString(localRepo.root().resolve("unrelated.txt"), "Unrelated", StandardCharsets.UTF_8);
             localRepo.add(unrelated);
             localRepo.commit("Unrelated", "some", "some@one");
             localRepo.merge(otherHash);
             var mergeHash = localRepo.commit("Merge commit", "some", "some@one");
-            localRepo.push(mergeHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.getName() + "xyz" + ":other");
+            localRepo.push(mergeHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.name() + "xyz" + ":other");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check the status
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push it
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with a failure message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("did not complete successfully"))
                           .count();
-            assertEquals(1, error, () -> pr.getComments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
+            assertEquals(1, error, () -> pr.comments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
 
-            var check = pr.getChecks(mergeHash).get("jcheck");
-            assertEquals("- Could not find project `" + author.getName() + "xyz` - check that it is correct.", check.summary().orElseThrow());
+            var check = pr.checks(mergeHash).get("jcheck");
+            assertEquals("- Could not find project `" + author.name() + "xyz` - check that it is correct.", check.summary().orElseThrow());
         }
     }
 
     @Test
     void invalidSourceBranch(TestInfo testInfo) throws IOException {
@@ -332,57 +332,57 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change in another branch
             var otherHash = CheckableRepository.appendAndCommit(localRepo, "Change in other",
                                                                 "Other\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(otherHash, author.getUrl(), "other", true);
+            localRepo.push(otherHash, author.url(), "other", true);
 
             // Go back to the original master
             localRepo.checkout(masterHash, true);
 
             // Make a change with a corresponding PR
             var unrelated = Files.writeString(localRepo.root().resolve("unrelated.txt"), "Unrelated", StandardCharsets.UTF_8);
             localRepo.add(unrelated);
             localRepo.commit("Unrelated", "some", "some@one");
             localRepo.merge(otherHash);
             var mergeHash = localRepo.commit("Merge commit", "some", "some@one");
-            localRepo.push(mergeHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.getName() + ":otherxyz");
+            localRepo.push(mergeHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.name() + ":otherxyz");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check the status
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push it
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with a failure message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("did not complete successfully"))
                           .count();
-            assertEquals(1, error, () -> pr.getComments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
+            assertEquals(1, error, () -> pr.comments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
 
-            var check = pr.getChecks(mergeHash).get("jcheck");
-            assertEquals("- Could not fetch branch `otherxyz` from project `" + author.getName() + "` - check that they are correct.", check.summary().orElseThrow());
+            var check = pr.checks(mergeHash).get("jcheck");
+            assertEquals("- Could not fetch branch `otherxyz` from project `" + author.name() + "` - check that they are correct.", check.summary().orElseThrow());
         }
     }
 
     @Test
     void wrongSourceBranch(TestInfo testInfo) throws IOException {
@@ -390,61 +390,61 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change in another branch
             var other1Hash = CheckableRepository.appendAndCommit(localRepo, "Change in other1",
                                                                 "Other\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(other1Hash, author.getUrl(), "other1", true);
+            localRepo.push(other1Hash, author.url(), "other1", true);
 
             // Go back to the original master
             localRepo.checkout(masterHash, true);
 
             // Make yet another change in another branch
             var other2Hash = CheckableRepository.appendAndCommit(localRepo, "Change in other2",
                                                                 "Unrelated\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(other2Hash, author.getUrl(), "other2", true);
+            localRepo.push(other2Hash, author.url(), "other2", true);
 
             // Make a change with a corresponding PR
             var unrelated = Files.writeString(localRepo.root().resolve("unrelated.txt"), "Unrelated", StandardCharsets.UTF_8);
             localRepo.add(unrelated);
             localRepo.commit("Unrelated", "some", "some@one");
             localRepo.merge(other1Hash, "ours");
             var mergeHash = localRepo.commit("Merge commit", "some", "some@one");
-            localRepo.push(mergeHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.getName() + ":other2");
+            localRepo.push(mergeHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.name() + ":other2");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check the status
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push it
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with a failure message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("did not complete successfully"))
                           .count();
-            assertEquals(1, error, () -> pr.getComments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
+            assertEquals(1, error, () -> pr.comments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
 
-            var check = pr.getChecks(mergeHash).get("jcheck");
+            var check = pr.checks(mergeHash).get("jcheck");
             assertEquals("- The merge contains commits that are not ancestors of the source", check.summary().orElseThrow());
         }
     }
 
     @Test
@@ -453,88 +453,88 @@
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change in another branch
             var otherHash = CheckableRepository.appendAndCommit(localRepo, "Change in other",
                                                                 "Other\n\nReviewed-by: integrationreviewer2");
-            localRepo.push(otherHash, author.getUrl(), "other", true);
+            localRepo.push(otherHash, author.url(), "other", true);
 
             // Go back to the original master
             localRepo.checkout(masterHash, true);
 
             // Make a change with a corresponding PR
             var unrelated = Files.writeString(localRepo.root().resolve("unrelated.txt"), "Unrelated", StandardCharsets.UTF_8);
             localRepo.add(unrelated);
             localRepo.commit("Unrelated", "some", "some@one");
             localRepo.merge(otherHash);
             var mergeHash = localRepo.commit("Merge commit", "some", "some@one");
-            localRepo.push(mergeHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.getName() + ":other");
+            localRepo.push(mergeHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.name() + ":other");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check the status
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push it
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with a failure message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("Merges require Committer status"))
                           .count();
-            assertEquals(1, error, () -> pr.getComments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
+            assertEquals(1, error, () -> pr.comments().stream().map(Comment::body).collect(Collectors.joining("\n\n")));
         }
     }
 
     @Test
     void unrelatedHistory(TestInfo testInfo) throws IOException {
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
 
             var author = credentials.getHostedRepository();
             // Need to force merge unrelated histories
-            assumeTrue(author.getRepositoryType().equals(VCS.GIT));
+            assumeTrue(author.repositoryType().equals(VCS.GIT));
 
             var integrator = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
 
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make an unrelated change in another branch
             var unrelatedRepoFolder = tempFolder.path().resolve("unrelated");
-            var unrelatedRepo = CheckableRepository.init(unrelatedRepoFolder, author.getRepositoryType(), Path.of("anotherfile.txt"));
+            var unrelatedRepo = CheckableRepository.init(unrelatedRepoFolder, author.repositoryType(), Path.of("anotherfile.txt"));
             unrelatedRepo.amend("Unrelated initial commit\n\nReviewed-by: integrationreviewer2", "some", "one@mail");
             var otherHash = CheckableRepository.appendAndCommit(unrelatedRepo, "Change in other",
                                                                 "Other\n\nReviewed-by: integrationreviewer2");
-            unrelatedRepo.push(otherHash, author.getUrl(), "other", true);
-            localRepo.fetch(author.getUrl(), "other");
+            unrelatedRepo.push(otherHash, author.url(), "other", true);
+            localRepo.fetch(author.url(), "other");
 
             // Go back to the original master
             localRepo.checkout(masterHash, true);
 
             // Make a change with a corresponding PR
@@ -550,26 +550,26 @@
                                   .execute();
             mergeCmd.check();
 
             //localRepo.merge(otherHash);
             var mergeHash = localRepo.commit("Merge commit", "some", "some@one");
-            localRepo.push(mergeHash, author.getUrl(), "edit", true);
-            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.getName() + ":other");
+            localRepo.push(mergeHash, author.url(), "edit", true);
+            var pr = credentials.createPullRequest(author, "master", "edit", "Merge " + author.name() + ":other");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot check the status
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Push it
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an ok message as we currently allow this
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
         }
     }
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/PullRequestAsserts.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/PullRequestAsserts.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/PullRequestAsserts.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/PullRequestAsserts.java
@@ -26,11 +26,11 @@
 
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 public class PullRequestAsserts {
     public static void assertLastCommentContains(PullRequest pr, String contains) {
-        var comments = pr.getComments();
+        var comments = pr.comments();
         assertTrue(!comments.isEmpty());
         var lastComment = comments.get(comments.size() - 1);
         assertTrue(lastComment.body().contains(contains), lastComment.body());
     }
 }
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/SponsorTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/SponsorTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/SponsorTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/SponsorTests.java
@@ -41,64 +41,64 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addReviewer(reviewer.host().currentUser().id());
             if (isAuthor) {
-                censusBuilder.addAuthor(author.host().getCurrentUserDetails().id());
+                censusBuilder.addAuthor(author.host().currentUser().id());
             }
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot see it
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Issue a merge command without being a Committer
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply that a sponsor is required
-            var sponsor = pr.getComments().stream()
+            var sponsor = pr.comments().stream()
                             .filter(comment -> comment.body().contains("sponsor"))
                             .filter(comment -> comment.body().contains("your change"))
                             .count();
             assertEquals(1, sponsor);
 
             // The bot should not have pushed the commit
-            var notPushed = pr.getComments().stream()
+            var notPushed = pr.comments().stream()
                               .filter(comment -> comment.body().contains("Pushed as commit"))
                               .count();
             assertEquals(0, notPushed);
 
             // Reviewer now agrees to sponsor
-            var reviewerPr = reviewer.getPullRequest(pr.getId());
+            var reviewerPr = reviewer.pullRequest(pr.id());
             reviewerPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should have pushed the commit
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
 
             // The change should now be present on the master branch
-            var pushedRepo = Repository.materialize(pushedFolder.path(), author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedFolder.path(), author.url(), "master");
             var headHash = pushedRepo.resolve("HEAD").orElseThrow();
             var headCommit = pushedRepo.commits(headHash.hex() + "^.." + headHash.hex()).asList().get(0);
 
             if (isAuthor) {
                 assertEquals("Generated Author 2", headCommit.author().name());
@@ -108,13 +108,13 @@
                 assertEquals("ta@none.none", headCommit.author().email());
             }
 
             assertEquals("Generated Reviewer 1", headCommit.committer().name());
             assertEquals("integrationreviewer1@openjdk.java.net", headCommit.committer().email());
-            assertTrue(pr.getLabels().contains("integrated"));
-            assertFalse(pr.getLabels().contains("ready"));
-            assertFalse(pr.getLabels().contains("sponsor"));
+            assertTrue(pr.labels().contains("integrated"));
+            assertFalse(pr.labels().contains("ready"));
+            assertFalse(pr.labels().contains("sponsor"));
         }
     }
 
     @Test
     void sponsorNonAuthor(TestInfo testInfo) throws IOException {
@@ -132,30 +132,30 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue an invalid command
             pr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("does not need sponsoring"))
                           .count();
             assertEquals(1, error);
         }
     }
@@ -166,30 +166,30 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue an invalid command
             pr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("Committers"))
                           .filter(comment -> comment.body().contains("are allowed to sponsor"))
                           .count();
             assertEquals(1, error);
         }
@@ -202,31 +202,31 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addReviewer(reviewer.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Reviewer now tries to sponsor
-            var reviewerPr = reviewer.getPullRequest(pr.getId());
+            var reviewerPr = reviewer.pullRequest(pr.id());
             reviewerPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("before the integration can be sponsored"))
                           .count();
             assertEquals(1, error);
         }
     }
@@ -238,84 +238,84 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addReviewer(reviewer.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot see it
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Flag it as ready for integration
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Bot should have replied
-            var ready = pr.getComments().stream()
+            var ready = pr.comments().stream()
                           .filter(comment -> comment.body().contains("now ready to be sponsored"))
                           .filter(comment -> comment.body().contains("at version " + editHash.hex()))
                           .count();
             assertEquals(1, ready);
-            assertTrue(pr.getLabels().contains("sponsor"));
+            assertTrue(pr.labels().contains("sponsor"));
 
             // Push another change
             var updateHash = CheckableRepository.appendAndCommit(localRepo,"Yet more stuff");
-            localRepo.push(updateHash, author.getUrl(), "edit");
+            localRepo.push(updateHash, author.url(), "edit");
 
             // Make sure that the push registered
-            var lastHeadHash = pr.getHeadHash();
+            var lastHeadHash = pr.headHash();
             var refreshCount = 0;
             do {
-                pr = author.getPullRequest(pr.getId());
+                pr = author.pullRequest(pr.id());
                 if (refreshCount++ > 100) {
                     fail("The PR did not update after the new push");
                 }
-            } while (pr.getHeadHash().equals(lastHeadHash));
+            } while (pr.headHash().equals(lastHeadHash));
 
             // The label should have been dropped
             TestBotRunner.runPeriodicItems(mergeBot);
-            assertFalse(pr.getLabels().contains("sponsor"));
+            assertFalse(pr.labels().contains("sponsor"));
 
             // Reviewer now tries to sponsor
-            var reviewerPr = reviewer.getPullRequest(pr.getId());
+            var reviewerPr = reviewer.pullRequest(pr.id());
             reviewerPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("The PR has been updated since the change"))
                           .count();
             assertEquals(1, error);
 
             // Flag it as ready for integration again
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
-            assertTrue(pr.getLabels().contains("sponsor"));
+            assertTrue(pr.labels().contains("sponsor"));
 
             // It should now be possible to sponsor
             reviewerPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
-            assertFalse(pr.getLabels().contains("sponsor"));
+            assertFalse(pr.labels().contains("sponsor"));
 
             // The bot should have pushed the commit
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
         }
     }
@@ -328,63 +328,63 @@
 
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id())
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(integrator.host().currentUser().id())
+                                           .addReviewer(reviewer.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Push something unrelated to master
             localRepo.checkout(masterHash, true);
             var unrelated = localRepo.root().resolve("unrelated.txt");
             Files.writeString(unrelated, "Hello");
             localRepo.add(unrelated);
             var unrelatedHash = localRepo.commit("Unrelated", "X", "x@y.z");
-            localRepo.push(unrelatedHash, author.getUrl(), "master");
+            localRepo.push(unrelatedHash, author.url(), "master");
 
             // Issue a merge command without being a Committer
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply that a sponsor is required
-            var sponsor = pr.getComments().stream()
+            var sponsor = pr.comments().stream()
                             .filter(comment -> comment.body().contains("sponsor"))
                             .filter(comment -> comment.body().contains("your change"))
                             .count();
             assertEquals(1, sponsor);
 
             // Reviewer now agrees to sponsor
-            var reviewerPr = reviewer.getPullRequest(pr.getId());
+            var reviewerPr = reviewer.pullRequest(pr.id());
             reviewerPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an ok message
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .filter(comment -> comment.body().contains("commit was automatically rebased without conflicts"))
                            .count();
             assertEquals(1, pushed);
 
             // The change should now be present on the master branch
-            var pushedRepo = Repository.materialize(pushedFolder.path(), author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedFolder.path(), author.url(), "master");
             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
         }
     }
 
     @Test
@@ -394,74 +394,74 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id());
+                                           .addReviewer(reviewer.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot see it
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Flag it as ready for integration
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Bot should have replied
-            var ready = pr.getComments().stream()
+            var ready = pr.comments().stream()
                           .filter(comment -> comment.body().contains("now ready to be sponsored"))
                           .filter(comment -> comment.body().contains("at version " + editHash.hex()))
                           .count();
             assertEquals(1, ready);
-            assertTrue(pr.getLabels().contains("sponsor"));
+            assertTrue(pr.labels().contains("sponsor"));
 
             // The reviewer now changes their mind
             approvalPr.addReview(Review.Verdict.DISAPPROVED, "No wait, disapproved");
 
             // The label should have been dropped
             TestBotRunner.runPeriodicItems(mergeBot);
-            assertFalse(pr.getLabels().contains("sponsor"));
+            assertFalse(pr.labels().contains("sponsor"));
 
             // Reviewer now tries to sponsor
-            var reviewerPr = reviewer.getPullRequest(pr.getId());
+            var reviewerPr = reviewer.pullRequest(pr.id());
             reviewerPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("merge request cannot be fulfilled at this time"))
                           .filter(comment -> comment.body().contains("failed the final jcheck"))
                           .count();
             assertEquals(1, error);
 
             // Make it ready for integration again
             approvalPr.addReview(Review.Verdict.APPROVED, "Sorry, wrong button");
             TestBotRunner.runPeriodicItems(mergeBot);
-            assertTrue(pr.getLabels().contains("sponsor"));
+            assertTrue(pr.labels().contains("sponsor"));
 
             // It should now be possible to sponsor
             reviewerPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
-            assertFalse(pr.getLabels().contains("sponsor"));
+            assertFalse(pr.labels().contains("sponsor"));
 
             // The bot should have pushed the commit
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
         }
     }
@@ -473,61 +473,61 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var reviewer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(reviewer.host().getCurrentUserDetails().id())
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addReviewer(reviewer.host().currentUser().id())
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Approve it as another user
-            var approvalPr = reviewer.getPullRequest(pr.getId());
+            var approvalPr = reviewer.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
 
             // Let the bot see it
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // Issue a merge command without being a Committer
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply that a sponsor is required
-            var sponsor = pr.getComments().stream()
+            var sponsor = pr.comments().stream()
                             .filter(comment -> comment.body().contains("sponsor"))
                             .filter(comment -> comment.body().contains("your change"))
                             .count();
             assertEquals(1, sponsor);
 
             // The bot should not have pushed the commit
-            var notPushed = pr.getComments().stream()
+            var notPushed = pr.comments().stream()
                               .filter(comment -> comment.body().contains("Pushed as commit"))
                               .count();
             assertEquals(0, notPushed);
 
             // Push something conflicting to master
             localRepo.checkout(masterHash, true);
             var conflictingHash = CheckableRepository.appendAndCommit(localRepo, "This looks like a conflict");
-            localRepo.push(conflictingHash, author.getUrl(), "master");
+            localRepo.push(conflictingHash, author.url(), "master");
 
             // Reviewer now agrees to sponsor
-            var reviewerPr = reviewer.getPullRequest(pr.getId());
+            var reviewerPr = reviewer.pullRequest(pr.id());
             reviewerPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("It was not possible to rebase your changes automatically."))
                           .filter(comment -> comment.body().contains("Please merge `master`"))
                           .count();
             assertEquals(1, error);
         }
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/SummaryTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/SummaryTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/SummaryTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/SummaryTests.java
@@ -40,24 +40,24 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addReviewer(integrator.host().getCurrentUserDetails().id())
-                                           .addCommitter(author.host().getCurrentUserDetails().id());
+                                           .addReviewer(integrator.host().currentUser().id())
+                                           .addCommitter(author.host().currentUser().id());
             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
             var localRepoFolder = tempFolder.path().resolve("localrepo");
-            var localRepo = CheckableRepository.init(localRepoFolder, author.getRepositoryType());
+            var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Try setting a summary when none has been set yet
             pr.addComment("/summary");
             TestBotRunner.runPeriodicItems(prBot);
@@ -85,11 +85,11 @@
 
             // The bot should reply with a success message
             assertLastCommentContains(pr,"Setting summary to");
 
             // Approve it as another user
-            var approvalPr = integrator.getPullRequest(pr.getId());
+            var approvalPr = integrator.pullRequest(pr.id());
             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
             TestBotRunner.runPeriodicItems(prBot);
             TestBotRunner.runPeriodicItems(prBot);
 
             // Now update it
@@ -99,17 +99,17 @@
 
             // The bot should reply with a success message
             assertLastCommentContains(pr,"Updating existing summary");
 
             // The commit message preview should contain the final summary
-            var summaryLine = pr.getComments().stream()
-                               .flatMap(comment -> comment.body().lines())
+            var summaryLine = pr.comments().stream()
+                                .flatMap(comment -> comment.body().lines())
                                 .filter(line -> !line.contains("/summary"))
                                 .filter(line -> !line.contains("Updating existing"))
-                               .filter(line -> line.contains("Third time"))
-                               .findAny()
-                               .orElseThrow();
+                                .filter(line -> line.contains("Third time"))
+                                .findAny()
+                                .orElseThrow();
             assertEquals("Third time is surely the charm", summaryLine);
 
             // Integrate
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(prBot);
@@ -117,11 +117,11 @@
             // The bot should reply with an ok message
             assertLastCommentContains(pr,"Pushed as commit");
 
             // The change should now be present on the master branch
             var pushedFolder = tempFolder.path().resolve("pushed");
-            var pushedRepo = Repository.materialize(pushedFolder, author.getUrl(), "master");
+            var pushedRepo = Repository.materialize(pushedFolder, author.url(), "master");
             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
 
             var headHash = pushedRepo.resolve("HEAD").orElseThrow();
             var headCommit = pushedRepo.commits(headHash.hex() + "^.." + headHash.hex()).asList().get(0);
 
@@ -140,31 +140,31 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var external = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id());
             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "refs/heads/edit", true);
+            localRepo.push(editHash, author.url(), "refs/heads/edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue a contributor command not as the PR author
-            var externalPr = external.getPullRequest(pr.getId());
+            var externalPr = external.pullRequest(pr.id());
             externalPr.addComment("/summary a summary");
             TestBotRunner.runPeriodicItems(mergeBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("Only the author"))
                           .count();
             assertEquals(1, error);
         }
     }
diff a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/VetoTests.java b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/VetoTests.java
--- a/bots/pr/src/test/java/org/openjdk/skara/bots/pr/VetoTests.java
+++ b/bots/pr/src/test/java/org/openjdk/skara/bots/pr/VetoTests.java
@@ -32,14 +32,14 @@
 import java.util.*;
 
 import static org.junit.jupiter.api.Assertions.*;
 
 class VetoTests {
-    private static HostUserDetails createUser(int id) {
-        return new HostUserDetails(id,
-                                   String.format("noname_%d", id),
-                                   String.format("No Name %d", id));
+    private static HostUser createUser(int id) {
+        return new HostUser(id,
+                            String.format("noname_%d", id),
+                            String.format("No Name %d", id));
     }
 
     private static class Comments {
         private final List<Comment> comments = new ArrayList<>();
 
@@ -89,30 +89,30 @@
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id());
             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Issue an invalid command
             pr.addComment("/reject");
             TestBotRunner.runPeriodicItems(prBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("reject your own changes"))
                           .count();
             assertEquals(1, error);
         }
     }
@@ -125,32 +125,32 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var vetoer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addCommitter(vetoer.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addCommitter(vetoer.host().currentUser().id());
             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Try to veto as a non committer
-            var vetoPr = vetoer.getPullRequest(pr.getId());
+            var vetoPr = vetoer.pullRequest(pr.id());
             vetoPr.addComment("/reject");
             TestBotRunner.runPeriodicItems(prBot);
 
             // The bot should reply with an error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("are allowed to reject"))
                           .count();
             assertEquals(1, error);
         }
     }
@@ -163,64 +163,64 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var vetoer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addCommitter(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(vetoer.host().getCurrentUserDetails().id());
+                                           .addCommitter(author.host().currentUser().id())
+                                           .addReviewer(vetoer.host().currentUser().id());
 
             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Place a veto
-            var vetoPr = vetoer.getPullRequest(pr.getId());
+            var vetoPr = vetoer.pullRequest(pr.id());
             vetoPr.addReview(Review.Verdict.APPROVED, "Approved");
             vetoPr.addComment("/reject");
             TestBotRunner.runPeriodicItems(prBot);
 
             // The bot should acknowledge
-            var ack = pr.getComments().stream()
-                          .filter(comment -> comment.body().contains("cannot be integrated"))
-                          .count();
+            var ack = pr.comments().stream()
+                        .filter(comment -> comment.body().contains("cannot be integrated"))
+                        .count();
             assertEquals(1, ack);
 
             // Now try to integrate
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(prBot);
 
             // There should be another error message
-            var error = pr.getComments().stream()
-                        .filter(comment -> comment.body().contains("change is currently blocked"))
-                        .count();
+            var error = pr.comments().stream()
+                          .filter(comment -> comment.body().contains("change is currently blocked"))
+                          .count();
             assertEquals(1, error);
 
             // Now drop the veto
             vetoPr.addComment("/allow");
             TestBotRunner.runPeriodicItems(prBot);
 
             // There should be an acknowledgement
-            var approve = pr.getComments().stream()
-                          .filter(comment -> comment.body().contains("now allowed to be integrated"))
-                          .count();
+            var approve = pr.comments().stream()
+                            .filter(comment -> comment.body().contains("now allowed to be integrated"))
+                            .count();
             assertEquals(1, approve);
 
             // Now try to integrate
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(prBot);
 
             // The bot should reply with an ok message
-            var pushed = pr.getComments().stream()
+            var pushed = pr.comments().stream()
                            .filter(comment -> comment.body().contains("Pushed as commit"))
                            .count();
             assertEquals(1, pushed);
         }
     }
@@ -233,54 +233,54 @@
             var author = credentials.getHostedRepository();
             var integrator = credentials.getHostedRepository();
             var vetoer = credentials.getHostedRepository();
 
             var censusBuilder = credentials.getCensusBuilder()
-                                           .addAuthor(author.host().getCurrentUserDetails().id())
-                                           .addReviewer(vetoer.host().getCurrentUserDetails().id());
+                                           .addAuthor(author.host().currentUser().id())
+                                           .addReviewer(vetoer.host().currentUser().id());
 
             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Place a veto
-            var vetoPr = vetoer.getPullRequest(pr.getId());
+            var vetoPr = vetoer.pullRequest(pr.id());
             vetoPr.addReview(Review.Verdict.APPROVED, "Approved");
             vetoPr.addComment("/reject");
             TestBotRunner.runPeriodicItems(prBot);
 
             // The bot should acknowledge
-            var ack = pr.getComments().stream()
+            var ack = pr.comments().stream()
                         .filter(comment -> comment.body().contains("cannot be integrated"))
                         .count();
             assertEquals(1, ack);
 
             // Author makes the PR ready for sponsoring
             pr.addComment("/integrate");
             TestBotRunner.runPeriodicItems(prBot);
 
             // The bot should acknowledge
-            var ready = pr.getComments().stream()
+            var ready = pr.comments().stream()
                           .filter(comment -> comment.body().contains("sponsor"))
                           .filter(comment -> comment.body().contains("your change"))
-                        .count();
+                          .count();
             assertEquals(1, ready);
 
             vetoPr.addComment("/sponsor");
             TestBotRunner.runPeriodicItems(prBot);
 
             // There should be another error message
-            var error = pr.getComments().stream()
+            var error = pr.comments().stream()
                           .filter(comment -> comment.body().contains("change is currently blocked"))
                           .count();
             assertEquals(1, error);
         }
     }
diff a/bots/submit/src/main/java/org/openjdk/skara/bots/submit/SubmitBot.java b/bots/submit/src/main/java/org/openjdk/skara/bots/submit/SubmitBot.java
--- a/bots/submit/src/main/java/org/openjdk/skara/bots/submit/SubmitBot.java
+++ b/bots/submit/src/main/java/org/openjdk/skara/bots/submit/SubmitBot.java
@@ -39,11 +39,11 @@
         this.updateCache = new PullRequestUpdateCache();
     }
 
     @Override
     public List<WorkItem> getPeriodicItems() {
-        return repository.getPullRequests().stream()
+        return repository.pullRequests().stream()
                          .filter(updateCache::needsUpdate)
                          .flatMap(pr -> executors.stream()
                                                  .map(executor -> new SubmitBotWorkItem(this, executor, pr)))
                          .collect(Collectors.toList());
     }
diff a/bots/submit/src/main/java/org/openjdk/skara/bots/submit/SubmitBotWorkItem.java b/bots/submit/src/main/java/org/openjdk/skara/bots/submit/SubmitBotWorkItem.java
--- a/bots/submit/src/main/java/org/openjdk/skara/bots/submit/SubmitBotWorkItem.java
+++ b/bots/submit/src/main/java/org/openjdk/skara/bots/submit/SubmitBotWorkItem.java
@@ -43,11 +43,11 @@
         this.pr = pr;
     }
 
     @Override
     public String toString() {
-        return "SubmitWorkItem@" + bot.repository().getName() + "#" + pr.getId() + ":" + executor.checkName();
+        return "SubmitWorkItem@" + bot.repository().name() + "#" + pr.id() + ":" + executor.checkName();
     }
 
     @Override
     public boolean concurrentWith(WorkItem other) {
         if (!(other instanceof SubmitBotWorkItem)) {
@@ -55,39 +55,39 @@
         }
         SubmitBotWorkItem otherItem = (SubmitBotWorkItem)other;
         if (!executor.checkName().equals(otherItem.executor.checkName())) {
             return true;
         }
-        if (!pr.getId().equals(otherItem.pr.getId())) {
+        if (!pr.id().equals(otherItem.pr.id())) {
             return true;
         }
-        if (!bot.repository().getName().equals(otherItem.bot.repository().getName())) {
+        if (!bot.repository().name().equals(otherItem.bot.repository().name())) {
             return true;
         }
         return false;
     }
 
     @Override
     public void run(Path scratchPath) {
         // Is the check already up to date?
-        var checks = pr.getChecks(pr.getHeadHash());
+        var checks = pr.checks(pr.headHash());
         if (checks.containsKey(executor.checkName())) {
             var check = checks.get(executor.checkName());
             if (check.startedAt().isBefore(ZonedDateTime.now().minus(executor.timeout())) && check.status() == CheckStatus.IN_PROGRESS) {
-                log.info("Check for hash " + pr.getHeadHash() + " is too old - running again");
+                log.info("Check for hash " + pr.headHash() + " is too old - running again");
             } else {
-                log.fine("Hash " + pr.getHeadHash() + " already has a check - skipping");
+                log.fine("Hash " + pr.headHash() + " already has a check - skipping");
                 return;
             }
         }
 
-        var prFolder = scratchPath.resolve("submit").resolve(pr.repository().getName());
+        var prFolder = scratchPath.resolve("submit").resolve(pr.repository().name());
 
         // Materialize the PR's target ref
         try {
-            var localRepo = Repository.materialize(prFolder, pr.repository().getUrl(), pr.getTargetRef());
-            var headHash = localRepo.fetch(pr.repository().getUrl(), pr.getHeadHash().hex());
+            var localRepo = Repository.materialize(prFolder, pr.repository().url(), pr.targetRef());
+            var headHash = localRepo.fetch(pr.repository().url(), pr.headHash().hex());
 
             var checkBuilder = CheckBuilder.create(executor.checkName(), headHash);
             pr.createCheck(checkBuilder.build());
 
             var checkUpdater = new CheckUpdater(pr, checkBuilder);
diff a/bots/submit/src/test/java/org/openjdk/skara/bots/submit/CheckUpdaterTests.java b/bots/submit/src/test/java/org/openjdk/skara/bots/submit/CheckUpdaterTests.java
--- a/bots/submit/src/test/java/org/openjdk/skara/bots/submit/CheckUpdaterTests.java
+++ b/bots/submit/src/test/java/org/openjdk/skara/bots/submit/CheckUpdaterTests.java
@@ -38,17 +38,17 @@
         try (var credentials = new HostCredentials(testInfo);
              var tempFolder = new TemporaryDirectory()) {
             var author = credentials.getHostedRepository();
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             var builder = CheckBuilder.create("test", editHash);
             pr.createCheck(builder.build());
 
@@ -56,21 +56,21 @@
             updater.setMaxUpdateRate(Duration.ofDays(1));
             builder.summary("In progress");
             updater.run();
 
             // Verify that the check is in progress
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("test");
             assertEquals(CheckStatus.IN_PROGRESS, check.status());
             assertEquals("In progress", check.summary().orElseThrow());
 
             builder.summary("Quick update");
             updater.run();
 
             // Verify that the check still is in progress and has not been updated due to the rate limiter
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("test");
             assertEquals(CheckStatus.IN_PROGRESS, check.status());
             assertEquals("In progress", check.summary().orElseThrow());
 
@@ -79,11 +79,11 @@
 
             builder.summary("Final update");
             updater.run();
 
             // The summary should now have been updated
-            checks = pr.getChecks(editHash);
+            checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             check = checks.get("test");
             assertEquals(CheckStatus.IN_PROGRESS, check.status());
             assertEquals("Final update", check.summary().orElseThrow());
         }
diff a/bots/submit/src/test/java/org/openjdk/skara/bots/submit/SubmitBotTests.java b/bots/submit/src/test/java/org/openjdk/skara/bots/submit/SubmitBotTests.java
--- a/bots/submit/src/test/java/org/openjdk/skara/bots/submit/SubmitBotTests.java
+++ b/bots/submit/src/test/java/org/openjdk/skara/bots/submit/SubmitBotTests.java
@@ -42,23 +42,23 @@
 
             var executor = new ShellExecutor("test", List.of("echo", "hello"), Duration.ofDays(1), Map.of());
             var bot = new SubmitBot(author, List.of(executor));
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             TestBotRunner.runPeriodicItems(bot);
 
             // Verify that the check passed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("test");
             assertEquals(CheckStatus.SUCCESS, check.status());
         }
     }
@@ -71,23 +71,23 @@
 
             var executor = new ShellExecutor("test", List.of("command_not_found"), Duration.ofDays(1), Map.of());
             var bot = new SubmitBot(author, List.of(executor));
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             TestBotRunner.runPeriodicItems(bot);
 
             // Verify that the check failed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("test");
             assertEquals(CheckStatus.FAILURE, check.status());
         }
     }
@@ -100,17 +100,17 @@
 
             var executor = new ShellExecutor("test", List.of("command_not_found"), Duration.ofDays(1), Map.of());
             var bot = new SubmitBot(author, List.of(executor));
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Create a fake check from a while back
             var checkBuilder = CheckBuilder.create("test", editHash);
             pr.createCheck(checkBuilder.build());
@@ -120,11 +120,11 @@
 
             // The bot should not overwrite the old check
             TestBotRunner.runPeriodicItems(bot);
 
             // Verify that the check is still listed as passed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("test");
             assertEquals(CheckStatus.SUCCESS, check.status());
         }
     }
@@ -136,17 +136,17 @@
 
             var executor = new ShellExecutor("test", List.of("echo", "hello"), Duration.ofDays(1), Map.of());
             var bot = new SubmitBot(author, List.of(executor));
 
             // Populate the projects repository
-            var localRepo = CheckableRepository.init(tempFolder.path(), author.getRepositoryType());
+            var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
             var masterHash = localRepo.resolve("master").orElseThrow();
-            localRepo.push(masterHash, author.getUrl(), "master", true);
+            localRepo.push(masterHash, author.url(), "master", true);
 
             // Make a change with a corresponding PR
             var editHash = CheckableRepository.appendAndCommit(localRepo);
-            localRepo.push(editHash, author.getUrl(), "edit", true);
+            localRepo.push(editHash, author.url(), "edit", true);
             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
 
             // Create a fake check from a while back
             var checkBuilder = CheckBuilder.create("test", editHash);
             pr.createCheck(checkBuilder.build());
@@ -156,11 +156,11 @@
 
             // The bot should overwrite the old check
             TestBotRunner.runPeriodicItems(bot);
 
             // Verify that the check passed
-            var checks = pr.getChecks(editHash);
+            var checks = pr.checks(editHash);
             assertEquals(1, checks.size());
             var check = checks.get("test");
             assertEquals(CheckStatus.SUCCESS, check.status());
         }
     }
diff a/bots/topological/src/main/java/module-info.java b/bots/topological/src/main/java/module-info.java
--- a/bots/topological/src/main/java/module-info.java
+++ b/bots/topological/src/main/java/module-info.java
@@ -21,9 +21,10 @@
  * questions.
  */
 module org.openjdk.skara.bots.topological {
     requires org.openjdk.skara.bot;
     requires org.openjdk.skara.vcs;
+    requires org.openjdk.skara.json;
     requires java.logging;
 
     provides org.openjdk.skara.bot.BotFactory with org.openjdk.skara.bots.topological.TopologicalBotFactory;
 }
diff a/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBot.java b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBot.java
--- a/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBot.java
+++ b/bots/topological/src/main/java/org/openjdk/skara/bots/topological/TopologicalBot.java
@@ -59,26 +59,26 @@
     public boolean concurrentWith(WorkItem other) {
         if (!(other instanceof TopologicalBot)) {
             return true;
         }
         var otherBot = (TopologicalBot) other;
-        return !hostedRepo.getName().equals(otherBot.hostedRepo.getName());
+        return !hostedRepo.name().equals(otherBot.hostedRepo.name());
     }
 
     @Override
     public void run(Path scratchPath) {
         log.info("Starting topobot run");
         try {
-            var sanitizedUrl = URLEncoder.encode(hostedRepo.getWebUrl().toString(), StandardCharsets.UTF_8);
+            var sanitizedUrl = URLEncoder.encode(hostedRepo.webUrl().toString(), StandardCharsets.UTF_8);
             var dir = storage.resolve(sanitizedUrl);
             Repository repo;
             if (!Files.exists(dir)) {
-                log.info("Cloning " + hostedRepo.getName());
+                log.info("Cloning " + hostedRepo.name());
                 Files.createDirectories(dir);
-                repo = Repository.clone(hostedRepo.getUrl(), dir);
+                repo = Repository.clone(hostedRepo.url(), dir);
             } else {
-                log.info("Found existing scratch directory for " + hostedRepo.getName());
+                log.info("Found existing scratch directory for " + hostedRepo.name());
                 repo = Repository.get(dir)
                         .orElseThrow(() -> new RuntimeException("Repository in " + dir + " has vanished"));
             }
 
             repo.fetchAll();
@@ -164,11 +164,11 @@
                         + commits
                             .map(Commit::toString)
                             .collect(Collectors.joining("\n", "\n", "\n")));
             }
             try {
-                repo.push(repo.head(), hostedRepo.getUrl(), branch.name());
+                repo.push(repo.head(), hostedRepo.url(), branch.name());
             } catch (IOException e) {
                 log.severe("Pushing failed! Aborting...");
                 repo.reset(oldHead, true);
                 throw e;
             }
diff a/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalBotTests.java b/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalBotTests.java
--- a/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalBotTests.java
+++ b/bots/topological/src/test/java/org/openjdk/skara/bots/topological/TopologicalBotTests.java
@@ -25,11 +25,10 @@
 import org.openjdk.skara.host.*;
 import org.openjdk.skara.test.*;
 import org.openjdk.skara.vcs.*;
 
 import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.TestInfo;
 
 import java.io.IOException;
 import java.io.UncheckedIOException;
 import java.nio.file.Files;
 import java.nio.file.StandardOpenOption;
@@ -42,11 +41,11 @@
 class TopologicalBotTests {
 
     @Test
     void testTopoMerge() throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var repo = Repository.init(fromDir, VCS.GIT);
             var gitConfig = repo.root().resolve(".git").resolve("config");
             Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"),
@@ -56,11 +55,11 @@
             // make non bare
             var readme = fromDir.resolve("README.txt");
             Files.writeString(readme, "Hello world\n");
             repo.add(readme);
             repo.commit("An initial commit", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             var aBranch = repo.branch(repo.head(), "A");
             // no deps -> depends on master
 
             var depsFileName = "deps.txt";
@@ -69,26 +68,26 @@
             repo.checkout(bBranch);
             var bDeps = fromDir.resolve(depsFileName);
             Files.writeString(bDeps, "A");
             repo.add(bDeps);
             repo.commit("Adding deps file to B", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             var cBranch = repo.branch(repo.head(), "C");
             repo.checkout(cBranch);
             var cDeps = fromDir.resolve(depsFileName);
             Files.writeString(cDeps, "B A");
             repo.add(cDeps);
             repo.commit("Adding deps file to C", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             repo.checkout(new Branch("master"));
             var newFile = fromDir.resolve("NewFile.txt");
             Files.writeString(newFile, "Hello world\n");
             repo.add(newFile);
             var preHash = repo.commit("An additional commit", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             var preCommits = repo.commits().asList();
             assertEquals(4, preCommits.size());
             assertEquals(preHash, repo.head());
 
@@ -112,11 +111,11 @@
     }
 
     @Test
     void testTopoMergeFailure() throws IOException {
         try (var temp = new TemporaryDirectory()) {
-            var host = TestHost.createNew(List.of(new HostUserDetails(0, "duke", "J. Duke")));
+            var host = TestHost.createNew(List.of(new HostUser(0, "duke", "J. Duke")));
 
             var fromDir = temp.path().resolve("from.git");
             var repo = Repository.init(fromDir, VCS.GIT);
             var gitConfig = repo.root().resolve(".git").resolve("config");
             Files.write(gitConfig, List.of("[receive]", "denyCurrentBranch = ignore"), APPEND);
@@ -125,42 +124,42 @@
             // make non bare
             var readme = fromDir.resolve("README.txt");
             Files.writeString(readme, "Hello world\n");
             repo.add(readme);
             repo.commit("An initial commit", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             var aBranch = repo.branch(repo.head(), "A");
             repo.checkout(aBranch);
             Files.writeString(readme, "A conflicting line\n", APPEND);
             repo.add(readme);
             var aStartHash = repo.commit("A conflicting commit", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             var depsFileName = "deps.txt";
 
             var bBranch = repo.branch(repo.head(), "B");
             repo.checkout(bBranch);
             var bDeps = fromDir.resolve(depsFileName);
             Files.writeString(bDeps, "A");
             repo.add(bDeps);
             var bDepsHash = repo.commit("Adding deps file to B", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             var cBranch = repo.branch(repo.head(), "C");
             repo.checkout(cBranch);
             var cDeps = fromDir.resolve(depsFileName);
             Files.writeString(cDeps, "B");
             repo.add(cDeps);
             var cDepsHash = repo.commit("Adding deps file to C", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             repo.checkout(new Branch("master"));
             Files.writeString(readme, "Goodbye world!\n", APPEND);
             repo.add(readme);
             var preHash = repo.commit("An additional commit", "duke", "duke@openjdk.org");
-            repo.pushAll(hostedRepo.getUrl());
+            repo.pushAll(hostedRepo.url());
 
             var preCommits = repo.commits().asList();
             assertEquals(5, preCommits.size());
             assertEquals(preHash, repo.head());
 
diff a/cli/src/main/java/org/openjdk/skara/cli/GitFork.java b/cli/src/main/java/org/openjdk/skara/cli/GitFork.java
--- a/cli/src/main/java/org/openjdk/skara/cli/GitFork.java
+++ b/cli/src/main/java/org/openjdk/skara/cli/GitFork.java
@@ -143,30 +143,30 @@
 
         if (credentials.username() == null) {
             exit("No username for host " + hostName + " found, use git-credentials or the flag --username");
         }
 
-        var host = Host.from(uri, new PersonalAccessToken(credentials.username(), credentials.password()));
+        var host = RepositoryHost.from(uri, new PersonalAccessToken(credentials.username(), credentials.password()));
         if (path.endsWith(".git")) {
             path = path.substring(0, path.length() - 4);
         }
         if (path.startsWith("/")) {
             path = path.substring(1);
         }
 
-        var fork = host.getRepository(path).fork();
+        var fork = host.repository(path).fork();
 
         if (token == null) {
             GitCredentials.approve(credentials);
         }
 
-        var webUrl = fork.getWebUrl();
+        var webUrl = fork.webUrl();
         if (isMercurial) {
             webUrl = URI.create("git+" + webUrl.toString());
         }
         if (arguments.at(1).isPresent()) {
-            System.out.println("Fork available at: " + fork.getWebUrl());
+            System.out.println("Fork available at: " + fork.webUrl());
             var dest = arguments.at(1).asString();
             System.out.println("Cloning " + webUrl + "...");
             var repo = clone(webUrl, Path.of(dest), isMercurial);
             var remoteWord = isMercurial ? "path" : "remote";
             System.out.print("Adding " + remoteWord + " 'upstream' for " + uri.toString() + "...");
diff a/cli/src/main/java/org/openjdk/skara/cli/GitPr.java b/cli/src/main/java/org/openjdk/skara/cli/GitPr.java
--- a/cli/src/main/java/org/openjdk/skara/cli/GitPr.java
+++ b/cli/src/main/java/org/openjdk/skara/cli/GitPr.java
@@ -25,11 +25,10 @@
 import org.openjdk.skara.args.*;
 import org.openjdk.skara.host.*;
 import org.openjdk.skara.vcs.*;
 import org.openjdk.skara.vcs.openjdk.*;
 import org.openjdk.skara.proxy.HttpProxy;
-import org.openjdk.skara.ssh.SSHConfig;
 
 import java.io.IOException;
 import java.net.URI;
 import java.nio.file.*;
 import java.util.*;
@@ -99,26 +98,26 @@
         }
         return name;
     }
 
     private static HostedRepository getHostedRepositoryFor(URI uri, GitCredentials credentials) throws IOException {
-        var host = Host.from(uri, new PersonalAccessToken(credentials.username(), credentials.password()));
+        var host = RepositoryHost.from(uri, new PersonalAccessToken(credentials.username(), credentials.password()));
         if (System.getenv("GIT_TOKEN") == null) {
             GitCredentials.approve(credentials);
         }
-        var remoteRepo = host.getRepository(projectName(uri));
-        var parentRepo = remoteRepo.getParent();
+        var remoteRepo = host.repository(projectName(uri));
+        var parentRepo = remoteRepo.parent();
         var targetRepo = parentRepo.isPresent() ? parentRepo.get() : remoteRepo;
         return targetRepo;
     }
 
     private static PullRequest getPullRequest(URI uri, GitCredentials credentials, Argument prId) throws IOException {
         if (!prId.isPresent()) {
             exit("error: missing pull request identifier");
         }
 
-        var pr = getHostedRepositoryFor(uri, credentials).getPullRequest(prId.asString());
+        var pr = getHostedRepositoryFor(uri, credentials).pullRequest(prId.asString());
         if (pr == null) {
             exit("error: could not fetch PR information");
         }
 
         return pr;
@@ -307,11 +306,11 @@
         var remotePullPath = repo.pullPath(remote);
         var username = arguments.contains("username") ? arguments.get("username").asString() : null;
         var token = isMercurial ? System.getenv("HG_TOKEN") :  System.getenv("GIT_TOKEN");
         var uri = Remote.toWebURI(remotePullPath);
         var credentials = GitCredentials.fill(uri.getHost(), uri.getPath(), username, token, uri.getScheme());
-        var host = Host.from(uri, new PersonalAccessToken(credentials.username(), credentials.password()));
+        var host = RepositoryHost.from(uri, new PersonalAccessToken(credentials.username(), credentials.password()));
 
         var action = arguments.at(0).asString();
         if (action.equals("create")) {
             if (isMercurial) {
                 var currentBookmark = repo.currentBookmark();
@@ -410,15 +409,15 @@
                     System.err.println("");
                     System.err.println("(You can later restore the changes by running: hg unshelve)");
                     System.exit(1);
                 }
 
-                var remoteRepo = host.getRepository(projectName(uri));
+                var remoteRepo = host.repository(projectName(uri));
                 if (token == null) {
                     GitCredentials.approve(credentials);
                 }
-                var parentRepo = remoteRepo.getParent().orElseThrow(() ->
+                var parentRepo = remoteRepo.parent().orElseThrow(() ->
                         new IOException("error: remote repository " + remotePullPath + " is not a fork of any repository"));
 
                 var file = Files.createTempFile("PULL_REQUEST_", ".txt");
                 if (commits.size() == 1) {
                     var commit = commits.get(0);
@@ -478,15 +477,15 @@
 
                 var pr = remoteRepo.createPullRequest(parentRepo, targetBranch, bookmark.name(), title, body);
                 if (arguments.contains("assignees")) {
                     var usernames = Arrays.asList(arguments.get("assignees").asString().split(","));
                     var assignees = usernames.stream()
-                                             .map(host::getUserDetails)
+                                             .map(host::user)
                                              .collect(Collectors.toList());
                     pr.setAssignees(assignees);
                 }
-                System.out.println(pr.getWebUrl().toString());
+                System.out.println(pr.webUrl().toString());
                 Files.deleteIfExists(file);
 
                 System.exit(0);
             }
             var currentBranch = repo.currentBranch();
@@ -567,15 +566,15 @@
                 System.err.println("");
                 System.err.println("(You can later restore the changes by running: git stash pop)");
                 System.exit(1);
             }
 
-            var remoteRepo = host.getRepository(projectName(uri));
+            var remoteRepo = host.repository(projectName(uri));
             if (token == null) {
                 GitCredentials.approve(credentials);
             }
-            var parentRepo = remoteRepo.getParent().orElseThrow(() ->
+            var parentRepo = remoteRepo.parent().orElseThrow(() ->
                     new IOException("error: remote repository " + remotePullPath + " is not a fork of any repository"));
 
             var file = Files.createTempFile("PULL_REQUEST_", ".txt");
             if (commits.size() == 1) {
                 var commit = commits.get(0);
@@ -635,15 +634,15 @@
 
             var pr = remoteRepo.createPullRequest(parentRepo, targetBranch, currentBranch.name(), title, body);
             if (arguments.contains("assignees")) {
                 var usernames = Arrays.asList(arguments.get("assignees").asString().split(","));
                 var assignees = usernames.stream()
-                                         .map(host::getUserDetails)
+                                         .map(host::user)
                                          .collect(Collectors.toList());
                 pr.setAssignees(assignees);
             }
-            System.out.println(pr.getWebUrl().toString());
+            System.out.println(pr.webUrl().toString());
             Files.deleteIfExists(file);
         } else if (action.equals("integrate") || action.equals("approve")) {
             var pr = getPullRequest(uri, credentials, arguments.at(1));
 
             if (action.equals("integrate")) {
@@ -653,11 +652,11 @@
             } else {
                 throw new IllegalStateException("unexpected action: " + action);
             }
         } else if (action.equals("list")) {
             var remoteRepo = getHostedRepositoryFor(uri, credentials);
-            var prs = remoteRepo.getPullRequests();
+            var prs = remoteRepo.pullRequests();
 
             var ids = new ArrayList<String>();
             var titles = new ArrayList<String>();
             var authors = new ArrayList<String>();
             var assignees = new ArrayList<String>();
@@ -690,30 +689,30 @@
                         System.exit(1);
                     }
                 }
             }
 
-            for (var pr : remoteRepo.getPullRequests()) {
-                var prAuthor = pr.getAuthor().userName();
+            for (var pr : remoteRepo.pullRequests()) {
+                var prAuthor = pr.author().userName();
                 if (!filterAuthors.isEmpty() && !filterAuthors.contains(prAuthor)) {
                     continue;
                 }
 
-                var prAssignees = pr.getAssignees().stream()
-                                   .map(HostUserDetails::userName)
-                                   .collect(Collectors.toSet());
+                var prAssignees = pr.assignees().stream()
+                                    .map(HostUser::userName)
+                                    .collect(Collectors.toSet());
                 if (!filterAssignees.isEmpty() && !filterAssignees.stream().anyMatch(prAssignees::contains)) {
                     continue;
                 }
 
-                var prLabels = new HashSet<>(pr.getLabels());
+                var prLabels = new HashSet<>(pr.labels());
                 if (!filterLabels.isEmpty() && !filterLabels.stream().anyMatch(prLabels::contains)) {
                     continue;
                 }
 
-                ids.add(pr.getId());
-                titles.add(pr.getTitle());
+                ids.add(pr.id());
+                titles.add(pr.title());
                 authors.add(prAuthor);
                 assignees.add(String.join(",", prAssignees));
                 labels.add(String.join(",", prLabels));
             }
 
@@ -745,20 +744,20 @@
             if (!prId.isPresent()) {
                 exit("error: missing pull request identifier");
             }
 
             var remoteRepo = getHostedRepositoryFor(uri, credentials);
-            var pr = remoteRepo.getPullRequest(prId.asString());
-            var repoUrl = remoteRepo.getWebUrl();
-            var prHeadRef = pr.getSourceRef();
+            var pr = remoteRepo.pullRequest(prId.asString());
+            var repoUrl = remoteRepo.webUrl();
+            var prHeadRef = pr.sourceRef();
             var isHgGit = isMercurial && Repository.exists(repo.root().resolve(".hg").resolve("git"));
             if (isHgGit) {
                 var hgGitRepo = Repository.get(repo.root().resolve(".hg").resolve("git")).get();
                 var hgGitFetchHead = hgGitRepo.fetch(repoUrl, prHeadRef);
 
                 if (action.equals("show") || action.equals("apply")) {
-                    var target = hgGitRepo.fetch(repoUrl, pr.getTargetRef());
+                    var target = hgGitRepo.fetch(repoUrl, pr.targetRef());
                     var hgGitMergeBase = hgGitRepo.mergeBase(target, hgGitFetchHead);
 
                     if (action.equals("show")) {
                         show(hgGitMergeBase.hex(), hgGitFetchHead, hgGitRepo.root());
                     } else {
@@ -789,11 +788,11 @@
                 }
 
                 return;
             }
 
-            var fetchHead = repo.fetch(repoUrl, pr.getSourceRef());
+            var fetchHead = repo.fetch(repoUrl, pr.sourceRef());
             if (action.equals("fetch")) {
                 if (arguments.contains("branch")) {
                     var branchName = arguments.get("branch").asString();
                     repo.branch(fetchHead, branchName);
                 } else {
@@ -806,37 +805,37 @@
                     repo.checkout(branch, false);
                 } else {
                     repo.checkout(fetchHead, false);
                 }
             } else if (action.equals("show")) {
-                show(pr.getTargetRef(), fetchHead);
+                show(pr.targetRef(), fetchHead);
             } else if (action.equals("apply")) {
-                var patch = diff(pr.getTargetRef(), fetchHead);
+                var patch = diff(pr.targetRef(), fetchHead);
                 apply(patch);
                 Files.deleteIfExists(patch);
             }
         } else if (action.equals("close")) {
             var prId = arguments.at(1);
             if (!prId.isPresent()) {
                 exit("error: missing pull request identifier");
             }
 
             var remoteRepo = getHostedRepositoryFor(uri, credentials);
-            var pr = remoteRepo.getPullRequest(prId.asString());
+            var pr = remoteRepo.pullRequest(prId.asString());
             pr.setState(PullRequest.State.CLOSED);
         } else if (action.equals("update")) {
             var prId = arguments.at(1);
             if (!prId.isPresent()) {
                 exit("error: missing pull request identifier");
             }
 
             var remoteRepo = getHostedRepositoryFor(uri, credentials);
-            var pr = remoteRepo.getPullRequest(prId.asString());
+            var pr = remoteRepo.pullRequest(prId.asString());
             if (arguments.contains("assignees")) {
                 var usernames = Arrays.asList(arguments.get("assignees").asString().split(","));
                 var assignees = usernames.stream()
-                    .map(host::getUserDetails)
+                    .map(host::user)
                     .collect(Collectors.toList());
                 pr.setAssignees(assignees);
             }
         } else {
             exit("error: unexpected action: " + action);
diff a/host/build.gradle b/host/build.gradle
--- a/host/build.gradle
+++ b/host/build.gradle
@@ -38,10 +38,11 @@
     implementation project(':census')
     implementation project(':json')
     implementation project(':ini')
     implementation project(':process')
     implementation project(':email')
+    implementation project(':network')
 
     testImplementation project(':test')
 }
 
 publishing {
diff a/host/src/main/java/module-info.java b/host/src/main/java/module-info.java
--- a/host/src/main/java/module-info.java
+++ b/host/src/main/java/module-info.java
@@ -25,11 +25,11 @@
     requires org.openjdk.skara.census;
     requires org.openjdk.skara.json;
     requires org.openjdk.skara.ini;
     requires org.openjdk.skara.process;
     requires org.openjdk.skara.email;
+    requires org.openjdk.skara.network;
     requires java.net.http;
     requires java.logging;
 
     exports org.openjdk.skara.host;
-    exports org.openjdk.skara.host.network;
 }
diff a/host/src/main/java/org/openjdk/skara/host/Comment.java b/host/src/main/java/org/openjdk/skara/host/Comment.java
--- a/host/src/main/java/org/openjdk/skara/host/Comment.java
+++ b/host/src/main/java/org/openjdk/skara/host/Comment.java
@@ -26,15 +26,15 @@
 
 public class Comment {
 
     private final String id;
     private final String body;
-    private final HostUserDetails author;
+    private final HostUser author;
     private final ZonedDateTime createdAt;
     private final ZonedDateTime updatedAt;
 
-    public Comment(String id, String body, HostUserDetails author, ZonedDateTime createdAt, ZonedDateTime updatedAt) {
+    public Comment(String id, String body, HostUser author, ZonedDateTime createdAt, ZonedDateTime updatedAt) {
         this.id = id;
         this.body = body;
         this.author = author;
         this.createdAt = createdAt;
         this.updatedAt = updatedAt;
@@ -46,11 +46,11 @@
 
     public String body() {
         return body;
     }
 
-    public HostUserDetails author() {
+    public HostUser author() {
         return author;
     }
 
     public ZonedDateTime createdAt() {
         return createdAt;
diff a/host/src/main/java/org/openjdk/skara/host/Host.java b/host/src/main/java/org/openjdk/skara/host/Host.java
--- a/host/src/main/java/org/openjdk/skara/host/Host.java
+++ b/host/src/main/java/org/openjdk/skara/host/Host.java
@@ -24,20 +24,9 @@
 
 import java.net.URI;
 
 public interface Host {
     boolean isValid();
-    HostedRepository getRepository(String name);
-    IssueProject getIssueProject(String name);
-    HostUserDetails getUserDetails(String username);
-    HostUserDetails getCurrentUserDetails();
-    boolean supportsReviewBody();
-    boolean isMemberOf(String groupId, HostUserDetails user);
-
-    static Host from(URI uri, PersonalAccessToken pat) {
-        return HostFactory.createFromURI(uri, pat);
-    }
-
-    static Host from(URI uri) {
-        return HostFactory.createFromURI(uri, null);
-    }
+    HostUser user(String username);
+    HostUser currentUser();
+    boolean isMemberOf(String groupId, HostUser user);
 }
diff a/host/src/main/java/org/openjdk/skara/host/HostFactory.java b/host/src/main/java/org/openjdk/skara/host/HostFactory.java
--- a/host/src/main/java/org/openjdk/skara/host/HostFactory.java
+++ b/host/src/main/java/org/openjdk/skara/host/HostFactory.java
@@ -28,39 +28,39 @@
 
 import java.net.URI;
 import java.util.regex.Pattern;
 
 public class HostFactory {
-    public static Host createGitHubHost(URI uri, Pattern webUriPattern, String webUriReplacement, String keyFile, String issue, String id) {
+    public static RepositoryHost createGitHubHost(URI uri, Pattern webUriPattern, String webUriReplacement, String keyFile, String issue, String id) {
         var app = new GitHubApplication(keyFile, issue, id);
         return new GitHubHost(uri, app, webUriPattern, webUriReplacement);
     }
 
-    public static Host createGitHubHost(URI uri, PersonalAccessToken pat) {
+    public static RepositoryHost createGitHubHost(URI uri, PersonalAccessToken pat) {
         if (pat != null) {
             return new GitHubHost(uri, pat);
         } else {
             return new GitHubHost(uri);
         }
     }
 
-    public static Host createGitLabHost(URI uri, PersonalAccessToken pat) {
+    public static RepositoryHost createGitLabHost(URI uri, PersonalAccessToken pat) {
         if (pat != null) {
             return new GitLabHost(uri, pat);
         } else {
             return new GitLabHost(uri);
         }
     }
 
-    public static Host createJiraHost(URI uri, PersonalAccessToken pat) {
+    public static IssueHost createJiraHost(URI uri, PersonalAccessToken pat) {
         if (pat != null) {
             throw new RuntimeException("authentication not implemented yet");
         }
         return new JiraHost(uri);
     }
 
-    public static Host createFromURI(URI uri, PersonalAccessToken pat) throws IllegalArgumentException {
+    public static RepositoryHost createFromURI(URI uri, PersonalAccessToken pat) throws IllegalArgumentException {
         // Short-circuit
         if (uri.toString().contains("github")) {
             return createGitHubHost(uri, pat);
         } else if (uri.toString().contains("gitlab")) {
             return createGitLabHost(uri, pat);
diff a/host/src/main/java/org/openjdk/skara/host/HostUser.java b/host/src/main/java/org/openjdk/skara/host/HostUser.java
--- /dev/null
+++ b/host/src/main/java/org/openjdk/skara/host/HostUser.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package org.openjdk.skara.host;
+
+import java.util.Objects;
+import java.util.function.Supplier;
+
+public class HostUser {
+    private final String id;
+    private final String username;
+    private final Supplier<String> nameSupplier;
+    private String name;
+
+    public HostUser(String id, String username, Supplier<String> nameSupplier) {
+        this.id = id;
+        this.username = username;
+        this.nameSupplier = nameSupplier;
+    }
+
+    public HostUser(String id, String username, String name) {
+        this(id, username, () -> name);
+    }
+
+    public HostUser(int id, String username, String name) {
+        this(String.valueOf(id), username, name);
+    }
+
+    public HostUser(int id, String username, Supplier<String> nameSupplier) {
+        this(String.valueOf(id), username, nameSupplier);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        HostUser that = (HostUser) o;
+        return id.equals(that.id) &&
+                Objects.equals(username, that.username);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(id, username);
+    }
+
+    public String id() {
+        return id;
+    }
+
+    public String userName() {
+        return username;
+    }
+
+    public String fullName() {
+        if (name == null) {
+            name = nameSupplier.get();
+        }
+        return name;
+    }
+
+    @Override
+    public String toString() {
+        return "HostUserDetails{" +
+                "id=" + id +
+                ", username='" + username + '\'' +
+                ", name='" + name + '\'' +
+                '}';
+    }
+}
diff a/host/src/main/java/org/openjdk/skara/host/HostUserDetails.java b/host/src/main/java/org/openjdk/skara/host/HostUserDetails.java
--- a/host/src/main/java/org/openjdk/skara/host/HostUserDetails.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code 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
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package org.openjdk.skara.host;
-
-import java.util.Objects;
-import java.util.function.Supplier;
-
-public class HostUserDetails {
-    private final String id;
-    private final String username;
-    private final Supplier<String> nameSupplier;
-    private String name;
-
-    public HostUserDetails(String id, String username, Supplier<String> nameSupplier) {
-        this.id = id;
-        this.username = username;
-        this.nameSupplier = nameSupplier;
-    }
-
-    public HostUserDetails(String id, String username, String name) {
-        this(id, username, () -> name);
-    }
-
-    public HostUserDetails(int id, String username, String name) {
-        this(String.valueOf(id), username, name);
-    }
-
-    public HostUserDetails(int id, String username, Supplier<String> nameSupplier) {
-        this(String.valueOf(id), username, nameSupplier);
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) {
-            return true;
-        }
-        if (o == null || getClass() != o.getClass()) {
-            return false;
-        }
-        HostUserDetails that = (HostUserDetails) o;
-        return id.equals(that.id) &&
-                Objects.equals(username, that.username);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(id, username);
-    }
-
-    public String id() {
-        return id;
-    }
-
-    public String userName() {
-        return username;
-    }
-
-    public String fullName() {
-        if (name == null) {
-            name = nameSupplier.get();
-        }
-        return name;
-    }
-
-    @Override
-    public String toString() {
-        return "HostUserDetails{" +
-                "id=" + id +
-                ", username='" + username + '\'' +
-                ", name='" + name + '\'' +
-                '}';
-    }
-}
diff a/host/src/main/java/org/openjdk/skara/host/HostedRepository.java b/host/src/main/java/org/openjdk/skara/host/HostedRepository.java
--- a/host/src/main/java/org/openjdk/skara/host/HostedRepository.java
+++ b/host/src/main/java/org/openjdk/skara/host/HostedRepository.java
@@ -27,32 +27,32 @@
 
 import java.net.URI;
 import java.util.*;
 
 public interface HostedRepository extends IssueProject {
-    Host host();
+    RepositoryHost host();
     PullRequest createPullRequest(HostedRepository target,
                                   String targetRef,
                                   String sourceRef,
                                   String title,
                                   List<String> body,
                                   boolean draft);
-    PullRequest getPullRequest(String id);
-    List<PullRequest> getPullRequests();
+    PullRequest pullRequest(String id);
+    List<PullRequest> pullRequests();
     List<PullRequest> findPullRequestsWithComment(String author, String body);
     Optional<PullRequest> parsePullRequestUrl(String url);
-    String getName();
-    Optional<HostedRepository> getParent();
-    URI getUrl();
-    URI getWebUrl(Hash hash);
-    VCS getRepositoryType();
-    String getFileContents(String filename, String ref);
-    String getNamespace();
+    String name();
+    Optional<HostedRepository> parent();
+    URI url();
+    URI webUrl(Hash hash);
+    VCS repositoryType();
+    String fileContents(String filename, String ref);
+    String namespace();
     Optional<WebHook> parseWebHook(JSONValue body);
     HostedRepository fork();
-    long getId();
-    Hash getBranchHash(String ref);
+    long id();
+    Hash branchHash(String ref);
 
     default PullRequest createPullRequest(HostedRepository target,
                                           String targetRef,
                                           String sourceRef,
                                           String title,
diff a/host/src/main/java/org/openjdk/skara/host/Issue.java b/host/src/main/java/org/openjdk/skara/host/Issue.java
--- a/host/src/main/java/org/openjdk/skara/host/Issue.java
+++ b/host/src/main/java/org/openjdk/skara/host/Issue.java
@@ -35,23 +35,23 @@
 
     /**
      * The repository-specific identifier.
      * @return
      */
-    String getId();
+    String id();
 
     /**
      * The host-specific author name.
      * @return
      */
-    HostUserDetails getAuthor();
+    HostUser author();
 
     /**
      * Title of the request.
      * @return
      */
-    String getTitle();
+    String title();
 
     /**
      * Update the title of the request.
      * @param title
      */
@@ -59,11 +59,11 @@
 
     /**
      * The main body of the request.
      * @return
      */
-    String getBody();
+    String body();
 
     /**
      * Update the main body of the request.
      * @param body
      */
@@ -71,11 +71,11 @@
 
     /**
      * All comments on the issue, in ascending creation time order.
      * @return
      */
-    List<Comment> getComments();
+    List<Comment> comments();
 
     /**
      * Posts a new comment.
      * @param body
      */
@@ -90,17 +90,17 @@
 
     /**
      * When the request was created.
      * @return
      */
-    ZonedDateTime getCreated();
+    ZonedDateTime createdAt();
 
     /**
      * When the request was last updated.
      * @return
      */
-    ZonedDateTime getUpdated();
+    ZonedDateTime updatedAt();
 
     enum State {
         OPEN,
         CLOSED
     }
@@ -125,23 +125,23 @@
 
     /**
      * Retrieves all the currently set labels.
      * @return
      */
-    List<String> getLabels();
+    List<String> labels();
 
     /**
      * Returns a link that will lead to the issue.
      */
-    URI getWebUrl();
+    URI webUrl();
 
     /**
      * Returns all usernames assigned to the issue.
      */
-    List<HostUserDetails> getAssignees();
+    List<HostUser> assignees();
 
     /**
      * Update the list of assignees.
      * @param assignees
      */
-    void setAssignees(List<HostUserDetails> assignees);
+    void setAssignees(List<HostUser> assignees);
 }
diff a/host/src/main/java/org/openjdk/skara/host/IssueHost.java b/host/src/main/java/org/openjdk/skara/host/IssueHost.java
--- /dev/null
+++ b/host/src/main/java/org/openjdk/skara/host/IssueHost.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package org.openjdk.skara.host;
+
+public interface IssueHost extends Host {
+    IssueProject project(String name);
+}
diff a/host/src/main/java/org/openjdk/skara/host/IssueProject.java b/host/src/main/java/org/openjdk/skara/host/IssueProject.java
--- a/host/src/main/java/org/openjdk/skara/host/IssueProject.java
+++ b/host/src/main/java/org/openjdk/skara/host/IssueProject.java
@@ -25,10 +25,10 @@
 import java.net.URI;
 import java.util.*;
 
 public interface IssueProject {
     Host host();
-    URI getWebUrl();
+    URI webUrl();
     Issue createIssue(String title, List<String> body);
-    Optional<Issue> getIssue(String id);
-    List<Issue> getIssues();
+    Optional<Issue> issue(String id);
+    List<Issue> issues();
 }
diff a/host/src/main/java/org/openjdk/skara/host/PullRequest.java b/host/src/main/java/org/openjdk/skara/host/PullRequest.java
--- a/host/src/main/java/org/openjdk/skara/host/PullRequest.java
+++ b/host/src/main/java/org/openjdk/skara/host/PullRequest.java
@@ -32,11 +32,11 @@
 
     /**
      * List of reviews, in descending chronological order.
      * @return
      */
-    List<Review> getReviews();
+    List<Review> reviews();
 
     /**
      * Adds a review with the given verdict.
      */
     void addReview(Review.Verdict verdict, String body);
@@ -61,41 +61,41 @@
 
     /**
      * Get all file specific comments.
      * @return
      */
-    List<ReviewComment> getReviewComments();
+    List<ReviewComment> reviewComments();
 
     /**
      * Hash of the current head of the request.
      * @return
      */
-    Hash getHeadHash();
+    Hash headHash();
 
     /**
      * Returns the name of the ref the request is created from.
      * @return
      */
-    String getSourceRef();
+    String sourceRef();
 
     /**
      * Returns the name of the ref the request is intended to be merged into.
      * @return
      */
-    String getTargetRef();
+    String targetRef();
 
     /**
      * Returns the current head of the ref the request is intended to be merged into.
      * @return
      */
-    Hash getTargetHash();
+    Hash targetHash();
 
     /**
      * List of completed checks on the given hash.
      * @return
      */
-    Map<String, Check> getChecks(Hash hash);
+    Map<String, Check> checks(Hash hash);
 
     /**
      * Creates a new check.
      * @param check
      */
@@ -108,16 +108,16 @@
     void updateCheck(Check check);
 
     /**
      * Returns a link that will lead to the list of changes done in the request.
      */
-    URI getChangeUrl();
+    URI changeUrl();
 
     /**
      * Returns a link that will lead to the list of changes with the specified base.
      */
-    URI getChangeUrl(Hash base);
+    URI changeUrl(Hash base);
 
     /**
      * Returns true if the request is in draft mode.
      * @return
      */
diff a/host/src/main/java/org/openjdk/skara/host/PullRequestUpdateCache.java b/host/src/main/java/org/openjdk/skara/host/PullRequestUpdateCache.java
--- a/host/src/main/java/org/openjdk/skara/host/PullRequestUpdateCache.java
+++ b/host/src/main/java/org/openjdk/skara/host/PullRequestUpdateCache.java
@@ -37,32 +37,32 @@
     private String getUniqueId(PullRequest pr) {
         var repo = pr.repository();
         if (!repositoryIds.containsKey(repo)) {
             repositoryIds.put(repo, Integer.toString(repositoryIds.size()));
         }
-        return repositoryIds.get(repo) + ";" + pr.getId();
+        return repositoryIds.get(repo) + ";" + pr.id();
     }
 
     public synchronized boolean needsUpdate(PullRequest pr) {
         // GitLab CE does not update this field on events such as adding an award
         if (pr instanceof GitLabMergeRequest) {
             return true;
         }
 
         var uniqueId = getUniqueId(pr);
-        var update = pr.getUpdated();
+        var update = pr.updatedAt();
 
         if (!lastUpdates.containsKey(uniqueId)) {
             lastUpdates.put(uniqueId, update);
             return true;
         }
         var lastUpdate = lastUpdates.get(uniqueId);
         if (lastUpdate.isBefore(update)) {
             lastUpdates.put(uniqueId, update);
             return true;
         }
-        log.info("Skipping update for " + pr.repository().getName() + "#" + pr.getId());
+        log.info("Skipping update for " + pr.repository().name() + "#" + pr.id());
         return false;
     }
 
     public synchronized void invalidate(PullRequest pr) {
         var uniqueId = getUniqueId(pr);
diff a/host/src/main/java/org/openjdk/skara/host/RepositoryHost.java b/host/src/main/java/org/openjdk/skara/host/RepositoryHost.java
--- /dev/null
+++ b/host/src/main/java/org/openjdk/skara/host/RepositoryHost.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package org.openjdk.skara.host;
+
+import java.net.URI;
+
+public interface RepositoryHost extends Host {
+    HostedRepository repository(String name);
+    boolean supportsReviewBody();
+
+    static RepositoryHost from(URI uri, PersonalAccessToken pat) {
+        return HostFactory.createFromURI(uri, pat);
+    }
+    static RepositoryHost from(URI uri) {
+        return HostFactory.createFromURI(uri, null);
+    }
+}
diff a/host/src/main/java/org/openjdk/skara/host/Review.java b/host/src/main/java/org/openjdk/skara/host/Review.java
--- a/host/src/main/java/org/openjdk/skara/host/Review.java
+++ b/host/src/main/java/org/openjdk/skara/host/Review.java
@@ -25,25 +25,25 @@
 import org.openjdk.skara.vcs.Hash;
 
 import java.util.Optional;
 
 public class Review {
-    private final HostUserDetails reviewer;
+    private final HostUser reviewer;
     private final Verdict verdict;
     private final Hash hash;
     private final int id;
     private final String body;
 
-    public Review(HostUserDetails reviewer, Verdict verdict, Hash hash, int id, String body) {
+    public Review(HostUser reviewer, Verdict verdict, Hash hash, int id, String body) {
         this.reviewer = reviewer;
         this.verdict = verdict;
         this.hash = hash;
         this.id = id;
         this.body = body;
     }
 
-    public HostUserDetails reviewer() {
+    public HostUser reviewer() {
         return reviewer;
     }
 
     public Verdict verdict() {
         return verdict;
diff a/host/src/main/java/org/openjdk/skara/host/ReviewComment.java b/host/src/main/java/org/openjdk/skara/host/ReviewComment.java
--- a/host/src/main/java/org/openjdk/skara/host/ReviewComment.java
+++ b/host/src/main/java/org/openjdk/skara/host/ReviewComment.java
@@ -32,11 +32,11 @@
     private final String threadId;
     private final Hash hash;
     private final String path;
     private final int line;
 
-    public ReviewComment(ReviewComment parent, String threadId, Hash hash, String path, int line, String id, String body, HostUserDetails author, ZonedDateTime createdAt, ZonedDateTime updatedAt) {
+    public ReviewComment(ReviewComment parent, String threadId, Hash hash, String path, int line, String id, String body, HostUser author, ZonedDateTime createdAt, ZonedDateTime updatedAt) {
         super(id, body, author, createdAt, updatedAt);
 
         this.parent = parent;
         this.threadId = threadId;
         this.hash = hash;
diff a/host/src/main/java/org/openjdk/skara/host/github/GitHubApplication.java b/host/src/main/java/org/openjdk/skara/host/github/GitHubApplication.java
--- a/host/src/main/java/org/openjdk/skara/host/github/GitHubApplication.java
+++ b/host/src/main/java/org/openjdk/skara/host/github/GitHubApplication.java
@@ -20,11 +20,11 @@
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 package org.openjdk.skara.host.github;
 
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.json.*;
 
 import java.io.*;
 import java.net.URI;
 import java.net.http.*;
diff a/host/src/main/java/org/openjdk/skara/host/github/GitHubHost.java b/host/src/main/java/org/openjdk/skara/host/github/GitHubHost.java
--- a/host/src/main/java/org/openjdk/skara/host/github/GitHubHost.java
+++ b/host/src/main/java/org/openjdk/skara/host/github/GitHubHost.java
@@ -21,26 +21,26 @@
  * questions.
  */
 package org.openjdk.skara.host.github;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.*;
 
 import java.net.*;
 import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.regex.Pattern;
 
-public class GitHubHost implements Host {
+public class GitHubHost implements RepositoryHost {
     private final URI uri;
     private final Pattern webUriPattern;
     private final String webUriReplacement;
     private final GitHubApplication application;
     private final PersonalAccessToken pat;
     private final RestRequest request;
-    private HostUserDetails currentUser;
+    private HostUser currentUser;
 
     public GitHubHost(URI uri, GitHubApplication application, Pattern webUriPattern, String webUriReplacement) {
         this.uri = uri;
         this.webUriPattern = webUriPattern;
         this.webUriReplacement = webUriReplacement;
@@ -117,22 +117,22 @@
             return pat.token();
         }
     }
 
     private String getFullName(String userName) {
-        var details = getUserDetails(userName);
+        var details = user(userName);
         return details.fullName();
     }
 
     // Most GitHub API's return user information in this format
-    HostUserDetails parseUserField(JSONValue json) {
+    HostUser parseUserField(JSONValue json) {
         return parseUserObject(json.get("user"));
     }
 
-    HostUserDetails parseUserObject(JSONValue json) {
-        return new HostUserDetails(json.get("id").asInt(), json.get("login").asString(),
-                                   () -> getFullName(json.get("login").asString()));
+    HostUser parseUserObject(JSONValue json) {
+        return new HostUser(json.get("id").asInt(), json.get("login").asString(),
+                            () -> getFullName(json.get("login").asString()));
     }
 
     @Override
     public boolean isValid() {
         var endpoints = request.get("")
@@ -153,43 +153,38 @@
                             .execute();
         return result.asObject();
     }
 
     @Override
-    public HostedRepository getRepository(String name) {
+    public HostedRepository repository(String name) {
         return new GitHubRepository(this, name);
     }
 
     @Override
-    public IssueProject getIssueProject(String name) {
-        throw new RuntimeException("not implemented yet");
-    }
-
-    @Override
-    public HostUserDetails getUserDetails(String username) {
+    public HostUser user(String username) {
         var details = request.get("users/" + URLEncoder.encode(username, StandardCharsets.UTF_8)).execute().asObject();
 
         // Always present
         var login = details.get("login").asString();
         var id = details.get("id").asInt();
 
         var name = details.get("name").asString();
         if (name == null) {
             name = login;
         }
-        return new HostUserDetails(id, login, name);
+        return new HostUser(id, login, name);
     }
 
     @Override
-    public HostUserDetails getCurrentUserDetails() {
+    public HostUser currentUser() {
         if (currentUser == null) {
             if (application != null) {
                 var appDetails = application.getAppDetails();
                 var appName = appDetails.get("name").asString() + "[bot]";
-                currentUser = getUserDetails(appName);
+                currentUser = user(appName);
             } else if (pat != null) {
-                currentUser = getUserDetails(pat.userName());
+                currentUser = user(pat.userName());
             } else {
                 throw new IllegalStateException("No credentials present");
             }
         }
         return currentUser;
@@ -199,11 +194,11 @@
     public boolean supportsReviewBody() {
         return true;
     }
 
     @Override
-    public boolean isMemberOf(String groupId, HostUserDetails user) {
+    public boolean isMemberOf(String groupId, HostUser user) {
         long gid = 0L;
         try {
             gid = Long.parseLong(groupId);
         } catch (NumberFormatException e) {
             throw new IllegalArgumentException("Group id is not a number: " + groupId);
diff a/host/src/main/java/org/openjdk/skara/host/github/GitHubPullRequest.java b/host/src/main/java/org/openjdk/skara/host/github/GitHubPullRequest.java
--- a/host/src/main/java/org/openjdk/skara/host/github/GitHubPullRequest.java
+++ b/host/src/main/java/org/openjdk/skara/host/github/GitHubPullRequest.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.host.github;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.*;
 import org.openjdk.skara.vcs.Hash;
 
 import java.net.URI;
 import java.time.*;
@@ -57,21 +57,21 @@
     public IssueProject project() {
         return repository;
     }
 
     @Override
-    public String getId() {
+    public String id() {
         return json.get("number").toString();
     }
 
     @Override
-    public HostUserDetails getAuthor() {
+    public HostUser author() {
         return host.parseUserField(json);
     }
 
     @Override
-    public List<Review> getReviews() {
+    public List<Review> reviews() {
         var reviews = request.get("pulls/" + json.get("number").toString() + "/reviews").execute().stream()
                              .map(JSONValue::asObject)
                              .filter(obj -> !(obj.get("state").asString().equals("COMMENTED") && obj.get("body").asString().isEmpty()))
                              .map(obj -> {
                                  var reviewer = host.parseUserField(obj);
@@ -165,11 +165,11 @@
                 .execute();
         return parseReviewComment(parent, response.asObject(), diff);
     }
 
     @Override
-    public List<ReviewComment> getReviewComments() {
+    public List<ReviewComment> reviewComments() {
         var rawDiff = request.get("pulls/" + json.get("number").toString())
                           .header("Accept", "application/vnd.github.v3.diff")
                           .executeUnparsed();
         var diff = PositionMapper.parse(rawDiff);
 
@@ -191,41 +191,41 @@
 
         return ret;
     }
 
     @Override
-    public Hash getHeadHash() {
+    public Hash headHash() {
         return new Hash(json.get("head").get("sha").asString());
     }
 
     @Override
-    public String getSourceRef() {
-        return "pull/" + getId() + "/head";
+    public String sourceRef() {
+        return "pull/" + id() + "/head";
     }
 
     @Override
-    public String getTargetRef() {
+    public String targetRef() {
         return json.get("base").get("ref").asString();
     }
 
     @Override
-    public Hash getTargetHash() {
-        return repository.getBranchHash(getTargetRef());
+    public Hash targetHash() {
+        return repository.branchHash(targetRef());
     }
 
     @Override
-    public String getTitle() {
+    public String title() {
         return json.get("title").asString();
     }
 
     @Override
     public void setTitle(String title) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public String getBody() {
+    public String body() {
         var body = json.get("body").asString();
         if (body == null) {
             body = "";
         }
         return body;
@@ -246,11 +246,11 @@
                               ZonedDateTime.parse(comment.get("updated_at").asString()));
         return ret;
     }
 
     @Override
-    public List<Comment> getComments() {
+    public List<Comment> comments() {
         return request.get("issues/" + json.get("number").toString() + "/comments").execute().stream()
                 .map(this::parseComment)
                 .collect(Collectors.toList());
     }
 
@@ -269,21 +269,21 @@
                 .execute();
         return parseComment(comment);
     }
 
     @Override
-    public ZonedDateTime getCreated() {
+    public ZonedDateTime createdAt() {
         return ZonedDateTime.parse(json.get("created_at").asString());
     }
 
     @Override
-    public ZonedDateTime getUpdated() {
+    public ZonedDateTime updatedAt() {
         return ZonedDateTime.parse(json.get("updated_at").asString());
     }
 
     @Override
-    public Map<String, Check> getChecks(Hash hash) {
+    public Map<String, Check> checks(Hash hash) {
         var checks = request.get("commits/" + hash.hex() + "/check-runs").execute();
 
         return checks.get("check_runs").stream()
                 .collect(Collectors.toMap(c -> c.get("name").asString(),
                         c -> {
@@ -388,17 +388,17 @@
 
         request.post("check-runs").body(completedQuery).execute();
     }
 
     @Override
-    public URI getChangeUrl() {
-        return URIBuilder.base(getWebUrl()).appendPath("/files").build();
+    public URI changeUrl() {
+        return URIBuilder.base(webUrl()).appendPath("/files").build();
     }
 
     @Override
-    public URI getChangeUrl(Hash base) {
-        return URIBuilder.base(getWebUrl()).appendPath("/files/" + base.abbreviate() + ".." + getHeadHash().abbreviate()).build();
+    public URI changeUrl(Hash base) {
+        return URIBuilder.base(webUrl()).appendPath("/files/" + base.abbreviate() + ".." + headHash().abbreviate()).build();
     }
 
     @Override
     public boolean isDraft() {
         return json.get("draft").asBoolean();
@@ -431,40 +431,40 @@
                })
                .execute();
     }
 
     @Override
-    public List<String> getLabels() {
+    public List<String> labels() {
         return request.get("issues/" + json.get("number").toString() + "/labels").execute().stream()
                       .map(JSONValue::asObject)
                       .map(obj -> obj.get("name").asString())
                       .sorted()
                       .collect(Collectors.toList());
     }
 
     @Override
-    public URI getWebUrl() {
+    public URI webUrl() {
         var host = (GitHubHost)repository.host();
-        var endpoint = "/" + repository.getName() + "/pull/" + getId();
+        var endpoint = "/" + repository.name() + "/pull/" + id();
         return host.getWebURI(endpoint);
     }
 
     @Override
     public String toString() {
-        return "GitHubPullRequest #" + getId() + " by " + getAuthor();
+        return "GitHubPullRequest #" + id() + " by " + author();
     }
 
     @Override
-    public List<HostUserDetails> getAssignees() {
+    public List<HostUser> assignees() {
         return json.get("assignees").asArray()
                                     .stream()
                                     .map(host::parseUserObject)
                                     .collect(Collectors.toList());
     }
 
     @Override
-    public void setAssignees(List<HostUserDetails> assignees) {
+    public void setAssignees(List<HostUser> assignees) {
         var assignee_ids = JSON.array();
         for (var assignee : assignees) {
             assignee_ids.add(assignee.userName());
         }
         var param = JSON.object().put("assignees", assignee_ids);
diff a/host/src/main/java/org/openjdk/skara/host/github/GitHubRepository.java b/host/src/main/java/org/openjdk/skara/host/github/GitHubRepository.java
--- a/host/src/main/java/org/openjdk/skara/host/github/GitHubRepository.java
+++ b/host/src/main/java/org/openjdk/skara/host/github/GitHubRepository.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.host.github;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.*;
 import org.openjdk.skara.vcs.*;
 
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
@@ -58,35 +58,35 @@
         var urlPattern = gitHubHost.getWebURI("/" + repository + "/pull/").toString();
         pullRequestPattern = Pattern.compile(urlPattern + "(\\d+)");
     }
 
     @Override
-    public Optional<HostedRepository> getParent() {
+    public Optional<HostedRepository> parent() {
         if (json.get("fork").asBoolean()) {
             var parent = json.get("parent").get("full_name").asString();
             return Optional.of(new GitHubRepository(gitHubHost, parent));
         }
         return Optional.empty();
     }
 
     @Override
-    public Host host() {
+    public RepositoryHost host() {
         return gitHubHost;
     }
 
     @Override
     public Issue createIssue(String title, List<String> body) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public Optional<Issue> getIssue(String id) {
+    public Optional<Issue> issue(String id) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public List<Issue> getIssues() {
+    public List<Issue> issues() {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
     public PullRequest createPullRequest(HostedRepository target,
@@ -98,11 +98,11 @@
         if (!(target instanceof GitHubRepository)) {
             throw new IllegalArgumentException("target repository must be a GitHub repository");
         }
 
         var upstream = (GitHubRepository) target;
-        var user = host().getCurrentUserDetails().userName();
+        var user = host().currentUser().userName();
         var namespace = user.endsWith("[bot]") ? "" : user + ":";
         var params = JSON.object()
                          .put("title", title)
                          .put("head", namespace + sourceRef)
                          .put("base", targetRef)
@@ -114,17 +114,17 @@
 
         return new GitHubPullRequest(upstream, pr, request);
     }
 
     @Override
-    public PullRequest getPullRequest(String id) {
+    public PullRequest pullRequest(String id) {
         var pr = request.get("pulls/" + id).execute();
         return new GitHubPullRequest(this, pr, request);
     }
 
     @Override
-    public List<PullRequest> getPullRequests() {
+    public List<PullRequest> pullRequests() {
         return request.get("pulls").execute().asArray().stream()
                       .map(jsonValue -> new GitHubPullRequest(this, jsonValue, request))
                       .collect(Collectors.toList());
     }
 
@@ -142,60 +142,60 @@
 
     @Override
     public Optional<PullRequest> parsePullRequestUrl(String url) {
         var matcher = pullRequestPattern.matcher(url);
         if (matcher.find()) {
-            return Optional.of(getPullRequest(matcher.group(1)));
+            return Optional.of(pullRequest(matcher.group(1)));
         } else {
             return Optional.empty();
         }
     }
 
     @Override
-    public String getName() {
+    public String name() {
         return repository;
     }
 
     @Override
-    public URI getUrl() {
+    public URI url() {
         return URIBuilder
                 .base(gitHubHost.getURI())
                 .setPath("/" + repository + ".git")
                 .setAuthentication("x-access-token:" + gitHubHost.getInstallationToken())
                 .build();
     }
 
     @Override
-    public URI getWebUrl() {
+    public URI webUrl() {
         var endpoint = "/" + repository;
         return gitHubHost.getWebURI(endpoint);
     }
 
     @Override
-    public URI getWebUrl(Hash hash) {
+    public URI webUrl(Hash hash) {
         var endpoint = "/" + repository + "/commit/" + hash.abbreviate();
         return gitHubHost.getWebURI(endpoint);
     }
 
     @Override
-    public VCS getRepositoryType() {
+    public VCS repositoryType() {
         return VCS.GIT;
     }
 
     @Override
-    public String getFileContents(String filename, String ref) {
+    public String fileContents(String filename, String ref) {
         var conf = request.get("contents/" + filename)
                           .param("ref", ref)
                           .execute().asObject();
         // Content may contain newline characters
         return conf.get("content").asString().lines()
                    .map(line -> new String(Base64.getDecoder().decode(line), StandardCharsets.UTF_8))
                    .collect(Collectors.joining());
     }
 
     @Override
-    public String getNamespace() {
+    public String namespace() {
         return URIBuilder.base(gitHubHost.getURI()).build().getHost();
     }
 
     @Override
     public Optional<WebHook> parseWebHook(JSONValue body) {
@@ -203,19 +203,19 @@
     }
 
     @Override
     public HostedRepository fork() {
         var response = request.post("forks").execute();
-        return gitHubHost.getRepository(response.get("full_name").asString());
+        return gitHubHost.repository(response.get("full_name").asString());
     }
 
     @Override
-    public long getId() {
+    public long id() {
         return json.get("id").asLong();
     }
 
     @Override
-    public Hash getBranchHash(String ref) {
+    public Hash branchHash(String ref) {
         var branch = request.get("branches/" + ref).execute();
         return new Hash(branch.get("commit").get("sha").asString());
     }
 }
diff a/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabHost.java b/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabHost.java
--- a/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabHost.java
+++ b/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabHost.java
@@ -21,18 +21,18 @@
  * questions.
  */
 package org.openjdk.skara.host.gitlab;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.*;
 
 import java.net.*;
 import java.nio.charset.StandardCharsets;
 import java.util.*;
 
-public class GitLabHost implements Host {
+public class GitLabHost implements RepositoryHost {
     private final URI uri;
     private final PersonalAccessToken pat;
     private final RestRequest request;
 
     public GitLabHost(URI uri, PersonalAccessToken pat) {
@@ -86,38 +86,33 @@
         }
         return project.asObject();
     }
 
     @Override
-    public HostedRepository getRepository(String name) {
+    public HostedRepository repository(String name) {
         return new GitLabRepository(this, name);
     }
 
-    @Override
-    public IssueProject getIssueProject(String name) {
-        throw new RuntimeException("not implemented yet");
-    }
-
-    private HostUserDetails parseUserDetails(JSONObject details) {
+    private HostUser parseUserDetails(JSONObject details) {
         var id = details.get("id").asInt();
         var username = details.get("username").asString();
         var name = details.get("name").asString();
-        return new HostUserDetails(id, username, name);
+        return new HostUser(id, username, name);
     }
 
     @Override
-    public HostUserDetails getUserDetails(String username) {
+    public HostUser user(String username) {
         var details = request.get("users").param("username", username).execute().asArray();
         if (details.size() != 1) {
             throw new RuntimeException("Couldn't find user: " + username);
         }
 
         return parseUserDetails(details.get(0).asObject());
     }
 
     @Override
-    public HostUserDetails getCurrentUserDetails() {
+    public HostUser currentUser() {
         var details = request.get("user").execute().asObject();
         return parseUserDetails(details);
     }
 
     @Override
@@ -142,11 +137,11 @@
             throw new RuntimeException("Project does not seem to be a fork");
         }
     }
 
     @Override
-    public boolean isMemberOf(String groupId, HostUserDetails user) {
+    public boolean isMemberOf(String groupId, HostUser user) {
         long gid = 0L;
         try {
             gid = Long.parseLong(groupId);
         } catch (NumberFormatException e) {
             throw new IllegalArgumentException("Group id is not a number: " + groupId);
diff a/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabMergeRequest.java b/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabMergeRequest.java
--- a/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabMergeRequest.java
+++ b/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabMergeRequest.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.host.gitlab;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.*;
 import org.openjdk.skara.vcs.Hash;
 
 import java.net.URI;
 import java.nio.charset.StandardCharsets;
@@ -57,21 +57,21 @@
     public IssueProject project() {
         return repository;
     }
 
     @Override
-    public String getId() {
+    public String id() {
         return json.get("iid").toString();
     }
 
     @Override
-    public HostUserDetails getAuthor() {
-        return repository.host().getUserDetails(json.get("author").get("username").asString());
+    public HostUser author() {
+        return repository.host().user(json.get("author").get("username").asString());
     }
 
     @Override
-    public List<Review> getReviews() {
+    public List<Review> reviews() {
 
         class CommitDate {
             private Hash hash;
             private ZonedDateTime date;
         }
@@ -95,11 +95,11 @@
                       .map(JSONValue::asObject)
                       .filter(obj -> obj.get("name").asString().equals("thumbsup") ||
                               obj.get("name").asString().equals("thumbsdown") ||
                               obj.get("name").asString().equals("question"))
                       .map(obj -> {
-                          var reviewer = repository.host().getUserDetails(obj.get("user").get("username").asString());
+                          var reviewer = repository.host().user(obj.get("user").get("username").asString());
                           Review.Verdict verdict;
                           switch (obj.get("name").asString()) {
                               case "thumbsup":
                                   verdict = Review.Verdict.APPROVED;
                                   break;
@@ -132,11 +132,11 @@
         var awards = request.get("award_emoji").execute().stream()
                             .map(JSONValue::asObject)
                             .filter(obj -> obj.get("name").asString().equals("thumbsup") ||
                                     obj.get("name").asString().equals("thumbsdown") ||
                                     obj.get("name").asString().equals("question"))
-                            .filter(obj -> obj.get("user").get("username").asString().equals(repository.host().getCurrentUserDetails().userName()))
+                            .filter(obj -> obj.get("user").get("username").asString().equals(repository.host().currentUser().userName()))
                             .map(obj -> obj.get("id").toString())
                             .collect(Collectors.toList());
         for (var award : awards) {
             request.delete("award_emoji/" + award).execute();
         }
@@ -164,13 +164,13 @@
                                         new Hash(note.get("position").get("head_sha").asString()),
                                         note.get("position").get("new_path").asString(),
                                         note.get("position").get("new_line").asInt(),
                                         note.get("id").toString(),
                                         note.get("body").asString(),
-                                        new HostUserDetails(note.get("author").get("id").asInt(),
-                                                            note.get("author").get("username").asString(),
-                                                            note.get("author").get("name").asString()),
+                                        new HostUser(note.get("author").get("id").asInt(),
+                                                     note.get("author").get("username").asString(),
+                                                     note.get("author").get("name").asString()),
                                         ZonedDateTime.parse(note.get("created_at").asString()),
                                         ZonedDateTime.parse(note.get("updated_at").asString()));
         return comment;
     }
 
@@ -225,49 +225,49 @@
 
         return ret;
     }
 
     @Override
-    public List<ReviewComment> getReviewComments() {
+    public List<ReviewComment> reviewComments() {
         return request.get("discussions").execute().stream()
                       .filter(entry -> !entry.get("individual_note").asBoolean())
                       .flatMap(entry -> parseDiscussion(entry.asObject()).stream())
                       .collect(Collectors.toList());
     }
 
     @Override
-    public Hash getHeadHash() {
+    public Hash headHash() {
         return new Hash(json.get("sha").asString());
     }
 
     @Override
-    public String getSourceRef() {
-        return "merge-requests/" + getId() + "/head";
+    public String sourceRef() {
+        return "merge-requests/" + id() + "/head";
     }
 
     @Override
-    public String getTargetRef() {
+    public String targetRef() {
         return json.get("target_branch").asString();
     }
 
     @Override
-    public Hash getTargetHash() {
-        return repository.getBranchHash(getTargetRef());
+    public Hash targetHash() {
+        return repository.branchHash(targetRef());
     }
 
     @Override
-    public String getTitle() {
+    public String title() {
         return json.get("title").asString();
     }
 
     @Override
     public void setTitle(String title) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public String getBody() {
+    public String body() {
         var body = json.get("description").asString();
         if (body == null) {
             body = "";
         }
         return body;
@@ -281,20 +281,20 @@
     }
 
     private Comment parseComment(JSONValue comment) {
         var ret = new Comment(comment.get("id").toString(),
                               comment.get("body").asString(),
-                              new HostUserDetails(comment.get("author").get("id").asInt(),
-                                                  comment.get("author").get("username").asString(),
-                                                  comment.get("author").get("name").asString()),
+                              new HostUser(comment.get("author").get("id").asInt(),
+                                           comment.get("author").get("username").asString(),
+                                           comment.get("author").get("name").asString()),
                               ZonedDateTime.parse(comment.get("created_at").asString()),
                               ZonedDateTime.parse(comment.get("updated_at").asString()));
         return ret;
     }
 
     @Override
-    public List<Comment> getComments() {
+    public List<Comment> comments() {
         return request.get("notes").param("sort", "asc").execute().stream()
                       .filter(entry -> !entry.contains("position")) // Ignore comments with a position - they are review comments
                       .filter(entry -> !entry.get("system").asBoolean()) // Ignore system generated comments
                 .map(this::parseComment)
                 .collect(Collectors.toList());
@@ -321,43 +321,43 @@
         log.fine("Id of updated comment: " + parsedComment.id());
         return parsedComment;
     }
 
     @Override
-    public ZonedDateTime getCreated() {
+    public ZonedDateTime createdAt() {
         return ZonedDateTime.parse(json.get("created_at").asString());
     }
 
     @Override
-    public ZonedDateTime getUpdated() {
+    public ZonedDateTime updatedAt() {
         return ZonedDateTime.parse(json.get("updated_at").asString());
     }
 
     private final String checkMarker = "<!-- Merge request status check message (%s) -->";
     private final String checkResultMarker = "<!-- Merge request status check result (%s) (%s) (%s) (%s) -->";
     private final String checkResultPattern = "<!-- Merge request status check result \\(([-\\w]+)\\) \\((\\w+)\\) \\(%s\\) \\((\\S+)\\) -->";
 
     private Optional<Comment> getStatusCheckComment(String name) {
         var marker = String.format(checkMarker, name);
 
-        return getComments().stream()
-                .filter(c -> c.body().contains(marker))
-                .findFirst();
+        return comments().stream()
+                         .filter(c -> c.body().contains(marker))
+                         .findFirst();
     }
 
     private String encodeMarkdown(String message) {
         return message.replaceAll("\n", "  \n");
     }
 
     private final Pattern checkBodyPattern = Pattern.compile("^##### ([^\\n\\r]*)\\R(.*)",
                                                              Pattern.DOTALL | Pattern.MULTILINE);
 
     @Override
-    public Map<String, Check> getChecks(Hash hash) {
+    public Map<String, Check> checks(Hash hash) {
         var pattern = Pattern.compile(String.format(checkResultPattern, hash.hex()));
-        var matchers = getComments().stream()
-                .collect(Collectors.toMap(comment -> comment,
+        var matchers = comments().stream()
+                                 .collect(Collectors.toMap(comment -> comment,
                         comment -> pattern.matcher(comment.body())));
 
         return matchers.entrySet().stream()
                 .filter(entry -> entry.getValue().find())
                 .collect(Collectors.toMap(entry -> entry.getValue().group(1),
@@ -418,12 +418,12 @@
         }
         return "NONE";
     }
 
     private String linkToDiff(String path, Hash hash, int line) {
-        return "[" + path + " line " + line + "](" + URIBuilder.base(repository.getUrl())
-                         .setPath("/" + repository.getName()+ "/blob/" + hash.hex() + "/" + path)
+        return "[" + path + " line " + line + "](" + URIBuilder.base(repository.url())
+                         .setPath("/" + repository.name()+ "/blob/" + hash.hex() + "/" + path)
                          .setAuthentication(null)
                          .build() + "#L" + Integer.toString(line) + ")";
     }
 
     private String bodyFor(Check check) {
@@ -505,17 +505,17 @@
                 .orElseGet(() -> addComment("Progress deleted?"));
         updateCheckComment(Optional.of(previous), check);
     }
 
     @Override
-    public URI getChangeUrl() {
-        return URIBuilder.base(getWebUrl()).appendPath("/diffs").build();
+    public URI changeUrl() {
+        return URIBuilder.base(webUrl()).appendPath("/diffs").build();
     }
 
     @Override
-    public URI getChangeUrl(Hash base) {
-        return URIBuilder.base(getWebUrl()).appendPath("/diffs")
+    public URI changeUrl(Hash base) {
+        return URIBuilder.base(webUrl()).appendPath("/diffs")
                          .setQuery(Map.of("start_sha", base.hex()))
                          .build();
     }
 
     @Override
@@ -556,49 +556,49 @@
                .body("labels", String.join(",", labels))
                .execute();
     }
 
     @Override
-    public List<String> getLabels() {
+    public List<String> labels() {
         var currentJson = request.get("").execute().asObject();
         return currentJson.get("labels").stream()
                 .map(JSONValue::asString)
                 .sorted()
                 .collect(Collectors.toList());
     }
 
     @Override
-    public URI getWebUrl() {
-        return URIBuilder.base(repository.getWebUrl())
-                         .setPath("/" + repository.getName() + "/merge_requests/" + getId())
+    public URI webUrl() {
+        return URIBuilder.base(repository.webUrl())
+                         .setPath("/" + repository.name() + "/merge_requests/" + id())
                          .build();
     }
 
     @Override
     public String toString() {
-        return "GitLabMergeRequest #" + getId() + " by " + getAuthor();
+        return "GitLabMergeRequest #" + id() + " by " + author();
     }
 
     @Override
-    public List<HostUserDetails> getAssignees() {
+    public List<HostUser> assignees() {
         var assignee = json.get("assignee").asObject();
         if (assignee != null) {
-            var user = repository.host().getUserDetails(assignee.get("username").asString());
+            var user = repository.host().user(assignee.get("username").asString());
             return List.of(user);
         }
         return Collections.emptyList();
     }
 
     @Override
-    public void setAssignees(List<HostUserDetails> assignees) {
+    public void setAssignees(List<HostUser> assignees) {
         var id = assignees.size() == 0 ? 0 : Integer.valueOf(assignees.get(0).id());
         var param = JSON.object().put("assignee_id", id);
         request.put().body(param).execute();
         if (assignees.size() > 1) {
             var rest = assignees.subList(1, assignees.size());
             var usernames = rest.stream()
-                                .map(HostUserDetails::userName)
+                                .map(HostUser::userName)
                                 .map(username -> "@" + username)
                                 .collect(Collectors.joining(" "));
             var comment = usernames + " can you have a look at this merge request?";
             addComment(comment);
         }
diff a/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabRepository.java b/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabRepository.java
--- a/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabRepository.java
+++ b/host/src/main/java/org/openjdk/skara/host/gitlab/GitLabRepository.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.host.gitlab;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.*;
 import org.openjdk.skara.vcs.*;
 
 import java.net.*;
 import java.nio.charset.StandardCharsets;
@@ -59,31 +59,31 @@
                                    .setPath("/" + projectName + "/merge_requests/").build();
         mergeRequestPattern = Pattern.compile(urlPattern.toString() + "(\\d+)");
     }
 
     @Override
-    public Host host() {
+    public RepositoryHost host() {
         return gitLabHost;
     }
 
     @Override
     public Issue createIssue(String title, List<String> body) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public Optional<Issue> getIssue(String id) {
+    public Optional<Issue> issue(String id) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public List<Issue> getIssues() {
+    public List<Issue> issues() {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public Optional<HostedRepository> getParent() {
+    public Optional<HostedRepository> parent() {
         if (json.contains("forked_from_project")) {
             var parent = json.get("forked_from_project").get("path_with_namespace").asString();
             return Optional.of(new GitLabRepository(gitLabHost, parent));
         }
         return Optional.empty();
@@ -103,25 +103,25 @@
         var pr = request.post("merge_requests")
                         .body("source_branch", sourceRef)
                         .body("target_branch", targetRef)
                         .body("title", draft ? "WIP: " : "" + title)
                         .body("description", String.join("\n", body))
-                        .body("target_project_id", Long.toString(target.getId()))
+                        .body("target_project_id", Long.toString(target.id()))
                         .execute();
 
         var targetRepo = (GitLabRepository) target;
         return new GitLabMergeRequest(targetRepo, pr, targetRepo.request);
     }
 
     @Override
-    public PullRequest getPullRequest(String id) {
+    public PullRequest pullRequest(String id) {
         var pr = request.get("merge_requests/" + id).execute();
         return new GitLabMergeRequest(this, pr, request);
     }
 
     @Override
-    public List<PullRequest> getPullRequests() {
+    public List<PullRequest> pullRequests() {
         return request.get("merge_requests")
                       .param("state", "opened")
                       .execute().stream()
                       .map(value -> new GitLabMergeRequest(this, value, request))
                       .collect(Collectors.toList());
@@ -134,51 +134,51 @@
 
     @Override
     public Optional<PullRequest> parsePullRequestUrl(String url) {
         var matcher = mergeRequestPattern.matcher(url);
         if (matcher.find()) {
-            return Optional.of(getPullRequest(matcher.group(1)));
+            return Optional.of(pullRequest(matcher.group(1)));
         } else {
             return Optional.empty();
         }
     }
 
     @Override
-    public String getName() {
+    public String name() {
         return projectName;
     }
 
     @Override
-    public URI getUrl() {
+    public URI url() {
         var builder = URIBuilder
                 .base(gitLabHost.getUri())
                 .setPath("/" + projectName + ".git");
         gitLabHost.getPat().ifPresent(pat -> builder.setAuthentication(pat.userName() + ":" + pat.token()));
         return builder.build();
     }
 
     @Override
-    public URI getWebUrl() {
+    public URI webUrl() {
         return URIBuilder.base(gitLabHost.getUri())
                          .setPath("/" + projectName)
                          .build();
     }
 
     @Override
-    public URI getWebUrl(Hash hash) {
+    public URI webUrl(Hash hash) {
         return URIBuilder.base(gitLabHost.getUri())
                          .setPath("/" + projectName + "/commit/" + hash.abbreviate())
                          .build();
     }
 
     @Override
-    public VCS getRepositoryType() {
+    public VCS repositoryType() {
         return VCS.GIT;
     }
 
     @Override
-    public String getFileContents(String filename, String ref) {
+    public String fileContents(String filename, String ref) {
         var confName = URLEncoder.encode(filename, StandardCharsets.UTF_8);
         var conf = request.get("repository/files/" + confName)
                           .param("ref", ref)
                           .onError(response -> {
                               // Retry once with additional escaping of the path fragment
@@ -190,11 +190,11 @@
         var content = Base64.getDecoder().decode(conf.get("content").asString());
         return new String(content, StandardCharsets.UTF_8);
     }
 
     @Override
-    public String getNamespace() {
+    public String namespace() {
         return URIBuilder.base(gitLabHost.getUri()).build().getHost();
     }
 
     @Override
     public Optional<WebHook> parseWebHook(JSONValue body) {
@@ -223,21 +223,21 @@
             }
             id = body.get("merge_request").get("iid").asInt();
         }
 
         if (id != -1) {
-            var pr = getPullRequest(Integer.toString(id));
+            var pr = pullRequest(Integer.toString(id));
             var webHook = new WebHook(List.of(pr));
             return Optional.of(webHook);
         } else {
             return Optional.empty();
         }
     }
 
     @Override
     public HostedRepository fork() {
-        var namespace = gitLabHost.getCurrentUserDetails().userName();
+        var namespace = gitLabHost.currentUser().userName();
         request.post("fork")
                .body("namespace", namespace)
                .onError(r -> r.statusCode() == 409 ? JSON.object().put("exists", true) : null)
                .execute();
         var nameOnlyStart = projectName.lastIndexOf('/');
@@ -248,19 +248,19 @@
                 Thread.sleep(Duration.ofSeconds(1).toMillis());
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
-        return gitLabHost.getRepository(forkedRepoName);
+        return gitLabHost.repository(forkedRepoName);
     }
 
     @Override
-    public long getId() {
+    public long id() {
         return json.get("id").asLong();
     }
 
     @Override
-    public Hash getBranchHash(String ref) {
+    public Hash branchHash(String ref) {
         var branch = request.get("repository/branches/" + ref).execute();
         return new Hash(branch.get("commit").get("id").asString());
     }
 }
diff a/host/src/main/java/org/openjdk/skara/host/jira/JiraHost.java b/host/src/main/java/org/openjdk/skara/host/jira/JiraHost.java
--- a/host/src/main/java/org/openjdk/skara/host/jira/JiraHost.java
+++ b/host/src/main/java/org/openjdk/skara/host/jira/JiraHost.java
@@ -21,16 +21,16 @@
  * questions.
  */
 package org.openjdk.skara.host.jira;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.JSON;
 
 import java.net.URI;
 
-public class JiraHost implements Host {
+public class JiraHost implements IssueHost {
     private final URI uri;
     private final RestRequest request;
 
     public JiraHost(URI uri) {
         this.uri = uri;
@@ -52,34 +52,24 @@
                              .execute();
         return !version.contains("invalid");
     }
 
     @Override
-    public HostedRepository getRepository(String name) {
-        throw new RuntimeException("Jira does not support repositories");
-    }
-
-    @Override
-    public IssueProject getIssueProject(String name) {
+    public IssueProject project(String name) {
         return new JiraProject(this, request, name);
     }
 
     @Override
-    public HostUserDetails getUserDetails(String username) {
+    public HostUser user(String username) {
         throw new RuntimeException("needs authentication; not implemented yet");
     }
 
     @Override
-    public HostUserDetails getCurrentUserDetails() {
+    public HostUser currentUser() {
         throw new RuntimeException("needs authentication; not implemented yet");
     }
 
     @Override
-    public boolean supportsReviewBody() {
-        return false;
-    }
-
-    @Override
-    public boolean isMemberOf(String groupId, HostUserDetails user) {
+    public boolean isMemberOf(String groupId, HostUser user) {
         throw new RuntimeException("not implemented yet");
     }
 }
diff a/host/src/main/java/org/openjdk/skara/host/jira/JiraIssue.java b/host/src/main/java/org/openjdk/skara/host/jira/JiraIssue.java
--- a/host/src/main/java/org/openjdk/skara/host/jira/JiraIssue.java
+++ b/host/src/main/java/org/openjdk/skara/host/jira/JiraIssue.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.host.jira;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.JSONValue;
 
 import java.net.URI;
 import java.time.ZonedDateTime;
 import java.util.List;
@@ -45,33 +45,33 @@
     public IssueProject project() {
         return jiraProject;
     }
 
     @Override
-    public String getId() {
+    public String id() {
         return json.get("key").asString();
     }
 
     @Override
-    public HostUserDetails getAuthor() {
-        return new HostUserDetails(json.get("fields").get("creator").get("key").asString(),
-                                   json.get("fields").get("creator").get("name").asString(),
-                                   json.get("fields").get("creator").get("displayName").asString());
+    public HostUser author() {
+        return new HostUser(json.get("fields").get("creator").get("key").asString(),
+                            json.get("fields").get("creator").get("name").asString(),
+                            json.get("fields").get("creator").get("displayName").asString());
     }
 
     @Override
-    public String getTitle() {
+    public String title() {
         return json.get("fields").get("summary").asString();
     }
 
     @Override
     public void setTitle(String title) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public String getBody() {
+    public String body() {
         if (json.get("fields").get("description").isNull()) {
             return "";
         } else {
             return json.get("fields").get("description").asString();
         }
@@ -81,11 +81,11 @@
     public void setBody(String body) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public List<Comment> getComments() {
+    public List<Comment> comments() {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
     public Comment addComment(String body) {
@@ -96,16 +96,16 @@
     public Comment updateComment(String id, String body) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public ZonedDateTime getCreated() {
+    public ZonedDateTime createdAt() {
         return ZonedDateTime.parse(json.get("fields").get("created").asString());
     }
 
     @Override
-    public ZonedDateTime getUpdated() {
+    public ZonedDateTime updatedAt() {
         return ZonedDateTime.parse(json.get("fields").get("updated").asString());
     }
 
     @Override
     public void setState(State state) {
@@ -121,26 +121,26 @@
     public void removeLabel(String label) {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public List<String> getLabels() {
+    public List<String> labels() {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public URI getWebUrl() {
-        return URIBuilder.base(jiraProject.getWebUrl())
-                         .setPath("/browse/" + getId())
+    public URI webUrl() {
+        return URIBuilder.base(jiraProject.webUrl())
+                         .setPath("/browse/" + id())
                          .build();
     }
 
     @Override
-    public List<HostUserDetails> getAssignees() {
+    public List<HostUser> assignees() {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public void setAssignees(List<HostUserDetails> assignees) {
+    public void setAssignees(List<HostUser> assignees) {
         throw new RuntimeException("not implemented yet");
     }
 }
diff a/host/src/main/java/org/openjdk/skara/host/jira/JiraProject.java b/host/src/main/java/org/openjdk/skara/host/jira/JiraProject.java
--- a/host/src/main/java/org/openjdk/skara/host/jira/JiraProject.java
+++ b/host/src/main/java/org/openjdk/skara/host/jira/JiraProject.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.host.jira;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.*;
+import org.openjdk.skara.network.*;
 import org.openjdk.skara.json.JSON;
 
 import java.net.URI;
 import java.util.*;
 
@@ -44,21 +44,21 @@
     public Host host() {
         return jiraHost;
     }
 
     @Override
-    public URI getWebUrl() {
+    public URI webUrl() {
         return URIBuilder.base(jiraHost.getUri()).setPath("/projects/" + projectName).build();
     }
 
     @Override
     public Issue createIssue(String title, List<String> body) {
         throw new RuntimeException("needs authentication; not implemented yet");
     }
 
     @Override
-    public Optional<Issue> getIssue(String id) {
+    public Optional<Issue> issue(String id) {
         if (id.indexOf('-') < 0) {
             id = projectName.toUpperCase() + "-" + id;
         }
         var issue = request.get("issue/" + id)
                            .onError(r -> r.statusCode() == 404 ? JSON.object().put("NOT_FOUND", true) : null)
@@ -69,11 +69,11 @@
             return Optional.empty();
         }
     }
 
     @Override
-    public List<Issue> getIssues() {
+    public List<Issue> issues() {
         var ret = new ArrayList<Issue>();
         var issues = request.post("search")
                             .body("jql", "project = " + projectName + " AND status in (Open, New)")
                             .execute();
         for (var issue : issues.get("issues").asArray()) {
diff a/host/src/test/java/org/openjdk/skara/host/HostTests.java b/host/src/test/java/org/openjdk/skara/host/HostTests.java
--- a/host/src/test/java/org/openjdk/skara/host/HostTests.java
+++ b/host/src/test/java/org/openjdk/skara/host/HostTests.java
@@ -34,9 +34,9 @@
     @Test
     public void isMemberOfNegativeTests(TestInfo info) throws IOException {
         try (var credentials = new HostCredentials(info)) {
             var host = credentials.getHostedRepository().host();
             var madeUpGroupIdThatCannotContainTestMember = "1234567890";
-            assertFalse(host.isMemberOf(madeUpGroupIdThatCannotContainTestMember, host.getCurrentUserDetails()));
+            assertFalse(host.isMemberOf(madeUpGroupIdThatCannotContainTestMember, host.currentUser()));
         }
     }
 }
diff a/host/src/test/java/org/openjdk/skara/host/github/GitHubHostTests.java b/host/src/test/java/org/openjdk/skara/host/github/GitHubHostTests.java
--- a/host/src/test/java/org/openjdk/skara/host/github/GitHubHostTests.java
+++ b/host/src/test/java/org/openjdk/skara/host/github/GitHubHostTests.java
@@ -20,11 +20,11 @@
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 package org.openjdk.skara.host.github;
 
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.test.TemporaryDirectory;
 
 import org.junit.jupiter.api.Test;
 
 import java.io.IOException;
diff a/host/src/test/java/org/openjdk/skara/host/network/RestRequestTests.java b/host/src/test/java/org/openjdk/skara/host/network/RestRequestTests.java
--- a/host/src/test/java/org/openjdk/skara/host/network/RestRequestTests.java
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code 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
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package org.openjdk.skara.host.network;
-
-import com.sun.net.httpserver.*;
-import org.openjdk.skara.json.*;
-
-import org.junit.jupiter.api.Test;
-
-import java.io.*;
-import java.net.*;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.util.*;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-class RestReceiver implements AutoCloseable {
-    private final HttpServer server;
-    private final List<JSONObject> requests;
-    private final String response;
-    private int responseCode;
-
-    private int truncatedResponseCount = 0;
-
-    class Handler implements HttpHandler {
-
-        @Override
-        public void handle(HttpExchange exchange) throws IOException {
-            var input = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
-            requests.add(JSON.parse(input).asObject());
-
-            exchange.sendResponseHeaders(responseCode, response.length());
-            OutputStream outputStream = exchange.getResponseBody();
-            if (truncatedResponseCount > 0) {
-                truncatedResponseCount--;
-            } else {
-                outputStream.write(response.getBytes());
-            }
-            outputStream.close();
-        }
-    }
-
-    RestReceiver() throws IOException {
-        this("{}", 200);
-    }
-
-    RestReceiver(String response, int responseCode) throws IOException
-    {
-        this.response = response;
-        this.responseCode = responseCode;
-        requests = new ArrayList<>();
-        InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
-        server = HttpServer.create(address, 0);
-        server.createContext("/test", new Handler());
-        server.setExecutor(null);
-        server.start();
-    }
-
-    URI getEndpoint() {
-        return URIBuilder.base("http://" + server.getAddress().getHostString() + ":" +  server.getAddress().getPort() + "/test").build();
-    }
-
-    List<JSONObject> getRequests() {
-        return requests;
-    }
-
-    void setTruncatedResponseCount(int count) {
-        truncatedResponseCount = count;
-    }
-
-    @Override
-    public void close() {
-        server.stop(0);
-    }
-}
-
-class RestRequestTests {
-    @Test
-    void simpleRequest() throws IOException {
-        try (var receiver = new RestReceiver()) {
-            var request = new RestRequest(receiver.getEndpoint());
-            request.post("/test").execute();
-        }
-    }
-
-    @Test
-    void retryOnTransientErrors() throws IOException {
-        try (var receiver = new RestReceiver()) {
-            receiver.setTruncatedResponseCount(1);
-
-            var request = new RestRequest(receiver.getEndpoint());
-            request.setRetryBackoffStep(Duration.ofMillis(1));
-            request.post("/test").execute();
-        }
-    }
-
-    @Test
-    void failOnNonTransientErrors() throws IOException {
-        try (var receiver = new RestReceiver()) {
-            receiver.setTruncatedResponseCount(6);
-
-            var request = new RestRequest(receiver.getEndpoint());
-            request.setRetryBackoffStep(Duration.ofMillis(1));
-            assertThrows(RuntimeException.class, () -> request.post("/test").execute());
-        }
-    }
-
-    @Test
-    void transformError() throws IOException {
-        try (var receiver = new RestReceiver("{}", 400)) {
-            var request = new RestRequest(receiver.getEndpoint());
-            var response = request.post("/test")
-                   .onError(r -> JSON.object().put("transformed", true))
-                   .execute();
-            assertTrue(response.contains("transformed"));
-        }
-    }
-
-    @Test
-    void parseError() throws IOException {
-        try (var receiver = new RestReceiver("{{bad_json", 200)) {
-            var request = new RestRequest(receiver.getEndpoint());
-            assertThrows(RuntimeException.class, () -> request.post("/test").execute());
-        }
-    }
-
-    @Test
-    void unparsed() throws IOException {
-        try (var receiver = new RestReceiver("{{bad", 200)) {
-            var request = new RestRequest(receiver.getEndpoint());
-            var response = request.post("/test").executeUnparsed();
-            assertEquals("{{bad", response);
-        }
-    }
-}
diff a/host/src/test/java/org/openjdk/skara/host/network/URIBuilderTests.java b/host/src/test/java/org/openjdk/skara/host/network/URIBuilderTests.java
--- a/host/src/test/java/org/openjdk/skara/host/network/URIBuilderTests.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code 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
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package org.openjdk.skara.host.network;
-
-import org.junit.jupiter.api.Test;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-class URIBuilderTests {
-    private final String validHost = "http://www.test.com";
-
-    @Test
-    void setPathSimple() {
-        var a = URIBuilder.base(validHost).setPath("/a").build();
-        var b = URIBuilder.base(validHost).setPath("/b").build();
-        var aToB = URIBuilder.base(a).setPath("/b").build();
-
-        assertEquals("www.test.com", a.getHost());
-        assertEquals("/a", a.getPath());
-        assertEquals("/b", b.getPath());
-        assertEquals("/b", aToB.getPath());
-    }
-
-    @Test
-    void appendPathSimple() {
-        var a = URIBuilder.base(validHost).setPath("/a").build();
-        var aPlusB = URIBuilder.base(a).appendPath("/b").build();
-
-        assertEquals("/a", a.getPath());
-        assertEquals("/a/b", aPlusB.getPath());
-    }
-
-    @Test
-    void invalidBase() {
-        assertThrows(URIBuilderException.class,
-                () -> URIBuilder.base("x:\\y").build());
-    }
-
-    @Test
-    void invalidSetPath() {
-        assertThrows(URIBuilderException.class,
-                () -> URIBuilder.base(validHost).setPath("\\c").build());
-    }
-
-    @Test
-    void invalidAppendPath() {
-        assertThrows(URIBuilderException.class,
-                () -> URIBuilder.base(validHost).appendPath("\\c").build());
-    }
-
-    @Test
-    void noHost() {
-        var a = URIBuilder.base("file:///a/b/c").build();
-        assertEquals("/a/b/c", a.getPath());
-    }
-}
diff a/mailinglist/build.gradle b/mailinglist/build.gradle
--- a/mailinglist/build.gradle
+++ b/mailinglist/build.gradle
@@ -29,11 +29,11 @@
         opens 'org.openjdk.skara.mailinglist' to 'org.junit.platform.commons'
     }
 }
 
 dependencies {
-    implementation project(':host')
+    implementation project(':network')
     implementation project(':vcs')
     implementation project(':email')
 
     testImplementation project(':test')
 }
diff a/mailinglist/src/main/java/module-info.java b/mailinglist/src/main/java/module-info.java
--- a/mailinglist/src/main/java/module-info.java
+++ b/mailinglist/src/main/java/module-info.java
@@ -21,11 +21,11 @@
  * questions.
  */
 module org.openjdk.skara.mailinglist {
     requires java.net.http;
     requires java.logging;
-    requires org.openjdk.skara.host;
+    requires org.openjdk.skara.network;
     requires org.openjdk.skara.vcs;
     requires org.openjdk.skara.email;
 
     exports org.openjdk.skara.mailinglist;
 }
diff a/mailinglist/src/main/java/org/openjdk/skara/mailinglist/mailman/MailmanServer.java b/mailinglist/src/main/java/org/openjdk/skara/mailinglist/mailman/MailmanServer.java
--- a/mailinglist/src/main/java/org/openjdk/skara/mailinglist/mailman/MailmanServer.java
+++ b/mailinglist/src/main/java/org/openjdk/skara/mailinglist/mailman/MailmanServer.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.mailinglist.mailman;
 
 import org.openjdk.skara.email.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.mailinglist.*;
 
 import java.io.*;
 import java.net.URI;
 import java.time.*;
diff a/network/build.gradle b/network/build.gradle
--- /dev/null
+++ b/network/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+module {
+    name = 'org.openjdk.skara.network'
+    test {
+        requires 'org.openjdk.skara.test'
+        requires 'org.junit.jupiter.api'
+        requires 'jdk.httpserver'
+        opens 'org.openjdk.skara.network' to 'org.junit.platform.commons'
+    }
+}
+
+dependencies {
+    implementation project(':json')
+    testImplementation project(':test')
+}
+
+publishing {
+    publications {
+        network(MavenPublication) {
+            from components.java
+        }
+    }
+}
diff a/network/src/main/java/module-info.java b/network/src/main/java/module-info.java
--- /dev/null
+++ b/network/src/main/java/module-info.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+module org.openjdk.skara.network {
+    requires java.logging;
+    requires org.openjdk.skara.json;
+    requires java.net.http;
+
+    exports org.openjdk.skara.network;
+}
diff a/host/src/main/java/org/openjdk/skara/host/network/RestRequest.java b/network/src/main/java/org/openjdk/skara/network/RestRequest.java
--- a/host/src/main/java/org/openjdk/skara/host/network/RestRequest.java
+++ b/network/src/main/java/org/openjdk/skara/network/RestRequest.java
@@ -18,11 +18,11 @@
  *
  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
-package org.openjdk.skara.host.network;
+package org.openjdk.skara.network;
 
 import org.openjdk.skara.json.*;
 
 import java.io.IOException;
 import java.net.URI;
diff a/host/src/main/java/org/openjdk/skara/host/network/URIBuilder.java b/network/src/main/java/org/openjdk/skara/network/URIBuilder.java
--- a/host/src/main/java/org/openjdk/skara/host/network/URIBuilder.java
+++ b/network/src/main/java/org/openjdk/skara/network/URIBuilder.java
@@ -18,11 +18,11 @@
  *
  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
-package org.openjdk.skara.host.network;
+package org.openjdk.skara.network;
 
 import java.io.UnsupportedEncodingException;
 import java.net.*;
 import java.util.Map;
 import java.util.stream.Collectors;
diff a/network/src/test/java/org/openjdk/skara/network/RestRequestTests.java b/network/src/test/java/org/openjdk/skara/network/RestRequestTests.java
--- /dev/null
+++ b/network/src/test/java/org/openjdk/skara/network/RestRequestTests.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package org.openjdk.skara.network;
+
+import com.sun.net.httpserver.*;
+import org.openjdk.skara.json.*;
+import org.openjdk.skara.network.*;
+
+import org.junit.jupiter.api.Test;
+
+import java.io.*;
+import java.net.*;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.*;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class RestReceiver implements AutoCloseable {
+    private final HttpServer server;
+    private final List<JSONObject> requests;
+    private final String response;
+    private int responseCode;
+
+    private int truncatedResponseCount = 0;
+
+    class Handler implements HttpHandler {
+
+        @Override
+        public void handle(HttpExchange exchange) throws IOException {
+            var input = new String(exchange.getRequestBody().readAllBytes(), StandardCharsets.UTF_8);
+            requests.add(JSON.parse(input).asObject());
+
+            exchange.sendResponseHeaders(responseCode, response.length());
+            OutputStream outputStream = exchange.getResponseBody();
+            if (truncatedResponseCount > 0) {
+                truncatedResponseCount--;
+            } else {
+                outputStream.write(response.getBytes());
+            }
+            outputStream.close();
+        }
+    }
+
+    RestReceiver() throws IOException {
+        this("{}", 200);
+    }
+
+    RestReceiver(String response, int responseCode) throws IOException
+    {
+        this.response = response;
+        this.responseCode = responseCode;
+        requests = new ArrayList<>();
+        InetSocketAddress address = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0);
+        server = HttpServer.create(address, 0);
+        server.createContext("/test", new Handler());
+        server.setExecutor(null);
+        server.start();
+    }
+
+    URI getEndpoint() {
+        return URIBuilder.base("http://" + server.getAddress().getHostString() + ":" +  server.getAddress().getPort() + "/test").build();
+    }
+
+    List<JSONObject> getRequests() {
+        return requests;
+    }
+
+    void setTruncatedResponseCount(int count) {
+        truncatedResponseCount = count;
+    }
+
+    @Override
+    public void close() {
+        server.stop(0);
+    }
+}
+
+class RestRequestTests {
+    @Test
+    void simpleRequest() throws IOException {
+        try (var receiver = new RestReceiver()) {
+            var request = new RestRequest(receiver.getEndpoint());
+            request.post("/test").execute();
+        }
+    }
+
+    @Test
+    void retryOnTransientErrors() throws IOException {
+        try (var receiver = new RestReceiver()) {
+            receiver.setTruncatedResponseCount(1);
+
+            var request = new RestRequest(receiver.getEndpoint());
+            request.setRetryBackoffStep(Duration.ofMillis(1));
+            request.post("/test").execute();
+        }
+    }
+
+    @Test
+    void failOnNonTransientErrors() throws IOException {
+        try (var receiver = new RestReceiver()) {
+            receiver.setTruncatedResponseCount(6);
+
+            var request = new RestRequest(receiver.getEndpoint());
+            request.setRetryBackoffStep(Duration.ofMillis(1));
+            assertThrows(RuntimeException.class, () -> request.post("/test").execute());
+        }
+    }
+
+    @Test
+    void transformError() throws IOException {
+        try (var receiver = new RestReceiver("{}", 400)) {
+            var request = new RestRequest(receiver.getEndpoint());
+            var response = request.post("/test")
+                   .onError(r -> JSON.object().put("transformed", true))
+                   .execute();
+            assertTrue(response.contains("transformed"));
+        }
+    }
+
+    @Test
+    void parseError() throws IOException {
+        try (var receiver = new RestReceiver("{{bad_json", 200)) {
+            var request = new RestRequest(receiver.getEndpoint());
+            assertThrows(RuntimeException.class, () -> request.post("/test").execute());
+        }
+    }
+
+    @Test
+    void unparsed() throws IOException {
+        try (var receiver = new RestReceiver("{{bad", 200)) {
+            var request = new RestRequest(receiver.getEndpoint());
+            var response = request.post("/test").executeUnparsed();
+            assertEquals("{{bad", response);
+        }
+    }
+}
diff a/network/src/test/java/org/openjdk/skara/network/URIBuilderTests.java b/network/src/test/java/org/openjdk/skara/network/URIBuilderTests.java
--- /dev/null
+++ b/network/src/test/java/org/openjdk/skara/network/URIBuilderTests.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2018, 2019, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code 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
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package org.openjdk.skara.network;
+
+import org.openjdk.skara.network.*;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class URIBuilderTests {
+    private final String validHost = "http://www.test.com";
+
+    @Test
+    void setPathSimple() {
+        var a = URIBuilder.base(validHost).setPath("/a").build();
+        var b = URIBuilder.base(validHost).setPath("/b").build();
+        var aToB = URIBuilder.base(a).setPath("/b").build();
+
+        assertEquals("www.test.com", a.getHost());
+        assertEquals("/a", a.getPath());
+        assertEquals("/b", b.getPath());
+        assertEquals("/b", aToB.getPath());
+    }
+
+    @Test
+    void appendPathSimple() {
+        var a = URIBuilder.base(validHost).setPath("/a").build();
+        var aPlusB = URIBuilder.base(a).appendPath("/b").build();
+
+        assertEquals("/a", a.getPath());
+        assertEquals("/a/b", aPlusB.getPath());
+    }
+
+    @Test
+    void invalidBase() {
+        assertThrows(URIBuilderException.class,
+                     () -> URIBuilder.base("x:\\y").build());
+    }
+
+    @Test
+    void invalidSetPath() {
+        assertThrows(URIBuilderException.class,
+                () -> URIBuilder.base(validHost).setPath("\\c").build());
+    }
+
+    @Test
+    void invalidAppendPath() {
+        assertThrows(URIBuilderException.class,
+                () -> URIBuilder.base(validHost).appendPath("\\c").build());
+    }
+
+    @Test
+    void noHost() {
+        var a = URIBuilder.base("file:///a/b/c").build();
+        assertEquals("/a/b/c", a.getPath());
+    }
+}
diff a/settings.gradle b/settings.gradle
--- a/settings.gradle
+++ b/settings.gradle
@@ -38,10 +38,11 @@
 include 'storage'
 include 'ssh'
 include 'test'
 include 'vcs'
 include 'webrev'
+include 'network'
 
 include 'bots:cli'
 include 'bots:forward'
 include 'bots:hgbridge'
 include 'bots:merge'
diff a/storage/build.gradle b/storage/build.gradle
--- a/storage/build.gradle
+++ b/storage/build.gradle
@@ -29,10 +29,11 @@
         opens 'org.openjdk.skara.storage' to 'org.junit.platform.commons'
     }
 }
 
 dependencies {
+    implementation project(':network')
     implementation project(':host')
     implementation project(':vcs')
 }
 
 publishing {
diff a/storage/src/main/java/module-info.java b/storage/src/main/java/module-info.java
--- a/storage/src/main/java/module-info.java
+++ b/storage/src/main/java/module-info.java
@@ -19,10 +19,11 @@
  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  * or visit www.oracle.com if you need additional information or have any
  * questions.
  */
 module org.openjdk.skara.storage {
+    requires org.openjdk.skara.network;
     requires org.openjdk.skara.host;
     requires org.openjdk.skara.vcs;
 
     exports org.openjdk.skara.storage;
 }
diff a/storage/src/main/java/org/openjdk/skara/storage/HostedRepositoryStorage.java b/storage/src/main/java/org/openjdk/skara/storage/HostedRepositoryStorage.java
--- a/storage/src/main/java/org/openjdk/skara/storage/HostedRepositoryStorage.java
+++ b/storage/src/main/java/org/openjdk/skara/storage/HostedRepositoryStorage.java
@@ -55,14 +55,14 @@
         this.deserializer = deserializer;
 
         try {
             Repository localRepository;
             try {
-                localRepository = Repository.materialize(localStorage, repository.getUrl(), ref);
+                localRepository = Repository.materialize(localStorage, repository.url(), ref);
             } catch (IOException e) {
                 // The remote ref may not yet exist
-                localRepository = Repository.init(localStorage, repository.getRepositoryType());
+                localRepository = Repository.init(localStorage, repository.repositoryType());
                 var storage = Files.writeString(localStorage.resolve(fileName), "");
                 localRepository.add(storage);
                 localRepository.commit(message, authorName, authorEmail);
             }
             this.localRepository = localRepository;
@@ -94,20 +94,20 @@
             }
 
             // The local storage has changed, try to push it to the remote
             try {
                 var updatedHash = localRepository.head();
-                localRepository.push(updatedHash, hostedRepository.getUrl(), ref);
+                localRepository.push(updatedHash, hostedRepository.url(), ref);
                 hash = updatedHash;
                 current = updated;
                 return;
             } catch (IOException e) {
                 lastException = e;
 
                 // Check if the remote has changed
                 try {
-                    var remoteHash = localRepository.fetch(hostedRepository.getUrl(), ref);
+                    var remoteHash = localRepository.fetch(hostedRepository.url(), ref);
                     if (!remoteHash.equals(lastRemoteHash)) {
                         localRepository.checkout(remoteHash, true);
                         repositoryStorage = new RepositoryStorage<>(localRepository, fileName, authorName, authorEmail, message, serializer, deserializer);
                         lastRemoteHash = remoteHash;
 
diff a/test/build.gradle b/test/build.gradle
--- a/test/build.gradle
+++ b/test/build.gradle
@@ -29,10 +29,11 @@
     implementation project(':json')
     implementation project(':census')
     implementation project(':vcs')
     implementation project(':bot')
     implementation project(':host')
+    implementation project(':network')
     implementation project(':email')
     implementation project(':mailinglist')
     implementation project(':proxy')
 
     implementation 'org.junit.jupiter:junit-jupiter-api:5.3.1'
diff a/test/src/main/java/module-info.java b/test/src/main/java/module-info.java
--- a/test/src/main/java/module-info.java
+++ b/test/src/main/java/module-info.java
@@ -27,10 +27,11 @@
     requires org.openjdk.skara.census;
     requires org.openjdk.skara.vcs;
     requires org.openjdk.skara.bot;
     requires org.openjdk.skara.json;
     requires org.openjdk.skara.host;
+    requires org.openjdk.skara.network;
     requires org.openjdk.skara.email;
     requires org.openjdk.skara.mailinglist;
     requires org.openjdk.skara.proxy;
 
     requires org.junit.jupiter.api;
diff a/test/src/main/java/org/openjdk/skara/test/CensusBuilder.java b/test/src/main/java/org/openjdk/skara/test/CensusBuilder.java
--- a/test/src/main/java/org/openjdk/skara/test/CensusBuilder.java
+++ b/test/src/main/java/org/openjdk/skara/test/CensusBuilder.java
@@ -204,12 +204,12 @@
         }
     }
 
     public HostedRepository build() {
         try {
-            var host = TestHost.createNew(List.of(new HostUserDetails(1, "cu", "Census User")));
-            var repository = host.getRepository("census");
+            var host = TestHost.createNew(List.of(new HostUser(1, "cu", "Census User")));
+            var repository = host.repository("census");
             var folder = Files.createTempDirectory("censusbuilder");
             var localRepository = Repository.init(folder, VCS.GIT);
 
             log.fine("Generating census XML files in " + folder);
             generateGroup(folder);
@@ -218,11 +218,11 @@
             generateNamespace(folder);
             generateVersion(folder);
 
             localRepository.add(folder);
             var hash = localRepository.commit("Generated census", "Census User", "cu@test.test");
-            localRepository.push(hash, repository.getUrl(), "master", true);
+            localRepository.push(hash, repository.url(), "master", true);
             return repository;
 
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
diff a/test/src/main/java/org/openjdk/skara/test/HostCredentials.java b/test/src/main/java/org/openjdk/skara/test/HostCredentials.java
--- a/test/src/main/java/org/openjdk/skara/test/HostCredentials.java
+++ b/test/src/main/java/org/openjdk/skara/test/HostCredentials.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.test;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.json.*;
 import org.openjdk.skara.proxy.HttpProxy;
 import org.openjdk.skara.vcs.*;
 
 import org.junit.jupiter.api.TestInfo;
@@ -46,13 +46,14 @@
     private int nextHostIndex;
 
     private final Logger log = Logger.getLogger("org.openjdk.skara.test");
 
     private interface Credentials {
-        Host createNewHost(int userIndex);
-        HostedRepository getHostedRepository(Host host);
-        IssueProject getIssueProject(Host host);
+        RepositoryHost createRepositoryHost(int userIndex);
+        IssueHost createIssueHost(int userIndex);
+        HostedRepository getHostedRepository(RepositoryHost host);
+        IssueProject getIssueProject(IssueHost host);
         String getNamespaceName();
         default void close() {}
     }
 
     private static class GitHubCredentials implements Credentials {
@@ -63,11 +64,11 @@
             this.config = config;
             this.configDir = configDir;
         }
 
         @Override
-        public Host createNewHost(int userIndex) {
+        public RepositoryHost createRepositoryHost(int userIndex) {
             var hostUri = URIBuilder.base(config.get("host").asString()).build();
             var apps = config.get("apps").asArray();
             var key = configDir.resolve(apps.get(userIndex).get("key").asString());
             return HostFactory.createGitHubHost(hostUri,
                                                 null,
@@ -76,17 +77,22 @@
                                                 apps.get(userIndex).get("id").asString(),
                                                 apps.get(userIndex).get("installation").asString());
         }
 
         @Override
-        public HostedRepository getHostedRepository(Host host) {
-            return host.getRepository(config.get("project").asString());
+        public IssueHost createIssueHost(int userIndex) {
+            throw new RuntimeException("not implemented yet");
         }
 
         @Override
-        public IssueProject getIssueProject(Host host) {
-            return host.getIssueProject(config.get("project").asString());
+        public HostedRepository getHostedRepository(RepositoryHost host) {
+            return host.repository(config.get("project").asString());
+        }
+
+        @Override
+        public IssueProject getIssueProject(IssueHost host) {
+            return host.project(config.get("project").asString());
         }
 
         @Override
         public String getNamespaceName() {
             return config.get("namespace").asString();
@@ -99,61 +105,75 @@
         GitLabCredentials(JSONObject config) {
             this.config = config;
         }
 
         @Override
-        public Host createNewHost(int userIndex) {
+        public RepositoryHost createRepositoryHost(int userIndex) {
             var hostUri = URIBuilder.base(config.get("host").asString()).build();
             var users = config.get("users").asArray();
             var pat = new PersonalAccessToken(users.get(userIndex).get("name").asString(),
                                               users.get(userIndex).get("pat").asString());
             return HostFactory.createGitLabHost(hostUri, pat);
         }
 
         @Override
-        public HostedRepository getHostedRepository(Host host) {
-            return host.getRepository(config.get("project").asString());
+        public IssueHost createIssueHost(int userIndex) {
+            throw new RuntimeException("not implemented yet");
         }
 
         @Override
-        public IssueProject getIssueProject(Host host) {
-            return host.getIssueProject(config.get("project").asString());
+        public HostedRepository getHostedRepository(RepositoryHost host) {
+            return host.repository(config.get("project").asString());
+        }
+
+        @Override
+        public IssueProject getIssueProject(IssueHost host) {
+            return host.project(config.get("project").asString());
         }
 
         @Override
         public String getNamespaceName() {
             return config.get("namespace").asString();
         }
     }
 
     private static class TestCredentials implements Credentials {
         private final List<TestHost> hosts = new ArrayList<>();
-        private final List<HostUserDetails> users = List.of(
-                new HostUserDetails(1, "user1", "User Number 1"),
-                new HostUserDetails(2, "user2", "User Number 2"),
-                new HostUserDetails(3, "user3", "User Number 3"),
-                new HostUserDetails(4, "user4", "User Number 4")
+        private final List<HostUser> users = List.of(
+                new HostUser(1, "user1", "User Number 1"),
+                new HostUser(2, "user2", "User Number 2"),
+                new HostUser(3, "user3", "User Number 3"),
+                new HostUser(4, "user4", "User Number 4")
         );
 
-        @Override
-        public Host createNewHost(int userIndex) {
+        private TestHost createHost(int userIndex) {
             if (userIndex == 0) {
                 hosts.add(TestHost.createNew(users));
             } else {
                 hosts.add(TestHost.createFromExisting(hosts.get(0), userIndex));
             }
             return hosts.get(hosts.size() - 1);
         }
 
         @Override
-        public HostedRepository getHostedRepository(Host host) {
-            return host.getRepository("test");
+        public RepositoryHost createRepositoryHost(int userIndex) {
+            return createHost(userIndex);
         }
 
         @Override
-        public IssueProject getIssueProject(Host host) {
-            return host.getIssueProject("test");
+        public IssueHost createIssueHost(int userIndex) {
+            return createHost(userIndex);
+        }
+
+        @Override
+        public HostedRepository getHostedRepository(RepositoryHost host) {
+            return host.repository("test");
+        }
+
+        @Override
+        public IssueProject getIssueProject(IssueHost host) {
+            return host.project("test");
         }
 
         @Override
         public String getNamespaceName() {
             return "test";
@@ -178,12 +198,18 @@
             default:
                 throw new RuntimeException("Unknown entry type: " + entry.get("type").asString());
         }
     }
 
-    private Host getHost() {
-        var host = credentials.createNewHost(nextHostIndex);
+    private RepositoryHost getRepositoryHost() {
+        var host = credentials.createRepositoryHost(nextHostIndex);
+        nextHostIndex++;
+        return host;
+    }
+
+    private IssueHost getIssueHost() {
+        var host = credentials.createIssueHost(nextHostIndex);
         nextHostIndex++;
         return host;
     }
 
     public HostCredentials(TestInfo testInfo) throws IOException  {
@@ -207,11 +233,11 @@
         try (var tempFolder = new TemporaryDirectory()) {
             var repoFolder = tempFolder.path().resolve("lock");
             var lockFile = repoFolder.resolve("lock.txt");
             Repository localRepo;
             try {
-                localRepo = Repository.materialize(repoFolder, repo.getUrl(), "testlock");
+                localRepo = Repository.materialize(repoFolder, repo.url(), "testlock");
             } catch (IOException e) {
                 // If the branch does not exist, we'll try to create it
                 localRepo = Repository.init(repoFolder, VCS.GIT);
             }
 
@@ -226,11 +252,11 @@
                 }
             }
 
             // The lock either doesn't exist or is stale, try to grab it
             var lockHash = commitLock(localRepo);
-            localRepo.push(lockHash, repo.getUrl(), "testlock");
+            localRepo.push(lockHash, repo.url(), "testlock");
             log.info("Obtained credentials lock");
 
             // If no exception occurs (such as the push fails), we have obtained the lock
             return true;
         }
@@ -239,14 +265,14 @@
     private void releaseLock(HostedRepository repo) throws IOException {
         try (var tempFolder = new TemporaryDirectory()) {
             var repoFolder = tempFolder.path().resolve("lock");
             var lockFile = repoFolder.resolve("lock.txt");
             Repository localRepo;
-            localRepo = Repository.materialize(repoFolder, repo.getUrl(), "testlock");
+            localRepo = Repository.materialize(repoFolder, repo.url(), "testlock");
             localRepo.remove(lockFile);
             var lockHash = localRepo.commit("Unlock", "test", "test@test.test");
-            localRepo.push(lockHash, repo.getUrl(), "testlock");
+            localRepo.push(lockHash, repo.url(), "testlock");
         }
     }
 
     public Hash commitLock(Repository localRepo) throws IOException {
         var lockFile = localRepo.root().resolve("lock.txt");
@@ -256,11 +282,11 @@
         localRepo.branch(lockHash, "testlock");
         return lockHash;
     }
 
     public HostedRepository getHostedRepository() {
-        var host = getHost();
+        var host = getRepositoryHost();
         var repo = credentials.getHostedRepository(host);
 
         while (credentialsLock == null) {
             try {
                 if (getLock(repo)) {
@@ -275,11 +301,11 @@
         }
         return repo;
     }
 
     public IssueProject getIssueProject() {
-        var host = getHost();
+        var host = getIssueHost();
         return credentials.getIssueProject(host);
     }
 
     public PullRequest createPullRequest(HostedRepository hostedRepository, String targetRef, String sourceRef, String title, boolean draft) {
         var pr = hostedRepository.createPullRequest(hostedRepository, targetRef, sourceRef, title, List.of(), draft);
diff a/test/src/main/java/org/openjdk/skara/test/TestHost.java b/test/src/main/java/org/openjdk/skara/test/TestHost.java
--- a/test/src/main/java/org/openjdk/skara/test/TestHost.java
+++ b/test/src/main/java/org/openjdk/skara/test/TestHost.java
@@ -29,16 +29,16 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.file.*;
 import java.util.*;
 import java.util.stream.Collectors;
 
-public class TestHost implements Host {
+public class TestHost implements RepositoryHost, IssueHost {
     private final int currentUser;
     private HostData data;
 
     private static class HostData {
-        final List<HostUserDetails> users = new ArrayList<>();
+        final List<HostUser> users = new ArrayList<>();
         final Map<String, Repository> repositories = new HashMap<>();
         final Map<String, IssueProject> issueProjects = new HashMap<>();
         final Set<TemporaryDirectory> folders = new HashSet<>();
         private final Map<String, TestPullRequest> pullRequests = new HashMap<>();
         private final Map<String, TestIssue> issues = new HashMap<>();
@@ -58,11 +58,11 @@
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
 
-    public static TestHost createNew(List<HostUserDetails> users) {
+    public static TestHost createNew(List<HostUser> users) {
         var data = new HostData();
         data.users.addAll(users);
         var host = new TestHost(data, 0);
         return host;
     }
@@ -81,11 +81,11 @@
     public boolean isValid() {
         return true;
     }
 
     @Override
-    public HostedRepository getRepository(String name) {
+    public HostedRepository repository(String name) {
         Repository localRepository;
         if (data.repositories.containsKey(name)) {
             localRepository = data.repositories.get(name);
         } else {
             if (data.repositories.size() > 0) {
@@ -96,11 +96,11 @@
         }
         return new TestHostedRepository(this, name, localRepository);
     }
 
     @Override
-    public IssueProject getIssueProject(String name) {
+    public IssueProject project(String name) {
         if (data.issueProjects.containsKey(name)) {
             return data.issueProjects.get(name);
         } else {
             if (data.issueProjects.size() > 0) {
                 throw new RuntimeException("A test host can only manage a single issue project");
@@ -110,29 +110,29 @@
             return issueProject;
         }
     }
 
     @Override
-    public HostUserDetails getUserDetails(String username) {
+    public HostUser user(String username) {
         return data.users.stream()
                     .filter(user -> user.userName().equals(username))
                     .findAny()
                     .orElseThrow();
     }
 
     @Override
-    public HostUserDetails getCurrentUserDetails() {
+    public HostUser currentUser() {
         return data.users.get(currentUser);
     }
 
     @Override
     public boolean supportsReviewBody() {
         return true;
     }
 
     @Override
-    public boolean isMemberOf(String groupId, HostUserDetails user) {
+    public boolean isMemberOf(String groupId, HostUser user) {
         return false;
     }
 
     void close() {
         if (currentUser == 0) {
diff a/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java b/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java
--- a/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java
+++ b/test/src/main/java/org/openjdk/skara/test/TestHostedRepository.java
@@ -42,106 +42,106 @@
     public TestHostedRepository(TestHost host, String projectName, Repository localRepository) {
         super(host, projectName);
         this.host = host;
         this.projectName = projectName;
         this.localRepository = localRepository;
-        pullRequestPattern = Pattern.compile(getUrl().toString() + "/pr/" + "(\\d+)");
+        pullRequestPattern = Pattern.compile(url().toString() + "/pr/" + "(\\d+)");
     }
 
     @Override
-    public Host host() {
+    public RepositoryHost host() {
         return host;
     }
 
     @Override
-    public Optional<HostedRepository> getParent() {
+    public Optional<HostedRepository> parent() {
         throw new RuntimeException("Not implemented yet");
     }
 
     @Override
     public PullRequest createPullRequest(HostedRepository target, String targetRef, String sourceRef, String title, List<String> body, boolean draft) {
         return host.createPullRequest(this, targetRef, sourceRef, title, body, draft);
     }
 
     @Override
-    public PullRequest getPullRequest(String id) {
+    public PullRequest pullRequest(String id) {
         return host.getPullRequest(this, id);
     }
 
     @Override
-    public List<PullRequest> getPullRequests() {
+    public List<PullRequest> pullRequests() {
         return new ArrayList<>(host.getPullRequests(this));
     }
 
     @Override
     public List<PullRequest> findPullRequestsWithComment(String author, String body) {
-        return getPullRequests().stream()
-                                .filter(pr -> pr.getComments().stream()
-                                        .filter(comment -> author == null || comment.author().userName().equals(author))
-                                        .filter(comment -> comment == null ||comment.body().contains(body))
-                                        .count() > 0
+        return pullRequests().stream()
+                             .filter(pr -> pr.comments().stream()
+                                                .filter(comment -> author == null || comment.author().userName().equals(author))
+                                                .filter(comment -> comment == null ||comment.body().contains(body))
+                                                .count() > 0
                                 )
-                                .collect(Collectors.toList());
+                             .collect(Collectors.toList());
     }
 
     @Override
     public Optional<PullRequest> parsePullRequestUrl(String url) {
         var matcher = pullRequestPattern.matcher(url);
         if (matcher.find()) {
-            return Optional.of(getPullRequest(matcher.group(1)));
+            return Optional.of(pullRequest(matcher.group(1)));
         } else {
             return Optional.empty();
         }
     }
 
     @Override
-    public String getName() {
+    public String name() {
         return projectName;
     }
 
     @Override
-    public URI getUrl() {
+    public URI url() {
         try {
             // We need a URL without a trailing slash
             var fileName = localRepository.root().getFileName().toString();
             return new URI(localRepository.root().getParent().toUri().toString() + fileName);
         } catch (IOException | URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
 
     @Override
-    public URI getWebUrl() {
-        return getUrl();
+    public URI webUrl() {
+        return url();
     }
 
     @Override
-    public URI getWebUrl(Hash hash) {
+    public URI webUrl(Hash hash) {
         try {
-            return new URI(getUrl().toString() + "/" + hash.hex());
+            return new URI(url().toString() + "/" + hash.hex());
         } catch (URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
 
     @Override
-    public VCS getRepositoryType() {
+    public VCS repositoryType() {
         return VCS.GIT;
     }
 
     @Override
-    public String getFileContents(String filename, String ref) {
+    public String fileContents(String filename, String ref) {
         try {
             var lines = localRepository.lines(Path.of(filename), localRepository.resolve(ref).orElseThrow());
             return String.join("\n", lines.orElseThrow());
         } catch (IOException e) {
             throw new UncheckedIOException(e);
         }
     }
 
     @Override
-    public String getNamespace() {
+    public String namespace() {
         return "test";
     }
 
     @Override
     public Optional<WebHook> parseWebHook(JSONValue body) {
@@ -152,16 +152,16 @@
     public HostedRepository fork() {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public long getId() {
+    public long id() {
         return 0L;
     }
 
     @Override
-    public Hash getBranchHash(String ref) {
+    public Hash branchHash(String ref) {
         try {
             var hash = localRepository.resolve(ref).orElseThrow();
             return hash;
         } catch (IOException e) {
             throw new RuntimeException(e);
diff a/test/src/main/java/org/openjdk/skara/test/TestIssue.java b/test/src/main/java/org/openjdk/skara/test/TestIssue.java
--- a/test/src/main/java/org/openjdk/skara/test/TestIssue.java
+++ b/test/src/main/java/org/openjdk/skara/test/TestIssue.java
@@ -21,24 +21,24 @@
  * questions.
  */
 package org.openjdk.skara.test;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 
 import java.net.URI;
 import java.time.ZonedDateTime;
 import java.util.*;
 
 public class TestIssue implements Issue {
     protected final String id;
     protected final IssueProject issueProject;
-    protected final HostUserDetails author;
-    protected final HostUserDetails user;
+    protected final HostUser author;
+    protected final HostUser user;
     protected final IssueData data;
 
-    protected TestIssue(TestIssueProject issueProject, String id, HostUserDetails author, HostUserDetails user, IssueData data) {
+    protected TestIssue(TestIssueProject issueProject, String id, HostUser author, HostUser user, IssueData data) {
         this.id = id;
         this.issueProject = issueProject;
         this.author = author;;
         this.user = user;
         this.data = data;
@@ -46,58 +46,58 @@
 
     static TestIssue createNew(TestIssueProject issueProject, String id, String title, List<String> body) {
         var data = new IssueData();
         data.title = title;
         data.body = String.join("\n", body);
-        var issue = new TestIssue(issueProject, id, issueProject.host().getCurrentUserDetails(), issueProject.host().getCurrentUserDetails(), data);
+        var issue = new TestIssue(issueProject, id, issueProject.host().currentUser(), issueProject.host().currentUser(), data);
         return issue;
     }
 
     static TestIssue createFrom(TestIssueProject issueProject, TestIssue other) {
-        var issue = new TestIssue(issueProject, other.id, other.author, issueProject.host().getCurrentUserDetails(), other.data);
+        var issue = new TestIssue(issueProject, other.id, other.author, issueProject.host().currentUser(), other.data);
         return issue;
     }
 
     @Override
     public IssueProject project() {
         return issueProject;
     }
 
     @Override
-    public String getId() {
+    public String id() {
         return id;
     }
 
     @Override
-    public HostUserDetails getAuthor() {
+    public HostUser author() {
         return author;
     }
 
     @Override
-    public String getTitle() {
+    public String title() {
         return data.title;
     }
 
     @Override
     public void setTitle(String title) {
         data.title = title;
         data.lastUpdate = ZonedDateTime.now();
     }
 
     @Override
-    public String getBody() {
+    public String body() {
         return data.body;
     }
 
     @Override
     public void setBody(String body) {
         data.body = body;
         data.lastUpdate = ZonedDateTime.now();
     }
 
     @Override
-    public List<Comment> getComments() {
+    public List<Comment> comments() {
         return new ArrayList<>(data.comments);
     }
 
     @Override
     public Comment addComment(String body) {
@@ -124,16 +124,16 @@
         data.lastUpdate = ZonedDateTime.now();
         return comment;
     }
 
     @Override
-    public ZonedDateTime getCreated() {
+    public ZonedDateTime createdAt() {
         return data.created;
     }
 
     @Override
-    public ZonedDateTime getUpdated() {
+    public ZonedDateTime updatedAt() {
         return data.lastUpdate;
     }
 
     @Override
     public void setState(State state) {
@@ -156,24 +156,24 @@
         data.labels.remove(label);
         data.lastUpdate = ZonedDateTime.now();
     }
 
     @Override
-    public List<String> getLabels() {
+    public List<String> labels() {
         return new ArrayList<>(data.labels);
     }
 
     @Override
-    public URI getWebUrl() {
-        return URIBuilder.base(issueProject.getWebUrl()).appendPath(id).build();
+    public URI webUrl() {
+        return URIBuilder.base(issueProject.webUrl()).appendPath(id).build();
     }
 
     @Override
-    public List<HostUserDetails> getAssignees() {
+    public List<HostUser> assignees() {
         throw new RuntimeException("not implemented yet");
     }
 
     @Override
-    public void setAssignees(List<HostUserDetails> assignees) {
+    public void setAssignees(List<HostUser> assignees) {
         throw new RuntimeException("not implemented yet");
     }
 }
diff a/test/src/main/java/org/openjdk/skara/test/TestIssueProject.java b/test/src/main/java/org/openjdk/skara/test/TestIssueProject.java
--- a/test/src/main/java/org/openjdk/skara/test/TestIssueProject.java
+++ b/test/src/main/java/org/openjdk/skara/test/TestIssueProject.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.test;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 
 import java.net.URI;
 import java.util.*;
 
 public class TestIssueProject implements IssueProject {
@@ -40,11 +40,11 @@
     public Host host() {
         return host;
     }
 
     @Override
-    public URI getWebUrl() {
+    public URI webUrl() {
         return URIBuilder.base("http://localhost/project/" + projectName).build();
     }
 
     public TestIssueProject(TestHost host, String projectName) {
         this.host = host;
@@ -55,18 +55,18 @@
     public Issue createIssue(String title, List<String> body) {
         return host.createIssue(this, title, body);
     }
 
     @Override
-    public Optional<Issue> getIssue(String id) {
+    public Optional<Issue> issue(String id) {
         if (id.indexOf('-') < 0) {
             id = projectName.toUpperCase() + "-" + id;
         }
 
         return Optional.ofNullable(host.getIssue(this, id));
     }
 
     @Override
-    public List<Issue> getIssues() {
+    public List<Issue> issues() {
         return new ArrayList<>(host.getIssues(this));
     }
 }
diff a/test/src/main/java/org/openjdk/skara/test/TestMailmanServer.java b/test/src/main/java/org/openjdk/skara/test/TestMailmanServer.java
--- a/test/src/main/java/org/openjdk/skara/test/TestMailmanServer.java
+++ b/test/src/main/java/org/openjdk/skara/test/TestMailmanServer.java
@@ -22,11 +22,11 @@
  */
 package org.openjdk.skara.test;
 
 import com.sun.net.httpserver.*;
 import org.openjdk.skara.email.EmailAddress;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.mailinglist.Mbox;
 
 import java.io.*;
 import java.net.*;
 import java.nio.charset.StandardCharsets;
diff a/test/src/main/java/org/openjdk/skara/test/TestPullRequest.java b/test/src/main/java/org/openjdk/skara/test/TestPullRequest.java
--- a/test/src/main/java/org/openjdk/skara/test/TestPullRequest.java
+++ b/test/src/main/java/org/openjdk/skara/test/TestPullRequest.java
@@ -21,11 +21,11 @@
  * questions.
  */
 package org.openjdk.skara.test;
 
 import org.openjdk.skara.host.*;
-import org.openjdk.skara.host.network.URIBuilder;
+import org.openjdk.skara.network.URIBuilder;
 import org.openjdk.skara.vcs.Hash;
 
 import java.io.*;
 import java.net.*;
 import java.time.ZonedDateTime;
@@ -37,11 +37,11 @@
     private final TestHostedRepository repository;
     private final String targetRef;
     private final String sourceRef;
     private final PullRequestData data;
 
-    private TestPullRequest(TestHostedRepository repository, String id, HostUserDetails author, HostUserDetails user, String targetRef, String sourceRef, PullRequestData data) {
+    private TestPullRequest(TestHostedRepository repository, String id, HostUser author, HostUser user, String targetRef, String sourceRef, PullRequestData data) {
         super(repository, id, author, user, data);
         this.repository = repository;
         this.targetRef = targetRef;
         this.sourceRef = sourceRef;
         this.data = data;
@@ -60,16 +60,16 @@
     static TestPullRequest createNew(TestHostedRepository repository, String id, String targetRef, String sourceRef, String title, List<String> body, boolean draft) {
         var data = new PullRequestData();
         data.title = title;
         data.body = String.join("\n", body);
         data.draft = draft;
-        var pr = new TestPullRequest(repository, id, repository.host().getCurrentUserDetails(), repository.host().getCurrentUserDetails(), targetRef, sourceRef, data);
+        var pr = new TestPullRequest(repository, id, repository.host().currentUser(), repository.host().currentUser(), targetRef, sourceRef, data);
         return pr;
     }
 
     static TestPullRequest createFrom(TestHostedRepository repository, TestPullRequest other) {
-        var pr = new TestPullRequest(repository, other.id, other.author, repository.host().getCurrentUserDetails(), other.targetRef, other.sourceRef, other.data);
+        var pr = new TestPullRequest(repository, other.id, other.author, repository.host().currentUser(), other.targetRef, other.sourceRef, other.data);
         return pr;
     }
 
     @Override
     public HostedRepository repository() {
@@ -80,18 +80,18 @@
     public IssueProject project() {
         return null;
     }
 
     @Override
-    public List<Review> getReviews() {
+    public List<Review> reviews() {
         return new ArrayList<>(data.reviews);
     }
 
     @Override
     public void addReview(Review.Verdict verdict, String body) {
         try {
-            var review = new Review(repository.host().getCurrentUserDetails(),
+            var review = new Review(repository.host().currentUser(),
                                     verdict, repository.localRepository().resolve(sourceRef).orElseThrow(),
                                     data.reviews.size(),
                                     body);
 
             data.reviews.add(review);
@@ -120,36 +120,36 @@
         data.lastUpdate = ZonedDateTime.now();
         return comment;
     }
 
     @Override
-    public List<ReviewComment> getReviewComments() {
+    public List<ReviewComment> reviewComments() {
         return new ArrayList<>(data.reviewComments);
     }
 
     @Override
-    public Hash getHeadHash() {
+    public Hash headHash() {
         return data.headHash;
     }
 
     @Override
-    public String getSourceRef() {
+    public String sourceRef() {
         return sourceRef;
     }
 
     @Override
-    public String getTargetRef() {
+    public String targetRef() {
         return targetRef;
     }
 
     @Override
-    public Hash getTargetHash() {
-        return repository.getBranchHash(targetRef);
+    public Hash targetHash() {
+        return repository.branchHash(targetRef);
     }
 
     @Override
-    public Map<String, Check> getChecks(Hash hash) {
+    public Map<String, Check> checks(Hash hash) {
         return data.checks.stream()
                 .filter(check -> check.hash().equals(hash))
                 .collect(Collectors.toMap(Check::name, Function.identity()));
     }
 
@@ -174,28 +174,28 @@
         data.checks.add(updated);
         data.lastUpdate = ZonedDateTime.now();
     }
 
     @Override
-    public URI getChangeUrl() {
-        return URIBuilder.base(getWebUrl()).appendPath("/files").build();
+    public URI changeUrl() {
+        return URIBuilder.base(webUrl()).appendPath("/files").build();
     }
 
     @Override
-    public URI getChangeUrl(Hash base) {
-        return URIBuilder.base(getWebUrl()).appendPath("/files/" + base.abbreviate()).build();
+    public URI changeUrl(Hash base) {
+        return URIBuilder.base(webUrl()).appendPath("/files/" + base.abbreviate()).build();
     }
 
     @Override
     public boolean isDraft() {
         return data.draft;
     }
 
     @Override
-    public URI getWebUrl() {
+    public URI webUrl() {
         try {
-            return new URI(repository.getUrl().toString() + "/pr/" + getId());
+            return new URI(repository.url().toString() + "/pr/" + id());
         } catch (URISyntaxException e) {
             throw new RuntimeException(e);
         }
     }
 }
