Have icon for "reset" entry in reflog
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / CompareUtils.java
blob5efc881404f1543e6dc574c37995af8dd1def50b
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
11 * Contributors:
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;
23 import java.io.File;
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$
123 * @param gitPath
124 * path within the commit's tree of the file.
125 * @param commit
126 * the commit the blob was identified to be within.
127 * @param db
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);
138 * @param gitPath
139 * path within the commit's tree of the file.
140 * @param commit
141 * the commit the blob was identified to be within.
142 * @param db
143 * the repository this commit was loaded out of, and that this
144 * file's blob should also be reachable through.
145 * @param blobId
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,
152 ObjectId blobId) {
153 ITypedElement right = new GitCompareFileRevisionEditorInput.EmptyTypedElement(
154 NLS.bind(UIText.GitHistoryPage_FileNotInCommit,
155 getName(gitPath), truncatedRevision(commit.name())));
157 try {
158 IFileRevision nextFile = getFileRevision(gitPath, commit, db,
159 blobId);
160 if (nextFile != null) {
161 String encoding = CompareCoreUtils.getResourceEncoding(db,
162 gitPath);
163 right = new FileRevisionTypedElement(nextFile, encoding);
165 } catch (IOException e) {
166 Activator.error(NLS.bind(UIText.GitHistoryPage_errorLookingUpPath,
167 gitPath, commit.getId()), e);
169 return right;
172 private static String getName(String gitPath) {
173 final int last = gitPath.lastIndexOf('/');
174 return last >= 0 ? gitPath.substring(last + 1) : gitPath;
179 * @param gitPath
180 * path within the commit's tree of the file.
181 * @param commit
182 * the commit the blob was identified to be within.
183 * @param db
184 * the repository this commit was loaded out of, and that this
185 * file's blob should also be reachable through.
186 * @param blobId
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)
194 throws IOException {
195 try (TreeWalk w = TreeWalk.forPath(db, gitPath, commit.getTree())) {
196 if (w == null) {
197 // Not in commit
198 return null;
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,
207 metadata);
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
217 * @param gitPath
218 * path within the ancestor commit's tree of the file.
219 * @param commit1
220 * @param commit2
221 * @param db
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,
228 Repository db) {
229 ITypedElement ancestor = null;
230 RevCommit commonAncestor = null;
231 try {
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
242 return null;
245 ITypedElement ancestorCandidate = CompareUtils
246 .getFileRevisionTypedElement(gitPath, commonAncestor, db);
247 if (!(ancestorCandidate instanceof EmptyTypedElement))
248 ancestor = ancestorCandidate;
250 return ancestor;
254 * @param ci
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);
260 else
261 return ci;
265 * Compares two files between the given commits, taking possible renames
266 * into account.
268 * @param commit1
269 * the "left" commit for the comparison editor
270 * @param commit2
271 * the "right" commit for the comparison editor
272 * @param commit1Path
273 * path to the file within commit1's tree
274 * @param commit2Path
275 * path to the file within commit2's tree
276 * @param repository
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,
289 next, null);
290 CompareUtils.openInCompare(workBenchPage, in);
294 * @param workBenchPage
295 * @param input
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);
306 else
307 workBenchPage.bringToTop(editor);
308 } else {
309 // if editor is currently not open on that input either re-use
310 // existing
311 CompareUI.reuseCompareEditor(input, (IReusableEditor) editor);
312 if (OpenStrategy.activateOnOpen())
313 workBenchPage.activate(editor);
314 else
315 workBenchPage.bringToTop(editor);
317 } else {
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);
328 if (part != null
329 && (part.getEditorInput() instanceof GitCompareFileRevisionEditorInput || part.getEditorInput() instanceof GitCompareEditorInput)
330 && part instanceof IReusableEditor
331 && part.getEditorInput().equals(input)) {
332 return part;
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);
340 if (part != null
341 && (part.getEditorInput() instanceof SaveableCompareEditorInput)
342 && part instanceof IReusableEditor && !part.isDirty()) {
343 return part;
347 // no re-usable editor found
348 return null;
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());
367 @Override
368 public void run() {
369 CompareUtils.setReuseOpenEditor(isChecked());
372 @Override
373 public void dispose() {
374 // stop listening
375 node.removePreferenceChangeListener(this);
378 @Override
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.
401 * @param repository
402 * @param file
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);
410 return;
412 String path = mapping.getRepoRelativePath(
413 file);
414 ITypedElement base = getHeadTypedElement(repository, path);
415 if (base == null)
416 return;
418 IFileRevision nextFile = new WorkspaceFileRevision(file);
419 String encoding = null;
420 try {
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(
427 next, base, null);
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
434 * {@code refName}.
436 * @param repository
437 * The repository to load file revisions from.
438 * @param file
439 * Resource to compare revisions for. Must be either
440 * {@link IFile} or a symbolic link to directory ({@link IFolder}).
441 * @param refName
442 * Reference to compare with the workspace version of
443 * {@code file}. Can be either a commit ID, a reference or a
444 * branch name.
445 * @param page
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) {
451 if (file == null) {
452 return;
454 final IPath location = file.getLocation();
455 if(location == null){
456 return;
459 Job job = new Job(UIText.CompareUtils_jobName) {
461 @Override
462 public IStatus run(IProgressMonitor monitor) {
463 if (monitor.isCanceled()) {
464 return Status.CANCEL_STATUS;
466 final RepositoryMapping mapping = RepositoryMapping
467 .getMapping(file);
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,
477 location);
478 } else if (file instanceof IFile) {
479 base = SaveableCompareEditorInput
480 .createFileElement((IFile) file);
481 } else {
482 return Activator.createErrorStatus(
483 NLS.bind(UIText.CompareUtils_wrongResourceArgument,
484 location, file));
487 final String gitPath = mapping.getRepoRelativePath(file);
488 CompareEditorInput in;
489 try {
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;
503 job.setUser(true);
504 job.schedule();
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
510 * thread.
512 * @param page
513 * can be null
514 * @param in
515 * non null
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() {
523 @Override
524 public void run() {
525 openCompareEditorRunnable(page, in);
528 return;
531 if (page != null) {
532 openInCompare(page, in);
533 } else {
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.
543 * @param repository
544 * The repository to load file revisions from.
545 * @param location
546 * Location of the file to compare revisions for.
547 * @param refName
548 * Reference to compare with the workspace version of
549 * {@code file}. Can be either a commit ID, a reference or a
550 * branch name.
551 * @param page
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) {
561 @Override
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;
571 try {
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;
585 job.setUser(true);
586 job.schedule();
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);
606 else {
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,
611 repository);
613 if (base != null) {
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);
626 return in;
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.
635 * <p>
636 * This can also be used to synchronize the whole repository if
637 * <code>resources</code> is empty.
638 * </p>
639 * <p>
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.
642 * </p>
644 * @param resources
645 * The set of resources to compare. Can be empty (in which case
646 * we'll synchronize the whole repository).
647 * @param repository
648 * The repository to load file revisions from.
649 * @param leftRev
650 * Left revision of the comparison (usually the local or "new"
651 * revision). Won't be used if <code>includeLocal</code> is
652 * <code>true</code>.
653 * @param rightRev
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.
658 * @param page
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
677 * resources set.
678 * <p>
679 * This can also be used to synchronize the whole repository if
680 * <code>resources</code> is empty.
681 * </p>
682 * <p>
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.
685 * </p>
687 * @param file
688 * The file to compare.
689 * @param repository
690 * The repository to load file revisions from.
691 * @param leftPath
692 * The repository relative path to be used for the left revision,
693 * when comparing directly.
694 * @param rightPath
695 * The repository relative path to be used for the right
696 * revision, when comparing directly.
697 * @param leftRev
698 * Left revision of the comparison (usually the local or "new"
699 * revision). Won't be used if <code>includeLocal</code> is
700 * <code>true</code>.
701 * @param rightRev
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.
706 * @param page
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,
716 rightPath,
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.
731 * @param location
732 * Location of the file to compare.
733 * @param repository
734 * The repository to load file revisions from.
735 * @param leftRev
736 * Left revision of the comparison (usually the local or "new"
737 * revision). Won't be used if <code>includeLocal</code> is
738 * <code>true</code>.
739 * @param rightRev
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.
744 * @param page
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) {
751 if (includeLocal)
752 compareLocalWithRef(repository, location, rightRev, page);
753 else {
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.
763 * @param repository
764 * The repository to load file revisions from.
765 * @param gitPath
766 * The repository relative path to be used for the left & right
767 * revisions.
768 * @param leftRev
769 * Left revision of the comparison (usually the local or "new"
770 * revision). Won't be used if <code>includeLocal</code> is
771 * <code>true</code>.
772 * @param rightRev
773 * Right revision of the comparison (usually the "old" revision).
774 * @param page
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.
787 * @param repository
788 * The repository to load file revisions from.
789 * @param leftGitPath
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
793 * revision.
794 * @param leftRev
795 * Left revision of the comparison (usually the local or "new"
796 * revision). Won't be used if <code>includeLocal</code> is
797 * <code>true</code>.
798 * @param rightRev
799 * Right revision of the comparison (usually the "old" revision).
800 * @param page
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) {
811 @Override
812 public IStatus run(IProgressMonitor monitor) {
813 if (monitor.isCanceled()) {
814 return Status.CANCEL_STATUS;
816 final ITypedElement left;
817 final ITypedElement right;
818 try {
819 left = getTypedElementFor(repository, leftGitPath, leftRev);
820 right = getTypedElementFor(repository, rightGitPath,
821 rightRev);
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);
832 } else {
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;
847 job.setUser(true);
848 job.schedule();
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();
855 return gitPath;
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);
864 else {
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,
869 repository);
872 return typedElement;
875 private static ITypedElement getTypedElementForCommonAncestor(
876 Repository repository, final String gitPath, String srcRev,
877 String dstRev) {
878 ITypedElement ancestor = null;
879 try {
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) {
886 Activator
887 .logError(NLS.bind(UIText.CompareUtils_errorCommonAncestor,
888 srcRev, dstRev), e);
890 return ancestor;
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.
898 * @param repository
899 * @param path
901 public static void compareHeadWithWorkingTree(Repository repository,
902 String path) {
903 ITypedElement base = getHeadTypedElement(repository, path);
904 if (base == null)
905 return;
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(
912 next, base, null);
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.
920 * <p>
921 * Returns an empty typed element if there is not yet a head (initial import
922 * case).
923 * <p>
924 * If there is an error getting the HEAD commit, it is handled and null
925 * returned.
927 * @param repository
928 * @param repoRelativePath
929 * @return typed element, or null if there was an error getting the HEAD
930 * commit
932 public static ITypedElement getHeadTypedElement(Repository repository, String repoRelativePath) {
933 try {
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();
948 // Fall back to HEAD
949 if (latestFileCommit == null)
950 latestFileCommit = headCommit;
953 return CompareUtils.getFileRevisionTypedElement(repoRelativePath, latestFileCommit, repository);
954 } catch (IOException e) {
955 Activator.handleError(UIText.CompareUtils_errorGettingHeadCommit,
956 e, true);
957 return null;
962 * Get a typed element for the file in the index.
964 * @param baseFile
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);
974 return 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.
985 * @param repository
986 * @param repoRelativePath
987 * @return typed element
988 * @throws IOException
990 public static ITypedElement getIndexTypedElement(
991 final Repository repository, final String repoRelativePath)
992 throws IOException {
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() {
1003 @Override
1004 public void contentChanged(IContentChangeNotifier source) {
1005 final byte[] newContent = next.getModifiedContent();
1006 setIndexEntryContents(repository, gitPath, newContent);
1010 next.addContentChangeListener(listener);
1011 return next;
1015 * Set contents on index entry of specified path. Line endings of contents
1016 * are canonicalized if configured, and gitattributes are honored.
1018 * @param repository
1019 * @param gitPath
1020 * @param newContent
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;
1026 try {
1027 cache = repository.lockDirCache();
1028 DirCacheEditor editor = cache.editor();
1029 if (newContent.length == 0) {
1030 editor.add(new DirCacheEditor.DeletePath(gitPath));
1031 } else {
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);
1044 if (walk.next()) {
1045 streamType = walk
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;
1058 } else {
1059 streamType = EolStreamType.AUTO_LF;
1062 ByteBuffer content = IO.readWholeStream(
1063 EolStreamTypeUtil.wrapInputStream(filtered, streamType),
1064 newContent.length);
1065 editor.add(
1066 new DirCacheEntryEditor(gitPath, repository, content));
1068 try {
1069 editor.commit();
1070 } catch (RuntimeException e) {
1071 if (e.getCause() instanceof IOException)
1072 throw (IOException) e.getCause();
1073 else
1074 throw e;
1077 } catch (IOException e) {
1078 Activator.handleError(
1079 UIText.CompareWithIndexAction_errorOnAddToIndex, e, true);
1080 } finally {
1081 if (cache != null)
1082 cache.unlock();
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) {
1094 super(path);
1095 this.repo = repo;
1096 this.content = content;
1099 @Override
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());
1107 try {
1108 ByteArrayInputStream in = new ByteArrayInputStream(
1109 content.array(), 0, content.limit());
1110 ent.setObjectId(
1111 inserter.insert(Constants.OBJ_BLOB, content.limit(),
1112 in));
1113 inserter.flush();
1114 } catch (IOException ex) {
1115 throw new RuntimeException(ex);