Merge "groups_bysubgroups: Batch loading values"
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index 716fa2f..f46c821 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -380,6 +380,14 @@
log in to Gerrit, then Gerrit will update the external ID record with
the new email address.
+=== Transition from LDAP to Google OAuth
+
+When authentication is changed from LDAP to Google Oauth gerrit will automatically
+adjust the external IDs in the `refs/meta/external-ids` branch. Gerrit will re-use
+the same account ID that was used by the LDAP account. Transition to other OAuth
+mechanisms will fail and require manual changes to the `refs/meta/external-ids` branch.
+The LDAP e-mail and Google OAuth e-mail must be the same.
+
[[starred-changes]]
== Starred Changes
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
index 0162c48..8586840 100644
--- a/Documentation/rest-api-changes.txt
+++ b/Documentation/rest-api-changes.txt
@@ -3012,7 +3012,7 @@
'GET /changes/link:#change-id[\{change-id\}]/edit
--
-Retrieves a change edit details.
+Retrieves the details of the change edit done by the caller to the given change.
.Request
----
@@ -6751,7 +6751,8 @@
|`patch` |required|
The details of the patch to be applied as a link:#applypatch-input[ApplyPatchInput] entity.
|`commit_message` |optional|
-The commit message for the new patch set. If not specified, a predefined message will be used.
+The commit message for the new patch set. If not specified, the latest patch-set message will be
+used.
|`base` |optional|
40-hex digit SHA-1 of the commit which will be the parent commit of the newly created patch set.
If set, it must be a merged commit or a change revision on the destination branch.
@@ -8139,9 +8140,7 @@
Rebasing on behalf of the uploader is only supported for trivial rebases.
This means this option cannot be combined with the `allow_conflicts` option. +
In addition, rebasing on behalf of the uploader is only supported for the
-current patch set of a change and not when rebasing a chain. +
-Using this option is not supported when rebasing a chain via the
-link:#rebase-chain[Rebase Chain] REST endpoint. +
+current patch set of a change. +
If the caller is the uploader this flag is ignored and a normal rebase is done.
|`validation_options` |optional|
Map with key-value pairs that are forwarded as options to the commit validation
diff --git a/Documentation/user-inline-edit.txt b/Documentation/user-inline-edit.txt
index 4a9d18f..e3e0f68 100644
--- a/Documentation/user-inline-edit.txt
+++ b/Documentation/user-inline-edit.txt
@@ -173,7 +173,7 @@
[[search-for-changes]]
== Searching for Changes with Pending Edits
-To find changes with pending edits:
+To find changes with pending edits created by you:
* From the Gerrit dashboard, select Your > Changes. All your changes are
listed, according to Work in progress, Outgoing reviews, Incoming reviews,
@@ -183,6 +183,12 @@
link:user-search.html[Searching Changes]. For example, to find only
those changes that contain edits, see link:user-search.html#has[has:edit].
+[NOTE]
+Though edits created by others are not accessible from the Gerrit UI, edits
+are not considered to be private data, and are stored in non-encrypted special
+branches under the target repository. As such, they can be accessed by users who
+have access to the repository.
+
[[change-edit-actions]]
== Modifying Changes
diff --git a/java/com/google/gerrit/server/account/AccountDelta.java b/java/com/google/gerrit/server/account/AccountDelta.java
index fac3233..8f285b5 100644
--- a/java/com/google/gerrit/server/account/AccountDelta.java
+++ b/java/com/google/gerrit/server/account/AccountDelta.java
@@ -161,6 +161,11 @@
*/
public abstract Optional<EditPreferencesInfo> getEditPreferences();
+ public boolean hasExternalIdUpdates() {
+ return !this.getCreatedExternalIds().isEmpty()
+ || !this.getDeletedExternalIds().isEmpty()
+ || !this.getUpdatedExternalIds().isEmpty();
+ }
/**
* Class to build an {@link AccountDelta}.
*
diff --git a/java/com/google/gerrit/server/account/AccountManager.java b/java/com/google/gerrit/server/account/AccountManager.java
index 891a467..edec52c 100644
--- a/java/com/google/gerrit/server/account/AccountManager.java
+++ b/java/com/google/gerrit/server/account/AccountManager.java
@@ -16,6 +16,8 @@
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GOOGLE_OAUTH;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
import com.google.common.annotations.VisibleForTesting;
@@ -146,10 +148,7 @@
try {
Optional<ExternalId> optionalExtId = externalIds.get(who.getExternalIdKey());
if (!optionalExtId.isPresent()) {
- logger.atFine().log(
- "External ID for account %s not found. A new account will be automatically created.",
- who.getUserName());
- return create(who);
+ return createOrLinkAccount(who);
}
ExternalId extId = optionalExtId.get();
@@ -180,6 +179,46 @@
}
}
+ /**
+ * Determines if a new account should be created or if we should link to an existing account.
+ *
+ * @param who identity of the user, with any details we received about them.
+ * @return the result of authenticating the user.
+ * @throws AccountException the account does not exist, and cannot be created, or exists, but
+ * cannot be located, is unable to be activated or deactivated, or is inactive, or cannot be
+ * added to the admin group (only for the first account).
+ */
+ private AuthResult createOrLinkAccount(AuthRequest who)
+ throws AccountException, IOException, ConfigInvalidException {
+ // TODO: in case of extension of further migration paths this code should
+ // probably be refactored out by creating an AccountMigrator extension point.
+ if (who.getExternalIdKey().isScheme(SCHEME_GOOGLE_OAUTH)) {
+ Optional<ExternalId> existingLDAPExtID = findLdapExternalId(who);
+ if (existingLDAPExtID.isPresent()) {
+ return migrateLdapAccountToOauth(who, existingLDAPExtID.get());
+ }
+ }
+ logger.atFine().log(
+ "External ID for account %s not found. A new account will be automatically created.",
+ who.getEmailAddress());
+ return create(who);
+ }
+
+ private AuthResult migrateLdapAccountToOauth(AuthRequest who, ExternalId ldapExternalId)
+ throws AccountException, IOException, ConfigInvalidException {
+ Account.Id extAccId = ldapExternalId.accountId();
+ AuthResult res = link(extAccId, who);
+ accountsUpdateProvider
+ .get()
+ .update(
+ "remove existing LDAP externalId with matching e-mail",
+ extAccId,
+ u -> {
+ u.deleteExternalId(ldapExternalId);
+ });
+ return res;
+ }
+
private void deactivateAccountIfItExists(AuthRequest authRequest) {
if (!shouldUpdateActiveStatus(authRequest)) {
return;
@@ -277,6 +316,17 @@
}
}
+ private Optional<ExternalId> findLdapExternalId(AuthRequest who) throws IOException {
+ String email = who.getEmailAddress();
+ if (email == null || email.isEmpty()) {
+ return Optional.empty();
+ }
+
+ Optional<ExternalId> ldapExternalId =
+ externalIds.byEmail(email).stream().filter(a -> a.isScheme(SCHEME_GERRIT)).findFirst();
+ return ldapExternalId;
+ }
+
private AuthResult create(AuthRequest who)
throws AccountException, IOException, ConfigInvalidException {
Account.Id newId = Account.id(sequences.nextAccountId());
diff --git a/java/com/google/gerrit/server/account/AccountsUpdate.java b/java/com/google/gerrit/server/account/AccountsUpdate.java
index 1f2bc8c..b706bca 100644
--- a/java/com/google/gerrit/server/account/AccountsUpdate.java
+++ b/java/com/google/gerrit/server/account/AccountsUpdate.java
@@ -26,6 +26,7 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Account;
import com.google.gerrit.exceptions.DuplicateKeyException;
import com.google.gerrit.exceptions.StorageException;
@@ -201,7 +202,7 @@
private final Runnable beforeCommit;
/** Single instance that accumulates updates from the batch. */
- private ExternalIdNotes externalIdNotes;
+ @Nullable private ExternalIdNotes externalIdNotes;
@AssistedInject
AccountsUpdate(
@@ -402,13 +403,17 @@
delta.getDeletedExternalIds()),
updateArguments.accountId);
- if (externalIdNotes == null) {
- externalIdNotes =
- extIdNotesLoader.load(
- repo, accountConfig.getExternalIdsRev().orElse(ObjectId.zeroId()));
+ if (delta.hasExternalIdUpdates()) {
+ // Only load the externalIds if they are going to be updated
+ // This makes e.g. preferences updates faster.
+ if (externalIdNotes == null) {
+ externalIdNotes =
+ extIdNotesLoader.load(
+ repo, accountConfig.getExternalIdsRev().orElse(ObjectId.zeroId()));
+ }
+ externalIdNotes.replace(delta.getDeletedExternalIds(), delta.getCreatedExternalIds());
+ externalIdNotes.upsert(delta.getUpdatedExternalIds());
}
- externalIdNotes.replace(delta.getDeletedExternalIds(), delta.getCreatedExternalIds());
- externalIdNotes.upsert(delta.getUpdatedExternalIds());
CachedPreferences cachedDefaultPreferences =
CachedPreferences.fromConfig(VersionedDefaultPreferences.get(repo, allUsersName));
@@ -509,17 +514,22 @@
beforeCommit.run();
BatchRefUpdate batchRefUpdate = allUsersRepo.getRefDatabase().newBatchUpdate();
-
- String externalIdUpdateMessage =
- updatedAccounts.size() == 1
- ? Iterables.getOnlyElement(updatedAccounts).message
- : "Batch update for " + updatedAccounts.size() + " accounts";
- ObjectId oldExternalIdsRevision = externalIdNotes.getRevision();
- // These update the same ref, so they need to be stacked on top of one another using the same
- // ExternalIdNotes instance.
- RevCommit revCommit =
- commitExternalIdUpdates(externalIdUpdateMessage, allUsersRepo, batchRefUpdate);
- boolean externalIdsUpdated = !Objects.equals(revCommit.getId(), oldExternalIdsRevision);
+ // External ids may be not updated if:
+ // * externalIdNotes is not loaded (there were no externalId updates in the delta)
+ // * new revCommit is identical to the previous externalId tip
+ boolean externalIdsUpdated = false;
+ if (externalIdNotes != null) {
+ String externalIdUpdateMessage =
+ updatedAccounts.size() == 1
+ ? Iterables.getOnlyElement(updatedAccounts).message
+ : "Batch update for " + updatedAccounts.size() + " accounts";
+ ObjectId oldExternalIdsRevision = externalIdNotes.getRevision();
+ // These update the same ref, so they need to be stacked on top of one another using the same
+ // ExternalIdNotes instance.
+ RevCommit revCommit =
+ commitExternalIdUpdates(externalIdUpdateMessage, allUsersRepo, batchRefUpdate);
+ externalIdsUpdated = !Objects.equals(revCommit.getId(), oldExternalIdsRevision);
+ }
for (UpdatedAccount updatedAccount : updatedAccounts) {
// These updates are all for different refs (because batches never update the same account
@@ -544,8 +554,10 @@
RefUpdateUtil.executeChecked(batchRefUpdate, allUsersRepo);
Set<Account.Id> accountsToSkipForReindex = getUpdatedAccountIds(batchRefUpdate);
- extIdNotesLoader.updateExternalIdCacheAndMaybeReindexAccounts(
- externalIdNotes, accountsToSkipForReindex);
+ if (externalIdsUpdated) {
+ extIdNotesLoader.updateExternalIdCacheAndMaybeReindexAccounts(
+ externalIdNotes, accountsToSkipForReindex);
+ }
gitRefUpdated.fire(
allUsersName, batchRefUpdate, currentUser.map(IdentifiedUser::state).orElse(null));
diff --git a/java/com/google/gerrit/server/account/AuthRequest.java b/java/com/google/gerrit/server/account/AuthRequest.java
index b4fbcdb..cf1e552 100644
--- a/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/java/com/google/gerrit/server/account/AuthRequest.java
@@ -16,6 +16,7 @@
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GOOGLE_OAUTH;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
import com.google.common.base.Strings;
@@ -66,6 +67,14 @@
return r;
}
+ public AuthRequest createForOAuthUser(String userName) {
+ AuthRequest r =
+ new AuthRequest(
+ externalIdKeyFactory.create(SCHEME_GOOGLE_OAUTH, userName), externalIdKeyFactory);
+ r.setUserName(userName);
+ return r;
+ }
+
/**
* Create a request for an email address registration.
*
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 1616198..9196db8 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -150,6 +150,9 @@
/** Scheme for xri resources. OpenID in particular makes use of these external IDs. */
public static final String SCHEME_XRI = "xri";
+ /** Scheme for Google OAuth external IDs. */
+ public static final String SCHEME_GOOGLE_OAUTH = "google-oauth";
+
@AutoValue
public abstract static class Key implements Serializable {
private static final long serialVersionUID = 1L;
diff --git a/java/com/google/gerrit/server/api/changes/ChangesImpl.java b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
index 0596524..6b107f1 100644
--- a/java/com/google/gerrit/server/api/changes/ChangesImpl.java
+++ b/java/com/google/gerrit/server/api/changes/ChangesImpl.java
@@ -21,6 +21,7 @@
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.gerrit.entities.Change;
+import com.google.gerrit.entities.Project;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.Changes;
import com.google.gerrit.extensions.client.ListChangesOption;
@@ -41,6 +42,7 @@
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.List;
+import org.eclipse.jgit.lib.ObjectId;
@Singleton
class ChangesImpl implements Changes {
@@ -101,7 +103,11 @@
public ChangeApi create(ChangeInput in) throws RestApiException {
try {
ChangeInfo out = createChange.apply(TopLevelResource.INSTANCE, in).value();
- return api.create(changes.parse(Change.id(out._number)));
+ return api.create(
+ changes.parse(
+ Project.nameKey(out.project),
+ Change.id(out._number),
+ ObjectId.fromString(out.metaRevId)));
} catch (Exception e) {
throw asRestApiException("Cannot create change", e);
}
diff --git a/java/com/google/gerrit/server/change/ReviewerModifier.java b/java/com/google/gerrit/server/change/ReviewerModifier.java
index b5e0181..49bd28a 100644
--- a/java/com/google/gerrit/server/change/ReviewerModifier.java
+++ b/java/com/google/gerrit/server/change/ReviewerModifier.java
@@ -113,6 +113,7 @@
private enum FailureType {
NOT_FOUND,
+ INACTIVE,
OTHER;
}
@@ -232,6 +233,9 @@
ReviewerModification byAccountId = byAccountId(input, notes, user);
+ if (byAccountId.isFailure() && byAccountId.failureType.equals(FailureType.INACTIVE)) {
+ return byAccountId;
+ }
ReviewerModification wholeGroup = null;
if (!byAccountId.exactMatchFound) {
wholeGroup = addWholeGroup(input, notes, user, confirmed, allowGroup, allowByEmail);
@@ -285,6 +289,13 @@
} else {
reviewerUser = accountResolver.resolveIncludeInactive(input.reviewer).asUniqueUser();
}
+ if (reviewerUser.getAccount().inactive()) {
+ return fail(
+ input,
+ FailureType.INACTIVE,
+ String.format(
+ "Cannot add inactive account %s, %s", reviewerUser.getAccountId(), input.reviewer));
+ }
if (input.reviewer.equalsIgnoreCase(reviewerUser.getName())
|| input.reviewer.equals(String.valueOf(reviewerUser.getAccountId()))) {
exactMatchFound = true;
diff --git a/java/com/google/gerrit/server/patch/DiffUtil.java b/java/com/google/gerrit/server/patch/DiffUtil.java
index 70a3208..79f5ab9 100644
--- a/java/com/google/gerrit/server/patch/DiffUtil.java
+++ b/java/com/google/gerrit/server/patch/DiffUtil.java
@@ -15,19 +15,29 @@
package com.google.gerrit.server.patch;
+import com.google.common.base.Strings;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ListMultimap;
+import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Patch.ChangeType;
+import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.server.patch.diff.ModifiedFilesCache;
import com.google.gerrit.server.patch.gitdiff.GitModifiedFilesCache;
import com.google.gerrit.server.patch.gitdiff.ModifiedFile;
import java.io.IOException;
+import java.io.OutputStream;
import java.util.Comparator;
import java.util.List;
+import java.util.Optional;
+import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.filter.PathFilter;
/**
* A utility class used by the diff cache interfaces {@link GitModifiedFilesCache} and {@link
@@ -116,6 +126,75 @@
return 0;
}
+ /**
+ * Get formatted diff between the given commits, either for a single path if specified, or for the
+ * full trees.
+ *
+ * @param repo to get the diff from
+ * @param baseCommit to compare with
+ * @param childCommit to compare
+ * @param path to narrow the diff to
+ * @param out to append the diff to
+ * @throws IOException if the diff couldn't be written
+ */
+ public static void getFormattedDiff(
+ Repository repo,
+ RevCommit baseCommit,
+ RevCommit childCommit,
+ @Nullable String path,
+ OutputStream out)
+ throws IOException {
+ getFormattedDiff(repo, null, baseCommit.getTree(), childCommit.getTree(), path, out);
+ }
+
+ public static void getFormattedDiff(
+ Repository repo,
+ @Nullable ObjectReader reader,
+ RevTree baseTree,
+ RevTree childTree,
+ @Nullable String path,
+ OutputStream out)
+ throws IOException {
+ try (DiffFormatter fmt = new DiffFormatter(out)) {
+ fmt.setRepository(repo);
+ if (reader != null) {
+ fmt.setReader(reader, repo.getConfig());
+ }
+ if (path != null) {
+ fmt.setPathFilter(PathFilter.create(path));
+ }
+ fmt.format(baseTree, childTree);
+ fmt.flush();
+ }
+ }
+
+ public static String cleanPatch(final String patch) {
+ String res = patch;
+ if (res.contains("\ndiff --git")) {
+ // Remove the header
+ res = res.substring(patch.lastIndexOf("\ndiff --git"), patch.length() - 1);
+ }
+ return res
+ // Remove "index NN..NN lines
+ .replaceAll("(?m)^index.*", "")
+ // Remove empty lines
+ .replaceAll("\n+", "\n")
+ // Trim
+ .trim();
+ }
+
+ public static Optional<String> getPatchHeader(final String patch) {
+ if (!patch.contains("\ndiff --git")) {
+ return Optional.empty();
+ }
+ return Optional.ofNullable(
+ Strings.emptyToNull(patch.trim().substring(0, patch.lastIndexOf("\ndiff --git"))));
+ }
+
+ public static String cleanPatch(BinaryResult bin) throws IOException {
+ return cleanPatch(bin.asString());
+ }
+
private static boolean isRootOrMergeCommit(RevCommit commit) {
return commit.getParentCount() != 1;
}
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
index f368eb9..da9810d 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatch.java
@@ -53,7 +53,6 @@
import com.google.gerrit.server.update.BatchUpdate;
import com.google.gerrit.server.update.UpdateException;
import com.google.gerrit.server.update.context.RefUpdateContext;
-import com.google.gerrit.server.util.CommitMessageUtil;
import com.google.gerrit.server.util.time.TimeUtil;
import com.google.inject.Inject;
import com.google.inject.Provider;
@@ -70,6 +69,7 @@
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
@Singleton
@@ -141,13 +141,14 @@
destChange.change().getStatus().name()));
}
+ RevCommit latestPatchset = revWalk.parseCommit(destChange.currentPatchSet().commitId());
+
RevCommit baseCommit;
if (!Strings.isNullOrEmpty(input.base)) {
baseCommit =
CommitUtil.getBaseCommit(
project.get(), queryProvider.get(), revWalk, destRef, input.base);
} else {
- RevCommit latestPatchset = revWalk.parseCommit(destChange.currentPatchSet().commitId());
if (latestPatchset.getParentCount() != 1) {
throw new BadRequestException(
String.format(
@@ -164,12 +165,17 @@
input.author == null
? committerIdent
: new PersonIdent(input.author.name, input.author.email, now, serverZoneId);
+ List<FooterLine> footerLines = latestPatchset.getFooterLines();
+ String messageWithNoFooters =
+ !Strings.isNullOrEmpty(input.commitMessage)
+ ? input.commitMessage
+ : removeFooters(latestPatchset.getFullMessage(), footerLines);
String commitMessage =
- CommitMessageUtil.checkAndSanitizeCommitMessage(
- input.commitMessage != null
- ? input.commitMessage
- : "The following patch was applied:\n>\t"
- + input.patch.patch.replaceAll("\n", "\n>\t"));
+ ApplyPatchUtil.buildCommitMessage(
+ messageWithNoFooters,
+ footerLines,
+ input.patch.patch,
+ ApplyPatchUtil.getResultPatch(repo, reader, baseCommit, revWalk.lookupTree(treeId)));
ObjectId appliedCommit =
CommitUtil.createCommitWithTree(
@@ -215,4 +221,11 @@
private static String buildMessageForPatchSet(PatchSet.Id psId) {
return new StringBuilder(String.format("Uploaded patch set %s.", psId.get())).toString();
}
+
+ private String removeFooters(String originalMessage, List<FooterLine> footerLines) {
+ if (footerLines.isEmpty()) {
+ return originalMessage;
+ }
+ return originalMessage.substring(0, originalMessage.indexOf(footerLines.get(0).getKey()));
+ }
}
diff --git a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
index 4021f77..74033c1 100644
--- a/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
+++ b/java/com/google/gerrit/server/restapi/change/ApplyPatchUtil.java
@@ -19,17 +19,26 @@
import com.google.gerrit.extensions.api.changes.ApplyPatchInput;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.server.patch.DiffUtil;
+import com.google.gerrit.server.util.CommitMessageUtil;
import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Optional;
import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang3.StringUtils;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.api.errors.PatchFormatException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.patch.PatchApplier;
+import org.eclipse.jgit.revwalk.FooterLine;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
@@ -52,12 +61,8 @@
throws IOException, RestApiException {
checkNotNull(mergeTip);
RevTree tip = mergeTip.getTree();
- InputStream patchStream;
- if (Base64.isBase64(input.patch)) {
- patchStream = new ByteArrayInputStream(org.eclipse.jgit.util.Base64.decode(input.patch));
- } else {
- patchStream = new ByteArrayInputStream(input.patch.getBytes(StandardCharsets.UTF_8));
- }
+ InputStream patchStream =
+ new ByteArrayInputStream(decodeIfNecessary(input.patch).getBytes(StandardCharsets.UTF_8));
try {
PatchApplier applier = new PatchApplier(repo, tip, oi);
PatchApplier.Result applyResult = applier.applyPatch(patchStream);
@@ -69,5 +74,88 @@
}
}
+ /**
+ * Build commit message for commits with applied patch.
+ *
+ * <p>Message structure:
+ *
+ * <ol>
+ * <li>Provided {@code message}.
+ * <li>In case the result change's patch is not the same as the original patch - a warning
+ * message which includes the diff; as well as the original patch's header if available, or
+ * the full original patch otherwise.
+ * <li>The provided {@code footerLines}, if any.
+ * </ol>
+ *
+ * @param message the first message piece, excluding footers
+ * @param footerLines footer lines to append to the message
+ * @param originalPatch to compare the result patch to
+ * @param resultPatch to validate accuracy for
+ * @return the commit message
+ * @throws BadRequestException if the commit message cannot be sanitized
+ */
+ public static String buildCommitMessage(
+ String message, List<FooterLine> footerLines, String originalPatch, String resultPatch)
+ throws BadRequestException {
+ String decodedOriginalPatch = decodeIfNecessary(originalPatch);
+ StringBuilder res = new StringBuilder(message.trim());
+ Optional<String> patchDiff = verifyAppliedPatch(decodedOriginalPatch, resultPatch);
+ if (!patchDiff.isEmpty()) {
+ res.append(
+ "\n\nNOTE FOR REVIEWERS - original patch and result patch are not identical."
+ + "\nPLEASE REVIEW CAREFULLY.\nDiffs between the patches:\n"
+ + patchDiff.get());
+ }
+ Optional<String> originalPatchHeader = DiffUtil.getPatchHeader(decodedOriginalPatch);
+ if (!patchDiff.isEmpty()) {
+ res.append(
+ "\n\nOriginal patch:\n"
+ + (originalPatchHeader.isEmpty() ? decodedOriginalPatch : originalPatchHeader.get()));
+ }
+ if (!footerLines.isEmpty()) {
+ res.append('\n');
+ }
+ for (FooterLine footer : footerLines) {
+ res.append("\n" + footer.toString());
+ }
+ return CommitMessageUtil.checkAndSanitizeCommitMessage(res.toString());
+ }
+
+ /**
+ * Fetch the patch of the result tree.
+ *
+ * @param repo in which the patch was applied
+ * @param reader for the repo objects, including {@code resultTree}
+ * @param baseCommit to generate patch against
+ * @param resultTree to generate the patch for
+ * @return the result patch
+ * @throws IOException if the result patch cannot be written
+ */
+ public static String getResultPatch(
+ Repository repo, ObjectReader reader, RevCommit baseCommit, RevTree resultTree)
+ throws IOException {
+ try (OutputStream resultPatchStream = new ByteArrayOutputStream()) {
+ DiffUtil.getFormattedDiff(
+ repo, reader, baseCommit.getTree(), resultTree, null, resultPatchStream);
+ return resultPatchStream.toString();
+ }
+ }
+
+ private static Optional<String> verifyAppliedPatch(String originalPatch, String resultPatch) {
+ String cleanOriginalPatch = DiffUtil.cleanPatch(originalPatch);
+ String cleanResultPatch = DiffUtil.cleanPatch(resultPatch);
+ if (cleanOriginalPatch.equals(cleanResultPatch)) {
+ return Optional.empty();
+ }
+ return Optional.of(StringUtils.difference(cleanOriginalPatch, cleanResultPatch));
+ }
+
+ private static String decodeIfNecessary(String patch) {
+ if (Base64.isBase64(patch)) {
+ return new String(org.eclipse.jgit.util.Base64.decode(patch), StandardCharsets.UTF_8);
+ }
+ return patch;
+ }
+
private ApplyPatchUtil() {}
}
diff --git a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
index a0c5b16..84cf209 100644
--- a/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
+++ b/java/com/google/gerrit/server/restapi/change/ChangesCollection.java
@@ -39,6 +39,7 @@
import java.io.IOException;
import java.util.List;
import java.util.Optional;
+import org.eclipse.jgit.lib.ObjectId;
@Singleton
public class ChangesCollection implements RestCollection<TopLevelResource, ChangeResource> {
@@ -49,6 +50,7 @@
private final ChangeResource.Factory changeResourceFactory;
private final PermissionBackend permissionBackend;
private final ProjectCache projectCache;
+ private final ChangeNotes.Factory changeNotesFactory;
@Inject
public ChangesCollection(
@@ -58,7 +60,9 @@
ChangeFinder changeFinder,
ChangeResource.Factory changeResourceFactory,
PermissionBackend permissionBackend,
- ProjectCache projectCache) {
+ ProjectCache projectCache,
+ ChangeNotes.Factory changeNotesFactory) {
+ this.changeNotesFactory = changeNotesFactory;
this.user = user;
this.queryFactory = queryFactory;
this.views = views;
@@ -78,6 +82,11 @@
return views;
}
+ /**
+ * Parses {@link ChangeResource} from {@link Change.Id}
+ *
+ * <p>Reads the change from index, since project is unknown.
+ */
@Override
public ChangeResource parse(TopLevelResource root, IdString id)
throws RestApiException, PermissionBackendException, IOException {
@@ -96,6 +105,28 @@
return changeResourceFactory.create(change, user.get());
}
+ /**
+ * Parses {@link ChangeResource} from {@link Change.Id} in {@code project} at {@code metaRevId}
+ *
+ * <p>Read change from ChangeNotesCache, so the method can be used upon creation, when the change
+ * might not be yet available in the index.
+ */
+ public ChangeResource parse(Project.NameKey project, Change.Id id, ObjectId metaRevId)
+ throws ResourceConflictException, ResourceNotFoundException, PermissionBackendException {
+ checkProjectStatePermitsRead(project);
+ ChangeNotes change = changeNotesFactory.createChecked(project, id, metaRevId);
+ if (!canRead(change)) {
+ throw new ResourceNotFoundException(toIdString(id));
+ }
+
+ return changeResourceFactory.create(change, user.get());
+ }
+
+ /**
+ * Parses {@link ChangeResource} from {@link Change.Id}
+ *
+ * <p>Reads the change from index, since project is unknown.
+ */
public ChangeResource parse(Change.Id id)
throws ResourceConflictException, ResourceNotFoundException, PermissionBackendException {
List<ChangeNotes> notes = changeFinder.find(id);
diff --git a/java/com/google/gerrit/server/restapi/change/CreateChange.java b/java/com/google/gerrit/server/restapi/change/CreateChange.java
index c0263df..fd3f11e 100644
--- a/java/com/google/gerrit/server/restapi/change/CreateChange.java
+++ b/java/com/google/gerrit/server/restapi/change/CreateChange.java
@@ -406,10 +406,18 @@
throw new BadRequestException("Cannot apply patch on top of an empty tree.");
}
ObjectId treeId = ApplyPatchUtil.applyPatch(git, oi, input.patch, mergeTip);
+ String appliedPatchCommitMessage =
+ getCommitMessage(
+ ApplyPatchUtil.buildCommitMessage(
+ input.subject,
+ ImmutableList.of(),
+ input.patch.patch,
+ ApplyPatchUtil.getResultPatch(git, reader, mergeTip, rw.lookupTree(treeId))),
+ me);
c =
rw.parseCommit(
CommitUtil.createCommitWithTree(
- oi, author, committer, mergeTip, commitMessage, treeId));
+ oi, author, committer, mergeTip, appliedPatchCommitMessage, treeId));
} else {
// create an empty commit.
c = createEmptyCommit(oi, rw, author, committer, mergeTip, commitMessage);
diff --git a/java/com/google/gerrit/server/restapi/change/GetPatch.java b/java/com/google/gerrit/server/restapi/change/GetPatch.java
index dea4dc4..d8946a7 100644
--- a/java/com/google/gerrit/server/restapi/change/GetPatch.java
+++ b/java/com/google/gerrit/server/restapi/change/GetPatch.java
@@ -24,6 +24,7 @@
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.DiffUtil;
import com.google.inject.Inject;
import java.io.IOException;
import java.io.OutputStream;
@@ -32,12 +33,10 @@
import java.util.Locale;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
-import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
-import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.kohsuke.args4j.Option;
public class GetPatch implements RestReadView<RevisionResource> {
@@ -98,14 +97,7 @@
if (path == null) {
out.write(formatEmailHeader(commit).getBytes(UTF_8));
}
- try (DiffFormatter fmt = new DiffFormatter(out)) {
- fmt.setRepository(repo);
- if (path != null) {
- fmt.setPathFilter(PathFilter.create(path));
- }
- fmt.format(base.getTree(), commit.getTree());
- fmt.flush();
- }
+ DiffUtil.getFormattedDiff(repo, base, commit, path, out);
}
@Override
diff --git a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
index 875b520..0d246e3 100644
--- a/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/accounts/AccountManagerIT.java
@@ -18,6 +18,7 @@
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static com.google.common.truth.Truth8.assertThat;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GOOGLE_OAUTH;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static com.google.gerrit.testing.TestActionRefUpdateContext.openTestRefUpdateContext;
import static java.util.stream.Collectors.toSet;
@@ -567,6 +568,108 @@
}
@Test
+ public void errorCreatingOAuthAccountDueToPresentDuplicateUsernameExternalID() throws Exception {
+ String username = "foo";
+ String gerritEmail = "bar@example.com";
+
+ ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
+ AuthRequest whoGerrit = authRequestFactory.createForUser(username);
+ whoGerrit.setEmailAddress(gerritEmail);
+ AuthResult authResultGerrit = accountManager.authenticate(whoGerrit);
+ assertAuthResultForNewAccount(authResultGerrit, gerritExtIdKey);
+
+ // Check that OAuth externalID is not in use.
+ ExternalId.Key externalExtIdKey = externalIdKeyFactory.create(SCHEME_GOOGLE_OAUTH, username);
+ assertNoSuchExternalIds(externalExtIdKey);
+
+ String googleOAuthEmail = "baz@example.com";
+ AuthRequest whoOAuth = authRequestFactory.createForOAuthUser(username);
+ whoOAuth.setEmailAddress(googleOAuthEmail);
+
+ AccountException thrown =
+ assertThrows(AccountException.class, () -> accountManager.authenticate(whoOAuth));
+ assertThat(thrown).hasMessageThat().contains("Cannot assign external ID \"username:foo\" to");
+ }
+
+ @Test
+ public void errorCreatingOAuthAccountDueToDuplicateEmailExternalIDInNonLDAPExternalId()
+ throws Exception {
+ String username = "foo";
+ String gerritEmail = "foo@example.com";
+
+ ExternalId.Key gerritExtIdKey =
+ externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
+ AuthRequest whoGerrit = authRequestFactory.createForExternalUser(username);
+ whoGerrit.setEmailAddress(gerritEmail);
+ AuthResult authResultGerrit = accountManager.authenticate(whoGerrit);
+ assertAuthResultForNewAccount(authResultGerrit, gerritExtIdKey);
+
+ // Check that OAuth externalID is not in use.
+ ExternalId.Key externalExtIdKey = externalIdKeyFactory.create(SCHEME_GOOGLE_OAUTH, username);
+ assertNoSuchExternalIds(externalExtIdKey);
+
+ String googleOAuthEmail = "foo@example.com";
+ AuthRequest whoOAuth = authRequestFactory.createForOAuthUser(username);
+ whoOAuth.setEmailAddress(googleOAuthEmail);
+
+ AccountException thrown =
+ assertThrows(AccountException.class, () -> accountManager.authenticate(whoOAuth));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Email 'foo@example.com' in use by another account");
+ }
+
+ @Test
+ public void errorCreatingOAuthAccountDueToDuplicateUsernameIdentityAlreadyInUse()
+ throws Exception {
+ String username = "foo";
+ String gerritEmail = "foo@example.com";
+
+ ExternalId.Key externalExtIdKey =
+ externalIdKeyFactory.create(ExternalId.SCHEME_EXTERNAL, username);
+ AuthRequest whoExternal = authRequestFactory.createForExternalUser(username);
+ whoExternal.setEmailAddress(gerritEmail);
+ AuthResult authResultGerrit = accountManager.authenticate(whoExternal);
+ assertAuthResultForNewAccount(authResultGerrit, externalExtIdKey);
+
+ // Check that OAuth externalID is not in use.
+ ExternalId.Key OAuthExtIdKey = externalIdKeyFactory.create(SCHEME_GOOGLE_OAUTH, username);
+ assertNoSuchExternalIds(OAuthExtIdKey);
+
+ String googleOAuthEmail = "baz@example.com";
+ AuthRequest whoOAuth = authRequestFactory.createForOAuthUser(username);
+ whoExternal.setEmailAddress(googleOAuthEmail);
+
+ AccountException thrown =
+ assertThrows(AccountException.class, () -> accountManager.authenticate(whoOAuth));
+ assertThat(thrown)
+ .hasMessageThat()
+ .contains("Cannot assign external ID \"username:foo\" to account");
+ }
+
+ @Test
+ public void linkOAuthAccountToLDAPAccountWithEmail() throws Exception {
+ String username = "foo";
+ String email = "foo@example.com";
+ ExternalId.Key gerritExtIdKey = externalIdKeyFactory.create(ExternalId.SCHEME_GERRIT, username);
+ AuthRequest whoGerrit = authRequestFactory.createForUser(username);
+ whoGerrit.setEmailAddress(email);
+ AuthResult authResultGerrit = accountManager.authenticate(whoGerrit);
+ Account.Id accID = authResultGerrit.getAccountId();
+ assertAuthResultForNewAccount(authResultGerrit, gerritExtIdKey);
+ // Check that OAuth externalID is not in use.
+ ExternalId.Key OAuthExtIdKey = externalIdKeyFactory.create(SCHEME_GOOGLE_OAUTH, username);
+ assertNoSuchExternalIds(OAuthExtIdKey);
+
+ AuthRequest whoOAuth = authRequestFactory.createForOAuthUser(username);
+ whoOAuth.setEmailAddress(email);
+ AuthResult authResultOAuth = accountManager.authenticate(whoOAuth);
+ assertAuthResultForExistingAccount(authResultOAuth, accID, OAuthExtIdKey);
+
+ assertThat(authResultOAuth.getAccountId()).isEqualTo(authResultGerrit.getAccountId());
+ }
+
+ @Test
public void updateExternalIdOnLink() throws Exception {
// Create an account with a SCHEME_GERRIT external ID and no email
String username = "foo";
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
index b451656..7b7e145a 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ApplyPatchIT.java
@@ -21,6 +21,7 @@
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_FILES;
import static com.google.gerrit.extensions.client.ListChangesOption.CURRENT_REVISION;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
+import static com.google.gerrit.server.patch.DiffUtil.cleanPatch;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.eclipse.jgit.lib.Constants.HEAD;
@@ -48,7 +49,6 @@
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.RestApiException;
import com.google.inject.Inject;
-import java.io.IOException;
import org.eclipse.jgit.api.errors.PatchApplyException;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.util.Base64;
@@ -56,7 +56,6 @@
public class ApplyPatchIT extends AbstractDaemonTest {
- private static final String COMMIT_MESSAGE = "Applying patch";
private static final String DESTINATION_BRANCH = "destBranch";
private static final String ADDED_FILE_NAME = "a_new_file.txt";
@@ -402,7 +401,7 @@
}
@Test
- public void applyPatchWithExplicitBase_OverrideParentId() throws Exception {
+ public void applyPatchWithExplicitBase_overrideParentId() throws Exception {
PushOneCommit.Result inputParent = createChange("Input parent", "file1", "content");
PushOneCommit.Result parent = createChange("Parent Change", "file2", "content");
parent.assertOkStatus();
@@ -440,6 +439,44 @@
ADDED_FILE_CONTENT);
}
+ @Test
+ public void commitMessage_providedMessage() throws Exception {
+ final String msg = "custom message";
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+ in.commitMessage = msg;
+
+ ChangeInfo result = applyPatch(in);
+
+ ChangeInfo info = get(result.changeId, CURRENT_REVISION, CURRENT_COMMIT);
+ assertThat(info.revisions.get(info.currentRevision).commit.message)
+ .isEqualTo(msg + "\n\nChange-Id: " + result.changeId + "\n");
+ }
+
+ @Test
+ public void commitMessage_defaultMessageAndPatchHeader() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput("Patch header\n" + ADDED_FILE_DIFF);
+
+ ChangeInfo result = applyPatch(in);
+
+ ChangeInfo info = get(result.changeId, CURRENT_REVISION, CURRENT_COMMIT);
+ assertThat(info.revisions.get(info.currentRevision).commit.message)
+ .isEqualTo("Default commit message\n\nChange-Id: " + result.changeId + "\n");
+ }
+
+ @Test
+ public void commitMessage_defaultMessageAndNoPatchHeader() throws Exception {
+ initDestBranch();
+ ApplyPatchPatchSetInput in = buildInput(ADDED_FILE_DIFF);
+
+ ChangeInfo result = applyPatch(in);
+
+ ChangeInfo info = get(result.changeId, CURRENT_REVISION, CURRENT_COMMIT);
+ assertThat(info.revisions.get(info.currentRevision).commit.message)
+ .isEqualTo("Default commit message\n\nChange-Id: " + result.changeId + "\n");
+ }
+
private void initDestBranch() throws Exception {
String head = getHead(repo(), HEAD).name();
createBranchWithRevision(BranchNameKey.create(project, ApplyPatchIT.DESTINATION_BRANCH), head);
@@ -462,27 +499,11 @@
private ChangeInfo applyPatch(ApplyPatchPatchSetInput input) throws RestApiException {
input.responseFormatOptions = ImmutableList.of(ListChangesOption.CURRENT_REVISION);
return gApi.changes()
- .create(new ChangeInput(project.get(), DESTINATION_BRANCH, COMMIT_MESSAGE))
+ .create(new ChangeInput(project.get(), DESTINATION_BRANCH, "Default commit message"))
.applyPatch(input);
}
private DiffInfo fetchDiffForFile(ChangeInfo result, String fileName) throws RestApiException {
return gApi.changes().id(result.id).current().file(fileName).diff();
}
-
- private String cleanPatch(BinaryResult bin) throws IOException {
- return cleanPatch(bin.asString());
- }
-
- private String cleanPatch(String s) {
- return s
- // Remove the header
- .substring(s.lastIndexOf("\ndiff --git"), s.length() - 1)
- // Remove "index NN..NN lines
- .replaceAll("(?m)^index.*", "")
- // Remove empty lines
- .replaceAll("\n+", "\n")
- // Trim
- .trim();
- }
}
diff --git a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
index d3ab2bb..393dacf 100644
--- a/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/api/change/ChangeIT.java
@@ -125,7 +125,6 @@
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.DraftHandling;
import com.google.gerrit.extensions.api.changes.ReviewResult;
-import com.google.gerrit.extensions.api.changes.ReviewerInfo;
import com.google.gerrit.extensions.api.changes.ReviewerInput;
import com.google.gerrit.extensions.api.changes.ReviewerResult;
import com.google.gerrit.extensions.api.changes.RevisionApi;
@@ -1356,26 +1355,23 @@
}
@Test
- public void addReviewerThatIsInactiveByUsername() throws Exception {
+ public void addReviewerThatIsInactiveByUsername_notAllowed() throws Exception {
PushOneCommit.Result result = createChange();
String username = name("new-user");
- Account.Id id = accountOperations.newAccount().username(username).inactive().create();
+ Account.Id accountId = accountOperations.newAccount().username(username).inactive().create();
ReviewerInput in = new ReviewerInput();
in.reviewer = username;
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
assertThat(r.input).isEqualTo(in.reviewer);
- assertThat(r.error).isNull();
- assertThat(r.reviewers).hasSize(1);
- ReviewerInfo reviewer = r.reviewers.get(0);
- assertThat(reviewer._accountId).isEqualTo(id.get());
- assertThat(reviewer.username).isEqualTo(username);
+ assertThat(r.error)
+ .isEqualTo(String.format("Cannot add inactive account %s, %s", accountId, username));
}
@Test
- public void addReviewerThatIsInactiveById() throws Exception {
+ public void addReviewerThatIsInactiveById_notAllowed() throws Exception {
PushOneCommit.Result result = createChange();
String username = name("new-user");
@@ -1386,33 +1382,28 @@
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
assertThat(r.input).isEqualTo(in.reviewer);
- assertThat(r.error).isNull();
- assertThat(r.reviewers).hasSize(1);
- ReviewerInfo reviewer = r.reviewers.get(0);
- assertThat(reviewer._accountId).isEqualTo(id.get());
- assertThat(reviewer.username).isEqualTo(username);
+
+ assertThat(r.error)
+ .isEqualTo(String.format("Cannot add inactive account %s, %s", id, id.get()));
}
@Test
- public void addReviewerThatIsInactiveByEmail() throws Exception {
+ public void addReviewerThatIsInactiveByEmail_notAllowed() throws Exception {
ConfigInput conf = new ConfigInput();
conf.enableReviewerByEmail = InheritableBoolean.TRUE;
gApi.projects().name(project.get()).config(conf);
PushOneCommit.Result result = createChange();
String username = "user@domain.com";
- Account.Id id = accountOperations.newAccount().username(username).inactive().create();
+ Account.Id accountId = accountOperations.newAccount().username(username).inactive().create();
ReviewerInput in = new ReviewerInput();
in.reviewer = username;
in.state = ReviewerState.CC;
ReviewerResult r = gApi.changes().id(result.getChangeId()).addReviewer(in);
- assertThat(r.input).isEqualTo(username);
- assertThat(r.error).isNull();
- assertThat(r.ccs).hasSize(1);
- AccountInfo reviewer = r.ccs.get(0);
- assertThat(reviewer._accountId).isEqualTo(id.get());
- assertThat(reviewer.username).isEqualTo(username);
+ assertThat(r.input).isEqualTo(in.reviewer);
+ assertThat(r.error)
+ .isEqualTo(String.format("Cannot add inactive account %s, %s", accountId, username));
}
@Test
diff --git a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
index 7a04ee5..0360622 100644
--- a/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
+++ b/javatests/com/google/gerrit/acceptance/rest/change/CreateChangeIT.java
@@ -1022,6 +1022,8 @@
DiffInfo diff = gApi.changes().id(info.id).current().file(PATCH_FILE_NAME).diff();
assertDiffForNewFile(diff, info.currentRevision, PATCH_FILE_NAME, PATCH_NEW_FILE_CONTENT);
+ assertThat(info.revisions.get(info.currentRevision).commit.message)
+ .isEqualTo("apply patch to other\n\nChange-Id: " + info.changeId + "\n");
}
@Test
diff --git a/plugins/codemirror-editor b/plugins/codemirror-editor
index 13dcd3f..597573b 160000
--- a/plugins/codemirror-editor
+++ b/plugins/codemirror-editor
@@ -1 +1 @@
-Subproject commit 13dcd3f4673c56e2b1001ef19eb54236943a35e6
+Subproject commit 597573b5d1ed44c75844d2b8047aae08f7ef8619
diff --git a/plugins/download-commands b/plugins/download-commands
index b83ce67..b4209a5 160000
--- a/plugins/download-commands
+++ b/plugins/download-commands
@@ -1 +1 @@
-Subproject commit b83ce6746fe404da5f4709d8f8270a99f04c800e
+Subproject commit b4209a5a4c334077b255002cbadc2ef659adee3c
diff --git a/plugins/package.json b/plugins/package.json
index 134c433..612062b 100644
--- a/plugins/package.json
+++ b/plugins/package.json
@@ -29,6 +29,7 @@
"@codemirror/lint": "^6.1.1",
"@codemirror/search": "^6.2.3",
"@codemirror/state": "^6.2.0",
+ "@codemirror/theme-one-dark": "^6.1.1",
"@codemirror/view": "^6.9.1",
"lit": "^2.2.3",
"rxjs": "^6.6.7",
diff --git a/plugins/yarn.lock b/plugins/yarn.lock
index 1512abe..3156f4c 100644
--- a/plugins/yarn.lock
+++ b/plugins/yarn.lock
@@ -129,9 +129,9 @@
"@lezer/php" "^1.0.0"
"@codemirror/lang-python@^6.0.0", "@codemirror/lang-python@^6.1.1":
- version "6.1.1"
- resolved "https://registry.yarnpkg.com/@codemirror/lang-python/-/lang-python-6.1.1.tgz#378c69199da41e0b09eaadc56f6d70ad6001fd34"
- integrity sha512-AddGMIKUssUAqaDKoxKWA5GAzy/CVE0eSY7/ANgNzdS1GYBkp6N49XKEyMElkuN04UsZ+bTIQdj+tVV75NMwJw==
+ version "6.1.2"
+ resolved "https://registry.yarnpkg.com/@codemirror/lang-python/-/lang-python-6.1.2.tgz#cabb57529679981f170491833dbf798576e7ab18"
+ integrity sha512-nbQfifLBZstpt6Oo4XxA2LOzlSp4b/7Bc5cmodG1R+Cs5PLLCTUvsMNWDnziiCfTOG/SW1rVzXq/GbIr6WXlcw==
dependencies:
"@codemirror/autocomplete" "^6.3.2"
"@codemirror/language" "^6.0.0"
@@ -239,6 +239,16 @@
resolved "https://registry.yarnpkg.com/@codemirror/state/-/state-6.2.0.tgz#a0fb08403ced8c2a68d1d0acee926bd20be922f2"
integrity sha512-69QXtcrsc3RYtOtd+GsvczJ319udtBf1PTrr2KbLWM/e2CXUPnh0Nz9AUo8WfhSQ7GeL8dPVNUmhQVgpmuaNGA==
+"@codemirror/theme-one-dark@^6.1.1":
+ version "6.1.1"
+ resolved "https://registry.yarnpkg.com/@codemirror/theme-one-dark/-/theme-one-dark-6.1.1.tgz#76600555cbb314c495216f018f75b0c28daff158"
+ integrity sha512-+CfzmScfJuD6uDF5bHJkAjWTQ2QAAHxODCPxUEgcImDYcJLT+4l5vLnBHmDVv46kCC5uUJGMrBJct2Z6JbvqyQ==
+ dependencies:
+ "@codemirror/language" "^6.0.0"
+ "@codemirror/state" "^6.0.0"
+ "@codemirror/view" "^6.0.0"
+ "@lezer/highlight" "^1.0.0"
+
"@codemirror/view@^6.0.0", "@codemirror/view@^6.2.2", "@codemirror/view@^6.6.0", "@codemirror/view@^6.9.1":
version "6.9.1"
resolved "https://registry.yarnpkg.com/@codemirror/view/-/view-6.9.1.tgz#2ce4c528974b6172a5a4a738b7b0a0f04a4c1140"
diff --git a/polygerrit-ui/app/.eslintrc.js b/polygerrit-ui/app/.eslintrc.js
index 89259dc..e18a3af 100644
--- a/polygerrit-ui/app/.eslintrc.js
+++ b/polygerrit-ui/app/.eslintrc.js
@@ -327,6 +327,8 @@
'error',
{argsIgnorePattern: '^_'},
],
+ // https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/es-builtins.md
+ 'node/no-unsupported-features/es-builtins': 'off',
// https://github.com/mysticatea/eslint-plugin-node/blob/master/docs/rules/no-unsupported-features/node-builtins.md
'node/no-unsupported-features/node-builtins': 'off',
// Disable no-invalid-this for ts files, because it incorrectly reports
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
index ccb750d..aba9a8b 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions.ts
@@ -1569,6 +1569,7 @@
const payload = {
base: e.detail.base,
allow_conflicts: e.detail.allowConflicts,
+ on_behalf_of_uploader: e.detail.onBehalfOfUploader,
};
const rebaseChain = !!e.detail.rebaseChain;
this.fireAction(
@@ -1576,7 +1577,10 @@
assertUIActionInfo(this.revisionActions.rebase),
rebaseChain ? false : true,
payload,
- {allow_conflicts: payload.allow_conflicts}
+ {
+ allow_conflicts: payload.allow_conflicts,
+ on_behalf_of_uploader: payload.on_behalf_of_uploader,
+ }
);
}
diff --git a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
index 9c01f2f..277eddd 100644
--- a/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-actions/gr-change-actions_test.ts
@@ -624,15 +624,20 @@
assert.isTrue(fetchChangesStub.called);
element.handleRebaseConfirm(
new CustomEvent('', {
- detail: {base: '1234', allowConflicts: false, rebaseChain: false},
+ detail: {
+ base: '1234',
+ allowConflicts: false,
+ rebaseChain: false,
+ onBehalfOfUploader: true,
+ },
})
);
assert.deepEqual(fireActionStub.lastCall.args, [
'/rebase',
assertUIActionInfo(rebaseAction),
true,
- {base: '1234', allow_conflicts: false},
- {allow_conflicts: false},
+ {base: '1234', allow_conflicts: false, on_behalf_of_uploader: true},
+ {allow_conflicts: false, on_behalf_of_uploader: true},
]);
});
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
index 34869f2..3982666 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog.ts
@@ -23,6 +23,10 @@
import {ValueChangedEvent} from '../../../types/events';
import {throwingErrorCallback} from '../../shared/gr-rest-api-interface/gr-rest-apis/gr-rest-api-helper';
import {fireNoBubbleNoCompose} from '../../../utils/event-util';
+import {KnownExperimentId} from '../../../services/flags/flags';
+import {resolve} from '../../../models/dependency';
+import {changeModelToken} from '../../../models/change/change-model';
+import {subscribe} from '../../lit/subscription-controller';
export interface RebaseChange {
name: string;
@@ -33,6 +37,7 @@
base: string | null;
allowConflicts: boolean;
rebaseChain: boolean;
+ onBehalfOfUploader: boolean;
}
@customElement('gr-confirm-rebase-dialog')
@@ -79,6 +84,12 @@
@state()
recentChanges?: RebaseChange[];
+ @state()
+ allowConflicts = false;
+
+ @state()
+ isOwner = false;
+
@query('#rebaseOnParentInput')
private rebaseOnParentInput!: HTMLInputElement;
@@ -99,9 +110,18 @@
private readonly restApiService = getAppContext().restApiService;
+ private readonly flagsService = getAppContext().flagsService;
+
+ private readonly getChangeModel = resolve(this, changeModelToken);
+
constructor() {
super();
this.query = input => this.getChangeSuggestions(input);
+ subscribe(
+ this,
+ () => this.getChangeModel().isOwner$,
+ x => (this.isOwner = x)
+ );
}
override willUpdate(changedProperties: PropertyValues): void {
@@ -135,7 +155,7 @@
display: block;
width: 100%;
}
- .rebaseAllowConflicts {
+ .rebaseCheckbox:first-of-type {
margin-top: var(--spacing-m);
}
.rebaseOption {
@@ -225,16 +245,34 @@
>
</gr-autocomplete>
</div>
- <div class="rebaseAllowConflicts">
- <input id="rebaseAllowConflicts" type="checkbox" />
+ <div class="rebaseCheckbox">
+ <input
+ id="rebaseAllowConflicts"
+ type="checkbox"
+ @change=${() => {
+ this.allowConflicts = !!this.rebaseAllowConflicts?.checked;
+ }}
+ />
<label for="rebaseAllowConflicts"
>Allow rebase with conflicts</label
>
</div>
${when(
+ this.flagsService.isEnabled(
+ KnownExperimentId.REBASE_ON_BEHALF_OF_UPLOADER
+ ) &&
+ !this.isOwner &&
+ this.allowConflicts,
+ () =>
+ html`<span class="message"
+ >Rebase cannot be done on behalf of the uploader when allowing
+ conflicts.</span
+ >`
+ )}
+ ${when(
this.hasParent,
() =>
- html`<div>
+ html`<div class="rebaseCheckbox">
<input
id="rebaseChain"
type="checkbox"
@@ -351,11 +389,24 @@
base: this.getSelectedBase(),
allowConflicts: this.rebaseAllowConflicts.checked,
rebaseChain: !!this.rebaseChain?.checked,
+ onBehalfOfUploader: this.rebaseOnBehalfOfUploader(),
};
fireNoBubbleNoCompose(this, 'confirm-rebase', detail);
this.text = '';
}
+ private rebaseOnBehalfOfUploader() {
+ if (
+ !this.flagsService.isEnabled(
+ KnownExperimentId.REBASE_ON_BEHALF_OF_UPLOADER
+ )
+ ) {
+ return false;
+ }
+ if (this.allowConflicts) return false;
+ return true;
+ }
+
private handleCancelTap(e: Event) {
e.preventDefault();
e.stopPropagation();
diff --git a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
index 2644d81..3064014 100644
--- a/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-confirm-rebase-dialog/gr-confirm-rebase-dialog_test.ts
@@ -9,6 +9,7 @@
import {
pressKey,
queryAndAssert,
+ stubFlags,
stubRestApi,
waitUntil,
} from '../../../test/test-utils';
@@ -22,6 +23,7 @@
let element: GrConfirmRebaseDialog;
setup(async () => {
+ stubFlags('isEnabled').returns(true);
element = await fixture(
html`<gr-confirm-rebase-dialog></gr-confirm-rebase-dialog>`
);
@@ -78,7 +80,7 @@
>
</gr-autocomplete>
</div>
- <div class="rebaseAllowConflicts">
+ <div class="rebaseCheckbox">
<input id="rebaseAllowConflicts" type="checkbox" />
<label for="rebaseAllowConflicts">
Allow rebase with conflicts
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
index 3a88eaf..a20135b 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog-it_test.ts
@@ -9,6 +9,7 @@
queryAndAssert,
stubRestApi,
waitEventLoop,
+ waitUntil,
} from '../../../test/test-utils';
import {GrReplyDialog} from './gr-reply-dialog';
@@ -23,6 +24,7 @@
import {GrButton} from '../../shared/gr-button/gr-button';
import {testResolver} from '../../../test/common-test-setup';
import {pluginLoaderToken} from '../../shared/gr-js-api-interface/gr-plugin-loader';
+import {GrComment} from '../../shared/gr-comment/gr-comment';
suite('gr-reply-dialog-it tests', () => {
let element: GrReplyDialog;
@@ -96,10 +98,15 @@
});
test('lgtm plugin', async () => {
+ const attachStub = sinon.stub();
+ const callbackStub = sinon.stub();
window.Gerrit.install(
plugin => {
const replyApi = plugin.changeReply();
+ const hook = plugin.hook('reply-text');
+ hook.onAttached(attachStub);
replyApi.addReplyTextChangedCallback(text => {
+ callbackStub(text);
const label = 'Code-Review';
const labelValue = replyApi.getLabelValue(label);
if (labelValue && labelValue === ' 0' && text.indexOf('LGTM') === 0) {
@@ -114,16 +121,27 @@
setupElement(element);
const pluginLoader = testResolver(pluginLoaderToken);
pluginLoader.loadPlugins([]);
- await pluginLoader.awaitPluginsLoaded();
- await waitEventLoop();
- await waitEventLoop();
+ // This may seem a bit weird, but we have to somehow make sure that the
+ // event listener is actually installed, and apparently a `gr-comment` is
+ // attached twice inside the 'reply-text' endpoint. Could not find a better
+ // way to make sure that the callback is ready to receive events.
+ await waitUntil(() => attachStub.callCount === 2);
+
+ const comment = queryAndAssert<GrComment>(
+ element,
+ 'gr-comment#patchsetLevelComment'
+ );
+ comment.messageText = 'LGTM';
+
+ await waitUntil(() => callbackStub.calledWith('LGTM'));
+
const labelScoreRows = queryAndAssert(
element.getLabelScores(),
'gr-label-score-row[name="Code-Review"]'
);
const selectedBtn = queryAndAssert(
labelScoreRows,
- 'gr-button[data-value="+1"]'
+ 'gr-button[data-value="+1"].iron-selected'
);
assert.isOk(selectedBtn);
});
diff --git a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
index 893eeb9..38acf80 100644
--- a/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
+++ b/polygerrit-ui/app/elements/change/gr-reply-dialog/gr-reply-dialog.ts
@@ -914,6 +914,8 @@
}}
@comment-text-changed=${(e: ValueChangedEvent<string>) => {
this.patchsetLevelDraftMessage = e.detail.value;
+ // See `addReplyTextChangedCallback` in `ChangeReplyPluginApi`.
+ fire(e.currentTarget as HTMLElement, 'value-changed', e.detail);
}}
.messagePlaceholder=${this.messagePlaceholder}
hide-header
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
index 1cc8468..a729e44 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.ts
@@ -383,7 +383,7 @@
></gr-smart-search>
<gr-endpoint-decorator
class="hideOnMobile"
- name="header-browse-source"
+ name="header-top-right"
></gr-endpoint-decorator>
<gr-endpoint-decorator class="feedbackButton" name="header-feedback">
${this.renderFeedback()}
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
index eced28c..155ba10 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header_test.ts
@@ -63,10 +63,7 @@
</gr-endpoint-decorator>
<gr-smart-search id="search" label="Search for changes">
</gr-smart-search>
- <gr-endpoint-decorator
- class="hideOnMobile"
- name="header-browse-source"
- >
+ <gr-endpoint-decorator class="hideOnMobile" name="header-top-right">
</gr-endpoint-decorator>
<gr-endpoint-decorator
class="feedbackButton"
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
index 12b19c7..cf555eb 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view.ts
@@ -39,6 +39,7 @@
} from '../../../models/views/change';
import {userModelToken} from '../../../models/user/user-model';
import {storageServiceToken} from '../../../services/storage/gr-storage_impl';
+import {isDarkTheme} from '../../../utils/theme-util';
const RESTORED_MESSAGE = 'Content restored from a previous edit.';
const SAVING_MESSAGE = 'Saving changes...';
@@ -88,6 +89,8 @@
// private but used in test
@state() latestPatchsetNumber?: RevisionPatchSetNum;
+ @state() private darkMode = false;
+
private readonly restApiService = getAppContext().restApiService;
private readonly reporting = getAppContext().reportingService;
@@ -132,6 +135,13 @@
() => this.getChangeModel().latestPatchNumWithEdit$,
x => (this.latestPatchsetNumber = x)
);
+ subscribe(
+ this,
+ () => this.getUserModel().preferenceTheme$,
+ theme => {
+ this.darkMode = isDarkTheme(theme);
+ }
+ );
this.shortcuts.addLocal({key: 's', modifiers: [Modifier.CTRL_KEY]}, () =>
this.handleSaveShortcut()
);
@@ -284,6 +294,10 @@
name="lineNum"
.value=${this.viewState?.editView?.lineNum}
></gr-endpoint-param>
+ <gr-endpoint-param
+ name="darkMode"
+ .value=${this.darkMode}
+ ></gr-endpoint-param>
<gr-default-editor
id="file"
.fileContent=${this.newContent}
diff --git a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
index 0a36a05..a2c92c5 100644
--- a/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
+++ b/polygerrit-ui/app/elements/edit/gr-editor-view/gr-editor-view_test.ts
@@ -117,6 +117,7 @@
<gr-endpoint-param name="prefs"> </gr-endpoint-param>
<gr-endpoint-param name="fileType"> </gr-endpoint-param>
<gr-endpoint-param name="lineNum"> </gr-endpoint-param>
+ <gr-endpoint-param name="darkMode"> </gr-endpoint-param>
<gr-default-editor id="file"> </gr-default-editor>
</gr-endpoint-decorator>
</div>
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index b8a77db..bfa27a2 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -870,14 +870,15 @@
}
private renderShowFixButton() {
- if (!(this.comment as RobotCommentInfo)?.fix_suggestions) return;
+ const fix_suggestions = (this.comment as RobotCommentInfo)?.fix_suggestions;
+ if (!fix_suggestions || fix_suggestions.length === 0) return;
return html`
<gr-button
link
secondary
class="action show-fix"
?disabled=${this.saving}
- @click=${this.handleShowFix}
+ @click=${() => this.handleShowFix()}
>
Show Fix
</gr-button>
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 023d8b5..011b80c 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -104,6 +104,9 @@
word-wrap: var(--linked-text-word-wrap, break-word);
}
.markdown-html {
+ /* code overrides white-space to pre, everything else should wrap as
+ normal. */
+ white-space: normal;
/* prose will automatically wrap but inline <code> blocks won't and we
should overflow in that case rather than wrapping or leaking out */
overflow-x: auto;
@@ -116,7 +119,15 @@
subscribe(
this,
() => this.getConfigModel().repoCommentLinks$,
- repoCommentLinks => (this.repoCommentLinks = repoCommentLinks)
+ repoCommentLinks => {
+ this.repoCommentLinks = repoCommentLinks;
+ // Always linkify URLs starting with https?://
+ this.repoCommentLinks.ALWAYS_LINK_HTTP = {
+ match: '(https?://\\S+[\\w/])',
+ link: '$1',
+ enabled: true,
+ };
+ }
);
}
@@ -140,15 +151,36 @@
}
private renderAsMarkdown() {
- // need to find out here, since customRender is not arrow function
+ // Need to find out here, since customRender is not arrow function
const suggestEditsEnable = this.flagsService.isEnabled(
KnownExperimentId.SUGGEST_EDIT
);
- // <marked-element> internals will be in charge of calling our custom
- // renderer so we wrap 'this.rewriteText' so that 'this' is preserved via
- // closure.
- const boundRewriteText = (text: string) =>
- linkifyUrlsAndApplyRewrite(text, this.repoCommentLinks);
+ // Bind `this` via closure.
+ const boundRewriteText = (text: string) => {
+ const nonAsteriskRewrites = Object.fromEntries(
+ Object.entries(this.repoCommentLinks).filter(
+ ([_name, rewrite]) => !rewrite.match.includes('\\*')
+ )
+ );
+ return linkifyUrlsAndApplyRewrite(text, nonAsteriskRewrites);
+ };
+
+ // Due to a tokenizer bug in the old version of markedjs we use, text with a
+ // single asterisk is separated into 2 tokens before passing to renderer
+ // ['text'] which breaks our rewrites that would span across the 2 tokens.
+ // Since upgrading our markedjs version is infeasible, we are applying those
+ // asterisk rewrites again at the end (using renderer['paragraph'] hook)
+ // after all the nodes are combined.
+ // Bind `this` via closure.
+ const boundRewriteAsterisks = (text: string) => {
+ const asteriskRewrites = Object.fromEntries(
+ Object.entries(this.repoCommentLinks).filter(([_name, rewrite]) =>
+ rewrite.match.includes('\\*')
+ )
+ );
+ const linkedText = linkifyUrlsAndApplyRewrite(text, asteriskRewrites);
+ return `<p>${linkedText}</p>`;
+ };
// We are overriding some marked-element renderers for a few reasons:
// 1. Disable inline images as a design/policy choice.
@@ -180,15 +212,20 @@
renderer['code'] = (text: string, infostring: string) => {
if (suggestEditsEnable && infostring === USER_SUGGESTION_INFO_STRING) {
// default santizer in markedjs is very restrictive, we need to use
- // existing html element to mark element. We cannot use css class for it.
- // Therefore we pick mark - as not frequently used html element to represent
- // unconverted gr-user-suggestion-fix.
- // TODO(milutin): Find a way to override sanitizer to directly use gr-user-suggestion-fix
+ // existing html element to mark element. We cannot use css class for
+ // it. Therefore we pick mark - as not frequently used html element to
+ // represent unconverted gr-user-suggestion-fix.
+ // TODO(milutin): Find a way to override sanitizer to directly use
+ // gr-user-suggestion-fix
return `<mark>${text}</mark>`;
} else {
return `<pre><code>${text}</code></pre>`;
}
};
+ // <marked-element> internals will be in charge of calling our custom
+ // renderer so we write these functions separately so that 'this' is
+ // preserved via closure.
+ renderer['paragraph'] = boundRewriteAsterisks;
renderer['text'] = boundRewriteText;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index 0e5117a..81048a3 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -208,6 +208,38 @@
/* HTML */ '<pre class="plaintext"># A Markdown Heading</pre>'
);
});
+
+ test('does default linking', async () => {
+ element.content = 'http://www.google.com';
+ await element.updateComplete;
+ assert.shadowDom.equal(
+ element,
+ /* HTML*/ `
+ <pre class="plaintext">
+ <a
+ href="http://www.google.com"
+ rel="noopener"
+ target="_blank"
+ >http://www.google.com</a>
+ </pre>
+ `
+ );
+
+ element.content = 'https://www.google.com';
+ await element.updateComplete;
+ assert.shadowDom.equal(
+ element,
+ /* HTML*/ `
+ <pre class="plaintext">
+ <a
+ href="https://www.google.com"
+ rel="noopener"
+ target="_blank"
+ >https://www.google.com</a>
+ </pre>
+ `
+ );
+ });
});
suite('as markdown', () => {
@@ -588,6 +620,74 @@
);
});
+ test('renders rewrites with an asterisk', async () => {
+ await setCommentLinks({
+ customLinkRewrite: {
+ match: 'asterisks (\\*) rule',
+ link: 'http://google.com',
+ },
+ });
+
+ element.content = 'I think asterisks * rule';
+ await element.updateComplete;
+
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html" class="markdown-html">
+ <p>
+ I think
+ <a href="http://google.com" rel="noopener" target="_blank"
+ >asterisks * rule</a
+ >
+ </p>
+ </div>
+ </marked-element>
+ `
+ );
+ });
+
+ test('does default linking', async () => {
+ element.content = 'http://www.google.com';
+ await element.updateComplete;
+ assert.shadowDom.equal(
+ element,
+ /* HTML*/ `
+ <marked-element>
+ <div slot="markdown-html" class="markdown-html">
+ <p>
+ <a
+ href="http://www.google.com"
+ rel="noopener"
+ target="_blank"
+ >http://www.google.com</a>
+ </p>
+ </div>
+ </marked-element>
+ `
+ );
+
+ element.content = 'https://www.google.com';
+ await element.updateComplete;
+ assert.shadowDom.equal(
+ element,
+ /* HTML*/ `
+ <marked-element>
+ <div slot="markdown-html" class="markdown-html">
+ <p>
+ <a
+ href="https://www.google.com"
+ rel="noopener"
+ target="_blank"
+ >https://www.google.com</a>
+ </p>
+ </div>
+ </marked-element>
+ `
+ );
+ });
+
suite('user suggest fix', () => {
setup(async () => {
const flagsService = getAppContext().flagsService;
diff --git a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
index a16a1d1..386a166 100644
--- a/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
+++ b/polygerrit-ui/app/elements/shared/gr-js-api-interface/gr-js-api-interface-element.ts
@@ -76,6 +76,7 @@
}
async handleShowChange(detail: ShowChangeDetail) {
+ if (!detail.change) return;
await this.waitForPluginsToLoad();
// Note (issue 8221) Shallow clone the change object and add a mergeable
// getter with deprecation warning. This makes the change detail appear as
diff --git a/polygerrit-ui/app/services/flags/flags.ts b/polygerrit-ui/app/services/flags/flags.ts
index 7488e79..5c7bf2d 100644
--- a/polygerrit-ui/app/services/flags/flags.ts
+++ b/polygerrit-ui/app/services/flags/flags.ts
@@ -19,4 +19,5 @@
PUSH_NOTIFICATIONS_DEVELOPER = 'UiFeature__push_notifications_developer',
PUSH_NOTIFICATIONS = 'UiFeature__push_notifications',
SUGGEST_EDIT = 'UiFeature__suggest_edit',
+ REBASE_ON_BEHALF_OF_UPLOADER = 'UiFeature__rebase_on_behalf_of_uploader',
}
diff --git a/polygerrit-ui/app/test/test-data-generators.ts b/polygerrit-ui/app/test/test-data-generators.ts
index a927a5b..a3f24d4 100644
--- a/polygerrit-ui/app/test/test-data-generators.ts
+++ b/polygerrit-ui/app/test/test-data-generators.ts
@@ -867,7 +867,24 @@
robot_id: 'robot-id-123' as RobotId,
robot_run_id: 'robot-run-id-456' as RobotRunId,
properties: {},
- fix_suggestions: [],
+ fix_suggestions: [
+ {
+ fix_id: 'robot-run-id-456-fix' as FixId,
+ description: 'Robot suggestion',
+ replacements: [
+ {
+ path: 'abc.txt'!,
+ range: {
+ start_line: 0,
+ start_character: 0,
+ end_line: 1,
+ end_character: 10,
+ },
+ replacement: 'replacement',
+ },
+ ],
+ },
+ ],
...extra,
};
}
diff --git a/polygerrit-ui/app/utils/link-util.ts b/polygerrit-ui/app/utils/link-util.ts
index ccafa5a..48e9c07 100644
--- a/polygerrit-ui/app/utils/link-util.ts
+++ b/polygerrit-ui/app/utils/link-util.ts
@@ -33,11 +33,6 @@
commentLinkInfo =>
commentLinkInfo.enabled !== false && commentLinkInfo.link !== undefined
);
- // Always linkify URLs starting with https?://
- enabledRewrites.push({
- match: '(https?://\\S+[\\w/])',
- link: '$1',
- });
return enabledRewrites.flatMap(rewrite => {
const regexp = new RegExp(rewrite.match, 'g');
const partialResults: RewriteResult[] = [];
diff --git a/polygerrit-ui/app/utils/link-util_test.ts b/polygerrit-ui/app/utils/link-util_test.ts
index 52b8288..e4e719b 100644
--- a/polygerrit-ui/app/utils/link-util_test.ts
+++ b/polygerrit-ui/app/utils/link-util_test.ts
@@ -12,17 +12,6 @@
}
suite('link rewrites', () => {
- test('default linking', () => {
- assert.equal(
- linkifyUrlsAndApplyRewrite('http://www.google.com', {}),
- link('http://www.google.com', 'http://www.google.com')
- );
- assert.equal(
- linkifyUrlsAndApplyRewrite('https://www.google.com', {}),
- link('https://www.google.com', 'https://www.google.com')
- );
- });
-
test('without text', () => {
assert.equal(
linkifyUrlsAndApplyRewrite('foo', {