From f3390ff7ff18e652cf5ff487e80b51d1b74192d6 Mon Sep 17 00:00:00 2001 From: Andrew Ayer Date: Thu, 26 Jun 2014 19:54:11 -0700 Subject: [PATCH] Initial implementation of 'git-crypt status' 'git-crypt status' tells you which files are and aren't encrypted and detects other problems with your git-crypt setup. 'git-crypt status -f' can be used to re-stage files that were incorrectly staged unencrypted. The UI needs work, and it needs to also output the overall repository status (such as, is git-crypt even configured yet?), but this is a good start. --- commands.cpp | 323 ++++++++++++++++++++++++++++++++++++++++++++++++++ commands.hpp | 1 + git-crypt.cpp | 3 + 3 files changed, 327 insertions(+) diff --git a/commands.cpp b/commands.cpp index acad590..85a60b1 100644 --- a/commands.cpp +++ b/commands.cpp @@ -161,6 +161,106 @@ static bool check_if_head_exists () return successful_exit(exec_command(command, output)); } +// returns filter and diff attributes as a pair +static std::pair get_file_attributes (const std::string& filename) +{ + // git check-attr filter diff -- filename + // TODO: pass -z to get machine-parseable output (this requires Git 1.8.5 or higher, which was released on 27 Nov 2013) + std::vector command; + command.push_back("git"); + command.push_back("check-attr"); + command.push_back("filter"); + command.push_back("diff"); + command.push_back("--"); + command.push_back(filename); + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git check-attr' failed - is this a Git repository?"); + } + + std::string filter_attr; + std::string diff_attr; + + std::string line; + // Example output: + // filename: filter: git-crypt + // filename: diff: git-crypt + while (std::getline(output, line)) { + // filename might contain ": ", so parse line backwards + // filename: attr_name: attr_value + // ^name_pos ^value_pos + const std::string::size_type value_pos(line.rfind(": ")); + if (value_pos == std::string::npos || value_pos == 0) { + continue; + } + const std::string::size_type name_pos(line.rfind(": ", value_pos - 1)); + if (name_pos == std::string::npos) { + continue; + } + + const std::string attr_name(line.substr(name_pos + 2, value_pos - (name_pos + 2))); + const std::string attr_value(line.substr(value_pos + 2)); + + if (attr_value != "unspecified" && attr_value != "unset" && attr_value != "set") { + if (attr_name == "filter") { + filter_attr = attr_value; + } else if (attr_name == "diff") { + diff_attr = attr_value; + } + } + } + + return std::make_pair(filter_attr, diff_attr); +} + +static bool check_if_blob_is_encrypted (const std::string& object_id) +{ + // git cat-file blob object_id + + std::vector command; + command.push_back("git"); + command.push_back("cat-file"); + command.push_back("blob"); + command.push_back(object_id); + + // TODO: do this more efficiently - don't read entire command output into buffer, only read what we need + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git cat-file' failed - is this a Git repository?"); + } + + char header[10]; + output.read(header, sizeof(header)); + return output.gcount() == sizeof(header) && std::memcmp(header, "\0GITCRYPT\0", 10) == 0; +} + +static bool check_if_file_is_encrypted (const std::string& filename) +{ + // git ls-files -sz filename + std::vector command; + command.push_back("git"); + command.push_back("ls-files"); + command.push_back("-sz"); + command.push_back("--"); + command.push_back(filename); + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git ls-files' failed - is this a Git repository?"); + } + + if (output.peek() == -1) { + return false; + } + + std::string mode; + std::string object_id; + output >> mode >> object_id; + + return check_if_blob_is_encrypted(object_id); +} + static void load_key (Key_file& key_file, const char* legacy_path =0) { if (legacy_path) { @@ -788,3 +888,226 @@ int refresh (int argc, char** argv) // TODO: do a force checkout, much like in u return 1; } +int status (int argc, char** argv) +{ + int argi = 0; + + // Usage: + // git-crypt status -r [-z] Show repo status + // git-crypt status [-e | -u] [-z] [FILE ...] Show encrypted status of files + // git-crypt status -f Fix unencrypted blobs + + // Flags: + // -e show encrypted files only + // -u show unencrypted files only + // -f fix problems + // -z machine-parseable output + // -r show repo status only + + // TODO: help option / usage output + + bool repo_status_only = false; + bool show_encrypted_only = false; + bool show_unencrypted_only = false; + bool fix_problems = false; + bool machine_output = false; + + while (argi < argc && argv[argi][0] == '-') { + if (std::strcmp(argv[argi], "--") == 0) { + ++argi; + break; + } + const char* flags = argv[argi] + 1; + while (char flag = *flags++) { + switch (flag) { + case 'r': + repo_status_only = true; + break; + case 'e': + show_encrypted_only = true; + break; + case 'u': + show_unencrypted_only = true; + break; + case 'f': + fix_problems = true; + break; + case 'z': + machine_output = true; + break; + default: + std::clog << "Error: unknown option `" << flag << "'" << std::endl; + return 2; + } + } + ++argi; + } + + if (repo_status_only) { + if (show_encrypted_only || show_unencrypted_only) { + std::clog << "Error: -e and -u options cannot be used with -r" << std::endl; + return 2; + } + if (fix_problems) { + std::clog << "Error: -f option cannot be used with -r" << std::endl; + return 2; + } + if (argc - argi != 0) { + std::clog << "Error: filenames cannot be specified when -r is used" << std::endl; + return 2; + } + } + + if (show_encrypted_only && show_unencrypted_only) { + std::clog << "Error: -e and -u options are mutually exclusive" << std::endl; + return 2; + } + + if (fix_problems && (show_encrypted_only || show_unencrypted_only)) { + std::clog << "Error: -e and -u options cannot be used with -f" << std::endl; + return 2; + } + + if (machine_output) { + // TODO: implement machine-parseable output + std::clog << "Sorry, machine-parseable output is not yet implemented" << std::endl; + return 2; + } + + if (argc - argi == 0) { + // TODO: check repo status: + // is it set up for git-crypt? + // which keys are unlocked? + // --> check for filter config (see configure_git_filters()) and corresponding internal key + + if (repo_status_only) { + return 0; + } + } + + // git ls-files -cotsz --exclude-standard ... + std::vector command; + command.push_back("git"); + command.push_back("ls-files"); + command.push_back("-cotsz"); + command.push_back("--exclude-standard"); + command.push_back("--"); + if (argc - argi == 0) { + const std::string path_to_top(get_path_to_top()); + if (!path_to_top.empty()) { + command.push_back(path_to_top); + } + } else { + for (int i = argi; i < argc; ++i) { + command.push_back(argv[i]); + } + } + + std::stringstream output; + if (!successful_exit(exec_command(command, output))) { + throw Error("'git ls-files' failed - is this a Git repository?"); + } + + // Output looks like (w/o newlines): + // ? .gitignore\0 + // H 100644 06ec22e5ed0de9280731ef000a10f9c3fbc26338 0 afile\0 + + std::vector files; + bool attribute_errors = false; + bool unencrypted_blob_errors = false; + unsigned int nbr_of_fixed_blobs = 0; + unsigned int nbr_of_fix_errors = 0; + + while (output.peek() != -1) { + std::string tag; + std::string object_id; + std::string filename; + output >> tag; + if (tag != "?") { + std::string mode; + std::string stage; + output >> mode >> object_id >> stage; + } + output >> std::ws; + std::getline(output, filename, '\0'); + + // TODO: get file attributes en masse for efficiency... unfortunately this requires machine-parseable output from git check-attr to be workable, and this is only supported in Git 1.8.5 and above (released 27 Nov 2013) + const std::pair file_attrs(get_file_attributes(filename)); + + if (file_attrs.first == "git-crypt") { + // File is encrypted + const bool blob_is_unencrypted = !object_id.empty() && !check_if_blob_is_encrypted(object_id); + + if (fix_problems && blob_is_unencrypted) { + if (access(filename.c_str(), F_OK) != 0) { + std::clog << "Error: " << filename << ": cannot stage encrypted version because not present in working tree - please 'git rm' or 'git checkout' it" << std::endl; + ++nbr_of_fix_errors; + } else { + touch_file(filename); + std::vector git_add_command; + git_add_command.push_back("git"); + git_add_command.push_back("add"); + git_add_command.push_back("--"); + git_add_command.push_back(filename); + if (!successful_exit(exec_command(git_add_command))) { + throw Error("'git-add' failed"); + } + if (check_if_file_is_encrypted(filename)) { + std::cout << filename << ": staged encrypted version" << std::endl; + ++nbr_of_fixed_blobs; + } else { + std::clog << "Error: " << filename << ": still unencrypted even after staging" << std::endl; + ++nbr_of_fix_errors; + } + } + } else if (!fix_problems && !show_unencrypted_only) { + std::cout << " encrypted: " << filename; + if (file_attrs.second != file_attrs.first) { + // but diff filter is not properly set + std::cout << " *** WARNING: diff=" << file_attrs.first << " attribute not set ***"; + attribute_errors = true; + } + if (blob_is_unencrypted) { + // File not actually encrypted + std::cout << " *** WARNING: staged/committed version is NOT ENCRYPTED! ***"; + unencrypted_blob_errors = true; + } + std::cout << std::endl; + } + } else { + // File not encrypted + if (!fix_problems && !show_encrypted_only) { + std::cout << "not encrypted: " << filename << std::endl; + } + } + } + + int exit_status = 0; + + if (attribute_errors) { + std::cout << std::endl; + std::cout << "Warning: one or more files has a git-crypt filter attribute but not a" << std::endl; + std::cout << "corresponding git-crypt diff attribute. For proper 'git diff' operation" << std::endl; + std::cout << "you should fix the .gitattributes file to specify the correct diff attribute." << std::endl; + std::cout << "Consult the git-crypt documentation for help." << std::endl; + exit_status = 1; + } + if (unencrypted_blob_errors) { + std::cout << std::endl; + std::cout << "Warning: one or more files is marked for encryption via .gitattributes but" << std::endl; + std::cout << "was staged and/or committed before the .gitattributes file was in effect." << std::endl; + std::cout << "Run 'git-crypt status' with the '-f' option to stage an encrypted version." << std::endl; + exit_status = 1; + } + if (nbr_of_fixed_blobs) { + std::cout << "Staged " << nbr_of_fixed_blobs << " encrypted file" << (nbr_of_fixed_blobs != 1 ? "s" : "") << "." << std::endl; + std::cout << "Warning: if these files were previously committed, unencrypted versions still exist in the repository's history." << std::endl; + } + if (nbr_of_fix_errors) { + std::cout << "Unable to stage " << nbr_of_fix_errors << " file" << (nbr_of_fix_errors != 1 ? "s" : "") << "." << std::endl; + exit_status = 1; + } + + return exit_status; +} + diff --git a/commands.hpp b/commands.hpp index 33d674b..dd2448e 100644 --- a/commands.hpp +++ b/commands.hpp @@ -53,6 +53,7 @@ int export_key (int argc, char** argv); int keygen (int argc, char** argv); int migrate_key (int argc, char** argv); int refresh (int argc, char** argv); +int status (int argc, char** argv); #endif diff --git a/git-crypt.cpp b/git-crypt.cpp index aaf27fb..b4e7261 100644 --- a/git-crypt.cpp +++ b/git-crypt.cpp @@ -159,6 +159,9 @@ try { if (std::strcmp(command, "refresh") == 0) { return refresh(argc, argv); } + if (std::strcmp(command, "status") == 0) { + return status(argc, argv); + } // Plumbing commands (executed by git, not by user): if (std::strcmp(command, "clean") == 0) { return clean(argc, argv);