From 41b9159795d6f64bba6a67ce2f22fe1b7679ea55 Mon Sep 17 00:00:00 2001 From: Thomas Wolf Date: Sat, 5 Dec 2020 22:01:25 +0100 Subject: [PATCH] TagCommand: support signing annotated tags Add the two config constants from C git that can switch on signing of annotated tags. Add them to the GpgConfig, and implement actually signing a tag in TagCommand. The interactions between command line options for "git tag" and config options is a bit murky in C git. There are two config settings for it: * tag.gpgSign is the main option, if set to true, it kicks in if neither -s nor -u are given on the command line. * tag.forceSignAnnotated signs only tags created via "git tag -m", but only if command-line option "-a" is not present. It applies even if tag.gpgSign is set explicitly to false. Giving -s or -u on the command line also forces an annotated tag since lightweight tags cannot be signed. Bug: 386908 Change-Id: Ic8a1a44b5f12f47d5cdf3aae2456c1f6ca9ef057 Signed-off-by: Thomas Wolf --- .../jgit/pgm/internal/CLIText.properties | 7 +- .../src/org/eclipse/jgit/pgm/Tag.java | 32 +++- .../org/eclipse/jgit/api/TagCommandTest.java | 70 +++++-- .../eclipse/jgit/internal/JGitText.properties | 1 - .../src/org/eclipse/jgit/api/TagCommand.java | 178 +++++++++++++++--- .../org/eclipse/jgit/internal/JGitText.java | 1 - .../org/eclipse/jgit/lib/ConfigConstants.java | 16 ++ .../src/org/eclipse/jgit/lib/GpgConfig.java | 25 +++ 8 files changed, 285 insertions(+), 45 deletions(-) diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties index 6112a272e..bf2455283 100644 --- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties +++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/internal/CLIText.properties @@ -143,6 +143,7 @@ metaVar_s3Region=REGION metaVar_s3StorageClass=STORAGE-CLASS metaVar_seconds=SECONDS metaVar_service=SERVICE +metaVar_tagLocalUser= metaVar_treeish=tree-ish metaVar_uriish=uri-ish metaVar_url=URL @@ -421,8 +422,12 @@ usage_sshDriver=Selects the built-in ssh library to use, JSch or Apache MINA ssh usage_symbolicVersionForTheProject=Symbolic version for the project usage_tags=fetch all tags usage_notags=do not fetch tags +usage_tagAnnotated=create an annotated tag, unsigned unless -s or -u are given, or config tag.gpgSign is true usage_tagDelete=delete tag -usage_tagMessage=tag message +usage_tagLocalUser=create a signed annotated tag using the specified GPG key ID +usage_tagMessage=create an annotated tag with the given message, unsigned unless -s or -u are given, or config tag.gpgSign is true, or tar.forceSignAnnotated is true and -a is not given +usage_tagSign=create a signed annotated tag +usage_tagNoSign=suppress signing the tag usage_untrackedFilesMode=show untracked files usage_updateRef=reference to update usage_updateRemoteRefsFromAnotherRepository=Update remote refs from another repository diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java index b408b78f3..4cc62b339 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Tag.java @@ -4,7 +4,7 @@ * Copyright (C) 2008, Charles O'Farrell * Copyright (C) 2008, Robin Rosenberg * Copyright (C) 2008, Robin Rosenberg - * Copyright (C) 2008, Shawn O. Pearce and others + * Copyright (C) 2008, 2020 Shawn O. Pearce 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 @@ -40,8 +40,24 @@ class Tag extends TextBuiltin { @Option(name = "-d", usage = "usage_tagDelete") private boolean delete; + @Option(name = "--annotate", aliases = { + "-a" }, usage = "usage_tagAnnotated") + private boolean annotated; + @Option(name = "-m", metaVar = "metaVar_message", usage = "usage_tagMessage") - private String message = ""; //$NON-NLS-1$ + private String message; + + @Option(name = "--sign", aliases = { "-s" }, forbids = { + "--no-sign" }, usage = "usage_tagSign") + private boolean sign; + + @Option(name = "--no-sign", usage = "usage_tagNoSign", forbids = { + "--sign" }) + private boolean noSign; + + @Option(name = "--local-user", aliases = { + "-u" }, metaVar = "metaVar_tagLocalUser", usage = "usage_tagLocalUser") + private String gpgKeyId; @Argument(index = 0, metaVar = "metaVar_name") private String tagName; @@ -70,6 +86,18 @@ protected void run() { command.setObjectId(walk.parseAny(object)); } } + if (noSign) { + command.setSigned(false); + } else if (sign) { + command.setSigned(true); + } + if (annotated) { + command.setAnnotated(true); + } else if (message == null && !sign && gpgKeyId == null) { + // None of -a, -m, -s, -u given + command.setAnnotated(false); + } + command.setSigningKey(gpgKeyId); try { command.call(); } catch (RefAlreadyExistsException e) { diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/TagCommandTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/TagCommandTest.java index b1c54b9ef..9630474b8 100644 --- a/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/TagCommandTest.java +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/api/TagCommandTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, 2013 Chris Aniszczyk and others + * Copyright (C) 2010, 2020 Chris Aniszczyk 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 @@ -11,6 +11,8 @@ import static org.eclipse.jgit.lib.Constants.R_TAGS; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; @@ -28,6 +30,59 @@ public class TagCommandTest extends RepositoryTestCase { + @Test + public void testTagKind() { + try (Git git = new Git(db)) { + assertTrue(git.tag().isAnnotated()); + assertTrue(git.tag().setSigned(true).isAnnotated()); + assertTrue(git.tag().setSigned(false).isAnnotated()); + assertTrue(git.tag().setSigningKey(null).isAnnotated()); + assertTrue(git.tag().setSigningKey("something").isAnnotated()); + assertTrue(git.tag().setSigned(false).setSigningKey(null) + .isAnnotated()); + assertTrue(git.tag().setSigned(false).setSigningKey("something") + .isAnnotated()); + assertTrue(git.tag().setSigned(true).setSigningKey(null) + .isAnnotated()); + assertTrue(git.tag().setSigned(true).setSigningKey("something") + .isAnnotated()); + assertTrue(git.tag().setAnnotated(true).isAnnotated()); + assertTrue( + git.tag().setAnnotated(true).setSigned(true).isAnnotated()); + assertTrue(git.tag().setAnnotated(true).setSigned(false) + .isAnnotated()); + assertTrue(git.tag().setAnnotated(true).setSigningKey(null) + .isAnnotated()); + assertTrue(git.tag().setAnnotated(true).setSigningKey("something") + .isAnnotated()); + assertTrue(git.tag().setAnnotated(true).setSigned(false) + .setSigningKey(null).isAnnotated()); + assertTrue(git.tag().setAnnotated(true).setSigned(false) + .setSigningKey("something").isAnnotated()); + assertTrue(git.tag().setAnnotated(true).setSigned(true) + .setSigningKey(null).isAnnotated()); + assertTrue(git.tag().setAnnotated(true).setSigned(true) + .setSigningKey("something").isAnnotated()); + assertFalse(git.tag().setAnnotated(false).isAnnotated()); + assertTrue(git.tag().setAnnotated(false).setSigned(true) + .isAnnotated()); + assertFalse(git.tag().setAnnotated(false).setSigned(false) + .isAnnotated()); + assertFalse(git.tag().setAnnotated(false).setSigningKey(null) + .isAnnotated()); + assertTrue(git.tag().setAnnotated(false).setSigningKey("something") + .isAnnotated()); + assertFalse(git.tag().setAnnotated(false).setSigned(false) + .setSigningKey(null).isAnnotated()); + assertTrue(git.tag().setAnnotated(false).setSigned(false) + .setSigningKey("something").isAnnotated()); + assertTrue(git.tag().setAnnotated(false).setSigned(true) + .setSigningKey(null).isAnnotated()); + assertTrue(git.tag().setAnnotated(false).setSigned(true) + .setSigningKey("something").isAnnotated()); + } + } + @Test public void testTaggingOnHead() throws GitAPIException, IOException { try (Git git = new Git(db); @@ -93,19 +148,6 @@ public void testInvalidTagName() throws GitAPIException { } } - @Test - public void testFailureOnSignedTags() throws GitAPIException { - try (Git git = new Git(db)) { - git.commit().setMessage("initial commit").call(); - try { - git.tag().setSigned(true).setName("tag").call(); - fail("We should have failed with an UnsupportedOperationException due to signed tag"); - } catch (UnsupportedOperationException e) { - // should hit here - } - } - } - private List getTags() throws Exception { return db.getRefDatabase().getRefsByPrefix(R_TAGS); } diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 6d15464d5..2b5f929dd 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -618,7 +618,6 @@ shortReadOfBlock=Short read of block. shortReadOfOptionalDIRCExtensionExpectedAnotherBytes=Short read of optional DIRC extension {0}; expected another {1} bytes within the section. shortSkipOfBlock=Short skip of block. signedTagMessageNoLf=A non-empty message of a signed tag must end in LF. -signingNotSupportedOnTag=Signing isn't supported on tag operations yet. signingServiceUnavailable=Signing service is not available similarityScoreMustBeWithinBounds=Similarity score must be between 0 and 100. skipMustBeNonNegative=skip must be >= 0 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 9a328a6ea..c8d4e413f 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/api/TagCommand.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010, 2013 Chris Aniszczyk and others + * Copyright (C) 2010, 2020 Chris Aniszczyk 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 @@ -18,8 +18,13 @@ import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.api.errors.NoHeadException; import org.eclipse.jgit.api.errors.RefAlreadyExistsException; +import org.eclipse.jgit.api.errors.ServiceUnavailableException; +import org.eclipse.jgit.api.errors.UnsupportedSigningFormatException; import org.eclipse.jgit.internal.JGitText; import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.GpgConfig; +import org.eclipse.jgit.lib.GpgObjectSigner; +import org.eclipse.jgit.lib.GpgSigner; import org.eclipse.jgit.lib.ObjectId; import org.eclipse.jgit.lib.ObjectInserter; import org.eclipse.jgit.lib.PersonIdent; @@ -29,8 +34,10 @@ 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; /** * Create/update an annotated tag object or a simple unannotated tag @@ -56,6 +63,7 @@ * >Git documentation about Tag */ public class TagCommand extends GitCommand { + private RevObject id; private String name; @@ -64,11 +72,17 @@ public class TagCommand extends GitCommand { private PersonIdent tagger; - private boolean signed; + private Boolean signed; private boolean forceUpdate; - private boolean annotated = true; + private Boolean annotated; + + private String signingKey; + + private GpgObjectSigner gpgSigner; + + private CredentialsProvider credentialsProvider; /** *

Constructor for TagCommand.

@@ -77,6 +91,7 @@ public class TagCommand extends GitCommand { */ protected TagCommand(Repository repo) { super(repo); + this.credentialsProvider = CredentialsProvider.getDefault(); } /** @@ -108,10 +123,7 @@ public Ref call() throws GitAPIException, ConcurrentRefUpdateException, id = revWalk.parseCommit(objectId); } - if (!annotated) { - if (message != null || tagger != null) - throw new JGitInternalException( - JGitText.get().messageAndTaggerNotAllowedInUnannotatedTags); + if (!isAnnotated()) { return updateTagRef(id, revWalk, name, "SimpleTag[" + name + " : " + id //$NON-NLS-1$ //$NON-NLS-2$ + "]"); //$NON-NLS-1$ @@ -124,6 +136,11 @@ public Ref call() throws GitAPIException, ConcurrentRefUpdateException, newTag.setTagger(tagger); newTag.setObjectId(id); + if (gpgSigner != null) { + gpgSigner.signObject(newTag, signingKey, tagger, + credentialsProvider); + } + // write the tag object try (ObjectInserter inserter = repo.newObjectInserter()) { ObjectId tagId = inserter.insert(newTag); @@ -177,20 +194,60 @@ private Ref updateTagRef(ObjectId tagId, RevWalk revWalk, * * @throws InvalidTagNameException * if the tag name is null or invalid - * @throws UnsupportedOperationException - * if the tag is signed (not supported yet) + * @throws ServiceUnavailableException + * if the tag should be signed but no signer can be found + * @throws UnsupportedSigningFormatException + * if the tag should be signed but {@code gpg.format} is not + * {@link GpgFormat#OPENPGP} */ private void processOptions(RepositoryState state) - throws InvalidTagNameException { - if (tagger == null && annotated) - tagger = new PersonIdent(repo); - if (name == null || !Repository.isValidRefName(Constants.R_TAGS + name)) + throws InvalidTagNameException, ServiceUnavailableException, + UnsupportedSigningFormatException { + if (name == null + || !Repository.isValidRefName(Constants.R_TAGS + name)) { throw new InvalidTagNameException( MessageFormat.format(JGitText.get().tagNameInvalid, name == null ? "" : name)); //$NON-NLS-1$ - if (signed) - throw new UnsupportedOperationException( - JGitText.get().signingNotSupportedOnTag); + } + if (!isAnnotated()) { + if ((message != null && !message.isEmpty()) || tagger != null) { + throw new JGitInternalException(JGitText + .get().messageAndTaggerNotAllowedInUnannotatedTags); + } + } else { + if (tagger == null) { + tagger = new PersonIdent(repo); + } + // Figure out whether to sign. + if (!(Boolean.FALSE.equals(signed) && signingKey == null)) { + GpgConfig gpgConfig = new GpgConfig(repo.getConfig()); + boolean doSign = isSigned() || gpgConfig.isSignAllTags(); + if (!Boolean.TRUE.equals(annotated) && !doSign) { + doSign = gpgConfig.isSignAnnotated(); + } + if (doSign) { + if (signingKey == null) { + signingKey = gpgConfig.getSigningKey(); + } + if (gpgConfig.getKeyFormat() != GpgFormat.OPENPGP) { + throw new UnsupportedSigningFormatException( + JGitText.get().onlyOpenPgpSupportedForSigning); + } + 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() + && !message.endsWith("\n")) { //$NON-NLS-1$ + message += '\n'; + } + } + } + } } /** @@ -238,24 +295,31 @@ public TagCommand setMessage(String message) { } /** - * Whether this tag is signed + * Whether {@link #setSigned(boolean) setSigned(true)} has been called or + * whether a {@link #setSigningKey(String) signing key ID} has been set; + * i.e., whether -s or -u was specified explicitly. * * @return whether the tag is signed */ public boolean isSigned() { - return signed; + return Boolean.TRUE.equals(signed) || signingKey != null; } /** * If set to true the Tag command creates a signed tag object. This - * corresponds to the parameter -s on the command line. + * corresponds to the parameter -s (--sign or --no-sign) on the command + * line. + *

+ * If {@code true}, the tag will be a signed annotated tag. + *

* * @param signed - * a boolean. + * whether to sign * @return {@code this} */ public TagCommand setSigned(boolean signed) { - this.signed = signed; + checkCallable(); + this.signed = Boolean.valueOf(signed); return this; } @@ -268,6 +332,7 @@ public TagCommand setSigned(boolean signed) { * @return {@code this} */ public TagCommand setTagger(PersonIdent tagger) { + checkCallable(); this.tagger = tagger; return this; } @@ -291,14 +356,15 @@ public RevObject getObjectId() { } /** - * Sets the object id of the tag. If the object id is null, the commit - * pointed to from HEAD will be used. + * Sets the object id of the tag. If the object id is {@code null}, the + * commit pointed to from HEAD will be used. * * @param id * a {@link org.eclipse.jgit.revwalk.RevObject} object. * @return {@code this} */ public TagCommand setObjectId(RevObject id) { + checkCallable(); this.id = id; return this; } @@ -321,6 +387,7 @@ public boolean isForceUpdate() { * @return {@code this} */ public TagCommand setForceUpdate(boolean forceUpdate) { + checkCallable(); this.forceUpdate = forceUpdate; return this; } @@ -334,18 +401,77 @@ public TagCommand setForceUpdate(boolean forceUpdate) { * @since 3.0 */ public TagCommand setAnnotated(boolean annotated) { - this.annotated = annotated; + checkCallable(); + this.annotated = Boolean.valueOf(annotated); return this; } /** - * Whether this will create an annotated command + * Whether this will create an annotated tag. * * @return true if this command will create an annotated tag (default is * true) * @since 3.0 */ public boolean isAnnotated() { - return annotated; + boolean setExplicitly = Boolean.TRUE.equals(annotated) || isSigned(); + if (setExplicitly) { + return true; + } + // Annotated at default (not set explicitly) + return annotated == null; } + + /** + * Sets the signing key. + *

+ * Per spec of {@code user.signingKey}: this will be sent to the GPG program + * as is, i.e. can be anything supported by the GPG program. + *

+ *

+ * Note, if none was set or {@code null} is specified a default will be + * obtained from the configuration. + *

+ *

+ * If set to a non-{@code null} value, the tag will be a signed annotated + * tag. + *

+ * + * @param signingKey + * signing key; {@code null} allowed + * @return {@code this} + * @since 5.11 + */ + public TagCommand setSigningKey(String signingKey) { + checkCallable(); + this.signingKey = signingKey; + return this; + } + + /** + * Retrieves the signing key ID. + * + * @return the key ID set, or {@code null} if none is set + * @since 5.11 + */ + public String getSigningKey() { + return signingKey; + } + + /** + * Sets a {@link CredentialsProvider} + * + * @param credentialsProvider + * the provider to use when querying for credentials (eg., during + * signing) + * @return {@code this} + * @since 5.11 + */ + public TagCommand setCredentialsProvider( + CredentialsProvider credentialsProvider) { + checkCallable(); + this.credentialsProvider = credentialsProvider; + return this; + } + } diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index a7daed131..154f32c25 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -646,7 +646,6 @@ public static JGitText get() { /***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes; /***/ public String shortSkipOfBlock; /***/ public String signedTagMessageNoLf; - /***/ public String signingNotSupportedOnTag; /***/ public String signingServiceUnavailable; /***/ public String similarityScoreMustBeWithinBounds; /***/ public String skipMustBeNonNegative; 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 834fff5dd..2587947c3 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ConfigConstants.java @@ -116,14 +116,30 @@ public final class ConfigConstants { */ public static final String CONFIG_COMMIT_SECTION = "commit"; + /** + * The "tag" section + * + * @since 5.11 + */ + public static final String CONFIG_TAG_SECTION = "tag"; + /** * The "gpgSign" key + * * @since 5.2 */ public static final String CONFIG_KEY_GPGSIGN = "gpgSign"; + /** + * The "forceSignAnnotated" key + * + * @since 5.11 + */ + public static final String CONFIG_KEY_FORCE_SIGN_ANNOTATED = "forceSignAnnotated"; + /** * The "hooksPath" key. + * * @since 5.6 */ public static final String CONFIG_KEY_HOOKS_PATH = "hooksPath"; 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 c1527bc47..5b4372973 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgConfig.java @@ -85,4 +85,29 @@ public boolean isSignCommits() { return config.getBoolean(ConfigConstants.CONFIG_COMMIT_SECTION, ConfigConstants.CONFIG_KEY_GPGSIGN, false); } + + /** + * Retrieves the value of git config {@code tag.gpgSign}. + * + * @return the value of {@code tag.gpgSign}; by default {@code false} + * + * @since 5.11 + */ + public boolean isSignAllTags() { + return config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION, + ConfigConstants.CONFIG_KEY_GPGSIGN, false); + } + + /** + * Retrieves the value of git config {@code tag.forceSignAnnotated}. + * + * @return the value of {@code tag.forceSignAnnotated}; by default + * {@code false} + * + * @since 5.11 + */ + public boolean isSignAnnotated() { + return config.getBoolean(ConfigConstants.CONFIG_TAG_SECTION, + ConfigConstants.CONFIG_KEY_FORCE_SIGN_ANNOTATED, false); + } }