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.rebase;
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;
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;
43 import java.util.ArrayList;
44 import java.util.Arrays;
45 import java.util.Collections;
46 import java.util.List;
49 * Editor for rebase entries. It allows reordering of
50 * the entries and changing commit status.
52 public class GitRebaseEditor extends DialogWrapper {
54 * The table that lists all commits
56 private JTable myCommitsTable;
60 private JButton myMoveUpButton;
62 * The move down button
64 private JButton myMoveDownButton;
66 * The view commit button
68 private JButton myViewButton;
72 private JPanel myPanel;
76 private final MyTableModel myTableModel;
80 private final String myFile;
84 private final Project myProject;
88 private final VirtualFile myGitRoot;
90 * The cygwin drive prefix
92 @NonNls private static final String CYGDRIVE_PREFIX = "/cygdrive/";
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
102 protected GitRebaseEditor(final Project project, final VirtualFile gitRoot, String file) throws IOException {
103 super(project, true);
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);
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);
129 myViewButton.addActionListener(new ActionListener() {
130 public void actionPerformed(final ActionEvent e) {
131 int row = myCommitsTable.getSelectedRow();
135 GitRebaseEntry entry = myTableModel.myEntries.get(row);
136 GitShowAllSubmittedFilesAction.showSubmittedFiles(project, entry.getCommit(), gitRoot, false, false);
140 myMoveUpButton.addActionListener(new MoveUpDownActionListener(MoveDirection.up));
141 myMoveDownButton.addActionListener(new MoveUpDownActionListener(MoveDirection.down));
143 myTableModel.addTableModelListener(new TableModelListener() {
144 public void tableChanged(final TableModelEvent e) {
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);
161 while (i < entries.size() && entries.get(i).getAction() == GitRebaseEntry.Action.skip) {
164 if (i < entries.size() && entries.get(i).getAction() == GitRebaseEntry.Action.squash) {
165 setErrorText(GitBundle.getString("rebase.editor.invalid.squash"));
166 setOKActionEnabled(false);
170 setOKActionEnabled(true);
174 * Save entries back to the file
176 * @throws IOException if there is IO problem with saving
178 public void save() throws IOException {
179 myTableModel.save(myFile);
185 protected JComponent createCenterPanel() {
193 protected String getDimensionServiceKey() {
194 return getClass().getName();
201 protected String getHelpId() {
202 return "reference.VersionControl.Git.RebaseCommits";
208 * @throws IOException if file cannot be reset to empty one
210 public void cancel() throws IOException {
211 myTableModel.cancel(myFile);
216 * The table model for the commits
218 private class MyTableModel extends AbstractTableModel {
222 private static final int ACTION = 0;
224 * The commit hash column
226 private static final int COMMIT = 1;
230 private static final int SUBJECT = 2;
235 final List<GitRebaseEntry> myEntries = new ArrayList<GitRebaseEntry>();
236 private int[] myLastEditableSelectedRows = new int[]{};
242 public Class<?> getColumnClass(final int columnIndex) {
243 return columnIndex == ACTION ? ListWithSelection.class : String.class;
250 public String getColumnName(final int column) {
253 return GitBundle.getString("rebase.editor.action.column");
255 return GitBundle.getString("rebase.editor.commit.column");
257 return GitBundle.getString("rebase.editor.comment.column");
259 throw new IllegalArgumentException("Unsupported column index: " + column);
266 public int getRowCount() {
267 return myEntries.size();
273 public int getColumnCount() {
280 public Object getValueAt(final int rowIndex, final int columnIndex) {
281 GitRebaseEntry e = myEntries.get(rowIndex);
282 switch (columnIndex) {
284 return new ListWithSelection<GitRebaseEntry.Action>(Arrays.asList(GitRebaseEntry.Action.values()), e.getAction());
286 return e.getCommit();
288 return e.getSubject();
290 throw new IllegalArgumentException("Unsupported column index: " + columnIndex);
298 @SuppressWarnings({"unchecked"})
299 public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) {
300 assert columnIndex == ACTION;
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);
308 setSelection(intervalBuilder);
310 setRowAction(aValue, rowIndex, columnIndex);
314 private void setSelection(ContiguousIntIntervalTracker intervalBuilder) {
315 myCommitsTable.getSelectionModel().setSelectionInterval( intervalBuilder.getMin() , intervalBuilder.getMax() );
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);
328 public boolean isCellEditable(final int rowIndex, final int columnIndex) {
329 myLastEditableSelectedRows = myCommitsTable.getSelectedRows();
330 return columnIndex == ACTION;
334 * Load data from the file
336 * @param file the file to load
337 * @throws IOException if file could not be loaded
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")) {
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));
356 * Save text to the file
358 * @param file the file to save to
359 * @throws IOException if there is IO problem
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));
365 for (GitRebaseEntry e : myEntries) {
366 if (e.getAction() != GitRebaseEntry.Action.skip) {
367 out.println(e.getAction().toString() + " " + e.getCommit() + " " + e.getSubject());
377 * Save text to the file
379 * @param file the file to save to
380 * @throws IOException if there is IO problem
382 public void cancel(final String file) throws IOException {
383 PrintWriter out = new PrintWriter(new FileWriter(file));
385 //noinspection HardCodedStringLiteral
386 out.println("# rebase is cancelled");
394 public void moveRows(int[] rows, MoveDirection direction) {
396 myCommitsTable.removeEditor();
398 final ContiguousIntIntervalTracker selectionInterval = new ContiguousIntIntervalTracker();
399 final ContiguousIntIntervalTracker rowsUpdatedInterval = new ContiguousIntIntervalTracker();
401 for (int row : direction.preprocessRowIndexes( rows )) {
402 final int targetIndex = row + direction.offset();
403 assertIndexInRange( row , targetIndex );
405 Collections.swap( myEntries , row , targetIndex );
407 rowsUpdatedInterval.track(targetIndex, row );
408 selectionInterval.track( targetIndex );
411 if ( selectionInterval.hasValues() ) {
412 setSelection(selectionInterval);
413 fireTableRowsUpdated(rowsUpdatedInterval.getMin(), rowsUpdatedInterval.getMax());
417 private void assertIndexInRange(int... rowIndexes) {
418 for (int rowIndex : rowIndexes) {
419 assert rowIndex >= 0;
420 assert rowIndex < myEntries.size();
425 private static class ContiguousIntIntervalTracker {
426 private Integer myMin = null;
427 private Integer myMax = null;
428 private static final int UNSET_VALUE = -1;
430 public Integer getMin() {
431 return myMin == null ? UNSET_VALUE : myMin;
434 public Integer getMax() {
435 return myMax == null ? UNSET_VALUE : myMax;
438 public void track( int... entries ) {
439 for (int entry : entries) {
445 private void checkMax(int entry) {
446 if ( null == myMax || entry > myMax ) {
451 private void checkMin(int entry) {
452 if ( null == myMin || entry < myMin ) {
457 public boolean hasValues() {
458 return ( null != myMin && null != myMax);
462 private enum MoveDirection {
464 public int offset() {
471 public int[] preprocessRowIndexes( int[] seletion ) {
472 int[] copy = seletion.clone();
477 return ArrayUtil.reverseArray( copy );
483 private class MoveUpDownActionListener implements ActionListener {
484 private final MoveDirection direction;
486 public MoveUpDownActionListener(MoveDirection direction) {
487 this.direction = direction;
490 public void actionPerformed(final ActionEvent e) {
491 myTableModel.moveRows(myCommitsTable.getSelectedRows(), direction );