Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f3390ff

Browse filesBrowse files
committed
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.
1 parent e6bb66b commit f3390ff
Copy full SHA for f3390ff

File tree

Expand file treeCollapse file tree

3 files changed

+327
-0
lines changed
Open diff view settings
Filter options
Expand file treeCollapse file tree

3 files changed

+327
-0
lines changed
Open diff view settings
Collapse file

‎commands.cpp‎

Copy file name to clipboardExpand all lines: commands.cpp
+323Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,106 @@ static bool check_if_head_exists ()
161161
return successful_exit(exec_command(command, output));
162162
}
163163

164+
// returns filter and diff attributes as a pair
165+
static std::pair<std::string, std::string> get_file_attributes (const std::string& filename)
166+
{
167+
// git check-attr filter diff -- filename
168+
// TODO: pass -z to get machine-parseable output (this requires Git 1.8.5 or higher, which was released on 27 Nov 2013)
169+
std::vector<std::string> command;
170+
command.push_back("git");
171+
command.push_back("check-attr");
172+
command.push_back("filter");
173+
command.push_back("diff");
174+
command.push_back("--");
175+
command.push_back(filename);
176+
177+
std::stringstream output;
178+
if (!successful_exit(exec_command(command, output))) {
179+
throw Error("'git check-attr' failed - is this a Git repository?");
180+
}
181+
182+
std::string filter_attr;
183+
std::string diff_attr;
184+
185+
std::string line;
186+
// Example output:
187+
// filename: filter: git-crypt
188+
// filename: diff: git-crypt
189+
while (std::getline(output, line)) {
190+
// filename might contain ": ", so parse line backwards
191+
// filename: attr_name: attr_value
192+
// ^name_pos ^value_pos
193+
const std::string::size_type value_pos(line.rfind(": "));
194+
if (value_pos == std::string::npos || value_pos == 0) {
195+
continue;
196+
}
197+
const std::string::size_type name_pos(line.rfind(": ", value_pos - 1));
198+
if (name_pos == std::string::npos) {
199+
continue;
200+
}
201+
202+
const std::string attr_name(line.substr(name_pos + 2, value_pos - (name_pos + 2)));
203+
const std::string attr_value(line.substr(value_pos + 2));
204+
205+
if (attr_value != "unspecified" && attr_value != "unset" && attr_value != "set") {
206+
if (attr_name == "filter") {
207+
filter_attr = attr_value;
208+
} else if (attr_name == "diff") {
209+
diff_attr = attr_value;
210+
}
211+
}
212+
}
213+
214+
return std::make_pair(filter_attr, diff_attr);
215+
}
216+
217+
static bool check_if_blob_is_encrypted (const std::string& object_id)
218+
{
219+
// git cat-file blob object_id
220+
221+
std::vector<std::string> command;
222+
command.push_back("git");
223+
command.push_back("cat-file");
224+
command.push_back("blob");
225+
command.push_back(object_id);
226+
227+
// TODO: do this more efficiently - don't read entire command output into buffer, only read what we need
228+
std::stringstream output;
229+
if (!successful_exit(exec_command(command, output))) {
230+
throw Error("'git cat-file' failed - is this a Git repository?");
231+
}
232+
233+
char header[10];
234+
output.read(header, sizeof(header));
235+
return output.gcount() == sizeof(header) && std::memcmp(header, "\0GITCRYPT\0", 10) == 0;
236+
}
237+
238+
static bool check_if_file_is_encrypted (const std::string& filename)
239+
{
240+
// git ls-files -sz filename
241+
std::vector<std::string> command;
242+
command.push_back("git");
243+
command.push_back("ls-files");
244+
command.push_back("-sz");
245+
command.push_back("--");
246+
command.push_back(filename);
247+
248+
std::stringstream output;
249+
if (!successful_exit(exec_command(command, output))) {
250+
throw Error("'git ls-files' failed - is this a Git repository?");
251+
}
252+
253+
if (output.peek() == -1) {
254+
return false;
255+
}
256+
257+
std::string mode;
258+
std::string object_id;
259+
output >> mode >> object_id;
260+
261+
return check_if_blob_is_encrypted(object_id);
262+
}
263+
164264
static void load_key (Key_file& key_file, const char* legacy_path =0)
165265
{
166266
if (legacy_path) {
@@ -788,3 +888,226 @@ int refresh (int argc, char** argv) // TODO: do a force checkout, much like in u
788888
return 1;
789889
}
790890

891+
int status (int argc, char** argv)
892+
{
893+
int argi = 0;
894+
895+
// Usage:
896+
// git-crypt status -r [-z] Show repo status
897+
// git-crypt status [-e | -u] [-z] [FILE ...] Show encrypted status of files
898+
// git-crypt status -f Fix unencrypted blobs
899+
900+
// Flags:
901+
// -e show encrypted files only
902+
// -u show unencrypted files only
903+
// -f fix problems
904+
// -z machine-parseable output
905+
// -r show repo status only
906+
907+
// TODO: help option / usage output
908+
909+
bool repo_status_only = false;
910+
bool show_encrypted_only = false;
911+
bool show_unencrypted_only = false;
912+
bool fix_problems = false;
913+
bool machine_output = false;
914+
915+
while (argi < argc && argv[argi][0] == '-') {
916+
if (std::strcmp(argv[argi], "--") == 0) {
917+
++argi;
918+
break;
919+
}
920+
const char* flags = argv[argi] + 1;
921+
while (char flag = *flags++) {
922+
switch (flag) {
923+
case 'r':
924+
repo_status_only = true;
925+
break;
926+
case 'e':
927+
show_encrypted_only = true;
928+
break;
929+
case 'u':
930+
show_unencrypted_only = true;
931+
break;
932+
case 'f':
933+
fix_problems = true;
934+
break;
935+
case 'z':
936+
machine_output = true;
937+
break;
938+
default:
939+
std::clog << "Error: unknown option `" << flag << "'" << std::endl;
940+
return 2;
941+
}
942+
}
943+
++argi;
944+
}
945+
946+
if (repo_status_only) {
947+
if (show_encrypted_only || show_unencrypted_only) {
948+
std::clog << "Error: -e and -u options cannot be used with -r" << std::endl;
949+
return 2;
950+
}
951+
if (fix_problems) {
952+
std::clog << "Error: -f option cannot be used with -r" << std::endl;
953+
return 2;
954+
}
955+
if (argc - argi != 0) {
956+
std::clog << "Error: filenames cannot be specified when -r is used" << std::endl;
957+
return 2;
958+
}
959+
}
960+
961+
if (show_encrypted_only && show_unencrypted_only) {
962+
std::clog << "Error: -e and -u options are mutually exclusive" << std::endl;
963+
return 2;
964+
}
965+
966+
if (fix_problems && (show_encrypted_only || show_unencrypted_only)) {
967+
std::clog << "Error: -e and -u options cannot be used with -f" << std::endl;
968+
return 2;
969+
}
970+
971+
if (machine_output) {
972+
// TODO: implement machine-parseable output
973+
std::clog << "Sorry, machine-parseable output is not yet implemented" << std::endl;
974+
return 2;
975+
}
976+
977+
if (argc - argi == 0) {
978+
// TODO: check repo status:
979+
// is it set up for git-crypt?
980+
// which keys are unlocked?
981+
// --> check for filter config (see configure_git_filters()) and corresponding internal key
982+
983+
if (repo_status_only) {
984+
return 0;
985+
}
986+
}
987+
988+
// git ls-files -cotsz --exclude-standard ...
989+
std::vector<std::string> command;
990+
command.push_back("git");
991+
command.push_back("ls-files");
992+
command.push_back("-cotsz");
993+
command.push_back("--exclude-standard");
994+
command.push_back("--");
995+
if (argc - argi == 0) {
996+
const std::string path_to_top(get_path_to_top());
997+
if (!path_to_top.empty()) {
998+
command.push_back(path_to_top);
999+
}
1000+
} else {
1001+
for (int i = argi; i < argc; ++i) {
1002+
command.push_back(argv[i]);
1003+
}
1004+
}
1005+
1006+
std::stringstream output;
1007+
if (!successful_exit(exec_command(command, output))) {
1008+
throw Error("'git ls-files' failed - is this a Git repository?");
1009+
}
1010+
1011+
// Output looks like (w/o newlines):
1012+
// ? .gitignore\0
1013+
// H 100644 06ec22e5ed0de9280731ef000a10f9c3fbc26338 0 afile\0
1014+
1015+
std::vector<std::string> files;
1016+
bool attribute_errors = false;
1017+
bool unencrypted_blob_errors = false;
1018+
unsigned int nbr_of_fixed_blobs = 0;
1019+
unsigned int nbr_of_fix_errors = 0;
1020+
1021+
while (output.peek() != -1) {
1022+
std::string tag;
1023+
std::string object_id;
1024+
std::string filename;
1025+
output >> tag;
1026+
if (tag != "?") {
1027+
std::string mode;
1028+
std::string stage;
1029+
output >> mode >> object_id >> stage;
1030+
}
1031+
output >> std::ws;
1032+
std::getline(output, filename, '\0');
1033+
1034+
// 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)
1035+
const std::pair<std::string, std::string> file_attrs(get_file_attributes(filename));
1036+
1037+
if (file_attrs.first == "git-crypt") {
1038+
// File is encrypted
1039+
const bool blob_is_unencrypted = !object_id.empty() && !check_if_blob_is_encrypted(object_id);
1040+
1041+
if (fix_problems && blob_is_unencrypted) {
1042+
if (access(filename.c_str(), F_OK) != 0) {
1043+
std::clog << "Error: " << filename << ": cannot stage encrypted version because not present in working tree - please 'git rm' or 'git checkout' it" << std::endl;
1044+
++nbr_of_fix_errors;
1045+
} else {
1046+
touch_file(filename);
1047+
std::vector<std::string> git_add_command;
1048+
git_add_command.push_back("git");
1049+
git_add_command.push_back("add");
1050+
git_add_command.push_back("--");
1051+
git_add_command.push_back(filename);
1052+
if (!successful_exit(exec_command(git_add_command))) {
1053+
throw Error("'git-add' failed");
1054+
}
1055+
if (check_if_file_is_encrypted(filename)) {
1056+
std::cout << filename << ": staged encrypted version" << std::endl;
1057+
++nbr_of_fixed_blobs;
1058+
} else {
1059+
std::clog << "Error: " << filename << ": still unencrypted even after staging" << std::endl;
1060+
++nbr_of_fix_errors;
1061+
}
1062+
}
1063+
} else if (!fix_problems && !show_unencrypted_only) {
1064+
std::cout << " encrypted: " << filename;
1065+
if (file_attrs.second != file_attrs.first) {
1066+
// but diff filter is not properly set
1067+
std::cout << " *** WARNING: diff=" << file_attrs.first << " attribute not set ***";
1068+
attribute_errors = true;
1069+
}
1070+
if (blob_is_unencrypted) {
1071+
// File not actually encrypted
1072+
std::cout << " *** WARNING: staged/committed version is NOT ENCRYPTED! ***";
1073+
unencrypted_blob_errors = true;
1074+
}
1075+
std::cout << std::endl;
1076+
}
1077+
} else {
1078+
// File not encrypted
1079+
if (!fix_problems && !show_encrypted_only) {
1080+
std::cout << "not encrypted: " << filename << std::endl;
1081+
}
1082+
}
1083+
}
1084+
1085+
int exit_status = 0;
1086+
1087+
if (attribute_errors) {
1088+
std::cout << std::endl;
1089+
std::cout << "Warning: one or more files has a git-crypt filter attribute but not a" << std::endl;
1090+
std::cout << "corresponding git-crypt diff attribute. For proper 'git diff' operation" << std::endl;
1091+
std::cout << "you should fix the .gitattributes file to specify the correct diff attribute." << std::endl;
1092+
std::cout << "Consult the git-crypt documentation for help." << std::endl;
1093+
exit_status = 1;
1094+
}
1095+
if (unencrypted_blob_errors) {
1096+
std::cout << std::endl;
1097+
std::cout << "Warning: one or more files is marked for encryption via .gitattributes but" << std::endl;
1098+
std::cout << "was staged and/or committed before the .gitattributes file was in effect." << std::endl;
1099+
std::cout << "Run 'git-crypt status' with the '-f' option to stage an encrypted version." << std::endl;
1100+
exit_status = 1;
1101+
}
1102+
if (nbr_of_fixed_blobs) {
1103+
std::cout << "Staged " << nbr_of_fixed_blobs << " encrypted file" << (nbr_of_fixed_blobs != 1 ? "s" : "") << "." << std::endl;
1104+
std::cout << "Warning: if these files were previously committed, unencrypted versions still exist in the repository's history." << std::endl;
1105+
}
1106+
if (nbr_of_fix_errors) {
1107+
std::cout << "Unable to stage " << nbr_of_fix_errors << " file" << (nbr_of_fix_errors != 1 ? "s" : "") << "." << std::endl;
1108+
exit_status = 1;
1109+
}
1110+
1111+
return exit_status;
1112+
}
1113+
Collapse file

‎commands.hpp‎

Copy file name to clipboardExpand all lines: commands.hpp
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ int export_key (int argc, char** argv);
5353
int keygen (int argc, char** argv);
5454
int migrate_key (int argc, char** argv);
5555
int refresh (int argc, char** argv);
56+
int status (int argc, char** argv);
5657

5758
#endif
5859

Collapse file

‎git-crypt.cpp‎

Copy file name to clipboardExpand all lines: git-crypt.cpp
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ try {
159159
if (std::strcmp(command, "refresh") == 0) {
160160
return refresh(argc, argv);
161161
}
162+
if (std::strcmp(command, "status") == 0) {
163+
return status(argc, argv);
164+
}
162165
// Plumbing commands (executed by git, not by user):
163166
if (std::strcmp(command, "clean") == 0) {
164167
return clean(argc, argv);

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.