]> source.dussan.org Git - jgit.git/commitdiff
Enable GpgSigner to also sign tags 38/173438/3
authorThomas Wolf <thomas.wolf@paranor.ch>
Sat, 5 Dec 2020 20:55:29 +0000 (21:55 +0100)
committerThomas Wolf <thomas.wolf@paranor.ch>
Mon, 7 Dec 2020 08:04:33 +0000 (09:04 +0100)
Factor out a common ObjectBuilder as super class of CommitBuilder
and TagBuilder, and make the GpgSigner work on ObjectBuilder.

In order not to break API, add the new method for signing an
ObjectBuilder in a new interface GpgObjectSigner.

The signature for a tag is just tacked onto the end of the tag
message. The message of a signed tag must end in LF.

Bug: 386908
Change-Id: I5e021e3c927f4051825cd7355b129113b949455e
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
org.eclipse.jgit.gpg.bc/src/org/eclipse/jgit/gpg/bc/internal/BouncyCastleGpgSigner.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/CommitBuilderTest.java
org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java [new file with mode: 0644]
org.eclipse.jgit.test/tst/org/eclipse/jgit/revwalk/RevTagParseTest.java
org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties
org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/CommitBuilder.java
org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java [new file with mode: 0644]
org.eclipse.jgit/src/org/eclipse/jgit/lib/TagBuilder.java
org.eclipse.jgit/src/org/eclipse/jgit/revwalk/RevTag.java

index ea159c547d9c2361b73a30f6e8b38194da803683..449c4a487b17ad907aad72e9f444e87987a6edd4 100644 (file)
@@ -38,6 +38,8 @@ import org.eclipse.jgit.errors.UnsupportedCredentialItem;
 import org.eclipse.jgit.lib.CommitBuilder;
 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.transport.CredentialsProvider;
 import org.eclipse.jgit.util.StringUtils;
@@ -45,7 +47,8 @@ import org.eclipse.jgit.util.StringUtils;
 /**
  * GPG Signer using BouncyCastle library
  */
-public class BouncyCastleGpgSigner extends GpgSigner {
+public class BouncyCastleGpgSigner extends GpgSigner
+               implements GpgObjectSigner {
 
        private static void registerBouncyCastleProviderIfNecessary() {
                if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
@@ -98,6 +101,13 @@ public class BouncyCastleGpgSigner extends GpgSigner {
        public void sign(@NonNull CommitBuilder commit,
                        @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
                        CredentialsProvider credentialsProvider) throws CanceledException {
+               signObject(commit, gpgSigningKey, committer, credentialsProvider);
+       }
+
+       @Override
+       public void signObject(@NonNull ObjectBuilder object,
+                       @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
+                       CredentialsProvider credentialsProvider) throws CanceledException {
                try (BouncyCastleGpgKeyPassphrasePrompt passphrasePrompt = new BouncyCastleGpgKeyPassphrasePrompt(
                                credentialsProvider)) {
                        BouncyCastleGpgKey gpgKey = locateSigningKey(gpgSigningKey,
@@ -158,10 +168,10 @@ public class BouncyCastleGpgSigner extends GpgSigner {
                        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
                        try (BCPGOutputStream out = new BCPGOutputStream(
                                        new ArmoredOutputStream(buffer))) {
-                               signatureGenerator.update(commit.build());
+                               signatureGenerator.update(object.build());
                                signatureGenerator.generate().encode(out);
                        }
-                       commit.setGpgSignature(new GpgSignature(buffer.toByteArray()));
+                       object.setGpgSignature(new GpgSignature(buffer.toByteArray()));
                } catch (PGPException | IOException | NoSuchAlgorithmException
                                | NoSuchProviderException | URISyntaxException e) {
                        throw new JGitInternalException(e.getMessage(), e);
index dee58f9cfcdc2e2568b4447f09117f98859382b1..2f1bada82a3b0daba72e6883ee494723d4ce9faf 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2018, Salesforce. and others
+ * Copyright (C) 2018, 2020 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
@@ -53,7 +53,7 @@ public class CommitBuilderTest {
        private void assertGpgSignatureStringOutcome(String signature,
                        String expectedOutcome) throws IOException {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
-               CommitBuilder.writeGpgSignatureString(signature, out);
+               ObjectBuilder.writeMultiLineHeader(signature, out, true);
                String formatted_signature = new String(out.toByteArray(), US_ASCII);
                assertEquals(expectedOutcome, formatted_signature);
        }
@@ -85,8 +85,8 @@ public class CommitBuilderTest {
                String signature = "Ü Ä";
                IllegalArgumentException e = assertThrows(
                                IllegalArgumentException.class,
-                               () -> CommitBuilder.writeGpgSignatureString(signature,
-                                               new ByteArrayOutputStream()));
+                               () -> ObjectBuilder.writeMultiLineHeader(signature,
+                                               new ByteArrayOutputStream(), true));
                String message = MessageFormat.format(JGitText.get().notASCIIString,
                                signature);
                assertEquals(message, e.getMessage());
diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/lib/TagBuilderTest.java
new file mode 100644 (file)
index 0000000..5786022
--- /dev/null
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import static java.nio.charset.StandardCharsets.US_ASCII;
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThrows;
+
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.junit.Test;
+
+public class TagBuilderTest {
+
+       // @formatter:off
+       private static final String SIGNATURE = "-----BEGIN PGP SIGNATURE-----\n" +
+                       "Version: BCPG v1.60\n" +
+                       "\n" +
+                       "iQEcBAABCAAGBQJb9cVhAAoJEKX+6Axg/6TZeFsH/0CY0WX/z7U8+7S5giFX4wH4\n" +
+                       "opvBwqyt6OX8lgNwTwBGHFNt8LdmDCCmKoq/XwkNi3ARVjLhe3gBcKXNoavvPk2Z\n" +
+                       "gIg5ChevGkU4afWCOMLVEYnkCBGw2+86XhrK1P7gTHEk1Rd+Yv1ZRDJBY+fFO7yz\n" +
+                       "uSBuF5RpEY2sJiIvp27Gub/rY3B5NTR/feO/z+b9oiP/fMUhpRwG5KuWUsn9NPjw\n" +
+                       "3tvbgawYpU/2UnS+xnavMY4t2fjRYjsoxndPLb2MUX8X7vC7FgWLBlmI/rquLZVM\n" +
+                       "IQEKkjnA+lhejjK1rv+ulq4kGZJFKGYWYYhRDwFg5PTkzhudhN2SGUq5Wxq1Eg4=\n" +
+                       "=b9OI\n" +
+                       "-----END PGP SIGNATURE-----";
+
+       // @formatter:on
+
+       private static final String TAGGER_LINE = "A U. Thor <a_u_thor@example.com> 1218123387 +0700";
+
+       private static final PersonIdent TAGGER = RawParseUtils
+                       .parsePersonIdent(TAGGER_LINE);
+
+       @Test
+       public void testTagSimple() throws Exception {
+               TagBuilder t = new TagBuilder();
+               t.setTag("sometag");
+               t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+               t.setEncoding(US_ASCII);
+               t.setMessage("Short message only");
+               t.setTagger(TAGGER);
+               String tag = new String(t.build(), UTF_8);
+               String expected = "object 0000000000000000000000000000000000000000\n"
+                               + "type commit\n" //
+                               + "tag sometag\n" //
+                               + "tagger " + TAGGER_LINE + '\n' //
+                               + "encoding US-ASCII\n" //
+                               + '\n' //
+                               + "Short message only";
+               assertEquals(expected, tag);
+       }
+
+       @Test
+       public void testTagWithSignatureShortMessageEndsInLF() throws Exception {
+               TagBuilder t = new TagBuilder();
+               t.setTag("sometag");
+               t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+               t.setEncoding(US_ASCII);
+               t.setMessage("Short message only\n");
+               t.setTagger(TAGGER);
+               t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+               String tag = new String(t.build(), UTF_8);
+               String expected = "object 0000000000000000000000000000000000000000\n"
+                               + "type commit\n" //
+                               + "tag sometag\n" //
+                               + "tagger " + TAGGER_LINE + '\n' //
+                               + "encoding US-ASCII\n" //
+                               + '\n' //
+                               + "Short message only\n" //
+                               + SIGNATURE + '\n';
+               assertEquals(expected, tag);
+       }
+
+       @Test
+       public void testTagWithSignatureMessageNoLF() {
+               TagBuilder t = new TagBuilder();
+               t.setTag("sometag");
+               t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+               t.setEncoding(US_ASCII);
+               t.setMessage("A message\n\nthat does not end in LF");
+               t.setTagger(TAGGER);
+               t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+               Throwable ex = assertThrows(Throwable.class, t::build);
+               assertEquals(JGitText.get().signedTagMessageNoLf, ex.getMessage());
+       }
+
+       @Test
+       public void testTagWithSignatureNoParagraphsMessage() throws Exception {
+               TagBuilder t = new TagBuilder();
+               t.setTag("sometag");
+               t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+               t.setEncoding(US_ASCII);
+               t.setMessage("A strange\ntag message\n");
+               t.setTagger(TAGGER);
+               t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+               String tag = new String(t.build(), UTF_8);
+               String expected = "object 0000000000000000000000000000000000000000\n"
+                               + "type commit\n" //
+                               + "tag sometag\n" //
+                               + "tagger " + TAGGER_LINE + '\n' //
+                               + "encoding US-ASCII\n" //
+                               + '\n' //
+                               + "A strange\ntag message\n" //
+                               + SIGNATURE + '\n';
+               assertEquals(expected, tag);
+       }
+
+       @Test
+       public void testTagWithSignatureLongMessage() throws Exception {
+               TagBuilder t = new TagBuilder();
+               t.setTag("sometag");
+               t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+               t.setMessage("Short message\n\nFollowed by explanations.\n");
+               t.setTagger(TAGGER);
+               t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+               String tag = new String(t.build(), UTF_8);
+               String expected = "object 0000000000000000000000000000000000000000\n"
+                               + "type commit\n" //
+                               + "tag sometag\n" //
+                               + "tagger " + TAGGER_LINE + '\n' //
+                               + '\n' //
+                               + "Short message\n\nFollowed by explanations.\n" //
+                               + SIGNATURE + '\n';
+               assertEquals(expected, tag);
+       }
+
+       @Test
+       public void testTagWithSignatureEmptyMessage() throws Exception {
+               TagBuilder t = new TagBuilder();
+               t.setTag("sometag");
+               t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+               t.setTagger(TAGGER);
+               t.setMessage("");
+               String emptyMsg = new String(t.build(), UTF_8);
+               t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+               String tag = new String(t.build(), UTF_8);
+               String expected = "object 0000000000000000000000000000000000000000\n"
+                               + "type commit\n" //
+                               + "tag sometag\n" //
+                               + "tagger " + TAGGER_LINE + '\n' //
+                               + '\n';
+               assertEquals(expected, emptyMsg);
+               assertEquals(expected + SIGNATURE + '\n', tag);
+       }
+
+       @Test
+       public void testTagWithSignatureOnly() throws Exception {
+               TagBuilder t = new TagBuilder();
+               t.setTag("sometag");
+               t.setObjectId(ObjectId.zeroId(), Constants.OBJ_COMMIT);
+               t.setTagger(TAGGER);
+               String emptyMsg = new String(t.build(), UTF_8);
+               t.setGpgSignature(new GpgSignature(SIGNATURE.getBytes(US_ASCII)));
+               String tag = new String(t.build(), UTF_8);
+               String expected = "object 0000000000000000000000000000000000000000\n"
+                               + "type commit\n" //
+                               + "tag sometag\n" //
+                               + "tagger " + TAGGER_LINE + '\n' //
+                               + '\n';
+               assertEquals(expected, emptyMsg);
+               assertEquals(expected + SIGNATURE + '\n', tag);
+       }
+
+}
index b92a0726ee4b056b76dcede878cc8cf69b8aabe5..edddc33a28b50171bbee6cc7fc4df51454706dbe 100644 (file)
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2008-2010, Google Inc. and others
+ * Copyright (C) 2008, 2020, Google Inc. 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,7 @@
 package org.eclipse.jgit.revwalk;
 
 import static java.nio.charset.StandardCharsets.ISO_8859_1;
+import static java.nio.charset.StandardCharsets.US_ASCII;
 import static java.nio.charset.StandardCharsets.UTF_8;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
@@ -18,6 +19,7 @@ import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertSame;
 
 import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
 
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.junit.RepositoryTestCase;
@@ -117,6 +119,7 @@ public class RevTagParseTest extends RepositoryTestCase {
                assertNotNull(c.getTagName());
                assertEquals(name, c.getTagName());
                assertEquals("", c.getFullMessage());
+               assertNull(c.getRawGpgSignature());
 
                final PersonIdent cTagger = c.getTaggerIdent();
                assertNotNull(cTagger);
@@ -128,13 +131,12 @@ public class RevTagParseTest extends RepositoryTestCase {
        public void testParseOldStyleNoTagger() throws Exception {
                final ObjectId treeId = id("9788669ad918b6fcce64af8882fc9a81cb6aba67");
                final String name = "v1.2.3.4.5";
-               final String message = "test\n" //
-                               + "\n" //
-                               + "-----BEGIN PGP SIGNATURE-----\n" //
+               final String fakeSignature = "-----BEGIN PGP SIGNATURE-----\n" //
                                + "Version: GnuPG v1.4.1 (GNU/Linux)\n" //
                                + "\n" //
                                + "iD8DBQBC0b9oF3Y\n" //
-                               + "-----END PGP SIGNATURE------n";
+                               + "-----END PGP SIGNATURE-----";
+               final String message = "test\n\n" + fakeSignature + '\n';
 
                final StringBuilder body = new StringBuilder();
 
@@ -167,6 +169,8 @@ public class RevTagParseTest extends RepositoryTestCase {
                assertEquals(name, c.getTagName());
                assertEquals("test", c.getShortMessage());
                assertEquals(message, c.getFullMessage());
+               assertEquals(fakeSignature + '\n',
+                               new String(c.getRawGpgSignature(), US_ASCII));
 
                assertNull(c.getTaggerIdent());
        }
@@ -385,6 +389,108 @@ public class RevTagParseTest extends RepositoryTestCase {
                assertEquals("message\n", t.getFullMessage());
        }
 
+       @Test
+       public void testParse_gpgSignature() throws Exception {
+               final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+                               + "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+                               + "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+                               + "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+                               + "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+                               + "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+                               + "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+                               + "=TClh\n" + "-----END PGP SIGNATURE-----";
+               ByteArrayOutputStream b = new ByteArrayOutputStream();
+               b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
+                               .getBytes(UTF_8));
+               b.write("type tree\n".getBytes(UTF_8));
+               b.write("tag v1.0\n".getBytes(UTF_8));
+               b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+               b.write('\n');
+               b.write("message\n\n".getBytes(UTF_8));
+               b.write(signature.getBytes(US_ASCII));
+               b.write('\n');
+
+               RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+               try (RevWalk rw = new RevWalk(db)) {
+                       t.parseCanonical(rw, b.toByteArray());
+               }
+
+               assertEquals("t", t.getTaggerIdent().getName());
+               assertEquals("message", t.getShortMessage());
+               assertEquals("message\n\n" + signature + '\n', t.getFullMessage());
+               String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
+               assertEquals(signature + '\n', gpgSig);
+       }
+
+       @Test
+       public void testParse_gpgSignature2() throws Exception {
+               final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+                               + "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+                               + "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+                               + "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+                               + "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+                               + "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+                               + "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+                               + "=TClh\n" + "-----END PGP SIGNATURE-----";
+               ByteArrayOutputStream b = new ByteArrayOutputStream();
+               b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
+                               .getBytes(UTF_8));
+               b.write("type tree\n".getBytes(UTF_8));
+               b.write("tag v1.0\n".getBytes(UTF_8));
+               b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+               b.write('\n');
+               String message = "message\n\n" + signature.replace("xXXy", "aAAb")
+                               + '\n';
+               b.write(message.getBytes(UTF_8));
+               b.write(signature.getBytes(US_ASCII));
+               b.write('\n');
+
+               RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+               try (RevWalk rw = new RevWalk(db)) {
+                       t.parseCanonical(rw, b.toByteArray());
+               }
+
+               assertEquals("t", t.getTaggerIdent().getName());
+               assertEquals("message", t.getShortMessage());
+               assertEquals(message + signature + '\n', t.getFullMessage());
+               String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
+               assertEquals(signature + '\n', gpgSig);
+       }
+
+       @Test
+       public void testParse_gpgSignature3() throws Exception {
+               final String signature = "-----BEGIN PGP SIGNATURE-----\n\n"
+                               + "wsBcBAABCAAQBQJbGB4pCRBK7hj4Ov3rIwAAdHIIAENrvz23867ZgqrmyPemBEZP\n"
+                               + "U24B1Tlq/DWvce2buaxmbNQngKZ0pv2s8VMc11916WfTIC9EKvioatmpjduWvhqj\n"
+                               + "znQTFyiMor30pyYsfrqFuQZvqBW01o8GEWqLg8zjf9Rf0R3LlOEw86aT8CdHRlm6\n"
+                               + "wlb22xb8qoX4RB+LYfz7MhK5F+yLOPXZdJnAVbuyoMGRnDpwdzjL5Hj671+XJxN5\n"
+                               + "SasRdhxkkfw/ZnHxaKEc4juMz8Nziz27elRwhOQqlTYoXNJnsV//wy5Losd7aKi1\n"
+                               + "xXXyUpndEOmT0CIcKHrN/kbYoVL28OJaxoBuva3WYQaRrzEe3X02NMxZe9gkSqA=\n"
+                               + "=TClh\n" + "-----END PGP SIGNATURE-----";
+               ByteArrayOutputStream b = new ByteArrayOutputStream();
+               b.write("object 9788669ad918b6fcce64af8882fc9a81cb6aba67\n"
+                               .getBytes(UTF_8));
+               b.write("type tree\n".getBytes(UTF_8));
+               b.write("tag v1.0\n".getBytes(UTF_8));
+               b.write("tagger t <t@example.com> 1218123387 +0700\n".getBytes(UTF_8));
+               b.write('\n');
+               String message = "message\n\n-----BEGIN PGP SIGNATURE-----\n";
+               b.write(message.getBytes(UTF_8));
+               b.write(signature.getBytes(US_ASCII));
+               b.write('\n');
+
+               RevTag t = new RevTag(id("9473095c4cb2f12aefe1db8a355fe3fafba42f67"));
+               try (RevWalk rw = new RevWalk(db)) {
+                       t.parseCanonical(rw, b.toByteArray());
+               }
+
+               assertEquals("t", t.getTaggerIdent().getName());
+               assertEquals("message", t.getShortMessage());
+               assertEquals(message + signature + '\n', t.getFullMessage());
+               String gpgSig = new String(t.getRawGpgSignature(), UTF_8);
+               assertEquals(signature + '\n', gpgSig);
+       }
+
        @Test
        public void testParse_NoMessage() throws Exception {
                final String msg = "";
@@ -447,7 +553,8 @@ public class RevTagParseTest extends RepositoryTestCase {
        }
 
        @Test
-       public void testParse_PublicParseMethod() throws CorruptObjectException {
+       public void testParse_PublicParseMethod()
+                       throws CorruptObjectException, UnsupportedEncodingException {
                TagBuilder src = new TagBuilder();
                try (ObjectInserter.Formatter fmt = new ObjectInserter.Formatter()) {
                        src.setObjectId(fmt.idFor(Constants.OBJ_TREE, new byte[] {}),
index 12902b9004cb16d938ce995d0b37d7e47a1cbfc9..6d15464d5a5d1a64d439ef88661efc97758ab117 100644 (file)
@@ -617,6 +617,7 @@ shortCompressedStreamAt=Short compressed stream at {0}
 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.
index 892657d5d3d13320dd313b093d4c27031ae7df27..a7daed1318624d72314a4c6fc3adc9c01ab80f90 100644 (file)
@@ -645,6 +645,7 @@ public class JGitText extends TranslationBundle {
        /***/ public String shortReadOfBlock;
        /***/ public String shortReadOfOptionalDIRCExtensionExpectedAnotherBytes;
        /***/ public String shortSkipOfBlock;
+       /***/ public String signedTagMessageNoLf;
        /***/ public String signingNotSupportedOnTag;
        /***/ public String signingServiceUnavailable;
        /***/ public String similarityScoreMustBeWithinBounds;
index 4f93fda49fec46fbc1465050a85195a6c070afbd..1665f051e9250f1c337f7bbeb4708fca63493776 100644 (file)
@@ -1,7 +1,7 @@
 /*
  * Copyright (C) 2007, Dave Watson <dwatson@mimvista.com>
- * Copyright (C) 2006-2007, Robin Rosenberg <robin.rosenberg@dewire.com>
- * Copyright (C) 2006-2007, Shawn O. Pearce <spearce@spearce.org> and others
+ * Copyright (C) 20062007, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 2006, 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
@@ -16,14 +16,11 @@ import static java.nio.charset.StandardCharsets.UTF_8;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
-import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.UnsupportedEncodingException;
 import java.nio.charset.Charset;
-import java.text.MessageFormat;
 import java.util.List;
 
-import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.util.References;
 
 /**
@@ -37,7 +34,7 @@ import org.eclipse.jgit.util.References;
  * and obtain a {@link org.eclipse.jgit.revwalk.RevCommit} instance by calling
  * {@link org.eclipse.jgit.revwalk.RevWalk#parseCommit(AnyObjectId)}.
  */
-public class CommitBuilder {
+public class CommitBuilder extends ObjectBuilder {
        private static final ObjectId[] EMPTY_OBJECTID_LIST = new ObjectId[0];
 
        private static final byte[] htree = Constants.encodeASCII("tree"); //$NON-NLS-1$
@@ -50,28 +47,17 @@ public class CommitBuilder {
 
        private static final byte[] hgpgsig = Constants.encodeASCII("gpgsig"); //$NON-NLS-1$
 
-       private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$
-
        private ObjectId treeId;
 
        private ObjectId[] parentIds;
 
-       private PersonIdent author;
-
        private PersonIdent committer;
 
-       private GpgSignature gpgSignature;
-
-       private String message;
-
-       private Charset encoding;
-
        /**
         * Initialize an empty commit.
         */
        public CommitBuilder() {
                parentIds = EMPTY_OBJECTID_LIST;
-               encoding = UTF_8;
        }
 
        /**
@@ -98,8 +84,9 @@ public class CommitBuilder {
         *
         * @return the author of this commit (who wrote it).
         */
+       @Override
        public PersonIdent getAuthor() {
-               return author;
+               return super.getAuthor();
        }
 
        /**
@@ -108,8 +95,9 @@ public class CommitBuilder {
         * @param newAuthor
         *            the new author. Should not be null.
         */
+       @Override
        public void setAuthor(PersonIdent newAuthor) {
-               author = newAuthor;
+               super.setAuthor(newAuthor);
        }
 
        /**
@@ -131,38 +119,6 @@ public class CommitBuilder {
                committer = newCommitter;
        }
 
-       /**
-        * Set the GPG signature of this commit.
-        * <p>
-        * Note, the signature set here will change the payload of the commit, i.e.
-        * the output of {@link #build()} will include the signature. Thus, the
-        * typical flow will be:
-        * <ol>
-        * <li>call {@link #build()} without a signature set to obtain payload</li>
-        * <li>create {@link GpgSignature} from payload</li>
-        * <li>set {@link GpgSignature}</li>
-        * </ol>
-        * </p>
-        *
-        * @param newSignature
-        *            the signature to set or <code>null</code> to unset
-        * @since 5.3
-        */
-       public void setGpgSignature(GpgSignature newSignature) {
-               gpgSignature = newSignature;
-       }
-
-       /**
-        * Get the GPG signature of this commit.
-        *
-        * @return the GPG signature of this commit, maybe <code>null</code> if the
-        *         commit is not to be signed
-        * @since 5.3
-        */
-       public GpgSignature getGpgSignature() {
-               return gpgSignature;
-       }
-
        /**
         * Get the ancestors of this commit.
         *
@@ -238,25 +194,6 @@ public class CommitBuilder {
                }
        }
 
-       /**
-        * Get the complete commit message.
-        *
-        * @return the complete commit message.
-        */
-       public String getMessage() {
-               return message;
-       }
-
-       /**
-        * Set the commit message.
-        *
-        * @param newMessage
-        *            the commit message. Should not be null.
-        */
-       public void setMessage(String newMessage) {
-               message = newMessage;
-       }
-
        /**
         * Set the encoding for the commit information.
         *
@@ -267,37 +204,10 @@ public class CommitBuilder {
         */
        @Deprecated
        public void setEncoding(String encodingName) {
-               encoding = Charset.forName(encodingName);
-       }
-
-       /**
-        * Set the encoding for the commit information.
-        *
-        * @param enc
-        *            the encoding to use.
-        */
-       public void setEncoding(Charset enc) {
-               encoding = enc;
+               setEncoding(Charset.forName(encodingName));
        }
 
-       /**
-        * Get the encoding that should be used for the commit message text.
-        *
-        * @return the encoding that should be used for the commit message text.
-        */
-       public Charset getEncoding() {
-               return encoding;
-       }
-
-       /**
-        * Format this builder's state as a commit object.
-        *
-        * @return this object in the canonical commit format, suitable for storage
-        *         in a repository.
-        * @throws java.io.UnsupportedEncodingException
-        *             the encoding specified by {@link #getEncoding()} is not
-        *             supported by this Java runtime.
-        */
+       @Override
        public byte[] build() throws UnsupportedEncodingException {
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                OutputStreamWriter w = new OutputStreamWriter(os, getEncoding());
@@ -326,19 +236,16 @@ public class CommitBuilder {
                        w.flush();
                        os.write('\n');
 
-                       if (getGpgSignature() != null) {
+                       GpgSignature signature = getGpgSignature();
+                       if (signature != null) {
                                os.write(hgpgsig);
                                os.write(' ');
-                               writeGpgSignatureString(getGpgSignature().toExternalString(), os);
+                               writeMultiLineHeader(signature.toExternalString(), os,
+                                               true);
                                os.write('\n');
                        }
 
-                       if (!References.isSameObject(getEncoding(), UTF_8)) {
-                               os.write(hencoding);
-                               os.write(' ');
-                               os.write(Constants.encodeASCII(getEncoding().name()));
-                               os.write('\n');
-                       }
+                       writeEncoding(getEncoding(), os);
 
                        os.write('\n');
 
@@ -355,58 +262,6 @@ public class CommitBuilder {
                return os.toByteArray();
        }
 
-       /**
-        * Writes signature to output as per <a href=
-        * "https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L66,L89">gpgsig
-        * header</a>.
-        * <p>
-        * CRLF and CR will be sanitized to LF and signature will have a hanging
-        * indent of one space starting with line two. A trailing line break is
-        * <em>not</em> written; the caller is supposed to terminate the GPG
-        * signature header by writing a single newline.
-        * </p>
-        *
-        * @param in
-        *            signature string with line breaks
-        * @param out
-        *            output stream
-        * @throws IOException
-        *             thrown by the output stream
-        * @throws IllegalArgumentException
-        *             if the signature string contains non 7-bit ASCII chars
-        */
-       static void writeGpgSignatureString(String in, OutputStream out)
-                       throws IOException, IllegalArgumentException {
-               int length = in.length();
-               for (int i = 0; i < length; ++i) {
-                       char ch = in.charAt(i);
-                       switch (ch) {
-                       case '\r':
-                               if (i + 1 < length && in.charAt(i + 1) == '\n') {
-                                       ++i;
-                               }
-                               if (i + 1 < length) {
-                                       out.write('\n');
-                                       out.write(' ');
-                               }
-                               break;
-                       case '\n':
-                               if (i + 1 < length) {
-                                       out.write('\n');
-                                       out.write(' ');
-                               }
-                               break;
-                       default:
-                               // sanity check
-                               if (ch > 127)
-                                       throw new IllegalArgumentException(MessageFormat
-                                                       .format(JGitText.get().notASCIIString, in));
-                               out.write(ch);
-                               break;
-                       }
-               }
-       }
-
        /**
         * Format this builder's state as a commit object.
         *
@@ -439,7 +294,7 @@ public class CommitBuilder {
                }
 
                r.append("author ");
-               r.append(author != null ? author.toString() : "NOT_SET");
+               r.append(getAuthor() != null ? getAuthor().toString() : "NOT_SET");
                r.append("\n");
 
                r.append("committer ");
@@ -447,17 +302,20 @@ public class CommitBuilder {
                r.append("\n");
 
                r.append("gpgSignature ");
-               r.append(gpgSignature != null ? gpgSignature.toString() : "NOT_SET");
+               GpgSignature signature = getGpgSignature();
+               r.append(signature != null ? signature.toString()
+                               : "NOT_SET");
                r.append("\n");
 
-               if (encoding != null && !References.isSameObject(encoding, UTF_8)) {
+               Charset encoding = getEncoding();
+               if (!References.isSameObject(encoding, UTF_8)) {
                        r.append("encoding ");
                        r.append(encoding.name());
                        r.append("\n");
                }
 
                r.append("\n");
-               r.append(message != null ? message : "");
+               r.append(getMessage() != null ? getMessage() : "");
                r.append("}");
                return r.toString();
        }
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/GpgObjectSigner.java
new file mode 100644 (file)
index 0000000..6fb7677
--- /dev/null
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2020 Thomas Wolf <thomas.wolf@paranor.ch> 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.api.errors.CanceledException;
+import org.eclipse.jgit.transport.CredentialsProvider;
+
+/**
+ * Creates GPG signatures for Git objects.
+ *
+ * @since 5.11
+ */
+public interface GpgObjectSigner {
+
+       /**
+        * Signs the specified object.
+        *
+        * <p>
+        * Implementors should obtain the payload for signing from the specified
+        * object via {@link ObjectBuilder#build()} and create a proper
+        * {@link GpgSignature}. The generated signature must be set on the
+        * specified {@code object} (see
+        * {@link ObjectBuilder#setGpgSignature(GpgSignature)}).
+        * </p>
+        * <p>
+        * Any existing signature on the object must be discarded prior obtaining
+        * the payload via {@link ObjectBuilder#build()}.
+        * </p>
+        *
+        * @param object
+        *            the object to sign (must not be {@code null} and must be
+        *            complete to allow proper calculation of payload)
+        * @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)
+        * @throws CanceledException
+        *             when signing was canceled (eg., user aborted when entering
+        *             passphrase)
+        */
+       void signObject(@NonNull ObjectBuilder object,
+                       @Nullable String gpgSigningKey, @NonNull PersonIdent committer,
+                       CredentialsProvider credentialsProvider) throws CanceledException;
+
+}
diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java b/org.eclipse.jgit/src/org/eclipse/jgit/lib/ObjectBuilder.java
new file mode 100644 (file)
index 0000000..4b7054f
--- /dev/null
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2020, Thomas Wolf <thomas.wolf@paranor.ch> 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
+ * https://www.eclipse.org/org/documents/edl-v10.php.
+ *
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+package org.eclipse.jgit.lib;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.text.MessageFormat;
+import java.util.Objects;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.annotations.Nullable;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.util.References;
+
+/**
+ * Common base class for {@link CommitBuilder} and {@link TagBuilder}.
+ *
+ * @since 5.11
+ */
+public abstract class ObjectBuilder {
+
+       /** Byte representation of "encoding". */
+       private static final byte[] hencoding = Constants.encodeASCII("encoding"); //$NON-NLS-1$
+
+       private PersonIdent author;
+
+       private GpgSignature gpgSignature;
+
+       private String message;
+
+       private Charset encoding = StandardCharsets.UTF_8;
+
+       /**
+        * Retrieves the author of this object.
+        *
+        * @return the author of this object, or {@code null} if not set yet
+        */
+       protected PersonIdent getAuthor() {
+               return author;
+       }
+
+       /**
+        * Sets the author (name, email address, and date) of this object.
+        *
+        * @param newAuthor
+        *            the new author, must be non-{@code null}
+        */
+       protected void setAuthor(PersonIdent newAuthor) {
+               author = Objects.requireNonNull(newAuthor);
+       }
+
+       /**
+        * Sets the GPG signature of this object.
+        * <p>
+        * Note, the signature set here will change the payload of the object, i.e.
+        * the output of {@link #build()} will include the signature. Thus, the
+        * typical flow will be:
+        * <ol>
+        * <li>call {@link #build()} without a signature set to obtain payload</li>
+        * <li>create {@link GpgSignature} from payload</li>
+        * <li>set {@link GpgSignature}</li>
+        * </ol>
+        * </p>
+        *
+        * @param gpgSignature
+        *            the signature to set or {@code null} to unset
+        * @since 5.3
+        */
+       public void setGpgSignature(@Nullable GpgSignature gpgSignature) {
+               this.gpgSignature = gpgSignature;
+       }
+
+       /**
+        * Retrieves the GPG signature of this object.
+        *
+        * @return the GPG signature of this object, or {@code null} if the object
+        *         is not signed
+        * @since 5.3
+        */
+       @Nullable
+       public GpgSignature getGpgSignature() {
+               return gpgSignature;
+       }
+
+       /**
+        * Retrieves the complete message of the object.
+        *
+        * @return the complete message; can be {@code null}.
+        */
+       @Nullable
+       public String getMessage() {
+               return message;
+       }
+
+       /**
+        * Sets the message (commit message, or message of an annotated tag).
+        *
+        * @param message
+        *            the message.
+        */
+       public void setMessage(@Nullable String message) {
+               this.message = message;
+       }
+
+       /**
+        * Retrieves the encoding that should be used for the message text.
+        *
+        * @return the encoding that should be used for the message text.
+        */
+       @NonNull
+       public Charset getEncoding() {
+               return encoding;
+       }
+
+       /**
+        * Sets the encoding for the object message.
+        *
+        * @param encoding
+        *            the encoding to use.
+        */
+       public void setEncoding(@NonNull Charset encoding) {
+               this.encoding = encoding;
+       }
+
+       /**
+        * Format this builder's state as a git object.
+        *
+        * @return this object in the canonical git format, suitable for storage in
+        *         a repository.
+        * @throws java.io.UnsupportedEncodingException
+        *             the encoding specified by {@link #getEncoding()} is not
+        *             supported by this Java runtime.
+        */
+       @NonNull
+       public abstract byte[] build() throws UnsupportedEncodingException;
+
+       /**
+        * Writes signature to output as per <a href=
+        * "https://github.com/git/git/blob/master/Documentation/technical/signature-format.txt#L66,L89">gpgsig
+        * header</a>.
+        * <p>
+        * CRLF and CR will be sanitized to LF and signature will have a hanging
+        * indent of one space starting with line two. A trailing line break is
+        * <em>not</em> written; the caller is supposed to terminate the GPG
+        * signature header by writing a single newline.
+        * </p>
+        *
+        * @param in
+        *            signature string with line breaks
+        * @param out
+        *            output stream
+        * @param enforceAscii
+        *            whether to throw {@link IllegalArgumentException} if non-ASCII
+        *            characters are encountered
+        * @throws IOException
+        *             thrown by the output stream
+        * @throws IllegalArgumentException
+        *             if the signature string contains non 7-bit ASCII chars and
+        *             {@code enforceAscii == true}
+        */
+       static void writeMultiLineHeader(@NonNull String in,
+                       @NonNull OutputStream out, boolean enforceAscii)
+                       throws IOException, IllegalArgumentException {
+               int length = in.length();
+               for (int i = 0; i < length; ++i) {
+                       char ch = in.charAt(i);
+                       switch (ch) {
+                       case '\r':
+                               if (i + 1 < length && in.charAt(i + 1) == '\n') {
+                                       ++i;
+                               }
+                               if (i + 1 < length) {
+                                       out.write('\n');
+                                       out.write(' ');
+                               }
+                               break;
+                       case '\n':
+                               if (i + 1 < length) {
+                                       out.write('\n');
+                                       out.write(' ');
+                               }
+                               break;
+                       default:
+                               // sanity check
+                               if (ch > 127 && enforceAscii)
+                                       throw new IllegalArgumentException(MessageFormat
+                                                       .format(JGitText.get().notASCIIString, in));
+                               out.write(ch);
+                               break;
+                       }
+               }
+       }
+
+       /**
+        * Writes an "encoding" header.
+        *
+        * @param encoding
+        *            to write
+        * @param out
+        *            to write to
+        * @throws IOException
+        *             if writing fails
+        */
+       static void writeEncoding(@NonNull Charset encoding,
+                       @NonNull OutputStream out) throws IOException {
+               if (!References.isSameObject(encoding, UTF_8)) {
+                       out.write(hencoding);
+                       out.write(' ');
+                       out.write(Constants.encodeASCII(encoding.name()));
+                       out.write('\n');
+               }
+       }
+}
index 71f01150c935c40fa37df547620e8f33fde6fd4c..facb4a54beaa1fc9139a6339f8320bae582c92ee 100644 (file)
@@ -1,7 +1,7 @@
 /*
- * Copyright (C) 2006-2008, Robin Rosenberg <robin.rosenberg@dewire.com>
+ * Copyright (C) 20062008, Robin Rosenberg <robin.rosenberg@dewire.com>
  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
- * Copyright (C) 2010, 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
@@ -17,8 +17,13 @@ import static java.nio.charset.StandardCharsets.UTF_8;
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStreamWriter;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
 
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.internal.JGitText;
 import org.eclipse.jgit.revwalk.RevObject;
+import org.eclipse.jgit.util.References;
 
 /**
  * Mutable builder to construct an annotated tag recording a project state.
@@ -30,17 +35,22 @@ import org.eclipse.jgit.revwalk.RevObject;
  * and obtain a {@link org.eclipse.jgit.revwalk.RevTag} instance by calling
  * {@link org.eclipse.jgit.revwalk.RevWalk#parseTag(AnyObjectId)}.
  */
-public class TagBuilder {
+public class TagBuilder extends ObjectBuilder {
+
+       private static final byte[] hobject = Constants.encodeASCII("object"); //$NON-NLS-1$
+
+       private static final byte[] htype = Constants.encodeASCII("type"); //$NON-NLS-1$
+
+       private static final byte[] htag = Constants.encodeASCII("tag"); //$NON-NLS-1$
+
+       private static final byte[] htagger = Constants.encodeASCII("tagger"); //$NON-NLS-1$
+
        private ObjectId object;
 
        private int type = Constants.OBJ_BAD;
 
        private String tag;
 
-       private PersonIdent tagger;
-
-       private String message;
-
        /**
         * Get the type of object this tag refers to.
         *
@@ -109,7 +119,7 @@ public class TagBuilder {
         * @return creator of this tag. May be null.
         */
        public PersonIdent getTagger() {
-               return tagger;
+               return getAuthor();
        }
 
        /**
@@ -119,26 +129,7 @@ public class TagBuilder {
         *            the creator. May be null.
         */
        public void setTagger(PersonIdent taggerIdent) {
-               tagger = taggerIdent;
-       }
-
-       /**
-        * Get the complete commit message.
-        *
-        * @return the complete commit message.
-        */
-       public String getMessage() {
-               return message;
-       }
-
-       /**
-        * Set the tag's message.
-        *
-        * @param newMessage
-        *            the tag's message.
-        */
-       public void setMessage(String newMessage) {
-               message = newMessage;
+               setAuthor(taggerIdent);
        }
 
        /**
@@ -147,31 +138,65 @@ public class TagBuilder {
         * @return this object in the canonical annotated tag format, suitable for
         *         storage in a repository.
         */
-       public byte[] build() {
+       @Override
+       public byte[] build() throws UnsupportedEncodingException {
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                try (OutputStreamWriter w = new OutputStreamWriter(os,
-                               UTF_8)) {
-                       w.write("object "); //$NON-NLS-1$
-                       getObjectId().copyTo(w);
-                       w.write('\n');
+                               getEncoding())) {
 
-                       w.write("type "); //$NON-NLS-1$
-                       w.write(Constants.typeString(getObjectType()));
-                       w.write("\n"); //$NON-NLS-1$
+                       os.write(hobject);
+                       os.write(' ');
+                       getObjectId().copyTo(os);
+                       os.write('\n');
 
-                       w.write("tag "); //$NON-NLS-1$
+                       os.write(htype);
+                       os.write(' ');
+                       os.write(Constants
+                                       .encodeASCII(Constants.typeString(getObjectType())));
+                       os.write('\n');
+
+                       os.write(htag);
+                       os.write(' ');
                        w.write(getTag());
-                       w.write("\n"); //$NON-NLS-1$
+                       w.flush();
+                       os.write('\n');
 
                        if (getTagger() != null) {
-                               w.write("tagger "); //$NON-NLS-1$
+                               os.write(htagger);
+                               os.write(' ');
                                w.write(getTagger().toExternalString());
-                               w.write('\n');
+                               w.flush();
+                               os.write('\n');
+                       }
+
+                       writeEncoding(getEncoding(), os);
+
+                       os.write('\n');
+                       String msg = getMessage();
+                       if (msg != null) {
+                               w.write(msg);
+                               w.flush();
                        }
 
-                       w.write('\n');
-                       if (getMessage() != null)
-                               w.write(getMessage());
+                       GpgSignature signature = getGpgSignature();
+                       if (signature != null) {
+                               if (msg != null && !msg.isEmpty() && !msg.endsWith("\n")) { //$NON-NLS-1$
+                                       // If signed, the message *must* end with a linefeed
+                                       // character, otherwise signature verification will fail.
+                                       // (The signature will have been computed over the payload
+                                       // containing the message without LF, but will be verified
+                                       // against a payload with the LF.) The signature must start
+                                       // on a new line.
+                                       throw new JGitInternalException(
+                                                       JGitText.get().signedTagMessageNoLf);
+                               }
+                               String externalForm = signature.toExternalString();
+                               w.write(externalForm);
+                               w.flush();
+                               if (!externalForm.endsWith("\n")) { //$NON-NLS-1$
+                                       os.write('\n');
+                               }
+                       }
                } catch (IOException err) {
                        // This should never occur, the only way to get it above is
                        // for the ByteArrayOutputStream to throw, but it doesn't.
@@ -185,10 +210,17 @@ public class TagBuilder {
         * Format this builder's state as an annotated tag object.
         *
         * @return this object in the canonical annotated tag format, suitable for
-        *         storage in a repository.
+        *         storage in a repository, or {@code null} if the tag cannot be
+        *         encoded
+        * @deprecated since 5.11; use {@link #build()} instead
         */
+       @Deprecated
        public byte[] toByteArray() {
-               return build();
+               try {
+                       return build();
+               } catch (UnsupportedEncodingException e) {
+                       return null;
+               }
        }
 
        /** {@inheritDoc} */
@@ -211,14 +243,23 @@ public class TagBuilder {
                r.append(tag != null ? tag : "NOT_SET");
                r.append("\n");
 
-               if (tagger != null) {
+               if (getTagger() != null) {
                        r.append("tagger ");
-                       r.append(tagger);
+                       r.append(getTagger());
+                       r.append("\n");
+               }
+
+               Charset encoding = getEncoding();
+               if (!References.isSameObject(encoding, UTF_8)) {
+                       r.append("encoding ");
+                       r.append(encoding.name());
                        r.append("\n");
                }
 
                r.append("\n");
-               r.append(message != null ? message : "");
+               r.append(getMessage() != null ? getMessage() : "");
+               GpgSignature signature = getGpgSignature();
+               r.append(signature != null ? signature.toExternalString() : "");
                r.append("}");
                return r.toString();
        }
index cac257199f9deda24a2e15352b7097d7b45c9d15..3bcdfafea75e9e1c4affc4cc5334f1788e87b63a 100644 (file)
@@ -18,7 +18,9 @@ import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.charset.IllegalCharsetNameException;
 import java.nio.charset.UnsupportedCharsetException;
+import java.util.Arrays;
 
+import org.eclipse.jgit.annotations.Nullable;
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -35,6 +37,10 @@ import org.eclipse.jgit.util.StringUtils;
  * An annotated tag.
  */
 public class RevTag extends RevObject {
+
+       private static final byte[] hSignature = Constants
+                       .encodeASCII("-----BEGIN PGP SIGNATURE-----"); //$NON-NLS-1$
+
        /**
         * Parse an annotated tag from its canonical format.
         *
@@ -171,6 +177,62 @@ public class RevTag extends RevObject {
                return RawParseUtils.parsePersonIdent(raw, nameB);
        }
 
+       private static int nextStart(byte[] prefix, byte[] buffer, int from) {
+               int stop = buffer.length - prefix.length + 1;
+               int ptr = from;
+               if (ptr > 0) {
+                       ptr = RawParseUtils.nextLF(buffer, ptr - 1);
+               }
+               while (ptr < stop) {
+                       int lineStart = ptr;
+                       boolean found = true;
+                       for (byte element : prefix) {
+                               if (element != buffer[ptr++]) {
+                                       found = false;
+                                       break;
+                               }
+                       }
+                       if (found) {
+                               return lineStart;
+                       }
+                       do {
+                               ptr = RawParseUtils.nextLF(buffer, ptr);
+                       } while (ptr < stop && buffer[ptr] == '\n');
+               }
+               return -1;
+       }
+
+       /**
+        * Parse the GPG signature from the raw buffer.
+        *
+        * @return contents of the GPG signature; {@code null} if the tag was not
+        *         signed.
+        * @since 5.11
+        */
+       @Nullable
+       public final byte[] getRawGpgSignature() {
+               byte[] raw = buffer;
+               int msgB = RawParseUtils.tagMessage(raw, 0);
+               if (msgB < 0) {
+                       return null;
+               }
+               // Find the last signature start and return the rest
+               int start = nextStart(hSignature, raw, msgB);
+               if (start < 0) {
+                       return null;
+               }
+               int next = RawParseUtils.nextLF(raw, start);
+               while (next < raw.length) {
+                       int newStart = nextStart(hSignature, raw, next);
+                       if (newStart < 0) {
+                               break;
+                       }
+                       start = newStart;
+                       next = RawParseUtils.nextLF(raw, start);
+               }
+               return Arrays.copyOfRange(raw, start, raw.length);
+       }
+
        /**
         * Parse the complete tag message and decode it to a string.
         * <p>