Merge branch 'jc/signed-commit' and 'jc/pull-signed-tag'

They both use the extended headers in commit objects, and the former has
necessary infrastructure to show them that is useful to view the result of
the latter.

Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Junio C Hamano 2012-01-05 11:00:38 -08:00
commit 9d3d78435f
15 changed files with 364 additions and 19 deletions

View File

@ -1094,6 +1094,17 @@ grep.lineNumber::
grep.extendedRegexp::
If set to true, enable '--extended-regexp' option by default.
gpg.program::
Use this custom program instead of "gpg" found on $PATH when
making or verifying a PGP signature. The program must support the
same command line interface as GPG, namely, to verify a detached
signature, "gpg --verify $file - <$signature" is run, and the
program is expected to signal a good signature by exiting with
code 0, and to generate an ascii-armored detached signature, the
standard input of "gpg -bsau $key" is fed with the contents to be
signed, and the program is expected to send the result to its
standard output.
gui.commitmsgwidth::
Defines how wide the commit message window is in the
linkgit:git-gui[1]. "75" is the default.

View File

@ -38,7 +38,9 @@ created (i.e. a lightweight tag).
A GnuPG signed tag object will be created when `-s` or `-u
<key-id>` is used. When `-u <key-id>` is not used, the
committer identity for the current user is used to find the
GnuPG key for signing.
GnuPG key for signing. The configuration variable `gpg.program`
is used to specify custom GnuPG binary.
OPTIONS
-------
@ -48,11 +50,11 @@ OPTIONS
-s::
--sign::
Make a GPG-signed tag, using the default e-mail address's key
Make a GPG-signed tag, using the default e-mail address's key.
-u <key-id>::
--local-user=<key-id>::
Make a GPG-signed tag, using the given key
Make a GPG-signed tag, using the given key.
-f::
--force::

View File

@ -8,8 +8,9 @@
#include "tree.h"
#include "builtin.h"
#include "utf8.h"
#include "gpg-interface.h"
static const char commit_tree_usage[] = "git commit-tree [(-p <sha1>)...] [-m <message>] [-F <file>] <sha1> <changelog";
static const char commit_tree_usage[] = "git commit-tree [(-p <sha1>)...] [-S<signer>] [-m <message>] [-F <file>] <sha1> <changelog";
static void new_parent(struct commit *parent, struct commit_list **parents_p)
{
@ -25,6 +26,14 @@ static void new_parent(struct commit *parent, struct commit_list **parents_p)
commit_list_insert(parent, parents_p);
}
static int commit_tree_config(const char *var, const char *value, void *cb)
{
int status = git_gpg_config(var, value, NULL);
if (status)
return status;
return git_default_config(var, value, cb);
}
int cmd_commit_tree(int argc, const char **argv, const char *prefix)
{
int i, got_tree = 0;
@ -32,12 +41,16 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix)
unsigned char tree_sha1[20];
unsigned char commit_sha1[20];
struct strbuf buffer = STRBUF_INIT;
const char *sign_commit = NULL;
git_config(git_default_config, NULL);
git_config(commit_tree_config, NULL);
if (argc < 2 || !strcmp(argv[1], "-h"))
usage(commit_tree_usage);
if (get_sha1(argv[1], tree_sha1))
die("Not a valid object name %s", argv[1]);
for (i = 1; i < argc; i++) {
const char *arg = argv[i];
if (!strcmp(arg, "-p")) {
@ -51,6 +64,11 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix)
continue;
}
if (!memcmp(arg, "-S", 2)) {
sign_commit = arg + 2;
continue;
}
if (!strcmp(arg, "-m")) {
if (argc <= ++i)
usage(commit_tree_usage);
@ -98,7 +116,8 @@ int cmd_commit_tree(int argc, const char **argv, const char *prefix)
die_errno("git commit-tree: failed to read");
}
if (commit_tree(buffer.buf, tree_sha1, parents, commit_sha1, NULL)) {
if (commit_tree(buffer.buf, tree_sha1, parents, commit_sha1,
NULL, sign_commit)) {
strbuf_release(&buffer);
return 1;
}

View File

@ -26,6 +26,7 @@
#include "unpack-trees.h"
#include "quote.h"
#include "submodule.h"
#include "gpg-interface.h"
static const char * const builtin_commit_usage[] = {
"git commit [options] [--] <filepattern>...",
@ -85,6 +86,8 @@ static int all, edit_flag, also, interactive, patch_interactive, only, amend, si
static int quiet, verbose, no_verify, allow_empty, dry_run, renew_authorship;
static int no_post_rewrite, allow_empty_message;
static char *untracked_files_arg, *force_date, *ignore_submodule_arg;
static char *sign_commit;
/*
* The default commit message cleanup mode will remove the lines
* beginning with # (shell comments) and leading and trailing
@ -144,6 +147,8 @@ static struct option builtin_commit_options[] = {
OPT_BOOLEAN('e', "edit", &edit_flag, "force edit of commit"),
OPT_STRING(0, "cleanup", &cleanup_arg, "default", "how to strip spaces and #comments from message"),
OPT_BOOLEAN(0, "status", &include_status, "include status in commit message template"),
{ OPTION_STRING, 'S', "gpg-sign", &sign_commit, "key id",
"GPG sign commit", PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
/* end commit message options */
OPT_GROUP("Commit contents options"),
@ -1324,6 +1329,7 @@ static void print_summary(const char *prefix, const unsigned char *sha1,
static int git_commit_config(const char *k, const char *v, void *cb)
{
struct wt_status *s = cb;
int status;
if (!strcmp(k, "commit.template"))
return git_config_pathname(&template_file, k, v);
@ -1332,6 +1338,9 @@ static int git_commit_config(const char *k, const char *v, void *cb)
return 0;
}
status = git_gpg_config(k, v, NULL);
if (status)
return status;
return git_status_config(k, v, s);
}
@ -1492,7 +1501,7 @@ int cmd_commit(int argc, const char **argv, const char *prefix)
}
if (commit_tree_extended(sb.buf, active_cache_tree->sha1, parents, sha1,
author_ident.buf, extra)) {
author_ident.buf, sign_commit, extra)) {
rollback_index_files();
die(_("failed to write commit object"));
}

View File

@ -26,6 +26,7 @@
#include "merge-recursive.h"
#include "resolve-undo.h"
#include "remote.h"
#include "gpg-interface.h"
#define DEFAULT_TWOHEAD (1<<0)
#define DEFAULT_OCTOPUS (1<<1)
@ -62,6 +63,7 @@ static int allow_rerere_auto;
static int abort_current_merge;
static int show_progress = -1;
static int default_to_upstream;
static const char *sign_commit;
static struct strategy all_strategy[] = {
{ "recursive", DEFAULT_TWOHEAD | NO_TRIVIAL },
@ -207,6 +209,8 @@ static struct option builtin_merge_options[] = {
OPT_BOOLEAN(0, "abort", &abort_current_merge,
"abort the current in-progress merge"),
OPT_SET_INT(0, "progress", &show_progress, "force progress reporting", 1),
{ OPTION_STRING, 'S', "gpg-sign", &sign_commit, "key id",
"GPG sign commit", PARSE_OPT_OPTARG, NULL, (intptr_t) "" },
OPT_END()
};
@ -533,6 +537,8 @@ static void parse_branch_merge_options(char *bmo)
static int git_merge_config(const char *k, const char *v, void *cb)
{
int status;
if (branch && !prefixcmp(k, "branch.") &&
!prefixcmp(k + 7, branch) &&
!strcmp(k + 7 + strlen(branch), ".mergeoptions")) {
@ -570,6 +576,10 @@ static int git_merge_config(const char *k, const char *v, void *cb)
default_to_upstream = git_config_bool(k, v);
return 0;
}
status = git_gpg_config(k, v, NULL);
if (status)
return status;
return git_diff_ui_config(k, v, cb);
}
@ -902,7 +912,8 @@ static int merge_trivial(struct commit *head)
parent->next->item = remoteheads->item;
parent->next->next = NULL;
prepare_to_commit();
commit_tree(merge_msg.buf, result_tree, parent, result_commit, NULL);
commit_tree(merge_msg.buf, result_tree, parent, result_commit, NULL,
sign_commit);
finish(head, result_commit, "In-index merge");
drop_save();
return 0;
@ -933,7 +944,8 @@ static int finish_automerge(struct commit *head,
strbuf_addch(&merge_msg, '\n');
prepare_to_commit();
free_commit_list(remoteheads);
commit_tree(merge_msg.buf, result_tree, parents, result_commit, NULL);
commit_tree(merge_msg.buf, result_tree, parents, result_commit,
NULL, sign_commit);
strbuf_addf(&buf, "Merge made by the '%s' strategy.", wt_strategy);
finish(head, result_commit, buf.buf);
strbuf_release(&buf);

View File

@ -6,6 +6,7 @@
#include "diff.h"
#include "revision.h"
#include "notes.h"
#include "gpg-interface.h"
int save_commit_buffer = 1;
@ -840,6 +841,86 @@ struct commit_list *reduce_heads(struct commit_list *heads)
return result;
}
static const char gpg_sig_header[] = "gpgsig";
static const int gpg_sig_header_len = sizeof(gpg_sig_header) - 1;
static int do_sign_commit(struct strbuf *buf, const char *keyid)
{
struct strbuf sig = STRBUF_INIT;
int inspos, copypos;
/* find the end of the header */
inspos = strstr(buf->buf, "\n\n") - buf->buf + 1;
if (!keyid || !*keyid)
keyid = get_signing_key();
if (sign_buffer(buf, &sig, keyid)) {
strbuf_release(&sig);
return -1;
}
for (copypos = 0; sig.buf[copypos]; ) {
const char *bol = sig.buf + copypos;
const char *eol = strchrnul(bol, '\n');
int len = (eol - bol) + !!*eol;
if (!copypos) {
strbuf_insert(buf, inspos, gpg_sig_header, gpg_sig_header_len);
inspos += gpg_sig_header_len;
}
strbuf_insert(buf, inspos++, " ", 1);
strbuf_insert(buf, inspos, bol, len);
inspos += len;
copypos += len;
}
strbuf_release(&sig);
return 0;
}
int parse_signed_commit(const unsigned char *sha1,
struct strbuf *payload, struct strbuf *signature)
{
unsigned long size;
enum object_type type;
char *buffer = read_sha1_file(sha1, &type, &size);
int in_signature, saw_signature = -1;
char *line, *tail;
if (!buffer || type != OBJ_COMMIT)
goto cleanup;
line = buffer;
tail = buffer + size;
in_signature = 0;
saw_signature = 0;
while (line < tail) {
const char *sig = NULL;
char *next = memchr(line, '\n', tail - line);
next = next ? next + 1 : tail;
if (in_signature && line[0] == ' ')
sig = line + 1;
else if (!prefixcmp(line, gpg_sig_header) &&
line[gpg_sig_header_len] == ' ')
sig = line + gpg_sig_header_len + 1;
if (sig) {
strbuf_add(signature, sig, next - sig);
saw_signature = 1;
in_signature = 1;
} else {
if (*line == '\n')
/* dump the whole remainder of the buffer */
next = tail;
strbuf_add(payload, line, next - line);
in_signature = 0;
}
line = next;
}
cleanup:
free(buffer);
return saw_signature;
}
static void handle_signed_tag(struct commit *parent, struct commit_extra_header ***tail)
{
struct merge_remote_desc *desc;
@ -975,13 +1056,14 @@ void free_commit_extra_headers(struct commit_extra_header *extra)
int commit_tree(const char *msg, unsigned char *tree,
struct commit_list *parents, unsigned char *ret,
const char *author)
const char *author, const char *sign_commit)
{
struct commit_extra_header *extra = NULL, **tail = &extra;
int result;
append_merge_tag_headers(parents, &tail);
result = commit_tree_extended(msg, tree, parents, ret, author, extra);
result = commit_tree_extended(msg, tree, parents, ret,
author, sign_commit, extra);
free_commit_extra_headers(extra);
return result;
}
@ -993,7 +1075,8 @@ static const char commit_utf8_warn[] =
int commit_tree_extended(const char *msg, unsigned char *tree,
struct commit_list *parents, unsigned char *ret,
const char *author, struct commit_extra_header *extra)
const char *author, const char *sign_commit,
struct commit_extra_header *extra)
{
int result;
int encoding_is_utf8;
@ -1043,6 +1126,9 @@ int commit_tree_extended(const char *msg, unsigned char *tree,
if (encoding_is_utf8 && !is_utf8(buffer.buf))
fprintf(stderr, commit_utf8_warn);
if (sign_commit && do_sign_commit(&buffer, sign_commit))
return -1;
result = write_sha1_file(buffer.buf, buffer.len, commit_type, ret);
strbuf_release(&buffer);
return result;

View File

@ -193,11 +193,11 @@ extern void append_merge_tag_headers(struct commit_list *parents,
extern int commit_tree(const char *msg, unsigned char *tree,
struct commit_list *parents, unsigned char *ret,
const char *author);
const char *author, const char *sign_commit);
extern int commit_tree_extended(const char *msg, unsigned char *tree,
struct commit_list *parents, unsigned char *ret,
const char *author,
const char *author, const char *sign_commit,
struct commit_extra_header *);
extern struct commit_extra_header *read_commit_extra_headers(struct commit *);
@ -218,4 +218,6 @@ struct merge_remote_desc {
*/
struct commit *get_merge_parent(const char *name);
extern int parse_signed_commit(const unsigned char *sha1,
struct strbuf *message, struct strbuf *signature);
#endif /* COMMIT_H */

View File

@ -5,6 +5,7 @@
#include "sigchain.h"
static char *configured_signing_key;
static const char *gpg_program = "gpg";
void set_signing_key(const char *key)
{
@ -15,9 +16,12 @@ void set_signing_key(const char *key)
int git_gpg_config(const char *var, const char *value, void *cb)
{
if (!strcmp(var, "user.signingkey")) {
set_signing_key(value);
}
if (!strcmp(var, "gpg.program")) {
if (!value)
return config_error_nonbool(var);
set_signing_key(value);
gpg_program = xstrdup(value);
}
return 0;
}
@ -46,7 +50,7 @@ int sign_buffer(struct strbuf *buffer, struct strbuf *signature, const char *sig
gpg.argv = args;
gpg.in = -1;
gpg.out = -1;
args[0] = "gpg";
args[0] = gpg_program;
args[1] = "-bsau";
args[2] = signing_key;
args[3] = NULL;
@ -101,10 +105,11 @@ int verify_signed_buffer(const char *payload, size_t payload_size,
struct strbuf *gpg_output)
{
struct child_process gpg;
const char *args_gpg[] = {"gpg", "--verify", "FILE", "-", NULL};
const char *args_gpg[] = {NULL, "--verify", "FILE", "-", NULL};
char path[PATH_MAX];
int fd, ret;
args_gpg[0] = gpg_program;
fd = git_mkstemp(path, PATH_MAX, ".git_vtag_tmpXXXXXX");
if (fd < 0)
return error("could not create temporary file '%s': %s",

View File

@ -8,6 +8,7 @@
#include "refs.h"
#include "string-list.h"
#include "color.h"
#include "gpg-interface.h"
struct decoration name_decoration = { "object names" };
@ -403,6 +404,41 @@ void log_write_email_headers(struct rev_info *opt, struct commit *commit,
*extra_headers_p = extra_headers;
}
static void show_signature(struct rev_info *opt, struct commit *commit)
{
struct strbuf payload = STRBUF_INIT;
struct strbuf signature = STRBUF_INIT;
struct strbuf gpg_output = STRBUF_INIT;
int status;
const char *color, *reset, *bol, *eol;
if (parse_signed_commit(commit->object.sha1, &payload, &signature) <= 0)
goto out;
status = verify_signed_buffer(payload.buf, payload.len,
signature.buf, signature.len,
&gpg_output);
if (status && !gpg_output.len)
strbuf_addstr(&gpg_output, "No signature\n");
color = diff_get_color_opt(&opt->diffopt,
status ? DIFF_WHITESPACE : DIFF_FRAGINFO);
reset = diff_get_color_opt(&opt->diffopt, DIFF_RESET);
bol = gpg_output.buf;
while (*bol) {
eol = strchrnul(bol, '\n');
printf("%s%.*s%s%s", color, (int)(eol - bol), bol, reset,
*eol ? "\n" : "");
bol = (*eol) ? (eol + 1) : eol;
}
out:
strbuf_release(&gpg_output);
strbuf_release(&payload);
strbuf_release(&signature);
}
void show_log(struct rev_info *opt)
{
struct strbuf msgbuf = STRBUF_INIT;
@ -514,6 +550,9 @@ void show_log(struct rev_info *opt)
}
}
if (opt->show_signature)
show_signature(opt, commit);
if (!commit->buffer)
return;

View File

@ -56,7 +56,7 @@ int notes_cache_write(struct notes_cache *c)
if (write_notes_tree(&c->tree, tree_sha1))
return -1;
if (commit_tree(c->validity, tree_sha1, NULL, commit_sha1, NULL) < 0)
if (commit_tree(c->validity, tree_sha1, NULL, commit_sha1, NULL, NULL) < 0)
return -1;
if (update_ref("update notes cache", c->tree.ref, commit_sha1, NULL,
0, QUIET_ON_ERR) < 0)

View File

@ -546,7 +546,7 @@ void create_notes_commit(struct notes_tree *t, struct commit_list *parents,
/* else: t->ref points to nothing, assume root/orphan commit */
}
if (commit_tree(msg, tree_sha1, parents, result_sha1, NULL))
if (commit_tree(msg, tree_sha1, parents, result_sha1, NULL, NULL))
die("Failed to commit notes tree to database");
}

View File

@ -9,6 +9,7 @@
#include "notes.h"
#include "color.h"
#include "reflog-walk.h"
#include "gpg-interface.h"
static char *user_format;
static struct cmt_fmt_map {
@ -640,6 +641,12 @@ struct format_commit_context {
const struct pretty_print_context *pretty_ctx;
unsigned commit_header_parsed:1;
unsigned commit_message_parsed:1;
unsigned commit_signature_parsed:1;
struct {
char *gpg_output;
char good_bad;
char *signer;
} signature;
char *message;
size_t width, indent1, indent2;
@ -822,6 +829,59 @@ static void rewrap_message_tail(struct strbuf *sb,
c->indent2 = new_indent2;
}
static struct {
char result;
const char *check;
} signature_check[] = {
{ 'G', ": Good signature from " },
{ 'B', ": BAD signature from " },
};
static void parse_signature_lines(struct format_commit_context *ctx)
{
const char *buf = ctx->signature.gpg_output;
int i;
for (i = 0; i < ARRAY_SIZE(signature_check); i++) {
const char *found = strstr(buf, signature_check[i].check);
const char *next;
if (!found)
continue;
ctx->signature.good_bad = signature_check[i].result;
found += strlen(signature_check[i].check);
next = strchrnul(found, '\n');
ctx->signature.signer = xmemdupz(found, next - found);
break;
}
}
static void parse_commit_signature(struct format_commit_context *ctx)
{
struct strbuf payload = STRBUF_INIT;
struct strbuf signature = STRBUF_INIT;
struct strbuf gpg_output = STRBUF_INIT;
int status;
ctx->commit_signature_parsed = 1;
if (parse_signed_commit(ctx->commit->object.sha1,
&payload, &signature) <= 0)
goto out;
status = verify_signed_buffer(payload.buf, payload.len,
signature.buf, signature.len,
&gpg_output);
if (status && !gpg_output.len)
goto out;
ctx->signature.gpg_output = strbuf_detach(&gpg_output, NULL);
parse_signature_lines(ctx);
out:
strbuf_release(&gpg_output);
strbuf_release(&payload);
strbuf_release(&signature);
}
static size_t format_commit_one(struct strbuf *sb, const char *placeholder,
void *context)
{
@ -974,6 +1034,30 @@ static size_t format_commit_one(struct strbuf *sb, const char *placeholder,
return 0;
}
if (placeholder[0] == 'G') {
if (!c->commit_signature_parsed)
parse_commit_signature(c);
switch (placeholder[1]) {
case 'G':
if (c->signature.gpg_output)
strbuf_addstr(sb, c->signature.gpg_output);
break;
case '?':
switch (c->signature.good_bad) {
case 'G':
case 'B':
strbuf_addch(sb, c->signature.good_bad);
}
break;
case 'S':
if (c->signature.signer)
strbuf_addstr(sb, c->signature.signer);
break;
}
return 2;
}
/* For the rest we have to parse the commit header. */
if (!c->commit_header_parsed)
parse_commit_header(c);
@ -1114,6 +1198,8 @@ void format_commit_message(const struct commit *commit,
if (context.message != commit->buffer)
free(context.message);
free(context.signature.gpg_output);
free(context.signature.signer);
}
static void pp_header(const struct pretty_print_context *pp,

View File

@ -1469,6 +1469,8 @@ static int handle_revision_opt(struct rev_info *revs, int argc, const char **arg
revs->show_notes = 1;
revs->show_notes_given = 1;
revs->notes_opt.use_default_notes = 1;
} else if (!strcmp(arg, "--show-signature")) {
revs->show_signature = 1;
} else if (!prefixcmp(arg, "--show-notes=") ||
!prefixcmp(arg, "--notes=")) {
struct strbuf buf = STRBUF_INIT;

View File

@ -110,6 +110,7 @@ struct rev_info {
show_merge:1,
show_notes:1,
show_notes_given:1,
show_signature:1,
pretty_given:1,
abbrev_commit:1,
abbrev_commit_given:1,

71
t/t7510-signed-commit.sh Executable file
View File

@ -0,0 +1,71 @@
#!/bin/sh
test_description='signed commit tests'
. ./test-lib.sh
. "$TEST_DIRECTORY/lib-gpg.sh"
test_expect_success GPG 'create signed commits' '
echo 1 >file && git add file &&
test_tick && git commit -S -m initial &&
git tag initial &&
git branch side &&
echo 2 >file && test_tick && git commit -a -S -m second &&
git tag second &&
git checkout side &&
echo 3 >elif && git add elif &&
test_tick && git commit -m "third on side" &&
git checkout master &&
test_tick && git merge -S side &&
git tag merge &&
echo 4 >file && test_tick && git commit -a -m "fourth unsigned" &&
git tag fourth-unsigned &&
test_tick && git commit --amend -S -m "fourth signed"
'
test_expect_success GPG 'show signatures' '
(
for commit in initial second merge master
do
git show --pretty=short --show-signature $commit >actual &&
grep "Good signature from" actual || exit 1
! grep "BAD signature from" actual || exit 1
echo $commit OK
done
) &&
(
for commit in merge^2 fourth-unsigned
do
git show --pretty=short --show-signature $commit >actual &&
grep "Good signature from" actual && exit 1
! grep "BAD signature from" actual || exit 1
echo $commit OK
done
)
'
test_expect_success GPG 'detect fudged signature' '
git cat-file commit master >raw &&
sed -e "s/fourth signed/4th forged/" raw >forged1 &&
git hash-object -w -t commit forged1 >forged1.commit &&
git show --pretty=short --show-signature $(cat forged1.commit) >actual1 &&
grep "BAD signature from" actual1 &&
! grep "Good signature from" actual1
'
test_expect_success GPG 'detect fudged signature with NUL' '
git cat-file commit master >raw &&
cat raw >forged2 &&
echo Qwik | tr "Q" "\000" >>forged2 &&
git hash-object -w -t commit forged2 >forged2.commit &&
git show --pretty=short --show-signature $(cat forged2.commit) >actual2 &&
grep "BAD signature from" actual2 &&
! grep "Good signature from" actual2
'
test_done