2ec97fc95d70b4adb48edfd61b8b19a35709475d
[idea/community.git] / plugins / git4idea / src / git4idea / rebase / GitRebaseEditor.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16 package git4idea.rebase;
17
18 import com.intellij.openapi.project.Project;
19 import com.intellij.openapi.ui.DialogWrapper;
20 import com.intellij.openapi.util.SystemInfo;
21 import com.intellij.openapi.util.io.FileUtil;
22 import com.intellij.openapi.vfs.VirtualFile;
23 import com.intellij.util.ArrayUtil;
24 import com.intellij.util.ListWithSelection;
25 import com.intellij.util.ui.ComboBoxTableCellEditor;
26 import com.intellij.util.ui.ComboBoxTableCellRenderer;
27 import git4idea.actions.GitShowAllSubmittedFilesAction;
28 import git4idea.util.StringScanner;
29 import git4idea.config.GitConfigUtil;
30 import git4idea.i18n.GitBundle;
31 import org.jetbrains.annotations.NonNls;
32
33 import javax.swing.*;
34 import javax.swing.event.ListSelectionEvent;
35 import javax.swing.event.ListSelectionListener;
36 import javax.swing.event.TableModelEvent;
37 import javax.swing.event.TableModelListener;
38 import javax.swing.table.AbstractTableModel;
39 import javax.swing.table.TableColumn;
40 import java.awt.event.ActionEvent;
41 import java.awt.event.ActionListener;
42 import java.io.*;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Collections;
46 import java.util.List;
47
48 /**
49  * Editor for rebase entries. It allows reordering of
50  * the entries and changing commit status.
51  */
52 public class GitRebaseEditor extends DialogWrapper {
53   /**
54    * The table that lists all commits
55    */
56   private JTable myCommitsTable;
57   /**
58    * The move up button
59    */
60   private JButton myMoveUpButton;
61   /**
62    * The move down button
63    */
64   private JButton myMoveDownButton;
65   /**
66    * The view commit button
67    */
68   private JButton myViewButton;
69   /**
70    * The root panel
71    */
72   private JPanel myPanel;
73   /**
74    * Table model
75    */
76   private final MyTableModel myTableModel;
77   /**
78    * The file name
79    */
80   private final String myFile;
81   /**
82    * The project
83    */
84   private final Project myProject;
85   /**
86    * The git root
87    */
88   private final VirtualFile myGitRoot;
89   /**
90    * The cygwin drive prefix
91    */
92   @NonNls private static final String CYGDRIVE_PREFIX = "/cygdrive/";
93
94   /**
95    * The constructor
96    *
97    * @param project the project
98    * @param gitRoot the git root
99    * @param file    the file to edit
100    * @throws IOException if file could not be loaded
101    */
102   protected GitRebaseEditor(final Project project, final VirtualFile gitRoot, String file) throws IOException {
103     super(project, true);
104     myProject = project;
105     myGitRoot = gitRoot;
106     setTitle(GitBundle.getString("rebase.editor.title"));
107     setOKButtonText(GitBundle.getString("rebase.editor.button"));
108     if (SystemInfo.isWindows && file.startsWith(CYGDRIVE_PREFIX)) {
109       final int prefixSize = CYGDRIVE_PREFIX.length();
110       file = file.substring(prefixSize, prefixSize + 1) + ":" + file.substring(prefixSize + 1);
111     }
112     myFile = file;
113     myTableModel = new MyTableModel();
114     myTableModel.load(file);
115     myCommitsTable.setModel(myTableModel);
116     myCommitsTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
117     TableColumn actionColumn = myCommitsTable.getColumnModel().getColumn(MyTableModel.ACTION);
118     actionColumn.setCellEditor(ComboBoxTableCellEditor.INSTANCE);
119     actionColumn.setCellRenderer(ComboBoxTableCellRenderer.INSTANCE);
120     myCommitsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
121       public void valueChanged(final ListSelectionEvent e) {
122         myViewButton.setEnabled(myCommitsTable.getSelectedRowCount() == 1);
123         final ListSelectionModel selectionModel = myCommitsTable.getSelectionModel();
124         myMoveUpButton.setEnabled( selectionModel.getMinSelectionIndex() > 0);
125         myMoveDownButton.setEnabled( selectionModel.getMaxSelectionIndex() != -1 &&
126                                      selectionModel.getMaxSelectionIndex() < myTableModel.myEntries.size() - 1);
127       }
128     });
129     myViewButton.addActionListener(new ActionListener() {
130       public void actionPerformed(final ActionEvent e) {
131         int row = myCommitsTable.getSelectedRow();
132         if (row < 0) {
133           return;
134         }
135         GitRebaseEntry entry = myTableModel.myEntries.get(row);
136         GitShowAllSubmittedFilesAction.showSubmittedFiles(project, entry.getCommit(), gitRoot, false, false);
137       }
138     });
139
140     myMoveUpButton.addActionListener(new MoveUpDownActionListener(MoveDirection.up));
141     myMoveDownButton.addActionListener(new MoveUpDownActionListener(MoveDirection.down));
142
143     myTableModel.addTableModelListener(new TableModelListener() {
144       public void tableChanged(final TableModelEvent e) {
145         validateFields();
146       }
147     });
148     init();
149   }
150   /**
151    * Validate fields
152    */
153   private void validateFields() {
154     final List<GitRebaseEntry> entries = myTableModel.myEntries;
155     if (entries.size() == 0) {
156       setErrorText(GitBundle.getString("rebase.editor.invalid.entryset"));
157       setOKActionEnabled(false);
158       return;
159     }
160     int i = 0;
161     while (i < entries.size() && entries.get(i).getAction() == GitRebaseEntry.Action.skip) {
162       i++;
163     }
164     if (i < entries.size() && entries.get(i).getAction() == GitRebaseEntry.Action.squash) {
165       setErrorText(GitBundle.getString("rebase.editor.invalid.squash"));
166       setOKActionEnabled(false);
167       return;
168     }
169     setErrorText(null);
170     setOKActionEnabled(true);
171   }
172
173   /**
174    * Save entries back to the file
175    *
176    * @throws IOException if there is IO problem with saving
177    */
178   public void save() throws IOException {
179     myTableModel.save(myFile);
180   }
181
182   /**
183    * {@inheritDoc}
184    */
185   protected JComponent createCenterPanel() {
186     return myPanel;
187   }
188
189   /**
190    * {@inheritDoc}
191    */
192   @Override
193   protected String getDimensionServiceKey() {
194     return getClass().getName();
195   }
196
197   /**
198    * {@inheritDoc}
199    */
200   @Override
201   protected String getHelpId() {
202     return "reference.VersionControl.Git.RebaseCommits";
203   }
204
205   /**
206    * Cancel rebase
207    *
208    * @throws IOException if file cannot be reset to empty one
209    */
210   public void cancel() throws IOException {
211     myTableModel.cancel(myFile);
212   }
213
214
215   /**
216    * The table model for the commits
217    */
218   private class MyTableModel extends AbstractTableModel {
219     /**
220      * The action column
221      */
222     private static final int ACTION = 0;
223     /**
224      * The commit hash column
225      */
226     private static final int COMMIT = 1;
227     /**
228      * The subject column
229      */
230     private static final int SUBJECT = 2;
231
232     /**
233      * The entries
234      */
235     final List<GitRebaseEntry> myEntries = new ArrayList<GitRebaseEntry>();
236     private int[] myLastEditableSelectedRows = new int[]{};
237
238     /**
239      * {@inheritDoc}
240      */
241     @Override
242     public Class<?> getColumnClass(final int columnIndex) {
243       return columnIndex == ACTION ? ListWithSelection.class : String.class;
244     }
245
246     /**
247      * {@inheritDoc}
248      */
249     @Override
250     public String getColumnName(final int column) {
251       switch (column) {
252         case ACTION:
253           return GitBundle.getString("rebase.editor.action.column");
254         case COMMIT:
255           return GitBundle.getString("rebase.editor.commit.column");
256         case SUBJECT:
257           return GitBundle.getString("rebase.editor.comment.column");
258         default:
259           throw new IllegalArgumentException("Unsupported column index: " + column);
260       }
261     }
262
263     /**
264      * {@inheritDoc}
265      */
266     public int getRowCount() {
267       return myEntries.size();
268     }
269
270     /**
271      * {@inheritDoc}
272      */
273     public int getColumnCount() {
274       return SUBJECT + 1;
275     }
276
277     /**
278      * {@inheritDoc}
279      */
280     public Object getValueAt(final int rowIndex, final int columnIndex) {
281       GitRebaseEntry e = myEntries.get(rowIndex);
282       switch (columnIndex) {
283         case ACTION:
284           return new ListWithSelection<GitRebaseEntry.Action>(Arrays.asList(GitRebaseEntry.Action.values()), e.getAction());
285         case COMMIT:
286           return e.getCommit();
287         case SUBJECT:
288           return e.getSubject();
289         default:
290           throw new IllegalArgumentException("Unsupported column index: " + columnIndex);
291       }
292     }
293
294     /**
295      * {@inheritDoc}
296      */
297     @Override
298     @SuppressWarnings({"unchecked"})
299     public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) {
300       assert columnIndex == ACTION;
301
302       if ( ArrayUtil.indexOf( myLastEditableSelectedRows , rowIndex ) > -1 ) {
303         final ContiguousIntIntervalTracker intervalBuilder = new ContiguousIntIntervalTracker();
304         for (int lastEditableSelectedRow : myLastEditableSelectedRows) {
305           intervalBuilder.track( lastEditableSelectedRow );
306           setRowAction(aValue, lastEditableSelectedRow, columnIndex);
307         }
308         setSelection(intervalBuilder);
309       } else {
310         setRowAction(aValue, rowIndex, columnIndex);
311       }
312     }
313
314     private void setSelection(ContiguousIntIntervalTracker intervalBuilder) {
315       myCommitsTable.getSelectionModel().setSelectionInterval( intervalBuilder.getMin() , intervalBuilder.getMax() );
316     }
317
318     private void setRowAction(Object aValue, int rowIndex, int columnIndex) {
319       GitRebaseEntry e = myEntries.get(rowIndex);
320       e.setAction((GitRebaseEntry.Action)aValue);
321       fireTableCellUpdated(rowIndex, columnIndex);
322     }
323
324     /**
325      * {@inheritDoc}
326      */
327     @Override
328     public boolean isCellEditable(final int rowIndex, final int columnIndex) {
329       myLastEditableSelectedRows = myCommitsTable.getSelectedRows();
330       return columnIndex == ACTION;
331     }
332
333     /**
334      * Load data from the file
335      *
336      * @param file the file to load
337      * @throws IOException if file could not be loaded
338      */
339     public void load(final String file) throws IOException {
340       String encoding = GitConfigUtil.getLogEncoding(myProject, myGitRoot);
341       final StringScanner s = new StringScanner(FileUtil.loadFile(new File(file), encoding));
342       while (s.hasMoreData()) {
343         if (s.isEol() || s.startsWith('#') || s.startsWith("noop")) {
344           s.nextLine();
345           continue;
346         }
347         String action = s.spaceToken();
348         assert "pick".equals(action) : "Initial action should be pick: " + action;
349         String hash = s.spaceToken();
350         String comment = s.line();
351         myEntries.add(new GitRebaseEntry(hash, comment));
352       }
353     }
354
355     /**
356      * Save text to the file
357      *
358      * @param file the file to save to
359      * @throws IOException if there is IO problem
360      */
361     public void save(final String file) throws IOException {
362       String encoding = GitConfigUtil.getLogEncoding(myProject, myGitRoot);
363       PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), encoding));
364       try {
365         for (GitRebaseEntry e : myEntries) {
366           if (e.getAction() != GitRebaseEntry.Action.skip) {
367             out.println(e.getAction().toString() + " " + e.getCommit() + " " + e.getSubject());
368           }
369         }
370       }
371       finally {
372         out.close();
373       }
374     }
375
376     /**
377      * Save text to the file
378      *
379      * @param file the file to save to
380      * @throws IOException if there is IO problem
381      */
382     public void cancel(final String file) throws IOException {
383       PrintWriter out = new PrintWriter(new FileWriter(file));
384       try {
385         //noinspection HardCodedStringLiteral
386         out.println("# rebase is cancelled");
387       }
388       finally {
389         out.close();
390       }
391     }
392
393
394     public void moveRows(int[] rows, MoveDirection direction) {
395
396       myCommitsTable.removeEditor();
397
398       final ContiguousIntIntervalTracker selectionInterval = new ContiguousIntIntervalTracker();
399       final ContiguousIntIntervalTracker rowsUpdatedInterval = new ContiguousIntIntervalTracker();
400
401       for (int row : direction.preprocessRowIndexes( rows )) {
402         final int targetIndex = row + direction.offset();
403         assertIndexInRange( row , targetIndex );
404
405         Collections.swap( myEntries , row , targetIndex );
406
407         rowsUpdatedInterval.track(targetIndex, row );
408         selectionInterval.track( targetIndex );
409       }
410
411       if ( selectionInterval.hasValues() ) {
412         setSelection(selectionInterval);
413         fireTableRowsUpdated(rowsUpdatedInterval.getMin(), rowsUpdatedInterval.getMax());
414       }
415     }
416
417     private void assertIndexInRange(int... rowIndexes) {
418       for (int rowIndex : rowIndexes) {
419         assert rowIndex >= 0;
420         assert rowIndex < myEntries.size();
421       }
422     }
423   }
424
425   private static class ContiguousIntIntervalTracker {
426     private Integer myMin = null;
427     private Integer myMax = null;
428     private static final int UNSET_VALUE = -1;
429
430     public Integer getMin() {
431       return myMin == null ? UNSET_VALUE : myMin;
432     }
433
434     public Integer getMax() {
435       return myMax == null ? UNSET_VALUE : myMax;
436     }
437
438     public void track( int... entries ) {
439       for (int entry : entries) {
440         checkMax( entry );
441         checkMin( entry );
442       }
443     }
444
445     private void checkMax(int entry) {
446       if ( null == myMax || entry > myMax ) {
447         myMax = entry;
448       }
449     }
450
451     private void checkMin(int entry) {
452       if ( null == myMin || entry < myMin ) {
453         myMin = entry;
454       }
455     }
456
457     public boolean hasValues() {
458       return ( null != myMin && null != myMax);
459     }
460   }
461
462   private enum MoveDirection {
463     up , down;
464     public int offset() {
465       if (this == up) {
466         return -1;
467       } else {
468         return +1;
469       }
470     }
471     public int[] preprocessRowIndexes( int[] seletion ) {
472       int[] copy = seletion.clone();
473       Arrays.sort( copy );
474       if (this == up) {
475         return copy;
476       } else {
477         return ArrayUtil.reverseArray( copy );
478       }
479     }
480   }
481
482
483   private class MoveUpDownActionListener implements ActionListener {
484     private final MoveDirection direction;
485
486     public MoveUpDownActionListener(MoveDirection direction) {
487       this.direction = direction;
488     }
489
490     public void actionPerformed(final ActionEvent e) {
491       myTableModel.moveRows(myCommitsTable.getSelectedRows(), direction );
492     }
493   }
494
495 }