Skip to content

Commit f382116

Browse files
committed
Merge branch 'cc/signed-fast-export-import' into jch
"git fast-export | git fast-import" learns to deal with commit and tag objects with embedded signatures a bit better. * cc/signed-fast-export-import: fast-export, fast-import: add support for signed-commits fast-export: do not modify memory from get_commit_buffer git-fast-export.txt: clarify why 'verbatim' may not be a good idea fast-export: rename --signed-tags='warn' to 'warn-verbatim' fast-export: fix missing whitespace after switch git-fast-import.adoc: add missing LF in the BNF
2 parents 6c65620 + 06e496e commit f382116

File tree

5 files changed

+317
-56
lines changed

5 files changed

+317
-56
lines changed

Documentation/git-fast-export.adoc

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,32 @@ OPTIONS
2727
Insert 'progress' statements every <n> objects, to be shown by
2828
'git fast-import' during import.
2929

30-
--signed-tags=(verbatim|warn|warn-strip|strip|abort)::
30+
--signed-tags=(verbatim|warn-verbatim|warn-strip|strip|abort)::
3131
Specify how to handle signed tags. Since any transformation
32-
after the export can change the tag names (which can also happen
33-
when excluding revisions) the signatures will not match.
32+
after the export (or during the export, such as excluding
33+
revisions) can change the hashes being signed, the signatures
34+
may become invalid.
3435
+
3536
When asking to 'abort' (which is the default), this program will die
3637
when encountering a signed tag. With 'strip', the tags will silently
3738
be made unsigned, with 'warn-strip' they will be made unsigned but a
3839
warning will be displayed, with 'verbatim', they will be silently
39-
exported and with 'warn', they will be exported, but you will see a
40-
warning.
40+
exported and with 'warn-verbatim' (or 'warn', a deprecated synonym),
41+
they will be exported, but you will see a warning. 'verbatim' and
42+
'warn-verbatim' should only be used if you know that no
43+
transformation affecting tags will be performed, or if you do not
44+
care that the resulting tag will have an invalid signature.
45+
46+
--signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::
47+
Specify how to handle signed commits. Behaves exactly as
48+
'--signed-tags', but for commits.
49+
+
50+
Earlier versions this command that did not have '--signed-commits'
51+
behaved as if '--signed-commits=strip'. As an escape hatch for users
52+
of tools that call 'git fast-export' but do not yet support
53+
'--signed-commits', you may set the environment variable
54+
'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
55+
from 'abort' to 'warn-strip'.
4156

4257
--tag-of-filtered-object=(abort|drop|rewrite)::
4358
Specify how to handle tags whose tagged object is filtered out.

Documentation/git-fast-import.adoc

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -431,13 +431,22 @@ and control the current import process. More detailed discussion
431431
Create or update a branch with a new commit, recording one logical
432432
change to the project.
433433

434+
////
435+
Yes, it's intentional that the 'gpgsig' line doesn't have a trailing
436+
`LF`; the definition of `data` has a byte-count prefix, so it
437+
doesn't need an `LF` to act as a terminator (and `data` also already
438+
includes an optional trailing `LF?` just in case you want to include
439+
one).
440+
////
441+
434442
....
435443
'commit' SP <ref> LF
436444
mark?
437445
original-oid?
438446
('author' (SP <name>)? SP LT <email> GT SP <when> LF)?
439447
'committer' (SP <name>)? SP LT <email> GT SP <when> LF
440-
('encoding' SP <encoding>)?
448+
('gpgsig' SP <alg> LF data)?
449+
('encoding' SP <encoding> LF)?
441450
data
442451
('from' SP <commit-ish> LF)?
443452
('merge' SP <commit-ish> LF)*
@@ -505,6 +514,15 @@ that was selected by the --date-format=<fmt> command-line option.
505514
See ``Date Formats'' above for the set of supported formats, and
506515
their syntax.
507516

517+
`gpgsig`
518+
^^^^^^^^
519+
520+
The optional `gpgsig` command is used to include a PGP/GPG signature
521+
that signs the commit data.
522+
523+
Here <alg> specifies which hashing algorithm is used for this
524+
signature, either `sha1` or `sha256`.
525+
508526
`encoding`
509527
^^^^^^^^^^
510528
The optional `encoding` command indicates the encoding of the commit

builtin/fast-export.c

Lines changed: 139 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,11 @@ static const char *fast_export_usage[] = {
3535
NULL
3636
};
3737

38+
enum sign_mode { SIGN_ABORT, SIGN_VERBATIM, SIGN_STRIP, SIGN_WARN_VERBATIM, SIGN_WARN_STRIP };
39+
3840
static int progress;
39-
static enum signed_tag_mode { SIGNED_TAG_ABORT, VERBATIM, WARN, WARN_STRIP, STRIP } signed_tag_mode = SIGNED_TAG_ABORT;
41+
static enum sign_mode signed_tag_mode = SIGN_ABORT;
42+
static enum sign_mode signed_commit_mode = SIGN_ABORT;
4043
static enum tag_of_filtered_mode { TAG_FILTERING_ABORT, DROP, REWRITE } tag_of_filtered_mode = TAG_FILTERING_ABORT;
4144
static enum reencode_mode { REENCODE_ABORT, REENCODE_YES, REENCODE_NO } reencode_mode = REENCODE_ABORT;
4245
static int fake_missing_tagger;
@@ -53,23 +56,24 @@ static int anonymize;
5356
static struct hashmap anonymized_seeds;
5457
static struct revision_sources revision_sources;
5558

56-
static int parse_opt_signed_tag_mode(const struct option *opt,
59+
static int parse_opt_sign_mode(const struct option *opt,
5760
const char *arg, int unset)
5861
{
59-
enum signed_tag_mode *val = opt->value;
60-
61-
if (unset || !strcmp(arg, "abort"))
62-
*val = SIGNED_TAG_ABORT;
62+
enum sign_mode *val = opt->value;
63+
if (unset)
64+
return 0;
65+
else if (!strcmp(arg, "abort"))
66+
*val = SIGN_ABORT;
6367
else if (!strcmp(arg, "verbatim") || !strcmp(arg, "ignore"))
64-
*val = VERBATIM;
65-
else if (!strcmp(arg, "warn"))
66-
*val = WARN;
68+
*val = SIGN_VERBATIM;
69+
else if (!strcmp(arg, "warn-verbatim") || !strcmp(arg, "warn"))
70+
*val = SIGN_WARN_VERBATIM;
6771
else if (!strcmp(arg, "warn-strip"))
68-
*val = WARN_STRIP;
72+
*val = SIGN_WARN_STRIP;
6973
else if (!strcmp(arg, "strip"))
70-
*val = STRIP;
74+
*val = SIGN_STRIP;
7175
else
72-
return error("Unknown signed-tags mode: %s", arg);
76+
return error("Unknown %s mode: %s", opt->long_name, arg);
7377
return 0;
7478
}
7579

@@ -510,21 +514,6 @@ static void show_filemodify(struct diff_queue_struct *q,
510514
}
511515
}
512516

513-
static const char *find_encoding(const char *begin, const char *end)
514-
{
515-
const char *needle = "\nencoding ";
516-
char *bol, *eol;
517-
518-
bol = memmem(begin, end ? end - begin : strlen(begin),
519-
needle, strlen(needle));
520-
if (!bol)
521-
return NULL;
522-
bol += strlen(needle);
523-
eol = strchrnul(bol, '\n');
524-
*eol = '\0';
525-
return bol;
526-
}
527-
528517
static char *anonymize_ref_component(void)
529518
{
530519
static int counter;
@@ -626,13 +615,54 @@ static void anonymize_ident_line(const char **beg, const char **end)
626615
*end = out->buf + out->len;
627616
}
628617

618+
/*
619+
* find_commit_multiline_header is similar to find_commit_header,
620+
* except that it handles multi-line headers, rathar than simply
621+
* returning the first line of the header.
622+
*
623+
* The returned string has had the ' ' line continuation markers
624+
* removed, and points to statically allocated memory (not to memory
625+
* within 'msg'), so it is only valid until the next call to
626+
* find_commit_multiline_header.
627+
*
628+
* If the header is found, then *end is set to point at the '\n' in
629+
* msg that immediately follows the header value.
630+
*/
631+
static const char *find_commit_multiline_header(const char *msg,
632+
const char *key,
633+
const char **end)
634+
{
635+
struct strbuf val = STRBUF_INIT;
636+
const char *bol, *eol;
637+
size_t len;
638+
639+
bol = find_commit_header(msg, key, &len);
640+
if (!bol)
641+
return NULL;
642+
eol = bol + len;
643+
strbuf_add(&val, bol, len);
644+
645+
while (eol[0] == '\n' && eol[1] == ' ') {
646+
bol = eol + 2;
647+
eol = strchrnul(bol, '\n');
648+
strbuf_addch(&val, '\n');
649+
strbuf_add(&val, bol, eol - bol);
650+
}
651+
652+
*end = eol;
653+
return strbuf_detach(&val, NULL);
654+
}
655+
629656
static void handle_commit(struct commit *commit, struct rev_info *rev,
630657
struct string_list *paths_of_changed_objects)
631658
{
632659
int saved_output_format = rev->diffopt.output_format;
633-
const char *commit_buffer;
660+
const char *commit_buffer, *commit_buffer_cursor;
634661
const char *author, *author_end, *committer, *committer_end;
635-
const char *encoding, *message;
662+
const char *encoding = NULL;
663+
size_t encoding_len;
664+
const char *signature_alg = NULL, *signature = NULL;
665+
const char *message;
636666
char *reencoded = NULL;
637667
struct commit_list *p;
638668
const char *refname;
@@ -641,21 +671,43 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
641671
rev->diffopt.output_format = DIFF_FORMAT_CALLBACK;
642672

643673
parse_commit_or_die(commit);
644-
commit_buffer = repo_get_commit_buffer(the_repository, commit, NULL);
645-
author = strstr(commit_buffer, "\nauthor ");
674+
commit_buffer_cursor = commit_buffer = repo_get_commit_buffer(the_repository, commit, NULL);
675+
676+
author = strstr(commit_buffer_cursor, "\nauthor ");
646677
if (!author)
647678
die("could not find author in commit %s",
648679
oid_to_hex(&commit->object.oid));
649680
author++;
650-
author_end = strchrnul(author, '\n');
651-
committer = strstr(author_end, "\ncommitter ");
681+
commit_buffer_cursor = author_end = strchrnul(author, '\n');
682+
683+
committer = strstr(commit_buffer_cursor, "\ncommitter ");
652684
if (!committer)
653685
die("could not find committer in commit %s",
654686
oid_to_hex(&commit->object.oid));
655687
committer++;
656-
committer_end = strchrnul(committer, '\n');
657-
message = strstr(committer_end, "\n\n");
658-
encoding = find_encoding(committer_end, message);
688+
commit_buffer_cursor = committer_end = strchrnul(committer, '\n');
689+
690+
/*
691+
* find_commit_header() and find_commit_multiline_header() get
692+
* a `+ 1` because commit_buffer_cursor points at the trailing
693+
* "\n" at the end of the previous line, but they want a
694+
* pointer to the beginning of the next line.
695+
*/
696+
697+
if (*commit_buffer_cursor == '\n') {
698+
encoding = find_commit_header(commit_buffer_cursor + 1, "encoding", &encoding_len);
699+
if (encoding)
700+
commit_buffer_cursor = encoding + encoding_len;
701+
}
702+
703+
if (*commit_buffer_cursor == '\n') {
704+
if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
705+
signature_alg = "sha1";
706+
else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
707+
signature_alg = "sha256";
708+
}
709+
710+
message = strstr(commit_buffer_cursor, "\n\n");
659711
if (message)
660712
message += 2;
661713

@@ -694,16 +746,20 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
694746
if (anonymize) {
695747
reencoded = anonymize_commit_message();
696748
} else if (encoding) {
697-
switch(reencode_mode) {
749+
char *buf;
750+
switch (reencode_mode) {
698751
case REENCODE_YES:
699-
reencoded = reencode_string(message, "UTF-8", encoding);
752+
buf = xstrfmt("%.*s", (int)encoding_len, encoding);
753+
reencoded = reencode_string(message, "UTF-8", buf);
754+
free(buf);
700755
break;
701756
case REENCODE_NO:
702757
break;
703758
case REENCODE_ABORT:
704-
die("Encountered commit-specific encoding %s in commit "
759+
die("Encountered commit-specific encoding %.*s in commit "
705760
"%s; use --reencode=[yes|no] to handle it",
706-
encoding, oid_to_hex(&commit->object.oid));
761+
(int)encoding_len, encoding,
762+
oid_to_hex(&commit->object.oid));
707763
}
708764
}
709765
if (!commit->parents)
@@ -714,8 +770,33 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
714770
printf("%.*s\n%.*s\n",
715771
(int)(author_end - author), author,
716772
(int)(committer_end - committer), committer);
773+
if (signature) {
774+
switch (signed_commit_mode) {
775+
case SIGN_ABORT:
776+
die("encountered signed commit %s; use "
777+
"--signed-commits=<mode> to handle it",
778+
oid_to_hex(&commit->object.oid));
779+
case SIGN_WARN_VERBATIM:
780+
warning("exporting signed commit %s",
781+
oid_to_hex(&commit->object.oid));
782+
/* fallthru */
783+
case SIGN_VERBATIM:
784+
printf("gpgsig %s\ndata %u\n%s",
785+
signature_alg,
786+
(unsigned)strlen(signature),
787+
signature);
788+
break;
789+
case SIGN_WARN_STRIP:
790+
warning("stripping signature from commit %s",
791+
oid_to_hex(&commit->object.oid));
792+
/* fallthru */
793+
case SIGN_STRIP:
794+
break;
795+
}
796+
free((char *)signature);
797+
}
717798
if (!reencoded && encoding)
718-
printf("encoding %s\n", encoding);
799+
printf("encoding %.*s\n", (int)encoding_len, encoding);
719800
printf("data %u\n%s",
720801
(unsigned)(reencoded
721802
? strlen(reencoded) : message
@@ -828,22 +909,22 @@ static void handle_tag(const char *name, struct tag *tag)
828909
const char *signature = strstr(message,
829910
"\n-----BEGIN PGP SIGNATURE-----\n");
830911
if (signature)
831-
switch(signed_tag_mode) {
832-
case SIGNED_TAG_ABORT:
912+
switch (signed_tag_mode) {
913+
case SIGN_ABORT:
833914
die("encountered signed tag %s; use "
834915
"--signed-tags=<mode> to handle it",
835916
oid_to_hex(&tag->object.oid));
836-
case WARN:
917+
case SIGN_WARN_VERBATIM:
837918
warning("exporting signed tag %s",
838919
oid_to_hex(&tag->object.oid));
839920
/* fallthru */
840-
case VERBATIM:
921+
case SIGN_VERBATIM:
841922
break;
842-
case WARN_STRIP:
923+
case SIGN_WARN_STRIP:
843924
warning("stripping signature from tag %s",
844925
oid_to_hex(&tag->object.oid));
845926
/* fallthru */
846-
case STRIP:
927+
case SIGN_STRIP:
847928
message_size = signature + 1 - message;
848929
break;
849930
}
@@ -853,7 +934,7 @@ static void handle_tag(const char *name, struct tag *tag)
853934
tagged = tag->tagged;
854935
tagged_mark = get_object_mark(tagged);
855936
if (!tagged_mark) {
856-
switch(tag_of_filtered_mode) {
937+
switch (tag_of_filtered_mode) {
857938
case TAG_FILTERING_ABORT:
858939
die("tag %s tags unexported object; use "
859940
"--tag-of-filtered-object=<mode> to handle it",
@@ -965,7 +1046,7 @@ static void get_tags_and_duplicates(struct rev_cmdline_info *info)
9651046
continue;
9661047
}
9671048

968-
switch(commit->object.type) {
1049+
switch (commit->object.type) {
9691050
case OBJ_COMMIT:
9701051
break;
9711052
case OBJ_BLOB:
@@ -1189,6 +1270,7 @@ int cmd_fast_export(int argc,
11891270
const char *prefix,
11901271
struct repository *repo UNUSED)
11911272
{
1273+
const char *env_signed_commits_noabort;
11921274
struct rev_info revs;
11931275
struct commit *commit;
11941276
char *export_filename = NULL,
@@ -1202,7 +1284,10 @@ int cmd_fast_export(int argc,
12021284
N_("show progress after <n> objects")),
12031285
OPT_CALLBACK(0, "signed-tags", &signed_tag_mode, N_("mode"),
12041286
N_("select handling of signed tags"),
1205-
parse_opt_signed_tag_mode),
1287+
parse_opt_sign_mode),
1288+
OPT_CALLBACK(0, "signed-commits", &signed_commit_mode, N_("mode"),
1289+
N_("select handling of signed commits"),
1290+
parse_opt_sign_mode),
12061291
OPT_CALLBACK(0, "tag-of-filtered-object", &tag_of_filtered_mode, N_("mode"),
12071292
N_("select handling of tags that tag filtered objects"),
12081293
parse_opt_tag_of_filtered_mode),
@@ -1243,6 +1328,10 @@ int cmd_fast_export(int argc,
12431328
if (argc == 1)
12441329
usage_with_options (fast_export_usage, options);
12451330

1331+
env_signed_commits_noabort = getenv("FAST_EXPORT_SIGNED_COMMITS_NOABORT");
1332+
if (env_signed_commits_noabort && *env_signed_commits_noabort)
1333+
signed_commit_mode = SIGN_WARN_STRIP;
1334+
12461335
/* we handle encodings */
12471336
git_config(git_default_config, NULL);
12481337

0 commit comments

Comments
 (0)