Merge "GitHook: make fields outputStream and errorStream private"
diff --git a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
index 449c4a4..f448d5e 100644
--- a/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
+++ b/org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
@@ -34,13 +34,17 @@
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.CanceledException;
 import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.errors.UnsupportedCredentialItem;
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.GpgConfig;
 import org.eclipse.jgit.lib.GpgSignature;
 import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.GpgObjectSigner;
 import org.eclipse.jgit.lib.ObjectBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
 import org.eclipse.jgit.transport.CredentialsProvider;
 import org.eclipse.jgit.util.StringUtils;
 
@@ -70,6 +74,24 @@
 	public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
 			PersonIdent committer, CredentialsProvider credentialsProvider)
 			throws CanceledException {
+		try {
+			return canLocateSigningKey(gpgSigningKey, committer,
+					credentialsProvider, null);
+		} catch (UnsupportedSigningFormatException e) {
+			// Cannot occur with a null config
+			return false;
+		}
+	}
+
+	@Override
+	public boolean canLocateSigningKey(@Nullable String gpgSigningKey,
+			PersonIdent committer, CredentialsProvider credentialsProvider,
+			GpgConfig config)
+			throws CanceledException, UnsupportedSigningFormatException {
+		if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
+			throw new UnsupportedSigningFormatException(
+					JGitText.get().onlyOpenPgpSupportedForSigning);
+		}
 		try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
 				credentialsProvider)) {
 			BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
@@ -101,13 +123,23 @@
 	public void sign(@NonNull CommitBuilder commit,
 			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
 			CredentialsProvider credentialsProvider) throws CanceledException {
-		signObject(commit, gpgSigningKey, committer, credentialsProvider);
+		try {
+			signObject(commit, gpgSigningKey, committer, credentialsProvider,
+					null);
+		} catch (UnsupportedSigningFormatException e) {
+			// Cannot occur with a null config
+		}
 	}
 
 	@Override
 	public void signObject(@NonNull ObjectBuilder object,
 			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
-			CredentialsProvider credentialsProvider) throws CanceledException {
+			CredentialsProvider credentialsProvider, GpgConfig config)
+			throws CanceledException, UnsupportedSigningFormatException {
+		if (config != null && config.getKeyFormat() != GpgFormat.OPENPGP) {
+			throw new UnsupportedSigningFormatException(
+					JGitText.get().onlyOpenPgpSupportedForSigning);
+		}
 		try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
 				credentialsProvider)) {
 			BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
index b4f7175..31f6a31 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/CommitCommand.java
@@ -47,6 +47,7 @@
 import org.eclipse.jgit.lib.FileMode;
 import org.eclipse.jgit.lib.GpgConfig;
 import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
+import org.eclipse.jgit.lib.GpgObjectSigner;
 import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -120,6 +121,8 @@
 
 	private GpgSigner gpgSigner;
 
+	private GpgConfig gpgConfig;
+
 	private CredentialsProvider credentialsProvider;
 
 	/**
@@ -247,8 +250,18 @@
 						throw new ServiceUnavailableException(
 								JGitText.get().signingServiceUnavailable);
 					}
-					gpgSigner.sign(commit, signingKey, committer,
-							credentialsProvider);
+					if (gpgSigner instanceof GpgObjectSigner) {
+						((GpgObjectSigner) gpgSigner).signObject(commit,
+								signingKey, committer, credentialsProvider,
+								gpgConfig);
+					} else {
+						if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) {
+							throw new UnsupportedSigningFormatException(JGitText
+									.get().onlyOpenPgpSupportedForSigning);
+						}
+						gpgSigner.sign(commit, signingKey, committer,
+								credentialsProvider);
+					}
 				}
 
 				ObjectId commitId = odi.insert(commit);
@@ -576,7 +589,9 @@
 			// an explicit message
 			throw new NoMessageException(JGitText.get().commitMessageNotSpecified);
 
-		GpgConfig gpgConfig = new GpgConfig(repo.getConfig());
+		if (gpgConfig == null) {
+			gpgConfig = new GpgConfig(repo.getConfig());
+		}
 		if (signCommit == null) {
 			signCommit = gpgConfig.isSignCommits() ? Boolean.TRUE
 					: Boolean.FALSE;
@@ -585,10 +600,6 @@
 			signingKey = gpgConfig.getSigningKey();
 		}
 		if (gpgSigner == null) {
-			if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) {
-				throw new UnsupportedSigningFormatException(
-						JGitText.get().onlyOpenPgpSupportedForSigning);
-			}
 			gpgSigner = GpgSigner.getDefault();
 		}
 	}
@@ -973,6 +984,36 @@
 	}
 
 	/**
+	 * Sets the {@link GpgSigner} to use if the commit is to be signed.
+	 *
+	 * @param signer
+	 *            to use; if {@code null}, the default signer will be used
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public CommitCommand setGpgSigner(GpgSigner signer) {
+		checkCallable();
+		this.gpgSigner = signer;
+		return this;
+	}
+
+	/**
+	 * Sets an external {@link GpgConfig} to use. Whether it will be used is at
+	 * the discretion of the {@link #setGpgSigner(GpgSigner)}.
+	 *
+	 * @param config
+	 *            to set; if {@code null}, the config will be loaded from the
+	 *            git config of the repository
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public CommitCommand setGpgConfig(GpgConfig config) {
+		checkCallable();
+		this.gpgConfig = config;
+		return this;
+	}
+
+	/**
 	 * Sets a {@link CredentialsProvider}
 	 *
 	 * @param credentialsProvider
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java b/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
index 75f942d..58c18b3 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java
@@ -23,6 +23,7 @@
 import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.GpgConfig;
+import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
 import org.eclipse.jgit.lib.GpgObjectSigner;
 import org.eclipse.jgit.lib.GpgSigner;
 import org.eclipse.jgit.lib.ObjectId;
@@ -34,7 +35,6 @@
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryState;
 import org.eclipse.jgit.lib.TagBuilder;
-import org.eclipse.jgit.lib.GpgConfig.GpgFormat;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.transport.CredentialsProvider;
@@ -80,6 +80,8 @@
 
 	private String signingKey;
 
+	private GpgConfig gpgConfig;
+
 	private GpgObjectSigner gpgSigner;
 
 	private CredentialsProvider credentialsProvider;
@@ -138,7 +140,7 @@
 
 			if (gpgSigner != null) {
 				gpgSigner.signObject(newTag, signingKey, tagger,
-						credentialsProvider);
+						credentialsProvider, gpgConfig);
 			}
 
 			// write the tag object
@@ -228,7 +230,9 @@
 			}
 			// Figure out whether to sign.
 			if (!(Boolean.FALSE.equals(signed) && signingKey == null)) {
-				GpgConfig gpgConfig = new GpgConfig(repo.getConfig());
+				if (gpgConfig == null) {
+					gpgConfig = new GpgConfig(repo.getConfig());
+				}
 				boolean doSign = isSigned() || gpgConfig.isSignAllTags();
 				if (!Boolean.TRUE.equals(annotated) && !doSign) {
 					doSign = gpgConfig.isSignAnnotated();
@@ -237,16 +241,14 @@
 					if (signingKey == null) {
 						signingKey = gpgConfig.getSigningKey();
 					}
-					if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) {
-						throw new UnsupportedSigningFormatException(
-								JGitText.get().onlyOpenPgpSupportedForSigning);
+					if (gpgSigner == null) {
+						GpgSigner signer = GpgSigner.getDefault();
+						if (!(signer instanceof GpgObjectSigner)) {
+							throw new ServiceUnavailableException(
+									JGitText.get().signingServiceUnavailable);
+						}
+						gpgSigner = (GpgObjectSigner) signer;
 					}
-					GpgSigner signer = GpgSigner.getDefault();
-					if (!(signer instanceof GpgObjectSigner)) {
-						throw new ServiceUnavailableException(
-								JGitText.get().signingServiceUnavailable);
-					}
-					gpgSigner = (GpgObjectSigner) signer;
 					// The message of a signed tag must end in a newline because
 					// the signature will be appended.
 					if (message != null && !message.isEmpty()
@@ -332,6 +334,36 @@
 	}
 
 	/**
+	 * Sets the {@link GpgSigner} to use if the commit is to be signed.
+	 *
+	 * @param signer
+	 *            to use; if {@code null}, the default signer will be used
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public TagCommand setGpgSigner(GpgObjectSigner signer) {
+		checkCallable();
+		this.gpgSigner = signer;
+		return this;
+	}
+
+	/**
+	 * Sets an external {@link GpgConfig} to use. Whether it will be used is at
+	 * the discretion of the {@link #setGpgSigner(GpgObjectSigner)}.
+	 *
+	 * @param config
+	 *            to set; if {@code null}, the config will be loaded from the
+	 *            git config of the repository
+	 * @return {@code this}
+	 * @since 5.11
+	 */
+	public TagCommand setGpgConfig(GpgConfig config) {
+		checkCallable();
+		this.gpgConfig = config;
+		return this;
+	}
+
+	/**
 	 * Sets the tagger of the tag. If the tagger is null, a PersonIdent will be
 	 * created from the info in the repository.
 	 *
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
index 954a75c..7381c90 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java
@@ -105,7 +105,15 @@
 	public static final String CONFIG_KEY_FORMAT = "format";
 
 	/**
+	 * The "program" key
+	 *
+	 * @since 5.11
+	 */
+	public static final String CONFIG_KEY_PROGRAM = "program";
+
+	/**
 	 * The "signingKey" key
+	 *
 	 * @since 5.2
 	 */
 	public static final String CONFIG_KEY_SIGNINGKEY = "signingKey";
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
index 5b43729..427a235 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Salesforce. and others
+ * Copyright (C) 2018, 2021 Salesforce and others
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Distribution License v. 1.0 which is available at
@@ -43,16 +43,65 @@
 		}
 	}
 
-	private final Config config;
+	private final GpgFormat keyFormat;
+
+	private final String signingKey;
+
+	private final String program;
+
+	private final boolean signCommits;
+
+	private final boolean signAllTags;
+
+	private final boolean forceAnnotated;
 
 	/**
-	 * Create a new GPG config, which will read configuration from config.
+	 * Create a {@link GpgConfig} with the given parameters and default
+	 * {@code true} for signing commits and {@code false} for tags.
+	 *
+	 * @param keySpec
+	 *            to use
+	 * @param format
+	 *            to use
+	 * @param gpgProgram
+	 *            to use
+	 * @since 5.11
+	 */
+	public GpgConfig(String keySpec, GpgFormat format, String gpgProgram) {
+		keyFormat = format;
+		signingKey = keySpec;
+		program = gpgProgram;
+		signCommits = true;
+		signAllTags = false;
+		forceAnnotated = false;
+	}
+
+	/**
+	 * Create a new GPG config that reads the configuration from config.
 	 *
 	 * @param config
 	 *            the config to read from
 	 */
 	public GpgConfig(Config config) {
-		this.config = config;
+		keyFormat = config.getEnum(GpgFormat.values(),
+				ConfigConstants.CONFIG_GPG_SECTION, null,
+				ConfigConstants.CONFIG_KEY_FORMAT, GpgFormat.OPENPGP);
+		signingKey = config.getString(ConfigConstants.CONFIG_USER_SECTION, null,
+				ConfigConstants.CONFIG_KEY_SIGNINGKEY);
+
+		String exe = config.getString(ConfigConstants.CONFIG_GPG_SECTION,
+				keyFormat.toConfigValue(), ConfigConstants.CONFIG_KEY_PROGRAM);
+		if (exe == null) {
+			exe = config.getString(ConfigConstants.CONFIG_GPG_SECTION, null,
+					ConfigConstants.CONFIG_KEY_PROGRAM);
+		}
+		program = exe;
+		signCommits = config.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION,
+				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
+		signAllTags = config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION,
+				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
+		forceAnnotated = config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION,
+				ConfigConstants.CONFIG_KEY_FORCE_SIGN_ANNOTATED, false);
 	}
 
 	/**
@@ -61,9 +110,19 @@
 	 * @return the {@link org.eclipse.jgit.lib.GpgConfig.GpgFormat}
 	 */
 	public GpgFormat getKeyFormat() {
-		return config.getEnum(GpgFormat.values(),
-				ConfigConstants.CONFIG_GPG_SECTION, null,
-				ConfigConstants.CONFIG_KEY_FORMAT, GpgFormat.OPENPGP);
+		return keyFormat;
+	}
+
+	/**
+	 * Retrieves the value of the configured GPG program to use, as defined by
+	 * gpg.openpgp.program, gpg.x509.program (depending on the defined
+	 * {@link #getKeyFormat() format}), or gpg.program.
+	 *
+	 * @return the program string configured, or {@code null} if none
+	 * @since 5.11
+	 */
+	public String getProgram() {
+		return program;
 	}
 
 	/**
@@ -72,8 +131,7 @@
 	 * @return the value of user.signingKey (may be <code>null</code>)
 	 */
 	public String getSigningKey() {
-		return config.getString(ConfigConstants.CONFIG_USER_SECTION, null,
-				ConfigConstants.CONFIG_KEY_SIGNINGKEY);
+		return signingKey;
 	}
 
 	/**
@@ -82,8 +140,7 @@
 	 * @return the value of commit.gpgSign (defaults to <code>false</code>)
 	 */
 	public boolean isSignCommits() {
-		return config.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION,
-				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
+		return signCommits;
 	}
 
 	/**
@@ -94,8 +151,7 @@
 	 * @since 5.11
 	 */
 	public boolean isSignAllTags() {
-		return config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION,
-				ConfigConstants.CONFIG_KEY_GPGSIGN, false);
+		return signAllTags;
 	}
 
 	/**
@@ -107,7 +163,6 @@
 	 * @since 5.11
 	 */
 	public boolean isSignAnnotated() {
-		return config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION,
-						ConfigConstants.CONFIG_KEY_FORCE_SIGN_ANNOTATED, false);
+		return forceAnnotated;
 	}
 }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
index 6fb7677..074f465 100644
--- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
+++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
@@ -12,6 +12,7 @@
 import org.eclipse.jgit.annotations.NonNull;
 import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException;
 import org.eclipse.jgit.transport.CredentialsProvider;
 
 /**
@@ -48,12 +49,47 @@
 	 * @param credentialsProvider
 	 *            provider to use when querying for signing key credentials (eg.
 	 *            passphrase)
+	 * @param config
+	 *            GPG settings from the git config
 	 * @throws CanceledException
 	 *             when signing was canceled (eg., user aborted when entering
 	 *             passphrase)
+	 * @throws UnsupportedSigningFormatException
+	 *             if a config is given and the wanted key format is not
+	 *             supported
 	 */
 	void signObject(@NonNull ObjectBuilder object,
 			@Nullable String gpgSigningKey, @NonNull PersonIdent committer,
-			CredentialsProvider credentialsProvider) throws CanceledException;
+			CredentialsProvider credentialsProvider, GpgConfig config)
+			throws CanceledException, UnsupportedSigningFormatException;
+
+	/**
+	 * Indicates if a signing key is available for the specified committer
+	 * and/or signing key.
+	 *
+	 * @param gpgSigningKey
+	 *            the signing key to locate (passed as is to the GPG signing
+	 *            tool as is; eg., value of <code>user.signingkey</code>)
+	 * @param committer
+	 *            the signing identity (to help with key lookup in case signing
+	 *            key is not specified)
+	 * @param credentialsProvider
+	 *            provider to use when querying for signing key credentials (eg.
+	 *            passphrase)
+	 * @param config
+	 *            GPG settings from the git config
+	 * @return <code>true</code> if a signing key is available,
+	 *         <code>false</code> otherwise
+	 * @throws CanceledException
+	 *             when signing was canceled (eg., user aborted when entering
+	 *             passphrase)
+	 * @throws UnsupportedSigningFormatException
+	 *             if a config is given and the wanted key format is not
+	 *             supported
+	 */
+	public abstract boolean canLocateSigningKey(@Nullable String gpgSigningKey,
+			@NonNull PersonIdent committer,
+			CredentialsProvider credentialsProvider, GpgConfig config)
+			throws CanceledException, UnsupportedSigningFormatException;
 
 }