<commit_before>// Copyright (C) 2013 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.api.revision;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
import static com.google.gerrit.acceptance.PushOneCommit.PATCH_FILE_ONLY;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.HEAD;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.ListMultimap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.DraftApi;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.NotifyInfo;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ETagView;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.restapi.change.GetRevisionActions;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;

public class RevisionIT extends AbstractDaemonTest {
  @Inject private GetRevisionActions getRevisionActions;
  @Inject private ProjectOperations projectOperations;
  @Inject private RequestScopeOperations requestScopeOperations;
  @Inject private ExtensionRegistry extensionRegistry;

  @Test
  public void reviewTriplet() throws Exception {
    PushOneCommit.Result r = createChange();
    gApi.changes()
        .id(project.get() + "~master~" + r.getChangeId())
        .revision(r.getCommit().name())
        .review(ReviewInput.approve());
  }

  @Test
  public void reviewCurrent() throws Exception {
    PushOneCommit.Result r = createChange();
    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
  }

  @Test
  public void reviewNumber() throws Exception {
    PushOneCommit.Result r = createChange();
    gApi.changes().id(r.getChangeId()).revision(1).review(ReviewInput.approve());

    r = updateChange(r, "new content");
    gApi.changes().id(r.getChangeId()).revision(2).review(ReviewInput.approve());
  }

  @Test
  public void submit() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = project.get() + "~master~" + r.getChangeId();
    gApi.changes().id(changeId).current().review(ReviewInput.approve());
    gApi.changes().id(changeId).current().submit();
    assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
  }

  @Test
  public void postSubmitApproval() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = project.get() + "~master~" + r.getChangeId();
    gApi.changes().id(changeId).current().review(ReviewInput.recommend());

    String label = "Code-Review";
    ApprovalInfo approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(1);
    assertThat(approval.postSubmit).isNull();

    // Submit by direct push.
    git().push().setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master")).call();
    assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);

    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(1);
    assertThat(approval.postSubmit).isNull();
    assertPermitted(gApi.changes().id(changeId).get(DETAILED_LABELS), "Code-Review", 1, 2);

    // Repeating the current label is allowed. Does not flip the postSubmit bit
    // due to deduplication codepath.
    gApi.changes().id(changeId).current().review(ReviewInput.recommend());
    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(1);
    assertThat(approval.postSubmit).isNull();

    // Reducing vote is not allowed.
    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(changeId).current().review(ReviewInput.dislike()));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(1);
    assertThat(approval.postSubmit).isNull();

    // Increasing vote is allowed.
    gApi.changes().id(changeId).current().review(ReviewInput.approve());
    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(2);
    assertThat(approval.postSubmit).isTrue();
    assertPermitted(gApi.changes().id(changeId).get(DETAILED_LABELS), "Code-Review", 2);

    // Decreasing to previous post-submit vote is still not allowed.
    thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(changeId).current().review(ReviewInput.dislike()));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(2);
    assertThat(approval.postSubmit).isTrue();
  }

  @Test
  public void postSubmitApprovalAfterVoteRemoved() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = project.get() + "~master~" + r.getChangeId();

    requestScopeOperations.setApiUser(admin.id());
    revision(r).review(ReviewInput.approve());

    requestScopeOperations.setApiUser(user.id());
    revision(r).review(ReviewInput.recommend());

    requestScopeOperations.setApiUser(admin.id());
    gApi.changes().id(changeId).reviewer(user.username()).deleteVote("Code-Review");
    Optional<ApprovalInfo> crUser =
        get(changeId, DETAILED_LABELS).labels.get("Code-Review").all.stream()
            .filter(a -> a._accountId == user.id().get())
            .findFirst();
    assertThat(crUser).isPresent();
    assertThat(crUser.get().value).isEqualTo(0);

    revision(r).submit();

    requestScopeOperations.setApiUser(user.id());
    ReviewInput in = new ReviewInput();
    in.label("Code-Review", 1);
    in.message = "Still LGTM";
    revision(r).review(in);

    ApprovalInfo cr =
        gApi.changes().id(changeId).get(DETAILED_LABELS).labels.get("Code-Review").all.stream()
            .filter(a -> a._accountId == user.id().get())
            .findFirst()
            .get();
    assertThat(cr.postSubmit).isTrue();
  }

  @Test
  public void postSubmitDeleteApprovalNotAllowed() throws Exception {
    PushOneCommit.Result r = createChange();

    revision(r).review(ReviewInput.approve());
    revision(r).submit();

    ReviewInput in = new ReviewInput();
    in.label("Code-Review", 0);

    ResourceConflictException thrown =
        assertThrows(ResourceConflictException.class, () -> revision(r).review(in));
    assertThat(thrown)
        .hasMessageThat()
        .contains("Cannot reduce vote on labels for closed change: Code-Review");
  }

  @TestProjectInput(submitType = SubmitType.CHERRY_PICK)
  @Test
  public void approvalCopiedDuringSubmitIsNotPostSubmit() throws Exception {
    PushOneCommit.Result r = createChange();
    Change.Id id = r.getChange().getId();
    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
    gApi.changes().id(id.get()).current().submit();

    ChangeData cd = r.getChange();
    assertThat(cd.patchSets()).hasSize(2);
    PatchSetApproval psa =
        Iterators.getOnlyElement(
            cd.currentApprovals().stream().filter(a -> !a.isLegacySubmit()).iterator());
    assertThat(psa.patchSetId().get()).isEqualTo(2);
    assertThat(psa.label()).isEqualTo("Code-Review");
    assertThat(psa.value()).isEqualTo(2);
    assertThat(psa.postSubmit()).isFalse();
  }

  @Test
  public void voteOnAbandonedChange() throws Exception {
    PushOneCommit.Result r = createChange();
    gApi.changes().id(r.getChangeId()).abandon();
    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(r.getChangeId()).current().review(ReviewInput.reject()));
    assertThat(thrown).hasMessageThat().contains("change is closed");
  }

  @Test
  public void voteNotAllowedWithoutPermission() throws Exception {
    PushOneCommit.Result r = createChange();
    requestScopeOperations.setApiUser(user.id());
    AuthException thrown =
        assertThrows(AuthException.class, () -> change(r).current().review(ReviewInput.approve()));
    assertThat(thrown).hasMessageThat().contains("is restricted");
  }

  @Test
  public void cherryPick() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master%topic=someTopic");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.message = "it goes to stable branch";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    assertThat(orig.get().messages).hasSize(1);
    ChangeInfo changeInfo = orig.revision(r.getCommit().name()).cherryPickAsInfo(in);
    assertThat(changeInfo.containsGitConflicts).isNull();
    assertThat(changeInfo.workInProgress).isNull();
    ChangeApi cherry = gApi.changes().id(changeInfo._number);

    ChangeInfo cherryPickChangeInfoWithDetails = cherry.get();
    assertThat(cherryPickChangeInfoWithDetails.workInProgress).isNull();
    assertThat(cherryPickChangeInfoWithDetails.messages).hasSize(1);
    Iterator<ChangeMessageInfo> cherryIt = cherryPickChangeInfoWithDetails.messages.iterator();
    assertThat(cherryIt.next().message).isEqualTo("Patch Set 1: Cherry Picked from branch master.");

    assertThat(cherry.get().subject).contains(in.message);
    assertThat(cherry.get().topic).isEqualTo("someTopic-foo");
    assertThat(cherry.get().cherryPickOfChange).isEqualTo(orig.get()._number);
    assertThat(cherry.get().cherryPickOfPatchSet).isEqualTo(1);

    cherry.current().review(ReviewInput.approve());
    cherry.current().submit();
  }

  @Test
  public void cherryPickWithoutMessage() throws Exception {
    String branch = "foo";

    // Create change to cherry-pick
    PushOneCommit.Result change = createChange();
    RevCommit revCommit = change.getCommit();

    // Create target branch to cherry-pick to.
    gApi.projects().name(project.get()).branch(branch).create(new BranchInput());

    // Cherry-pick without message.
    CherryPickInput input = new CherryPickInput();
    input.destination = branch;
    String changeId =
        gApi.changes()
            .id(change.getChangeId())
            .revision(revCommit.name())
            .cherryPickAsInfo(input)
            .id;

    // Expect that the message of the cherry-picked commit was used for the cherry-pick change.
    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
    assertThat(revInfo).isNotNull();
    assertThat(revInfo.commit.message).isEqualTo(revCommit.getFullMessage());
  }

  @Test
  public void cherryPickSetChangeId() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    String id = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbe3f";
    in.message = "it goes to foo branch\n\nChange-Id: " + id;

    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    assertThat(orig.get().messages).hasSize(1);
    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);

    ChangeInfo changeInfo = cherry.get();

    // The cherry-pick honors the ChangeId specified in the input message:
    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
    assertThat(revInfo).isNotNull();
    assertThat(revInfo.commit.message).endsWith(id + "\n");
  }

  @Test
  public void cherryPickWithNoTopic() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.message = "it goes to stable branch";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isNull();
    cherry.current().review(ReviewInput.approve());
    cherry.current().submit();
  }

  @Test
  public void cherryPickWithSetTopic() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.topic = "topic";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isEqualTo("topic");
  }

  @Test
  public void cherryPickNewPatchsetWithSetTopic() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isNull();
    in.topic = "topic";
    cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isEqualTo("topic");
  }

  @Test
  public void cherryPickWorkInProgressChange() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master%wip");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.message = "cherry pick message";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().cherryPickOfChange).isEqualTo(orig.get()._number);
    assertThat(cherry.get().cherryPickOfPatchSet).isEqualTo(1);
    assertThat(cherry.get().workInProgress).isTrue();
  }

  @Test
  public void cherryPickToSameBranch() throws Exception {
    PushOneCommit.Result r = createChange();
    ChangeApi change = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
    CherryPickInput in = new CherryPickInput();
    in.destination = "master";
    in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
    ChangeInfo cherryInfo = change.revision(r.getCommit().name()).cherryPick(in).get();
    assertThat(cherryInfo.messages).hasSize(2);
    Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();<commit_msg>Add test for cherry-picking with topic<commit_after>// Copyright (C) 2013 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.api.revision;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_CONTENT;
import static com.google.gerrit.acceptance.PushOneCommit.FILE_NAME;
import static com.google.gerrit.acceptance.PushOneCommit.PATCH;
import static com.google.gerrit.acceptance.PushOneCommit.PATCH_FILE_ONLY;
import static com.google.gerrit.acceptance.PushOneCommit.SUBJECT;
import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow;
import static com.google.gerrit.entities.Patch.COMMIT_MSG;
import static com.google.gerrit.entities.Patch.MERGE_LIST;
import static com.google.gerrit.extensions.client.ListChangesOption.ALL_REVISIONS;
import static com.google.gerrit.extensions.client.ListChangesOption.DETAILED_LABELS;
import static com.google.gerrit.git.ObjectIds.abbreviateName;
import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS;
import static com.google.gerrit.testing.GerritJUnit.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;
import static org.eclipse.jgit.lib.Constants.HEAD;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Iterators;
import com.google.common.collect.ListMultimap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.ExtensionRegistry;
import com.google.gerrit.acceptance.ExtensionRegistry.Registration;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.RestResponse;
import com.google.gerrit.acceptance.TestAccount;
import com.google.gerrit.acceptance.TestProjectInput;
import com.google.gerrit.acceptance.config.GerritConfig;
import com.google.gerrit.acceptance.testsuite.project.ProjectOperations;
import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.entities.Account;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSetApproval;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.extensions.api.changes.ChangeApi;
import com.google.gerrit.extensions.api.changes.CherryPickInput;
import com.google.gerrit.extensions.api.changes.DraftApi;
import com.google.gerrit.extensions.api.changes.DraftInput;
import com.google.gerrit.extensions.api.changes.NotifyHandling;
import com.google.gerrit.extensions.api.changes.NotifyInfo;
import com.google.gerrit.extensions.api.changes.RecipientType;
import com.google.gerrit.extensions.api.changes.ReviewInput;
import com.google.gerrit.extensions.api.changes.ReviewInput.CommentInput;
import com.google.gerrit.extensions.api.changes.RevisionApi;
import com.google.gerrit.extensions.api.projects.BranchInput;
import com.google.gerrit.extensions.client.ChangeStatus;
import com.google.gerrit.extensions.client.ReviewerState;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.AccountInfo;
import com.google.gerrit.extensions.common.ApprovalInfo;
import com.google.gerrit.extensions.common.ChangeInfo;
import com.google.gerrit.extensions.common.ChangeMessageInfo;
import com.google.gerrit.extensions.common.CommentInfo;
import com.google.gerrit.extensions.common.CommitInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.common.GitPerson;
import com.google.gerrit.extensions.common.LabelInfo;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.extensions.common.RevisionInfo;
import com.google.gerrit.extensions.common.WebLinkInfo;
import com.google.gerrit.extensions.events.ChangeIndexedListener;
import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.BinaryResult;
import com.google.gerrit.extensions.restapi.ETagView;
import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
import com.google.gerrit.server.change.RevisionResource;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.restapi.change.GetRevisionActions;
import com.google.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import org.eclipse.jgit.junit.TestRepository;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.junit.Test;

public class RevisionIT extends AbstractDaemonTest {
  @Inject private GetRevisionActions getRevisionActions;
  @Inject private ProjectOperations projectOperations;
  @Inject private RequestScopeOperations requestScopeOperations;
  @Inject private ExtensionRegistry extensionRegistry;

  @Test
  public void reviewTriplet() throws Exception {
    PushOneCommit.Result r = createChange();
    gApi.changes()
        .id(project.get() + "~master~" + r.getChangeId())
        .revision(r.getCommit().name())
        .review(ReviewInput.approve());
  }

  @Test
  public void reviewCurrent() throws Exception {
    PushOneCommit.Result r = createChange();
    gApi.changes().id(r.getChangeId()).current().review(ReviewInput.approve());
  }

  @Test
  public void reviewNumber() throws Exception {
    PushOneCommit.Result r = createChange();
    gApi.changes().id(r.getChangeId()).revision(1).review(ReviewInput.approve());

    r = updateChange(r, "new content");
    gApi.changes().id(r.getChangeId()).revision(2).review(ReviewInput.approve());
  }

  @Test
  public void submit() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = project.get() + "~master~" + r.getChangeId();
    gApi.changes().id(changeId).current().review(ReviewInput.approve());
    gApi.changes().id(changeId).current().submit();
    assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);
  }

  @Test
  public void postSubmitApproval() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = project.get() + "~master~" + r.getChangeId();
    gApi.changes().id(changeId).current().review(ReviewInput.recommend());

    String label = "Code-Review";
    ApprovalInfo approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(1);
    assertThat(approval.postSubmit).isNull();

    // Submit by direct push.
    git().push().setRefSpecs(new RefSpec(r.getCommit().name() + ":refs/heads/master")).call();
    assertThat(gApi.changes().id(changeId).get().status).isEqualTo(ChangeStatus.MERGED);

    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(1);
    assertThat(approval.postSubmit).isNull();
    assertPermitted(gApi.changes().id(changeId).get(DETAILED_LABELS), "Code-Review", 1, 2);

    // Repeating the current label is allowed. Does not flip the postSubmit bit
    // due to deduplication codepath.
    gApi.changes().id(changeId).current().review(ReviewInput.recommend());
    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(1);
    assertThat(approval.postSubmit).isNull();

    // Reducing vote is not allowed.
    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(changeId).current().review(ReviewInput.dislike()));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(1);
    assertThat(approval.postSubmit).isNull();

    // Increasing vote is allowed.
    gApi.changes().id(changeId).current().review(ReviewInput.approve());
    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(2);
    assertThat(approval.postSubmit).isTrue();
    assertPermitted(gApi.changes().id(changeId).get(DETAILED_LABELS), "Code-Review", 2);

    // Decreasing to previous post-submit vote is still not allowed.
    thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(changeId).current().review(ReviewInput.dislike()));
    assertThat(thrown)
        .hasMessageThat()
        .isEqualTo("Cannot reduce vote on labels for closed change: Code-Review");
    approval = getApproval(changeId, label);
    assertThat(approval.value).isEqualTo(2);
    assertThat(approval.postSubmit).isTrue();
  }

  @Test
  public void postSubmitApprovalAfterVoteRemoved() throws Exception {
    PushOneCommit.Result r = createChange();
    String changeId = project.get() + "~master~" + r.getChangeId();

    requestScopeOperations.setApiUser(admin.id());
    revision(r).review(ReviewInput.approve());

    requestScopeOperations.setApiUser(user.id());
    revision(r).review(ReviewInput.recommend());

    requestScopeOperations.setApiUser(admin.id());
    gApi.changes().id(changeId).reviewer(user.username()).deleteVote("Code-Review");
    Optional<ApprovalInfo> crUser =
        get(changeId, DETAILED_LABELS).labels.get("Code-Review").all.stream()
            .filter(a -> a._accountId == user.id().get())
            .findFirst();
    assertThat(crUser).isPresent();
    assertThat(crUser.get().value).isEqualTo(0);

    revision(r).submit();

    requestScopeOperations.setApiUser(user.id());
    ReviewInput in = new ReviewInput();
    in.label("Code-Review", 1);
    in.message = "Still LGTM";
    revision(r).review(in);

    ApprovalInfo cr =
        gApi.changes().id(changeId).get(DETAILED_LABELS).labels.get("Code-Review").all.stream()
            .filter(a -> a._accountId == user.id().get())
            .findFirst()
            .get();
    assertThat(cr.postSubmit).isTrue();
  }

  @Test
  public void postSubmitDeleteApprovalNotAllowed() throws Exception {
    PushOneCommit.Result r = createChange();

    revision(r).review(ReviewInput.approve());
    revision(r).submit();

    ReviewInput in = new ReviewInput();
    in.label("Code-Review", 0);

    ResourceConflictException thrown =
        assertThrows(ResourceConflictException.class, () -> revision(r).review(in));
    assertThat(thrown)
        .hasMessageThat()
        .contains("Cannot reduce vote on labels for closed change: Code-Review");
  }

  @TestProjectInput(submitType = SubmitType.CHERRY_PICK)
  @Test
  public void approvalCopiedDuringSubmitIsNotPostSubmit() throws Exception {
    PushOneCommit.Result r = createChange();
    Change.Id id = r.getChange().getId();
    gApi.changes().id(id.get()).current().review(ReviewInput.approve());
    gApi.changes().id(id.get()).current().submit();

    ChangeData cd = r.getChange();
    assertThat(cd.patchSets()).hasSize(2);
    PatchSetApproval psa =
        Iterators.getOnlyElement(
            cd.currentApprovals().stream().filter(a -> !a.isLegacySubmit()).iterator());
    assertThat(psa.patchSetId().get()).isEqualTo(2);
    assertThat(psa.label()).isEqualTo("Code-Review");
    assertThat(psa.value()).isEqualTo(2);
    assertThat(psa.postSubmit()).isFalse();
  }

  @Test
  public void voteOnAbandonedChange() throws Exception {
    PushOneCommit.Result r = createChange();
    gApi.changes().id(r.getChangeId()).abandon();
    ResourceConflictException thrown =
        assertThrows(
            ResourceConflictException.class,
            () -> gApi.changes().id(r.getChangeId()).current().review(ReviewInput.reject()));
    assertThat(thrown).hasMessageThat().contains("change is closed");
  }

  @Test
  public void voteNotAllowedWithoutPermission() throws Exception {
    PushOneCommit.Result r = createChange();
    requestScopeOperations.setApiUser(user.id());
    AuthException thrown =
        assertThrows(AuthException.class, () -> change(r).current().review(ReviewInput.approve()));
    assertThat(thrown).hasMessageThat().contains("is restricted");
  }

  @Test
  public void cherryPick() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master%topic=someTopic");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.message = "it goes to stable branch";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    assertThat(orig.get().messages).hasSize(1);
    ChangeInfo changeInfo = orig.revision(r.getCommit().name()).cherryPickAsInfo(in);
    assertThat(changeInfo.containsGitConflicts).isNull();
    assertThat(changeInfo.workInProgress).isNull();
    ChangeApi cherry = gApi.changes().id(changeInfo._number);

    ChangeInfo cherryPickChangeInfoWithDetails = cherry.get();
    assertThat(cherryPickChangeInfoWithDetails.workInProgress).isNull();
    assertThat(cherryPickChangeInfoWithDetails.messages).hasSize(1);
    Iterator<ChangeMessageInfo> cherryIt = cherryPickChangeInfoWithDetails.messages.iterator();
    assertThat(cherryIt.next().message).isEqualTo("Patch Set 1: Cherry Picked from branch master.");

    assertThat(cherry.get().subject).contains(in.message);
    assertThat(cherry.get().topic).isEqualTo("someTopic-foo");
    assertThat(cherry.get().cherryPickOfChange).isEqualTo(orig.get()._number);
    assertThat(cherry.get().cherryPickOfPatchSet).isEqualTo(1);

    cherry.current().review(ReviewInput.approve());
    cherry.current().submit();
  }

  @Test
  public void cherryPickWithoutMessage() throws Exception {
    String branch = "foo";

    // Create change to cherry-pick
    PushOneCommit.Result change = createChange();
    RevCommit revCommit = change.getCommit();

    // Create target branch to cherry-pick to.
    gApi.projects().name(project.get()).branch(branch).create(new BranchInput());

    // Cherry-pick without message.
    CherryPickInput input = new CherryPickInput();
    input.destination = branch;
    String changeId =
        gApi.changes()
            .id(change.getChangeId())
            .revision(revCommit.name())
            .cherryPickAsInfo(input)
            .id;

    // Expect that the message of the cherry-picked commit was used for the cherry-pick change.
    ChangeInfo changeInfo = gApi.changes().id(changeId).get();
    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
    assertThat(revInfo).isNotNull();
    assertThat(revInfo.commit.message).isEqualTo(revCommit.getFullMessage());
  }

  @Test
  public void cherryPickSetChangeId() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    String id = "Ideadbeefdeadbeefdeadbeefdeadbeefdeadbe3f";
    in.message = "it goes to foo branch\n\nChange-Id: " + id;

    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    assertThat(orig.get().messages).hasSize(1);
    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);

    ChangeInfo changeInfo = cherry.get();

    // The cherry-pick honors the ChangeId specified in the input message:
    RevisionInfo revInfo = changeInfo.revisions.get(changeInfo.currentRevision);
    assertThat(revInfo).isNotNull();
    assertThat(revInfo.commit.message).endsWith(id + "\n");
  }

  @Test
  public void cherryPickWithNoTopic() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.message = "it goes to stable branch";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isNull();
    cherry.current().review(ReviewInput.approve());
    cherry.current().submit();
  }

  @Test
  public void cherryPickWithSetTopic() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.topic = "topic";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isEqualTo("topic");
  }

  @Test
  public void cherryPickNewPatchsetWithSetTopic() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isNull();
    in.topic = "topic";
    cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isEqualTo("topic");
  }

  @Test
  public void cherryPickNewPatchsetWithNoTopic() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.topic = "topic";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().topic).isEqualTo("topic");

    in.topic = null;
    cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    // confirm that the topic doesn't change when not specified.
    assertThat(cherry.get().topic).isEqualTo("topic");
  }

  @Test
  public void cherryPickWorkInProgressChange() throws Exception {
    PushOneCommit.Result r = pushTo("refs/for/master%wip");
    CherryPickInput in = new CherryPickInput();
    in.destination = "foo";
    in.message = "cherry pick message";
    gApi.projects().name(project.get()).branch(in.destination).create(new BranchInput());
    ChangeApi orig = gApi.changes().id(project.get() + "~master~" + r.getChangeId());

    ChangeApi cherry = orig.revision(r.getCommit().name()).cherryPick(in);
    assertThat(cherry.get().cherryPickOfChange).isEqualTo(orig.get()._number);
    assertThat(cherry.get().cherryPickOfPatchSet).isEqualTo(1);
    assertThat(cherry.get().workInProgress).isTrue();
  }

  @Test
  public void cherryPickToSameBranch() throws Exception {
    PushOneCommit.Result r = createChange();
    ChangeApi change = gApi.changes().id(project.get() + "~master~" + r.getChangeId());
    CherryPickInput in = new CherryPickInput();
    in.destination = "master";
    in.message = "it generates a new patch set\n\nChange-Id: " + r.getChangeId();
    ChangeInfo cherryInfo = change.revision(r.getCommit().name()).cherryPick(in).get();
    assertThat(cherryInfo.messages).hasSize(2);
    Iterator<ChangeMessageInfo> cherryIt = cherryInfo.messages.iterator();
{'fraction_non_alphanumeric': 0.13590924682706915, 'fraction_numerical': 0.0017150977605723526, 'mean_word_length': 4.665602443087174, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 25, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4824151', 'n_tokens_mistral': 12470, 'n_tokens_neox': 12173, 'n_words': 2052}