Mark hunks which are present due to a rebase when diffing patch sets

The diff between two patch sets contains hunks which weren't
introduced by either patch set if a rebase occurred between those
patch sets. Previous optimizations for this case simply omitted all
files which aren't touched by either of the patch sets.

This change goes one step further: All hunks which can clearly be
identified as being introduced by a rebase are marked. In case of
doubt (e.g. they overlap with a regular hunk), they aren't marked.

To be consistent with the previous behavior, we exclude all files from
the result which only contain hunks due to rebase. In some cases (e.g.
a patch set touches 'fileA' but all identified hunks were introduced
by the rebase), this rule can be stricter than the previously mentioned
(as the previous rule would still show file 'fileA' but we exclude it
now).

Hunks which are introduced by a rebase are identified by computing
the diff between the parents of the two patch sets and transforming
the result to differences in terms of treeA of patch set A and treeB
of patch set B. This follows a suggestion which was posted as a comment
(https://gerrit-review.googlesource.com/c/33091/5//COMMIT_MSG#7) in
change I63d3a21ad4f.

As we always determine which hunks are introduced by a rebase when
two commits are explicitly specified which don't share a common
parent, we will determine those hunks even when we compute the
diff between the parents of the patch sets provided that those
parents fulfill the condition of being separated by a rebase. Those
situations should be rare and hence we refrain from adding
optimizations for this case for the moment.

Bug: Issue 217
Change-Id: If06381d506d360f0e3d24d078dcb54692698e766
This commit is contained in:
Alice Kober-Sotzek 2017-05-04 09:59:28 +02:00
parent 3227396d83
commit 2f62486149
13 changed files with 1713 additions and 91 deletions

View File

@ -5905,19 +5905,21 @@ in a file.
[options="header",cols="1,^1,5"]
|==========================
|Field Name ||Description
|`a` |optional|Content only in the file on side A (deleted in B).
|`b` |optional|Content only in the file on side B (added in B).
|`ab` |optional|Content in the file on both sides (unchanged).
|`edit_a` |only present during a replace, i.e. both `a` and `b` are present|
|Field Name ||Description
|`a` |optional|Content only in the file on side A (deleted in B).
|`b` |optional|Content only in the file on side B (added in B).
|`ab` |optional|Content in the file on both sides (unchanged).
|`edit_a` |only present during a replace, i.e. both `a` and `b` are present|
Text sections deleted from side A as a
link:#diff-intraline-info[DiffIntralineInfo] entity.
|`edit_b` |only present during a replace, i.e. both `a` and `b` are present|
|`edit_b` |only present during a replace, i.e. both `a` and `b` are present|
Text sections inserted in side B as a
link:#diff-intraline-info[DiffIntralineInfo] entity.
|`skip` |optional|count of lines skipped on both sides when the file is
|`due_to_rebase`|not set if `false`|Indicates whether this entry was introduced by a
rebase.
|`skip` |optional|count of lines skipped on both sides when the file is
too large to include all common lines.
|`common` |optional|Set to `true` if the region is common according
|`common` |optional|Set to `true` if the region is common according
to the requested ignore-whitespace parameter, but a and b contain
differing amounts of whitespace. When present and true a and b are
used instead of ab.

View File

@ -0,0 +1,975 @@
// Copyright (C) 2017 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.gerrit.extensions.common.DiffInfoSubject.assertThat;
import static com.google.gerrit.extensions.common.FileInfoSubject.assertThat;
import static com.google.gerrit.reviewdb.client.Patch.COMMIT_MSG;
import com.google.common.collect.ImmutableMap;
import com.google.gerrit.acceptance.AbstractDaemonTest;
import com.google.gerrit.acceptance.GitUtil;
import com.google.gerrit.acceptance.PushOneCommit;
import com.google.gerrit.acceptance.PushOneCommit.Result;
import com.google.gerrit.common.RawInputUtil;
import com.google.gerrit.extensions.api.changes.RebaseInput;
import com.google.gerrit.extensions.common.ChangeType;
import com.google.gerrit.extensions.common.DiffInfo;
import com.google.gerrit.extensions.common.FileInfo;
import com.google.gerrit.extensions.restapi.BinaryResult;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.eclipse.jgit.lib.ObjectId;
import org.junit.Before;
import org.junit.Test;
public class RevisionDiffIT extends AbstractDaemonTest {
private static final String FILE_NAME = "some_file.txt";
private static final String FILE_NAME2 = "another_file.txt";
private static final String FILE_CONTENT =
IntStream.rangeClosed(1, 100)
.mapToObj(number -> String.format("Line %d\n", number))
.collect(Collectors.joining());
private static final String FILE_CONTENT2 = "1st line\n2nd line\n3rd line\n";
private ObjectId commit1;
private String changeId;
private String initialPatchSetId;
@Before
public void setUp() throws Exception {
ObjectId headCommit = testRepo.getRepository().resolve("HEAD");
commit1 =
addCommit(headCommit, ImmutableMap.of(FILE_NAME, FILE_CONTENT, FILE_NAME2, FILE_CONTENT2));
Result result = createEmptyChange();
changeId = result.getChangeId();
initialPatchSetId = result.getPatchSetId().getId();
}
@Test
public void addedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
ObjectId commit2 = addCommit(commit1, "file_added_in_another_commit.txt", "Some file content");
rebaseChangeOn(changeId, commit2);
addModifiedPatchSet(changeId, FILE_NAME, "Another line\n"::concat);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME);
}
@Test
public void removedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
ObjectId commit2 = addCommitRemovingFiles(commit1, FILE_NAME2);
rebaseChangeOn(changeId, commit2);
addModifiedPatchSet(changeId, FILE_NAME, "Another line\n"::concat);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME);
}
@Test
public void renamedUnrelatedFileIsIgnored_ForPatchSetDiffWithRebase() throws Exception {
ObjectId commit2 = addCommitRenamingFile(commit1, FILE_NAME2, "a_new_file_name.txt");
rebaseChangeOn(changeId, commit2);
addModifiedPatchSet(changeId, FILE_NAME, "Another line\n"::concat);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG, FILE_NAME);
}
@Test
public void filesNotTouchedByPatchSetsAndContainingOnlyRebaseHunksAreIgnored() throws Exception {
String newFileContent = FILE_CONTENT.replace("Line 10\n", "Line ten\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
ObjectId commit3 = addCommitRenamingFile(commit2, FILE_NAME2, "a_new_file_name.txt");
rebaseChangeOn(changeId, commit3);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG);
}
@Test
public void filesTouchedByPatchSetsAndContainingOnlyRebaseHunksAreIgnored() throws Exception {
addModifiedPatchSet(
changeId, FILE_NAME, fileContent -> fileContent.replace("Line 50\n", "Line fifty\n"));
addModifiedPatchSet(
changeId, FILE_NAME2, fileContent -> fileContent.replace("1st line\n", "First line\n"));
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
// Revert the modification to allow rebasing.
addModifiedPatchSet(
changeId, FILE_NAME2, fileContent -> fileContent.replace("First line\n", "1st line\n"));
String newFileContent = FILE_CONTENT.replace("Line 10\n", "Line ten\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
String newFilePath = "a_new_file_name.txt";
ObjectId commit3 = addCommitRenamingFile(commit2, FILE_NAME2, newFilePath);
rebaseChangeOn(changeId, commit3);
// Apply the modification again to bring the file into the same state as for the previous
// patch set.
addModifiedPatchSet(
changeId, newFilePath, fileContent -> fileContent.replace("1st line\n", "First line\n"));
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(previousPatchSetId);
assertThat(changedFiles.keySet()).containsExactly(COMMIT_MSG);
}
@Test
public void rebaseHunksAtStartOfFileAreIdentified() throws Exception {
String newFileContent =
FILE_CONTENT.replace("Line 1\n", "Line one\n").replace("Line 5\n", "Line five\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line 50\n", "Line fifty\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1");
assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one");
assertThat(diffInfo).content().element(0).isDueToRebase();
assertThat(diffInfo).content().element(1).commonLines().hasSize(3);
assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 5");
assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line five");
assertThat(diffInfo).content().element(2).isDueToRebase();
assertThat(diffInfo).content().element(3).commonLines().hasSize(44);
assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 50");
assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line fifty");
assertThat(diffInfo).content().element(4).isNotDueToRebase();
assertThat(diffInfo).content().element(5).commonLines().hasSize(50);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunksAtEndOfFileAreIdentified() throws Exception {
String newFileContent =
FILE_CONTENT
.replace("Line 60\n", "Line sixty\n")
.replace("Line 100\n", "Line one hundred\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line 50\n", "Line fifty\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(49);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 50");
assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line fifty");
assertThat(diffInfo).content().element(1).isNotDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(9);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 60");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line sixty");
assertThat(diffInfo).content().element(3).isDueToRebase();
assertThat(diffInfo).content().element(4).commonLines().hasSize(39);
assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 100");
assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line one hundred");
assertThat(diffInfo).content().element(5).isDueToRebase();
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunksInBetweenRegularHunksAreIdentified() throws Exception {
String newFileContent =
FILE_CONTENT.replace("Line 40\n", "Line forty\n").replace("Line 45\n", "Line forty five\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent ->
fileContent
.replace("Line 1\n", "Line one\n")
.replace("Line 100\n", "Line one hundred\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1");
assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one");
assertThat(diffInfo).content().element(0).isNotDueToRebase();
assertThat(diffInfo).content().element(1).commonLines().hasSize(38);
assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 40");
assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line forty");
assertThat(diffInfo).content().element(2).isDueToRebase();
assertThat(diffInfo).content().element(3).commonLines().hasSize(4);
assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 45");
assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line forty five");
assertThat(diffInfo).content().element(4).isDueToRebase();
assertThat(diffInfo).content().element(5).commonLines().hasSize(54);
assertThat(diffInfo).content().element(6).linesOfA().containsExactly("Line 100");
assertThat(diffInfo).content().element(6).linesOfB().containsExactly("Line one hundred");
assertThat(diffInfo).content().element(6).isNotDueToRebase();
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
}
@Test
public void rebaseHunkIsIdentifiedWhenMovedDownInPreviousPatchSet() throws Exception {
// Move the code down by introducing additional lines (pure insert + enlarging replacement) in
// the previous patch set.
Function<String, String> contentModification1 =
fileContent ->
"Line zero\n" + fileContent.replace("Line 10\n", "Line ten\nLine ten and a half\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification1);
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification2 =
fileContent -> fileContent.replace("Line 100\n", "Line one hundred\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification2);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(41);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 40");
assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line forty");
assertThat(diffInfo).content().element(1).isDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(59);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 100");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line one hundred");
assertThat(diffInfo).content().element(3).isNotDueToRebase();
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(previousPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunkIsIdentifiedWhenMovedDownInLatestPatchSet() throws Exception {
String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
// Move the code down by introducing additional lines (pure insert + enlarging replacement) in
// the latest patch set.
Function<String, String> contentModification =
fileContent ->
"Line zero\n" + fileContent.replace("Line 10\n", "Line ten\nLine ten and a half\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).linesOfA().isNull();
assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line zero");
assertThat(diffInfo).content().element(0).isNotDueToRebase();
assertThat(diffInfo).content().element(1).commonLines().hasSize(9);
assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 10");
assertThat(diffInfo)
.content()
.element(2)
.linesOfB()
.containsExactly("Line ten", "Line ten and a half");
assertThat(diffInfo).content().element(2).isNotDueToRebase();
assertThat(diffInfo).content().element(3).commonLines().hasSize(29);
assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 40");
assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line forty");
assertThat(diffInfo).content().element(4).isDueToRebase();
assertThat(diffInfo).content().element(5).commonLines().hasSize(60);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunkIsIdentifiedWhenMovedUpInPreviousPatchSet() throws Exception {
// Move the code up by removing lines (pure deletion + shrinking replacement) in the previous
// patch set.
Function<String, String> contentModification1 =
fileContent ->
fileContent.replace("Line 1\n", "").replace("Line 10\nLine 11\n", "Line ten\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification1);
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification2 =
fileContent -> fileContent.replace("Line 100\n", "Line one hundred\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification2);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(37);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 40");
assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line forty");
assertThat(diffInfo).content().element(1).isDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(59);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 100");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line one hundred");
assertThat(diffInfo).content().element(3).isNotDueToRebase();
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(previousPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunkIsIdentifiedWhenMovedUpInLatestPatchSet() throws Exception {
String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
// Move the code up by removing lines (pure deletion + shrinking replacement) in the latest
// patch set.
Function<String, String> contentModification =
fileContent ->
fileContent.replace("Line 1\n", "").replace("Line 10\nLine 11\n", "Line ten\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1");
assertThat(diffInfo).content().element(0).linesOfB().isNull();
assertThat(diffInfo).content().element(0).isNotDueToRebase();
assertThat(diffInfo).content().element(1).commonLines().hasSize(8);
assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 10", "Line 11");
assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line ten");
assertThat(diffInfo).content().element(2).isNotDueToRebase();
assertThat(diffInfo).content().element(3).commonLines().hasSize(28);
assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 40");
assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line forty");
assertThat(diffInfo).content().element(4).isDueToRebase();
assertThat(diffInfo).content().element(5).commonLines().hasSize(60);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(3);
}
@Test
public void modifiedRebaseHunkWithSameRegionConsideredAsRegularHunk() throws Exception {
String newFileContent = FILE_CONTENT.replace("Line 40\n", "Line forty\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line forty\n", "Line modified after rebase\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(39);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 40");
assertThat(diffInfo)
.content()
.element(1)
.linesOfB()
.containsExactly("Line modified after rebase");
assertThat(diffInfo).content().element(1).isNotDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(60);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunkOverlappingAtBeginningConsideredAsRegularHunk() throws Exception {
String newFileContent =
FILE_CONTENT.replace("Line 40\nLine 41\n", "Line forty\nLine forty one\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent ->
fileContent
.replace("Line 39\n", "Line thirty nine\n")
.replace("Line forty one\n", "Line 41\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(38);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 39", "Line 40");
assertThat(diffInfo)
.content()
.element(1)
.linesOfB()
.containsExactly("Line thirty nine", "Line forty");
assertThat(diffInfo).content().element(1).isNotDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(60);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
}
@Test
public void rebaseHunkOverlappingAtEndConsideredAsRegularHunk() throws Exception {
String newFileContent =
FILE_CONTENT.replace("Line 40\nLine 41\n", "Line forty\nLine forty one\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent ->
fileContent
.replace("Line forty\n", "Line 40\n")
.replace("Line 42\n", "Line forty two\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(40);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 41", "Line 42");
assertThat(diffInfo)
.content()
.element(1)
.linesOfB()
.containsExactly("Line forty one", "Line forty two");
assertThat(diffInfo).content().element(1).isNotDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(58);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(2);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
}
@Test
public void rebaseHunkModifiedInsideConsideredAsRegularHunk() throws Exception {
String newFileContent =
FILE_CONTENT.replace(
"Line 39\nLine 40\nLine 41\n", "Line thirty nine\nLine forty\nLine forty one\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line forty\n", "A different line forty\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(38);
assertThat(diffInfo)
.content()
.element(1)
.linesOfA()
.containsExactly("Line 39", "Line 40", "Line 41");
assertThat(diffInfo)
.content()
.element(1)
.linesOfB()
.containsExactly("Line thirty nine", "A different line forty", "Line forty one");
assertThat(diffInfo).content().element(1).isNotDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(59);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(3);
}
@Test
public void rebaseHunkAfterLineNumberChangingOverlappingHunksIsIdentified() throws Exception {
String newFileContent =
FILE_CONTENT
.replace("Line 40\nLine 41\n", "Line forty\nLine forty one\n")
.replace("Line 60\n", "Line sixty\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent ->
fileContent
.replace("Line forty\n", "Line 40\n")
.replace("Line 42\n", "Line forty two\nLine forty two and a half\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(40);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 41", "Line 42");
assertThat(diffInfo)
.content()
.element(1)
.linesOfB()
.containsExactly("Line forty one", "Line forty two", "Line forty two and a half");
assertThat(diffInfo).content().element(1).isNotDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(17);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 60");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line sixty");
assertThat(diffInfo).content().element(3).isDueToRebase();
assertThat(diffInfo).content().element(4).commonLines().hasSize(40);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(2);
}
@Test
public void rebaseHunksOneLineApartFromRegularHunkAreIdentified() throws Exception {
String newFileContent =
FILE_CONTENT.replace("Line 1\n", "Line one\n").replace("Line 5\n", "Line five\n");
ObjectId commit2 = addCommit(commit1, FILE_NAME, newFileContent);
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line 3\n", "Line three\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1");
assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one");
assertThat(diffInfo).content().element(0).isDueToRebase();
assertThat(diffInfo).content().element(1).commonLines().hasSize(1);
assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 3");
assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line three");
assertThat(diffInfo).content().element(2).isNotDueToRebase();
assertThat(diffInfo).content().element(3).commonLines().hasSize(1);
assertThat(diffInfo).content().element(4).linesOfA().containsExactly("Line 5");
assertThat(diffInfo).content().element(4).linesOfB().containsExactly("Line five");
assertThat(diffInfo).content().element(4).isDueToRebase();
assertThat(diffInfo).content().element(5).commonLines().hasSize(95);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
@Test
public void multipleRebaseEditsMixedWithRegularEditsCanBeIdentified() throws Exception {
addModifiedPatchSet(
changeId,
FILE_NAME,
fileContent -> fileContent.replace("Line 7\n", "Line seven\n").replace("Line 24\n", ""));
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
ObjectId commit2 =
addCommit(
commit1,
FILE_NAME,
FILE_CONTENT
.replace("Line 2\n", "Line two\n")
.replace("Line 18\nLine 19\n", "Line eighteen\nLine nineteen\n")
.replace("Line 50\n", "Line fifty\n"));
rebaseChangeOn(changeId, commit2);
addModifiedPatchSet(
changeId,
FILE_NAME,
fileContent ->
fileContent
.replace("Line seven\n", "Line 7\n")
.replace("Line 9\n", "Line nine\n")
.replace("Line 60\n", "Line sixty\n"));
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(1);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 2");
assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line two");
assertThat(diffInfo).content().element(1).isDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(4);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line seven");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 7");
assertThat(diffInfo).content().element(3).isNotDueToRebase();
assertThat(diffInfo).content().element(4).commonLines().hasSize(1);
assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 9");
assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line nine");
assertThat(diffInfo).content().element(5).isNotDueToRebase();
assertThat(diffInfo).content().element(6).commonLines().hasSize(8);
assertThat(diffInfo).content().element(7).linesOfA().containsExactly("Line 18", "Line 19");
assertThat(diffInfo)
.content()
.element(7)
.linesOfB()
.containsExactly("Line eighteen", "Line nineteen");
assertThat(diffInfo).content().element(7).isDueToRebase();
assertThat(diffInfo).content().element(8).commonLines().hasSize(29);
assertThat(diffInfo).content().element(9).linesOfA().containsExactly("Line 50");
assertThat(diffInfo).content().element(9).linesOfB().containsExactly("Line fifty");
assertThat(diffInfo).content().element(9).isDueToRebase();
assertThat(diffInfo).content().element(10).commonLines().hasSize(9);
assertThat(diffInfo).content().element(11).linesOfA().containsExactly("Line 60");
assertThat(diffInfo).content().element(11).linesOfB().containsExactly("Line sixty");
assertThat(diffInfo).content().element(11).isNotDueToRebase();
assertThat(diffInfo).content().element(12).commonLines().hasSize(40);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(previousPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(3);
}
@Test
public void multipleRebaseEditsMixedWithRegularEditsCanBeIdentified_WithIntraline()
throws Exception {
addModifiedPatchSet(
changeId,
FILE_NAME,
fileContent -> fileContent.replace("Line 7\n", "Line seven\n").replace("Line 24\n", ""));
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
ObjectId commit2 =
addCommit(
commit1,
FILE_NAME,
FILE_CONTENT
.replace("Line 2\n", "Line two\n")
.replace("Line 18\nLine 19\n", "Line eighteen\nLine nineteen\n")
.replace("Line 50\n", "Line fifty\n"));
rebaseChangeOn(changeId, commit2);
addModifiedPatchSet(
changeId,
FILE_NAME,
fileContent ->
fileContent
.replace("Line seven\n", "Line 7\n")
.replace("Line 9\n", "Line nine\n")
.replace("Line 60\n", "Line sixty\n"));
DiffInfo diffInfo =
gApi.changes()
.id(changeId)
.current()
.file(FILE_NAME)
.diffRequest()
.withBase(previousPatchSetId)
.withIntraline(true)
.get();
assertThat(diffInfo).content().element(0).commonLines().hasSize(1);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 2");
assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line two");
assertThat(diffInfo).content().element(1).isDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(4);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line seven");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 7");
assertThat(diffInfo).content().element(3).isNotDueToRebase();
assertThat(diffInfo).content().element(4).commonLines().hasSize(1);
assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 9");
assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line nine");
assertThat(diffInfo).content().element(5).isNotDueToRebase();
assertThat(diffInfo).content().element(6).commonLines().hasSize(8);
assertThat(diffInfo).content().element(7).linesOfA().containsExactly("Line 18", "Line 19");
assertThat(diffInfo)
.content()
.element(7)
.linesOfB()
.containsExactly("Line eighteen", "Line nineteen");
assertThat(diffInfo).content().element(7).isDueToRebase();
assertThat(diffInfo).content().element(8).commonLines().hasSize(29);
assertThat(diffInfo).content().element(9).linesOfA().containsExactly("Line 50");
assertThat(diffInfo).content().element(9).linesOfB().containsExactly("Line fifty");
assertThat(diffInfo).content().element(9).isDueToRebase();
assertThat(diffInfo).content().element(10).commonLines().hasSize(9);
assertThat(diffInfo).content().element(11).linesOfA().containsExactly("Line 60");
assertThat(diffInfo).content().element(11).linesOfB().containsExactly("Line sixty");
assertThat(diffInfo).content().element(11).isNotDueToRebase();
assertThat(diffInfo).content().element(12).commonLines().hasSize(40);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(previousPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(3);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(3);
}
@Test
public void deletedFileDuringRebaseConsideredAsRegularHunkWhenModifiedInDiff() throws Exception {
// Modify the file and revert the modifications to allow rebasing.
addModifiedPatchSet(
changeId, FILE_NAME, fileContent -> fileContent.replace("Line 50\n", "Line fifty\n"));
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
addModifiedPatchSet(
changeId, FILE_NAME, fileContent -> fileContent.replace("Line fifty\n", "Line 50\n"));
ObjectId commit2 = addCommitRemovingFiles(commit1, FILE_NAME);
rebaseChangeOn(changeId, commit2);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId);
assertThat(diffInfo).changeType().isEqualTo(ChangeType.DELETED);
assertThat(diffInfo).content().element(0).linesOfA().hasSize(100);
assertThat(diffInfo).content().element(0).linesOfB().isNull();
assertThat(diffInfo).content().element(0).isNotDueToRebase();
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(previousPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isNull();
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(100);
}
@Test
public void addedFileDuringRebaseConsideredAsRegularHunkWhenModifiedInDiff() throws Exception {
String newFilePath = "a_new_file.txt";
ObjectId commit2 = addCommit(commit1, newFilePath, "1st line\n2nd line\n3rd line\n");
rebaseChangeOn(changeId, commit2);
addModifiedPatchSet(
changeId, newFilePath, fileContent -> fileContent.replace("1st line\n", "First line\n"));
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(newFilePath).diff(initialPatchSetId);
assertThat(diffInfo).changeType().isEqualTo(ChangeType.ADDED);
assertThat(diffInfo).content().element(0).linesOfA().isNull();
assertThat(diffInfo).content().element(0).linesOfB().hasSize(3);
assertThat(diffInfo).content().element(0).isNotDueToRebase();
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(newFilePath)).linesInserted().isEqualTo(3);
assertThat(changedFiles.get(newFilePath)).linesDeleted().isNull();
}
@Test
public void rebaseHunkInRenamedFileIsIdentified_WhenFileIsRenamedDuringRebase() throws Exception {
String renamedFilePath = "renamed_some_file.txt";
ObjectId commit2 =
addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 1\n", "Line one\n"));
ObjectId commit3 = addCommitRenamingFile(commit2, FILE_NAME, renamedFilePath);
rebaseChangeOn(changeId, commit3);
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line 50\n", "Line fifty\n");
addModifiedPatchSet(changeId, renamedFilePath, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(renamedFilePath).diff(initialPatchSetId);
assertThat(diffInfo).content().element(0).linesOfA().containsExactly("Line 1");
assertThat(diffInfo).content().element(0).linesOfB().containsExactly("Line one");
assertThat(diffInfo).content().element(0).isDueToRebase();
assertThat(diffInfo).content().element(1).commonLines().hasSize(48);
assertThat(diffInfo).content().element(2).linesOfA().containsExactly("Line 50");
assertThat(diffInfo).content().element(2).linesOfB().containsExactly("Line fifty");
assertThat(diffInfo).content().element(2).isNotDueToRebase();
assertThat(diffInfo).content().element(3).commonLines().hasSize(50);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(initialPatchSetId);
assertThat(changedFiles.get(renamedFilePath)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(renamedFilePath)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunkInRenamedFileIsIdentified_WhenFileIsRenamedInPatchSets() throws Exception {
String renamedFilePath = "renamed_some_file.txt";
gApi.changes().id(changeId).edit().renameFile(FILE_NAME, renamedFilePath);
gApi.changes().id(changeId).edit().publish();
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
// Revert the renaming to be able to rebase.
gApi.changes().id(changeId).edit().renameFile(renamedFilePath, FILE_NAME);
gApi.changes().id(changeId).edit().publish();
ObjectId commit2 =
addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 5\n", "Line five\n"));
rebaseChangeOn(changeId, commit2);
gApi.changes().id(changeId).edit().renameFile(FILE_NAME, renamedFilePath);
gApi.changes().id(changeId).edit().publish();
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line 50\n", "Line fifty\n");
addModifiedPatchSet(changeId, renamedFilePath, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(renamedFilePath).diff(previousPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(4);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line 5");
assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line five");
assertThat(diffInfo).content().element(1).isDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(44);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 50");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line fifty");
assertThat(diffInfo).content().element(3).isNotDueToRebase();
assertThat(diffInfo).content().element(4).commonLines().hasSize(50);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(previousPatchSetId);
assertThat(changedFiles.get(renamedFilePath)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(renamedFilePath)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunksWhenRebasingOnAnotherChangeAreIdentified() throws Exception {
ObjectId commit2 =
addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 5\n", "Line five\n"));
rebaseChangeOn(changeId, commit2);
String previousPatchSetId = gApi.changes().id(changeId).get().currentRevision;
ObjectId commit3 =
addCommit(commit1, FILE_NAME, FILE_CONTENT.replace("Line 35\n", "Line thirty five\n"));
rebaseChangeOn(changeId, commit3);
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line 20\n", "Line twenty\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
DiffInfo diffInfo =
gApi.changes().id(changeId).current().file(FILE_NAME).diff(previousPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(4);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line five");
assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 5");
assertThat(diffInfo).content().element(1).isDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(14);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line 20");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line twenty");
assertThat(diffInfo).content().element(3).isNotDueToRebase();
assertThat(diffInfo).content().element(4).commonLines().hasSize(14);
assertThat(diffInfo).content().element(5).linesOfA().containsExactly("Line 35");
assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line thirty five");
assertThat(diffInfo).content().element(5).isDueToRebase();
assertThat(diffInfo).content().element(6).commonLines().hasSize(65);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).current().files(previousPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
@Test
public void rebaseHunksWhenReversingPatchSetOrderAreIdentified() throws Exception {
ObjectId commit2 =
addCommit(
commit1,
FILE_NAME,
FILE_CONTENT.replace("Line 5\n", "Line five\n").replace("Line 35\n", ""));
rebaseChangeOn(changeId, commit2);
Function<String, String> contentModification =
fileContent -> fileContent.replace("Line 20\n", "Line twenty\n");
addModifiedPatchSet(changeId, FILE_NAME, contentModification);
String currentPatchSetId = gApi.changes().id(changeId).get().currentRevision;
DiffInfo diffInfo =
gApi.changes()
.id(changeId)
.revision(initialPatchSetId)
.file(FILE_NAME)
.diff(currentPatchSetId);
assertThat(diffInfo).content().element(0).commonLines().hasSize(4);
assertThat(diffInfo).content().element(1).linesOfA().containsExactly("Line five");
assertThat(diffInfo).content().element(1).linesOfB().containsExactly("Line 5");
assertThat(diffInfo).content().element(1).isDueToRebase();
assertThat(diffInfo).content().element(2).commonLines().hasSize(14);
assertThat(diffInfo).content().element(3).linesOfA().containsExactly("Line twenty");
assertThat(diffInfo).content().element(3).linesOfB().containsExactly("Line 20");
assertThat(diffInfo).content().element(3).isNotDueToRebase();
assertThat(diffInfo).content().element(4).commonLines().hasSize(14);
assertThat(diffInfo).content().element(5).linesOfA().isNull();
assertThat(diffInfo).content().element(5).linesOfB().containsExactly("Line 35");
assertThat(diffInfo).content().element(5).isDueToRebase();
assertThat(diffInfo).content().element(6).commonLines().hasSize(65);
Map<String, FileInfo> changedFiles =
gApi.changes().id(changeId).revision(initialPatchSetId).files(currentPatchSetId);
assertThat(changedFiles.get(FILE_NAME)).linesInserted().isEqualTo(1);
assertThat(changedFiles.get(FILE_NAME)).linesDeleted().isEqualTo(1);
}
private void rebaseChangeOn(String changeId, ObjectId newParent) throws Exception {
RebaseInput rebaseInput = new RebaseInput();
rebaseInput.base = newParent.getName();
gApi.changes().id(changeId).current().rebase(rebaseInput);
}
private ObjectId addCommit(ObjectId parentCommit, String filePath, String fileContent)
throws Exception {
ImmutableMap<String, String> files = ImmutableMap.of(filePath, fileContent);
return addCommit(parentCommit, files);
}
private ObjectId addCommit(ObjectId parentCommit, ImmutableMap<String, String> files)
throws Exception {
testRepo.reset(parentCommit);
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), testRepo, "Adjust files of repo", files);
PushOneCommit.Result result = push.to("refs/for/master");
return result.getCommit();
}
private ObjectId addCommitRemovingFiles(ObjectId parentCommit, String... removedFilePaths)
throws Exception {
testRepo.reset(parentCommit);
Map<String, String> files =
Arrays.stream(removedFilePaths)
.collect(Collectors.toMap(Function.identity(), path -> "Irrelevant content"));
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), testRepo, "Remove files from repo", files);
PushOneCommit.Result result = push.rm("refs/for/master");
return result.getCommit();
}
private ObjectId addCommitRenamingFile(
ObjectId parentCommit, String oldFilePath, String newFilePath) throws Exception {
testRepo.reset(parentCommit);
PushOneCommit.Result result = createEmptyChange();
String changeId = result.getChangeId();
gApi.changes().id(changeId).edit().renameFile(oldFilePath, newFilePath);
gApi.changes().id(changeId).edit().publish();
String currentRevision = gApi.changes().id(changeId).get().currentRevision;
GitUtil.fetch(testRepo, "refs/*:refs/*");
return ObjectId.fromString(currentRevision);
}
private Result createEmptyChange() throws Exception {
PushOneCommit push =
pushFactory.create(db, admin.getIdent(), testRepo, "Test change", ImmutableMap.of());
return push.to("refs/for/master");
}
private void addModifiedPatchSet(
String changeId, String filePath, Function<String, String> contentModification)
throws Exception {
try (BinaryResult content = gApi.changes().id(changeId).current().file(filePath).content()) {
String newContent = contentModification.apply(content.asString());
gApi.changes().id(changeId).edit().modifyFile(filePath, RawInputUtil.create(newContent));
}
gApi.changes().id(changeId).edit().publish();
}
}

View File

@ -21,6 +21,7 @@ import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.diff.Edit;
public class PatchScript {
@ -47,6 +48,7 @@ public class PatchScript {
private SparseFileContent a;
private SparseFileContent b;
private List<Edit> edits;
private Set<Edit> editsDueToRebase;
private DisplayMethod displayMethodA;
private DisplayMethod displayMethodB;
private transient String mimeTypeA;
@ -62,30 +64,31 @@ public class PatchScript {
private transient String commitIdB;
public PatchScript(
final Change.Key ck,
final ChangeType ct,
final String on,
final String nn,
final FileMode om,
final FileMode nm,
final List<String> h,
final DiffPreferencesInfo dp,
final SparseFileContent ca,
final SparseFileContent cb,
final List<Edit> e,
final DisplayMethod ma,
final DisplayMethod mb,
final String mta,
final String mtb,
final CommentDetail cd,
final List<Patch> hist,
final boolean hf,
final boolean id,
final boolean idf,
final boolean idt,
Change.Key ck,
ChangeType ct,
String on,
String nn,
FileMode om,
FileMode nm,
List<String> h,
DiffPreferencesInfo dp,
SparseFileContent ca,
SparseFileContent cb,
List<Edit> e,
Set<Edit> editsDueToRebase,
DisplayMethod ma,
DisplayMethod mb,
String mta,
String mtb,
CommentDetail cd,
List<Patch> hist,
boolean hf,
boolean id,
boolean idf,
boolean idt,
boolean bin,
final String cma,
final String cmb) {
String cma,
String cmb) {
changeId = ck;
changeType = ct;
oldName = on;
@ -97,6 +100,7 @@ public class PatchScript {
a = ca;
b = cb;
edits = e;
this.editsDueToRebase = editsDueToRebase;
displayMethodA = ma;
displayMethodB = mb;
mimeTypeA = mta;
@ -210,6 +214,10 @@ public class PatchScript {
return edits;
}
public Set<Edit> getEditsDueToRebase() {
return editsDueToRebase;
}
public boolean isBinary() {
return binary;
}

View File

@ -69,6 +69,10 @@ public class DiffInfo {
public List<List<Integer>> editA;
public List<List<Integer>> editB;
// Indicates that this entry only exists because of a rebase (and not because of a real change
// between 'a' and 'b').
public Boolean dueToRebase;
// a and b are actually common with this whitespace ignore setting.
public Boolean common;

View File

@ -54,6 +54,7 @@ import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.ReplaceEdit;
@ -168,6 +169,7 @@ public class GetDiff implements RestReadView<FileResource> {
psf.setLoadComments(context != DiffPreferencesInfo.WHOLE_FILE_CONTEXT);
PatchScript ps = psf.call();
Content content = new Content(ps);
Set<Edit> editsDueToRebase = ps.getEditsDueToRebase();
for (Edit edit : ps.getEdits()) {
if (edit.getType() == Edit.Type.EMPTY) {
continue;
@ -190,7 +192,8 @@ public class GetDiff implements RestReadView<FileResource> {
case REPLACE:
List<Edit> internalEdit =
edit instanceof ReplaceEdit ? ((ReplaceEdit) edit).getInternalEdits() : null;
content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit);
boolean dueToRebase = editsDueToRebase.contains(edit);
content.addDiff(edit.getEndA(), edit.getEndB(), internalEdit, dueToRebase);
break;
case EMPTY:
default:
@ -372,7 +375,7 @@ public class GetDiff implements RestReadView<FileResource> {
}
}
void addDiff(int endA, int endB, List<Edit> internalEdit) {
void addDiff(int endA, int endB, List<Edit> internalEdit, boolean dueToRebase) {
int lenA = endA - nextA;
int lenB = endB - nextB;
checkState(lenA > 0 || lenB > 0);
@ -408,6 +411,7 @@ public class GetDiff implements RestReadView<FileResource> {
}
}
}
e.dueToRebase = dueToRebase ? true : null;
}
private ContentEntry entry() {

View File

@ -0,0 +1,285 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server.patch;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.Multimaps.toMultimap;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import com.google.auto.value.AutoValue;
import com.google.common.base.MoreObjects;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Stream;
import org.eclipse.jgit.diff.Edit;
/**
* Transformer of edits regarding their base trees. An edit describes a difference between {@code
* treeA} and {@code treeB}. This class allows to describe the edit as a difference between {@code
* treeA'} and {@code treeB'} given the transformation of {@code treeA} to {@code treeA'} and {@code
* treeB} to {@code treeB'}. Edits which can't be transformed due to conflicts with the
* transformation are omitted.
*/
class EditTransformer {
private List<ContextAwareEdit> edits;
/**
* Creates a new {@code EditTransformer} for the edits contained in the specified {@code
* PatchListEntry}s.
*
* @param patchListEntries a list of {@code PatchListEntry}s containing the edits
*/
public EditTransformer(List<PatchListEntry> patchListEntries) {
edits = patchListEntries.stream().flatMap(EditTransformer::toEdits).collect(toImmutableList());
}
/**
* Transforms the references of side A of the edits. If the edits describe differences between
* {@code treeA} and {@code treeB} and the specified {@code PatchListEntry}s define a
* transformation from {@code treeA} to {@code treeA'}, the resulting edits will be defined as
* differences between {@code treeA'} and {@code treeB}. Edits which can't be transformed due to
* conflicts with the transformation are omitted.
*
* @param transformationEntries a list of {@code PatchListEntry}s defining the transformation of
* {@code treeA} to {@code treeA'}
*/
public void transformReferencesOfSideA(List<PatchListEntry> transformationEntries) {
transformEdits(transformationEntries, SideAStrategy.INSTANCE);
}
/**
* Transforms the references of side B of the edits. If the edits describe differences between
* {@code treeA} and {@code treeB} and the specified {@code PatchListEntry}s define a
* transformation from {@code treeB} to {@code treeB'}, the resulting edits will be defined as
* differences between {@code treeA} and {@code treeB'}. Edits which can't be transformed due to
* conflicts with the transformation are omitted.
*
* @param transformationEntries a list of {@code PatchListEntry}s defining the transformation of
* {@code treeB} to {@code treeB'}
*/
public void transformReferencesOfSideB(List<PatchListEntry> transformationEntries) {
transformEdits(transformationEntries, SideBStrategy.INSTANCE);
}
/**
* Returns the transformed {@code Edit}s per file path they modify in {@code treeB'}.
*
* @return the transformed {@code Edit}s per file path
*/
public Multimap<String, Edit> getEditsPerFilePath() {
return edits
.stream()
.collect(
toMultimap(
ContextAwareEdit::getNewFilePath,
ContextAwareEdit::toEdit,
ArrayListMultimap::create));
}
private static Stream<ContextAwareEdit> toEdits(PatchListEntry patchListEntry) {
return patchListEntry
.getEdits()
.stream()
.map(edit -> ContextAwareEdit.create(patchListEntry, edit));
}
private void transformEdits(List<PatchListEntry> transformingEntries, SideStrategy sideStrategy) {
Map<String, List<ContextAwareEdit>> editsPerFilePath =
edits.stream().collect(groupingBy(sideStrategy::getFilePath));
Map<String, PatchListEntry> transformingEntryPerPath =
transformingEntries
.stream()
.collect(toMap(EditTransformer::getOldFilePath, Function.identity()));
edits =
editsPerFilePath
.entrySet()
.stream()
.flatMap(
pathAndEdits -> {
PatchListEntry transformingEntry =
transformingEntryPerPath.get(pathAndEdits.getKey());
return transformEdits(sideStrategy, pathAndEdits.getValue(), transformingEntry)
.stream();
})
.collect(toList());
}
private static String getOldFilePath(PatchListEntry patchListEntry) {
return MoreObjects.firstNonNull(patchListEntry.getOldName(), patchListEntry.getNewName());
}
private static List<ContextAwareEdit> transformEdits(
SideStrategy sideStrategy,
List<ContextAwareEdit> originalEdits,
PatchListEntry transformingEntry) {
if (transformingEntry == null) {
return originalEdits;
}
return transformEdits(
sideStrategy, originalEdits, transformingEntry.getEdits(), transformingEntry.getNewName());
}
private static List<ContextAwareEdit> transformEdits(
SideStrategy sideStrategy,
List<ContextAwareEdit> unorderedOriginalEdits,
List<Edit> unorderedTransformingEdits,
String adjustedFilePath) {
List<ContextAwareEdit> originalEdits = new ArrayList<>(unorderedOriginalEdits);
originalEdits.sort(comparing(sideStrategy::getBegin).thenComparing(sideStrategy::getEnd));
List<Edit> transformingEdits = new ArrayList<>(unorderedTransformingEdits);
transformingEdits.sort(comparing(Edit::getBeginA).thenComparing(Edit::getEndA));
int shiftedAmount = 0;
int transIndex = 0;
int origIndex = 0;
List<ContextAwareEdit> resultingEdits = new ArrayList<>(originalEdits.size());
while (origIndex < originalEdits.size() && transIndex < transformingEdits.size()) {
ContextAwareEdit originalEdit = originalEdits.get(origIndex);
Edit transformingEdit = transformingEdits.get(transIndex);
if (transformingEdit.getEndA() < sideStrategy.getBegin(originalEdit)) {
shiftedAmount = transformingEdit.getEndB() - transformingEdit.getEndA();
transIndex++;
} else if (sideStrategy.getEnd(originalEdit) < transformingEdit.getBeginA()) {
resultingEdits.add(sideStrategy.create(originalEdit, shiftedAmount, adjustedFilePath));
origIndex++;
} else {
// Overlapping -> ignore.
origIndex++;
}
}
for (int i = origIndex; i < originalEdits.size(); i++) {
resultingEdits.add(
sideStrategy.create(originalEdits.get(i), shiftedAmount, adjustedFilePath));
}
return resultingEdits;
}
@AutoValue
abstract static class ContextAwareEdit {
static ContextAwareEdit create(PatchListEntry patchListEntry, Edit edit) {
return create(
patchListEntry.getOldName(),
patchListEntry.getNewName(),
edit.getBeginA(),
edit.getEndA(),
edit.getBeginB(),
edit.getEndB());
}
static ContextAwareEdit create(
String oldFilePath, String newFilePath, int beginA, int endA, int beginB, int endB) {
String adjustedOldFilePath = MoreObjects.firstNonNull(oldFilePath, newFilePath);
return new AutoValue_EditTransformer_ContextAwareEdit(
adjustedOldFilePath, newFilePath, beginA, endA, beginB, endB);
}
public abstract String getOldFilePath();
public abstract String getNewFilePath();
public abstract int getBeginA();
public abstract int getEndA();
public abstract int getBeginB();
public abstract int getEndB();
public Edit toEdit() {
return new Edit(getBeginA(), getEndA(), getBeginB(), getEndB());
}
}
private interface SideStrategy {
String getFilePath(ContextAwareEdit edit);
int getBegin(ContextAwareEdit edit);
int getEnd(ContextAwareEdit edit);
ContextAwareEdit create(ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath);
}
private enum SideAStrategy implements SideStrategy {
INSTANCE;
@Override
public String getFilePath(ContextAwareEdit edit) {
return edit.getOldFilePath();
}
@Override
public int getBegin(ContextAwareEdit edit) {
return edit.getBeginA();
}
@Override
public int getEnd(ContextAwareEdit edit) {
return edit.getEndA();
}
@Override
public ContextAwareEdit create(
ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath) {
return ContextAwareEdit.create(
adjustedFilePath,
edit.getNewFilePath(),
edit.getBeginA() + shiftedAmount,
edit.getEndA() + shiftedAmount,
edit.getBeginB(),
edit.getEndB());
}
}
private enum SideBStrategy implements SideStrategy {
INSTANCE;
@Override
public String getFilePath(ContextAwareEdit edit) {
return edit.getNewFilePath();
}
@Override
public int getBegin(ContextAwareEdit edit) {
return edit.getBeginB();
}
@Override
public int getEnd(ContextAwareEdit edit) {
return edit.getEndB();
}
@Override
public ContextAwareEdit create(
ContextAwareEdit edit, int shiftedAmount, String adjustedFilePath) {
return ContextAwareEdit.create(
edit.getOldFilePath(),
adjustedFilePath,
edit.getBeginA(),
edit.getEndA(),
edit.getBeginB() + shiftedAmount,
edit.getEndB() + shiftedAmount);
}
}
}

View File

@ -25,6 +25,8 @@ import static com.google.gerrit.server.ioutil.BasicSerialization.writeFixInt64;
import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Patch.ChangeType;
import com.google.gerrit.reviewdb.client.Patch.PatchType;
@ -33,9 +35,9 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.patch.CombinedFileHeader;
@ -53,7 +55,8 @@ public class PatchListEntry {
null,
fileName,
EMPTY_HEADER,
Collections.<Edit>emptyList(),
ImmutableList.of(),
ImmutableSet.of(),
0,
0,
0,
@ -65,7 +68,8 @@ public class PatchListEntry {
private final String oldName;
private final String newName;
private final byte[] header;
private final List<Edit> edits;
private final ImmutableList<Edit> edits;
private final ImmutableSet<Edit> editsDueToRebase;
private final int insertions;
private final int deletions;
private final long size;
@ -73,7 +77,8 @@ public class PatchListEntry {
// Note: When adding new fields, the serialVersionUID in PatchListKey must be
// incremented so that entries from the cache are automatically invalidated.
PatchListEntry(FileHeader hdr, List<Edit> editList, long size, long sizeDelta) {
PatchListEntry(
FileHeader hdr, List<Edit> editList, Set<Edit> editsDueToRebase, long size, long sizeDelta) {
changeType = toChangeType(hdr);
patchType = toPatchType(hdr);
@ -103,16 +108,19 @@ public class PatchListEntry {
header = compact(hdr);
if (hdr instanceof CombinedFileHeader || hdr.getHunks().isEmpty()) {
edits = Collections.emptyList();
edits = ImmutableList.of();
} else {
edits = Collections.unmodifiableList(editList);
edits = ImmutableList.copyOf(editList);
}
this.editsDueToRebase = ImmutableSet.copyOf(editsDueToRebase);
int ins = 0;
int del = 0;
for (Edit e : editList) {
del += e.getEndA() - e.getBeginA();
ins += e.getEndB() - e.getBeginB();
if (!editsDueToRebase.contains(e)) {
del += e.getEndA() - e.getBeginA();
ins += e.getEndB() - e.getBeginB();
}
}
insertions = ins;
deletions = del;
@ -126,7 +134,8 @@ public class PatchListEntry {
String oldName,
String newName,
byte[] header,
List<Edit> edits,
ImmutableList<Edit> edits,
ImmutableSet<Edit> editsDueToRebase,
int insertions,
int deletions,
long size,
@ -137,6 +146,7 @@ public class PatchListEntry {
this.newName = newName;
this.header = header;
this.edits = edits;
this.editsDueToRebase = editsDueToRebase;
this.insertions = insertions;
this.deletions = deletions;
this.size = size;
@ -175,10 +185,14 @@ public class PatchListEntry {
return newName;
}
public List<Edit> getEdits() {
public ImmutableList<Edit> getEdits() {
return edits;
}
public ImmutableSet<Edit> getEditsDueToRebase() {
return editsDueToRebase;
}
public int getInsertions() {
return insertions;
}
@ -230,12 +244,17 @@ public class PatchListEntry {
writeFixInt64(out, size);
writeFixInt64(out, sizeDelta);
writeEditArray(out, edits);
writeEditArray(out, editsDueToRebase);
}
private static void writeEditArray(OutputStream out, Collection<Edit> edits) throws IOException {
writeVarInt32(out, edits.size());
for (final Edit e : edits) {
writeVarInt32(out, e.getBeginA());
writeVarInt32(out, e.getEndA());
writeVarInt32(out, e.getBeginB());
writeVarInt32(out, e.getEndB());
for (Edit edit : edits) {
writeVarInt32(out, edit.getBeginA());
writeVarInt32(out, edit.getEndA());
writeVarInt32(out, edit.getBeginB());
writeVarInt32(out, edit.getEndB());
}
}
@ -250,22 +269,34 @@ public class PatchListEntry {
long size = readFixInt64(in);
long sizeDelta = readFixInt64(in);
int editCount = readVarInt32(in);
Edit[] editArray = new Edit[editCount];
for (int i = 0; i < editCount; i++) {
Edit[] editArray = readEditArray(in);
Edit[] editsDueToRebase = readEditArray(in);
return new PatchListEntry(
changeType,
patchType,
oldName,
newName,
hdr,
ImmutableList.copyOf(editArray),
ImmutableSet.copyOf(editsDueToRebase),
ins,
del,
size,
sizeDelta);
}
private static Edit[] readEditArray(InputStream in) throws IOException {
int numEdits = readVarInt32(in);
Edit[] edits = new Edit[numEdits];
for (int i = 0; i < numEdits; i++) {
int beginA = readVarInt32(in);
int endA = readVarInt32(in);
int beginB = readVarInt32(in);
int endB = readVarInt32(in);
editArray[i] = new Edit(beginA, endA, beginB, endB);
edits[i] = new Edit(beginA, endA, beginB, endB);
}
return new PatchListEntry(
changeType, patchType, oldName, newName, hdr, toList(editArray), ins, del, size, sizeDelta);
}
private static List<Edit> toList(Edit[] l) {
return Collections.unmodifiableList(Arrays.asList(l));
return edits;
}
private static byte[] compact(final FileHeader h) {

View File

@ -32,7 +32,7 @@ import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.ObjectId;
public class PatchListKey implements Serializable {
public static final long serialVersionUID = 24L;
public static final long serialVersionUID = 25L;
public static final ImmutableBiMap<Whitespace, Character> WHITESPACE_TYPES =
ImmutableBiMap.of(

View File

@ -17,10 +17,13 @@ package com.google.gerrit.server.patch;
import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toSet;
import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.gerrit.extensions.client.DiffPreferencesInfo.Whitespace;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.Project;
@ -33,8 +36,8 @@ import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
@ -42,8 +45,8 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.stream.Stream;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffEntry.ChangeType;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.diff.Edit;
import org.eclipse.jgit.diff.EditList;
@ -144,7 +147,16 @@ public class PatchListLoader implements Callable<PatchList> {
return save ? repo.newObjectInserter() : new InMemoryInserter(repo);
}
public PatchList readPatchList(Repository repo, RevWalk rw, ObjectInserter ins)
/**
* Computes the {@code PatchList} for a given {@code PatchListKey}.
*
* <p><b>Warning:</b> This method may reset the specified {@code RevWalk}. Don't call it in the
* middle of a walk.
*
* @throws IOException if the repository can't be accessed
* @throws PatchListNotAvailableException if the {@code PatchList} can't be computed
*/
private PatchList readPatchList(Repository repo, RevWalk rw, ObjectInserter ins)
throws IOException, PatchListNotAvailableException {
ObjectReader reader = rw.getObjectReader();
checkArgument(reader.getCreatedFromInserter() == ins);
@ -178,19 +190,6 @@ public class PatchListLoader implements Callable<PatchList> {
df.setDetectRenames(true);
List<DiffEntry> diffEntries = df.scan(aTree, bTree);
Set<String> paths = null;
if (key.getOldId() != null && b.getParentCount() == 1) {
PatchListKey newKey = PatchListKey.againstDefaultBase(key.getNewId(), key.getWhitespace());
PatchListKey oldKey = PatchListKey.againstDefaultBase(key.getOldId(), key.getWhitespace());
paths =
Stream.concat(
patchListCache.get(newKey, project).getPatches().stream(),
patchListCache.get(oldKey, project).getPatches().stream())
.map(PatchListEntry::getNewName)
.collect(toSet());
}
int cnt = diffEntries.size();
List<PatchListEntry> entries = new ArrayList<>();
entries.add(
newCommitMessage(
@ -205,21 +204,141 @@ public class PatchListLoader implements Callable<PatchList> {
b,
comparisonType));
}
for (int i = 0; i < cnt; i++) {
DiffEntry e = diffEntries.get(i);
if (paths == null || paths.contains(e.getNewPath()) || paths.contains(e.getOldPath())) {
FileHeader fh = toFileHeader(key, df, e);
long oldSize = getFileSize(reader, e.getOldMode(), e.getOldPath(), aTree);
long newSize = getFileSize(reader, e.getNewMode(), e.getNewPath(), bTree);
entries.add(newEntry(aTree, fh, newSize, newSize - oldSize));
Multimap<String, Edit> editsDueToRebasePerFilePath =
getEditsDueToRebasePerFilePath(rw, b, aCommit);
for (DiffEntry diffEntry : diffEntries) {
String filePath = diffEntry.getNewPath();
if (diffEntry.getChangeType() == ChangeType.DELETE) {
filePath = diffEntry.getOldPath();
}
Set<Edit> editsDueToRebase = ImmutableSet.copyOf(editsDueToRebasePerFilePath.get(filePath));
Optional<PatchListEntry> patchListEntry =
getPatchListEntry(reader, df, diffEntry, aTree, bTree, editsDueToRebase);
patchListEntry.ifPresent(entries::add);
}
return new PatchList(
a, b, isMerge, comparisonType, entries.toArray(new PatchListEntry[entries.size()]));
}
}
/**
* Identifies the {@code Edit}s which are present between {@code commitA} and {@code commitB} due
* to other commits in between those two. {@code Edit}s which cannot be clearly attributed to
* those other commits (because they overlap with modifications introduced by {@code commitA} or
* {@code commitB}) are omitted from the result. The {@code Edit}s are expressed as differences
* between {@code treeA} of {@code commitA} and {@code treeB} of {@code commitB}.
*
* <p><b>Note:</b> If one of the commits is a merge commit, an empty {@code Multimap} will be
* returned.
*
* <p><b>Warning:</b> This method assumes that commitA and commitB are either a parent and child
* commit or represent two patch sets which belong to the same change. No checks are made to
* confirm this assumption! Passing arbitrary commits to this method may lead to strange results
* or take very long.
*
* <p>This logic could be expanded to arbitrary commits if the following adjustments were applied:
*
* <ul>
* <li>If {@code commitA} is an ancestor of {@code commitB} (or the other way around), {@code
* commitA} (or {@code commitB}) is used instead of its parent in {@link
* #getEditsDueToRebasePerFilePath(RevCommit, RevCommit)}.
* <li>The method {@link #arePatchSetCommitsWithRebaseInBetween(RevWalk, RevCommit, RevCommit)}
* is adjusted to only short-circuit this method if {@code commitA} is the parent of {@code
* commitB} (or the other way around).
* <li>A flag is added to {@link PatchListKey} indicating whether this method should be called.
* As this method calls {@link PatchListCache#get(PatchListKey, Project.NameKey)} (which
* will end up in this method again), we have to make sure that this method doesn't recurse
* until a parent/child pair of commits (or the root of the history) is reached. Introducing
* a flag would be the simplest approach but there certainly are other ways, too.
* <li>Special handling for merge commits is added. If only one of them is a merge commit, the
* whole computation has to be done between the single parent and all parents of the merge
* commit. If both of them are merge commits, all combinations of parents have to be
* considered. Alternatively, we could decide to not support this feature for merge commits
* (or just for specific types of merge commits).
* </ul>
*
* @param revWalk a {@code RevWalk} for the repository
* @param commitA the commit defining {@code treeA}
* @param commitB the commit defining {@code treeB}
* @return the {@code Edit}s per file path they modify in {@code treeB}
* @throws IOException if the repository can't be accessed
* @throws PatchListNotAvailableException if the {@code Edit}s can't be identified
*/
private Multimap<String, Edit> getEditsDueToRebasePerFilePath(
RevWalk revWalk, RevCommit commitB, RevCommit commitA)
throws IOException, PatchListNotAvailableException {
if (!arePatchSetCommitsWithRebaseInBetween(revWalk, commitA, commitB)) {
return ImmutableMultimap.of();
}
return getEditsDueToRebasePerFilePath(commitA, commitB);
}
/**
* Indicates whether {@code commitA} and {@code commitB} represent two patch sets separated by a
* rebase provided the below-mentioned assumption is met.
*
* <p><b>Warning:</b> This method assumes that commitA and commitB are either a parent and child
* commit or represent two patch sets which belong to the same change. No checks are made to
* confirm this assumption!
*
* @throws IOException if the repository can't be accessed
*/
private boolean arePatchSetCommitsWithRebaseInBetween(
RevWalk revWalk, RevCommit commitA, RevCommit commitB) throws IOException {
return key.getOldId() != null
&& commitB.getParentCount() == 1
&& commitA != null
&& commitA.getParentCount() == 1
&& !ObjectId.equals(commitB.getParent(0), commitA.getParent(0))
&& !revWalk.isMergedInto(commitA, commitB)
&& !revWalk.isMergedInto(commitB, commitA);
}
/**
* Determines all {@code Edit}s which were introduced by a rebase. The {@code Edit}s are expressed
* as differences between {@code treeA} of {@code commitA} and {@code treeB} of {@code commitB}.
*
* @param commitA the commit defining {@code treeA}
* @param commitB the commit defining {@code treeB}
* @return the {@code Edit}s per file path they modify in {@code treeB}
* @throws PatchListNotAvailableException if the {@code Edit}s can't be determined
*/
private Multimap<String, Edit> getEditsDueToRebasePerFilePath(
RevCommit commitA, RevCommit commitB) throws PatchListNotAvailableException {
PatchListKey parentDiffKey =
new PatchListKey(commitA.getParent(0), commitB.getParent(0), key.getWhitespace());
PatchList parentPatchList = patchListCache.get(parentDiffKey, project);
PatchListKey oldKey = PatchListKey.againstDefaultBase(key.getOldId(), key.getWhitespace());
PatchList oldPatchList = patchListCache.get(oldKey, project);
PatchListKey newKey = PatchListKey.againstDefaultBase(key.getNewId(), key.getWhitespace());
PatchList newPatchList = patchListCache.get(newKey, project);
EditTransformer editTransformer = new EditTransformer(parentPatchList.getPatches());
editTransformer.transformReferencesOfSideA(oldPatchList.getPatches());
editTransformer.transformReferencesOfSideB(newPatchList.getPatches());
return editTransformer.getEditsPerFilePath();
}
private Optional<PatchListEntry> getPatchListEntry(
ObjectReader objectReader,
DiffFormatter diffFormatter,
DiffEntry diffEntry,
RevTree treeA,
RevTree treeB,
Set<Edit> editsDueToRebase)
throws IOException {
FileHeader fileHeader = toFileHeader(key, diffFormatter, diffEntry);
long oldSize = getFileSize(objectReader, diffEntry.getOldMode(), diffEntry.getOldPath(), treeA);
long newSize = getFileSize(objectReader, diffEntry.getNewMode(), diffEntry.getNewPath(), treeB);
PatchListEntry patchListEntry =
newEntry(treeA, fileHeader, editsDueToRebase, newSize, newSize - oldSize);
// All edits in a file are due to rebase -> exclude the file from the diff.
if (patchListEntry.getEditsDueToRebase().containsAll(patchListEntry.getEdits())) {
return Optional.empty();
}
return Optional.of(patchListEntry);
}
private ComparisonType getComparisonType(RevObject a, RevCommit b) {
for (int i = 0; i < b.getParentCount(); i++) {
if (b.getParent(i).equals(a)) {
@ -327,7 +446,7 @@ public class PatchListLoader implements Callable<PatchList> {
RawText bRawText = new RawText(bContent);
EditList edits = new HistogramDiff().diff(cmp, aRawText, bRawText);
FileHeader fh = new FileHeader(rawHdr, edits, PatchType.UNIFIED);
return new PatchListEntry(fh, edits, size, sizeDelta);
return new PatchListEntry(fh, edits, ImmutableSet.of(), size, sizeDelta);
}
private static byte[] getRawHeader(boolean hasA, String fileName) {
@ -350,18 +469,19 @@ public class PatchListLoader implements Callable<PatchList> {
return hdr.toString().getBytes(UTF_8);
}
private PatchListEntry newEntry(RevTree aTree, FileHeader fileHeader, long size, long sizeDelta) {
private static PatchListEntry newEntry(
RevTree aTree, FileHeader fileHeader, Set<Edit> editsDueToRebase, long size, long sizeDelta) {
if (aTree == null // want combined diff
|| fileHeader.getPatchType() != PatchType.UNIFIED
|| fileHeader.getHunks().isEmpty()) {
return new PatchListEntry(fileHeader, Collections.<Edit>emptyList(), size, sizeDelta);
return new PatchListEntry(fileHeader, ImmutableList.of(), ImmutableSet.of(), size, sizeDelta);
}
List<Edit> edits = fileHeader.toEditList();
if (edits.isEmpty()) {
return new PatchListEntry(fileHeader, Collections.<Edit>emptyList(), size, sizeDelta);
return new PatchListEntry(fileHeader, ImmutableList.of(), ImmutableSet.of(), size, sizeDelta);
}
return new PatchListEntry(fileHeader, edits, size, sizeDelta);
return new PatchListEntry(fileHeader, edits, editsDueToRebase, size, sizeDelta);
}
private RevObject aFor(

View File

@ -216,6 +216,7 @@ class PatchScriptBuilder {
a.dst,
b.dst,
edits,
content.getEditsDueToRebase(),
a.displayMethod,
b.displayMethod,
a.mimeType.toString(),

View File

@ -0,0 +1,81 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.extensions.common;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.StringSubject;
import com.google.common.truth.Subject;
import com.google.common.truth.SubjectFactory;
import com.google.common.truth.Truth;
import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
import com.google.gerrit.truth.ListSubject;
public class ContentEntrySubject extends Subject<ContentEntrySubject, ContentEntry> {
private static final SubjectFactory<ContentEntrySubject, ContentEntry> DIFF_INFO_SUBJECT_FACTORY =
new SubjectFactory<ContentEntrySubject, ContentEntry>() {
@Override
public ContentEntrySubject getSubject(
FailureStrategy failureStrategy, ContentEntry contentEntry) {
return new ContentEntrySubject(failureStrategy, contentEntry);
}
};
public static ContentEntrySubject assertThat(ContentEntry contentEntry) {
return assertAbout(DIFF_INFO_SUBJECT_FACTORY).that(contentEntry);
}
private ContentEntrySubject(FailureStrategy failureStrategy, ContentEntry contentEntry) {
super(failureStrategy, contentEntry);
}
public void isDueToRebase() {
isNotNull();
ContentEntry contentEntry = actual();
Truth.assertWithMessage("Entry should be marked 'dueToRebase'")
.that(contentEntry.dueToRebase)
.named("dueToRebase")
.isTrue();
}
public void isNotDueToRebase() {
isNotNull();
ContentEntry contentEntry = actual();
Truth.assertWithMessage("Entry should not be marked 'dueToRebase'")
.that(contentEntry.dueToRebase)
.named("dueToRebase")
.isNull();
}
public ListSubject<StringSubject, String> commonLines() {
isNotNull();
ContentEntry contentEntry = actual();
return ListSubject.assertThat(contentEntry.ab, Truth::assertThat).named("common lines");
}
public ListSubject<StringSubject, String> linesOfA() {
isNotNull();
ContentEntry contentEntry = actual();
return ListSubject.assertThat(contentEntry.a, Truth::assertThat).named("lines of 'a'");
}
public ListSubject<StringSubject, String> linesOfB() {
isNotNull();
ContentEntry contentEntry = actual();
return ListSubject.assertThat(contentEntry.b, Truth::assertThat).named("lines of 'b'");
}
}

View File

@ -0,0 +1,57 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.extensions.common;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.ComparableSubject;
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.Subject;
import com.google.common.truth.SubjectFactory;
import com.google.common.truth.Truth;
import com.google.gerrit.extensions.common.DiffInfo.ContentEntry;
import com.google.gerrit.truth.ListSubject;
public class DiffInfoSubject extends Subject<DiffInfoSubject, DiffInfo> {
private static final SubjectFactory<DiffInfoSubject, DiffInfo> DIFF_INFO_SUBJECT_FACTORY =
new SubjectFactory<DiffInfoSubject, DiffInfo>() {
@Override
public DiffInfoSubject getSubject(FailureStrategy failureStrategy, DiffInfo diffInfo) {
return new DiffInfoSubject(failureStrategy, diffInfo);
}
};
public static DiffInfoSubject assertThat(DiffInfo diffInfo) {
return assertAbout(DIFF_INFO_SUBJECT_FACTORY).that(diffInfo);
}
private DiffInfoSubject(FailureStrategy failureStrategy, DiffInfo diffInfo) {
super(failureStrategy, diffInfo);
}
public ListSubject<ContentEntrySubject, ContentEntry> content() {
isNotNull();
DiffInfo diffInfo = actual();
return ListSubject.assertThat(diffInfo.content, ContentEntrySubject::assertThat)
.named("content");
}
public ComparableSubject<?, ChangeType> changeType() {
isNotNull();
DiffInfo diffInfo = actual();
return Truth.assertThat(diffInfo.changeType).named("changeType");
}
}

View File

@ -0,0 +1,54 @@
// Copyright (C) 2017 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.extensions.common;
import static com.google.common.truth.Truth.assertAbout;
import com.google.common.truth.FailureStrategy;
import com.google.common.truth.IntegerSubject;
import com.google.common.truth.Subject;
import com.google.common.truth.SubjectFactory;
import com.google.common.truth.Truth;
public class FileInfoSubject extends Subject<FileInfoSubject, FileInfo> {
private static final SubjectFactory<FileInfoSubject, FileInfo> FILE_INFO_SUBJECT_FACTORY =
new SubjectFactory<FileInfoSubject, FileInfo>() {
@Override
public FileInfoSubject getSubject(FailureStrategy failureStrategy, FileInfo fileInfo) {
return new FileInfoSubject(failureStrategy, fileInfo);
}
};
public static FileInfoSubject assertThat(FileInfo fileInfo) {
return assertAbout(FILE_INFO_SUBJECT_FACTORY).that(fileInfo);
}
private FileInfoSubject(FailureStrategy failureStrategy, FileInfo fileInfo) {
super(failureStrategy, fileInfo);
}
public IntegerSubject linesInserted() {
isNotNull();
FileInfo fileInfo = actual();
return Truth.assertThat(fileInfo.linesInserted).named("linesInserted");
}
public IntegerSubject linesDeleted() {
isNotNull();
FileInfo fileInfo = actual();
return Truth.assertThat(fileInfo.linesDeleted).named("linesDeleted");
}
}