mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
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:
@@ -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
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user