diff --git a/plugins/git4idea/src/git4idea/PlatformFacade.java b/plugins/git4idea/src/git4idea/PlatformFacade.java index 1384d7327205..3367c89abdc4 100644 --- a/plugins/git4idea/src/git4idea/PlatformFacade.java +++ b/plugins/git4idea/src/git4idea/PlatformFacade.java @@ -17,6 +17,7 @@ package git4idea; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.DialogWrapper; @@ -86,4 +87,6 @@ public interface PlatformFacade { @Nullable IdeaPluginDescriptor getPluginByClassName(@NotNull String name); + @NotNull + FileDocumentManager getFileDocumentManager(); } diff --git a/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java b/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java index 2408d808b7a1..0176e80ebb63 100644 --- a/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java +++ b/plugins/git4idea/src/git4idea/PlatformFacadeImpl.java @@ -20,6 +20,7 @@ import com.intellij.ide.plugins.PluginManager; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ModalityState; import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.DialogWrapper; @@ -109,6 +110,12 @@ public class PlatformFacadeImpl implements PlatformFacade { return PluginManager.getPlugin(PluginManager.getPluginByClassName(name)); } + @NotNull + @Override + public FileDocumentManager getFileDocumentManager() { + return FileDocumentManager.getInstance(); + } + @NotNull @Override public AbstractVcs getVcs(@NotNull Project project) { diff --git a/plugins/git4idea/src/git4idea/attributes/GitAttribute.java b/plugins/git4idea/src/git4idea/attributes/GitAttribute.java new file mode 100644 index 000000000000..ade52297328e --- /dev/null +++ b/plugins/git4idea/src/git4idea/attributes/GitAttribute.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * 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 git4idea.attributes; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author Kirill Likhodedov + */ +public enum GitAttribute { + TEXT("text"), + CRLF("crlf"); + + @NotNull private final String myName; + + GitAttribute(@NotNull String name) { + myName = name; + } + + @NotNull + public String getName() { + return myName; + } + + @Nullable + public static GitAttribute forName(String attribute) { + for (GitAttribute attr : values()) { + if (attr.myName.equalsIgnoreCase(attribute)) { + return attr; + } + } + return null; + } +} diff --git a/plugins/git4idea/src/git4idea/attributes/GitCheckAttrParser.java b/plugins/git4idea/src/git4idea/attributes/GitCheckAttrParser.java new file mode 100644 index 000000000000..62cba82998a3 --- /dev/null +++ b/plugins/git4idea/src/git4idea/attributes/GitCheckAttrParser.java @@ -0,0 +1,75 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * 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 git4idea.attributes; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.text.StringUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.*; + +/** + * Parses the output of {@code git check-attr}. + * All supported attributes are instances of {@link GitAttribute}. Others just ignored until needed. + * Values are also ignored: we just need to know if there is a specified attribute on a file, or not. + * + * @author Kirill Likhodedov + */ +public class GitCheckAttrParser { + + private static final Logger LOG = Logger.getInstance(GitCheckAttrParser.class); + private static final String UNSPECIFIED_VALUE = "unspecified"; + + @NotNull private final Map> myAttributes; + + private GitCheckAttrParser(@NotNull List output) { + myAttributes = new HashMap>(); + + for (String line : output) { + if (line.isEmpty()) { + continue; + } + List split = StringUtil.split(line, ":"); + LOG.assertTrue(split.size() == 3, String.format("Output doesn't match the expected format. Line: %s%nAll output:%n%s", + line, StringUtil.join(output, "\n"))); + String file = split.get(0).trim(); + String attribute = split.get(1).trim(); + String info = split.get(2).trim(); + + GitAttribute attr = GitAttribute.forName(attribute); + if (attr == null || info.equalsIgnoreCase(UNSPECIFIED_VALUE)) { + // ignoring attributes that we are not interested in + continue; + } + + if (myAttributes.get(file) == null) { + myAttributes.put(file, new ArrayList()); + } + myAttributes.get(file).add(attr); + } + } + + @NotNull + public static GitCheckAttrParser parse(@NotNull List output) { + return new GitCheckAttrParser(output); + } + + @NotNull + public Map> getAttributes() { + return myAttributes; + } + +} diff --git a/plugins/git4idea/src/git4idea/checkin/GitCheckinHandlerFactory.java b/plugins/git4idea/src/git4idea/checkin/GitCheckinHandlerFactory.java index e3620698bad0..312b8b6d3a22 100644 --- a/plugins/git4idea/src/git4idea/checkin/GitCheckinHandlerFactory.java +++ b/plugins/git4idea/src/git4idea/checkin/GitCheckinHandlerFactory.java @@ -15,6 +15,7 @@ */ package git4idea.checkin; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.Messages; @@ -33,6 +34,8 @@ import com.intellij.util.PairConsumer; import com.intellij.util.ui.UIUtil; import git4idea.GitUtil; import git4idea.GitVcs; +import git4idea.PlatformFacade; +import git4idea.commands.Git; import git4idea.config.GitConfigUtil; import git4idea.config.GitVcsSettings; import git4idea.config.GitVersion; @@ -102,7 +105,11 @@ public class GitCheckinHandlerFactory extends VcsCheckinHandlerFactory { return ReturnResult.COMMIT; } - GitCrlfProblemsDetector crlfHelper = GitCrlfProblemsDetector.detect(myPanel.getVirtualFiles()); + PlatformFacade platformFacade = ServiceManager.getService(myProject, PlatformFacade.class); + Git git = ServiceManager.getService(Git.class); + + Collection files = myPanel.getVirtualFiles(); // deleted files aren't included, but for them we don't care about CRLFs. + GitCrlfProblemsDetector crlfHelper = GitCrlfProblemsDetector.detect(myProject, platformFacade, git, files); if (crlfHelper.shouldWarn()) { final GitCrlfDialog dialog = new GitCrlfDialog(myProject); UIUtil.invokeAndWaitIfNeeded(new Runnable() { diff --git a/plugins/git4idea/src/git4idea/commands/Git.java b/plugins/git4idea/src/git4idea/commands/Git.java index d959628e31d8..bfd3cefcc984 100644 --- a/plugins/git4idea/src/git4idea/commands/Git.java +++ b/plugins/git4idea/src/git4idea/commands/Git.java @@ -48,6 +48,9 @@ public interface Git { @NotNull GitCommandResult clone(@NotNull Project project, @NotNull File parentDirectory, @NotNull String url, @NotNull String clonedDirectoryName); + @NotNull + GitCommandResult config(@NotNull GitRepository repository, String... params); + @NotNull GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @Nullable List additionalParams, @NotNull GitLineHandlerListener... listeners); @@ -101,4 +104,7 @@ public interface Git { @NotNull GitCommandResult getUnmergedFiles(@NotNull GitRepository repository); + @NotNull + GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection attributes, + @NotNull Collection files); } diff --git a/plugins/git4idea/src/git4idea/commands/GitImpl.java b/plugins/git4idea/src/git4idea/commands/GitImpl.java index 69618900545b..8ac0a6c5329c 100644 --- a/plugins/git4idea/src/git4idea/commands/GitImpl.java +++ b/plugins/git4idea/src/git4idea/commands/GitImpl.java @@ -136,6 +136,25 @@ public class GitImpl implements Git { return run(handler, true); } + @NotNull + @Override + public GitCommandResult config(@NotNull GitRepository repository, String... params) { + final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CONFIG); + h.addParameters(params); + return run(h); + } + + @NotNull + @Override + public GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection attributes, + @NotNull Collection files) { + final GitLineHandler h = new GitLineHandler(repository.getProject(), repository.getRoot(), GitCommand.CONFIG); + h.addParameters(new ArrayList(attributes)); + h.endOptions(); + h.addRelativeFiles(files); + return run(h); + } + @Override @NotNull public GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, diff --git a/plugins/git4idea/src/git4idea/crlf/GitCrlfProblemsDetector.java b/plugins/git4idea/src/git4idea/crlf/GitCrlfProblemsDetector.java index a109ab4626ea..df3dc8bbd666 100644 --- a/plugins/git4idea/src/git4idea/crlf/GitCrlfProblemsDetector.java +++ b/plugins/git4idea/src/git4idea/crlf/GitCrlfProblemsDetector.java @@ -15,10 +15,22 @@ */ package git4idea.crlf; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vfs.VirtualFile; +import git4idea.GitUtil; +import git4idea.PlatformFacade; +import git4idea.attributes.GitAttribute; +import git4idea.attributes.GitCheckAttrParser; +import git4idea.commands.Git; +import git4idea.commands.GitCommandResult; +import git4idea.config.GitConfigUtil; +import git4idea.repo.GitRepository; import org.jetbrains.annotations.NotNull; -import java.util.Collection; +import java.util.*; /** * Given a number of files, detects if CRLF line separators in them are about to be committed to Git. That is: @@ -32,17 +44,152 @@ import java.util.Collection; * * All checks are made only for Windows system. * + * Everywhere in the detection process we fail gracefully in case of exceptions: we log the fact, but don't fail totally, preferring to + * tell that the calling code should not warn the user. + * * @author Kirill Likhodedov */ public class GitCrlfProblemsDetector { + private static final Logger LOG = Logger.getInstance(GitCrlfProblemsDetector.class); + private static final String CRLF = "\r\n"; + + @NotNull private final Project myProject; + @NotNull private final PlatformFacade myPlatformFacade; + @NotNull private final Git myGit; + + private final boolean myShouldWarn; + @NotNull - public static GitCrlfProblemsDetector detect(Collection files) { - return new GitCrlfProblemsDetector(); + public static GitCrlfProblemsDetector detect(@NotNull Project project, @NotNull PlatformFacade platformFacade, + @NotNull Git git, @NotNull Collection files) { + return new GitCrlfProblemsDetector(project, platformFacade, git, files); + } + + private GitCrlfProblemsDetector(@NotNull Project project, @NotNull PlatformFacade platformFacade, @NotNull Git git, + @NotNull Collection files) { + myProject = project; + myPlatformFacade = platformFacade; + myGit = git; + + Map> filesByRoots = sortFilesByRoots(files); + + boolean shouldWarn = false; + Collection rootsWithIncorrectAutoCrlf = getRootsWithIncorrectAutoCrlf(filesByRoots); + if (!rootsWithIncorrectAutoCrlf.isEmpty()) { + Map> crlfFilesByRoots = findFilesWithCrlf(filesByRoots, rootsWithIncorrectAutoCrlf); + if (!crlfFilesByRoots.isEmpty()) { + Map> crlfFilesWithoutAttrsByRoots = findFilesWithoutAttrs(crlfFilesByRoots); + shouldWarn = !crlfFilesWithoutAttrsByRoots.isEmpty(); + } + } + myShouldWarn = shouldWarn; + } + + private Map> findFilesWithoutAttrs(Map> filesByRoots) { + Map> filesWithoutAttrsByRoot = new HashMap>(); + for (Map.Entry> entry : filesByRoots.entrySet()) { + VirtualFile root = entry.getKey(); + Collection files = entry.getValue(); + Collection filesWithoutAttrs = findFilesWithoutAttrs(root, files); + if (!filesWithoutAttrs.isEmpty()) { + filesWithoutAttrsByRoot.put(root, filesWithoutAttrs); + } + } + return filesWithoutAttrsByRoot; + } + + @NotNull + private Collection findFilesWithoutAttrs(@NotNull VirtualFile root, @NotNull Collection files) { + GitRepository repository = myPlatformFacade.getRepositoryManager(myProject).getRepositoryForRoot(root); + if (repository == null) { + LOG.warn("Repository is null for " + root); + return Collections.emptyList(); + } + Collection interestingAttributes = Arrays.asList(GitAttribute.TEXT.getName(), GitAttribute.CRLF.getName()); + GitCommandResult result = myGit.checkAttr(repository, interestingAttributes, files); + GitCheckAttrParser parser = GitCheckAttrParser.parse(result.getOutput()); + Map> attributes = parser.getAttributes(); + Collection filesWithoutAttrs = new ArrayList(); + for (VirtualFile file : files) { + String relativePath = FileUtil.getRelativePath(root.getPath(), file.getPath(), '/'); + Collection attrs = attributes.get(relativePath); + if (attrs == null || !attrs.contains(GitAttribute.TEXT) && !attrs.contains(GitAttribute.CRLF)) { + filesWithoutAttrs.add(file); + } + } + return filesWithoutAttrs; + } + + @NotNull + private Map> findFilesWithCrlf(@NotNull Map> allFilesByRoots, + @NotNull Collection rootsWithIncorrectAutoCrlf) { + Map> filesWithCrlfByRoots = new HashMap>(); + for (Map.Entry> entry : allFilesByRoots.entrySet()) { + VirtualFile root = entry.getKey(); + List files = entry.getValue(); + if (rootsWithIncorrectAutoCrlf.contains(root)) { + Collection filesWithCrlf = findFilesWithCrlf(files); + if (!filesWithCrlf.isEmpty()) { + filesWithCrlfByRoots.put(root, filesWithCrlf); + } + } + } + return filesWithCrlfByRoots; + } + + @NotNull + private Collection findFilesWithCrlf(@NotNull Collection files) { + Collection filesWithCrlf = new ArrayList(); + for (VirtualFile file : files) { + if (CRLF.equals(myPlatformFacade.getFileDocumentManager().getLineSeparator(file, myProject))) { + filesWithCrlf.add(file); + } + } + return filesWithCrlf; + } + + @NotNull + private Collection getRootsWithIncorrectAutoCrlf(@NotNull Map> filesByRoots) { + Collection rootsWithIncorrectAutoCrlf = new ArrayList(); + for (Map.Entry> entry : filesByRoots.entrySet()) { + VirtualFile root = entry.getKey(); + boolean autocrlf = isAutoCrlfSetRight(root); + if (!autocrlf) { + rootsWithIncorrectAutoCrlf.add(root); + } + } + return rootsWithIncorrectAutoCrlf; + } + + private boolean isAutoCrlfSetRight(@NotNull VirtualFile root) { + GitRepository repository = myPlatformFacade.getRepositoryManager(myProject).getRepositoryForRoot(root); + if (repository == null) { + LOG.warn("Repository is null for " + root); + return true; + } + GitCommandResult result = myGit.config(repository, GitConfigUtil.CORE_AUTOCRLF); + if (!result.success()) { + LOG.warn("Couldn't get git config core.autocrlf for " + root); + return true; + } + String value = result.getOutputAsJoinedString(); + return value.equalsIgnoreCase("true") || value.equalsIgnoreCase("input"); + } + + @NotNull + private static Map> sortFilesByRoots(@NotNull Collection files) { + try { + return GitUtil.sortFilesByGitRoot(files, true); + } + catch (VcsException e) { + LOG.error("Should never happen, since we passed 'ignore non-git' parameter", e); + return Collections.emptyMap(); + } } public boolean shouldWarn() { - return false; + return myShouldWarn; } } diff --git a/plugins/git4idea/tests/git4idea/attributes/GitCheckAttrParserTest.groovy b/plugins/git4idea/tests/git4idea/attributes/GitCheckAttrParserTest.groovy new file mode 100644 index 000000000000..d62281b2e0cd --- /dev/null +++ b/plugins/git4idea/tests/git4idea/attributes/GitCheckAttrParserTest.groovy @@ -0,0 +1,45 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * 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 git4idea.attributes + +import org.junit.Test + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertTrue + +/** + * + * @author Kirill Likhodedov + */ +class GitCheckAttrParserTest { + + @Test + void "test"() { + String output = +""" +org/example/MyClass.java: crlf: unset +org/example/MyClass.java: diff: java +org/example/MyClass.java: myAttr: set +""" + GitCheckAttrParser parse = GitCheckAttrParser.parse(Arrays.asList(output.split("\n"))) + String path = "org/example/MyClass.java" + assertTrue "Missing path", parse.attributes.containsKey(path) + Collection attrs = parse.attributes.get(path) + assertTrue "CRLF attribute not found", attrs.contains(GitAttribute.CRLF) + assertEquals "Too many attributes", 1, attrs.size() + } + +} diff --git a/plugins/git4idea/tests/git4idea/crlf/GitCrlfProblemsDetectorTest.groovy b/plugins/git4idea/tests/git4idea/crlf/GitCrlfProblemsDetectorTest.groovy new file mode 100644 index 000000000000..2df8c3b47b80 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/crlf/GitCrlfProblemsDetectorTest.groovy @@ -0,0 +1,200 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * 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 git4idea.crlf + +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vfs.VirtualFile +import git4idea.PlatformFacade +import git4idea.commands.Git +import git4idea.repo.GitRepository +import git4idea.repo.GitRepositoryImpl +import git4idea.test.* +import org.junit.After +import org.junit.Before +import org.junit.Test + +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue +/** + * + * @author Kirill Likhodedov + */ +@Mixin(GitExecutor) +class GitCrlfProblemsDetectorTest { + + private GitMockProject myProject + private String myRootDir + + private PlatformFacade myPlatformFacade + private Git myGit + + private String myOldCoreAutoCrlfValue + + @Before + public void setUp() throws Exception { + myRootDir = FileUtil.createTempDirectory("", "").getPath() + myProject = new GitMockProject(myRootDir) + myPlatformFacade = new GitTestPlatformFacade() + myGit = new GitTestImpl() + + cd myRootDir + myOldCoreAutoCrlfValue = git "config --global core.autocrlf" + + git "init" + git "config --global --unset core.autocrlf" + + GitRepository repository = GitRepositoryImpl.getLightInstance(new GitMockVirtualFile(myRootDir), myProject, myProject) + ((GitTestRepositoryManager)myPlatformFacade.getRepositoryManager(myProject)).add(repository) + } + + @After + public void tearDown() throws Exception { + if (!StringUtil.isEmptyOrSpaces(myOldCoreAutoCrlfValue)) { + git "config --global core.autocrlf " + myOldCoreAutoCrlfValue; + } + FileUtil.delete(new File(myRootDir)) + } + + @Test + void "core.autocrlf is true = no warning"() { + git "config core.autocrlf true" + assertFalse "No warning should be done if core.autocrlf is true", detect("temp").shouldWarn() + } + + @Test + void "core.autocrlf is input = no warning"() { + git "config core.autocrlf input" + assertFalse "No warning should be done if core.autocrlf is input", detect("temp").shouldWarn() + } + + @Test + void "no files with CRLF = no warning"() { + createFile("temp", "Unix file\nNice separators\nOnly LF\n") + assertFalse "No warning should be done if all files are LFs", detect("temp").shouldWarn() + } + + @Test + void "file with CRLF, no attrs, core.autocrlf is false = warning"() { + createCrlfFile("win") + assertFalse "Warning should be done for a CRLF file with core.autocrlf = false", detect("temp").shouldWarn() + } + + @Test + void "file with CRLF, but text is set = no warning"() { + gitattributes("* text=auto") + createCrlfFile("win") + assertFalse "No warning should be done if the file has a text attribute", detect("win").shouldWarn() + } + + @Test + void "file with CRLF, but crlf is set = no warning"() { + gitattributes("win crlf") + createCrlfFile("win") + assertFalse "No warning should be done if the file has a crlf attribute", detect("win").shouldWarn() + } + + @Test + void "file with CRLF, but crlf is explicitly unset = no warning"() { + gitattributes("win -crlf") + createCrlfFile("win") + assertFalse "No warning should be done if the file has an explicitly unset crlf attribute", detect("win").shouldWarn() + } + + @Test + void "file with CRLF, but crlf is set to input = no warning"() { + gitattributes("wi* crlf=input") + createCrlfFile("win") + assertFalse "No warning should be done if the file has a crlf attribute", detect("win").shouldWarn() + } + + @Test + void "file with CRLF, nothing is set = warning"() { + createCrlfFile("win") + assertTrue "Warning should be done if the file has CRLFs inside, and no explicit attributes", detect("win").shouldWarn() + } + + @Test + void "various files with various attributes, one doesn't match = warning"() { + gitattributes """ +win1 text crlf diff +win2 -text +win3 text=auto +win4 crlf +win5 -crlf +win6 crlf=input +""" + + createFile("unix", "Unix file\nNice separators\nOnly LF\n") + createCrlfFile("win1") + createCrlfFile("win2") + createCrlfFile("win3") + createCrlfFile("src/win4") + createCrlfFile("src/win5") + createCrlfFile("src/win6") + createCrlfFile("src/win7") + + assertTrue "Warning should be done, since one of the files has CRLFs and no related attributes", + GitCrlfProblemsDetector.detect(myProject, myPlatformFacade, myGit, + ["unix", "win1", "win2", "win3", "src/win4", "src/win5", "src/win6", "src/win7"] + .collect { it -> GitMockVirtualFile.fromPath(it, myRootDir) as VirtualFile}) + .shouldWarn() + } + + private void gitattributes(String content) { + createFile(".gitattributes", content) + } + + private GitCrlfProblemsDetector detect(String relPath) { + return detect(createVirtualFile(relPath)) + } + + private GitCrlfProblemsDetector detect(VirtualFile file) { + GitCrlfProblemsDetector.detect(myProject, myPlatformFacade, myGit, Collections. singleton(file)) + } + + private void createCrlfFile(String relPath) { + createFile(relPath, "Windows file\r\nBad separators\r\nCRLF in action\r\n") + } + + private void createFile(String relPath, String content) { + String path = createFile(relPath) + File file = new File(path) + FileUtil.appendToFile(file, content) + } + + private VirtualFile createVirtualFile(String relPath) { + return new GitMockVirtualFile(createFile(relPath)) + } + + private String createFile(String relPath) { + List split = StringUtil.split(relPath, "/") + File parent = new File(myRootDir) + for (Iterator it = split.iterator(); it.hasNext(); ) { + String item = it.next() + File file = new File(parent, item) + if (it.hasNext()) { + parent = file + parent.mkdir() + } + else { + file.createNewFile() + return file.getPath() + } + } + } + +} diff --git a/plugins/git4idea/tests/git4idea/test/GitExec.java b/plugins/git4idea/tests/git4idea/test/GitExec.java index 9c2c08d23874..faaa945d817a 100644 --- a/plugins/git4idea/tests/git4idea/test/GitExec.java +++ b/plugins/git4idea/tests/git4idea/test/GitExec.java @@ -153,7 +153,7 @@ public class GitExec { } @NotNull - private static String run(@NotNull GitRepository repository, @NotNull String command, String... params) throws IOException { + public static String run(@NotNull GitRepository repository, @NotNull String command, String... params) throws IOException { return new GitTestRunEnv(new File(repository.getRoot().getPath())).run(command, params); } diff --git a/plugins/git4idea/tests/git4idea/test/GitExecutor.groovy b/plugins/git4idea/tests/git4idea/test/GitExecutor.groovy new file mode 100644 index 000000000000..8e77a52b57fb --- /dev/null +++ b/plugins/git4idea/tests/git4idea/test/GitExecutor.groovy @@ -0,0 +1,39 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * 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 git4idea.test + +import com.intellij.openapi.util.text.StringUtil +import com.intellij.util.ArrayUtil + +/** + * + * @author Kirill Likhodedov + */ +class GitExecutor { + + private String myCurrentDir + + void cd(String path) { + myCurrentDir = path + } + + String git(String command) { + List split = StringUtil.split(command, " ") + String[] params = split.size() > 1 ? ArrayUtil.toObjectArray(split.subList(1, split.size()), String) : ArrayUtil.EMPTY_STRING_ARRAY + return new GitTestRunEnv(new File(myCurrentDir)).run(split.get(0), params); + } + +} diff --git a/plugins/git4idea/tests/git4idea/test/GitMockFileDocumentManager.java b/plugins/git4idea/tests/git4idea/test/GitMockFileDocumentManager.java new file mode 100644 index 000000000000..4c0174703eea --- /dev/null +++ b/plugins/git4idea/tests/git4idea/test/GitMockFileDocumentManager.java @@ -0,0 +1,104 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * 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 git4idea.test; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; + +/** + * @author Kirill Likhodedov + */ +class GitMockFileDocumentManager extends FileDocumentManager { + @Override + public Document getDocument(@NotNull VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @Override + public Document getCachedDocument(@NotNull VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @Override + public VirtualFile getFile(@NotNull Document document) { + throw new UnsupportedOperationException(); + } + + @Override + public void saveAllDocuments() { + throw new UnsupportedOperationException(); + } + + @Override + public void saveDocument(@NotNull Document document) { + throw new UnsupportedOperationException(); + } + + @Override + public void saveDocumentAsIs(@NotNull Document document) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Document[] getUnsavedDocuments() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isDocumentUnsaved(@NotNull Document document) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isFileModified(@NotNull VirtualFile file) { + throw new UnsupportedOperationException(); + } + + @Override + public void reloadFromDisk(@NotNull Document document) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public String getLineSeparator(@Nullable VirtualFile file, @Nullable Project project) { + try { + return FileUtil.loadFile(new File(file.getPath())).contains("\r\n") ? "\r\n" : "\n"; + } + catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean requestWriting(@NotNull Document document, @Nullable Project project) { + throw new UnsupportedOperationException(); + } + + @Override + public void reloadFiles(VirtualFile... files) { + throw new UnsupportedOperationException(); + } +} diff --git a/plugins/git4idea/tests/git4idea/test/GitMockProject.groovy b/plugins/git4idea/tests/git4idea/test/GitMockProject.groovy index 3405ade94154..c13a4682396d 100644 --- a/plugins/git4idea/tests/git4idea/test/GitMockProject.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitMockProject.groovy @@ -139,7 +139,7 @@ class GitMockProject implements Project { @Override MessageBus getMessageBus() { - throw new UnsupportedOperationException() + null } @Override @@ -155,7 +155,7 @@ class GitMockProject implements Project { @NotNull @Override Condition getDisposed() { - throw new UnsupportedOperationException() + Condition.FALSE } @Override diff --git a/plugins/git4idea/tests/git4idea/test/GitMockVirtualFile.groovy b/plugins/git4idea/tests/git4idea/test/GitMockVirtualFile.groovy index 9580ab15844d..67e824703c6f 100644 --- a/plugins/git4idea/tests/git4idea/test/GitMockVirtualFile.groovy +++ b/plugins/git4idea/tests/git4idea/test/GitMockVirtualFile.groovy @@ -42,6 +42,11 @@ class GitMockVirtualFile extends VirtualFile { return fromPath(new File(project.getBaseDir().getPath() + "/" + relativePath).getCanonicalPath()) } + @NotNull + static VirtualFile fromPath(@NotNull String relativePath, @NotNull String basePath) { + return fromPath(new File(basePath + "/" + relativePath).getCanonicalPath()) + } + GitMockVirtualFile(@NotNull String path) { myPath = FileUtil.toSystemIndependentName(path); } diff --git a/plugins/git4idea/tests/git4idea/test/GitTestImpl.groovy b/plugins/git4idea/tests/git4idea/test/GitTestImpl.groovy new file mode 100644 index 000000000000..58892c461610 --- /dev/null +++ b/plugins/git4idea/tests/git4idea/test/GitTestImpl.groovy @@ -0,0 +1,198 @@ +/* + * Copyright 2000-2012 JetBrains s.r.o. + * + * 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 git4idea.test +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.text.StringUtil +import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vfs.VirtualFile +import git4idea.commands.Git +import git4idea.commands.GitCommandResult +import git4idea.commands.GitLineHandlerListener +import git4idea.push.GitPushSpec +import git4idea.repo.GitRepository +import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.Nullable +/** + * @author Kirill Likhodedov + */ +@Mixin(GitExecutor) +public class GitTestImpl implements Git { + + @NotNull + @Override + public GitCommandResult init(@NotNull Project project, @NotNull VirtualFile root, @NotNull GitLineHandlerListener... listeners) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Set untrackedFiles(@NotNull Project project, @NotNull VirtualFile root, @Nullable Collection files) + throws VcsException { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public Collection untrackedFilesNoChunk(@NotNull Project project, + @NotNull VirtualFile root, + @Nullable List relativePaths) throws VcsException { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult clone(@NotNull Project project, + @NotNull File parentDirectory, + @NotNull String url, + @NotNull String clonedDirectoryName) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult config(@NotNull GitRepository repository, String... params) { + cd repository.getRoot().getPath() + String output = git "config " + params.join(" ") + return new GitCommandResult(!output.contains("fatal"), 0, Collections.emptyList(), + Arrays.asList(StringUtil.splitByLines(output))) + } + + @NotNull + @Override + GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection attributes, @NotNull Collection files) { + String root = repository.getRoot().getPath() + cd root + String output = git "check-attr " + attributes.join(" ") + " -- " + + files.collect({it -> FileUtil.getRelativePath(root, it.path, (char)'/')}).join(" ") + return new GitCommandResult(!output.contains("fatal"), 0, Collections.emptyList(), + Arrays.asList(StringUtil.splitByLines(output))) + } + + @NotNull + @Override + public GitCommandResult merge(@NotNull GitRepository repository, + @NotNull String branchToMerge, + @Nullable List additionalParams, + @NotNull GitLineHandlerListener... listeners) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult checkout(@NotNull GitRepository repository, + @NotNull String reference, + @Nullable String newBranch, + boolean force, + @NotNull GitLineHandlerListener... listeners) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult checkoutNewBranch(@NotNull GitRepository repository, + @NotNull String branchName, + @Nullable GitLineHandlerListener listener) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult createNewTag(@NotNull GitRepository repository, + @NotNull String tagName, + @Nullable GitLineHandlerListener listener, + @NotNull String reference) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult branchDelete(@NotNull GitRepository repository, + @NotNull String branchName, + boolean force, + @NotNull GitLineHandlerListener... listeners) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult branchContains(@NotNull GitRepository repository, @NotNull String commit) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult branchCreate(@NotNull GitRepository repository, @NotNull String branchName) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult resetHard(@NotNull GitRepository repository, @NotNull String revision) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult resetMerge(@NotNull GitRepository repository, @Nullable String revision) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult tip(@NotNull GitRepository repository, @NotNull String branchName) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult push(@NotNull GitRepository repository, + @NotNull String remote, + @NotNull String spec, + @NotNull GitLineHandlerListener... listeners) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult push(@NotNull GitRepository repository, + @NotNull GitPushSpec pushSpec, + @NotNull GitLineHandlerListener... listeners) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult show(@NotNull GitRepository repository, @NotNull String... params) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult cherryPick(@NotNull GitRepository repository, + @NotNull String hash, + boolean autoCommit, + @NotNull GitLineHandlerListener... listeners) { + throw new UnsupportedOperationException(); + } + + @NotNull + @Override + public GitCommandResult getUnmergedFiles(@NotNull GitRepository repository) { + throw new UnsupportedOperationException(); + } + +} diff --git a/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java b/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java index 05e93a9d1ba7..f651a6b138da 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestPlatformFacade.java @@ -18,6 +18,7 @@ package git4idea.test; import com.intellij.ide.plugins.IdeaPluginDescriptor; import com.intellij.mock.MockLocalFileSystem; import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.ui.DialogWrapper; @@ -145,6 +146,12 @@ public class GitTestPlatformFacade implements PlatformFacade { return null; } + @NotNull + @Override + public FileDocumentManager getFileDocumentManager() { + return new GitMockFileDocumentManager(); + } + @NotNull @Override public AbstractVcs getVcs(@NotNull Project project) { diff --git a/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java b/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java index 0f83aa90a900..e6c789294932 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestRepositoryManager.java @@ -30,7 +30,7 @@ import java.util.List; /** * @author Kirill Likhodedov */ -class GitTestRepositoryManager implements GitRepositoryManager { +public class GitTestRepositoryManager implements GitRepositoryManager { private final Collection myRepositories = new ArrayList(); @@ -40,7 +40,12 @@ class GitTestRepositoryManager implements GitRepositoryManager { @Override public GitRepository getRepositoryForRoot(@Nullable VirtualFile root) { - return myRepositories.iterator().next(); + for (GitRepository repository : myRepositories) { + if (repository.getRoot().equals(root)) { + return repository; + } + } + return null; } @Override diff --git a/plugins/git4idea/tests/git4idea/test/GitTestUtil.java b/plugins/git4idea/tests/git4idea/test/GitTestUtil.java index d1ce14edf255..86b486d852d1 100644 --- a/plugins/git4idea/tests/git4idea/test/GitTestUtil.java +++ b/plugins/git4idea/tests/git4idea/test/GitTestUtil.java @@ -20,6 +20,7 @@ import com.intellij.openapi.command.WriteCommandAction; import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.Ref; +import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.io.FileUtil; import com.intellij.openapi.vfs.CharsetToolkit; import com.intellij.openapi.vfs.VirtualFile; @@ -35,11 +36,14 @@ import org.picocontainer.MutablePicoContainer; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; +import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.fail; @@ -225,6 +229,21 @@ public class GitTestUtil { return new File(pluginRoot, "testData"); } + // for testing purposes we test the behavior of windows and unix gits on both platforms + // this method sets SystemInfo.isWindows to whatever we want + public static void setWindows(boolean windows) throws NoSuchFieldException, IllegalAccessException { + Field win = SystemInfo.class.getDeclaredField("isWindows"); + win.setAccessible(true); + + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(win, win.getModifiers() & ~Modifier.FINAL); + + win.set(null, windows); + + assertEquals(SystemInfo.isWindows, windows); + } + public interface EqualityChecker { boolean areEqual(T actual, E expected); } diff --git a/plugins/git4idea/tests/git4idea/test/MockGit.groovy b/plugins/git4idea/tests/git4idea/test/MockGit.groovy index a7a8274b09d8..c396f244806c 100644 --- a/plugins/git4idea/tests/git4idea/test/MockGit.groovy +++ b/plugins/git4idea/tests/git4idea/test/MockGit.groovy @@ -86,6 +86,12 @@ class MockGit implements Git { throw new UnsupportedOperationException() } + @NotNull + @Override + GitCommandResult config(@NotNull GitRepository repository, String... params) { + throw new UnsupportedOperationException() + } + @NotNull @Override GitCommandResult merge(@NotNull GitRepository repository, @NotNull String branchToMerge, @Nullable List additionalParams, @@ -187,6 +193,12 @@ class MockGit implements Git { return FAKE_SUCCESS_RESULT } + @NotNull + @Override + GitCommandResult checkAttr(@NotNull GitRepository repository, @NotNull Collection attributes, @NotNull Collection files) { + + } + private void produceOutput(String output, GitLineHandlerListener... listeners) { for (String line : output.split("\n")) { // for simplicity all output goes to OUTPUT, no ERROR listeners.each { it.onLineAvailable(line, ProcessOutputTypes.STDOUT) } diff --git a/plugins/git4idea/tests/git4idea/tests/GitVersionTest.java b/plugins/git4idea/tests/git4idea/tests/GitVersionTest.java index 1940bd942485..93a34b82322b 100644 --- a/plugins/git4idea/tests/git4idea/tests/GitVersionTest.java +++ b/plugins/git4idea/tests/git4idea/tests/GitVersionTest.java @@ -15,12 +15,11 @@ */ package git4idea.tests; -import com.intellij.openapi.util.SystemInfo; import git4idea.config.GitVersion; +import git4idea.test.GitTestUtil; import org.testng.annotations.Test; import java.lang.reflect.Field; -import java.lang.reflect.Modifier; import static git4idea.config.GitVersion.Type; import static org.testng.Assert.assertEquals; @@ -53,13 +52,13 @@ public class GitVersionTest { */ @Test public void testParse() throws Exception { - setWindows(false); + GitTestUtil.setWindows(false); for (TestGitVersion test : commonTests) { GitVersion version = GitVersion.parse(test.output); assertEqualVersions(version, test, Type.UNIX); } - setWindows(true); + GitTestUtil.setWindows(true); for (TestGitVersion test : commonTests) { GitVersion version = GitVersion.parse(test.output); assertEqualVersions(version, test, Type.CYGWIN); @@ -73,7 +72,7 @@ public class GitVersionTest { @Test public void testEqualsAndCompare() throws Exception { - setWindows(true); + GitTestUtil.setWindows(true); GitVersion v1 = GitVersion.parse("git version 1.6.3"); GitVersion v2 = GitVersion.parse("git version 1.7.3"); GitVersion v3 = GitVersion.parse("git version 1.7.3"); @@ -119,21 +118,6 @@ public class GitVersionTest { assertEquals(type, expectedType); } - // for testing purposes we test the behavior of windows and unix gits on both platforms - // this method sets SystemInfo.isWindows to whatever we want - private static void setWindows(boolean windows) throws NoSuchFieldException, IllegalAccessException { - Field win = SystemInfo.class.getDeclaredField("isWindows"); - win.setAccessible(true); - - Field modifiersField = Field.class.getDeclaredField("modifiers"); - modifiersField.setAccessible(true); - modifiersField.setInt(win, win.getModifiers() & ~Modifier.FINAL); - - win.set(null, windows); - - assertEquals(SystemInfo.isWindows, windows); - } - private static class TestGitVersion { private final String output; private final int major;