Append Signed-off-by text in the commit message editor
[egit/torarne.git] / org.spearce.egit.ui / src / org / spearce / egit / ui / internal / dialogs / CommitDialog.java
blobbbe7193195fe986224af73fc7e88bda97e6eb5f9
1 /*******************************************************************************
2 * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
3 * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com.dewire.com>
4 * Copyright (C) 2007, Robin Rosenberg <me@lathund.dewire.com>
5 * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
6 * Copyright (C) 2007, Shawn O. Pearce <spearce@spearce.org>
8 * All rights reserved. This program and the accompanying materials
9 * are made available under the terms of the Eclipse Public License v1.0
10 * See LICENSE for the full license text, also available.
11 *******************************************************************************/
12 package org.spearce.egit.ui.internal.dialogs;
14 import java.io.File;
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.Collections;
18 import java.util.Comparator;
19 import java.util.Iterator;
21 import org.eclipse.core.resources.IFile;
22 import org.eclipse.core.resources.IProject;
23 import org.eclipse.jface.dialogs.Dialog;
24 import org.eclipse.jface.dialogs.IDialogConstants;
25 import org.eclipse.jface.dialogs.MessageDialog;
26 import org.eclipse.jface.layout.GridDataFactory;
27 import org.eclipse.jface.viewers.CheckboxTableViewer;
28 import org.eclipse.jface.viewers.IStructuredContentProvider;
29 import org.eclipse.jface.viewers.IStructuredSelection;
30 import org.eclipse.jface.viewers.ITableLabelProvider;
31 import org.eclipse.jface.viewers.Viewer;
32 import org.eclipse.jface.viewers.ViewerComparator;
33 import org.eclipse.swt.SWT;
34 import org.eclipse.swt.events.KeyAdapter;
35 import org.eclipse.swt.events.KeyEvent;
36 import org.eclipse.swt.events.ModifyEvent;
37 import org.eclipse.swt.events.ModifyListener;
38 import org.eclipse.swt.events.SelectionAdapter;
39 import org.eclipse.swt.events.SelectionEvent;
40 import org.eclipse.swt.events.SelectionListener;
41 import org.eclipse.swt.graphics.Image;
42 import org.eclipse.swt.layout.GridLayout;
43 import org.eclipse.swt.widgets.Button;
44 import org.eclipse.swt.widgets.Composite;
45 import org.eclipse.swt.widgets.Control;
46 import org.eclipse.swt.widgets.Event;
47 import org.eclipse.swt.widgets.Label;
48 import org.eclipse.swt.widgets.Listener;
49 import org.eclipse.swt.widgets.Menu;
50 import org.eclipse.swt.widgets.MenuItem;
51 import org.eclipse.swt.widgets.Shell;
52 import org.eclipse.swt.widgets.Table;
53 import org.eclipse.swt.widgets.TableColumn;
54 import org.eclipse.swt.widgets.Text;
55 import org.eclipse.ui.model.WorkbenchLabelProvider;
56 import org.spearce.egit.core.project.RepositoryMapping;
57 import org.spearce.egit.ui.UIText;
58 import org.spearce.jgit.lib.Constants;
59 import org.spearce.jgit.lib.GitIndex;
60 import org.spearce.jgit.lib.PersonIdent;
61 import org.spearce.jgit.lib.Repository;
62 import org.spearce.jgit.lib.Tree;
63 import org.spearce.jgit.lib.TreeEntry;
64 import org.spearce.jgit.lib.GitIndex.Entry;
66 /**
67 * Dialog is shown to user when they request to commit files. Changes in the
68 * selected portion of the tree are shown.
70 public class CommitDialog extends Dialog {
72 class CommitContentProvider implements IStructuredContentProvider {
74 public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
75 // Empty
78 public void dispose() {
79 // Empty
82 public Object[] getElements(Object inputElement) {
83 return items.toArray();
88 class CommitLabelProvider extends WorkbenchLabelProvider implements
89 ITableLabelProvider {
90 public String getColumnText(Object obj, int columnIndex) {
91 CommitItem item = (CommitItem) obj;
93 switch (columnIndex) {
94 case 0:
95 return item.status;
97 case 1:
98 return item.file.getProject().getName() + ": " //$NON-NLS-1$
99 + item.file.getProjectRelativePath();
101 default:
102 return null;
106 public Image getColumnImage(Object element, int columnIndex) {
107 if (columnIndex == 0)
108 return getImage(element);
109 return null;
113 ArrayList<CommitItem> items = new ArrayList<CommitItem>();
116 * @param parentShell
118 public CommitDialog(Shell parentShell) {
119 super(parentShell);
122 @Override
123 protected void createButtonsForButtonBar(Composite parent) {
124 createButton(parent, IDialogConstants.SELECT_ALL_ID, UIText.CommitDialog_SelectAll, false);
125 createButton(parent, IDialogConstants.DESELECT_ALL_ID, UIText.CommitDialog_DeselectAll, false);
127 createButton(parent, IDialogConstants.OK_ID, UIText.CommitDialog_Commit, true);
128 createButton(parent, IDialogConstants.CANCEL_ID,
129 IDialogConstants.CANCEL_LABEL, false);
132 Text commitText;
133 Text authorText;
134 Text committerText;
135 Button amendingButton;
136 Button signedOffButton;
138 CheckboxTableViewer filesViewer;
140 @Override
141 protected Control createDialogArea(Composite parent) {
142 Composite container = (Composite) super.createDialogArea(parent);
143 parent.getShell().setText(UIText.CommitDialog_CommitChanges);
145 GridLayout layout = new GridLayout(2, false);
146 container.setLayout(layout);
148 Label label = new Label(container, SWT.LEFT);
149 label.setText(UIText.CommitDialog_CommitMessage);
150 label.setLayoutData(GridDataFactory.fillDefaults().span(2, 1).grab(true, false).create());
152 commitText = new Text(container, SWT.MULTI | SWT.BORDER | SWT.V_SCROLL);
153 commitText.setLayoutData(GridDataFactory.fillDefaults().span(2, 1).grab(true, true)
154 .hint(600, 200).create());
156 // allow to commit with ctrl-enter
157 commitText.addKeyListener(new KeyAdapter() {
158 public void keyPressed(KeyEvent arg0) {
159 if (arg0.keyCode == SWT.CR
160 && (arg0.stateMask & SWT.CONTROL) > 0) {
161 okPressed();
162 } else if (arg0.keyCode == SWT.TAB
163 && (arg0.stateMask & SWT.SHIFT) == 0) {
164 arg0.doit = false;
165 commitText.traverse(SWT.TRAVERSE_TAB_NEXT);
170 new Label(container, SWT.LEFT).setText(UIText.CommitDialog_Author);
171 authorText = new Text(container, SWT.BORDER);
172 authorText.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create());
173 if (author != null)
174 authorText.setText(author);
176 new Label(container, SWT.LEFT).setText(UIText.CommitDialog_Committer);
177 committerText = new Text(container, SWT.BORDER);
178 committerText.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).create());
179 if (committer != null)
180 committerText.setText(committer);
181 committerText.addModifyListener(new ModifyListener() {
182 public void modifyText(ModifyEvent e) {
183 if (signedOffButton.getSelection()) {
184 // the commit message is signed
185 // the signature must be updated
186 String oldCommitText = commitText.getText();
187 oldCommitText = removeLastLine(oldCommitText);
188 oldCommitText = signOff(oldCommitText);
189 commitText.setText(oldCommitText);
194 amendingButton = new Button(container, SWT.CHECK);
195 if (amending) {
196 amendingButton.setSelection(amending);
197 amendingButton.setEnabled(false); // if already set, don't allow any changes
198 commitText.setText(previousCommitMessage);
199 authorText.setText(previousAuthor);
200 } else if (!amendAllowed) {
201 amendingButton.setEnabled(false);
203 amendingButton.addSelectionListener(new SelectionListener() {
204 boolean alreadyAdded = false;
205 public void widgetSelected(SelectionEvent arg0) {
206 if (alreadyAdded)
207 return;
208 if (amendingButton.getSelection()) {
209 alreadyAdded = true;
210 String curText = commitText.getText();
211 if (curText.length() > 0)
212 curText += "\n"; //$NON-NLS-1$
213 commitText.setText(curText + previousCommitMessage);
214 authorText.setText(previousAuthor);
218 public void widgetDefaultSelected(SelectionEvent arg0) {
219 // Empty
223 amendingButton.setText(UIText.CommitDialog_AmendPreviousCommit);
224 amendingButton.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).span(2, 1).create());
226 signedOffButton = new Button(container, SWT.CHECK);
227 signedOffButton.setSelection(signedOff);
228 signedOffButton.setText(UIText.CommitDialog_AddSOB);
229 signedOffButton.setLayoutData(GridDataFactory.fillDefaults().grab(true, false).span(2, 1).create());
231 signedOffButton.addSelectionListener(new SelectionListener() {
232 public void widgetSelected(SelectionEvent arg0) {
233 if (signedOffButton.getSelection()) {
234 // add signed off line
235 commitText.setText(signOff(commitText.getText()));
236 } else {
237 // remove signed off line
238 commitText.setText(removeLastLine(commitText.getText()));
242 public void widgetDefaultSelected(SelectionEvent arg0) {
243 // Empty
247 commitText.addModifyListener(new ModifyListener() {
248 public void modifyText(ModifyEvent e) {
249 updateSignedOffButton();
252 updateSignedOffButton();
254 Table resourcesTable = new Table(container, SWT.H_SCROLL | SWT.V_SCROLL
255 | SWT.FULL_SELECTION | SWT.MULTI | SWT.CHECK | SWT.BORDER);
256 resourcesTable.setLayoutData(GridDataFactory.fillDefaults().hint(600,
257 200).span(2,1).grab(true, true).create());
259 resourcesTable.setHeaderVisible(true);
260 TableColumn statCol = new TableColumn(resourcesTable, SWT.LEFT);
261 statCol.setText(UIText.CommitDialog_Status);
262 statCol.setWidth(150);
263 statCol.addSelectionListener(new HeaderSelectionListener(CommitItem.Order.ByStatus));
265 TableColumn resourceCol = new TableColumn(resourcesTable, SWT.LEFT);
266 resourceCol.setText(UIText.CommitDialog_File);
267 resourceCol.setWidth(415);
268 resourceCol.addSelectionListener(new HeaderSelectionListener(CommitItem.Order.ByFile));
270 filesViewer = new CheckboxTableViewer(resourcesTable);
271 filesViewer.setContentProvider(new CommitContentProvider());
272 filesViewer.setLabelProvider(new CommitLabelProvider());
273 filesViewer.setInput(items);
274 filesViewer.setAllChecked(true);
275 filesViewer.getTable().setMenu(getContextMenu());
277 container.pack();
278 return container;
281 private void updateSignedOffButton() {
282 signedOffButton.setSelection(getLastLine(commitText.getText()).equals(getSignedOff()));
285 private String getSignedOff() {
286 return Constants.SIGNED_OFF_BY_TAG + committerText.getText();
289 private String signOff(String input) {
290 String output = input;
291 if (!output.endsWith(Text.DELIMITER))
292 output += Text.DELIMITER;
294 // if the last line is not a signed off (amend a commit), had a line break
295 if (!getLastLine(output).startsWith(Constants.SIGNED_OFF_BY_TAG))
296 output += Text.DELIMITER;
297 output += getSignedOff();
298 return output;
301 private String getLastLine(String input) {
302 String output = removeLastLineBreak(input);
303 int breakLength = Text.DELIMITER.length();
305 // get the last line
306 int lastIndexOfLineBreak = output.lastIndexOf(Text.DELIMITER);
307 return lastIndexOfLineBreak == -1 ? output : output.substring(lastIndexOfLineBreak + breakLength, output.length());
310 private String removeLastLine(String input) {
311 String output = removeLastLineBreak(input);
313 // remove the last line if possible
314 int lastIndexOfLineBreak = output.lastIndexOf(Text.DELIMITER);
315 return lastIndexOfLineBreak == -1 ? "" : output.substring(0, lastIndexOfLineBreak); //$NON-NLS-1$
318 private String removeLastLineBreak(String input) {
319 String output = input;
320 int breakLength = Text.DELIMITER.length();
322 // remove last line break if exist
323 int lastIndexOfLineBreak = output.lastIndexOf(Text.DELIMITER);
324 if (lastIndexOfLineBreak != -1 && lastIndexOfLineBreak == output.length() - breakLength)
325 output = output.substring(0, output.length() - breakLength);
327 return output;
330 private Menu getContextMenu() {
331 Menu menu = new Menu(filesViewer.getTable());
332 MenuItem item = new MenuItem(menu, SWT.PUSH);
333 item.setText(UIText.CommitDialog_AddFileOnDiskToIndex);
334 item.addListener(SWT.Selection, new Listener() {
335 public void handleEvent(Event arg0) {
336 IStructuredSelection sel = (IStructuredSelection) filesViewer.getSelection();
337 if (sel.isEmpty()) {
338 return;
340 try {
341 ArrayList<GitIndex> changedIndexes = new ArrayList<GitIndex>();
342 for (Iterator<Object> it = sel.iterator(); it.hasNext();) {
343 CommitItem commitItem = (CommitItem) it.next();
345 IProject project = commitItem.file.getProject();
346 RepositoryMapping map = RepositoryMapping.getMapping(project);
348 Repository repo = map.getRepository();
349 GitIndex index = null;
350 index = repo.getIndex();
351 Entry entry = index.getEntry(map.getRepoRelativePath(commitItem.file));
352 if (entry != null && entry.isModified(map.getWorkDir())) {
353 entry.update(new File(map.getWorkDir(), entry.getName()));
354 if (!changedIndexes.contains(index))
355 changedIndexes.add(index);
358 if (!changedIndexes.isEmpty()) {
359 for (GitIndex idx : changedIndexes) {
360 idx.write();
362 filesViewer.refresh(true);
364 } catch (IOException e) {
365 e.printStackTrace();
366 return;
371 return menu;
374 private static String getFileStatus(IFile file) {
375 String prefix = UIText.CommitDialog_StatusUnknown;
377 try {
378 RepositoryMapping repositoryMapping = RepositoryMapping
379 .getMapping(file.getProject());
381 Repository repo = repositoryMapping.getRepository();
382 GitIndex index = repo.getIndex();
383 Tree headTree = repo.mapTree(Constants.HEAD);
385 String repoPath = repositoryMapping.getRepoRelativePath(file);
386 TreeEntry headEntry = headTree.findBlobMember(repoPath);
387 boolean headExists = headTree.existsBlob(repoPath);
389 Entry indexEntry = index.getEntry(repoPath);
390 if (headEntry == null) {
391 prefix = UIText.CommitDialog_StatusAdded;
392 if (indexEntry.isModified(repositoryMapping.getWorkDir()))
393 prefix = UIText.CommitDialog_StatusAddedIndexDiff;
394 } else if (indexEntry == null) {
395 prefix = UIText.CommitDialog_StatusRemoved;
396 } else if (headExists
397 && !headEntry.getId().equals(indexEntry.getObjectId())) {
398 prefix = UIText.CommitDialog_StatusModified;
400 if (indexEntry.isModified(repositoryMapping.getWorkDir()))
401 prefix = UIText.CommitDialog_StatusModifiedIndexDiff;
402 } else if (!new File(repositoryMapping.getWorkDir(), indexEntry
403 .getName()).isFile()) {
404 prefix = UIText.CommitDialog_StatusRemovedNotStaged;
405 } else if (indexEntry.isModified(repositoryMapping.getWorkDir())) {
406 prefix = UIText.CommitDialog_StatusModifiedNotStaged;
409 } catch (Exception e) {
412 return prefix;
416 * @return The message the user entered
418 public String getCommitMessage() {
419 return commitMessage.replaceAll(Text.DELIMITER, "\n"); //$NON-NLS-1$;
423 * Preset a commit message. This might be for amending a commit.
424 * @param s the commit message
426 public void setCommitMessage(String s) {
427 this.commitMessage = s;
430 private String commitMessage = ""; //$NON-NLS-1$
431 private String author = null;
432 private String committer = null;
433 private String previousAuthor = null;
434 private boolean signedOff = false;
435 private boolean amending = false;
436 private boolean amendAllowed = true;
438 private ArrayList<IFile> selectedFiles = new ArrayList<IFile>();
439 private String previousCommitMessage = ""; //$NON-NLS-1$
442 * Pre-select suggested set of resources to commit
444 * @param items
446 public void setSelectedFiles(IFile[] items) {
447 Collections.addAll(selectedFiles, items);
451 * @return the resources selected by the user to commit.
453 public IFile[] getSelectedFiles() {
454 return selectedFiles.toArray(new IFile[0]);
457 class HeaderSelectionListener extends SelectionAdapter {
459 private CommitItem.Order order;
461 private boolean reversed;
463 public HeaderSelectionListener(CommitItem.Order order) {
464 this.order = order;
467 @Override
468 public void widgetSelected(SelectionEvent e) {
469 TableColumn column = (TableColumn)e.widget;
470 Table table = column.getParent();
472 if (column == table.getSortColumn()) {
473 reversed = !reversed;
474 } else {
475 reversed = false;
477 table.setSortColumn(column);
479 Comparator<CommitItem> comparator;
480 if (reversed) {
481 comparator = order.descending();
482 table.setSortDirection(SWT.DOWN);
483 } else {
484 comparator = order;
485 table.setSortDirection(SWT.UP);
488 filesViewer.setComparator(new CommitViewerComparator(comparator));
493 @Override
494 protected void okPressed() {
495 commitMessage = commitText.getText();
496 author = authorText.getText().trim();
497 committer = committerText.getText().trim();
498 signedOff = signedOffButton.getSelection();
499 amending = amendingButton.getSelection();
501 Object[] checkedElements = filesViewer.getCheckedElements();
502 selectedFiles.clear();
503 for (Object obj : checkedElements)
504 selectedFiles.add(((CommitItem) obj).file);
506 if (commitMessage.trim().length() == 0) {
507 MessageDialog.openWarning(getShell(), UIText.CommitDialog_ErrorNoMessage, UIText.CommitDialog_ErrorMustEnterCommitMessage);
508 return;
511 boolean authorValid = false;
512 if (author.length() > 0) {
513 try {
514 new PersonIdent(author);
515 authorValid = true;
516 } catch (IllegalArgumentException e) {
517 authorValid = false;
520 if (!authorValid) {
521 MessageDialog.openWarning(getShell(), UIText.CommitDialog_ErrorInvalidAuthor, UIText.CommitDialog_ErrorInvalidAuthorSpecified);
522 return;
525 boolean committerValid = false;
526 if (committer.length() > 0) {
527 try {
528 new PersonIdent(committer);
529 committerValid = true;
530 } catch (IllegalArgumentException e) {
531 committerValid = false;
534 if (!committerValid) {
535 MessageDialog.openWarning(getShell(), UIText.CommitDialog_ErrorInvalidAuthor, UIText.CommitDialog_ErrorInvalidCommitterSpecified);
536 return;
539 if (selectedFiles.isEmpty() && !amending) {
540 MessageDialog.openWarning(getShell(), UIText.CommitDialog_ErrorNoItemsSelected, UIText.CommitDialog_ErrorNoItemsSelectedToBeCommitted);
541 return;
543 super.okPressed();
547 * Set the total list of changed resources, including additions and
548 * removals
550 * @param files potentially affected by a new commit
552 public void setFileList(ArrayList<IFile> files) {
553 items.clear();
554 for (IFile file : files) {
555 CommitItem item = new CommitItem();
556 item.status = getFileStatus(file);
557 item.file = file;
558 items.add(item);
562 @Override
563 protected void buttonPressed(int buttonId) {
564 if (IDialogConstants.SELECT_ALL_ID == buttonId) {
565 filesViewer.setAllChecked(true);
567 if (IDialogConstants.DESELECT_ALL_ID == buttonId) {
568 filesViewer.setAllChecked(false);
570 super.buttonPressed(buttonId);
574 * @return The author to set for the commit
576 public String getAuthor() {
577 return author;
581 * Pre-set author for the commit
583 * @param author
585 public void setAuthor(String author) {
586 this.author = author;
590 * @return The committer to set for the commit
592 public String getCommitter() {
593 return committer;
597 * Pre-set committer for the commit
599 * @param committer
601 public void setCommitter(String committer) {
602 this.committer = committer;
606 * Pre-set the previous author if amending the commit
608 * @param previousAuthor
610 public void setPreviousAuthor(String previousAuthor) {
611 this.previousAuthor = previousAuthor;
615 * @return whether to auto-add a signed-off line to the message
617 public boolean isSignedOff() {
618 return signedOff;
622 * Pre-set whether a signed-off line should be included in the commit
623 * message.
625 * @param signedOff
627 public void setSignedOff(boolean signedOff) {
628 this.signedOff = signedOff;
632 * @return whether the last commit is to be amended
634 public boolean isAmending() {
635 return amending;
639 * Pre-set whether the last commit is going to be amended
641 * @param amending
643 public void setAmending(boolean amending) {
644 this.amending = amending;
648 * Set the message from the previous commit for amending.
650 * @param string
652 public void setPreviousCommitMessage(String string) {
653 this.previousCommitMessage = string;
657 * Set whether the previous commit may be amended
659 * @param amendAllowed
661 public void setAmendAllowed(boolean amendAllowed) {
662 this.amendAllowed = amendAllowed;
665 @Override
666 protected int getShellStyle() {
667 return super.getShellStyle() | SWT.RESIZE;
671 class CommitItem {
672 String status;
674 IFile file;
676 public static enum Order implements Comparator<CommitItem> {
677 ByStatus() {
679 public int compare(CommitItem o1, CommitItem o2) {
680 return o1.status.compareTo(o2.status);
685 ByFile() {
687 public int compare(CommitItem o1, CommitItem o2) {
688 return o1.file.getProjectRelativePath().toString().
689 compareTo(o2.file.getProjectRelativePath().toString());
694 public Comparator<CommitItem> ascending() {
695 return this;
698 public Comparator<CommitItem> descending() {
699 return Collections.reverseOrder(this);
704 class CommitViewerComparator extends ViewerComparator {
706 public CommitViewerComparator(Comparator comparator){
707 super(comparator);
710 @SuppressWarnings("unchecked")
711 @Override
712 public int compare(Viewer viewer, Object e1, Object e2) {
713 return getComparator().compare(e1, e2);