Skip to content

Commit bce939f

Browse files
authored
feat: add merge support for git-crypt and update documentation (#1)
* feat: add merge support for git-crypt and update documentation AGWA#180 * fix: update download-artifact action to version 4 in Linux release workflow * chore: update github-script action to version 7 in release workflows
1 parent ba020f9 commit bce939f

File tree

7 files changed

+154
-38
lines changed

7 files changed

+154
-38
lines changed

README

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,8 @@ Configure a repository to use git-crypt:
2828

2929
Specify files to encrypt by creating a .gitattributes file:
3030

31-
secretfile filter=git-crypt diff=git-crypt
32-
*.key filter=git-crypt diff=git-crypt
33-
secretdir/** filter=git-crypt diff=git-crypt
31+
secretfile filter=git-crypt diff=git-crypt merge=git-crypt
32+
*.key filter=git-crypt diff=git-crypt merge=git-crypt
3433

3534
Like a .gitignore file, it can match wildcards and should be checked into
3635
the repository. See below for more information about .gitattributes.
@@ -151,10 +150,10 @@ encrypt all files beneath it.
151150
Also note that the pattern `dir/*` does not match files under
152151
sub-directories of dir/. To encrypt an entire sub-tree dir/, use `dir/**`:
153152

154-
dir/** filter=git-crypt diff=git-crypt
153+
dir/** filter=git-crypt diff=git-crypt merge=git-crypt
155154

156155
The .gitattributes file must not be encrypted, so make sure wildcards don't
157156
match it accidentally. If necessary, you can exclude .gitattributes from
158157
encryption like this:
159158

160-
.gitattributes !filter !diff
159+
.gitattributes !filter !diff !merge

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,9 @@ Configure a repository to use git-crypt:
2929

3030
Specify files to encrypt by creating a .gitattributes file:
3131

32-
secretfile eol=lf filter=git-crypt diff=git-crypt
33-
*.key eol=lf filter=git-crypt diff=git-crypt
34-
secretdir/** eol=lf filter=git-crypt diff=git-crypt
32+
secretfile eol=lf filter=git-crypt diff=git-crypt merge=git-crypt
33+
*.key eol=lf filter=git-crypt diff=git-crypt merge=git-crypt
34+
secretdir/** eol=lf filter=git-crypt diff=git-crypt merge=git-crypt
3535

3636
***eol=lf settings is necessary if autocrlf is true.***
3737

@@ -158,10 +158,10 @@ encrypt all files beneath it.
158158
Also note that the pattern `dir/*` does not match files under
159159
sub-directories of dir/. To encrypt an entire sub-tree dir/, use `dir/**`:
160160

161-
dir/** filter=git-crypt diff=git-crypt
161+
dir/** filter=git-crypt diff=git-crypt merge=git-crypt
162162

163163
The .gitattributes file must not be encrypted, so make sure wildcards don't
164164
match it accidentally. If necessary, you can exclude .gitattributes from
165165
encryption like this:
166166

167-
.gitattributes !filter !diff
167+
.gitattributes !filter !diff !merge

commands.cpp

Lines changed: 130 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,16 @@ static void configure_git_filters (const char* key_name)
169169
git_config(std::string("filter.git-crypt-") + key_name + ".required", "true");
170170
git_config(std::string("diff.git-crypt-") + key_name + ".textconv",
171171
escaped_git_crypt_path + " diff --key-name=" + key_name);
172+
git_config(std::string("merge.git-crypt-") + key_name + ".name", "git-crypt merge driver");
173+
git_config(std::string("merge.git-crypt-") + key_name + ".driver",
174+
escaped_git_crypt_path + " merge --key-name=" + key_name + " %A %O %B %L");
172175
} else {
173176
git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge");
174177
git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean");
175178
git_config("filter.git-crypt.required", "true");
176179
git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff");
180+
git_config("merge.git-crypt.name", "git-crypt merge driver");
181+
git_config("merge.git-crypt.driver", escaped_git_crypt_path + " merge %A %O %B %L");
177182
}
178183
}
179184

@@ -190,6 +195,12 @@ static void deconfigure_git_filters (const char* key_name)
190195
if (git_has_config("diff." + attribute_name(key_name) + ".textconv")) {
191196
git_deconfig("diff." + attribute_name(key_name));
192197
}
198+
199+
if (git_has_config("merge." + attribute_name(key_name) + ".name") ||
200+
git_has_config("merge." + attribute_name(key_name) + ".driver")) {
201+
202+
git_deconfig("merge." + attribute_name(key_name));
203+
}
193204
}
194205

195206
static bool git_checkout_batch (std::vector<std::string>::const_iterator paths_begin, std::vector<std::string>::const_iterator paths_end)
@@ -717,8 +728,8 @@ static int parse_plumbing_options (const char** key_name, const char** key_file,
717728
return parse_options(options, argc, argv);
718729
}
719730

720-
// Encrypt contents of stdin and write to stdout
721-
int clean (int argc, const char** argv)
731+
// Encrypt contents of &in and write to &out
732+
int clean (int argc, const char** argv, std::istream& in, std::ostream& out)
722733
{
723734
const char* key_name = 0;
724735
const char* key_path = 0;
@@ -751,10 +762,10 @@ int clean (int argc, const char** argv)
751762

752763
char buffer[1024];
753764

754-
while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
755-
std::cin.read(buffer, sizeof(buffer));
765+
while (in && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
766+
in.read(buffer, sizeof(buffer));
756767

757-
const size_t bytes_read = std::cin.gcount();
768+
const size_t bytes_read = in.gcount();
758769

759770
hmac.add(reinterpret_cast<unsigned char*>(buffer), bytes_read);
760771
file_size += bytes_read;
@@ -802,8 +813,8 @@ int clean (int argc, const char** argv)
802813
hmac.get(digest);
803814

804815
// Write a header that...
805-
std::cout.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file
806-
std::cout.write(reinterpret_cast<char*>(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce
816+
out.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file
817+
out.write(reinterpret_cast<char*>(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce
807818

808819
// Now encrypt the file and write to stdout
809820
Aes_ctr_encryptor aes(key->aes_key, digest);
@@ -814,7 +825,7 @@ int clean (int argc, const char** argv)
814825
while (file_data_len > 0) {
815826
const size_t buffer_len = std::min(sizeof(buffer), file_data_len);
816827
aes.process(file_data, reinterpret_cast<unsigned char*>(buffer), buffer_len);
817-
std::cout.write(buffer, buffer_len);
828+
out.write(buffer, buffer_len);
818829
file_data += buffer_len;
819830
file_data_len -= buffer_len;
820831
}
@@ -830,14 +841,14 @@ int clean (int argc, const char** argv)
830841
aes.process(reinterpret_cast<unsigned char*>(buffer),
831842
reinterpret_cast<unsigned char*>(buffer),
832843
buffer_len);
833-
std::cout.write(buffer, buffer_len);
844+
out.write(buffer, buffer_len);
834845
}
835846
}
836847

837848
return 0;
838849
}
839850

840-
static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char* header, std::istream& in)
851+
static int decrypt_file_to_stream (const Key_file& key_file, const unsigned char* header, std::istream& in, std::ostream& out = std::cout)
841852
{
842853
const unsigned char* nonce = header + 10;
843854
uint32_t key_version = 0; // TODO: get the version from the file header
@@ -855,7 +866,7 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char
855866
in.read(reinterpret_cast<char*>(buffer), sizeof(buffer));
856867
aes.process(buffer, buffer, in.gcount());
857868
hmac.add(buffer, in.gcount());
858-
std::cout.write(reinterpret_cast<char*>(buffer), in.gcount());
869+
out.write(reinterpret_cast<char*>(buffer), in.gcount());
859870
}
860871

861872
unsigned char digest[Hmac_sha1_state::LEN];
@@ -871,8 +882,8 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char
871882
return 0;
872883
}
873884

874-
// Decrypt contents of stdin and write to stdout
875-
int smudge (int argc, const char** argv)
885+
// Decrypt contents of &in and write to &out
886+
int smudge (int argc, const char** argv, std::istream& in, std::ostream& out)
876887
{
877888
const char* key_name = 0;
878889
const char* key_path = 0;
@@ -891,21 +902,21 @@ int smudge (int argc, const char** argv)
891902

892903
// Read the header to get the nonce and make sure it's actually encrypted
893904
unsigned char header[10 + Aes_ctr_decryptor::NONCE_LEN];
894-
std::cin.read(reinterpret_cast<char*>(header), sizeof(header));
895-
if (std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
905+
in.read(reinterpret_cast<char*>(header), sizeof(header));
906+
if (in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
896907
// File not encrypted - just copy it out to stdout
897908
std::clog << "git-crypt: Warning: file not encrypted" << std::endl;
898909
std::clog << "git-crypt: Run 'git-crypt status' to make sure all files are properly encrypted." << std::endl;
899910
std::clog << "git-crypt: If 'git-crypt status' reports no problems, then an older version of" << std::endl;
900911
std::clog << "git-crypt: this file may be unencrypted in the repository's history. If this" << std::endl;
901912
std::clog << "git-crypt: file contains sensitive information, you can use 'git filter-branch'" << std::endl;
902913
std::clog << "git-crypt: to remove its old versions from the history." << std::endl;
903-
std::cout.write(reinterpret_cast<char*>(header), std::cin.gcount()); // include the bytes which we already read
904-
std::cout << std::cin.rdbuf();
914+
out.write(reinterpret_cast<char*>(header), in.gcount()); // include the bytes which we already read
915+
out << in.rdbuf();
905916
return 0;
906917
}
907918

908-
return decrypt_file_to_stdout(key_file, header, std::cin);
919+
return decrypt_file_to_stream(key_file, header, in, out);
909920
}
910921

911922
int diff (int argc, const char** argv)
@@ -947,7 +958,107 @@ int diff (int argc, const char** argv)
947958
}
948959

949960
// Go ahead and decrypt it
950-
return decrypt_file_to_stdout(key_file, header, in);
961+
return decrypt_file_to_stream(key_file, header, in);
962+
}
963+
964+
int merge (int argc, const char** argv)
965+
{
966+
const char* key_name = 0; // unused but needed
967+
const char* key_path = 0; // unused but needed
968+
const char* current_path = 0; // %A
969+
const char* base_path = 0; // %O
970+
const char* other_path = 0; // %B
971+
const char* marker_size = 0; // %L
972+
973+
int argi = parse_plumbing_options(&key_name, &key_path, argc, argv);
974+
if (argc - argi == 4) {
975+
current_path = argv[argi];
976+
base_path = argv[argi + 1];
977+
other_path = argv[argi + 2];
978+
marker_size = argv[argi + 3];
979+
} else {
980+
std::clog << "Usage: git-crypt merge [--key-name=NAME] [--key-file=PATH] CURRENT BASE OTHER MARKER_SIZE" << std::endl;
981+
return 2;
982+
}
983+
984+
// Run smudge on input files
985+
std::vector<std::string> smudge_files;
986+
smudge_files.push_back(current_path);
987+
smudge_files.push_back(base_path);
988+
smudge_files.push_back(other_path);
989+
990+
for (std::vector<std::string>::const_iterator file(smudge_files.begin()); file != smudge_files.end(); ++file) {
991+
std::ifstream in(*file, std::ifstream::binary);
992+
if (!in) {
993+
std::clog << "git-crypt: " << *file << ": unable to open for reading" << std::endl;
994+
return 1;
995+
}
996+
in.exceptions(std::ifstream::badbit);
997+
998+
std::ofstream out(*file + ".tmp", std::ofstream::binary | std::ofstream::trunc);
999+
if (!out) {
1000+
std::clog << "git-crypt: " << *file << ".tmp: unable to open for writing" << std::endl;
1001+
return 1;
1002+
}
1003+
out.exceptions(std::ifstream::badbit);
1004+
1005+
if (smudge(argi, argv, in, out) != 0) {
1006+
std::clog << "Error: failed to smudge " << *file << ": unable to merge file" << std::endl;
1007+
return 1;
1008+
}
1009+
in.close();
1010+
out.close();
1011+
}
1012+
1013+
// git merge-file --marker-size <marker_size> <current_path> <base_path> <other_path>
1014+
std::vector<std::string> command;
1015+
command.push_back("git");
1016+
command.push_back("merge-file");
1017+
command.push_back("-L");
1018+
command.push_back("ours");
1019+
command.push_back("-L");
1020+
command.push_back("base");
1021+
command.push_back("-L");
1022+
command.push_back("theirs");
1023+
command.push_back("--marker-size");
1024+
command.push_back(marker_size);
1025+
command.push_back(std::string(current_path) + ".tmp");
1026+
command.push_back(std::string(base_path) + ".tmp");
1027+
command.push_back(std::string(other_path) + ".tmp");
1028+
int ret = exit_status(exec_command(command));
1029+
1030+
// Run clean on output file
1031+
// We have to clean (encrypt) the output file because git runs smudge filter on it
1032+
// afterwards which would complain about the file not being encrypted.
1033+
{
1034+
std::ifstream in(std::string(current_path) + ".tmp", std::ifstream::binary);
1035+
if (!in) {
1036+
std::clog << "git-crypt: " << current_path << ".tmp: unable to open for reading" << std::endl;
1037+
return 1;
1038+
}
1039+
in.exceptions(std::ifstream::badbit);
1040+
1041+
std::ofstream out(current_path, std::ofstream::binary | std::ofstream::trunc);
1042+
if (!out) {
1043+
std::clog << "git-crypt: " << current_path << ": unable to open for writing" << std::endl;
1044+
return 1;
1045+
}
1046+
out.exceptions(std::ifstream::badbit);
1047+
1048+
if (clean(argi, argv, in, out) != 0) {
1049+
std::clog << "Error: failed to clean " << current_path << ": unable to merge file" << std::endl;
1050+
return 1;
1051+
}
1052+
in.close();
1053+
out.close();
1054+
}
1055+
1056+
// Clean-up temporary files
1057+
for (std::vector<std::string>::const_iterator file(smudge_files.begin()); file != smudge_files.end(); ++file) {
1058+
remove_file(*file + ".tmp");
1059+
}
1060+
1061+
return ret;
9511062
}
9521063

9531064
void help_init (std::ostream& out)

commands.hpp

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
#include <string>
3535
#include <iosfwd>
36+
#include <iostream>
3637

3738
struct Error {
3839
std::string message;
@@ -41,9 +42,10 @@ struct Error {
4142
};
4243

4344
// Plumbing commands:
44-
int clean (int argc, const char** argv);
45-
int smudge (int argc, const char** argv);
45+
int clean (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
46+
int smudge (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
4647
int diff (int argc, const char** argv);
48+
int merge (int argc, const char** argv);
4749
// Public commands:
4850
int init (int argc, const char** argv);
4951
int unlock (int argc, const char** argv);

doc/multiple_keys.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ option to `git-crypt init` as follows:
1111
To encrypt a file with an alternative key, use the `git-crypt-KEYNAME`
1212
filter in `.gitattributes` as follows:
1313

14-
secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME
14+
secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME merge=git-crypt-KEYNAME
1515

1616
To export an alternative key or share it with a GPG user, pass the `-k
1717
KEYNAME` option to `git-crypt export-key` or `git-crypt add-gpg-user`

git-crypt.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ static void print_usage (std::ostream& out)
7373
out << " clean [LEGACY-KEYFILE]" << std::endl;
7474
out << " smudge [LEGACY-KEYFILE]" << std::endl;
7575
out << " diff [LEGACY-KEYFILE] FILE" << std::endl;
76+
out << " merge" << std::endl;
7677
*/
7778
out << std::endl;
7879
out << "See 'git-crypt help COMMAND' for more information on a specific command." << std::endl;
@@ -231,6 +232,9 @@ try {
231232
if (std::strcmp(command, "diff") == 0) {
232233
return diff(argc, argv);
233234
}
235+
if (std::strcmp(command, "merge") == 0) {
236+
return merge(argc, argv);
237+
}
234238
} catch (const Option_error& e) {
235239
std::clog << "git-crypt: Error: " << e.option_name << ": " << e.message << std::endl;
236240
help_for_command(command, std::clog);

man/git-crypt.xml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
-->
88
<refentryinfo>
99
<title>git-crypt</title>
10-
<date>2022-04-21</date>
11-
<productname>git-crypt 0.7.0</productname>
10+
<date>2024-11-11</date>
11+
<productname>git-crypt 0.8.0</productname>
1212

1313
<author>
1414
<othername>Andrew Ayer</othername>
@@ -310,11 +310,11 @@
310310
<para>
311311
Then, you specify the files to encrypt by creating a
312312
<citerefentry><refentrytitle>gitattributes</refentrytitle><manvolnum>5</manvolnum></citerefentry> file.
313-
Each file which you want to encrypt should be assigned the "<literal>filter=git-crypt diff=git-crypt</literal>"
313+
Each file which you want to encrypt should be assigned the "<literal>filter=git-crypt diff=git-crypt merge=git-crypt</literal>"
314314
attributes. For example:
315315
</para>
316316

317-
<screen>secretfile filter=git-crypt diff=git-crypt&#10;*.key filter=git-crypt diff=git-crypt</screen>
317+
<screen>secretfile filter=git-crypt diff=git-crypt merge=git-crypt&#10;*.key filter=git-crypt diff=git-crypt merge=git-crypt</screen>
318318

319319
<para>
320320
Like a <filename>.gitignore</filename> file, <filename>.gitattributes</filename> files can match wildcards and
@@ -383,7 +383,7 @@
383383
following in <filename>dir/.gitattributes</filename>:
384384
</para>
385385

386-
<screen>* filter=git-crypt diff=git-crypt&#10;.gitattributes !filter !diff</screen>
386+
<screen>* filter=git-crypt diff=git-crypt merge=git-crypt&#10;.gitattributes !filter !diff !merge</screen>
387387

388388
<para>
389389
The second pattern is essential for ensuring that <filename>.gitattributes</filename> itself
@@ -414,7 +414,7 @@
414414
filter in <filename>.gitattributes</filename> as follows:
415415
</para>
416416

417-
<screen><replaceable>secretfile</replaceable> filter=git-crypt-<replaceable>KEYNAME</replaceable> diff=git-crypt-<replaceable>KEYNAME</replaceable></screen>
417+
<screen><replaceable>secretfile</replaceable> filter=git-crypt-<replaceable>KEYNAME</replaceable> diff=git-crypt-<replaceable>KEYNAME</replaceable> merge=git-crypt-<replaceable>KEYNAME</replaceable></screen>
418418

419419
<para>
420420
To export an alternative key or share it with a GPG user, pass the

0 commit comments

Comments
 (0)