blob: fc0d1102326e038411f826db4a4341c20bf9181d [file] [log] [blame]
/*
* Copyright 2000-2009 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.rebase;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ListWithSelection;
import com.intellij.util.ui.ComboBoxTableCellRenderer;
import git4idea.GitUtil;
import git4idea.config.GitConfigUtil;
import git4idea.i18n.GitBundle;
import git4idea.util.StringScanner;
import org.jetbrains.annotations.NonNls;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Editor for rebase entries. It allows reordering of
* the entries and changing commit status.
*/
public class GitRebaseEditor extends DialogWrapper {
/**
* The table that lists all commits
*/
private JTable myCommitsTable;
/**
* The move up button
*/
private JButton myMoveUpButton;
/**
* The move down button
*/
private JButton myMoveDownButton;
/**
* The view commit button
*/
private JButton myViewButton;
/**
* The root panel
*/
private JPanel myPanel;
/**
* Table model
*/
private final MyTableModel myTableModel;
/**
* The file name
*/
private final String myFile;
/**
* The project
*/
private final Project myProject;
/**
* The git root
*/
private final VirtualFile myGitRoot;
/**
* The cygwin drive prefix
*/
@NonNls private static final String CYGDRIVE_PREFIX = "/cygdrive/";
/**
* The constructor
*
* @param project the project
* @param gitRoot the git root
* @param file the file to edit
* @throws IOException if file could not be loaded
*/
protected GitRebaseEditor(final Project project, final VirtualFile gitRoot, String file) throws IOException {
super(project, true);
myProject = project;
myGitRoot = gitRoot;
setTitle(GitBundle.getString("rebase.editor.title"));
setOKButtonText(GitBundle.getString("rebase.editor.button"));
if (SystemInfo.isWindows && file.startsWith(CYGDRIVE_PREFIX)) {
final int prefixSize = CYGDRIVE_PREFIX.length();
file = file.substring(prefixSize, prefixSize + 1) + ":" + file.substring(prefixSize + 1);
}
myFile = file;
myTableModel = new MyTableModel();
myTableModel.load(file);
myCommitsTable.setModel(myTableModel);
myCommitsTable.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
final JComboBox editorComboBox = new JComboBox();
for (Object option : GitRebaseEntry.Action.values()) {
editorComboBox.addItem(option);
}
TableColumn actionColumn = myCommitsTable.getColumnModel().getColumn(MyTableModel.ACTION);
actionColumn.setCellEditor(new DefaultCellEditor(editorComboBox));
actionColumn.setCellRenderer(ComboBoxTableCellRenderer.INSTANCE);
myCommitsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
public void valueChanged(final ListSelectionEvent e) {
myViewButton.setEnabled(myCommitsTable.getSelectedRowCount() == 1);
final ListSelectionModel selectionModel = myCommitsTable.getSelectionModel();
myMoveUpButton.setEnabled( selectionModel.getMinSelectionIndex() > 0);
myMoveDownButton.setEnabled( selectionModel.getMaxSelectionIndex() != -1 &&
selectionModel.getMaxSelectionIndex() < myTableModel.myEntries.size() - 1);
}
});
myViewButton.addActionListener(new ActionListener() {
public void actionPerformed(final ActionEvent e) {
int row = myCommitsTable.getSelectedRow();
if (row < 0) {
return;
}
GitRebaseEntry entry = myTableModel.myEntries.get(row);
GitUtil.showSubmittedFiles(project, entry.getCommit(), gitRoot, false, false);
}
});
myMoveUpButton.addActionListener(new MoveUpDownActionListener(MoveDirection.up));
myMoveDownButton.addActionListener(new MoveUpDownActionListener(MoveDirection.down));
myTableModel.addTableModelListener(new TableModelListener() {
public void tableChanged(final TableModelEvent e) {
validateFields();
}
});
init();
}
/**
* Validate fields
*/
private void validateFields() {
final List<GitRebaseEntry> entries = myTableModel.myEntries;
if (entries.size() == 0) {
setErrorText(GitBundle.getString("rebase.editor.invalid.entryset"));
setOKActionEnabled(false);
return;
}
int i = 0;
while (i < entries.size() && entries.get(i).getAction() == GitRebaseEntry.Action.skip) {
i++;
}
if (i < entries.size() && entries.get(i).getAction() == GitRebaseEntry.Action.squash) {
setErrorText(GitBundle.getString("rebase.editor.invalid.squash"));
setOKActionEnabled(false);
return;
}
setErrorText(null);
setOKActionEnabled(true);
}
/**
* Save entries back to the file
*
* @throws IOException if there is IO problem with saving
*/
public void save() throws IOException {
myTableModel.save(myFile);
}
/**
* {@inheritDoc}
*/
protected JComponent createCenterPanel() {
return myPanel;
}
/**
* {@inheritDoc}
*/
@Override
protected String getDimensionServiceKey() {
return getClass().getName();
}
/**
* {@inheritDoc}
*/
@Override
protected String getHelpId() {
return "reference.VersionControl.Git.RebaseCommits";
}
/**
* Cancel rebase
*
* @throws IOException if file cannot be reset to empty one
*/
public void cancel() throws IOException {
myTableModel.cancel(myFile);
}
/**
* The table model for the commits
*/
private class MyTableModel extends AbstractTableModel {
/**
* The action column
*/
private static final int ACTION = 0;
/**
* The commit hash column
*/
private static final int COMMIT = 1;
/**
* The subject column
*/
private static final int SUBJECT = 2;
/**
* The entries
*/
final List<GitRebaseEntry> myEntries = new ArrayList<GitRebaseEntry>();
private int[] myLastEditableSelectedRows = new int[]{};
/**
* {@inheritDoc}
*/
@Override
public Class<?> getColumnClass(final int columnIndex) {
return columnIndex == ACTION ? ListWithSelection.class : String.class;
}
/**
* {@inheritDoc}
*/
@Override
public String getColumnName(final int column) {
switch (column) {
case ACTION:
return GitBundle.getString("rebase.editor.action.column");
case COMMIT:
return GitBundle.getString("rebase.editor.commit.column");
case SUBJECT:
return GitBundle.getString("rebase.editor.comment.column");
default:
throw new IllegalArgumentException("Unsupported column index: " + column);
}
}
/**
* {@inheritDoc}
*/
public int getRowCount() {
return myEntries.size();
}
/**
* {@inheritDoc}
*/
public int getColumnCount() {
return SUBJECT + 1;
}
/**
* {@inheritDoc}
*/
public Object getValueAt(final int rowIndex, final int columnIndex) {
GitRebaseEntry e = myEntries.get(rowIndex);
switch (columnIndex) {
case ACTION:
return new ListWithSelection<GitRebaseEntry.Action>(Arrays.asList(GitRebaseEntry.Action.values()), e.getAction());
case COMMIT:
return e.getCommit();
case SUBJECT:
return e.getSubject();
default:
throw new IllegalArgumentException("Unsupported column index: " + columnIndex);
}
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings({"unchecked"})
public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) {
assert columnIndex == ACTION;
if ( ArrayUtil.indexOf( myLastEditableSelectedRows , rowIndex ) > -1 ) {
final ContiguousIntIntervalTracker intervalBuilder = new ContiguousIntIntervalTracker();
for (int lastEditableSelectedRow : myLastEditableSelectedRows) {
intervalBuilder.track( lastEditableSelectedRow );
setRowAction(aValue, lastEditableSelectedRow, columnIndex);
}
setSelection(intervalBuilder);
} else {
setRowAction(aValue, rowIndex, columnIndex);
}
}
private void setSelection(ContiguousIntIntervalTracker intervalBuilder) {
myCommitsTable.getSelectionModel().setSelectionInterval( intervalBuilder.getMin() , intervalBuilder.getMax() );
}
private void setRowAction(Object aValue, int rowIndex, int columnIndex) {
GitRebaseEntry e = myEntries.get(rowIndex);
e.setAction((GitRebaseEntry.Action)aValue);
fireTableCellUpdated(rowIndex, columnIndex);
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCellEditable(final int rowIndex, final int columnIndex) {
myLastEditableSelectedRows = myCommitsTable.getSelectedRows();
return columnIndex == ACTION;
}
/**
* Load data from the file
*
* @param file the file to load
* @throws IOException if file could not be loaded
*/
public void load(final String file) throws IOException {
String encoding = GitConfigUtil.getLogEncoding(myProject, myGitRoot);
final StringScanner s = new StringScanner(FileUtil.loadFile(new File(file), encoding));
while (s.hasMoreData()) {
if (s.isEol() || s.startsWith('#') || s.startsWith("noop")) {
s.nextLine();
continue;
}
String action = s.spaceToken();
assert "pick".equals(action) : "Initial action should be pick: " + action;
String hash = s.spaceToken();
String comment = s.line();
myEntries.add(new GitRebaseEntry(hash, comment));
}
}
/**
* Save text to the file
*
* @param file the file to save to
* @throws IOException if there is IO problem
*/
public void save(final String file) throws IOException {
String encoding = GitConfigUtil.getLogEncoding(myProject, myGitRoot);
PrintWriter out = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), encoding));
try {
for (GitRebaseEntry e : myEntries) {
if (e.getAction() != GitRebaseEntry.Action.skip) {
out.println(e.getAction().toString() + " " + e.getCommit() + " " + e.getSubject());
}
}
}
finally {
out.close();
}
}
/**
* Save text to the file
*
* @param file the file to save to
* @throws IOException if there is IO problem
*/
public void cancel(final String file) throws IOException {
PrintWriter out = new PrintWriter(new FileWriter(file));
try {
//noinspection HardCodedStringLiteral
out.println("# rebase is cancelled");
}
finally {
out.close();
}
}
public void moveRows(int[] rows, MoveDirection direction) {
myCommitsTable.removeEditor();
final ContiguousIntIntervalTracker selectionInterval = new ContiguousIntIntervalTracker();
final ContiguousIntIntervalTracker rowsUpdatedInterval = new ContiguousIntIntervalTracker();
for (int row : direction.preprocessRowIndexes( rows )) {
final int targetIndex = row + direction.offset();
assertIndexInRange( row , targetIndex );
Collections.swap( myEntries , row , targetIndex );
rowsUpdatedInterval.track(targetIndex, row );
selectionInterval.track( targetIndex );
}
if ( selectionInterval.hasValues() ) {
setSelection(selectionInterval);
fireTableRowsUpdated(rowsUpdatedInterval.getMin(), rowsUpdatedInterval.getMax());
}
}
private void assertIndexInRange(int... rowIndexes) {
for (int rowIndex : rowIndexes) {
assert rowIndex >= 0;
assert rowIndex < myEntries.size();
}
}
}
private static class ContiguousIntIntervalTracker {
private Integer myMin = null;
private Integer myMax = null;
private static final int UNSET_VALUE = -1;
public Integer getMin() {
return myMin == null ? UNSET_VALUE : myMin;
}
public Integer getMax() {
return myMax == null ? UNSET_VALUE : myMax;
}
public void track( int... entries ) {
for (int entry : entries) {
checkMax( entry );
checkMin( entry );
}
}
private void checkMax(int entry) {
if ( null == myMax || entry > myMax ) {
myMax = entry;
}
}
private void checkMin(int entry) {
if ( null == myMin || entry < myMin ) {
myMin = entry;
}
}
public boolean hasValues() {
return ( null != myMin && null != myMax);
}
}
private enum MoveDirection {
up , down;
public int offset() {
if (this == up) {
return -1;
} else {
return +1;
}
}
public int[] preprocessRowIndexes( int[] seletion ) {
int[] copy = seletion.clone();
Arrays.sort( copy );
if (this == up) {
return copy;
} else {
return ArrayUtil.reverseArray( copy );
}
}
}
private class MoveUpDownActionListener implements ActionListener {
private final MoveDirection direction;
public MoveUpDownActionListener(MoveDirection direction) {
this.direction = direction;
}
public void actionPerformed(final ActionEvent e) {
myTableModel.moveRows(myCommitsTable.getSelectedRows(), direction );
}
}
}