Merge "PostReview: Add debug logs that are included into traces"
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 1e1b852..b764439 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -1281,7 +1281,7 @@
 [[change.strictLabels]]change.strictLabels::
 +
 Reject invalid label votes: invalid labels or invalid values. This
-configuration option is provided for backwards compaitbility and may
+configuration option is provided for backwards compatibility and may
 be removed in future gerrit versions.
 +
 Default is false.
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
index 2b49e3e..77be4b3 100644
--- a/Documentation/rest-api-projects.txt
+++ b/Documentation/rest-api-projects.txt
@@ -1252,57 +1252,6 @@
   }
 ----
 
-[[create-change]]
-=== Create Change for review.
-
-This endpoint is functionally equivalent to
-link:rest-api-changes.html#create-change[create change in the change
-API], but it has the project name in the URL, which is easier to route
-in sharded deployments.
-
-.Request
-----
-  POST /projects/myProject/create.change HTTP/1.0
-  Content-Type: application/json; charset=UTF-8
-
-  {
-    "subject" : "Let's support 100% Gerrit workflow direct in browser",
-    "branch" : "master",
-    "topic" : "create-change-in-browser",
-    "status" : "NEW"
-  }
-----
-
-As response a link:#change-info[ChangeInfo] entity is returned that describes
-the resulting change.
-
-.Response
-----
-  HTTP/1.1 201 OK
-  Content-Disposition: attachment
-  Content-Type: application/json; charset=UTF-8
-
-  )]}'
-  {
-    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9941",
-    "project": "myProject",
-    "branch": "master",
-    "topic": "create-change-in-browser",
-    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9941",
-    "subject": "Let's support 100% Gerrit workflow direct in browser",
-    "status": "NEW",
-    "created": "2014-05-05 07:15:44.639000000",
-    "updated": "2014-05-05 07:15:44.639000000",
-    "mergeable": true,
-    "insertions": 0,
-    "deletions": 0,
-    "_number": 4711,
-    "owner": {
-      "name": "John Doe"
-    }
-  }
-----
-
 [[create-access-change]]
 === Create Access Rights Change for review.
 --
diff --git a/WORKSPACE b/WORKSPACE
index 1a0fd4e..befde25 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -776,8 +776,8 @@
 
 maven_jar(
     name = "dropwizard-core",
-    artifact = "io.dropwizard.metrics:metrics-core:4.0.5",
-    sha1 = "b81ef162970cdb9f4512ee2da09715a856ff4c4c",
+    artifact = "io.dropwizard.metrics:metrics-core:4.0.7",
+    sha1 = "673899f605f52ca35836673ccfee97154a496a61",
 )
 
 # When updating Bouncy Castle, also update it in bazlets.
@@ -1024,8 +1024,8 @@
 
 maven_jar(
     name = "jackson-core",
-    artifact = "com.fasterxml.jackson.core:jackson-core:2.9.8",
-    sha1 = "0f5a654e4675769c716e5b387830d19b501ca191",
+    artifact = "com.fasterxml.jackson.core:jackson-core:2.10.0",
+    sha1 = "4e2c5fa04648ec9772c63e2101c53af6504e624e",
 )
 
 TESTCONTAINERS_VERSION = "1.12.3"
diff --git a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
index 20088ec..a390de3 100644
--- a/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
+++ b/java/com/google/gerrit/server/mail/send/OutgoingEmail.java
@@ -18,6 +18,7 @@
 import static com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy.DISABLED;
 import static java.util.Objects.requireNonNull;
 
+import com.google.common.collect.Sets;
 import com.google.common.flogger.FluentLogger;
 import com.google.gerrit.common.Nullable;
 import com.google.gerrit.entities.Account;
@@ -92,13 +93,16 @@
    * @throws EmailException
    */
   public void send() throws EmailException {
-    if (!notify.shouldNotify()) {
-      return;
-    }
-
     if (!args.emailSender.isEnabled()) {
       // Server has explicitly disabled email sending.
       //
+      logger.atFine().log(
+          "Not sending '%s': Email sending is disabled by server config", messageClass);
+      return;
+    }
+
+    if (!notify.shouldNotify()) {
+      logger.atFine().log("Not sending '%s': Notify handling is NONE", messageClass);
       return;
     }
 
@@ -149,6 +153,7 @@
           }
         }
         if (smtpRcptTo.isEmpty() && smtpRcptToPlaintextOnly.isEmpty()) {
+          logger.atFine().log("Not sending '%s': No SMTP recipients", messageClass);
           return;
         }
       }
@@ -187,16 +192,26 @@
         try {
           validator.validateOutgoingEmail(va);
         } catch (ValidationException e) {
+          logger.atFine().log(
+              "Not sending '%s': Rejected by outgoing email validator: %s",
+              messageClass, e.getMessage());
           return;
         }
       }
 
+      Set<Address> intersection = Sets.intersection(smtpRcptTo, smtpRcptToPlaintextOnly);
+      if (!intersection.isEmpty()) {
+        logger.atSevere().log("Email '%s' will be sent twice to %s", messageClass, intersection);
+      }
+
       if (!smtpRcptTo.isEmpty()) {
         // Send multipart message
+        logger.atFine().log("Sending multipart '%s'", messageClass);
         args.emailSender.send(va.smtpFromAddress, va.smtpRcptTo, va.headers, va.body, va.htmlBody);
       }
 
       if (!smtpRcptToPlaintextOnly.isEmpty()) {
+        logger.atFine().log("Sending plaintext '%s'", messageClass);
         // Send plaintext message
         Map<String, EmailHeader> shallowCopy = new HashMap<>();
         shallowCopy.putAll(headers);
@@ -398,6 +413,7 @@
   protected boolean shouldSendMessage() {
     if (textBody.length() == 0) {
       // If we have no message body, don't send.
+      logger.atFine().log("Not sending '%s': No message body", messageClass);
       return false;
     }
 
@@ -405,6 +421,7 @@
       // If we have nobody to send this message to, then all of our
       // selection filters previously for this type of message were
       // unable to match a destination. Don't bother sending it.
+      logger.atFine().log("Not sending '%s': No recipients", messageClass);
       return false;
     }
 
@@ -414,6 +431,7 @@
         && rcptTo.contains(fromId)) {
       // If the only recipient is also the sender, don't bother.
       //
+      logger.atFine().log("Not sending '%s': Sender is only recipient", messageClass);
       return false;
     }
 
diff --git a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
index 9c45aaf..3322b68 100644
--- a/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
+++ b/java/com/google/gerrit/server/notedb/ChangeNotesParser.java
@@ -363,6 +363,15 @@
       submissionId = parseSubmissionId(commit);
     }
 
+    if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
+      lastUpdatedOn = ts;
+    }
+
+    if (deletedPatchSets.contains(psId)) {
+      // Do not update PS details as PS was deleted and this meta data is of no relevance.
+      return;
+    }
+
     // Parse mutable patch set fields first so they can be recorded in the PendingPatchSetFields.
     parseDescription(psId, commit);
     parseGroups(psId, commit);
@@ -410,10 +419,6 @@
 
     previousWorkInProgressFooter = null;
     parseWorkInProgress(commit);
-
-    if (lastUpdatedOn == null || ts.after(lastUpdatedOn)) {
-      lastUpdatedOn = ts;
-    }
   }
 
   private String parseSubmissionId(ChangeNotesCommit commit) throws ConfigInvalidException {
@@ -487,10 +492,6 @@
       throw parseException("patch set %s requires an identified user as uploader", psId.get());
     }
     if (patchSetCommitParsed(psId)) {
-      if (deletedPatchSets.contains(psId)) {
-        // Do not update PS details as PS was deleted and this meta data is of no relevance.
-        return;
-      }
       ObjectId commitId = patchSets.get(psId).commitId().orElseThrow(IllegalStateException::new);
       throw new ConfigInvalidException(
           String.format(
diff --git a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
index f3c8eab..71b35d1 100644
--- a/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
+++ b/java/com/google/gerrit/server/patch/PatchScriptBuilder.java
@@ -53,6 +53,7 @@
 import org.eclipse.jgit.treewalk.TreeWalk;
 
 class PatchScriptBuilder {
+
   static final int MAX_CONTEXT = 5000000;
   static final int BIG_FILE = 9000;
 
@@ -66,10 +67,6 @@
   private ComparisonType comparisonType;
   private ObjectId aId;
   private ObjectId bId;
-
-  private final Side a;
-  private final Side b;
-
   private List<Edit> edits;
   private final FileTypeRegistry registry;
   private final PatchListCache patchListCache;
@@ -77,8 +74,6 @@
 
   @Inject
   PatchScriptBuilder(FileTypeRegistry ftr, PatchListCache plc) {
-    a = new Side();
-    b = new Side();
     registry = ftr;
     patchListCache = plc;
   }
@@ -124,11 +119,9 @@
     boolean intralineFailure = false;
     boolean intralineTimeout = false;
 
-    a.path = oldName(content);
-    b.path = newName(content);
-
-    a.resolve(null, aId);
-    b.resolve(a, bId);
+    SideResolver resolver = new SideResolver();
+    Side a = resolver.resolve(oldName(content), null, aId);
+    Side b = resolver.resolve(newName(content), a, bId);
 
     edits = new ArrayList<>(content.getEdits());
     ImmutableSet<Edit> editsDueToRebase = content.getEditsDueToRebase();
@@ -161,7 +154,7 @@
       }
     }
 
-    correctForDifferencesInNewlineAtEnd();
+    correctForDifferencesInNewlineAtEnd(a, b);
 
     if (comments != null) {
       ensureCommentsVisible(comments);
@@ -193,7 +186,7 @@
       //
       context = MAX_CONTEXT;
 
-      packContent(diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE);
+      packContent(a, b, diffPrefs.ignoreWhitespace != Whitespace.IGNORE_NONE);
     }
 
     return new PatchScript(
@@ -267,7 +260,7 @@
     }
   }
 
-  private void correctForDifferencesInNewlineAtEnd() {
+  private void correctForDifferencesInNewlineAtEnd(Side a, Side b) {
     // a.src.size() is the size ignoring a newline at the end whereas a.size() considers it.
     int aSize = a.src.size();
     int bSize = b.src.size();
@@ -280,7 +273,7 @@
     }
 
     Optional<Edit> lastEdit = getLast(edits);
-    if (isNewlineAtEndDeleted()) {
+    if (isNewlineAtEndDeleted(a, b)) {
       Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndA() == aSize);
       if (lastLineEdit.isPresent()) {
         lastLineEdit.get().extendA();
@@ -288,7 +281,7 @@
         Edit newlineEdit = new Edit(aSize, aSize + 1, bSize, bSize);
         edits.add(newlineEdit);
       }
-    } else if (isNewlineAtEndAdded()) {
+    } else if (isNewlineAtEndAdded(a, b)) {
       Optional<Edit> lastLineEdit = lastEdit.filter(edit -> edit.getEndB() == bSize);
       if (lastLineEdit.isPresent()) {
         lastLineEdit.get().extendB();
@@ -303,11 +296,11 @@
     return list.isEmpty() ? Optional.empty() : Optional.ofNullable(list.get(list.size() - 1));
   }
 
-  private boolean isNewlineAtEndDeleted() {
+  private boolean isNewlineAtEndDeleted(Side a, Side b) {
     return !a.src.isMissingNewlineAtEnd() && b.src.isMissingNewlineAtEnd();
   }
 
-  private boolean isNewlineAtEndAdded() {
+  private boolean isNewlineAtEndAdded(Side a, Side b) {
     return a.src.isMissingNewlineAtEnd() && !b.src.isMissingNewlineAtEnd();
   }
 
@@ -425,7 +418,7 @@
     return last.getEndA() + (b - last.getEndB());
   }
 
-  private void packContent(boolean ignoredWhitespace) {
+  private void packContent(Side a, Side b, boolean ignoredWhitespace) {
     EditList list = new EditList(edits, context, a.size(), b.size());
     for (EditList.Hunk hunk : list.getHunks()) {
       while (hunk.next()) {
@@ -459,16 +452,38 @@
     }
   }
 
-  private class Side {
-    String path;
-    ObjectId id;
-    FileMode mode;
-    byte[] srcContent;
-    Text src;
-    MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE;
-    DisplayMethod displayMethod = DisplayMethod.DIFF;
-    PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
-    final SparseFileContent dst = new SparseFileContent();
+  private static class Side {
+
+    final String path;
+    final ObjectId id;
+    final FileMode mode;
+    final byte[] srcContent;
+    final Text src;
+    final MimeType mimeType;
+    final DisplayMethod displayMethod;
+    final PatchScript.FileMode fileMode;
+    final SparseFileContent dst;
+
+    public Side(
+        String path,
+        ObjectId id,
+        FileMode mode,
+        byte[] srcContent,
+        Text src,
+        MimeType mimeType,
+        DisplayMethod displayMethod,
+        PatchScript.FileMode fileMode) {
+      this.path = path;
+      this.id = id;
+      this.mode = mode;
+      this.srcContent = srcContent;
+      this.src = src;
+      this.mimeType = mimeType;
+      this.displayMethod = displayMethod;
+      this.fileMode = fileMode;
+      dst = new SparseFileContent();
+      dst.setSize(size());
+    }
 
     int size() {
       if (src == null) {
@@ -488,58 +503,60 @@
     String getSourceLine(int lineNumber) {
       return lineNumber >= src.size() ? "" : src.getString(lineNumber);
     }
+  }
 
-    void resolve(Side other, ObjectId within) throws IOException {
+  private class SideResolver {
+
+    Side resolve(final String path, final Side other, final ObjectId within) throws IOException {
       try {
-        final boolean reuse;
-        if (Patch.COMMIT_MSG.equals(path)) {
+        boolean isCommitMsg = Patch.COMMIT_MSG.equals(path);
+        boolean isMergeList = Patch.MERGE_LIST.equals(path);
+        if (isCommitMsg || isMergeList) {
           if (comparisonType.isAgainstParentOrAutoMerge() && Objects.equals(aId, within)) {
-            id = ObjectId.zeroId();
-            src = Text.EMPTY;
-            srcContent = Text.NO_BYTES;
-            mode = FileMode.MISSING;
-            displayMethod = DisplayMethod.NONE;
+            return createSide(
+                path,
+                ObjectId.zeroId(),
+                FileMode.MISSING,
+                Text.NO_BYTES,
+                Text.EMPTY,
+                MimeUtil2.UNKNOWN_MIME_TYPE,
+                DisplayMethod.NONE,
+                false);
           } else {
-            id = within;
-            src = Text.forCommit(reader, within);
-            srcContent = src.getContent();
+            Text src =
+                isCommitMsg
+                    ? Text.forCommit(reader, within)
+                    : Text.forMergeList(comparisonType, reader, within);
+            byte[] srcContent = src.getContent();
+            DisplayMethod displayMethod;
+            FileMode mode;
             if (src == Text.EMPTY) {
               mode = FileMode.MISSING;
               displayMethod = DisplayMethod.NONE;
             } else {
               mode = FileMode.REGULAR_FILE;
+              displayMethod = DisplayMethod.DIFF;
             }
+            return createSide(
+                path,
+                within,
+                mode,
+                srcContent,
+                src,
+                MimeUtil2.UNKNOWN_MIME_TYPE,
+                displayMethod,
+                false);
           }
-          reuse = false;
-        } else if (Patch.MERGE_LIST.equals(path)) {
-          if (comparisonType.isAgainstParentOrAutoMerge() && Objects.equals(aId, within)) {
-            id = ObjectId.zeroId();
-            src = Text.EMPTY;
-            srcContent = Text.NO_BYTES;
-            mode = FileMode.MISSING;
-            displayMethod = DisplayMethod.NONE;
-          } else {
-            id = within;
-            src = Text.forMergeList(comparisonType, reader, within);
-            srcContent = src.getContent();
-            if (src == Text.EMPTY) {
-              mode = FileMode.MISSING;
-              displayMethod = DisplayMethod.NONE;
-            } else {
-              mode = FileMode.REGULAR_FILE;
-            }
-          }
-          reuse = false;
         } else {
-          final TreeWalk tw = find(within);
-
-          id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
-          mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
-          reuse =
+          final TreeWalk tw = find(path, within);
+          ObjectId id = tw != null ? tw.getObjectId(0) : ObjectId.zeroId();
+          FileMode mode = tw != null ? tw.getFileMode(0) : FileMode.MISSING;
+          boolean reuse =
               other != null
                   && other.id.equals(id)
                   && (other.mode == mode || isBothFile(other.mode, mode));
-
+          Text src = null;
+          byte[] srcContent;
           if (reuse) {
             srcContent = other.srcContent;
 
@@ -553,7 +570,8 @@
           } else {
             srcContent = Text.NO_BYTES;
           }
-
+          MimeType mimeType = MimeUtil2.UNKNOWN_MIME_TYPE;
+          DisplayMethod displayMethod = DisplayMethod.DIFF;
           if (reuse) {
             mimeType = other.mimeType;
             displayMethod = other.displayMethod;
@@ -565,33 +583,42 @@
               displayMethod = DisplayMethod.IMG;
             }
           }
-        }
-
-        if (mode == FileMode.MISSING) {
-          displayMethod = DisplayMethod.NONE;
-        }
-
-        if (!reuse) {
-          if (srcContent == Text.NO_BYTES) {
-            src = Text.EMPTY;
-          } else {
-            src = new Text(srcContent);
-          }
-        }
-
-        dst.setSize(size());
-
-        if (mode == FileMode.SYMLINK) {
-          fileMode = PatchScript.FileMode.SYMLINK;
-        } else if (mode == FileMode.GITLINK) {
-          fileMode = PatchScript.FileMode.GITLINK;
+          return createSide(path, id, mode, srcContent, src, mimeType, displayMethod, reuse);
         }
       } catch (IOException err) {
         throw new IOException("Cannot read " + within.name() + ":" + path, err);
       }
     }
 
-    private TreeWalk find(ObjectId within)
+    private Side createSide(
+        String path,
+        ObjectId id,
+        FileMode mode,
+        byte[] srcContent,
+        Text src,
+        MimeType mimeType,
+        DisplayMethod displayMethod,
+        boolean reuse) {
+      if (!reuse) {
+        if (srcContent == Text.NO_BYTES) {
+          src = Text.EMPTY;
+        } else {
+          src = new Text(srcContent);
+        }
+      }
+      if (mode == FileMode.MISSING) {
+        displayMethod = DisplayMethod.NONE;
+      }
+      PatchScript.FileMode fileMode = PatchScript.FileMode.FILE;
+      if (mode == FileMode.SYMLINK) {
+        fileMode = PatchScript.FileMode.SYMLINK;
+      } else if (mode == FileMode.GITLINK) {
+        fileMode = PatchScript.FileMode.GITLINK;
+      }
+      return new Side(path, id, mode, srcContent, src, mimeType, displayMethod, fileMode);
+    }
+
+    private TreeWalk find(String path, ObjectId within)
         throws MissingObjectException, IncorrectObjectTypeException, CorruptObjectException,
             IOException {
       if (path == null || within == null) {
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index 758cf47..acc6465 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -170,28 +170,16 @@
       BatchUpdate.Factory updateFactory, TopLevelResource parent, ChangeInput input)
       throws IOException, InvalidChangeOperationException, RestApiException, UpdateException,
           PermissionBackendException, ConfigInvalidException {
-    if (Strings.isNullOrEmpty(input.project)) {
-      throw new BadRequestException("project must be non-empty");
-    }
-
-    return execute(updateFactory, input, projectsCollection.parse(input.project));
-  }
-
-  /** Creates the changes in the given project. This is public for reuse in the project API. */
-  public Response<ChangeInfo> execute(
-      BatchUpdate.Factory updateFactory, ChangeInput input, ProjectResource projectResource)
-      throws IOException, RestApiException, UpdateException, PermissionBackendException,
-          ConfigInvalidException {
     if (!user.get().isIdentifiedUser()) {
       throw new AuthException("Authentication required");
     }
-
-    ProjectState projectState = projectResource.getProjectState();
-    projectState.checkStatePermitsWrite();
-
     IdentifiedUser me = user.get().asIdentifiedUser();
     checkAndSanitizeChangeInput(input, me);
 
+    ProjectResource projectResource = projectsCollection.parse(input.project);
+    ProjectState projectState = projectResource.getProjectState();
+    projectState.checkStatePermitsWrite();
+
     Project.NameKey project = projectResource.getNameKey();
     contributorAgreements.check(project, user.get());
 
@@ -214,6 +202,10 @@
    */
   private void checkAndSanitizeChangeInput(ChangeInput input, IdentifiedUser me)
       throws RestApiException, PermissionBackendException, IOException {
+    if (Strings.isNullOrEmpty(input.project)) {
+      throw new BadRequestException("project must be non-empty");
+    }
+
     if (Strings.isNullOrEmpty(input.branch)) {
       throw new BadRequestException("branch must be non-empty");
     }
diff --git a/java/com/google/gerrit/server/restapi/project/CreateChange.java b/java/com/google/gerrit/server/restapi/project/CreateChange.java
deleted file mode 100644
index de11ffe..0000000
--- a/java/com/google/gerrit/server/restapi/project/CreateChange.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2019 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.project;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.exceptions.InvalidNameException;
-import com.google.gerrit.extensions.common.ChangeInfo;
-import com.google.gerrit.extensions.common.ChangeInput;
-import com.google.gerrit.extensions.restapi.AuthException;
-import com.google.gerrit.extensions.restapi.BadRequestException;
-import com.google.gerrit.extensions.restapi.Response;
-import com.google.gerrit.extensions.restapi.RestApiException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.permissions.PermissionBackendException;
-import com.google.gerrit.server.project.InvalidChangeOperationException;
-import com.google.gerrit.server.project.ProjectResource;
-import com.google.gerrit.server.update.BatchUpdate;
-import com.google.gerrit.server.update.RetryHelper;
-import com.google.gerrit.server.update.RetryingRestModifyView;
-import com.google.gerrit.server.update.UpdateException;
-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;
-
-@Singleton
-public class CreateChange extends RetryingRestModifyView<ProjectResource, ChangeInput, ChangeInfo> {
-  private final com.google.gerrit.server.restapi.change.CreateChange changeCreateChange;
-  private final Provider<CurrentUser> user;
-
-  @Inject
-  public CreateChange(
-      RetryHelper retryHelper,
-      Provider<CurrentUser> user,
-      com.google.gerrit.server.restapi.change.CreateChange changeCreateChange) {
-    super(retryHelper);
-    this.changeCreateChange = changeCreateChange;
-    this.user = user;
-  }
-
-  @Override
-  public Response<ChangeInfo> applyImpl(
-      BatchUpdate.Factory updateFactory, ProjectResource rsrc, ChangeInput input)
-      throws PermissionBackendException, IOException, ConfigInvalidException,
-          InvalidChangeOperationException, InvalidNameException, UpdateException, RestApiException {
-    if (!user.get().isIdentifiedUser()) {
-      throw new AuthException("Authentication required");
-    }
-
-    if (!Strings.isNullOrEmpty(input.project)) {
-      throw new BadRequestException("may not specify project");
-    }
-
-    input.project = rsrc.getName();
-    return changeCreateChange.execute(updateFactory, input, rsrc);
-  }
-}
diff --git a/java/com/google/gerrit/server/restapi/project/Module.java b/java/com/google/gerrit/server/restapi/project/Module.java
index 4ed21cc..2c76cbd 100644
--- a/java/com/google/gerrit/server/restapi/project/Module.java
+++ b/java/com/google/gerrit/server/restapi/project/Module.java
@@ -85,7 +85,6 @@
 
     child(PROJECT_KIND, "branches").to(BranchesCollection.class);
     create(BRANCH_KIND).to(CreateBranch.class);
-    post(PROJECT_KIND, "create.change").to(CreateChange.class);
     put(BRANCH_KIND).to(PutBranch.class);
     get(BRANCH_KIND).to(GetBranch.class);
     delete(BRANCH_KIND).to(DeleteBranch.class);
diff --git a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
index c2577e7..9a7ced5 100644
--- a/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
+++ b/java/com/google/gerrit/server/submit/MergeOpRepoManager.java
@@ -42,10 +42,9 @@
 import java.util.Map;
 import java.util.Objects;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.ObjectReader;
-import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevSort;
@@ -130,19 +129,17 @@
   }
 
   public static class OpenBranch {
-    final RefUpdate update;
     final CodeReviewCommit oldTip;
     MergeTip mergeTip;
 
     OpenBranch(OpenRepo or, BranchNameKey name) throws IntegrationException {
       try {
-        update = or.repo.updateRef(name.branch());
-        if (update.getOldObjectId() != null) {
-          oldTip = or.rw.parseCommit(update.getOldObjectId());
+        Ref ref = or.getRepo().exactRef(name.branch());
+        if (ref != null) {
+          oldTip = or.rw.parseCommit(ref.getObjectId());
         } else if (Objects.equals(or.repo.getFullBranch(), name.branch())
             || Objects.equals(RefNames.REFS_CONFIG, name.branch())) {
           oldTip = null;
-          update.setExpectedOldObjectId(ObjectId.zeroId());
         } else {
           throw new IntegrationException(
               "The destination branch " + name + " does not exist anymore.");
diff --git a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
index 02557cc..b8ab752 100644
--- a/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/binding/ProjectsRestApiBindingsIT.java
@@ -70,7 +70,6 @@
           RestCall.get("/projects/%s/statistics.git"),
           RestCall.post("/projects/%s/index"),
           RestCall.post("/projects/%s/gc"),
-          RestCall.post("/projects/%s/create.change"),
           RestCall.get("/projects/%s/children"),
           RestCall.get("/projects/%s/branches"),
           RestCall.post("/projects/%s/branches:delete"),
diff --git a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
deleted file mode 100644
index ca3707d..0000000
--- a/javatests/com/google/gerrit/acceptance/rest/project/CreateChangeIT.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2019 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.acceptance.rest.project;
-
-import static com.google.common.truth.Truth8.assertThat;
-import static com.google.gerrit.entities.RefNames.REFS_HEADS;
-
-import com.google.gerrit.acceptance.AbstractDaemonTest;
-import com.google.gerrit.acceptance.RestResponse;
-import com.google.gerrit.extensions.api.projects.BranchInput;
-import com.google.gerrit.extensions.common.ChangeInput;
-import org.junit.Test;
-
-public class CreateChangeIT extends AbstractDaemonTest {
-
-  // Just a basic test. The real functionality is tested under the restapi.change acceptance tests.
-  @Test
-  public void basic() throws Exception {
-    BranchInput branchInput = new BranchInput();
-    branchInput.ref = "foo";
-    assertThat(gApi.projects().name(project.get()).branches().get().stream().map(i -> i.ref))
-        .doesNotContain(REFS_HEADS + branchInput.ref);
-    RestResponse r =
-        adminRestSession.put(
-            "/projects/" + project.get() + "/branches/" + branchInput.ref, branchInput);
-    r.assertCreated();
-
-    ChangeInput input = new ChangeInput();
-    input.branch = "foo";
-    input.subject = "subject";
-    RestResponse cr = adminRestSession.post("/projects/" + project.get() + "/create.change", input);
-    cr.assertCreated();
-  }
-}
diff --git a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
index 5993206..145e914 100644
--- a/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
+++ b/javatests/com/google/gerrit/server/notedb/ChangeNotesTest.java
@@ -3062,6 +3062,38 @@
     assertThat(newNotes(c).getUpdateCount()).isEqualTo(3);
   }
 
+  @Test
+  public void createPatchSetAfterPatchSetDeletion() throws Exception {
+    Change c = newChange();
+    assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(1);
+
+    // Create PS2.
+    incrementCurrentPatchSetFieldOnly(c);
+    RevCommit commit = tr.commit().message("PS" + c.currentPatchSetId().get()).create();
+    ChangeUpdate update = newUpdate(c, changeOwner);
+    update.setCommit(rw, commit);
+    update.setGroups(ImmutableList.of(commit.name()));
+    update.commit();
+    assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2);
+
+    // Delete PS2.
+    update = newUpdate(c, changeOwner);
+    update.setPatchSetState(PatchSetState.DELETED);
+    update.commit();
+    c = newNotes(c).getChange();
+    assertThat(c.currentPatchSetId().get()).isEqualTo(1);
+
+    // Create another PS2
+    incrementCurrentPatchSetFieldOnly(c);
+    commit = tr.commit().message("PS" + c.currentPatchSetId().get()).create();
+    update = newUpdate(c, changeOwner);
+    update.setPatchSetState(PatchSetState.PUBLISHED);
+    update.setCommit(rw, commit);
+    update.setGroups(ImmutableList.of(commit.name()));
+    update.commit();
+    assertThat(newNotes(c).getChange().currentPatchSetId().get()).isEqualTo(2);
+  }
+
   private String readNote(ChangeNotes notes, ObjectId noteId) throws Exception {
     ObjectId dataId = notes.revisionNoteMap.noteMap.getNote(noteId).getData();
     return new String(rw.getObjectReader().open(dataId, OBJ_BLOB).getCachedBytes(), UTF_8);
diff --git a/polygerrit-ui/app/elements/gr-app-element.js b/polygerrit-ui/app/elements/gr-app-element.js
index 2a73be3..cf18241 100644
--- a/polygerrit-ui/app/elements/gr-app-element.js
+++ b/polygerrit-ui/app/elements/gr-app-element.js
@@ -427,7 +427,7 @@
 
     _computePluginScreenName({plugin, screen}) {
       if (!plugin || !screen) return '';
-      return `${plugin.getPluginName()}-screen-${screen}`;
+      return `${plugin}-screen-${screen}`;
     },
 
     _logWelcome() {
diff --git a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
index c84a113..80462d3 100644
--- a/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
+++ b/polygerrit-ui/app/elements/shared/gr-rest-api-interface/gr-rest-api-interface.js
@@ -1776,8 +1776,9 @@
         opt_workInProgress, opt_baseChange, opt_baseCommit) {
       return this._restApiHelper.send({
         method: 'POST',
-        url: `/projects/${encodeURIComponent(project)}/create.change`,
+        url: '/changes/',
         body: {
+          project,
           branch,
           subject,
           topic: opt_topic,
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
index 43b4a16..261ddda 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea.js
@@ -20,32 +20,32 @@
   const MAX_ITEMS_DROPDOWN = 10;
 
   const ALL_SUGGESTIONS = [
+    {value: '😊', match: 'smile :)'},
+    {value: '👍', match: 'thumbs up'},
+    {value: '😄', match: 'laugh :D'},
+    {value: '🎉', match: 'party'},
+    {value: '😞', match: 'sad :('},
+    {value: '😂', match: 'tears :\')'},
+    {value: '🙏', match: 'pray'},
+    {value: '😐', match: 'neutral :|'},
+    {value: '😮', match: 'shock :O'},
+    {value: '👎', match: 'thumbs down'},
+    {value: '😎', match: 'cool |;)'},
+    {value: '😕', match: 'confused'},
+    {value: '👌', match: 'ok'},
+    {value: '🔥', match: 'fire'},
+    {value: '👊', match: 'fistbump'},
     {value: '💯', match: '100'},
     {value: '💔', match: 'broken heart'},
     {value: '🍺', match: 'beer'},
     {value: '✔', match: 'check'},
-    {value: '😎', match: 'cool |;)'},
-    {value: '😕', match: 'confused'},
+    {value: '😋', match: 'tongue'},
     {value: '😭', match: 'crying :\'('},
-    {value: '🔥', match: 'fire'},
-    {value: '👊', match: 'fistbump'},
     {value: '🐨', match: 'koala'},
-    {value: '😄', match: 'laugh :D'},
     {value: '🤓', match: 'glasses'},
     {value: '😆', match: 'grin'},
-    {value: '😐', match: 'neutral :|'},
-    {value: '👌', match: 'ok'},
-    {value: '🎉', match: 'party'},
     {value: '💩', match: 'poop'},
-    {value: '🙏', match: 'pray'},
-    {value: '😞', match: 'sad :('},
-    {value: '😮', match: 'shock :O'},
-    {value: '😊', match: 'smile :)'},
     {value: '😢', match: 'tear'},
-    {value: '😂', match: 'tears :\')'},
-    {value: '😋', match: 'tongue'},
-    {value: '👍', match: 'thumbs up'},
-    {value: '👎', match: 'thumbs down'},
     {value: '😒', match: 'unamused'},
     {value: '😉', match: 'wink ;)'},
     {value: '🍷', match: 'wine'},
diff --git a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
index d9078b6..4c59f50 100644
--- a/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
+++ b/polygerrit-ui/app/elements/shared/gr-textarea/gr-textarea_test.html
@@ -179,9 +179,10 @@
       element._determineSuggestions(emojiText);
       assert.isTrue(formatSpy.called);
       assert.isTrue(formatSpy.lastCall.calledWithExactly(
-          [{dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
-            {dataValue: '😂', value: '😂', match: 'tears :\')',
-              text: '😂 tears :\')'}]));
+          [{dataValue: '😂', value: '😂', match: 'tears :\')',
+            text: '😂 tears :\')'},
+            {dataValue: '😢', value: '😢', match: 'tear', text: '😢 tear'},
+          ]));
     });
 
     test('_formatSuggestions', () => {
diff --git a/polygerrit-ui/app/styles/fonts.css b/polygerrit-ui/app/styles/fonts.css
index 41aec27..c837492 100644
--- a/polygerrit-ui/app/styles/fonts.css
+++ b/polygerrit-ui/app/styles/fonts.css
@@ -34,7 +34,7 @@
   font-family: 'Roboto';
   font-style: normal;
   font-weight: 400;
-  src: local('Roboto'), local('RobotoMono-Regular'),
+  src: local('Roboto'), local('Roboto-Regular'),
        url('../fonts/Roboto-Regular.woff2') format('woff2'),
        url('../fonts/Roboto-Regular.woff') format('woff');
   unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;