Merge "Add directory listing to server.go"
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index b1ce1ce..b4a5cef 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -7,10 +7,10 @@
 link:note-db.html[NoteDb].
 
 The account data consists of a sequence number (account ID), account
-properties (full name, preferred email, registration date, status,
-inactive flag), preferences (general, diff and edit preferences),
-project watches, SSH keys, external IDs, starred changes and reviewed
-flags.
+properties (full name, display name, preferred email, registration
+date, status, inactive flag), preferences (general, diff and edit
+preferences), project watches, SSH keys, external IDs, starred changes
+and reviewed flags.
 
 Most account data is stored in a special link:#all-users[All-Users]
 repository, which has one branch per user. Within the user branch there
@@ -155,6 +155,7 @@
 ----
 [account]
   fullName = John Doe
+  displayName = John
   preferredEmail = john.doe@example.com
   status = OOO
   active = false
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
index bb83d95..28ecc97 100644
--- a/Documentation/rest-api-accounts.txt
+++ b/Documentation/rest-api-accounts.txt
@@ -58,8 +58,8 @@
 
 [[details]]
 --
-* `DETAILS`: Includes full name, preferred email, username, avatars,
-status and state for each account.
+* `DETAILS`: Includes full name, preferred email, username, display
+name, avatars, status and state for each account.
 --
 
 [[all-emails]]
@@ -99,12 +99,14 @@
       "name": "John Doe",
       "email": "john.doe@example.com",
       "username": "john"
+      "display_name": "John D"
     },
     {
       "_account_id": 1001439,
       "name": "John Smith",
       "email": "john.smith@example.com",
       "username": "jsmith"
+      "display_name": "Johnny"
     },
   ]
 ----
@@ -134,6 +136,7 @@
     "name": "John Doe",
     "email": "john.doe@example.com",
     "username": "john"
+    "display_name": "Super John"
   }
 ----
 
@@ -155,6 +158,7 @@
 
   {
     "name": "John Doe",
+    "display_name": "Super John",
     "email": "john.doe@example.com",
     "ssh_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw==",
     "http_password": "19D9aIn7zePb",
@@ -208,6 +212,7 @@
     "name": "John Doe",
     "email": "john.doe@example.com",
     "username": "john"
+    "display_name": "Super John"
   }
 ----
 
@@ -401,6 +406,27 @@
 
 As response the new username is returned.
 
+[[set-display-name]]
+=== Set Display Name
+--
+'PUT /accounts/link:#account-id[\{account-id\}]/displayname'
+--
+
+The new display name must be provided in the request body inside
+a link:#display-name-input[DisplayNameInput] entity.
+
+.Request
+----
+  PUT /accounts/self/displayname HTTP/1.0
+  Content-Type: application/json; charset=UTF-8
+
+  {
+    "display_name": "John"
+  }
+----
+
+As response the new display name is returned.
+
 [[get-active]]
 === Get Active
 --
@@ -2228,6 +2254,11 @@
 See option link:rest-api-changes.html#detailed-accounts[
 DETAILED_ACCOUNTS] for change queries +
 and option link:#details[DETAILS] for account queries.
+|`display_name`    |optional|The display name of the user. +
+Only set if detailed account information is requested. +
+See option link:rest-api-changes.html#detailed-accounts[
+DETAILED_ACCOUNTS] for change queries +
+and option link:#details[DETAILS] for account queries.
 |`email`           |optional|
 The email address the user prefers to be contacted through. +
 Only set if detailed account information is requested. +
@@ -2268,6 +2299,7 @@
 |`username`     |optional|
 The user name. If provided, must match the user name from the URL.
 |`name`         |optional|The full name of the user.
+|`display_name` |optional|The display name of the user.
 |`email`        |optional|The email address of the user.
 |`ssh_key`      |optional|The public SSH key of the user.
 |`http_password`|optional|The HTTP password of the user.
@@ -2870,6 +2902,17 @@
 |`username` |The new username of the account.
 |=======================
 
+[[display-name-input]]
+=== DisplayNameInput
+The `DisplayNameInput` entity contains information for setting the
+display name for an account.
+
+[options="header",cols="1,6"]
+|=======================
+|Field Name     |Description
+|`display_name` |The new display name of the account.
+|=======================
+
 [[project-watch-info]]
 === ProjectWatchInfo
 The `WatchedProjectsInfo` entity contains information about a project watch
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
index a99c3bb..5086e19 100644
--- a/Documentation/rest-api-groups.txt
+++ b/Documentation/rest-api-groups.txt
@@ -503,8 +503,9 @@
   }
 ----
 
-If the group creation fails because the name is already in use the
-response is "`409 Conflict`".
+If the group creation fails because the name is already in use, or the
+UUID was specified and the UUID is already in use, the response is
+"`409 Conflict`".
 
 [[get-group-detail]]
 === Get Group Detail
@@ -1591,6 +1592,7 @@
 |Field Name      ||Description
 |`name`          |optional|The name of the group (not encoded). +
 If set, must match the group name in the URL.
+|`uuid`          |optional|The UUID of the group.
 |`description`   |optional|The description of the group.
 |`visible_to_all`|optional|
 Whether the group is visible to all registered users. +
diff --git a/java/com/google/gerrit/entities/Account.java b/java/com/google/gerrit/entities/Account.java
index b2e0017..fd3df97 100644
--- a/java/com/google/gerrit/entities/Account.java
+++ b/java/com/google/gerrit/entities/Account.java
@@ -129,6 +129,10 @@
   @Nullable
   public abstract String fullName();
 
+  /** Optional display name of the user to be shown in the UI. */
+  @Nullable
+  public abstract String displayName();
+
   /** Email address the user prefers to be contacted through. */
   @Nullable
   public abstract String preferredEmail();
@@ -236,6 +240,11 @@
     public abstract Builder setFullName(String fullName);
 
     @Nullable
+    public abstract String displayName();
+
+    public abstract Builder setDisplayName(String displayName);
+
+    @Nullable
     public abstract String preferredEmail();
 
     public abstract Builder setPreferredEmail(String preferredEmail);
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
index 67d6fd2..6df9889 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountApi.java
@@ -87,6 +87,8 @@
 
   void setStatus(String status) throws RestApiException;
 
+  void setDisplayName(String displayName) throws RestApiException;
+
   List<SshKeyInfo> listSshKeys() throws RestApiException;
 
   SshKeyInfo addSshKey(String key) throws RestApiException;
@@ -269,6 +271,11 @@
     }
 
     @Override
+    public void setDisplayName(String displayName) throws RestApiException {
+      throw new NotImplementedException();
+    }
+
+    @Override
     public List<SshKeyInfo> listSshKeys() throws RestApiException {
       throw new NotImplementedException();
     }
diff --git a/java/com/google/gerrit/extensions/api/accounts/AccountInput.java b/java/com/google/gerrit/extensions/api/accounts/AccountInput.java
index 259838b..2bcce30 100644
--- a/java/com/google/gerrit/extensions/api/accounts/AccountInput.java
+++ b/java/com/google/gerrit/extensions/api/accounts/AccountInput.java
@@ -20,6 +20,7 @@
 public class AccountInput {
   @DefaultInput public String username;
   public String name;
+  public String displayName;
   public String email;
   public String sshKey;
   public String httpPassword;
diff --git a/java/com/google/gerrit/extensions/api/accounts/DisplayNameInput.java b/java/com/google/gerrit/extensions/api/accounts/DisplayNameInput.java
new file mode 100644
index 0000000..16e7afc
--- /dev/null
+++ b/java/com/google/gerrit/extensions/api/accounts/DisplayNameInput.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.api.accounts;
+
+import com.google.gerrit.extensions.restapi.DefaultInput;
+
+public class DisplayNameInput {
+  public @DefaultInput String displayName;
+
+  public DisplayNameInput(String displayName) {
+    this.displayName = displayName;
+  }
+
+  public DisplayNameInput() {}
+}
diff --git a/java/com/google/gerrit/extensions/api/groups/GroupInput.java b/java/com/google/gerrit/extensions/api/groups/GroupInput.java
index ab38434..30af08f 100644
--- a/java/com/google/gerrit/extensions/api/groups/GroupInput.java
+++ b/java/com/google/gerrit/extensions/api/groups/GroupInput.java
@@ -18,6 +18,7 @@
 
 public class GroupInput {
   public String name;
+  public String uuid;
   public String description;
   public Boolean visibleToAll;
   public String ownerId;
diff --git a/java/com/google/gerrit/extensions/common/AccountInfo.java b/java/com/google/gerrit/extensions/common/AccountInfo.java
index d39d99a..803225e 100644
--- a/java/com/google/gerrit/extensions/common/AccountInfo.java
+++ b/java/com/google/gerrit/extensions/common/AccountInfo.java
@@ -33,6 +33,14 @@
   /** The full name of the user. */
   public String name;
 
+  /**
+   * The display name of the user. This allows users to control how their name is displayed in the
+   * UI. It will likely be unset for most users. Host admins will just choose a default (full name,
+   * user name, first name, ...) for all users, and this account property is just a way to opt out
+   * of the host wide default strategy of choosing the display name.
+   */
+  public String displayName;
+
   /** The preferred email address of the user. */
   public String email;
 
@@ -73,6 +81,7 @@
       AccountInfo accountInfo = (AccountInfo) o;
       return Objects.equals(_accountId, accountInfo._accountId)
           && Objects.equals(name, accountInfo.name)
+          && Objects.equals(displayName, accountInfo.displayName)
           && Objects.equals(email, accountInfo.email)
           && Objects.equals(secondaryEmails, accountInfo.secondaryEmails)
           && Objects.equals(username, accountInfo.username)
@@ -88,6 +97,7 @@
     return MoreObjects.toStringHelper(this)
         .add("id", _accountId)
         .add("name", name)
+        .add("displayname", displayName)
         .add("email", email)
         .add("username", username)
         .toString();
@@ -96,7 +106,15 @@
   @Override
   public int hashCode() {
     return Objects.hash(
-        _accountId, name, email, secondaryEmails, username, avatars, _moreAccounts, status);
+        _accountId,
+        name,
+        displayName,
+        email,
+        secondaryEmails,
+        username,
+        avatars,
+        _moreAccounts,
+        status);
   }
 
   protected AccountInfo() {}
diff --git a/java/com/google/gerrit/server/account/AccountConfig.java b/java/com/google/gerrit/server/account/AccountConfig.java
index 5a1bb8a..afe8c7b 100644
--- a/java/com/google/gerrit/server/account/AccountConfig.java
+++ b/java/com/google/gerrit/server/account/AccountConfig.java
@@ -190,6 +190,7 @@
             InternalAccountUpdate.builder()
                 .setActive(account.isActive())
                 .setFullName(account.fullName())
+                .setDisplayName(account.displayName())
                 .setPreferredEmail(account.preferredEmail())
                 .setStatus(account.status())
                 .build());
diff --git a/java/com/google/gerrit/server/account/AccountProperties.java b/java/com/google/gerrit/server/account/AccountProperties.java
index 4f29b25..5ae5567 100644
--- a/java/com/google/gerrit/server/account/AccountProperties.java
+++ b/java/com/google/gerrit/server/account/AccountProperties.java
@@ -33,6 +33,7 @@
  *   [account]
  *     active = false
  *     fullName = John Doe
+ *     displayName = John
  *     preferredEmail = john.doe@foo.com
  *     status = Overloaded with reviews
  * </pre>
@@ -51,6 +52,7 @@
   public static final String ACCOUNT = "account";
   public static final String KEY_ACTIVE = "active";
   public static final String KEY_FULL_NAME = "fullName";
+  public static final String KEY_DISPLAY_NAME = "displayName";
   public static final String KEY_PREFERRED_EMAIL = "preferredEmail";
   public static final String KEY_STATUS = "status";
 
@@ -91,6 +93,7 @@
     Account.Builder accountBuilder = Account.builder(accountId, registeredOn);
     accountBuilder.setActive(accountConfig.getBoolean(ACCOUNT, null, KEY_ACTIVE, true));
     accountBuilder.setFullName(get(accountConfig, KEY_FULL_NAME));
+    accountBuilder.setDisplayName(get(accountConfig, KEY_DISPLAY_NAME));
 
     String preferredEmail = get(accountConfig, KEY_PREFERRED_EMAIL);
     accountBuilder.setPreferredEmail(preferredEmail);
@@ -109,6 +112,9 @@
     accountUpdate.getActive().ifPresent(active -> setActive(cfg, active));
     accountUpdate.getFullName().ifPresent(fullName -> set(cfg, KEY_FULL_NAME, fullName));
     accountUpdate
+        .getDisplayName()
+        .ifPresent(displayName -> set(cfg, KEY_DISPLAY_NAME, displayName));
+    accountUpdate
         .getPreferredEmail()
         .ifPresent(preferredEmail -> set(cfg, KEY_PREFERRED_EMAIL, preferredEmail));
     accountUpdate.getStatus().ifPresent(status -> set(cfg, KEY_STATUS, status));
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 125edfe..8b2a081 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -83,14 +83,14 @@
  *
  * <p>The account updates are written to NoteDb. In NoteDb accounts are represented as user branches
  * in the {@code All-Users} repository. Optionally a user branch can contain a 'account.config' file
- * that stores account properties, such as full name, preferred email, status and the active flag.
- * The timestamp of the first commit on a user branch denotes the registration date. The initial
- * commit on the user branch may be empty (since having an 'account.config' is optional). See {@link
- * AccountConfig} for details of the 'account.config' file format. In addition the user branch can
- * contain a 'preferences.config' config file to store preferences (see {@link StoredPreferences})
- * and a 'watch.config' config file to store project watches (see {@link ProjectWatches}). External
- * IDs are stored separately in the {@code refs/meta/external-ids} notes branch (see {@link
- * ExternalIdNotes}).
+ * that stores account properties, such as full name, display name, preferred email, status and the
+ * active flag. The timestamp of the first commit on a user branch denotes the registration date.
+ * The initial commit on the user branch may be empty (since having an 'account.config' is
+ * optional). See {@link AccountConfig} for details of the 'account.config' file format. In addition
+ * the user branch can contain a 'preferences.config' config file to store preferences (see {@link
+ * StoredPreferences}) and a 'watch.config' config file to store project watches (see {@link
+ * ProjectWatches}). External IDs are stored separately in the {@code refs/meta/external-ids} notes
+ * branch (see {@link ExternalIdNotes}).
  *
  * <p>On updating an account the account is evicted from the account cache and reindexed. The
  * eviction from the account cache and the reindexing is done by the {@link ReindexAfterRefUpdate}
diff --git a/java/com/google/gerrit/server/account/CreateGroupArgs.java b/java/com/google/gerrit/server/account/CreateGroupArgs.java
index 2c59a08..2a764cc 100644
--- a/java/com/google/gerrit/server/account/CreateGroupArgs.java
+++ b/java/com/google/gerrit/server/account/CreateGroupArgs.java
@@ -20,6 +20,7 @@
 
 public class CreateGroupArgs {
   private AccountGroup.NameKey groupName;
+  public AccountGroup.UUID uuid;
   public String groupDescription;
   public boolean visibleToAll;
   public AccountGroup.UUID ownerGroupUuid;
diff --git a/java/com/google/gerrit/server/account/InternalAccountUpdate.java b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
index cf77a75..bfbe917 100644
--- a/java/com/google/gerrit/server/account/InternalAccountUpdate.java
+++ b/java/com/google/gerrit/server/account/InternalAccountUpdate.java
@@ -18,6 +18,7 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.extensions.client.DiffPreferencesInfo;
 import com.google.gerrit.extensions.client.EditPreferencesInfo;
@@ -61,6 +62,15 @@
   public abstract Optional<String> getFullName();
 
   /**
+   * Returns the new value for the display name.
+   *
+   * @return the new value for the display name, {@code Optional#empty()} if the display name is not
+   *     being updated, {@code Optional#of("")} if the display name is unset, the wrapped value is
+   *     never {@code null}
+   */
+  public abstract Optional<String> getDisplayName();
+
+  /**
    * Returns the new value for the preferred email.
    *
    * @return the new value for the preferred email, {@code Optional#empty()} if the preferred email
@@ -166,25 +176,30 @@
      * Sets a new full name for the account.
      *
      * @param fullName the new full name, if {@code null} or empty string the full name is unset
-     * @return the builder
      */
-    public abstract Builder setFullName(String fullName);
+    public abstract Builder setFullName(@Nullable String fullName);
+
+    /**
+     * Sets a new display name for the account.
+     *
+     * @param displayName the new display name, if {@code null} or empty string the display name is
+     *     unset
+     */
+    public abstract Builder setDisplayName(@Nullable String displayName);
 
     /**
      * Sets a new preferred email for the account.
      *
      * @param preferredEmail the new preferred email, if {@code null} or empty string the preferred
      *     email is unset
-     * @return the builder
      */
-    public abstract Builder setPreferredEmail(String preferredEmail);
+    public abstract Builder setPreferredEmail(@Nullable String preferredEmail);
 
     /**
      * Sets the active flag for the account.
      *
      * @param active {@code true} if the account should be set to active, {@code false} if the
      *     account should be set to inactive
-     * @return the builder
      */
     public abstract Builder setActive(boolean active);
 
@@ -192,9 +207,8 @@
      * Sets a new status for the account.
      *
      * @param status the new status, if {@code null} or empty string the status is unset
-     * @return the builder
      */
-    public abstract Builder setStatus(String status);
+    public abstract Builder setStatus(@Nullable String status);
 
     /**
      * Returns a builder for the set of created external IDs.
@@ -487,6 +501,12 @@
       }
 
       @Override
+      public Builder setDisplayName(String displayName) {
+        delegate.setDisplayName(Strings.nullToEmpty(displayName));
+        return this;
+      }
+
+      @Override
       public Builder setPreferredEmail(String preferredEmail) {
         delegate.setPreferredEmail(Strings.nullToEmpty(preferredEmail));
         return this;
diff --git a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
index 7fa9767..4f85412 100644
--- a/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
+++ b/java/com/google/gerrit/server/api/accounts/AccountApiImpl.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.extensions.api.accounts.AgreementInput;
 import com.google.gerrit.extensions.api.accounts.DeleteDraftCommentsInput;
 import com.google.gerrit.extensions.api.accounts.DeletedDraftCommentInfo;
+import com.google.gerrit.extensions.api.accounts.DisplayNameInput;
 import com.google.gerrit.extensions.api.accounts.EmailApi;
 import com.google.gerrit.extensions.api.accounts.EmailInput;
 import com.google.gerrit.extensions.api.accounts.GpgKeyApi;
@@ -76,6 +77,7 @@
 import com.google.gerrit.server.restapi.account.PostWatchedProjects;
 import com.google.gerrit.server.restapi.account.PutActive;
 import com.google.gerrit.server.restapi.account.PutAgreement;
+import com.google.gerrit.server.restapi.account.PutDisplayName;
 import com.google.gerrit.server.restapi.account.PutHttpPassword;
 import com.google.gerrit.server.restapi.account.PutName;
 import com.google.gerrit.server.restapi.account.PutStatus;
@@ -134,6 +136,7 @@
   private final DeleteExternalIds deleteExternalIds;
   private final DeleteDraftComments deleteDraftComments;
   private final PutStatus putStatus;
+  private final PutDisplayName putDisplayName;
   private final GetGroups getGroups;
   private final EmailApiImpl.Factory emailApi;
   private final PutName putName;
@@ -177,6 +180,7 @@
       DeleteExternalIds deleteExternalIds,
       DeleteDraftComments deleteDraftComments,
       PutStatus putStatus,
+      PutDisplayName putDisplayName,
       GetGroups getGroups,
       EmailApiImpl.Factory emailApi,
       PutName putName,
@@ -219,6 +223,7 @@
     this.deleteExternalIds = deleteExternalIds;
     this.deleteDraftComments = deleteDraftComments;
     this.putStatus = putStatus;
+    this.putDisplayName = putDisplayName;
     this.getGroups = getGroups;
     this.emailApi = emailApi;
     this.putName = putName;
@@ -477,6 +482,16 @@
   }
 
   @Override
+  public void setDisplayName(String displayName) throws RestApiException {
+    DisplayNameInput in = new DisplayNameInput(displayName);
+    try {
+      putDisplayName.apply(account, in);
+    } catch (Exception e) {
+      throw asRestApiException("Cannot set display name", e);
+    }
+  }
+
+  @Override
   public List<SshKeyInfo> listSshKeys() throws RestApiException {
     try {
       return getSshKeys.apply(account).value();
diff --git a/java/com/google/gerrit/server/restapi/account/Module.java b/java/com/google/gerrit/server/restapi/account/Module.java
index f41764d..51055b8 100644
--- a/java/com/google/gerrit/server/restapi/account/Module.java
+++ b/java/com/google/gerrit/server/restapi/account/Module.java
@@ -53,6 +53,7 @@
     delete(ACCOUNT_KIND, "name").to(PutName.class);
     get(ACCOUNT_KIND, "status").to(GetStatus.class);
     put(ACCOUNT_KIND, "status").to(PutStatus.class);
+    put(ACCOUNT_KIND, "displayname").to(PutDisplayName.class);
     get(ACCOUNT_KIND, "username").to(GetUsername.class);
     put(ACCOUNT_KIND, "username").to(PutUsername.class);
     get(ACCOUNT_KIND, "active").to(GetActive.class);
diff --git a/java/com/google/gerrit/server/restapi/account/PutDisplayName.java b/java/com/google/gerrit/server/restapi/account/PutDisplayName.java
new file mode 100644
index 0000000..557e660
--- /dev/null
+++ b/java/com/google/gerrit/server/restapi/account/PutDisplayName.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2020 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.restapi.account;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.extensions.api.accounts.DisplayNameInput;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ServerInitiated;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AccountsUpdate;
+import com.google.gerrit.server.permissions.GlobalPermission;
+import com.google.gerrit.server.permissions.PermissionBackend;
+import com.google.gerrit.server.permissions.PermissionBackendException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.io.IOException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+/**
+ * REST endpoint to set the display name of an account.
+ *
+ * <p>This REST endpoint handles {@code PUT /accounts/<account-identifier>/displayname} requests.
+ *
+ * <p>The display name is a free-form text that a user can set for their own account. It defines how
+ * the user's name will be rendered in the UI in most screens. It is optional, and if not set, then
+ * the UI falls back to whatever is configured as the default display name, e.g. the full name.
+ */
+@Singleton
+public class PutDisplayName implements RestModifyView<AccountResource, DisplayNameInput> {
+  private final Provider<CurrentUser> self;
+  private final PermissionBackend permissionBackend;
+  private final Provider<AccountsUpdate> accountsUpdateProvider;
+
+  @Inject
+  PutDisplayName(
+      Provider<CurrentUser> self,
+      PermissionBackend permissionBackend,
+      @ServerInitiated Provider<AccountsUpdate> accountsUpdateProvider) {
+    this.self = self;
+    this.permissionBackend = permissionBackend;
+    this.accountsUpdateProvider = accountsUpdateProvider;
+  }
+
+  @Override
+  public Response<String> apply(AccountResource rsrc, @Nullable DisplayNameInput input)
+      throws AuthException, ResourceNotFoundException, IOException, PermissionBackendException,
+          ConfigInvalidException {
+    IdentifiedUser user = rsrc.getUser();
+    if (!self.get().hasSameAccountId(user)) {
+      permissionBackend.currentUser().check(GlobalPermission.MODIFY_ACCOUNT);
+    }
+    if (input == null) {
+      input = new DisplayNameInput();
+    }
+
+    String newDisplayName = input.displayName;
+    AccountState accountState =
+        accountsUpdateProvider
+            .get()
+            .update(
+                "Set Display Name via API",
+                user.getAccountId(),
+                u -> u.setDisplayName(newDisplayName))
+            .orElseThrow(() -> new ResourceNotFoundException("account not found"));
+    return Strings.isNullOrEmpty(accountState.account().displayName())
+        ? Response.none()
+        : Response.ok(accountState.account().displayName());
+  }
+}
diff --git a/java/com/google/gerrit/server/restapi/group/CreateGroup.java b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
index 531d350..e5a1478 100644
--- a/java/com/google/gerrit/server/restapi/group/CreateGroup.java
+++ b/java/com/google/gerrit/server/restapi/group/CreateGroup.java
@@ -138,6 +138,16 @@
     AccountGroup.UUID ownerUuid = owner(input);
     CreateGroupArgs args = new CreateGroupArgs();
     args.setGroupName(name);
+    args.uuid = Strings.isNullOrEmpty(input.uuid) ? null : AccountGroup.UUID.parse(input.uuid);
+    if (args.uuid != null) {
+      if (!args.uuid.isInternalGroup()) {
+        throw new BadRequestException(String.format("invalid group UUID '%s'", args.uuid.get()));
+      }
+      if (groupCache.get(args.uuid).isPresent()) {
+        throw new ResourceConflictException(
+            String.format("group with UUID '%s' already exists", args.uuid.get()));
+      }
+    }
     args.groupDescription = Strings.emptyToNull(input.description);
     args.visibleToAll = MoreObjects.firstNonNull(input.visibleToAll, defaultVisibleToAll);
     args.ownerGroupUuid = ownerUuid;
@@ -196,9 +206,11 @@
 
     AccountGroup.Id groupId = AccountGroup.id(sequences.nextGroupId());
     AccountGroup.UUID uuid =
-        GroupUuid.make(
-            createGroupArgs.getGroupName(),
-            self.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone()));
+        MoreObjects.firstNonNull(
+            createGroupArgs.uuid,
+            GroupUuid.make(
+                createGroupArgs.getGroupName(),
+                self.get().newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone())));
     InternalGroupCreation groupCreation =
         InternalGroupCreation.builder()
             .setGroupUUID(uuid)
diff --git a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
index 988580e..17a0ea6 100644
--- a/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/group/GroupsIT.java
@@ -415,6 +415,43 @@
   }
 
   @Test
+  public void createGroupWithUuid() throws Exception {
+    AccountGroup.UUID uuid = AccountGroup.UUID.parse("4eb25d1cca562f53b9356117f33840706a36a349");
+    GroupInput input = new GroupInput();
+    input.uuid = uuid.get();
+    input.name = name("new-group");
+    GroupInfo info = gApi.groups().create(input).get();
+    assertThat(info.name).isEqualTo(input.name);
+    assertThat(info.id).isEqualTo(input.uuid);
+  }
+
+  @Test
+  public void createGroupWithExistingUuid_Conflict() throws Exception {
+    GroupInfo existingGroup = gApi.groups().create(name("new-group")).get();
+    GroupInput input = new GroupInput();
+    input.uuid = existingGroup.id;
+    input.name = name("another-new-group");
+    ResourceConflictException thrown =
+        assertThrows(ResourceConflictException.class, () -> gApi.groups().create(input).get());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(String.format("group with UUID '%s' already exists", input.uuid));
+  }
+
+  @Test
+  public void createGroupWithInvalidUuid_BadRequest() throws Exception {
+    AccountGroup.UUID uuid = AccountGroup.UUID.parse("foo:bar");
+    GroupInput input = new GroupInput();
+    input.uuid = uuid.get();
+    input.name = name("new-group");
+    BadRequestException thrown =
+        assertThrows(BadRequestException.class, () -> gApi.groups().create(input).get());
+    assertThat(thrown)
+        .hasMessageThat()
+        .isEqualTo(String.format("invalid group UUID '%s'", input.uuid));
+  }
+
+  @Test
   public void createGroupWithProperties() throws Exception {
     GroupInput in = new GroupInput();
     in.name = name("newGroup");
diff --git a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
index cbfc728..8e396f1 100644
--- a/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
+++ b/polygerrit-ui/app/elements/diff/gr-diff-view/gr-diff-view.html
@@ -75,6 +75,12 @@
       header {
         padding: var(--spacing-s) var(--spacing-l);
       }
+      .changeNumberColon {
+        color: transparent;
+      }
+      .headerSubject {
+        margin-right: var(--spacing-m);
+      }
       .patchRangeLeft {
         align-items: center;
         display: flex;
@@ -215,16 +221,15 @@
     >
       <header>
         <h3>
-          <a href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">
-            [[_changeNum]]</a><span>:</span>
-          <span>[[_change.subject]]</span>
-          <span class="dash">—</span>
+          <a href$="[[_computeChangePath(_change, _patchRange.*, _change.revisions)]]">[[_changeNum]]</a><!--
+       --><span class="changeNumberColon">:</span>
+          <span class="headerSubject">[[_change.subject]]</span>
           <input id="reviewed"
               class="reviewed hideOnEdit"
               type="checkbox"
               on-change="_handleReviewedChange"
-              hidden$="[[!_loggedIn]]" hidden>
-          <div class="jumpToFileContainer">
+              hidden$="[[!_loggedIn]]" hidden><!--
+       --><div class="jumpToFileContainer">
             <gr-dropdown-list
                 id="dropdown"
                 value="[[_path]]"