1 /*******************************************************************************
2 * Copyright (c) 2011, 2021 GitHub Inc. and others.
3 * All rights reserved. This program and the accompanying materials
4 * are made available under the terms of the Eclipse Public License 2.0
5 * which accompanies this distribution, and is available at
6 * https://www.eclipse.org/legal/epl-2.0/
8 * SPDX-License-Identifier: EPL-2.0
11 * Kevin Sawicki (GitHub Inc.) - initial API and implementation
12 * Daniel Megert <daniel_megert@ch.ibm.com> - Added context menu to the Commit Editor's header text
13 * Tomasz Zarna <Tomasz.Zarna@pl.ibm.com> - Add "Revert" action to Commit Editor
14 *******************************************************************************/
15 package org
.eclipse
.egit
.ui
.internal
.commit
;
17 import java
.text
.MessageFormat
;
18 import java
.util
.concurrent
.atomic
.AtomicBoolean
;
20 import org
.eclipse
.core
.runtime
.Adapters
;
21 import org
.eclipse
.core
.runtime
.IProgressMonitor
;
22 import org
.eclipse
.core
.runtime
.IStatus
;
23 import org
.eclipse
.core
.runtime
.Status
;
24 import org
.eclipse
.egit
.core
.RepositoryCache
;
25 import org
.eclipse
.egit
.core
.internal
.IRepositoryCommit
;
26 import org
.eclipse
.egit
.ui
.Activator
;
27 import org
.eclipse
.egit
.ui
.internal
.CommonUtils
;
28 import org
.eclipse
.egit
.ui
.internal
.UIIcons
;
29 import org
.eclipse
.egit
.ui
.internal
.UIText
;
30 import org
.eclipse
.egit
.ui
.internal
.actions
.ActionCommands
;
31 import org
.eclipse
.egit
.ui
.internal
.components
.EditorVisibilityTracker
;
32 import org
.eclipse
.egit
.ui
.internal
.properties
.CommitPropertySource
;
33 import org
.eclipse
.egit
.ui
.internal
.properties
.GitPropertySheetPage
;
34 import org
.eclipse
.egit
.ui
.internal
.repository
.RepositoriesView
;
35 import org
.eclipse
.jface
.action
.Action
;
36 import org
.eclipse
.jface
.action
.ActionContributionItem
;
37 import org
.eclipse
.jface
.action
.ContributionManager
;
38 import org
.eclipse
.jface
.action
.ControlContribution
;
39 import org
.eclipse
.jface
.action
.IAction
;
40 import org
.eclipse
.jface
.action
.IContributionItem
;
41 import org
.eclipse
.jface
.action
.IToolBarManager
;
42 import org
.eclipse
.jface
.action
.ToolBarManager
;
43 import org
.eclipse
.jface
.dialogs
.IPageChangedListener
;
44 import org
.eclipse
.jface
.resource
.ImageDescriptor
;
45 import org
.eclipse
.jface
.resource
.JFaceResources
;
46 import org
.eclipse
.jface
.viewers
.ISelection
;
47 import org
.eclipse
.jface
.viewers
.ISelectionChangedListener
;
48 import org
.eclipse
.jface
.viewers
.ISelectionProvider
;
49 import org
.eclipse
.jface
.viewers
.StructuredSelection
;
50 import org
.eclipse
.jgit
.api
.StashListCommand
;
51 import org
.eclipse
.jgit
.events
.ListenerHandle
;
52 import org
.eclipse
.jgit
.events
.RefsChangedEvent
;
53 import org
.eclipse
.jgit
.events
.RefsChangedListener
;
54 import org
.eclipse
.jgit
.lib
.ObjectId
;
55 import org
.eclipse
.jgit
.lib
.Repository
;
56 import org
.eclipse
.jgit
.revwalk
.RevCommit
;
57 import org
.eclipse
.swt
.SWT
;
58 import org
.eclipse
.swt
.widgets
.Composite
;
59 import org
.eclipse
.swt
.widgets
.Control
;
60 import org
.eclipse
.ui
.IEditorActionBarContributor
;
61 import org
.eclipse
.ui
.IEditorInput
;
62 import org
.eclipse
.ui
.IEditorPart
;
63 import org
.eclipse
.ui
.IEditorSite
;
64 import org
.eclipse
.ui
.IPartService
;
65 import org
.eclipse
.ui
.ISharedImages
;
66 import org
.eclipse
.ui
.ISources
;
67 import org
.eclipse
.ui
.IWorkbenchPartReference
;
68 import org
.eclipse
.ui
.PartInitException
;
69 import org
.eclipse
.ui
.PlatformUI
;
70 import org
.eclipse
.ui
.forms
.IFormColors
;
71 import org
.eclipse
.ui
.forms
.IManagedForm
;
72 import org
.eclipse
.ui
.forms
.editor
.IFormPage
;
73 import org
.eclipse
.ui
.forms
.editor
.SharedHeaderFormEditor
;
74 import org
.eclipse
.ui
.forms
.events
.HyperlinkAdapter
;
75 import org
.eclipse
.ui
.forms
.events
.HyperlinkEvent
;
76 import org
.eclipse
.ui
.forms
.widgets
.FormToolkit
;
77 import org
.eclipse
.ui
.forms
.widgets
.ImageHyperlink
;
78 import org
.eclipse
.ui
.forms
.widgets
.ScrolledForm
;
79 import org
.eclipse
.ui
.ide
.IDE
;
80 import org
.eclipse
.ui
.menus
.IMenuService
;
81 import org
.eclipse
.ui
.part
.IShowInSource
;
82 import org
.eclipse
.ui
.part
.IShowInTargetList
;
83 import org
.eclipse
.ui
.part
.MultiPageEditorSite
;
84 import org
.eclipse
.ui
.part
.ShowInContext
;
85 import org
.eclipse
.ui
.progress
.UIJob
;
86 import org
.eclipse
.ui
.services
.IEvaluationService
;
87 import org
.eclipse
.ui
.views
.contentoutline
.IContentOutlinePage
;
88 import org
.eclipse
.ui
.views
.properties
.IPropertySheetPage
;
89 import org
.eclipse
.ui
.views
.properties
.IPropertySource
;
90 import org
.eclipse
.ui
.views
.properties
.PropertySheetPage
;
93 * Editor class to view a commit in a form editor.
95 public class CommitEditor
extends SharedHeaderFormEditor
implements
96 RefsChangedListener
, IShowInSource
, IShowInTargetList
{
101 public static final String ID
= "org.eclipse.egit.ui.commitEditor"; //$NON-NLS-1$
103 private static final String TOOLBAR_HEADER_ID
= ID
+ ".header.toolbar"; //$NON-NLS-1$
106 * Open commit in editor
109 * @return opened editor part
110 * @throws PartInitException
112 public static final IEditorPart
open(RepositoryCommit commit
)
113 throws PartInitException
{
114 return open(commit
, true);
118 * Open commit in editor
121 * @param activateOnOpen <code>true</code> if the newly opened editor should be activated
122 * @return opened editor part
123 * @throws PartInitException
126 public static final IEditorPart
open(RepositoryCommit commit
, boolean activateOnOpen
)
127 throws PartInitException
{
128 CommitEditorInput input
= new CommitEditorInput(commit
);
129 return IDE
.openEditor(PlatformUI
.getWorkbench()
130 .getActiveWorkbenchWindow().getActivePage(), input
, ID
, activateOnOpen
);
134 * Open commit in editor
137 * @param activateOnOpen <code>true</code> if the newly opened editor should be activated
138 * @return opened editor part or null if opening fails
141 public static final IEditorPart
openQuiet(RepositoryCommit commit
, boolean activateOnOpen
) {
143 return open(commit
, activateOnOpen
);
144 } catch (PartInitException e
) {
145 Activator
.logError(e
.getMessage(), e
);
151 * Open commit in editor
154 * @return opened editor part or null if opening fails
156 public static final IEditorPart
openQuiet(RepositoryCommit commit
) {
157 return openQuiet(commit
, true);
160 private final AtomicBoolean runRefresh
= new AtomicBoolean();
162 private IContentOutlinePage outlinePage
;
164 private CommitEditorPage commitPage
;
166 private DiffEditorPage diffPage
;
168 private NotesEditorPage notePage
;
170 private ListenerHandle refListenerHandle
;
172 private FocusTracker headerFocusTracker
= new FocusTracker();
174 private IToolBarManager toolbar
;
176 private IPageChangedListener pageListener
;
179 * Ensures that the toolbar buttons in the header are properly updated, and
180 * that the editor refreshes on {@link RefsChangedEvent}s only when visible.
181 * Created and installed in {@link #createHeaderContents(IManagedForm)},
182 * which occurs before {@link #addPages()} where we start listening for
183 * {@link RefsChangedEvent}s.
185 private EditorVisibilityTracker visibilityListener
;
187 private static class CommitEditorNestedSite
extends MultiPageEditorSite
{
189 public CommitEditorNestedSite(CommitEditor topLevelEditor
,
190 IEditorPart nestedEditor
) {
191 super(topLevelEditor
, nestedEditor
);
195 public IEditorActionBarContributor
getActionBarContributor() {
196 IEditorActionBarContributor globalContributor
= getMultiPageEditor()
197 .getEditorSite().getActionBarContributor();
198 if (globalContributor
instanceof CommitEditorActionBarContributor
) {
199 return ((CommitEditorActionBarContributor
) globalContributor
)
200 .getTextEditorActionContributor();
202 return super.getActionBarContributor();
208 protected IEditorSite
createSite(IEditorPart editor
) {
209 return new CommitEditorNestedSite(this, editor
);
213 * @see org.eclipse.ui.forms.editor.FormEditor#addPages()
216 protected void addPages() {
218 if (getCommit().isStash()) {
219 commitPage
= new StashEditorPage(this);
221 commitPage
= new CommitEditorPage(this);
224 RepositoryCommit commit
= getCommit();
225 if (commit
!= null) {
226 diffPage
= new DiffEditorPage(this);
227 addPage(diffPage
, new DiffEditorInput(commit
));
228 if (commit
.getNotes().length
> 0) {
229 notePage
= new NotesEditorPage(this);
233 } catch (PartInitException e
) {
234 Activator
.error("Error adding page", e
); //$NON-NLS-1$
236 refListenerHandle
= RepositoryCache
.INSTANCE
.getGlobalListenerList()
237 .addRefsChangedListener(this);
239 pageListener
= event
-> {
240 IEvaluationService service
= PlatformUI
.getWorkbench()
241 .getService(IEvaluationService
.class);
242 if (service
!= null) {
243 // Update enablement of "Save As..."
244 service
.requestEvaluation(ISources
.ACTIVE_PART_NAME
);
246 if (event
.getSelectedPage() == commitPage
) {
250 addPageChangedListener(pageListener
);
253 private IContributionItem
createActionContributionItem(String commandId
,
254 String title
, ImageDescriptor icon
) {
255 IAction action
= new Action(title
, icon
) {
259 CommonUtils
.runCommand(commandId
,
260 new StructuredSelection(getCommit()));
263 return new ActionContributionItem(action
);
267 * @see org.eclipse.ui.forms.editor.SharedHeaderFormEditor#createHeaderContents(org.eclipse.ui.forms.IManagedForm)
270 protected void createHeaderContents(IManagedForm headerForm
) {
271 headerForm
.addPart(new FocusManagerFormPart(headerFocusTracker
) {
274 public void setDefaultFocus() {
275 headerForm
.getForm().getForm().setFocus();
278 RepositoryCommit commit
= getCommit();
279 ScrolledForm form
= headerForm
.getForm();
280 String commitName
= commit
.getRevCommit().name();
281 String title
= getFormattedHeaderTitle(commitName
);
282 HeaderText text
= new HeaderText(form
.getForm(), title
, commitName
);
283 Control textControl
= text
.getControl();
284 if (textControl
!= null) {
285 headerFocusTracker
.addToFocusTracking(textControl
);
287 form
.setToolTipText(commitName
);
288 getToolkit().decorateFormHeading(form
.getForm());
290 toolbar
= form
.getToolBarManager();
292 ControlContribution repositoryLabelControl
= new ControlContribution(
293 "repositoryLabel") { //$NON-NLS-1$
295 protected Control
createControl(Composite parent
) {
296 FormToolkit toolkit
= getHeaderForm().getToolkit();
297 String label
= getCommit().getRepositoryName();
299 ImageHyperlink link
= new ImageHyperlink(parent
, SWT
.NONE
);
300 // Focus tracking on this link doesn't really work. It's a
301 // focusable control inside another focusable control (the
302 // toolbar). When focus leaves this control through tabbing
303 // or deactivating the editor, the toolbar gets the focus (and
304 // possibly loses it right away again). Thus the focus tracker
305 // will always see the toolbar as the last focused control.
306 // Unfortunately there is no other way to get some text onto
307 // the first line of a FormHeading.
308 headerFocusTracker
.addToFocusTracking(link
);
310 link
.setFont(JFaceResources
.getBannerFont());
311 link
.setForeground(toolkit
.getColors().getColor(
313 link
.setToolTipText(UIText
.CommitEditor_showGitRepo
);
314 link
.addHyperlinkListener(new HyperlinkAdapter() {
316 public void linkActivated(HyperlinkEvent event
) {
317 RepositoriesView view
;
319 view
= (RepositoriesView
) PlatformUI
.getWorkbench()
320 .getActiveWorkbenchWindow().getActivePage()
321 .showView(RepositoriesView
.VIEW_ID
);
322 view
.showRepository(getCommit().getRepository());
323 } catch (PartInitException e
) {
324 Activator
.handleError(
325 UIText
.CommitEditor_couldNotShowRepository
,
334 toolbar
.add(repositoryLabelControl
);
335 visibilityListener
= new EditorVisibilityTracker(this) {
337 private boolean isActive
;
340 public void partActivated(IWorkbenchPartReference partRef
) {
346 } else if (isActive
) {
353 getSite().getService(IPartService
.class)
354 .addPartListener(visibilityListener
);
355 if (commit
.isStash()) {
356 toolbar
.add(createActionContributionItem(ActionCommands
.STASH_APPLY
,
357 UIText
.CommitEditor_toolbarApplyStash
,
358 UIIcons
.STASH_APPLY
));
359 toolbar
.add(createActionContributionItem(ActionCommands
.STASH_DROP
,
360 UIText
.CommitEditor_toolbarDeleteStash
,
361 PlatformUI
.getWorkbench().getSharedImages()
363 ISharedImages
.IMG_TOOL_DELETE
)));
365 toolbar
.add(createActionContributionItem(ActionCommands
.TAG_ACTION
,
366 UIText
.CommitEditor_toolbarCreateTag
, UIIcons
.CREATE_TAG
));
368 createActionContributionItem(ActionCommands
.BRANCH_CREATE
,
369 UIText
.CommitEditor_toolbarCreateBranch
,
370 UIIcons
.CREATE_BRANCH
));
371 toolbar
.add(createActionContributionItem(ActionCommands
.CHECK_OUT
,
372 UIText
.CommitEditor_toolbarCheckOut
, UIIcons
.CHECKOUT
));
373 toolbar
.add(createActionContributionItem(ActionCommands
.CHERRY_PICK
,
374 UIText
.CommitEditor_toolbarCherryPick
,
375 UIIcons
.CHERRY_PICK
));
376 toolbar
.add(createActionContributionItem(ActionCommands
.REVERT
,
377 UIText
.CommitEditor_toolbarRevert
, UIIcons
.REVERT
));
378 toolbar
.add(createActionContributionItem(
379 ActionCommands
.SHOW_IN_HISTORY
,
380 UIText
.CommitEditor_toolbarShowInHistory
, UIIcons
.HISTORY
));
382 addContributions(toolbar
);
383 toolbar
.update(true);
384 getSite().setSelectionProvider(new ISelectionProvider() {
387 public void setSelection(ISelection selection
) {
392 public void removeSelectionChangedListener(
393 ISelectionChangedListener listener
) {
398 public ISelection
getSelection() {
399 return new StructuredSelection(getCommit());
403 public void addSelectionChangedListener(
404 ISelectionChangedListener listener
) {
408 if (toolbar
instanceof ToolBarManager
) {
409 Control control
= ((ToolBarManager
) toolbar
).getControl();
410 if (control
!= null) {
411 headerFocusTracker
.addToFocusTracking(control
);
416 private void updateToolbar() {
417 if (toolbar
!= null) {
418 // isEnabled() on a CommandContributionItem actually re-evaluates
420 for (IContributionItem item
: toolbar
.getItems()) {
423 toolbar
.update(true);
427 private String
getFormattedHeaderTitle(String commitName
) {
428 if (getCommit().isStash()) {
429 int stashIndex
= getStashIndex(getCommit().getRepository(),
430 getCommit().getRevCommit().getId());
431 String stashName
= MessageFormat
.format("stash@'{'{0}'}'", //$NON-NLS-1$
432 Integer
.valueOf(stashIndex
));
433 return MessageFormat
.format(
434 UIText
.CommitEditor_TitleHeaderStashedCommit
,
437 return MessageFormat
.format(UIText
.CommitEditor_TitleHeaderCommit
,
442 private int getStashIndex(Repository repo
, ObjectId id
) {
445 for (RevCommit commit
: new StashListCommand(repo
).call()) {
446 if (commit
.getId().equals(id
)) {
451 throw new IllegalStateException(
452 UIText
.CommitEditor_couldNotFindStashCommit
);
453 } catch (Exception e
) {
454 String message
= MessageFormat
.format(
455 UIText
.CommitEditor_couldNotGetStashIndex
, id
.name());
456 Activator
.logError(message
, e
);
462 private void addContributions(IToolBarManager toolBarManager
) {
463 IMenuService menuService
= getSite().getService(IMenuService
.class);
464 if (menuService
!= null
465 && toolBarManager
instanceof ContributionManager
) {
466 ContributionManager contributionManager
= (ContributionManager
) toolBarManager
;
467 String toolbarUri
= "toolbar:" + TOOLBAR_HEADER_ID
; //$NON-NLS-1$
468 menuService
.populateContributionManager(contributionManager
,
473 private RepositoryCommit
getCommit() {
474 return getAdapter(RepositoryCommit
.class);
478 * @see org.eclipse.ui.part.MultiPageEditorPart#getAdapter(java.lang.Class)
481 public <T
> T
getAdapter(Class
<T
> adapter
) {
482 if (RepositoryCommit
.class == adapter
) {
483 return Adapters
.adapt(getEditorInput(), adapter
);
484 } else if (IContentOutlinePage
.class == adapter
) {
485 return adapter
.cast(getOutlinePage());
486 } else if (IPropertySheetPage
.class == adapter
) {
487 PropertySheetPage page
= new GitPropertySheetPage();
488 page
.setPropertySourceProvider(object
-> {
489 if (object
instanceof IPropertySource
) {
490 return (IPropertySource
) object
;
492 if (object
instanceof IRepositoryCommit
) {
493 return new CommitPropertySource(
494 ((IRepositoryCommit
) object
).getRevCommit(), page
);
498 return adapter
.cast(page
);
500 return super.getAdapter(adapter
);
504 * @see org.eclipse.ui.forms.editor.FormEditor#init(org.eclipse.ui.IEditorSite,
505 * org.eclipse.ui.IEditorInput)
508 public void init(IEditorSite site
, IEditorInput input
)
509 throws PartInitException
{
510 if (Adapters
.adapt(input
, RepositoryCommit
.class) == null)
511 throw new PartInitException(
512 "Input could not be adapted to commit object"); //$NON-NLS-1$
513 super.init(site
, input
);
514 setPartName(input
.getName());
515 setTitleToolTip(input
.getToolTipText());
519 public void dispose() {
520 refListenerHandle
.remove();
521 if (pageListener
!= null) {
522 removePageChangedListener(pageListener
);
525 if (visibilityListener
!= null) {
526 getSite().getService(IPartService
.class)
527 .removePartListener(visibilityListener
);
528 visibilityListener
= null;
530 headerFocusTracker
.dispose();
535 * @see org.eclipse.ui.part.EditorPart#doSave(org.eclipse.core.runtime.IProgressMonitor)
538 public void doSave(IProgressMonitor monitor
) {
539 // Save not supported
543 * @see org.eclipse.ui.part.EditorPart#doSaveAs()
546 public void doSaveAs() {
547 IEditorPart editor
= getActiveEditor();
548 if (editor
!= null && editor
.isSaveAsAllowed()) {
554 * @see org.eclipse.ui.part.EditorPart#isSaveAsAllowed()
557 public boolean isSaveAsAllowed() {
558 IEditorPart editor
= getActiveEditor();
559 return editor
!= null && editor
.isSaveAsAllowed();
563 public void onRefsChanged(RefsChangedEvent event
) {
564 if (getCommit().getRepository().getDirectory()
565 .equals(event
.getRepository().getDirectory())) {
566 visibilityListener
.runWhenVisible(() -> {
567 runRefresh
.set(true);
568 if (getActivePageInstance() == commitPage
) {
571 // Otherwise the pageListener will run the refresh when the
572 // commitPage becomes active
577 private void refreshCommitPage() {
578 if (!runRefresh
.getAndSet(false) || getContainer().isDisposed()) {
581 UIJob job
= new UIJob("Refreshing editor") { //$NON-NLS-1$
584 public IStatus
runInUIThread(IProgressMonitor monitor
) {
586 if (!getContainer().isDisposed()) {
587 commitPage
.refresh();
590 if (monitor
!= null) {
594 return Status
.OK_STATUS
;
600 private IContentOutlinePage
getOutlinePage() {
601 if (outlinePage
== null) {
602 outlinePage
= new MultiPageEditorContentOutlinePage(this);
608 public ShowInContext
getShowInContext() {
609 IFormPage currentPage
= getActivePageInstance();
610 IShowInSource showInSource
= Adapters
.adapt(currentPage
,
611 IShowInSource
.class);
612 if (showInSource
!= null) {
613 return showInSource
.getShowInContext();
619 public String
[] getShowInTargetIds() {
620 IFormPage currentPage
= getActivePageInstance();
621 IShowInTargetList targetList
= Adapters
.adapt(currentPage
,
622 IShowInTargetList
.class);
623 if (targetList
!= null) {
624 return targetList
.getShowInTargetIds();
630 public void setFocus() {
631 // super class sets focus to form header
632 IFormPage currentPage
= getActivePageInstance();
633 if (currentPage
!= null) {
634 currentPage
.setFocus();