IDEA-53175 properly commit case only rename

If there are case only renames selected for commit perform
commit without paths (otherwise it would fail):
1. stage addition/removal of other selected files;
2. check if there are deselected files which are already in index;
3. unstage them via `git reset`
   (a proper warning about that should be show by a CheckinHandler).
4. `git commit` without specifying paths.
5. stage back the changes unstaged on step #3.
This commit is contained in:
Kirill Likhodedov
2016-05-30 17:46:16 +03:00
parent 34206c66dd
commit 7f29cae5ab
6 changed files with 483 additions and 52 deletions
@@ -39,7 +39,7 @@ abstract class VcsPlatformTest : PlatformTestCase() {
private lateinit var myTestStartedIndicator: String
protected lateinit var myChangeListManager: ChangeListManager
protected lateinit var changeListManager: ChangeListManager
@Throws(Exception::class)
override fun setUp() {
@@ -56,7 +56,7 @@ abstract class VcsPlatformTest : PlatformTestCase() {
myProjectRoot = myProject.baseDir
myProjectPath = myProjectRoot.path
myChangeListManager = ChangeListManager.getInstance(myProject)
changeListManager = ChangeListManager.getInstance(myProject)
}
@Throws(Exception::class)
@@ -35,6 +35,7 @@ import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.changes.ChangeListManagerEx;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vcs.vfs.AbstractVcsVirtualFile;
import com.intellij.openapi.vfs.CharsetToolkit;
@@ -70,6 +71,7 @@ import java.util.*;
import static com.intellij.dvcs.DvcsUtil.getShortRepositoryName;
import static com.intellij.dvcs.DvcsUtil.joinShortNames;
import static com.intellij.util.ObjectUtils.assertNotNull;
/**
* Git utility/helper methods
@@ -1068,4 +1070,23 @@ public class GitUtil {
}
return false;
}
@NotNull
public static String getLogString(@NotNull String root, @NotNull Collection<Change> changes) {
return StringUtil.join(changes, change -> {
ContentRevision after = change.getAfterRevision();
ContentRevision before = change.getBeforeRevision();
switch (change.getType()) {
case NEW: return "A: " + getRelativePath(root, assertNotNull(after));
case DELETED: return "D: " + getRelativePath(root, assertNotNull(before));
case MOVED: return "M: " + getRelativePath(root, assertNotNull(before)) + " -> " + getRelativePath(root, assertNotNull(after));
default: return "M: " + getRelativePath(root, assertNotNull(after));
}
}, ", ");
}
@Nullable
public static String getRelativePath(@NotNull String root, @NotNull ContentRevision after) {
return FileUtil.getRelativePath(root, after.getFile().getPath(), File.separatorChar);
}
}
@@ -26,6 +26,8 @@ import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsUtil;
import git4idea.GitContentRevision;
import git4idea.GitRevisionNumber;
import git4idea.GitUtil;
@@ -395,6 +397,17 @@ public class GitChangeUtils {
return changes;
}
@NotNull
public static Collection<Change> getStagedChanges(@NotNull Project project, @NotNull VirtualFile root) throws VcsException {
GitSimpleHandler diff = new GitSimpleHandler(project, root, GitCommand.DIFF);
diff.addParameters("--name-status", "--cached", "-M");
String output = diff.run();
Collection<Change> changes = new ArrayList<>();
parseChanges(project, root, null, GitRevisionNumber.HEAD, output, changes, Collections.emptySet());
return changes;
}
@NotNull
public static Collection<Change> getDiffWithWorkingDir(@NotNull Project project,
@NotNull VirtualFile root,
@@ -23,7 +23,9 @@ import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vcs.CheckinProjectPanel;
@@ -41,7 +43,6 @@ import com.intellij.util.Function;
import com.intellij.util.FunctionUtil;
import com.intellij.util.NullableFunction;
import com.intellij.util.PairConsumer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.textCompletion.DefaultTextCompletionValueDescriptor;
import com.intellij.util.textCompletion.TextCompletionProvider;
import com.intellij.util.textCompletion.TextFieldWithCompletion;
@@ -56,6 +57,7 @@ import com.intellij.vcsUtil.VcsUtil;
import git4idea.GitUtil;
import git4idea.GitVcs;
import git4idea.branch.GitBranchUtil;
import git4idea.changes.GitChangeUtils;
import git4idea.commands.GitCommand;
import git4idea.commands.GitSimpleHandler;
import git4idea.config.GitConfigUtil;
@@ -76,8 +78,14 @@ import java.text.SimpleDateFormat;
import java.util.*;
import java.util.List;
import static com.intellij.dvcs.DvcsUtil.getShortRepositoryName;
import static com.intellij.openapi.vcs.changes.ChangesUtil.getAfterPath;
import static com.intellij.openapi.vcs.changes.ChangesUtil.getBeforePath;
import static com.intellij.util.containers.ContainerUtil.*;
import static git4idea.GitUtil.getLogString;
public class GitCheckinEnvironment implements CheckinEnvironment {
private static final Logger log = Logger.getInstance(GitCheckinEnvironment.class.getName());
private static final Logger LOG = Logger.getInstance(GitCheckinEnvironment.class);
@NonNls private static final String GIT_COMMIT_MSG_FILE_PREFIX = "git-commit-msg-"; // the file name prefix for commit message file
@NonNls private static final String GIT_COMMIT_MSG_FILE_EXT = ".txt"; // the file extension for commit message file
@@ -116,12 +124,12 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
@Nullable
public String getDefaultMessageFor(FilePath[] filesToCheckin) {
LinkedHashSet<String> messages = ContainerUtil.newLinkedHashSet();
LinkedHashSet<String> messages = newLinkedHashSet();
GitRepositoryManager manager = GitUtil.getRepositoryManager(myProject);
for (VirtualFile root : GitUtil.gitRoots(Arrays.asList(filesToCheckin))) {
GitRepository repository = manager.getRepositoryForRoot(root);
if (repository == null) { // unregistered nested submodule found by GitUtil.getGitRoot
log.warn("Unregistered repository: " + root);
LOG.warn("Unregistered repository: " + root);
continue;
}
File mergeMsg = repository.getRepositoryFiles().getMergeMessageFile();
@@ -139,8 +147,8 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
}
}
catch (IOException e) {
if (log.isDebugEnabled()) {
log.debug("Unable to load merge message", e);
if (LOG.isDebugEnabled()) {
LOG.debug("Unable to load merge message", e);
}
}
}
@@ -164,7 +172,7 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
@NotNull NullableFunction<Object, Object> parametersHolder, Set<String> feedback) {
List<VcsException> exceptions = new ArrayList<VcsException>();
Map<VirtualFile, Collection<Change>> sortedChanges = sortChangesByGitRoot(changes, exceptions);
log.assertTrue(!sortedChanges.isEmpty(), "Trying to commit an empty list of changes: " + changes);
LOG.assertTrue(!sortedChanges.isEmpty(), "Trying to commit an empty list of changes: " + changes);
for (Map.Entry<VirtualFile, Collection<Change>> entry : sortedChanges.entrySet()) {
VirtualFile root = entry.getKey();
File messageFile;
@@ -179,6 +187,7 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
Set<FilePath> added = new HashSet<>();
Set<FilePath> removed = new HashSet<>();
final Set<Change> caseOnlyRenames = new HashSet<>();
for (Change change : entry.getValue()) {
switch (change.getType()) {
case NEW:
@@ -191,8 +200,11 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
case MOVED:
FilePath afterPath = change.getAfterRevision().getFile();
FilePath beforePath = change.getBeforeRevision().getFile();
added.add(afterPath);
if (!GitFileUtils.shouldIgnoreCaseChange(afterPath.getPath(), beforePath.getPath())) {
if (!SystemInfo.isFileSystemCaseSensitive && GitUtil.isCaseOnlyChange(beforePath.getPath(), afterPath.getPath())) {
caseOnlyRenames.add(change);
}
else {
added.add(afterPath);
removed.add(beforePath);
}
break;
@@ -202,19 +214,26 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
}
try {
try {
Set<FilePath> files = new HashSet<FilePath>();
files.addAll(added);
files.addAll(removed);
commit(myProject, root, files, messageFile, myNextCommitAuthor, myNextCommitAmend, myNextCommitAuthorDate);
if (!caseOnlyRenames.isEmpty()) {
List<VcsException> exs = commitWithCaseOnlyRename(myProject, root, caseOnlyRenames, added, removed,
messageFile, myNextCommitAuthor);
exceptions.addAll(map(exs, GitCheckinEnvironment::cleanupExceptionText));
}
catch (VcsException ex) {
PartialOperation partialOperation = isMergeCommit(ex);
if (partialOperation == PartialOperation.NONE) {
throw ex;
else {
try {
Set<FilePath> files = new HashSet<FilePath>();
files.addAll(added);
files.addAll(removed);
commit(myProject, root, files, messageFile, myNextCommitAuthor, myNextCommitAmend, myNextCommitAuthorDate);
}
if (!mergeCommit(myProject, root, added, removed, messageFile, myNextCommitAuthor, exceptions, partialOperation)) {
throw ex;
catch (VcsException ex) {
PartialOperation partialOperation = isMergeCommit(ex);
if (partialOperation == PartialOperation.NONE) {
throw ex;
}
if (!mergeCommit(myProject, root, added, removed, messageFile, myNextCommitAuthor, exceptions, partialOperation)) {
throw ex;
}
}
}
}
@@ -223,14 +242,14 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
}
finally {
if (!messageFile.delete()) {
log.warn("Failed to remove temporary file: " + messageFile);
LOG.warn("Failed to remove temporary file: " + messageFile);
}
}
}
if (myNextCommitIsPushed != null && myNextCommitIsPushed.booleanValue() && exceptions.isEmpty()) {
GitRepositoryManager manager = GitUtil.getRepositoryManager(myProject);
Collection<GitRepository> repositories = GitUtil.getRepositoriesFromRoots(manager, sortedChanges.keySet());
final List<GitRepository> preselectedRepositories = ContainerUtil.newArrayList(repositories);
final List<GitRepository> preselectedRepositories = newArrayList(repositories);
GuiUtils.invokeLaterIfNeeded(() ->
new VcsPushDialog(myProject, preselectedRepositories, GitBranchUtil.getCurrentRepository(myProject)).show(),
ModalityState.defaultModalityState());
@@ -238,6 +257,83 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
return exceptions;
}
@NotNull
private static List<VcsException> commitWithCaseOnlyRename(@NotNull Project project,
@NotNull VirtualFile root,
@NotNull Set<Change> caseOnlyRenames,
@NotNull Set<FilePath> added,
@NotNull Set<FilePath> removed,
@NotNull File messageFile,
@Nullable String author) {
String rootPath = root.getPath();
LOG.info("Committing case only rename: " + getLogString(rootPath, caseOnlyRenames) + " in " + getShortRepositoryName(project, root));
// 1. Check what is staged besides case-only renames
Collection<Change> stagedChanges;
try {
stagedChanges = GitChangeUtils.getStagedChanges(project, root);
LOG.debug("Found staged changes: " + getLogString(rootPath, stagedChanges));
}
catch (VcsException e) {
return Collections.singletonList(e);
}
// 2. Reset staged changes which are not selected for commit
Collection<Change> excludedStagedChanges = filter(stagedChanges, change ->
!caseOnlyRenames.contains(change) && !added.contains(getAfterPath(change)) && !removed.contains(getBeforePath(change)));
if (!excludedStagedChanges.isEmpty()) {
LOG.info("Staged changes excluded for commit: " + getLogString(rootPath, excludedStagedChanges));
try {
reset(project, root, excludedStagedChanges);
}
catch (VcsException e) {
return Collections.singletonList(e);
}
}
List<VcsException> exceptions = new ArrayList<>();
try {
// 3. Stage what else is needed to commit
List<FilePath> newPathsOfCaseRenames = map(caseOnlyRenames, ChangesUtil::getAfterPath);
LOG.debug("Updating index for added:" + added + "\n, removed: " + removed + "\n, and case-renames: " + newPathsOfCaseRenames);
Set<FilePath> toAdd = new HashSet<>(added);
toAdd.addAll(newPathsOfCaseRenames);
updateIndex(project, root, toAdd, removed, exceptions);
if (!exceptions.isEmpty()) return exceptions;
// 4. Commit the staging area
LOG.debug("Performing commit...");
try {
commitWithoutPaths(project, root, messageFile, author);
}
catch (VcsException e) {
return Collections.singletonList(e);
}
}
finally {
// 5. Stage back the changes unstaged before commit
if (!excludedStagedChanges.isEmpty()) {
LOG.debug("Restoring changes which were unstaged before commit: " + getLogString(rootPath, excludedStagedChanges));
Set<FilePath> toAdd = map2SetNotNull(excludedStagedChanges, ChangesUtil::getAfterPath);
Condition<Change> isMovedOrDeleted = change -> change.getType() == Change.Type.MOVED || change.getType() == Change.Type.DELETED;
Set<FilePath> toRemove = map2SetNotNull(filter(excludedStagedChanges, isMovedOrDeleted), ChangesUtil::getBeforePath);
updateIndex(project, root, toAdd, toRemove, exceptions);
}
}
return exceptions;
}
private static void reset(@NotNull Project project, @NotNull VirtualFile root, @NotNull Collection<Change> changes) throws VcsException {
Set<FilePath> paths = new HashSet<>();
paths.addAll(mapNotNull(changes, ChangesUtil::getAfterPath));
paths.addAll(mapNotNull(changes, ChangesUtil::getBeforePath));
GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.RESET);
handler.endOptions();
handler.addRelativePaths(paths);
handler.run();
}
@NotNull
private static VcsException cleanupExceptionText(VcsException original) {
String msg = original.getMessage();
@@ -351,14 +447,7 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
}
// perform merge commit
try {
GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.COMMIT);
handler.setStdoutSuppressed(false);
handler.addParameters("-F", messageFile.getAbsolutePath());
if (author != null) {
handler.addParameters("--author=" + author);
}
handler.endOptions();
handler.run();
commitWithoutPaths(project, root, messageFile, author);
GitRepositoryManager manager = GitUtil.getRepositoryManager(project);
manager.updateRepository(root);
}
@@ -369,6 +458,20 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
return true;
}
private static void commitWithoutPaths(@NotNull Project project,
@NotNull VirtualFile root,
@NotNull File messageFile,
@Nullable String author) throws VcsException {
GitSimpleHandler handler = new GitSimpleHandler(project, root, GitCommand.COMMIT);
handler.setStdoutSuppressed(false);
handler.addParameters("-F", messageFile.getAbsolutePath());
if (author != null) {
handler.addParameters("--author=" + author);
}
handler.endOptions();
handler.run();
}
/**
* Check if commit has failed due to unfinished merge or cherry-pick.
*
@@ -612,7 +715,7 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
c.fill = GridBagConstraints.HORIZONTAL;
Set<String> authors = new HashSet<String>(getUsersList(project));
ContainerUtil.addAll(authors, mySettings.getCommitAuthors());
addAll(authors, mySettings.getCommitAuthors());
List<String> list = new ArrayList<String>(authors);
Collections.sort(list);
@@ -657,7 +760,7 @@ public class GitCheckinEnvironment implements CheckinEnvironment {
@NotNull
private List<String> getUsersList(@NotNull Project project) {
VcsUserRegistry userRegistry = ServiceManager.getService(project, VcsUserRegistry.class);
return ContainerUtil.map(userRegistry.getUsers(), new Function<VcsUser, String>() {
return map(userRegistry.getUsers(), new Function<VcsUser, String>() {
@Override
public String fun(VcsUser user) {
return VcsUserUtil.toExactString(user);
@@ -181,18 +181,4 @@ public class GitFileUtils {
}
return path;
}
/**
* Checks if two file paths are different only by case in a case insensitive OS.
* @return true if the difference between paths should probably be ignored, i.e. the OS is case-insensitive, and case is the only
* difference between paths.
*/
public static boolean shouldIgnoreCaseChange(@NotNull String onePath, @NotNull String secondPath) {
return !SystemInfo.isFileSystemCaseSensitive && onlyCaseChanged(onePath, secondPath);
}
private static boolean onlyCaseChanged(@NotNull String one, @NotNull String second) {
return one.compareToIgnoreCase(second) == 0;
}
}
@@ -15,9 +15,16 @@
*/
package git4idea.tests
import com.intellij.openapi.util.io.FileUtil
import com.intellij.openapi.vcs.Executor.overwrite
import com.intellij.openapi.vcs.FileStatus
import com.intellij.openapi.vcs.VcsException
import com.intellij.openapi.vcs.changes.Change
import com.intellij.util.containers.ContainerUtil
import git4idea.GitUtil
import git4idea.changes.GitChangeUtils
import git4idea.checkin.GitCheckinEnvironment
import git4idea.history.GitHistoryUtils
import git4idea.test.GitExecutor.*
import git4idea.test.GitSingleRepoTest
import git4idea.test.GitTestUtil.createFileStructure
@@ -26,6 +33,8 @@ import java.util.*
class GitCommitTest : GitSingleRepoTest() {
override fun getDebugLogCategories() = super.getDebugLogCategories().plus("#" + GitCheckinEnvironment::class.java.name)
// IDEA-50318
fun `test merge commit with spaces in path`() {
val PATH = "dir with spaces/file with spaces.txt"
@@ -49,14 +58,234 @@ class GitCommitTest : GitSingleRepoTest() {
git("add .")
updateChangeListManager()
val changes = ArrayList(myChangeListManager.getChangesIn(myProjectRoot))
val changes = changeListManager.allChanges
assertTrue(!changes.isEmpty())
val exceptions = myVcs.checkinEnvironment!!.commit(changes, "comment")
assertNoExceptions(exceptions)
commit(changes)
assertNoChanges()
}
fun `test commit case rename`() {
generateCaseRename("a.java", "A.java")
val changes = assertChanges() {
rename("a.java", "A.java")
}
commit(changes)
assertNoChanges()
assertCommitted() {
rename("a.java", "A.java")
}
}
fun `test commit case rename + one staged file`() {
generateCaseRename("a.java", "A.java")
touch("s.java")
git("add s.java")
val changes = assertChanges() {
rename("a.java", "A.java")
added("s.java")
}
commit(changes)
assertNoChanges()
assertCommitted() {
rename("a.java", "A.java")
added("s.java")
}
}
fun `test commit case rename + one unstaged file`() {
tac("m.java")
generateCaseRename("a.java", "A.java")
echo("m.java", "unstaged")
val changes = assertChanges() {
rename("a.java", "A.java")
modified("m.java")
}
commit(changes)
assertNoChanges()
assertCommitted() {
rename("a.java", "A.java")
modified("m.java")
}
}
fun `test commit case rename & don't commit one unstaged file`() {
tac("m.java")
generateCaseRename("a.java", "A.java")
echo("m.java", "unstaged")
val changes = assertChanges() {
rename("a.java", "A.java")
modified("m.java")
}
commit(listOf(changes[0]))
assertChanges() {
modified("m.java")
}
assertCommitted() {
rename("a.java", "A.java")
}
}
fun `test commit case rename & don't commit one staged file`() {
tac("s.java")
generateCaseRename("a.java", "A.java")
echo("s.java", "staged")
git("add s.java")
val changes = assertChanges() {
rename("a.java", "A.java")
modified("s.java")
}
commit(listOf(changes[0]))
assertCommitted() {
rename("a.java", "A.java")
}
assertChanges() {
modified("s.java")
}
assertStagedChanges() {
modified("s.java")
}
}
fun `test commit case rename & don't commit one staged simple rename, then rename should remain staged`() {
echo("before.txt", "some\ncontent\nere")
addCommit("created before.txt")
generateCaseRename("a.java", "A.java")
git("mv before.txt after.txt")
val changes = assertChanges() {
rename("a.java", "A.java")
rename("before.txt", "after.txt")
}
commit(listOf(changes[0]))
assertCommitted() {
rename("a.java", "A.java")
}
assertChanges() {
rename("before.txt", "after.txt")
}
assertStagedChanges() {
rename("before.txt", "after.txt")
}
}
fun `test commit case rename + one unstaged file & don't commit one staged file`() {
tac("s.java")
tac("m.java")
generateCaseRename("a.java", "A.java")
echo("s.java", "staged")
echo("m.java", "unstaged")
git("add s.java m.java")
val changes = assertChanges() {
rename("a.java", "A.java")
modified("s.java")
modified("m.java")
}
commit(listOf(changes[0], changes[2]))
assertCommitted() {
rename("a.java", "A.java")
modified("m.java")
}
assertChanges() {
modified("s.java")
}
assertStagedChanges() {
modified("s.java")
}
}
fun `test commit case rename & don't commit a file which is both staged and unstaged, should reset and restore`() {
tac("c.java")
generateCaseRename("a.java", "A.java")
echo("c.java", "staged")
git("add c.java")
overwrite("c.java", "unstaged")
val changes = assertChanges() {
rename("a.java", "A.java")
modified("c.java")
}
commit(listOf(changes[0]))
assertCommitted() {
rename("a.java", "A.java")
}
assertChanges() {
modified("c.java")
}
assertStagedChanges() {
modified("c.java")
}
// this is intentional data loss: it is a rare case, while restoring both staged and unstaged part is not so easy,
// so we are not doing it, at least until IDEA supports Git index
// (which will mean that users will be able to produce such situation intentionally with a help of IDE).
assertEquals("unstaged", git("show :c.java"))
assertEquals("unstaged", FileUtil.loadFile(File(myProjectPath, "c.java")))
}
fun `test commit case rename with additional non-staged changes should commit everything`() {
val initialContent = """
some
large
content
to let
rename
detection
work
""".trimIndent()
touch("a.java", initialContent)
addCommit("initial a.java")
git("mv -f a.java A.java")
val additionalContent = "non-staged content"
append("A.java", additionalContent)
val changes = assertChanges() {
rename("a.java", "A.java")
}
commit(changes)
assertCommitted() {
rename("a.java", "A.java")
}
assertNoChanges()
assertEquals(initialContent + additionalContent, git("show HEAD:A.java"))
}
private fun generateCaseRename(from: String, to: String) {
tac(from)
git("mv -f $from $to")
}
private fun commit(changes: Collection<Change>) {
val exceptions = myVcs.checkinEnvironment!!.commit(ArrayList(changes), "comment")
assertNoExceptions(exceptions)
updateChangeListManager()
assertTrue(myChangeListManager.getChangesIn(myProjectRoot).isEmpty())
}
private fun assertNoChanges() {
val changes = changeListManager.getChangesIn(myProjectRoot)
assertTrue("We expected no changes but found these: " + GitUtil.getLogString(myProjectPath, changes), changes.isEmpty())
}
private fun assertNoExceptions(exceptions: List<VcsException>?) {
@@ -66,4 +295,83 @@ class GitCommitTest : GitSingleRepoTest() {
fail("Exception during executing the commit: " + ex.message)
}
}
private fun assertChanges(changes: ChangesBuilder.() -> Unit) : List<Change> {
val cb = ChangesBuilder()
cb.changes()
updateChangeListManager()
val allChanges = mutableListOf<Change>()
val actualChanges = HashSet(changeListManager.allChanges)
for (change in cb.changes) {
val found = actualChanges.find(change.matcher)
assertNotNull("The change [$change] not found", found)
actualChanges.remove(found)
allChanges.add(found!!)
}
assertTrue(actualChanges.isEmpty())
return allChanges
}
private fun assertStagedChanges(changes: ChangesBuilder.() -> Unit) {
val cb = ChangesBuilder()
cb.changes()
val actualChanges = GitChangeUtils.getStagedChanges(myProject, myProjectRoot)
for (change in cb.changes) {
val found = actualChanges.find(change.matcher)
assertNotNull("The change [$change] is not staged", found)
actualChanges.remove(found)
}
assertTrue(actualChanges.isEmpty())
}
private fun assertCommitted(changes: ChangesBuilder.() -> Unit) {
val cb = ChangesBuilder()
cb.changes()
val actualChanges = GitHistoryUtils.history(myProject, myProjectRoot, "-1")[0].changes
for (change in cb.changes) {
val found = actualChanges.find(change.matcher)
assertNotNull("The change [$change] wasn't committed", found)
actualChanges.remove(found)
}
assertTrue(actualChanges.isEmpty())
}
class ChangesBuilder {
data class AChange(val type: FileStatus, val nameBefore: String?, val nameAfter: String, val matcher: (Change) -> Boolean) {
constructor(type: FileStatus, nameAfter: String, matcher: (Change) -> Boolean) : this(type, null, nameAfter, matcher)
override fun toString(): String {
when (type) {
Change.Type.NEW -> return "A: $nameAfter"
Change.Type.DELETED -> return "D: $nameAfter"
Change.Type.MOVED -> return "M: $nameBefore -> $nameAfter"
else -> return "M: $nameAfter"
}
}
}
val changes = linkedSetOf<AChange>()
fun added(name: String) {
assertTrue(changes.add(AChange(FileStatus.ADDED, name) {
it.fileStatus == FileStatus.ADDED && it.beforeRevision == null && it.afterRevision!!.file.name == name
}))
}
fun modified(name:String) {
assertTrue(changes.add(AChange(FileStatus.MODIFIED, name) {
it.fileStatus == FileStatus.MODIFIED && it.beforeRevision!!.file.name == name && it.afterRevision!!.file.name == name
}))
}
fun rename(from: String, to: String) {
assertTrue(changes.add(AChange(FileStatus.MODIFIED, from, to) {
it.isRenamed && from == it.beforeRevision!!.file.name && to == it.afterRevision!!.file.name
}))
}
}
}