Skip to content

Commit 01d17c0

Browse files
committed
Merge branch 'cc/signed-fast-export-import'
"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.adoc: 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 66b90d9 + d9cb0e6 commit 01d17c0

File tree

5 files changed

+317
-56
lines changed

5 files changed

+317
-56
lines changed

Documentation/git-fast-export.adoc

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,33 @@ 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 transformation
43+
affecting tags or any commit in their history will be performed by you
44+
or by fast-export or fast-import, or if you do not care that the
45+
resulting tag will have an invalid signature.
46+
47+
--signed-commits=(verbatim|warn-verbatim|warn-strip|strip|abort)::
48+
Specify how to handle signed commits. Behaves exactly as
49+
'--signed-tags', but for commits. Default is 'abort'.
50+
+
51+
Earlier versions this command that did not have '--signed-commits'
52+
behaved as if '--signed-commits=strip'. As an escape hatch for users
53+
of tools that call 'git fast-export' but do not yet support
54+
'--signed-commits', you may set the environment variable
55+
'FAST_EXPORT_SIGNED_COMMITS_NOABORT=1' in order to change the default
56+
from 'abort' to 'warn-strip'.
4157

4258
--tag-of-filtered-object=(abort|drop|rewrite)::
4359
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: 138 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,53 @@ 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, rather 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 allocated memory that must be free()d (not
625+
* to memory within 'msg').
626+
*
627+
* If the header is found, then *end is set to point at the '\n' in
628+
* msg that immediately follows the header value.
629+
*/
630+
static const char *find_commit_multiline_header(const char *msg,
631+
const char *key,
632+
const char **end)
633+
{
634+
struct strbuf val = STRBUF_INIT;
635+
const char *bol, *eol;
636+
size_t len;
637+
638+
bol = find_commit_header(msg, key, &len);
639+
if (!bol)
640+
return NULL;
641+
eol = bol + len;
642+
strbuf_add(&val, bol, len);
643+
644+
while (eol[0] == '\n' && eol[1] == ' ') {
645+
bol = eol + 2;
646+
eol = strchrnul(bol, '\n');
647+
strbuf_addch(&val, '\n');
648+
strbuf_add(&val, bol, eol - bol);
649+
}
650+
651+
*end = eol;
652+
return strbuf_detach(&val, NULL);
653+
}
654+
629655
static void handle_commit(struct commit *commit, struct rev_info *rev,
630656
struct string_list *paths_of_changed_objects)
631657
{
632658
int saved_output_format = rev->diffopt.output_format;
633-
const char *commit_buffer;
659+
const char *commit_buffer, *commit_buffer_cursor;
634660
const char *author, *author_end, *committer, *committer_end;
635-
const char *encoding, *message;
661+
const char *encoding = NULL;
662+
size_t encoding_len;
663+
const char *signature_alg = NULL, *signature = NULL;
664+
const char *message;
636665
char *reencoded = NULL;
637666
struct commit_list *p;
638667
const char *refname;
@@ -641,21 +670,43 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
641670
rev->diffopt.output_format = DIFF_FORMAT_CALLBACK;
642671

643672
parse_commit_or_die(commit);
644-
commit_buffer = repo_get_commit_buffer(the_repository, commit, NULL);
645-
author = strstr(commit_buffer, "\nauthor ");
673+
commit_buffer_cursor = commit_buffer = repo_get_commit_buffer(the_repository, commit, NULL);
674+
675+
author = strstr(commit_buffer_cursor, "\nauthor ");
646676
if (!author)
647677
die("could not find author in commit %s",
648678
oid_to_hex(&commit->object.oid));
649679
author++;
650-
author_end = strchrnul(author, '\n');
651-
committer = strstr(author_end, "\ncommitter ");
680+
commit_buffer_cursor = author_end = strchrnul(author, '\n');
681+
682+
committer = strstr(commit_buffer_cursor, "\ncommitter ");
652683
if (!committer)
653684
die("could not find committer in commit %s",
654685
oid_to_hex(&commit->object.oid));
655686
committer++;
656-
committer_end = strchrnul(committer, '\n');
657-
message = strstr(committer_end, "\n\n");
658-
encoding = find_encoding(committer_end, message);
687+
commit_buffer_cursor = committer_end = strchrnul(committer, '\n');
688+
689+
/*
690+
* find_commit_header() and find_commit_multiline_header() get
691+
* a `+ 1` because commit_buffer_cursor points at the trailing
692+
* "\n" at the end of the previous line, but they want a
693+
* pointer to the beginning of the next line.
694+
*/
695+
696+
if (*commit_buffer_cursor == '\n') {
697+
encoding = find_commit_header(commit_buffer_cursor + 1, "encoding", &encoding_len);
698+
if (encoding)
699+
commit_buffer_cursor = encoding + encoding_len;
700+
}
701+
702+
if (*commit_buffer_cursor == '\n') {
703+
if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig", &commit_buffer_cursor)))
704+
signature_alg = "sha1";
705+
else if ((signature = find_commit_multiline_header(commit_buffer_cursor + 1, "gpgsig-sha256", &commit_buffer_cursor)))
706+
signature_alg = "sha256";
707+
}
708+
709+
message = strstr(commit_buffer_cursor, "\n\n");
659710
if (message)
660711
message += 2;
661712

@@ -694,16 +745,20 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
694745
if (anonymize) {
695746
reencoded = anonymize_commit_message();
696747
} else if (encoding) {
697-
switch(reencode_mode) {
748+
char *buf;
749+
switch (reencode_mode) {
698750
case REENCODE_YES:
699-
reencoded = reencode_string(message, "UTF-8", encoding);
751+
buf = xstrfmt("%.*s", (int)encoding_len, encoding);
752+
reencoded = reencode_string(message, "UTF-8", buf);
753+
free(buf);
700754
break;
701755
case REENCODE_NO:
702756
break;
703757
case REENCODE_ABORT:
704-
die("Encountered commit-specific encoding %s in commit "
758+
die("Encountered commit-specific encoding %.*s in commit "
705759
"%s; use --reencode=[yes|no] to handle it",
706-
encoding, oid_to_hex(&commit->object.oid));
760+
(int)encoding_len, encoding,
761+
oid_to_hex(&commit->object.oid));
707762
}
708763
}
709764
if (!commit->parents)
@@ -714,8 +769,33 @@ static void handle_commit(struct commit *commit, struct rev_info *rev,
714769
printf("%.*s\n%.*s\n",
715770
(int)(author_end - author), author,
716771
(int)(committer_end - committer), committer);
772+
if (signature) {
773+
switch (signed_commit_mode) {
774+
case SIGN_ABORT:
775+
die("encountered signed commit %s; use "
776+
"--signed-commits=<mode> to handle it",
777+
oid_to_hex(&commit->object.oid));
778+
case SIGN_WARN_VERBATIM:
779+
warning("exporting signed commit %s",
780+
oid_to_hex(&commit->object.oid));
781+
/* fallthru */
782+
case SIGN_VERBATIM:
783+
printf("gpgsig %s\ndata %u\n%s",
784+
signature_alg,
785+
(unsigned)strlen(signature),
786+
signature);
787+
break;
788+
case SIGN_WARN_STRIP:
789+
warning("stripping signature from commit %s",
790+
oid_to_hex(&commit->object.oid));
791+
/* fallthru */
792+
case SIGN_STRIP:
793+
break;
794+
}
795+
free((char *)signature);
796+
}
717797
if (!reencoded && encoding)
718-
printf("encoding %s\n", encoding);
798+
printf("encoding %.*s\n", (int)encoding_len, encoding);
719799
printf("data %u\n%s",
720800
(unsigned)(reencoded
721801
? strlen(reencoded) : message
@@ -828,22 +908,22 @@ static void handle_tag(const char *name, struct tag *tag)
828908
const char *signature = strstr(message,
829909
"\n-----BEGIN PGP SIGNATURE-----\n");
830910
if (signature)
831-
switch(signed_tag_mode) {
832-
case SIGNED_TAG_ABORT:
911+
switch (signed_tag_mode) {
912+
case SIGN_ABORT:
833913
die("encountered signed tag %s; use "
834914
"--signed-tags=<mode> to handle it",
835915
oid_to_hex(&tag->object.oid));
836-
case WARN:
916+
case SIGN_WARN_VERBATIM:
837917
warning("exporting signed tag %s",
838918
oid_to_hex(&tag->object.oid));
839919
/* fallthru */
840-
case VERBATIM:
920+
case SIGN_VERBATIM:
841921
break;
842-
case WARN_STRIP:
922+
case SIGN_WARN_STRIP:
843923
warning("stripping signature from tag %s",
844924
oid_to_hex(&tag->object.oid));
845925
/* fallthru */
846-
case STRIP:
926+
case SIGN_STRIP:
847927
message_size = signature + 1 - message;
848928
break;
849929
}
@@ -853,7 +933,7 @@ static void handle_tag(const char *name, struct tag *tag)
853933
tagged = tag->tagged;
854934
tagged_mark = get_object_mark(tagged);
855935
if (!tagged_mark) {
856-
switch(tag_of_filtered_mode) {
936+
switch (tag_of_filtered_mode) {
857937
case TAG_FILTERING_ABORT:
858938
die("tag %s tags unexported object; use "
859939
"--tag-of-filtered-object=<mode> to handle it",
@@ -965,7 +1045,7 @@ static void get_tags_and_duplicates(struct rev_cmdline_info *info)
9651045
continue;
9661046
}
9671047

968-
switch(commit->object.type) {
1048+
switch (commit->object.type) {
9691049
case OBJ_COMMIT:
9701050
break;
9711051
case OBJ_BLOB:
@@ -1189,6 +1269,7 @@ int cmd_fast_export(int argc,
11891269
const char *prefix,
11901270
struct repository *repo UNUSED)
11911271
{
1272+
const char *env_signed_commits_noabort;
11921273
struct rev_info revs;
11931274
struct commit *commit;
11941275
char *export_filename = NULL,
@@ -1202,7 +1283,10 @@ int cmd_fast_export(int argc,
12021283
N_("show progress after <n> objects")),
12031284
OPT_CALLBACK(0, "signed-tags", &signed_tag_mode, N_("mode"),
12041285
N_("select handling of signed tags"),
1205-
parse_opt_signed_tag_mode),
1286+
parse_opt_sign_mode),
1287+
OPT_CALLBACK(0, "signed-commits", &signed_commit_mode, N_("mode"),
1288+
N_("select handling of signed commits"),
1289+
parse_opt_sign_mode),
12061290
OPT_CALLBACK(0, "tag-of-filtered-object", &tag_of_filtered_mode, N_("mode"),
12071291
N_("select handling of tags that tag filtered objects"),
12081292
parse_opt_tag_of_filtered_mode),
@@ -1243,6 +1327,10 @@ int cmd_fast_export(int argc,
12431327
if (argc == 1)
12441328
usage_with_options (fast_export_usage, options);
12451329

1330+
env_signed_commits_noabort = getenv("FAST_EXPORT_SIGNED_COMMITS_NOABORT");
1331+
if (env_signed_commits_noabort && *env_signed_commits_noabort)
1332+
signed_commit_mode = SIGN_WARN_STRIP;
1333+
12461334
/* we handle encodings */
12471335
git_config(git_default_config, NULL);
12481336

0 commit comments

Comments
 (0)