Skip to main content
aboutsummaryrefslogtreecommitdiffstats
blob: 222ac16ce54bc3dcd169ab3ec13d000345f2b966 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
/*******************************************************************************
 * Copyright (C) 2010, Mathias Kinzler <mathias.kinzler@sap.com>
 *
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *******************************************************************************/
package org.eclipse.egit.ui.internal.merge;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;

import org.eclipse.compare.CompareConfiguration;
import org.eclipse.compare.CompareEditorInput;
import org.eclipse.compare.ITypedElement;
import org.eclipse.compare.structuremergeviewer.DiffNode;
import org.eclipse.compare.structuremergeviewer.Differencer;
import org.eclipse.compare.structuremergeviewer.IDiffContainer;
import org.eclipse.compare.structuremergeviewer.IDiffElement;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.egit.core.internal.storage.GitFileRevision;
import org.eclipse.egit.core.project.RepositoryMapping;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIText;
import org.eclipse.egit.ui.internal.CompareUtils;
import org.eclipse.egit.ui.internal.EditableRevision;
import org.eclipse.egit.ui.internal.LocalFileRevision;
import org.eclipse.egit.ui.internal.GitCompareFileRevisionEditorInput.EmptyTypedElement;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jgit.api.RebaseCommand;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.dircache.DirCacheIterator;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryState;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.revwalk.filter.RevFilter;
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
import org.eclipse.jgit.treewalk.filter.OrTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.eclipse.jgit.util.IO;
import org.eclipse.jgit.util.RawParseUtils;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.graphics.Image;
import org.eclipse.team.core.history.IFileRevision;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.ide.IDE.SharedImages;

/**
 * A Git-specific {@link CompareEditorInput}
 */
public class GitMergeEditorInput extends CompareEditorInput {
	private static final String LABELPATTERN = "{0} - {1}"; //$NON-NLS-1$

	private static final Image FOLDER_IMAGE = PlatformUI.getWorkbench()
			.getSharedImages().getImage(ISharedImages.IMG_OBJ_FOLDER);

	private static final Image PROJECT_IMAGE = PlatformUI.getWorkbench()
			.getSharedImages().getImage(SharedImages.IMG_OBJ_PROJECT);

	private final boolean useWorkspace;

	private final IResource[] resources;

	/**
	 * @param useWorkspace
	 *            if <code>true</code>, use the workspace content (i.e. the
	 *            Git-merged version) as "left" content, otherwise use HEAD
	 *            (i.e. the previous, non-merged version)
	 * @param resources
	 *            as selected by the user
	 */
	public GitMergeEditorInput(boolean useWorkspace, IResource... resources) {
		super(new CompareConfiguration());
		this.useWorkspace = useWorkspace;
		this.resources = resources;
		CompareConfiguration config = getCompareConfiguration();
		config.setLeftEditable(true);
	}

	@Override
	protected Object prepareInput(IProgressMonitor monitor)
			throws InvocationTargetException, InterruptedException {
		// make sure all resources belong to the same repository
		RevWalk rw = null;
		try {
			monitor.beginTask(
					UIText.GitMergeEditorInput_CheckingResourcesTaskName,
					IProgressMonitor.UNKNOWN);
			List<String> filterPaths = new ArrayList<String>();
			Repository repo = null;
			for (IResource resource : resources) {
				RepositoryMapping map = RepositoryMapping.getMapping(resource
						.getProject());
				if (repo != null && repo != map.getRepository())
					throw new InvocationTargetException(
							new IllegalStateException(
									UIText.AbstractHistoryCommanndHandler_NoUniqueRepository));
				filterPaths.add(map.getRepoRelativePath(resource));
				repo = map.getRepository();
			}

			if (repo == null)
				throw new InvocationTargetException(
						new IllegalStateException(
								UIText.AbstractHistoryCommanndHandler_NoUniqueRepository));

			if (monitor.isCanceled())
				throw new InterruptedException();

			rw = new RevWalk(repo);

			// get the "right" side (MERGE_HEAD for merge, ORIG_HEAD for rebase)
			final RevCommit rightCommit;
			try {
				String target;
				if (repo.getRepositoryState().equals(RepositoryState.MERGING))
					target = Constants.MERGE_HEAD;
				else if (repo.getRepositoryState().equals(RepositoryState.CHERRY_PICKING))
					target = Constants.CHERRY_PICK_HEAD;
				else if (repo.getRepositoryState().equals(
						RepositoryState.REBASING_INTERACTIVE))
					target = readFile(repo.getDirectory(),
							RebaseCommand.REBASE_MERGE + File.separatorChar
									+ RebaseCommand.STOPPED_SHA);
				else
					target = Constants.ORIG_HEAD;
				ObjectId mergeHead = repo.resolve(target);
				if (mergeHead == null)
					throw new IOException(NLS.bind(
							UIText.ValidationUtils_CanNotResolveRefMessage,
							target));
				rightCommit = rw.parseCommit(mergeHead);
			} catch (IOException e) {
				throw new InvocationTargetException(e);
			}

			// we need the HEAD, also to determine the common
			// ancestor
			final RevCommit headCommit;
			try {
				ObjectId head = repo.resolve(Constants.HEAD);
				if (head == null)
					throw new IOException(NLS.bind(
							UIText.ValidationUtils_CanNotResolveRefMessage,
							Constants.HEAD));
				headCommit = rw.parseCommit(head);
			} catch (IOException e) {
				throw new InvocationTargetException(e);
			}

			final String fullBranch;
			try {
				fullBranch = repo.getFullBranch();
			} catch (IOException e) {
				throw new InvocationTargetException(e);
			}

			// try to obtain the common ancestor
			List<RevCommit> startPoints = new ArrayList<RevCommit>();
			rw.setRevFilter(RevFilter.MERGE_BASE);
			startPoints.add(rightCommit);
			startPoints.add(headCommit);
			RevCommit ancestorCommit;
			try {
				rw.markStart(startPoints);
				ancestorCommit = rw.next();
			} catch (Exception e) {
				ancestorCommit = null;
			}

			if (monitor.isCanceled())
				throw new InterruptedException();

			// set the labels
			CompareConfiguration config = getCompareConfiguration();
			config.setRightLabel(NLS.bind(LABELPATTERN, rightCommit
					.getShortMessage(), rightCommit.name()));

			if (!useWorkspace)
				config.setLeftLabel(NLS.bind(LABELPATTERN, headCommit
						.getShortMessage(), headCommit.name()));
			else
				config.setLeftLabel(UIText.GitMergeEditorInput_WorkspaceHeader);

			if (ancestorCommit != null)
				config.setAncestorLabel(NLS.bind(LABELPATTERN, ancestorCommit
						.getShortMessage(), ancestorCommit.name()));

			// set title and icon
			setTitle(NLS.bind(UIText.GitMergeEditorInput_MergeEditorTitle,
					new Object[] {
							Activator.getDefault().getRepositoryUtil()
									.getRepositoryName(repo),
							rightCommit.getShortMessage(), fullBranch }));

			// build the nodes
			try {
				return buildDiffContainer(repo, rightCommit, headCommit,
						ancestorCommit, filterPaths, rw, monitor);
			} catch (IOException e) {
				throw new InvocationTargetException(e);
			}
		} finally {
			if (rw != null)
				rw.dispose();
			monitor.done();
		}
	}

	@Override
	protected void contentsCreated() {
		super.contentsCreated();
		// select the first conflict
		getNavigator().selectChange(true);
	}

	@Override
	protected void handleDispose() {
		super.handleDispose();
		// we do NOT dispose the images, as these are shared
	}

	private IDiffContainer buildDiffContainer(Repository repository,
			RevCommit rightCommit, RevCommit headCommit,
			RevCommit ancestorCommit, List<String> filterPaths, RevWalk rw,
			IProgressMonitor monitor) throws IOException, InterruptedException {

		monitor.setTaskName(UIText.GitMergeEditorInput_CalculatingDiffTaskName);
		IDiffContainer result = new DiffNode(Differencer.CONFLICTING);

		TreeWalk tw = new TreeWalk(repository);
		try {
			int dirCacheIndex = tw.addTree(new DirCacheIterator(repository
					.readDirCache()));
			int fileTreeIndex = tw.addTree(new FileTreeIterator(repository));
			int repositoryTreeIndex = tw.addTree(rw.parseTree(repository
					.resolve(Constants.HEAD)));

			// skip ignored resources
			NotIgnoredFilter notIgnoredFilter = new NotIgnoredFilter(
					fileTreeIndex);
			// filter by selected resources
			if (filterPaths.size() > 1) {
				List<TreeFilter> suffixFilters = new ArrayList<TreeFilter>();
				for (String filterPath : filterPaths)
					suffixFilters.add(PathFilter.create(filterPath));
				TreeFilter otf = OrTreeFilter.create(suffixFilters);
				tw.setFilter(AndTreeFilter.create(otf, notIgnoredFilter));
			} else if (filterPaths.size() > 0)
				tw.setFilter(AndTreeFilter.create(PathFilter.create(filterPaths
						.get(0)), notIgnoredFilter));
			else
				tw.setFilter(notIgnoredFilter);

			tw.setRecursive(true);

			while (tw.next()) {
				if (monitor.isCanceled())
					throw new InterruptedException();
				String gitPath = tw.getPathString();
				monitor.setTaskName(gitPath);

				FileTreeIterator fit = tw.getTree(fileTreeIndex,
						FileTreeIterator.class);
				if (fit == null)
					continue;

				DirCacheIterator dit = tw.getTree(dirCacheIndex,
						DirCacheIterator.class);

				final DirCacheEntry dirCacheEntry = dit == null ? null : dit
						.getDirCacheEntry();

				boolean conflicting = dirCacheEntry != null
						&& dirCacheEntry.getStage() > 0;

				AbstractTreeIterator rt = tw.getTree(repositoryTreeIndex,
						AbstractTreeIterator.class);

				// compare local file against HEAD to see if it was modified
				boolean modified = rt != null
						&& !fit.getEntryObjectId()
								.equals(rt.getEntryObjectId());

				// if this is neither conflicting nor changed, we skip it
				if (!conflicting && !modified)
					continue;

				ITypedElement right;
				if (conflicting)
					right = CompareUtils.getFileRevisionTypedElement(gitPath,
							rightCommit, repository);
				else
					right = CompareUtils.getFileRevisionTypedElement(gitPath,
							headCommit, repository);

				// can this really happen?
				if (right instanceof EmptyTypedElement)
					continue;

				IFileRevision rev;
				// if the file is not conflicting (as it was auto-merged)
				// we will show the auto-merged (local) version

				IPath locationPath = new Path(fit.getEntryFile().getPath());
				final IFile file = ResourcesPlugin.getWorkspace().getRoot()
						.getFileForLocation(locationPath);
				if (file == null)
					// TODO in the future, we should be able to show a version
					// for a non-workspace file as well
					continue;
				if (!conflicting || useWorkspace)
					rev = new LocalFileRevision(file);
				else
					rev = GitFileRevision.inCommit(repository, headCommit,
							gitPath, null);

				EditableRevision leftEditable = new EditableRevision(rev) {
					@Override
					public void setContent(final byte[] newContent) {
						try {
							run(false, false, new IRunnableWithProgress() {
								public void run(IProgressMonitor myMonitor)
										throws InvocationTargetException,
										InterruptedException {
									try {
										file.setContents(
												new ByteArrayInputStream(
														newContent), false,
												true, myMonitor);
									} catch (CoreException e) {
										throw new InvocationTargetException(e);
									}
								}
							});
						} catch (InvocationTargetException e) {
							Activator
									.handleError(e.getTargetException()
											.getMessage(), e
											.getTargetException(), true);
						} catch (InterruptedException e) {
							// ignore here
						}
					}
				};
				// make sure we don't need a round trip later
				try {
					leftEditable.cacheContents(monitor);
				} catch (CoreException e) {
					throw new IOException(e.getMessage());
				}

				int kind = Differencer.NO_CHANGE;
				if (conflicting)
					kind = Differencer.CONFLICTING;
				else if (modified)
					kind = Differencer.PSEUDO_CONFLICT;

				DiffNode fileParent = getFileParent(result, file);

				ITypedElement anc;
				if (ancestorCommit != null)
					anc = CompareUtils.getFileRevisionTypedElement(gitPath,
							ancestorCommit, repository);
				else
					anc = null;
				// we get an ugly black icon if we have an EmptyTypedElement
				// instead of null
				if (anc instanceof EmptyTypedElement)
					anc = null;
				// create the node as child
				new DiffNode(fileParent, kind, anc, leftEditable, right);
			}
			return result;
		} finally {
			tw.release();
		}
	}

	private DiffNode getFileParent(IDiffContainer root, IFile file) {
		String projectName = file.getProject().getName();
		DiffNode child = getOrCreateChild(root, projectName, true);
		IPath path = file.getProjectRelativePath();
		for (int i = 0; i < path.segmentCount() - 1; i++)
			child = getOrCreateChild(child, path.segment(i), false);
		return child;
	}

	private DiffNode getOrCreateChild(IDiffContainer parent, final String name,
			final boolean projectMode) {
		for (IDiffElement child : parent.getChildren()) {
			if (child.getName().equals(name)) {
				return ((DiffNode) child);
			}
		}
		DiffNode child = new DiffNode(parent, Differencer.NO_CHANGE) {

			@Override
			public String getName() {
				return name;
			}

			@Override
			public Image getImage() {
				if (projectMode)
					return PROJECT_IMAGE;
				else
					return FOLDER_IMAGE;
			}
		};
		return child;
	}

	private String readFile(File directory, String fileName) throws IOException {
		byte[] content = IO.readFully(new File(directory, fileName));
		// strip off the last LF
		int end = content.length;
		while (0 < end && content[end - 1] == '\n')
			end--;
		return RawParseUtils.decode(content, 0, end);
	}
}

Back to the top