1 /*******************************************************************************
2 * Copyright (c) 2010, 2017 SAP AG and others.
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License 2.0
6 * which accompanies this distribution, and is available at
7 * https://www.eclipse.org/legal/epl-2.0/
9 * SPDX-License-Identifier: EPL-2.0
12 * Dariusz Luksza - add getFileCachedRevisionTypedElement(String, Repository)
13 * Stefan Lay (SAP AG) - initial implementation
14 * Yann Simon <yann.simon.fr@gmail.com> - implementation of getHeadTypedElement
15 * Robin Stocker <robin@nibor.org>
16 * Laurent Goubet <laurent.goubet@obeo.fr>
17 * Gunnar Wagenknecht <gunnar@wagenknecht.org>
18 * Thomas Wolf <thomas.wolf@paranor.ch> - git attributes
19 *******************************************************************************/
20 package org
.eclipse
.egit
.ui
.internal
;
22 import java
.io
.ByteArrayInputStream
;
24 import java
.io
.IOException
;
25 import java
.io
.InputStream
;
26 import java
.nio
.ByteBuffer
;
27 import java
.nio
.file
.Files
;
29 import org
.eclipse
.compare
.CompareEditorInput
;
30 import org
.eclipse
.compare
.CompareUI
;
31 import org
.eclipse
.compare
.IContentChangeListener
;
32 import org
.eclipse
.compare
.IContentChangeNotifier
;
33 import org
.eclipse
.compare
.ITypedElement
;
34 import org
.eclipse
.core
.resources
.IFile
;
35 import org
.eclipse
.core
.resources
.IFolder
;
36 import org
.eclipse
.core
.resources
.IResource
;
37 import org
.eclipse
.core
.resources
.ResourcesPlugin
;
38 import org
.eclipse
.core
.runtime
.CoreException
;
39 import org
.eclipse
.core
.runtime
.IPath
;
40 import org
.eclipse
.core
.runtime
.IProgressMonitor
;
41 import org
.eclipse
.core
.runtime
.IStatus
;
42 import org
.eclipse
.core
.runtime
.Path
;
43 import org
.eclipse
.core
.runtime
.Status
;
44 import org
.eclipse
.core
.runtime
.jobs
.Job
;
45 import org
.eclipse
.core
.runtime
.preferences
.DefaultScope
;
46 import org
.eclipse
.core
.runtime
.preferences
.IEclipsePreferences
;
47 import org
.eclipse
.core
.runtime
.preferences
.IEclipsePreferences
.IPreferenceChangeListener
;
48 import org
.eclipse
.core
.runtime
.preferences
.IEclipsePreferences
.PreferenceChangeEvent
;
49 import org
.eclipse
.core
.runtime
.preferences
.InstanceScope
;
50 import org
.eclipse
.egit
.core
.RevUtils
;
51 import org
.eclipse
.egit
.core
.attributes
.Filtering
;
52 import org
.eclipse
.egit
.core
.internal
.CompareCoreUtils
;
53 import org
.eclipse
.egit
.core
.internal
.storage
.GitFileRevision
;
54 import org
.eclipse
.egit
.core
.internal
.storage
.WorkingTreeFileRevision
;
55 import org
.eclipse
.egit
.core
.internal
.storage
.WorkspaceFileRevision
;
56 import org
.eclipse
.egit
.core
.project
.RepositoryMapping
;
57 import org
.eclipse
.egit
.ui
.Activator
;
58 import org
.eclipse
.egit
.ui
.UIPreferences
;
59 import org
.eclipse
.egit
.ui
.internal
.merge
.GitCompareEditorInput
;
60 import org
.eclipse
.egit
.ui
.internal
.revision
.EditableRevision
;
61 import org
.eclipse
.egit
.ui
.internal
.revision
.FileRevisionTypedElement
;
62 import org
.eclipse
.egit
.ui
.internal
.revision
.GitCompareFileRevisionEditorInput
;
63 import org
.eclipse
.egit
.ui
.internal
.revision
.GitCompareFileRevisionEditorInput
.EmptyTypedElement
;
64 import org
.eclipse
.egit
.ui
.internal
.synchronize
.DefaultGitSynchronizer
;
65 import org
.eclipse
.egit
.ui
.internal
.synchronize
.GitSynchronizer
;
66 import org
.eclipse
.egit
.ui
.internal
.synchronize
.ModelAwareGitSynchronizer
;
67 import org
.eclipse
.egit
.ui
.internal
.synchronize
.compare
.LocalNonWorkspaceTypedElement
;
68 import org
.eclipse
.jface
.action
.Action
;
69 import org
.eclipse
.jface
.util
.OpenStrategy
;
70 import org
.eclipse
.jgit
.annotations
.NonNull
;
71 import org
.eclipse
.jgit
.dircache
.DirCache
;
72 import org
.eclipse
.jgit
.dircache
.DirCacheCheckout
.CheckoutMetadata
;
73 import org
.eclipse
.jgit
.dircache
.DirCacheEditor
;
74 import org
.eclipse
.jgit
.dircache
.DirCacheEntry
;
75 import org
.eclipse
.jgit
.dircache
.DirCacheIterator
;
76 import org
.eclipse
.jgit
.lib
.Constants
;
77 import org
.eclipse
.jgit
.lib
.CoreConfig
.AutoCRLF
;
78 import org
.eclipse
.jgit
.lib
.CoreConfig
.EolStreamType
;
79 import org
.eclipse
.jgit
.lib
.FileMode
;
80 import org
.eclipse
.jgit
.lib
.ObjectId
;
81 import org
.eclipse
.jgit
.lib
.ObjectInserter
;
82 import org
.eclipse
.jgit
.lib
.Ref
;
83 import org
.eclipse
.jgit
.lib
.Repository
;
84 import org
.eclipse
.jgit
.revwalk
.RevCommit
;
85 import org
.eclipse
.jgit
.revwalk
.RevWalk
;
86 import org
.eclipse
.jgit
.treewalk
.FileTreeIterator
;
87 import org
.eclipse
.jgit
.treewalk
.TreeWalk
;
88 import org
.eclipse
.jgit
.treewalk
.TreeWalk
.OperationType
;
89 import org
.eclipse
.jgit
.treewalk
.WorkingTreeOptions
;
90 import org
.eclipse
.jgit
.treewalk
.filter
.AndTreeFilter
;
91 import org
.eclipse
.jgit
.treewalk
.filter
.NotIgnoredFilter
;
92 import org
.eclipse
.jgit
.treewalk
.filter
.PathFilterGroup
;
93 import org
.eclipse
.jgit
.treewalk
.filter
.TreeFilter
;
94 import org
.eclipse
.jgit
.util
.IO
;
95 import org
.eclipse
.jgit
.util
.io
.EolStreamTypeUtil
;
96 import org
.eclipse
.osgi
.util
.NLS
;
97 import org
.eclipse
.swt
.widgets
.Display
;
98 import org
.eclipse
.team
.core
.history
.IFileRevision
;
99 import org
.eclipse
.team
.ui
.synchronize
.SaveableCompareEditorInput
;
100 import org
.eclipse
.ui
.IEditorInput
;
101 import org
.eclipse
.ui
.IEditorPart
;
102 import org
.eclipse
.ui
.IEditorReference
;
103 import org
.eclipse
.ui
.IReusableEditor
;
104 import org
.eclipse
.ui
.IWorkbenchPage
;
105 import org
.eclipse
.ui
.PlatformUI
;
106 import org
.eclipse
.ui
.actions
.ActionFactory
.IWorkbenchAction
;
109 * A collection of helper methods useful for comparing content
111 public class CompareUtils
{
113 * A copy of the non-accessible preference constant
114 * IPreferenceIds.REUSE_OPEN_COMPARE_EDITOR from the team ui plug in
116 private static final String REUSE_COMPARE_EDITOR_PREFID
= "org.eclipse.team.ui.reuse_open_compare_editors"; //$NON-NLS-1$
118 /** The team ui plugin ID which is not accessible */
119 private static final String TEAM_UI_PLUGIN
= "org.eclipse.team.ui"; //$NON-NLS-1$
124 * path within the commit's tree of the file.
126 * the commit the blob was identified to be within.
128 * the repository this commit was loaded out of.
129 * @return an instance of {@link ITypedElement} which can be used in
130 * {@link CompareEditorInput}
132 public static ITypedElement
getFileRevisionTypedElement(
133 final String gitPath
, final RevCommit commit
, final Repository db
) {
134 return getFileRevisionTypedElement(gitPath
, commit
, db
, null);
139 * path within the commit's tree of the file.
141 * the commit the blob was identified to be within.
143 * the repository this commit was loaded out of, and that this
144 * file's blob should also be reachable through.
146 * unique name of the content.
147 * @return an instance of {@link ITypedElement} which can be used in
148 * {@link CompareEditorInput}
150 public static ITypedElement
getFileRevisionTypedElement(
151 final String gitPath
, final RevCommit commit
, final Repository db
,
153 ITypedElement right
= new GitCompareFileRevisionEditorInput
.EmptyTypedElement(
154 NLS
.bind(UIText
.GitHistoryPage_FileNotInCommit
,
155 getName(gitPath
), truncatedRevision(commit
.name())));
158 IFileRevision nextFile
= getFileRevision(gitPath
, commit
, db
,
160 if (nextFile
!= null) {
161 String encoding
= CompareCoreUtils
.getResourceEncoding(db
,
163 right
= new FileRevisionTypedElement(nextFile
, encoding
);
165 } catch (IOException e
) {
166 Activator
.error(NLS
.bind(UIText
.GitHistoryPage_errorLookingUpPath
,
167 gitPath
, commit
.getId()), e
);
172 private static String
getName(String gitPath
) {
173 final int last
= gitPath
.lastIndexOf('/');
174 return last
>= 0 ? gitPath
.substring(last
+ 1) : gitPath
;
180 * path within the commit's tree of the file.
182 * the commit the blob was identified to be within.
184 * the repository this commit was loaded out of, and that this
185 * file's blob should also be reachable through.
187 * unique name of the content.
188 * @return an instance of {@link IFileRevision} or null if the file is not
189 * contained in {@code commit}
190 * @throws IOException
192 public static IFileRevision
getFileRevision(String gitPath
,
193 RevCommit commit
, Repository db
, ObjectId blobId
)
195 try (TreeWalk w
= TreeWalk
.forPath(db
, gitPath
, commit
.getTree())) {
200 CheckoutMetadata metadata
= new CheckoutMetadata(
201 w
.getEolStreamType(OperationType
.CHECKOUT_OP
),
202 w
.getFilterCommand(Constants
.ATTR_FILTER_TYPE_SMUDGE
));
203 if (blobId
== null) {
204 blobId
= w
.getObjectId(0);
206 return GitFileRevision
.inCommit(db
, commit
, gitPath
, blobId
,
212 * Creates a {@link ITypedElement} for the commit which is the common
213 * ancestor of the provided commits. Returns null if no such commit exists
214 * or if {@code gitPath} is not contained in the common ancestor or if the
215 * common ancestor is equal to one of the given commits
218 * path within the ancestor commit's tree of the file.
222 * the repository this commit was loaded out of.
223 * @return an instance of {@link ITypedElement} which can be used in
224 * {@link CompareEditorInput}
226 public static ITypedElement
getFileRevisionTypedElementForCommonAncestor(
227 final String gitPath
, ObjectId commit1
, ObjectId commit2
,
229 ITypedElement ancestor
= null;
230 RevCommit commonAncestor
= null;
232 commonAncestor
= RevUtils
.getCommonAncestor(db
, commit1
, commit2
);
233 } catch (IOException e
) {
234 Activator
.logError(NLS
.bind(UIText
.CompareUtils_errorCommonAncestor
,
235 commit1
.getName(), commit2
.getName()), e
);
237 if (commonAncestor
!= null) {
238 if (commit1
.equals(commonAncestor
.getId())
239 || commit2
.equals(commonAncestor
.getId())) {
240 // Don't use 3 way compare if the common ancestor is same as one
241 // of given commits, see bug 512395
245 ITypedElement ancestorCandidate
= CompareUtils
246 .getFileRevisionTypedElement(gitPath
, commonAncestor
, db
);
247 if (!(ancestorCandidate
instanceof EmptyTypedElement
))
248 ancestor
= ancestorCandidate
;
255 * @return a truncated revision identifier if it is long
257 public static String
truncatedRevision(String ci
) {
258 if (ObjectId
.isId(ci
))
259 return ci
.substring(0, 7);
265 * Compares two files between the given commits, taking possible renames
269 * the "left" commit for the comparison editor
271 * the "right" commit for the comparison editor
273 * path to the file within commit1's tree
275 * path to the file within commit2's tree
277 * the repository this commit was loaded out of
278 * @param workBenchPage
279 * the page to open the compare editor in
281 public static void openInCompare(RevCommit commit1
, RevCommit commit2
,
282 String commit1Path
, String commit2Path
, Repository repository
,
283 IWorkbenchPage workBenchPage
) {
284 final ITypedElement base
= CompareUtils
.getFileRevisionTypedElement(
285 commit1Path
, commit1
, repository
);
286 final ITypedElement next
= CompareUtils
.getFileRevisionTypedElement(
287 commit2Path
, commit2
, repository
);
288 CompareEditorInput in
= new GitCompareFileRevisionEditorInput(base
,
290 CompareUtils
.openInCompare(workBenchPage
, in
);
294 * @param workBenchPage
297 public static void openInCompare(IWorkbenchPage workBenchPage
,
298 CompareEditorInput input
) {
299 IEditorPart editor
= findReusableCompareEditor(input
, workBenchPage
);
300 if (editor
!= null) {
301 IEditorInput otherInput
= editor
.getEditorInput();
302 if (otherInput
.equals(input
)) {
303 // simply provide focus to editor
304 if (OpenStrategy
.activateOnOpen())
305 workBenchPage
.activate(editor
);
307 workBenchPage
.bringToTop(editor
);
309 // if editor is currently not open on that input either re-use
311 CompareUI
.reuseCompareEditor(input
, (IReusableEditor
) editor
);
312 if (OpenStrategy
.activateOnOpen())
313 workBenchPage
.activate(editor
);
315 workBenchPage
.bringToTop(editor
);
318 CompareUI
.openCompareEditor(input
);
322 private static IEditorPart
findReusableCompareEditor(
323 CompareEditorInput input
, IWorkbenchPage page
) {
324 IEditorReference
[] editorRefs
= page
.getEditorReferences();
325 // first loop looking for an editor with the same input
326 for (int i
= 0; i
< editorRefs
.length
; i
++) {
327 IEditorPart part
= editorRefs
[i
].getEditor(false);
329 && (part
.getEditorInput() instanceof GitCompareFileRevisionEditorInput
|| part
.getEditorInput() instanceof GitCompareEditorInput
)
330 && part
instanceof IReusableEditor
331 && part
.getEditorInput().equals(input
)) {
335 // if none found and "Reuse open compare editors" preference is on use
336 // a non-dirty editor
337 if (isReuseOpenEditor()) {
338 for (int i
= 0; i
< editorRefs
.length
; i
++) {
339 IEditorPart part
= editorRefs
[i
].getEditor(false);
341 && (part
.getEditorInput() instanceof SaveableCompareEditorInput
)
342 && part
instanceof IReusableEditor
&& !part
.isDirty()) {
347 // no re-usable editor found
352 * Action to toggle the team 'reuse compare editor' preference
354 public static class ReuseCompareEditorAction
extends Action
implements
355 IPreferenceChangeListener
, IWorkbenchAction
{
356 IEclipsePreferences node
= InstanceScope
.INSTANCE
.getNode(TEAM_UI_PLUGIN
);
359 * Default constructor
361 public ReuseCompareEditorAction() {
362 node
.addPreferenceChangeListener(this);
363 setText(UIText
.GitHistoryPage_ReuseCompareEditorMenuLabel
);
364 setChecked(CompareUtils
.isReuseOpenEditor());
369 CompareUtils
.setReuseOpenEditor(isChecked());
373 public void dispose() {
375 node
.removePreferenceChangeListener(this);
379 public void preferenceChange(PreferenceChangeEvent event
) {
380 setChecked(isReuseOpenEditor());
385 private static boolean isReuseOpenEditor() {
386 boolean defaultReuse
= DefaultScope
.INSTANCE
.getNode(TEAM_UI_PLUGIN
)
387 .getBoolean(REUSE_COMPARE_EDITOR_PREFID
, false);
388 return InstanceScope
.INSTANCE
.getNode(TEAM_UI_PLUGIN
).getBoolean(
389 REUSE_COMPARE_EDITOR_PREFID
, defaultReuse
);
392 private static void setReuseOpenEditor(boolean value
) {
393 InstanceScope
.INSTANCE
.getNode(TEAM_UI_PLUGIN
).putBoolean(
394 REUSE_COMPARE_EDITOR_PREFID
, value
);
398 * Opens a compare editor. The workspace version of the given file is
399 * compared with the version in the HEAD commit.
404 public static void compareHeadWithWorkspace(Repository repository
,
405 @NonNull IFile file
) {
406 RepositoryMapping mapping
= RepositoryMapping
.getMapping(file
);
407 if (mapping
== null) {
408 Activator
.error(NLS
.bind(UIText
.GitHistoryPage_errorLookingUpPath
,
409 file
.getLocation(), repository
), null);
412 String path
= mapping
.getRepoRelativePath(
414 ITypedElement base
= getHeadTypedElement(repository
, path
);
418 IFileRevision nextFile
= new WorkspaceFileRevision(file
);
419 String encoding
= null;
421 encoding
= file
.getCharset();
422 } catch (CoreException e
) {
423 Activator
.handleError(UIText
.CompareUtils_errorGettingEncoding
, e
, true);
425 ITypedElement next
= new FileRevisionTypedElement(nextFile
, encoding
);
426 GitCompareFileRevisionEditorInput input
= new GitCompareFileRevisionEditorInput(
428 CompareUI
.openCompareDialog(input
);
432 * Opens a compare editor comparing the working directory version of the
433 * given file or link with the version of that file corresponding to
437 * The repository to load file revisions from.
439 * Resource to compare revisions for. Must be either
440 * {@link IFile} or a symbolic link to directory ({@link IFolder}).
442 * Reference to compare with the workspace version of
443 * {@code file}. Can be either a commit ID, a reference or a
446 * If not {@null} try to re-use a compare editor on this page if
447 * any is available. Otherwise open a new one.
449 public static void compareWorkspaceWithRef(@NonNull final Repository repository
,
450 final IResource file
, final String refName
, final IWorkbenchPage page
) {
454 final IPath location
= file
.getLocation();
455 if(location
== null){
459 Job job
= new Job(UIText
.CompareUtils_jobName
) {
462 public IStatus
run(IProgressMonitor monitor
) {
463 if (monitor
.isCanceled()) {
464 return Status
.CANCEL_STATUS
;
466 final RepositoryMapping mapping
= RepositoryMapping
468 if (mapping
== null) {
469 return Activator
.createErrorStatus(
470 NLS
.bind(UIText
.GitHistoryPage_errorLookingUpPath
,
471 location
, repository
));
474 final ITypedElement base
;
475 if (Files
.isSymbolicLink(location
.toFile().toPath())) {
476 base
= new LocalNonWorkspaceTypedElement(repository
,
478 } else if (file
instanceof IFile
) {
479 base
= SaveableCompareEditorInput
480 .createFileElement((IFile
) file
);
482 return Activator
.createErrorStatus(
483 NLS
.bind(UIText
.CompareUtils_wrongResourceArgument
,
487 final String gitPath
= mapping
.getRepoRelativePath(file
);
488 CompareEditorInput in
;
490 in
= prepareCompareInput(repository
, gitPath
, base
, refName
);
491 } catch (IOException e
) {
492 return Activator
.createErrorStatus(
493 UIText
.CompareWithRefAction_errorOnSynchronize
, e
);
496 if (monitor
.isCanceled()) {
497 return Status
.CANCEL_STATUS
;
499 openCompareEditorRunnable(page
, in
);
500 return Status
.OK_STATUS
;
508 * Opens compare editor in UI thread. Safe to start from background threads
509 * too - in this case the operation will be started asynchronously in UI
517 private static void openCompareEditorRunnable(
518 final IWorkbenchPage page
,
519 final CompareEditorInput in
) {
520 // safety check: make sure we open compare editor from UI thread
521 if (Display
.getCurrent() == null) {
522 PlatformUI
.getWorkbench().getDisplay().asyncExec(new Runnable() {
525 openCompareEditorRunnable(page
, in
);
532 openInCompare(page
, in
);
534 CompareUI
.openCompareEditor(in
);
539 * Opens a compare editor comparing the working directory version of the
540 * file at the given location with the version corresponding to
541 * {@code refName} of the same file.
544 * The repository to load file revisions from.
546 * Location of the file to compare revisions for.
548 * Reference to compare with the workspace version of
549 * {@code file}. Can be either a commit ID, a reference or a
552 * If not {@null} try to re-use a compare editor on this
553 * page if any is available. Otherwise open a new one.
555 private static void compareLocalWithRef(@NonNull final Repository repository
,
556 @NonNull final IPath location
, final String refName
,
557 final IWorkbenchPage page
) {
559 Job job
= new Job(UIText
.CompareUtils_jobName
) {
562 public IStatus
run(IProgressMonitor monitor
) {
563 if (monitor
.isCanceled()) {
564 return Status
.CANCEL_STATUS
;
566 final String gitPath
= getRepoRelativePath(location
, repository
);
567 final ITypedElement base
= new LocalNonWorkspaceTypedElement(
568 repository
, location
);
570 CompareEditorInput in
;
572 in
= prepareCompareInput(repository
, gitPath
, base
, refName
);
573 } catch (IOException e
) {
574 return Activator
.createErrorStatus(
575 UIText
.CompareWithRefAction_errorOnSynchronize
, e
);
578 if (monitor
.isCanceled()) {
579 return Status
.CANCEL_STATUS
;
581 openCompareEditorRunnable(page
, in
);
582 return Status
.OK_STATUS
;
590 * Creates a compare input that can be used to compare a given local file
591 * with another reference. The given "base" element should always reflect a
592 * local file, either in the workspace (IFile) or on the file system
593 * (java.io.File) since we'll use "HEAD" to find a common ancestor of this
594 * base and the reference we compare it with.
596 private static CompareEditorInput
prepareCompareInput(
597 Repository repository
, String gitPath
, ITypedElement base
,
598 String refName
) throws IOException
{
599 final ITypedElement destCommit
;
600 ITypedElement commonAncestor
= null;
602 if (GitFileRevision
.INDEX
.equals(refName
))
603 destCommit
= getIndexTypedElement(repository
, gitPath
);
604 else if (Constants
.HEAD
.equals(refName
))
605 destCommit
= getHeadTypedElement(repository
, gitPath
);
607 final ObjectId destCommitId
= repository
.resolve(refName
);
608 try (RevWalk rw
= new RevWalk(repository
)) {
609 RevCommit commit
= rw
.parseCommit(destCommitId
);
610 destCommit
= getFileRevisionTypedElement(gitPath
, commit
,
614 final ObjectId headCommitId
= repository
615 .resolve(Constants
.HEAD
);
616 commonAncestor
= getFileRevisionTypedElementForCommonAncestor(
617 gitPath
, headCommitId
, destCommitId
, repository
);
623 final GitCompareFileRevisionEditorInput in
= new GitCompareFileRevisionEditorInput(
624 base
, destCommit
, commonAncestor
, null);
625 in
.getCompareConfiguration().setRightLabel(refName
);
630 * This can be used to compare a given set of resources between two
631 * revisions. If only one resource is to be compared, and that resource is
632 * not part of a larger logical model, we'll open a comparison editor for
633 * that file alone. Otherwise, we'll launch a synchronization restrained of
634 * the given resources set.
636 * This can also be used to synchronize the whole repository if
637 * <code>resources</code> is empty.
640 * Note that this can be used to compare with the index by using
641 * {@link GitFileRevision#INDEX} as either one of the two revs.
645 * The set of resources to compare. Can be empty (in which case
646 * we'll synchronize the whole repository).
648 * The repository to load file revisions from.
650 * Left revision of the comparison (usually the local or "new"
651 * revision). Won't be used if <code>includeLocal</code> is
654 * Right revision of the comparison (usually the "old" revision).
655 * @param includeLocal
656 * If <code>true</code>, this will use the local data as the
657 * "left" side of the comparison.
659 * If not {@code null} try to re-use a compare editor on this
660 * page if any is available. Otherwise open a new one.
661 * @throws IOException
663 public static void compare(@NonNull IResource
[] resources
,
664 @NonNull Repository repository
,
665 String leftRev
, String rightRev
, boolean includeLocal
,
666 IWorkbenchPage page
) throws IOException
{
667 getSynchronizer().compare(resources
, repository
, leftRev
,
668 rightRev
, includeLocal
, page
);
672 * This can be used to compare a given set of resources between two
673 * revisions. If only one resource is to be compared, and that resource is
674 * not part of a larger logical model, we'll open a comparison editor for
675 * that file alone, also taking leftPath and rightPath into account.
676 * Otherwise, we'll launch a synchronization restrained of the given
679 * This can also be used to synchronize the whole repository if
680 * <code>resources</code> is empty.
683 * Note that this can be used to compare with the index by using
684 * {@link GitFileRevision#INDEX} as either one of the two revs.
688 * The file to compare.
690 * The repository to load file revisions from.
692 * The repository relative path to be used for the left revision,
693 * when comparing directly.
695 * The repository relative path to be used for the right
696 * revision, when comparing directly.
698 * Left revision of the comparison (usually the local or "new"
699 * revision). Won't be used if <code>includeLocal</code> is
702 * Right revision of the comparison (usually the "old" revision).
703 * @param includeLocal
704 * If <code>true</code>, this will use the local data as the
705 * "left" side of the comparison.
707 * If not {@code null} try to re-use a compare editor on this
708 * page if any is available. Otherwise open a new one.
709 * @throws IOException
711 public static void compare(@NonNull IFile file
,
712 @NonNull Repository repository
,
713 String leftPath
, String rightPath
, String leftRev
, String rightRev
,
714 boolean includeLocal
, IWorkbenchPage page
) throws IOException
{
715 getSynchronizer().compare(file
, repository
, leftPath
,
717 leftRev
, rightRev
, includeLocal
, page
);
720 private static GitSynchronizer
getSynchronizer() {
721 if (Activator
.getDefault().getPreferenceStore()
722 .getBoolean(UIPreferences
.USE_LOGICAL_MODEL
)) {
723 return new ModelAwareGitSynchronizer();
725 return new DefaultGitSynchronizer();
729 * This can be used to compare a given file between two revisions.
732 * Location of the file to compare.
734 * The repository to load file revisions from.
736 * Left revision of the comparison (usually the local or "new"
737 * revision). Won't be used if <code>includeLocal</code> is
740 * Right revision of the comparison (usually the "old" revision).
741 * @param includeLocal
742 * If <code>true</code>, this will use the local data as the
743 * "left" side of the comparison.
745 * If not {@null} try to re-use a compare editor on this
746 * page if any is available. Otherwise open a new one.
748 public static void compare(@NonNull IPath location
,
749 @NonNull Repository repository
, String leftRev
, String rightRev
,
750 boolean includeLocal
, IWorkbenchPage page
) {
752 compareLocalWithRef(repository
, location
, rightRev
, page
);
754 String gitPath
= getRepoRelativePath(location
, repository
);
755 compareBetween(repository
, gitPath
, leftRev
, rightRev
, page
);
760 * Compares two explicit files specified by leftGitPath and rightGitPath
761 * between the two revisions leftRev and rightRev.
764 * The repository to load file revisions from.
766 * The repository relative path to be used for the left & right
769 * Left revision of the comparison (usually the local or "new"
770 * revision). Won't be used if <code>includeLocal</code> is
773 * Right revision of the comparison (usually the "old" revision).
775 * If not {@null} try to re-use a compare editor on this page if
776 * any is available. Otherwise open a new one.
778 public static void compareBetween(Repository repository
, String gitPath
,
779 String leftRev
, String rightRev
, IWorkbenchPage page
) {
780 compareBetween(repository
, gitPath
, gitPath
, leftRev
, rightRev
, page
);
784 * Compares two explicit files specified by leftGitPath and rightGitPath
785 * between the two revisions leftRev and rightRev.
788 * The repository to load file revisions from.
790 * The repository relative path to be used for the left revision.
791 * @param rightGitPath
792 * The repository relative path to be used for the right
795 * Left revision of the comparison (usually the local or "new"
796 * revision). Won't be used if <code>includeLocal</code> is
799 * Right revision of the comparison (usually the "old" revision).
801 * If not {@null} try to re-use a compare editor on this
802 * page if any is available. Otherwise open a new one.
804 public static void compareBetween(final Repository repository
,
805 final String leftGitPath
, final String rightGitPath
,
806 final String leftRev
, final String rightRev
,
807 final IWorkbenchPage page
) {
809 Job job
= new Job(UIText
.CompareUtils_jobName
) {
812 public IStatus
run(IProgressMonitor monitor
) {
813 if (monitor
.isCanceled()) {
814 return Status
.CANCEL_STATUS
;
816 final ITypedElement left
;
817 final ITypedElement right
;
819 left
= getTypedElementFor(repository
, leftGitPath
, leftRev
);
820 right
= getTypedElementFor(repository
, rightGitPath
,
822 } catch (IOException e
) {
823 return Activator
.createErrorStatus(
824 UIText
.CompareWithRefAction_errorOnSynchronize
, e
);
826 final ITypedElement commonAncestor
;
827 if (left
!= null && right
!= null
828 && !GitFileRevision
.INDEX
.equals(leftRev
)
829 && !GitFileRevision
.INDEX
.equals(rightRev
)) {
830 commonAncestor
= getTypedElementForCommonAncestor(
831 repository
, rightGitPath
, leftRev
, rightRev
);
833 commonAncestor
= null;
836 final GitCompareFileRevisionEditorInput in
= new GitCompareFileRevisionEditorInput(
837 left
, right
, commonAncestor
, null);
838 in
.getCompareConfiguration().setLeftLabel(leftRev
);
839 in
.getCompareConfiguration().setRightLabel(rightRev
);
840 if (monitor
.isCanceled()) {
841 return Status
.CANCEL_STATUS
;
843 openCompareEditorRunnable(page
, in
);
844 return Status
.OK_STATUS
;
851 private static String
getRepoRelativePath(@NonNull IPath location
,
852 @NonNull Repository repository
) {
853 IPath repoRoot
= new Path(repository
.getWorkTree().getPath());
854 final String gitPath
= location
.makeRelativeTo(repoRoot
).toString();
858 private static ITypedElement
getTypedElementFor(Repository repository
, String gitPath
, String rev
) throws IOException
{
859 final ITypedElement typedElement
;
860 if (GitFileRevision
.INDEX
.equals(rev
))
861 typedElement
= getIndexTypedElement(repository
, gitPath
);
862 else if (Constants
.HEAD
.equals(rev
))
863 typedElement
= getHeadTypedElement(repository
, gitPath
);
865 final ObjectId id
= repository
.resolve(rev
);
866 try (final RevWalk rw
= new RevWalk(repository
)) {
867 final RevCommit revCommit
= rw
.parseCommit(id
);
868 typedElement
= getFileRevisionTypedElement(gitPath
, revCommit
,
875 private static ITypedElement
getTypedElementForCommonAncestor(
876 Repository repository
, final String gitPath
, String srcRev
,
878 ITypedElement ancestor
= null;
880 final ObjectId srcID
= repository
.resolve(srcRev
);
881 final ObjectId dstID
= repository
.resolve(dstRev
);
882 if (srcID
!= null && dstID
!= null)
883 ancestor
= getFileRevisionTypedElementForCommonAncestor(
884 gitPath
, srcID
, dstID
, repository
);
885 } catch (IOException e
) {
887 .logError(NLS
.bind(UIText
.CompareUtils_errorCommonAncestor
,
894 * Opens a compare editor. The working tree version of the given file is
895 * compared with the version in the HEAD commit. Use this method if the
896 * given file is outide the workspace.
901 public static void compareHeadWithWorkingTree(Repository repository
,
903 ITypedElement base
= getHeadTypedElement(repository
, path
);
906 IFileRevision nextFile
;
907 nextFile
= new WorkingTreeFileRevision(new File(
908 repository
.getWorkTree(), path
));
909 String encoding
= ResourcesPlugin
.getEncoding();
910 ITypedElement next
= new FileRevisionTypedElement(nextFile
, encoding
);
911 GitCompareFileRevisionEditorInput input
= new GitCompareFileRevisionEditorInput(
913 CompareUI
.openCompareDialog(input
);
917 * Get a typed element for the file as contained in HEAD. Tries to return
918 * the last commit that modified the file in order to have more useful
919 * author information.
921 * Returns an empty typed element if there is not yet a head (initial import
924 * If there is an error getting the HEAD commit, it is handled and null
928 * @param repoRelativePath
929 * @return typed element, or null if there was an error getting the HEAD
932 public static ITypedElement
getHeadTypedElement(Repository repository
, String repoRelativePath
) {
934 Ref head
= repository
.exactRef(Constants
.HEAD
);
935 if (head
== null || head
.getObjectId() == null)
936 // Initial import, not yet a HEAD commit
937 return new EmptyTypedElement(""); //$NON-NLS-1$
939 RevCommit latestFileCommit
;
940 try (RevWalk rw
= new RevWalk(repository
)) {
941 RevCommit headCommit
= rw
.parseCommit(head
.getObjectId());
942 rw
.markStart(headCommit
);
943 rw
.setTreeFilter(AndTreeFilter
.create(
944 PathFilterGroup
.createFromStrings(repoRelativePath
),
945 TreeFilter
.ANY_DIFF
));
946 rw
.setRewriteParents(false);
947 latestFileCommit
= rw
.next();
949 if (latestFileCommit
== null)
950 latestFileCommit
= headCommit
;
953 return CompareUtils
.getFileRevisionTypedElement(repoRelativePath
, latestFileCommit
, repository
);
954 } catch (IOException e
) {
955 Activator
.handleError(UIText
.CompareUtils_errorGettingHeadCommit
,
962 * Get a typed element for the file in the index.
965 * @return typed element
966 * @throws IOException
968 public static ITypedElement
getIndexTypedElement(
969 final @NonNull IFile baseFile
) throws IOException
{
970 final RepositoryMapping mapping
= RepositoryMapping
.getMapping(baseFile
);
971 if (mapping
== null) {
972 Activator
.error(NLS
.bind(UIText
.GitHistoryPage_errorLookingUpPath
,
973 baseFile
.getLocation(), null), null);
976 final Repository repository
= mapping
.getRepository();
977 final String gitPath
= mapping
.getRepoRelativePath(baseFile
);
978 final String encoding
= CompareCoreUtils
.getResourceEncoding(baseFile
);
979 return getIndexTypedElement(repository
, gitPath
, encoding
);
983 * Get a typed element for the repository and repository-relative path in the index.
986 * @param repoRelativePath
987 * @return typed element
988 * @throws IOException
990 public static ITypedElement
getIndexTypedElement(
991 final Repository repository
, final String repoRelativePath
)
993 String encoding
= CompareCoreUtils
.getResourceEncoding(repository
, repoRelativePath
);
994 return getIndexTypedElement(repository
, repoRelativePath
, encoding
);
997 private static ITypedElement
getIndexTypedElement(
998 final Repository repository
, final String gitPath
, String encoding
) {
999 IFileRevision nextFile
= GitFileRevision
.inIndex(repository
, gitPath
);
1000 final EditableRevision next
= new EditableRevision(nextFile
, encoding
);
1002 IContentChangeListener listener
= new IContentChangeListener() {
1004 public void contentChanged(IContentChangeNotifier source
) {
1005 final byte[] newContent
= next
.getModifiedContent();
1006 setIndexEntryContents(repository
, gitPath
, newContent
);
1010 next
.addContentChangeListener(listener
);
1015 * Set contents on index entry of specified path. Line endings of contents
1016 * are canonicalized if configured, and gitattributes are honored.
1021 * content with working directory line endings
1023 private static void setIndexEntryContents(final Repository repository
,
1024 final String gitPath
, final byte[] newContent
) {
1025 DirCache cache
= null;
1027 cache
= repository
.lockDirCache();
1028 DirCacheEditor editor
= cache
.editor();
1029 if (newContent
.length
== 0) {
1030 editor
.add(new DirCacheEditor
.DeletePath(gitPath
));
1032 EolStreamType streamType
= null;
1033 String command
= null;
1034 try (TreeWalk walk
= new TreeWalk(repository
)) {
1035 walk
.setOperationType(OperationType
.CHECKIN_OP
);
1036 walk
.addTree(new DirCacheIterator(cache
));
1037 FileTreeIterator files
= new FileTreeIterator(repository
);
1038 files
.setDirCacheIterator(walk
, 0);
1039 walk
.addTree(files
);
1040 walk
.setFilter(AndTreeFilter
.create(
1041 PathFilterGroup
.createFromStrings(gitPath
),
1042 new NotIgnoredFilter(1)));
1043 walk
.setRecursive(true);
1046 .getEolStreamType(OperationType
.CHECKIN_OP
);
1047 command
= walk
.getFilterCommand(
1048 Constants
.ATTR_FILTER_TYPE_CLEAN
);
1051 InputStream filtered
= Filtering
.filter(repository
, gitPath
,
1052 new ByteArrayInputStream(newContent
), command
);
1053 if (streamType
== null) {
1054 WorkingTreeOptions workingTreeOptions
= repository
1055 .getConfig().get(WorkingTreeOptions
.KEY
);
1056 if (workingTreeOptions
.getAutoCRLF() == AutoCRLF
.FALSE
) {
1057 streamType
= EolStreamType
.DIRECT
;
1059 streamType
= EolStreamType
.AUTO_LF
;
1062 ByteBuffer content
= IO
.readWholeStream(
1063 EolStreamTypeUtil
.wrapInputStream(filtered
, streamType
),
1066 new DirCacheEntryEditor(gitPath
, repository
, content
));
1070 } catch (RuntimeException e
) {
1071 if (e
.getCause() instanceof IOException
)
1072 throw (IOException
) e
.getCause();
1077 } catch (IOException e
) {
1078 Activator
.handleError(
1079 UIText
.CompareWithIndexAction_errorOnAddToIndex
, e
, true);
1086 private static class DirCacheEntryEditor
extends DirCacheEditor
.PathEdit
{
1088 private final Repository repo
;
1090 private final ByteBuffer content
;
1092 public DirCacheEntryEditor(String path
, Repository repo
,
1093 ByteBuffer content
) {
1096 this.content
= content
;
1100 public void apply(DirCacheEntry ent
) {
1101 ObjectInserter inserter
= repo
.newObjectInserter();
1102 if (ent
.getFileMode() != FileMode
.REGULAR_FILE
)
1103 ent
.setFileMode(FileMode
.REGULAR_FILE
);
1105 ent
.setLength(content
.limit());
1106 ent
.setLastModified(System
.currentTimeMillis());
1108 ByteArrayInputStream in
= new ByteArrayInputStream(
1109 content
.array(), 0, content
.limit());
1111 inserter
.insert(Constants
.OBJ_BLOB
, content
.limit(),
1114 } catch (IOException ex
) {
1115 throw new RuntimeException(ex
);