ExternalToolUtils.java

/*
 * Copyright (C) 2018-2021, Andre Bossert <andre.bossert@siemens.com>
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Distribution License v. 1.0 which is available at
 * https://www.eclipse.org/org/documents/edl-v10.php.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */
package org.eclipse.jgit.internal.diffmergetool;

import java.util.TreeMap;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;

import org.eclipse.jgit.attributes.Attributes;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.treewalk.WorkingTreeIterator;
import org.eclipse.jgit.treewalk.filter.NotIgnoredFilter;
import org.eclipse.jgit.util.FS;

/**
 * Utilities for diff- and merge-tools.
 */
public class ExternalToolUtils {

	/**
	 * Key for merge tool git configuration section
	 */
	public static final String KEY_MERGE_TOOL = "mergetool"; //$NON-NLS-1$

	/**
	 * Key for diff tool git configuration section
	 */
	public static final String KEY_DIFF_TOOL = "difftool"; //$NON-NLS-1$

	/**
	 * Prepare command for execution.
	 *
	 * @param command
	 *            the input "command" string
	 * @param localFile
	 *            the local file (ours)
	 * @param remoteFile
	 *            the remote file (theirs)
	 * @param mergedFile
	 *            the merged file (worktree)
	 * @param baseFile
	 *            the base file (can be null)
	 * @return the prepared (with replaced variables) command string
	 * @throws IOException
	 */
	public static String prepareCommand(String command, FileElement localFile,
			FileElement remoteFile, FileElement mergedFile,
			FileElement baseFile) throws IOException {
		if (localFile != null) {
			command = localFile.replaceVariable(command);
		}
		if (remoteFile != null) {
			command = remoteFile.replaceVariable(command);
		}
		if (mergedFile != null) {
			command = mergedFile.replaceVariable(command);
		}
		if (baseFile != null) {
			command = baseFile.replaceVariable(command);
		}
		return command;
	}

	/**
	 * Prepare environment needed for execution.
	 *
	 * @param gitDir
	 *            the .git directory
	 * @param localFile
	 *            the local file (ours)
	 * @param remoteFile
	 *            the remote file (theirs)
	 * @param mergedFile
	 *            the merged file (worktree)
	 * @param baseFile
	 *            the base file (can be null)
	 * @return the environment map with variables and values (file paths)
	 * @throws IOException
	 */
	public static Map<String, String> prepareEnvironment(File gitDir,
			FileElement localFile, FileElement remoteFile,
			FileElement mergedFile, FileElement baseFile) throws IOException {
		Map<String, String> env = new TreeMap<>();
		if (gitDir != null) {
			env.put(Constants.GIT_DIR_KEY, gitDir.getAbsolutePath());
		}
		if (localFile != null) {
			localFile.addToEnv(env);
		}
		if (remoteFile != null) {
			remoteFile.addToEnv(env);
		}
		if (mergedFile != null) {
			mergedFile.addToEnv(env);
		}
		if (baseFile != null) {
			baseFile.addToEnv(env);
		}
		return env;
	}

	/**
	 * @param path
	 *            the path to be quoted
	 * @return quoted path if it contains spaces
	 */
	@SuppressWarnings("nls")
	public static String quotePath(String path) {
		// handling of spaces in path
		if (path.contains(" ")) {
			// add quotes before if needed
			if (!path.startsWith("\"")) {
				path = "\"" + path;
			}
			// add quotes after if needed
			if (!path.endsWith("\"")) {
				path = path + "\"";
			}
		}
		return path;
	}

	/**
	 * @param fs
	 *            the file system abstraction
	 * @param gitDir
	 *            the .git directory
	 * @param directory
	 *            the working directory
	 * @param path
	 *            the tool path
	 * @return true if tool available and false otherwise
	 */
	public static boolean isToolAvailable(FS fs, File gitDir, File directory,
			String path) {
		boolean available = true;
		try {
			CommandExecutor cmdExec = new CommandExecutor(fs, false);
			available = cmdExec.checkExecutable(path, directory,
					prepareEnvironment(gitDir, null, null, null, null));
		} catch (Exception e) {
			available = false;
		}
		return available;
	}

	/**
	 * @param defaultName
	 *            the default tool name
	 * @param userDefinedNames
	 *            the user defined tool names
	 * @param preDefinedNames
	 *            the pre defined tool names
	 * @return the sorted tool names set: first element is default tool name if
	 *         valid, then user defined tool names and then pre defined tool
	 *         names
	 */
	public static Set<String> createSortedToolSet(String defaultName,
			Set<String> userDefinedNames, Set<String> preDefinedNames) {
		Set<String> names = new LinkedHashSet<>();
		if (defaultName != null) {
			// remove defaultName from both sets
			Set<String> namesPredef = new LinkedHashSet<>();
			Set<String> namesUser = new LinkedHashSet<>();
			namesUser.addAll(userDefinedNames);
			namesUser.remove(defaultName);
			namesPredef.addAll(preDefinedNames);
			namesPredef.remove(defaultName);
			// add defaultName as first in set
			names.add(defaultName);
			names.addAll(namesUser);
			names.addAll(namesPredef);
		} else {
			names.addAll(userDefinedNames);
			names.addAll(preDefinedNames);
		}
		return names;
	}

	/**
	 * Provides {@link Optional} with the name of an external tool if specified
	 * in git configuration for a path.
	 *
	 * The formed git configuration results from global rules as well as merged
	 * rules from info and worktree attributes.
	 *
	 * Triggers {@link TreeWalk} until specified path found in the tree.
	 *
	 * @param repository
	 *            target repository to traverse into
	 * @param path
	 *            path to the node in repository to parse git attributes for
	 * @param toolKey
	 *            config key name for the tool
	 * @return attribute value for the given tool key if set
	 * @throws ToolException
	 */
	public static Optional<String> getExternalToolFromAttributes(
			final Repository repository, final String path,
			final String toolKey) throws ToolException {
		try {
			WorkingTreeIterator treeIterator = new FileTreeIterator(repository);
			try (TreeWalk walk = new TreeWalk(repository)) {
				walk.addTree(treeIterator);
				walk.setFilter(new NotIgnoredFilter(0));
				while (walk.next()) {
					String treePath = walk.getPathString();
					if (treePath.equals(path)) {
						Attributes attrs = walk.getAttributes();
						if (attrs.containsKey(toolKey)) {
							return Optional.of(attrs.getValue(toolKey));
						}
					}
					if (walk.isSubtree()) {
						walk.enterSubtree();
					}
				}
				// no external tool specified
				return Optional.empty();
			}

		} catch (RevisionSyntaxException | IOException e) {
			throw new ToolException(e);
		}
	}

}