git4idea: IDEADEV-42007: added check for valid line separators before stashing during...
[fedora-idea.git] / plugins / git4idea / src / git4idea / ui / GitConvertFilesDialog.java
blob8da47ec7b6fb852a8027e7a9c15ffe29db5eee41
1 /*
2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
16 package git4idea.ui;
18 import com.intellij.codeStyle.CodeStyleFacade;
19 import com.intellij.openapi.fileEditor.impl.LoadTextUtil;
20 import com.intellij.openapi.project.Project;
21 import com.intellij.openapi.ui.DialogWrapper;
22 import com.intellij.openapi.vcs.VcsException;
23 import com.intellij.openapi.vcs.changes.Change;
24 import com.intellij.openapi.vfs.LocalFileSystem;
25 import com.intellij.openapi.vfs.VfsUtil;
26 import com.intellij.openapi.vfs.VirtualFile;
27 import com.intellij.ui.*;
28 import com.intellij.util.Processor;
29 import com.intellij.util.containers.HashMap;
30 import com.intellij.util.ui.UIUtil;
31 import com.intellij.util.ui.tree.TreeUtil;
32 import git4idea.GitUtil;
33 import git4idea.GitVcs;
34 import git4idea.commands.GitCommand;
35 import git4idea.commands.GitSimpleHandler;
36 import git4idea.commands.StringScanner;
37 import git4idea.config.GitVcsSettings;
38 import git4idea.config.GitVersion;
39 import git4idea.i18n.GitBundle;
41 import javax.swing.*;
42 import java.awt.event.ActionEvent;
43 import java.io.IOException;
44 import java.io.OutputStream;
45 import java.io.OutputStreamWriter;
46 import java.util.*;
48 /**
49 * This dialog allows converting the specified files before committing them.
51 public class GitConvertFilesDialog extends DialogWrapper {
52 /**
53 * The version when option --stdin was added
55 private static final GitVersion CHECK_ATTR_STDIN_SUPPORTED = new GitVersion(1, 6, 1, 0);
56 /**
57 * Do not convert exit code
59 public static final int DO_NOT_CONVERT = NEXT_USER_EXIT_CODE;
60 /**
61 * The checkbox used to indicate that dialog should not be shown
63 private JCheckBox myDoNotShowCheckBox;
64 /**
65 * The root panel of the dialog
67 private JPanel myRootPanel;
68 /**
69 * The tree of files to convert
71 private CheckboxTreeBase myFilesToConvert;
72 /**
73 * The root node in the tree
75 private CheckedTreeNode myRootNode;
77 /**
78 * The constructor
80 * @param project the project to which this dialog is related
81 * @param filesToShow the files to show sorted by vcs root
83 GitConvertFilesDialog(Project project, Map<VirtualFile, Set<VirtualFile>> filesToShow) {
84 super(project, true);
85 ArrayList<VirtualFile> roots = new ArrayList<VirtualFile>(filesToShow.keySet());
86 Collections.sort(roots, GitUtil.VIRTUAL_FILE_COMPARATOR);
87 for (VirtualFile root : roots) {
88 CheckedTreeNode vcsRoot = new CheckedTreeNode(root);
89 myRootNode.add(vcsRoot);
90 ArrayList<VirtualFile> files = new ArrayList<VirtualFile>(filesToShow.get(root));
91 Collections.sort(files, GitUtil.VIRTUAL_FILE_COMPARATOR);
92 for (VirtualFile file : files) {
93 vcsRoot.add(new CheckedTreeNode(file));
96 TreeUtil.expandAll(myFilesToConvert);
97 setTitle(GitBundle.getString("crlf.convert.title"));
98 setOKButtonText(GitBundle.getString("crlf.convert.convert"));
99 init();
103 * {@inheritDoc}
105 @Override
106 protected Action[] createActions() {
107 return new Action[]{getOKAction(), new DoNotConvertAction(), getCancelAction()};
112 * Create custom UI components
114 private void createUIComponents() {
115 myRootNode = new CheckedTreeNode("ROOT");
116 myFilesToConvert = new CheckboxTree(new FileTreeCellRenderer(), myRootNode) {
117 protected void onNodeStateChanged(CheckedTreeNode node) {
118 VirtualFile[] files = myFilesToConvert.getCheckedNodes(VirtualFile.class, null);
119 setOKActionEnabled(files != null && files.length > 0);
120 super.onNodeStateChanged(node);
126 * {@inheritDoc}
128 @Override
129 protected JComponent createCenterPanel() {
130 return myRootPanel;
134 * {@inheritDoc}
136 @Override
137 protected String getDimensionServiceKey() {
138 return getClass().getName();
142 * Check if files need to be converted to other line separator. The method could be invoked from non-UI thread.
144 * @param project the project to use
145 * @param settings the vcs settings
146 * @param sortedChanges sorted changes
147 * @param exceptions the collection with exceptions
148 * @return true if conversion completed successfully, false if process was cancelled or there were errors
150 public static boolean showDialogIfNeeded(final Project project,
151 final GitVcsSettings settings,
152 Map<VirtualFile, List<Change>> sortedChanges,
153 final List<VcsException> exceptions) {
154 try {
155 if (settings.LINE_SEPARATORS_CONVERSION_ASK ||
156 settings.LINE_SEPARATORS_CONVERSION == GitVcsSettings.ConversionPolicy.PROJECT_LINE_SEPARATORS) {
157 LocalFileSystem lfs = LocalFileSystem.getInstance();
158 final String nl = CodeStyleFacade.getInstance(project).getLineSeparator();
159 final Map<VirtualFile, Set<VirtualFile>> files = new HashMap<VirtualFile, Set<VirtualFile>>();
160 // preliminary screening of files
161 for (Map.Entry<VirtualFile, List<Change>> entry : sortedChanges.entrySet()) {
162 final VirtualFile root = entry.getKey();
163 final Set<VirtualFile> added = new HashSet<VirtualFile>();
164 for (Change change : entry.getValue()) {
165 switch (change.getType()) {
166 case NEW:
167 case MODIFICATION:
168 case MOVED:
169 VirtualFile f = lfs.findFileByPath(change.getAfterRevision().getFile().getPath());
170 if (f != null && !f.getFileType().isBinary() && !nl.equals(LoadTextUtil.detectLineSeparator(f, false))) {
171 added.add(f);
173 break;
174 case DELETED:
177 if (!added.isEmpty()) {
178 files.put(root, added);
181 // ignore files with CRLF unset
182 ignoreFilesWithCrlfUnset(project, files);
183 // check crlf for real
184 for (Iterator<Map.Entry<VirtualFile, Set<VirtualFile>>> i = files.entrySet().iterator(); i.hasNext();) {
185 Map.Entry<VirtualFile, Set<VirtualFile>> e = i.next();
186 Set<VirtualFile> fs = e.getValue();
187 for (Iterator<VirtualFile> j = fs.iterator(); j.hasNext();) {
188 VirtualFile f = j.next();
189 String detectedLineSeparator = LoadTextUtil.detectLineSeparator(f, true);
190 if (detectedLineSeparator == null || nl.equals(detectedLineSeparator)) {
191 j.remove();
194 if (fs.isEmpty()) {
195 i.remove();
198 if (files.isEmpty()) {
199 return true;
201 UIUtil.invokeAndWaitIfNeeded(new Runnable() {
202 public void run() {
203 VirtualFile[] selectedFiles = null;
204 if (settings.LINE_SEPARATORS_CONVERSION_ASK) {
205 GitConvertFilesDialog d = new GitConvertFilesDialog(project, files);
206 d.show();
207 if (d.isOK()) {
208 settings.LINE_SEPARATORS_CONVERSION_ASK = !d.myDoNotShowCheckBox.isSelected();
209 settings.LINE_SEPARATORS_CONVERSION = GitVcsSettings.ConversionPolicy.PROJECT_LINE_SEPARATORS;
210 selectedFiles = d.myFilesToConvert.getCheckedNodes(VirtualFile.class, null);
212 else if (d.getExitCode() == DO_NOT_CONVERT) {
213 settings.LINE_SEPARATORS_CONVERSION_ASK = !d.myDoNotShowCheckBox.isSelected();
214 settings.LINE_SEPARATORS_CONVERSION = GitVcsSettings.ConversionPolicy.NONE;
216 else {
217 //noinspection ThrowableInstanceNeverThrown
218 exceptions.add(new VcsException("Commit was cancelled in file conversion dialog"));
221 else {
222 ArrayList<VirtualFile> fileList = new ArrayList<VirtualFile>();
223 for (Set<VirtualFile> fileSet : files.values()) {
224 fileList.addAll(fileSet);
226 selectedFiles = VfsUtil.toVirtualFileArray(fileList);
228 if (selectedFiles != null) {
229 for (VirtualFile f : selectedFiles) {
230 try {
231 LoadTextUtil.changeLineSeparator(project, GitConvertFilesDialog.class.getName(), f, nl);
233 catch (IOException e) {
234 //noinspection ThrowableInstanceNeverThrown
235 exceptions.add(new VcsException("Failed to change line separators for the file: " + f.getPresentableUrl(), e));
243 catch (VcsException e) {
244 exceptions.add(e);
246 return exceptions.isEmpty();
250 * Remove files that have -crlf attribute specified
252 * @param project the context project
253 * @param files the files to check (map from vcs roots to the set of files under root)
254 * @throws VcsException if there is problem with running git
256 private static void ignoreFilesWithCrlfUnset(Project project, Map<VirtualFile, Set<VirtualFile>> files) throws VcsException {
257 boolean stdin = CHECK_ATTR_STDIN_SUPPORTED.isLessOrEqual(GitVcs.getInstance(project).version());
258 for (final Map.Entry<VirtualFile, Set<VirtualFile>> e : files.entrySet()) {
259 final VirtualFile r = e.getKey();
260 GitSimpleHandler h = new GitSimpleHandler(project, r, GitCommand.CHECK_ATTR);
261 if (stdin) {
262 h.addParameters("--stdin", "-z");
264 h.addParameters("crlf");
265 h.setSilent(true);
266 h.setNoSSH(true);
267 final HashMap<String, VirtualFile> filesToCheck = new HashMap<String, VirtualFile>();
268 Set<VirtualFile> fileSet = e.getValue();
269 for (VirtualFile file : fileSet) {
270 filesToCheck.put(GitUtil.relativePath(r, file), file);
272 if (stdin) {
273 h.setInputProcessor(new Processor<OutputStream>() {
274 public boolean process(OutputStream outputStream) {
275 try {
276 OutputStreamWriter out = new OutputStreamWriter(outputStream, GitUtil.UTF8_CHARSET);
277 try {
278 for (String file : filesToCheck.keySet()) {
279 out.write(file);
280 out.write("\u0000");
283 finally {
284 out.close();
287 catch (IOException ex) {
288 try {
289 outputStream.close();
291 catch (IOException ioe) {
292 // ignore exception
295 return true;
299 else {
300 h.endOptions();
301 h.addRelativeFiles(filesToCheck.values());
303 StringScanner output = new StringScanner(h.run());
304 String unsetIndicator = ": crlf: unset";
305 while (output.hasMoreData()) {
306 String l = output.line();
307 if (l.endsWith(unsetIndicator)) {
308 fileSet.remove(filesToCheck.get(GitUtil.unescapePath(l.substring(0, l.length() - unsetIndicator.length()))));
315 * Action used to indicate that no conversion should be performed
317 class DoNotConvertAction extends AbstractAction {
318 private static final long serialVersionUID = 1931383640152023206L;
321 * The constructor
323 DoNotConvertAction() {
324 putValue(NAME, GitBundle.getString("crlf.convert.leave"));
325 putValue(DEFAULT_ACTION, Boolean.FALSE);
329 * {@inheritDoc}
331 public void actionPerformed(ActionEvent e) {
332 if (myPerformAction) return;
333 try {
334 myPerformAction = true;
335 close(DO_NOT_CONVERT);
337 finally {
338 myPerformAction = false;
345 * The cell renderer for the tree
347 static class FileTreeCellRenderer extends CheckboxTree.CheckboxTreeCellRenderer {
349 * {@inheritDoc}
351 @Override
352 public void customizeRenderer(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
353 ColoredTreeCellRenderer r = getTextRenderer();
354 if (!(value instanceof CheckedTreeNode)) {
355 // unknown node type
356 renderUnknown(r, value);
357 return;
359 CheckedTreeNode node = (CheckedTreeNode)value;
360 if (!(node.getUserObject() instanceof VirtualFile)) {
361 // unknown node type
362 renderUnknown(r, node.getUserObject());
363 return;
365 VirtualFile file = (VirtualFile)node.getUserObject();
366 if (leaf) {
367 VirtualFile parent = (VirtualFile)((CheckedTreeNode)node.getParent()).getUserObject();
368 // the real file
369 Icon i = file.getIcon();
370 if (i != null) {
371 r.setIcon(i);
373 r.append(GitUtil.getRelativeFilePath(file, parent), SimpleTextAttributes.REGULAR_ATTRIBUTES, true);
375 else {
376 // the vcs root node
377 r.append(file.getPresentableUrl(), SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES, true);
382 * Render unknown node
384 * @param r a renderer to use
385 * @param value the unknown value
387 private static void renderUnknown(ColoredTreeCellRenderer r, Object value) {
388 r.append("UNSUPPORTED NODE TYPE: " + (value == null ? "null" : value.getClass().getName()), SimpleTextAttributes.ERROR_ATTRIBUTES);