From b6f4f33ba969cfd44cdd9a5df2048ad5b6330fb0 Mon Sep 17 00:00:00 2001
From: Jakub Jirutka <jakub@jirutka.cz>
Date: Sun, 28 Jul 2019 17:14:09 +0200
Subject: [PATCH 1/2] Change clean() and smudge() functions to accept in/out
 stream

This is a preparation for the merge command.

I've extracted this change from #107.

Co-Authored-By: Shlomo Shachar <shlomo.shachar@binatix.com>
Patch-Source: https://github.com/AGWA/git-crypt/pull/180
---
 commands.cpp | 38 +++++++++++++++++++-------------------
 commands.hpp |  5 +++--
 2 files changed, 22 insertions(+), 21 deletions(-)

diff --git a/commands.cpp b/commands.cpp
index d25c4cc..93a840e 100644
--- a/commands.cpp
+++ b/commands.cpp
@@ -690,8 +690,8 @@ static int parse_plumbing_options (const char** key_name, const char** key_file,
 	return parse_options(options, argc, argv);
 }
 
-// Encrypt contents of stdin and write to stdout
-int clean (int argc, const char** argv)
+// Encrypt contents of &in and write to &out
+int clean (int argc, const char** argv, std::istream& in, std::ostream& out)
 {
 	const char*		key_name = 0;
 	const char*		key_path = 0;
@@ -724,10 +724,10 @@ int clean (int argc, const char** argv)
 
 	char			buffer[1024];
 
-	while (std::cin && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
-		std::cin.read(buffer, sizeof(buffer));
+	while (in && file_size < Aes_ctr_encryptor::MAX_CRYPT_BYTES) {
+		in.read(buffer, sizeof(buffer));
 
-		const size_t	bytes_read = std::cin.gcount();
+		const size_t	bytes_read = in.gcount();
 
 		hmac.add(reinterpret_cast<unsigned char*>(buffer), bytes_read);
 		file_size += bytes_read;
@@ -775,8 +775,8 @@ int clean (int argc, const char** argv)
 	hmac.get(digest);
 
 	// Write a header that...
-	std::cout.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file
-	std::cout.write(reinterpret_cast<char*>(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce
+	out.write("\0GITCRYPT\0", 10); // ...identifies this as an encrypted file
+	out.write(reinterpret_cast<char*>(digest), Aes_ctr_encryptor::NONCE_LEN); // ...includes the nonce
 
 	// Now encrypt the file and write to stdout
 	Aes_ctr_encryptor	aes(key->aes_key, digest);
@@ -787,7 +787,7 @@ int clean (int argc, const char** argv)
 	while (file_data_len > 0) {
 		const size_t	buffer_len = std::min(sizeof(buffer), file_data_len);
 		aes.process(file_data, reinterpret_cast<unsigned char*>(buffer), buffer_len);
-		std::cout.write(buffer, buffer_len);
+		out.write(buffer, buffer_len);
 		file_data += buffer_len;
 		file_data_len -= buffer_len;
 	}
@@ -803,14 +803,14 @@ int clean (int argc, const char** argv)
 			aes.process(reinterpret_cast<unsigned char*>(buffer),
 			            reinterpret_cast<unsigned char*>(buffer),
 			            buffer_len);
-			std::cout.write(buffer, buffer_len);
+			out.write(buffer, buffer_len);
 		}
 	}
 
 	return 0;
 }
 
-static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char* header, std::istream& in)
+static int decrypt_file_to_stream (const Key_file& key_file, const unsigned char* header, std::istream& in, std::ostream& out = std::cout)
 {
 	const unsigned char*	nonce = header + 10;
 	uint32_t		key_version = 0; // TODO: get the version from the file header
@@ -828,7 +828,7 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char
 		in.read(reinterpret_cast<char*>(buffer), sizeof(buffer));
 		aes.process(buffer, buffer, in.gcount());
 		hmac.add(buffer, in.gcount());
-		std::cout.write(reinterpret_cast<char*>(buffer), in.gcount());
+		out.write(reinterpret_cast<char*>(buffer), in.gcount());
 	}
 
 	unsigned char		digest[Hmac_sha1_state::LEN];
@@ -844,8 +844,8 @@ static int decrypt_file_to_stdout (const Key_file& key_file, const unsigned char
 	return 0;
 }
 
-// Decrypt contents of stdin and write to stdout
-int smudge (int argc, const char** argv)
+// Decrypt contents of &in and write to &out
+int smudge (int argc, const char** argv, std::istream& in, std::ostream& out)
 {
 	const char*		key_name = 0;
 	const char*		key_path = 0;
@@ -864,8 +864,8 @@ int smudge (int argc, const char** argv)
 
 	// Read the header to get the nonce and make sure it's actually encrypted
 	unsigned char		header[10 + Aes_ctr_decryptor::NONCE_LEN];
-	std::cin.read(reinterpret_cast<char*>(header), sizeof(header));
-	if (std::cin.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
+	in.read(reinterpret_cast<char*>(header), sizeof(header));
+	if (in.gcount() != sizeof(header) || std::memcmp(header, "\0GITCRYPT\0", 10) != 0) {
 		// File not encrypted - just copy it out to stdout
 		std::clog << "git-crypt: Warning: file not encrypted" << std::endl;
 		std::clog << "git-crypt: Run 'git-crypt status' to make sure all files are properly encrypted." << std::endl;
@@ -873,12 +873,12 @@ int smudge (int argc, const char** argv)
 		std::clog << "git-crypt: this file may be unencrypted in the repository's history.  If this" << std::endl;
 		std::clog << "git-crypt: file contains sensitive information, you can use 'git filter-branch'" << std::endl;
 		std::clog << "git-crypt: to remove its old versions from the history." << std::endl;
-		std::cout.write(reinterpret_cast<char*>(header), std::cin.gcount()); // include the bytes which we already read
-		std::cout << std::cin.rdbuf();
+		out.write(reinterpret_cast<char*>(header), in.gcount()); // include the bytes which we already read
+		out << in.rdbuf();
 		return 0;
 	}
 
-	return decrypt_file_to_stdout(key_file, header, std::cin);
+	return decrypt_file_to_stream(key_file, header, in, out);
 }
 
 int diff (int argc, const char** argv)
@@ -920,7 +920,7 @@ int diff (int argc, const char** argv)
 	}
 
 	// Go ahead and decrypt it
-	return decrypt_file_to_stdout(key_file, header, in);
+	return decrypt_file_to_stream(key_file, header, in);
 }
 
 void help_init (std::ostream& out)
diff --git a/commands.hpp b/commands.hpp
index f441e93..bf4632c 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -33,6 +33,7 @@
 
 #include <string>
 #include <iosfwd>
+#include <iostream>
 
 struct Error {
 	std::string	message;
@@ -41,8 +42,8 @@ struct Error {
 };
 
 // Plumbing commands:
-int clean (int argc, const char** argv);
-int smudge (int argc, const char** argv);
+int clean (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
+int smudge (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
 int diff (int argc, const char** argv);
 // Public commands:
 int init (int argc, const char** argv);

From b2efa705c4bd8c5d5590816226aace123bf19265 Mon Sep 17 00:00:00 2001
From: Jakub Jirutka <jakub@jirutka.cz>
Date: Sun, 28 Jul 2019 19:18:39 +0200
Subject: [PATCH 2/2] Add git-crypt merge driver to support secret files
 merging

This commit is based on #107.

Co-Authored-By: Shlomo Shachar <shlomo.shachar@binatix.com>
Patch-Source: https://github.com/AGWA/git-crypt/pull/180
---
 README               |   8 ++--
 README.md            |   8 ++--
 commands.cpp         | 111 +++++++++++++++++++++++++++++++++++++++++++
 commands.hpp         |   1 +
 doc/multiple_keys.md |   2 +-
 git-crypt.cpp        |   4 ++
 man/git-crypt.xml    |   8 ++--
 7 files changed, 129 insertions(+), 13 deletions(-)

diff --git a/README b/README
index 232947f..2810a26 100644
--- a/README
+++ b/README
@@ -28,8 +28,8 @@ Configure a repository to use git-crypt:
 
 Specify files to encrypt by creating a .gitattributes file:
 
-	secretfile filter=git-crypt diff=git-crypt
-	*.key filter=git-crypt diff=git-crypt
+	secretfile filter=git-crypt diff=git-crypt merge=git-crypt
+	*.key filter=git-crypt diff=git-crypt merge=git-crypt
 
 Like a .gitignore file, it can match wildcards and should be checked into
 the repository.  See below for more information about .gitattributes.
@@ -151,8 +151,8 @@ Also note that the pattern `dir/*` does not match files under
 sub-directories of dir/.  To encrypt an entire sub-tree dir/, place the
 following in dir/.gitattributes:
 
-	* filter=git-crypt diff=git-crypt
-	.gitattributes !filter !diff
+	* filter=git-crypt diff=git-crypt merge=git-crypt
+	.gitattributes !filter !diff !merge
 
 The second pattern is essential for ensuring that .gitattributes itself
 is not encrypted.
diff --git a/README.md b/README.md
index d24517a..d008aab 100644
--- a/README.md
+++ b/README.md
@@ -29,8 +29,8 @@ Configure a repository to use git-crypt:
 
 Specify files to encrypt by creating a .gitattributes file:
 
-    secretfile filter=git-crypt diff=git-crypt
-    *.key filter=git-crypt diff=git-crypt
+    secretfile filter=git-crypt diff=git-crypt merge=git-crypt
+    *.key filter=git-crypt diff=git-crypt merge=git-crypt
 
 Like a .gitignore file, it can match wildcards and should be checked into
 the repository.  See below for more information about .gitattributes.
@@ -153,8 +153,8 @@ Also note that the pattern `dir/*` does not match files under
 sub-directories of dir/.  To encrypt an entire sub-tree dir/, place the
 following in dir/.gitattributes:
 
-    * filter=git-crypt diff=git-crypt
-    .gitattributes !filter !diff
+    * filter=git-crypt diff=git-crypt merge=git-crypt
+    .gitattributes !filter !diff !merge
 
 The second pattern is essential for ensuring that .gitattributes itself
 is not encrypted.
diff --git a/commands.cpp b/commands.cpp
index 93a840e..415b36a 100644
--- a/commands.cpp
+++ b/commands.cpp
@@ -160,11 +160,16 @@ static void configure_git_filters (const char* key_name)
 		git_config(std::string("filter.git-crypt-") + key_name + ".required", "true");
 		git_config(std::string("diff.git-crypt-") + key_name + ".textconv",
 		           escaped_git_crypt_path + " diff --key-name=" + key_name);
+		git_config(std::string("merge.git-crypt-") + key_name + ".name", "git-crypt merge driver");
+		git_config(std::string("merge.git-crypt-") + key_name + ".driver",
+		           escaped_git_crypt_path + " merge --key-name=" + key_name + " %A %O %B %L");
 	} else {
 		git_config("filter.git-crypt.smudge", escaped_git_crypt_path + " smudge");
 		git_config("filter.git-crypt.clean", escaped_git_crypt_path + " clean");
 		git_config("filter.git-crypt.required", "true");
 		git_config("diff.git-crypt.textconv", escaped_git_crypt_path + " diff");
+		git_config("merge.git-crypt.name", "git-crypt merge driver");
+		git_config("merge.git-crypt.driver", escaped_git_crypt_path + " merge %A %O %B %L");
 	}
 }
 
@@ -181,6 +186,12 @@ static void deconfigure_git_filters (const char* key_name)
 	if (git_has_config("diff." + attribute_name(key_name) + ".textconv")) {
 		git_deconfig("diff." + attribute_name(key_name));
 	}
+
+	if (git_has_config("merge." + attribute_name(key_name) + ".name") ||
+			git_has_config("merge." + attribute_name(key_name) + ".driver")) {
+
+		git_deconfig("merge." + attribute_name(key_name));
+	}
 }
 
 static bool git_checkout (const std::vector<std::string>& paths)
@@ -923,6 +934,106 @@ int diff (int argc, const char** argv)
 	return decrypt_file_to_stream(key_file, header, in);
 }
 
+int merge (int argc, const char** argv)
+{
+	const char*		key_name = 0;     // unused but needed
+	const char*		key_path = 0;     // unused but needed
+	const char*		current_path = 0; // %A
+	const char*		base_path = 0;    // %O
+	const char*		other_path = 0;   // %B
+	const char*		marker_size = 0;  // %L
+
+	int			argi = parse_plumbing_options(&key_name, &key_path, argc, argv);
+	if (argc - argi == 4) {
+		current_path = argv[argi];
+		base_path = argv[argi + 1];
+		other_path = argv[argi + 2];
+		marker_size = argv[argi + 3];
+	} else {
+		std::clog << "Usage: git-crypt merge [--key-name=NAME] [--key-file=PATH] CURRENT BASE OTHER MARKER_SIZE" << std::endl;
+		return 2;
+	}
+
+	// Run smudge on input files
+	std::vector<std::string>	smudge_files;
+	smudge_files.push_back(current_path);
+	smudge_files.push_back(base_path);
+	smudge_files.push_back(other_path);
+
+	for (std::vector<std::string>::const_iterator file(smudge_files.begin()); file != smudge_files.end(); ++file) {
+		std::ifstream	in(*file, std::ifstream::binary);
+		if (!in) {
+			std::clog << "git-crypt: " << *file << ": unable to open for reading" << std::endl;
+			return 1;
+		}
+		in.exceptions(std::ifstream::badbit);
+
+		std::ofstream	out(*file + ".tmp", std::ofstream::binary | std::ofstream::trunc);
+		if (!out) {
+			std::clog << "git-crypt: " << *file << ".tmp: unable to open for writing" << std::endl;
+			return 1;
+		}
+		out.exceptions(std::ifstream::badbit);
+
+		if (smudge(argi, argv, in, out) != 0) {
+			std::clog << "Error: failed to smudge " << *file << ": unable to merge file" << std::endl;
+			return 1;
+		}
+		in.close();
+		out.close();
+	}
+
+	// git merge-file --marker-size <marker_size> <current_path> <base_path> <other_path>
+	std::vector<std::string> command;
+	command.push_back("git");
+	command.push_back("merge-file");
+	command.push_back("-L");
+	command.push_back("ours");
+	command.push_back("-L");
+	command.push_back("base");
+	command.push_back("-L");
+	command.push_back("theirs");
+	command.push_back("--marker-size");
+	command.push_back(marker_size);
+	command.push_back(std::string(current_path) + ".tmp");
+	command.push_back(std::string(base_path) + ".tmp");
+	command.push_back(std::string(other_path) + ".tmp");
+	int ret = exit_status(exec_command(command));
+
+	// Run clean on output file
+	// We have to clean (encrypt) the output file because git runs smudge filter on it
+	// afterwards which would complain about the file not being encrypted.
+	{
+		std::ifstream	in(std::string(current_path) + ".tmp", std::ifstream::binary);
+		if (!in) {
+			std::clog << "git-crypt: " << current_path << ".tmp: unable to open for reading" << std::endl;
+			return 1;
+		}
+		in.exceptions(std::ifstream::badbit);
+
+		std::ofstream	out(current_path, std::ofstream::binary | std::ofstream::trunc);
+		if (!out) {
+			std::clog << "git-crypt: " << current_path << ": unable to open for writing" << std::endl;
+			return 1;
+		}
+		out.exceptions(std::ifstream::badbit);
+
+		if (clean(argi, argv, in, out) != 0) {
+			std::clog << "Error: failed to clean " << current_path << ": unable to merge file" << std::endl;
+			return 1;
+		}
+		in.close();
+		out.close();
+	}
+
+	// Clean-up temporary files
+	for (std::vector<std::string>::const_iterator file(smudge_files.begin()); file != smudge_files.end(); ++file) {
+		remove_file(*file + ".tmp");
+	}
+
+	return ret;
+}
+
 void help_init (std::ostream& out)
 {
 	//     |--------------------------------------------------------------------------------| 80 chars
diff --git a/commands.hpp b/commands.hpp
index bf4632c..51f4aea 100644
--- a/commands.hpp
+++ b/commands.hpp
@@ -45,6 +45,7 @@ struct Error {
 int clean (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
 int smudge (int argc, const char** argv, std::istream& in = std::cin, std::ostream& out = std::cout);
 int diff (int argc, const char** argv);
+int merge (int argc, const char** argv);
 // Public commands:
 int init (int argc, const char** argv);
 int unlock (int argc, const char** argv);
diff --git a/doc/multiple_keys.md b/doc/multiple_keys.md
index 6d7fc69..66b462a 100644
--- a/doc/multiple_keys.md
+++ b/doc/multiple_keys.md
@@ -11,7 +11,7 @@ option to `git-crypt init` as follows:
 To encrypt a file with an alternative key, use the `git-crypt-KEYNAME`
 filter in `.gitattributes` as follows:
 
-    secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME
+    secretfile filter=git-crypt-KEYNAME diff=git-crypt-KEYNAME merge=git-crypt-KEYNAME
 
 To export an alternative key or share it with a GPG user, pass the `-k
 KEYNAME` option to `git-crypt export-key` or `git-crypt add-gpg-user`
diff --git a/git-crypt.cpp b/git-crypt.cpp
index 9505834..d8c2072 100644
--- a/git-crypt.cpp
+++ b/git-crypt.cpp
@@ -73,6 +73,7 @@ static void print_usage (std::ostream& out)
 	out << "   clean [LEGACY-KEYFILE]" << std::endl;
 	out << "   smudge [LEGACY-KEYFILE]" << std::endl;
 	out << "   diff [LEGACY-KEYFILE] FILE" << std::endl;
+	out << "   merge" << std::endl;
 	*/
 	out << std::endl;
 	out << "See 'git-crypt help COMMAND' for more information on a specific command." << std::endl;
@@ -231,6 +232,9 @@ try {
 		if (std::strcmp(command, "diff") == 0) {
 			return diff(argc, argv);
 		}
+		if (std::strcmp(command, "merge") == 0) {
+			return merge(argc, argv);
+		}
 	} catch (const Option_error& e) {
 		std::clog << "git-crypt: Error: " << e.option_name << ": " << e.message << std::endl;
 		help_for_command(command, std::clog);
diff --git a/man/git-crypt.xml b/man/git-crypt.xml
index 96f53d7..21e1359 100644
--- a/man/git-crypt.xml
+++ b/man/git-crypt.xml
@@ -310,11 +310,11 @@
 		<para>
 			Then, you specify the files to encrypt by creating a
 			<citerefentry><refentrytitle>gitattributes</refentrytitle><manvolnum>5</manvolnum></citerefentry> file.
-			Each file which you want to encrypt should be assigned the "<literal>filter=git-crypt diff=git-crypt</literal>"
+			Each file which you want to encrypt should be assigned the "<literal>filter=git-crypt diff=git-crypt merge=git-crypt</literal>"
 			attributes.  For example:
 		</para>
 
-		<screen>secretfile filter=git-crypt diff=git-crypt&#10;*.key filter=git-crypt diff=git-crypt</screen>
+		<screen>secretfile filter=git-crypt diff=git-crypt merge=git-crypt&#10;*.key filter=git-crypt diff=git-crypt merge=git-crypt</screen>
 
 		<para>
 			Like a <filename>.gitignore</filename> file, <filename>.gitattributes</filename> files can match wildcards and
@@ -383,7 +383,7 @@
 			following in <filename>dir/.gitattributes</filename>:
 		</para>
 
-		<screen>* filter=git-crypt diff=git-crypt&#10;.gitattributes !filter !diff</screen>
+		<screen>* filter=git-crypt diff=git-crypt merge=git-crypt&#10;.gitattributes !filter !diff !merge</screen>
 
 		<para>
 			The second pattern is essential for ensuring that <filename>.gitattributes</filename> itself
@@ -414,7 +414,7 @@
 			filter in <filename>.gitattributes</filename> as follows:
 		</para>
 
-		<screen><replaceable>secretfile</replaceable> filter=git-crypt-<replaceable>KEYNAME</replaceable> diff=git-crypt-<replaceable>KEYNAME</replaceable></screen>
+		<screen><replaceable>secretfile</replaceable> filter=git-crypt-<replaceable>KEYNAME</replaceable> diff=git-crypt-<replaceable>KEYNAME</replaceable> merge=git-crypt-<replaceable>KEYNAME</replaceable></screen>
 
 		<para>
 			To export an alternative key or share it with a GPG user, pass the