[compare] Ensure HiddenResources can create linked IFiles

Eclipse has a preference at 'General->Workspace->Linked Resources'
where the user can disable linked resources. If he does, linked
resources cannot be created anymore (but existing linked resources
continue to work perfectly fine).

EGit _needs_ to be able to create linked resources to fully implement
comparisons in the merge tool. Otherwise comparisons using stage 2 as
input but saving to a non-workspace file won't work properly, and the
new "working tree pre-merged to 'ours'" input also relies on creating
linked resources being possible.

Therefore just enable creating linked resources by force and reset the
preference afterwards unless it has been changed again in the meantime.

Bug: 574780
Change-Id: Ia9536575f541f5574e7698e889119977d9f53ed6
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
diff --git a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/efs/HiddenResources.java b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/efs/HiddenResources.java
index 0b6d27b..cad4b4c 100644
--- a/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/efs/HiddenResources.java
+++ b/org.eclipse.egit.core/src/org/eclipse/egit/core/internal/efs/HiddenResources.java
@@ -19,6 +19,7 @@
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.text.MessageFormat;
+import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.eclipse.core.resources.IFile;
 import org.eclipse.core.resources.IFolder;
@@ -32,7 +33,11 @@
 import org.eclipse.core.runtime.CoreException;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.core.runtime.Platform;
 import org.eclipse.core.runtime.SubMonitor;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences.IPreferenceChangeListener;
+import org.eclipse.core.runtime.preferences.InstanceScope;
 import org.eclipse.egit.core.Activator;
 import org.eclipse.egit.core.internal.efs.EgitFileSystem.UriComponents;
 import org.eclipse.jgit.lib.Repository;
@@ -68,6 +73,8 @@
 
 	private boolean initialized;
 
+	private final Object lock = new Object();
+
 	/**
 	 * Create new linked {@link IFile} in a hidden project with the given uri
 	 * and encoding.
@@ -326,10 +333,53 @@
 			Charset encoding, IProgressMonitor monitor) throws CoreException {
 		SubMonitor progress = SubMonitor.convert(monitor, 2);
 		IFile file = folder.getFile(name);
-		file.createLink(uri, IResource.NONE, progress.newChild(1));
+		linkFile(file, uri, progress.newChild(1));
 		if (encoding != null) {
 			file.setCharset(encoding.name(), progress.newChild(1));
 		}
 		return file;
 	}
+
+	private void linkFile(IFile file, URI uri, IProgressMonitor monitor)
+			throws CoreException {
+		synchronized (lock) {
+			boolean linkingDisabled = Platform.getPreferencesService()
+					.getBoolean(ResourcesPlugin.PI_RESOURCES,
+							ResourcesPlugin.PREF_DISABLE_LINKING, false, null);
+			IEclipsePreferences prefs = null;
+			IPreferenceChangeListener listener = null;
+			AtomicBoolean prefChanged = new AtomicBoolean();
+			if (linkingDisabled) {
+				// The user has disabled creating linked resources. Force-
+				// enable the preference, then reset it afterwards.
+				//
+				// Note that the preference only guards *creating* linked
+				// resources. Existing linked resources are handled perfectly
+				// well by Eclipse even when the preference is true.
+				prefs = InstanceScope.INSTANCE
+						.getNode(ResourcesPlugin.PI_RESOURCES);
+				prefs.putBoolean(ResourcesPlugin.PREF_DISABLE_LINKING, false);
+				listener = event -> {
+					if (ResourcesPlugin.PREF_DISABLE_LINKING
+							.equals(event.getKey())) {
+						prefChanged.set(true);
+					}
+				};
+				prefs.addPreferenceChangeListener(listener);
+			}
+			try {
+				file.createLink(uri, IResource.NONE, monitor);
+			} finally {
+				if (prefs != null) {
+					prefs.removePreferenceChangeListener(listener);
+					// Don't reset if somebody else changed the preference in
+					// the meantime.
+					if (!prefChanged.get()) {
+						prefs.putBoolean(ResourcesPlugin.PREF_DISABLE_LINKING,
+								linkingDisabled);
+					}
+				}
+			}
+		}
+	}
 }
diff --git a/org.eclipse.egit.ui.test/META-INF/MANIFEST.MF b/org.eclipse.egit.ui.test/META-INF/MANIFEST.MF
index 4179a52..ada6b9e 100644
--- a/org.eclipse.egit.ui.test/META-INF/MANIFEST.MF
+++ b/org.eclipse.egit.ui.test/META-INF/MANIFEST.MF
@@ -49,9 +49,10 @@
  org.junit;version="[4.13.0,5.0.0)",
  org.junit.rules;version="[4.13.0,5.0.0)",
  org.junit.runner;version="[4.13.0,5.0.0)",
+ org.junit.runners;version="[4.13.0,5.0.0)",
  org.mockito;version="[2.13.0,3.0.0)",
- org.mockito.junit;version="[2.13.0,3.0.0)",
- org.mockito.stubbing;version="[2.13.0,3.0.0)",
  org.mockito.hamcrest;version="[2.13.0,3.0.0)",
  org.mockito.invocation;version="[2.13.0,3.0.0)",
+ org.mockito.junit;version="[2.13.0,3.0.0)",
+ org.mockito.stubbing;version="[2.13.0,3.0.0)",
  org.osgi.framework;version="[1.4.0,2.0.0)"
diff --git a/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/test/team/actions/MergeToolTest.java b/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/test/team/actions/MergeToolTest.java
index 4662b08..0e6a687 100644
--- a/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/test/team/actions/MergeToolTest.java
+++ b/org.eclipse.egit.ui.test/src/org/eclipse/egit/ui/test/team/actions/MergeToolTest.java
@@ -13,12 +13,18 @@
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.hamcrest.Matchers.hasItem;
 import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
 
 import java.io.File;
+import java.util.Arrays;
+import java.util.List;
 
+import org.eclipse.core.resources.ResourcesPlugin;
 import org.eclipse.core.runtime.IPath;
 import org.eclipse.core.runtime.Path;
+import org.eclipse.core.runtime.preferences.IEclipsePreferences;
+import org.eclipse.core.runtime.preferences.InstanceScope;
 import org.eclipse.egit.core.JobFamilies;
 import org.eclipse.egit.core.internal.indexdiff.IndexDiffCache;
 import org.eclipse.egit.core.op.CherryPickOperation;
@@ -42,16 +48,33 @@
 import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
 
 /**
  * Test for the "Merge Tool" action on conflicting files.
  */
+@RunWith(Parameterized.class)
 public class MergeToolTest extends LocalRepositoryTestCase {
 
 	private TestRepository testRepository;
 
 	private int mergeMode;
 
+	private IEclipsePreferences prefs;
+
+	private boolean prefEnabled;
+
+	@Parameter
+	public Boolean linkingDisabled;
+
+	@Parameters(name = "linkingDisabled={0}")
+	public static List<Boolean> getParameters() {
+		return Arrays.asList(Boolean.FALSE, Boolean.TRUE);
+	}
+
 	@Before
 	public void setUp() throws Exception {
 		File repositoryFile = createProjectAndCommitToRepository();
@@ -59,12 +82,22 @@
 		testRepository = new TestRepository<>(repository);
 		mergeMode = Activator.getDefault()
 				.getPreferenceStore().getInt(UIPreferences.MERGE_MODE);
+		prefs = InstanceScope.INSTANCE.getNode(ResourcesPlugin.PI_RESOURCES);
+		prefEnabled = prefs.getBoolean(ResourcesPlugin.PREF_DISABLE_LINKING,
+				false);
+		prefs.putBoolean(ResourcesPlugin.PREF_DISABLE_LINKING,
+				linkingDisabled.booleanValue());
 	}
 
 	@After
 	public void resetMergeMode() throws Exception {
 		Activator.getDefault().getPreferenceStore()
 				.setValue(UIPreferences.MERGE_MODE, mergeMode);
+		boolean currentValue = prefs.getBoolean(
+				ResourcesPlugin.PREF_DISABLE_LINKING,
+				linkingDisabled.booleanValue());
+		prefs.putBoolean(ResourcesPlugin.PREF_DISABLE_LINKING, prefEnabled);
+		assertEquals(linkingDisabled, Boolean.valueOf(currentValue));
 	}
 
 	@Test