diff --git a/platform/vcs-tests/testSrc/com/intellij/vcs/test/VcsPlatformTest.kt b/platform/vcs-tests/testSrc/com/intellij/vcs/test/VcsPlatformTest.kt index 0acd2302fbe0..dd9bc487be53 100644 --- a/platform/vcs-tests/testSrc/com/intellij/vcs/test/VcsPlatformTest.kt +++ b/platform/vcs-tests/testSrc/com/intellij/vcs/test/VcsPlatformTest.kt @@ -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) diff --git a/plugins/git4idea/src/git4idea/GitUtil.java b/plugins/git4idea/src/git4idea/GitUtil.java index d2ec7bf34402..8e735889a33b 100644 --- a/plugins/git4idea/src/git4idea/GitUtil.java +++ b/plugins/git4idea/src/git4idea/GitUtil.java @@ -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 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); + } } diff --git a/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java b/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java index 6739c6cbd861..ca7f5206f715 100644 --- a/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java +++ b/plugins/git4idea/src/git4idea/changes/GitChangeUtils.java @@ -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 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 changes = new ArrayList<>(); + parseChanges(project, root, null, GitRevisionNumber.HEAD, output, changes, Collections.emptySet()); + return changes; + } + @NotNull public static Collection getDiffWithWorkingDir(@NotNull Project project, @NotNull VirtualFile root, diff --git a/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java b/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java index 896f7c95919c..8b15fbb8a36a 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java +++ b/plugins/git4idea/src/git4idea/checkin/GitCheckinEnvironment.java @@ -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 messages = ContainerUtil.newLinkedHashSet(); + LinkedHashSet 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 parametersHolder, Set feedback) { List exceptions = new ArrayList(); Map> 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> entry : sortedChanges.entrySet()) { VirtualFile root = entry.getKey(); File messageFile; @@ -179,6 +187,7 @@ public class GitCheckinEnvironment implements CheckinEnvironment { Set added = new HashSet<>(); Set removed = new HashSet<>(); + final Set 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 files = new HashSet(); - files.addAll(added); - files.addAll(removed); - commit(myProject, root, files, messageFile, myNextCommitAuthor, myNextCommitAmend, myNextCommitAuthorDate); + if (!caseOnlyRenames.isEmpty()) { + List 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 files = new HashSet(); + 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 repositories = GitUtil.getRepositoriesFromRoots(manager, sortedChanges.keySet()); - final List preselectedRepositories = ContainerUtil.newArrayList(repositories); + final List 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 commitWithCaseOnlyRename(@NotNull Project project, + @NotNull VirtualFile root, + @NotNull Set caseOnlyRenames, + @NotNull Set added, + @NotNull Set 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 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 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 exceptions = new ArrayList<>(); + try { + // 3. Stage what else is needed to commit + List newPathsOfCaseRenames = map(caseOnlyRenames, ChangesUtil::getAfterPath); + LOG.debug("Updating index for added:" + added + "\n, removed: " + removed + "\n, and case-renames: " + newPathsOfCaseRenames); + Set 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 toAdd = map2SetNotNull(excludedStagedChanges, ChangesUtil::getAfterPath); + Condition isMovedOrDeleted = change -> change.getType() == Change.Type.MOVED || change.getType() == Change.Type.DELETED; + Set 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 changes) throws VcsException { + Set 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 authors = new HashSet(getUsersList(project)); - ContainerUtil.addAll(authors, mySettings.getCommitAuthors()); + addAll(authors, mySettings.getCommitAuthors()); List list = new ArrayList(authors); Collections.sort(list); @@ -657,7 +760,7 @@ public class GitCheckinEnvironment implements CheckinEnvironment { @NotNull private List getUsersList(@NotNull Project project) { VcsUserRegistry userRegistry = ServiceManager.getService(project, VcsUserRegistry.class); - return ContainerUtil.map(userRegistry.getUsers(), new Function() { + return map(userRegistry.getUsers(), new Function() { @Override public String fun(VcsUser user) { return VcsUserUtil.toExactString(user); diff --git a/plugins/git4idea/src/git4idea/util/GitFileUtils.java b/plugins/git4idea/src/git4idea/util/GitFileUtils.java index 5687d873812b..39b7287e5bf8 100644 --- a/plugins/git4idea/src/git4idea/util/GitFileUtils.java +++ b/plugins/git4idea/src/git4idea/util/GitFileUtils.java @@ -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; - } - } diff --git a/plugins/git4idea/tests/git4idea/tests/GitCommitTest.kt b/plugins/git4idea/tests/git4idea/tests/GitCommitTest.kt index ba6f11ef153c..66a3e1f71685 100644 --- a/plugins/git4idea/tests/git4idea/tests/GitCommitTest.kt +++ b/plugins/git4idea/tests/git4idea/tests/GitCommitTest.kt @@ -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) { + 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?) { @@ -66,4 +295,83 @@ class GitCommitTest : GitSingleRepoTest() { fail("Exception during executing the commit: " + ex.message) } } + + private fun assertChanges(changes: ChangesBuilder.() -> Unit) : List { + val cb = ChangesBuilder() + cb.changes() + + updateChangeListManager() + val allChanges = mutableListOf() + 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() + + 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 + })) + } + } }