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 <thomas.wolf@paranor.ch>
This commit is contained in:
Thomas Wolf 2020-12-05 22:01:25 +01:00
parent 5abd8a4feb
commit 41b9159795
8 changed files with 285 additions and 45 deletions

View File

@ -143,6 +143,7 @@ metaVar_s3Region=REGION
metaVar_s3StorageClass=STORAGE-CLASS
metaVar_seconds=SECONDS
metaVar_service=SERVICE
metaVar_tagLocalUser=<GPG key ID>
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

View File

@ -4,7 +4,7 @@
* Copyright (C) 2008, Charles O'Farrell <charleso@charleso.org>
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg.lists@dewire.com>
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
* Copyright (C) 2008, 2020 Shawn O. Pearce <spearce@spearce.org> 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) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2010, 2013 Chris Aniszczyk <caniszczyk@gmail.com> and others
* Copyright (C) 2010, 2020 Chris Aniszczyk <caniszczyk@gmail.com> 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<Ref> getTags() throws Exception {
return db.getRefDatabase().getRefsByPrefix(R_TAGS);
}

View File

@ -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

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2010, 2013 Chris Aniszczyk <caniszczyk@gmail.com> and others
* Copyright (C) 2010, 2020 Chris Aniszczyk <caniszczyk@gmail.com> 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</a>
*/
public class TagCommand extends GitCommand<Ref> {
private RevObject id;
private String name;
@ -64,11 +72,17 @@ public class TagCommand extends GitCommand<Ref> {
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;
/**
* <p>Constructor for TagCommand.</p>
@ -77,6 +91,7 @@ public class TagCommand extends GitCommand<Ref> {
*/
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 ? "<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.
* <p>
* If {@code true}, the tag will be a signed annotated tag.
* </p>
*
* @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.
* <p>
* 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.
* </p>
* <p>
* Note, if none was set or {@code null} is specified a default will be
* obtained from the configuration.
* </p>
* <p>
* If set to a non-{@code null} value, the tag will be a signed annotated
* tag.
* </p>
*
* @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;
}
}

View File

@ -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;

View File

@ -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";

View File

@ -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);
}
}