mirror of
https://github.com/AGWA/git-crypt.git
synced 2025-12-29 06:05:34 -08:00
Initial GPG support
Run 'git-crypt add-collab KEYID' to authorize the holder of the given
GPG secret key to access the encrypted files. The secret git-crypt key
will be encrypted with the corresponding GPG public key and stored in the
root of the Git repository under .git-crypt/keys.
After cloning a repo with encrypted files, run 'git-crypt unlock'
(with no arguments) to use a secret key in your GPG keyring to unlock
the repository.
Multiple collaborators are supported, however commands to list the
collaborators ('git-crypt ls-collabs') and to remove a collaborator
('git-crypt rm-collab') are not yet supported.
This commit is contained in:
2
Makefile
2
Makefile
@@ -3,7 +3,7 @@ CXXFLAGS := -Wall -pedantic -ansi -Wno-long-long -O2
|
||||
LDFLAGS := -lcrypto
|
||||
PREFIX := /usr/local
|
||||
|
||||
OBJFILES = git-crypt.o commands.o crypto.o key.o util.o
|
||||
OBJFILES = git-crypt.o commands.o crypto.o gpg.o key.o util.o
|
||||
|
||||
all: git-crypt
|
||||
|
||||
|
||||
164
commands.cpp
164
commands.cpp
@@ -32,6 +32,7 @@
|
||||
#include "crypto.hpp"
|
||||
#include "util.hpp"
|
||||
#include "key.hpp"
|
||||
#include "gpg.hpp"
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
@@ -46,6 +47,7 @@
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <errno.h>
|
||||
#include <vector>
|
||||
|
||||
static void configure_git_filters ()
|
||||
{
|
||||
@@ -90,6 +92,26 @@ static std::string get_internal_key_path ()
|
||||
return path;
|
||||
}
|
||||
|
||||
static std::string get_repo_keys_path ()
|
||||
{
|
||||
std::stringstream output;
|
||||
|
||||
if (!successful_exit(exec_command("git rev-parse --show-toplevel", output))) {
|
||||
throw Error("'git rev-parse --show-toplevel' failed - is this a Git repository?");
|
||||
}
|
||||
|
||||
std::string path;
|
||||
std::getline(output, path);
|
||||
|
||||
if (path.empty()) {
|
||||
// could happen for a bare repo
|
||||
throw Error("Could not determine Git working tree - is this a non-bare repo?");
|
||||
}
|
||||
|
||||
path += "/.git-crypt/keys";
|
||||
return path;
|
||||
}
|
||||
|
||||
static void load_key (Key_file& key_file, const char* legacy_path =0)
|
||||
{
|
||||
if (legacy_path) {
|
||||
@@ -107,6 +129,53 @@ static void load_key (Key_file& key_file, const char* legacy_path =0)
|
||||
}
|
||||
}
|
||||
|
||||
static bool decrypt_repo_key (Key_file& key_file, uint32_t key_version, const std::vector<std::string>& secret_keys, const std::string& keys_path)
|
||||
{
|
||||
for (std::vector<std::string>::const_iterator seckey(secret_keys.begin()); seckey != secret_keys.end(); ++seckey) {
|
||||
std::ostringstream path_builder;
|
||||
path_builder << keys_path << '/' << key_version << '/' << *seckey;
|
||||
std::string path(path_builder.str());
|
||||
if (access(path.c_str(), F_OK) == 0) {
|
||||
std::stringstream decrypted_contents;
|
||||
gpg_decrypt_from_file(path, decrypted_contents);
|
||||
Key_file this_version_key_file;
|
||||
this_version_key_file.load(decrypted_contents);
|
||||
const Key_file::Entry* this_version_entry = this_version_key_file.get(key_version);
|
||||
if (!this_version_entry) {
|
||||
throw Error("GPG-encrypted keyfile is malformed because it does not contain expected key version");
|
||||
}
|
||||
key_file.add(key_version, *this_version_entry);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static void encrypt_repo_key (uint32_t key_version, const Key_file::Entry& key, const std::vector<std::string>& collab_keys, const std::string& keys_path, std::vector<std::string>* new_files)
|
||||
{
|
||||
std::string key_file_data;
|
||||
{
|
||||
Key_file this_version_key_file;
|
||||
this_version_key_file.add(key_version, key);
|
||||
key_file_data = this_version_key_file.store_to_string();
|
||||
}
|
||||
|
||||
for (std::vector<std::string>::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) {
|
||||
std::ostringstream path_builder;
|
||||
path_builder << keys_path << '/' << key_version << '/' << *collab;
|
||||
std::string path(path_builder.str());
|
||||
|
||||
if (access(path.c_str(), F_OK) == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
mkdir_parent(path);
|
||||
gpg_encrypt_to_file(path, *collab, key_file_data.data(), key_file_data.size());
|
||||
new_files->push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Encrypt contents of stdin and write to stdout
|
||||
int clean (int argc, char** argv)
|
||||
@@ -409,9 +478,17 @@ int unlock (int argc, char** argv)
|
||||
return 1;
|
||||
}
|
||||
} else {
|
||||
// Decrypt GPG key from root of repo (TODO NOW)
|
||||
std::clog << "Error: GPG support is not yet implemented" << std::endl;
|
||||
return 1;
|
||||
// Decrypt GPG key from root of repo
|
||||
std::string repo_keys_path(get_repo_keys_path());
|
||||
std::vector<std::string> gpg_secret_keys(gpg_list_secret_keys());
|
||||
// TODO: command-line option to specify the precise secret key to use
|
||||
// TODO: don't hard code key version 0 here - instead, determine the most recent version and try to decrypt that, or decrypt all versions if command-line option specified
|
||||
if (!decrypt_repo_key(key_file, 0, gpg_secret_keys, repo_keys_path)) {
|
||||
std::clog << "Error: no GPG secret key available to unlock this repository." << std::endl;
|
||||
std::clog << "To unlock with a shared symmetric key instead, specify the path to the symmetric key as an argument to 'git-crypt unlock'." << std::endl;
|
||||
std::clog << "To see a list of GPG keys authorized to unlock this repository, run 'git-crypt ls-collabs'." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
std::string internal_key_path(get_internal_key_path());
|
||||
// TODO: croak if internal_key_path already exists???
|
||||
@@ -449,17 +526,78 @@ int unlock (int argc, char** argv)
|
||||
return 0;
|
||||
}
|
||||
|
||||
int add_collab (int argc, char** argv) // TODO NOW
|
||||
int add_collab (int argc, char** argv)
|
||||
{
|
||||
// Sketch:
|
||||
// 1. Resolve the key ID to a long hex ID
|
||||
// 2. Create the in-repo key directory if it doesn't exist yet.
|
||||
// 3. For most recent key version KEY_VERSION (or for each key version KEY_VERSION if retroactive option specified):
|
||||
// Encrypt KEY_VERSION with the GPG key and stash it in .git-crypt/keys/KEY_VERSION/LONG_HEX_ID
|
||||
// if file already exists, print a notice and move on
|
||||
// 4. Commit the new file(s) (if any) with a meanignful commit message, unless -n was passed
|
||||
std::clog << "Error: add-collab is not yet implemented." << std::endl;
|
||||
return 1;
|
||||
if (argc == 0) {
|
||||
std::clog << "Usage: git-crypt add-collab GPG_USER_ID [...]" << std::endl;
|
||||
return 2;
|
||||
}
|
||||
|
||||
// build a list of key fingerprints for every collaborator specified on the command line
|
||||
std::vector<std::string> collab_keys;
|
||||
|
||||
for (int i = 0; i < argc; ++i) {
|
||||
std::vector<std::string> keys(gpg_lookup_key(argv[i]));
|
||||
if (keys.empty()) {
|
||||
std::clog << "Error: public key for '" << argv[i] << "' not found in your GPG keyring" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
if (keys.size() > 1) {
|
||||
std::clog << "Error: more than one public key matches '" << argv[i] << "' - please be more specific" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
collab_keys.push_back(keys[0]);
|
||||
}
|
||||
|
||||
// TODO: have a retroactive option to grant access to all key versions, not just the most recent
|
||||
Key_file key_file;
|
||||
load_key(key_file);
|
||||
const Key_file::Entry* key = key_file.get_latest();
|
||||
if (!key) {
|
||||
std::clog << "Error: key file is empty" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string keys_path(get_repo_keys_path());
|
||||
std::vector<std::string> new_files;
|
||||
|
||||
encrypt_repo_key(key_file.latest(), *key, collab_keys, keys_path, &new_files);
|
||||
|
||||
// add/commit the new files
|
||||
if (!new_files.empty()) {
|
||||
// git add ...
|
||||
std::string command("git add");
|
||||
for (std::vector<std::string>::const_iterator file(new_files.begin()); file != new_files.end(); ++file) {
|
||||
command += " ";
|
||||
command += escape_shell_arg(*file);
|
||||
}
|
||||
if (!successful_exit(system(command.c_str()))) {
|
||||
std::clog << "Error: 'git add' failed" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// git commit ...
|
||||
// TODO: add a command line option (-n perhaps) to inhibit committing
|
||||
std::ostringstream commit_message_builder;
|
||||
commit_message_builder << "Add " << collab_keys.size() << " git-crypt collaborator" << (collab_keys.size() != 1 ? "s" : "") << "\n\nNew collaborators:\n\n";
|
||||
for (std::vector<std::string>::const_iterator collab(collab_keys.begin()); collab != collab_keys.end(); ++collab) {
|
||||
commit_message_builder << '\t' << gpg_shorten_fingerprint(*collab) << ' ' << gpg_get_uid(*collab) << '\n';
|
||||
}
|
||||
|
||||
command = "git commit -m ";
|
||||
command += escape_shell_arg(commit_message_builder.str());
|
||||
for (std::vector<std::string>::const_iterator file(new_files.begin()); file != new_files.end(); ++file) {
|
||||
command += " ";
|
||||
command += escape_shell_arg(*file);
|
||||
}
|
||||
|
||||
if (!successful_exit(system(command.c_str()))) {
|
||||
std::clog << "Error: 'git commit' failed" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int rm_collab (int argc, char** argv) // TODO
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
#include "util.hpp"
|
||||
#include "crypto.hpp"
|
||||
#include "key.hpp"
|
||||
#include "gpg.hpp"
|
||||
#include <cstring>
|
||||
#include <unistd.h>
|
||||
#include <iostream>
|
||||
@@ -181,6 +182,9 @@ try {
|
||||
} catch (const Error& e) {
|
||||
std::cerr << "git-crypt: Error: " << e.message << std::endl;
|
||||
return 1;
|
||||
} catch (const Gpg_error& e) {
|
||||
std::cerr << "git-crypt: GPG error: " << e.message << std::endl;
|
||||
return 1;
|
||||
} catch (const System_error& e) {
|
||||
std::cerr << "git-crypt: " << e.action << ": ";
|
||||
if (!e.target.empty()) {
|
||||
|
||||
154
gpg.cpp
Normal file
154
gpg.cpp
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2014 Andrew Ayer
|
||||
*
|
||||
* This file is part of git-crypt.
|
||||
*
|
||||
* git-crypt is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* git-crypt is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with git-crypt. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional permission under GNU GPL version 3 section 7:
|
||||
*
|
||||
* If you modify the Program, or any covered work, by linking or
|
||||
* combining it with the OpenSSL project's OpenSSL library (or a
|
||||
* modified version of that library), containing parts covered by the
|
||||
* terms of the OpenSSL or SSLeay licenses, the licensors of the Program
|
||||
* grant you additional permission to convey the resulting work.
|
||||
* Corresponding Source for a non-source form of such a combination
|
||||
* shall include the source code for the parts of OpenSSL used as well
|
||||
* as that of the covered work.
|
||||
*/
|
||||
|
||||
#include "gpg.hpp"
|
||||
#include "util.hpp"
|
||||
#include <sstream>
|
||||
|
||||
static std::string gpg_nth_column (const std::string& line, unsigned int col)
|
||||
{
|
||||
std::string::size_type pos = 0;
|
||||
|
||||
for (unsigned int i = 0; i < col; ++i) {
|
||||
pos = line.find_first_of(':', pos);
|
||||
if (pos == std::string::npos) {
|
||||
throw Gpg_error("Malformed output from gpg");
|
||||
}
|
||||
pos = pos + 1;
|
||||
}
|
||||
|
||||
const std::string::size_type end_pos = line.find_first_of(':', pos);
|
||||
|
||||
return end_pos != std::string::npos ?
|
||||
line.substr(pos, end_pos - pos) :
|
||||
line.substr(pos);
|
||||
}
|
||||
|
||||
// given a key fingerprint, return the last 8 nibbles
|
||||
std::string gpg_shorten_fingerprint (const std::string& fingerprint)
|
||||
{
|
||||
return fingerprint.size() == 40 ? fingerprint.substr(32) : fingerprint;
|
||||
}
|
||||
|
||||
// given a key fingerprint, return the key's UID (e.g. "John Smith <jsmith@example.com>")
|
||||
std::string gpg_get_uid (const std::string& fingerprint)
|
||||
{
|
||||
// gpg --batch --with-colons --fixed-list-mode --list-keys 0x7A399B2DB06D039020CD1CE1D0F3702D61489532
|
||||
std::string command("gpg --batch --with-colons --fixed-list-mode --list-keys ");
|
||||
command += escape_shell_arg("0x" + fingerprint);
|
||||
std::stringstream command_output;
|
||||
if (!successful_exit(exec_command(command.c_str(), command_output))) {
|
||||
// This could happen if the keyring does not contain a public key with this fingerprint
|
||||
return "";
|
||||
}
|
||||
|
||||
while (command_output.peek() != -1) {
|
||||
std::string line;
|
||||
std::getline(command_output, line);
|
||||
if (line.substr(0, 4) == "uid:") {
|
||||
// uid:u::::1395975462::AB97D6E3E5D8789988CA55E5F77D9E7397D05229::John Smith <jsmith@example.com>:
|
||||
// want the 9th column (counting from 0)
|
||||
return gpg_nth_column(line, 9);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
// return a list of fingerprints of public keys matching the given search query (such as jsmith@example.com)
|
||||
std::vector<std::string> gpg_lookup_key (const std::string& query)
|
||||
{
|
||||
std::vector<std::string> fingerprints;
|
||||
|
||||
// gpg --batch --with-colons --fingerprint --list-keys jsmith@example.com
|
||||
std::string command("gpg --batch --with-colons --fingerprint --list-keys ");
|
||||
command += escape_shell_arg(query);
|
||||
std::stringstream command_output;
|
||||
if (successful_exit(exec_command(command.c_str(), command_output))) {
|
||||
while (command_output.peek() != -1) {
|
||||
std::string line;
|
||||
std::getline(command_output, line);
|
||||
if (line.substr(0, 4) == "fpr:") {
|
||||
// fpr:::::::::7A399B2DB06D039020CD1CE1D0F3702D61489532:
|
||||
// want the 9th column (counting from 0)
|
||||
fingerprints.push_back(gpg_nth_column(line, 9));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fingerprints;
|
||||
}
|
||||
|
||||
std::vector<std::string> gpg_list_secret_keys ()
|
||||
{
|
||||
// gpg --batch --with-colons --list-secret-keys --fingerprint
|
||||
std::stringstream command_output;
|
||||
if (!successful_exit(exec_command("gpg --batch --with-colons --list-secret-keys --fingerprint", command_output))) {
|
||||
throw Gpg_error("gpg --list-secret-keys failed");
|
||||
}
|
||||
|
||||
std::vector<std::string> secret_keys;
|
||||
|
||||
while (command_output.peek() != -1) {
|
||||
std::string line;
|
||||
std::getline(command_output, line);
|
||||
if (line.substr(0, 4) == "fpr:") {
|
||||
// fpr:::::::::7A399B2DB06D039020CD1CE1D0F3702D61489532:
|
||||
// want the 9th column (counting from 0)
|
||||
secret_keys.push_back(gpg_nth_column(line, 9));
|
||||
}
|
||||
}
|
||||
|
||||
return secret_keys;
|
||||
}
|
||||
|
||||
void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, const char* p, size_t len)
|
||||
{
|
||||
// gpg --batch -o FILENAME -r RECIPIENT -e
|
||||
std::string command("gpg --batch -o ");
|
||||
command += escape_shell_arg(filename);
|
||||
command += " -r ";
|
||||
command += escape_shell_arg("0x" + recipient_fingerprint);
|
||||
command += " -e";
|
||||
if (!successful_exit(exec_command_with_input(command.c_str(), p, len))) {
|
||||
throw Gpg_error("Failed to encrypt");
|
||||
}
|
||||
}
|
||||
|
||||
void gpg_decrypt_from_file (const std::string& filename, std::ostream& output)
|
||||
{
|
||||
// gpg -q -d
|
||||
std::string command("gpg -q -d ");
|
||||
command += escape_shell_arg(filename);
|
||||
if (!successful_exit(exec_command(command.c_str(), output))) {
|
||||
throw Gpg_error("Failed to decrypt");
|
||||
}
|
||||
}
|
||||
|
||||
51
gpg.hpp
Normal file
51
gpg.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright 2014 Andrew Ayer
|
||||
*
|
||||
* This file is part of git-crypt.
|
||||
*
|
||||
* git-crypt is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* git-crypt is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with git-crypt. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* Additional permission under GNU GPL version 3 section 7:
|
||||
*
|
||||
* If you modify the Program, or any covered work, by linking or
|
||||
* combining it with the OpenSSL project's OpenSSL library (or a
|
||||
* modified version of that library), containing parts covered by the
|
||||
* terms of the OpenSSL or SSLeay licenses, the licensors of the Program
|
||||
* grant you additional permission to convey the resulting work.
|
||||
* Corresponding Source for a non-source form of such a combination
|
||||
* shall include the source code for the parts of OpenSSL used as well
|
||||
* as that of the covered work.
|
||||
*/
|
||||
|
||||
#ifndef _GPG_H
|
||||
#define _GPG_H
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
|
||||
struct Gpg_error {
|
||||
std::string message;
|
||||
|
||||
explicit Gpg_error (std::string m) : message(m) { }
|
||||
};
|
||||
|
||||
std::string gpg_shorten_fingerprint (const std::string& fingerprint);
|
||||
std::string gpg_get_uid (const std::string& fingerprint);
|
||||
std::vector<std::string> gpg_lookup_key (const std::string& query);
|
||||
std::vector<std::string> gpg_list_secret_keys ();
|
||||
void gpg_encrypt_to_file (const std::string& filename, const std::string& recipient_fingerprint, const char* p, size_t len);
|
||||
void gpg_decrypt_from_file (const std::string& filename, std::ostream&);
|
||||
|
||||
#endif
|
||||
8
key.cpp
8
key.cpp
@@ -36,6 +36,7 @@
|
||||
#include <fstream>
|
||||
#include <istream>
|
||||
#include <ostream>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
@@ -146,6 +147,13 @@ bool Key_file::store_to_file (const char* key_file_name) const
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string Key_file::store_to_string () const
|
||||
{
|
||||
std::ostringstream ss;
|
||||
store(ss);
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
void Key_file::generate ()
|
||||
{
|
||||
entries[is_empty() ? 0 : latest() + 1].generate();
|
||||
|
||||
3
key.hpp
3
key.hpp
@@ -35,6 +35,7 @@
|
||||
#include <functional>
|
||||
#include <stdint.h>
|
||||
#include <iosfwd>
|
||||
#include <string>
|
||||
|
||||
enum {
|
||||
HMAC_KEY_LEN = 64,
|
||||
@@ -67,6 +68,8 @@ public:
|
||||
bool load_from_file (const char* filename);
|
||||
bool store_to_file (const char* filename) const;
|
||||
|
||||
std::string store_to_string () const;
|
||||
|
||||
void generate ();
|
||||
|
||||
bool is_empty () const { return entries.empty(); }
|
||||
|
||||
42
util.cpp
42
util.cpp
@@ -146,6 +146,48 @@ int exec_command (const char* command, std::ostream& output)
|
||||
return status;
|
||||
}
|
||||
|
||||
int exec_command_with_input (const char* command, const char* p, size_t len)
|
||||
{
|
||||
int pipefd[2];
|
||||
if (pipe(pipefd) == -1) {
|
||||
throw System_error("pipe", "", errno);
|
||||
}
|
||||
pid_t child = fork();
|
||||
if (child == -1) {
|
||||
int fork_errno = errno;
|
||||
close(pipefd[0]);
|
||||
close(pipefd[1]);
|
||||
throw System_error("fork", "", fork_errno);
|
||||
}
|
||||
if (child == 0) {
|
||||
close(pipefd[1]);
|
||||
if (pipefd[0] != 0) {
|
||||
dup2(pipefd[0], 0);
|
||||
close(pipefd[0]);
|
||||
}
|
||||
execl("/bin/sh", "sh", "-c", command, NULL);
|
||||
perror("/bin/sh");
|
||||
_exit(-1);
|
||||
}
|
||||
close(pipefd[0]);
|
||||
while (len > 0) {
|
||||
ssize_t bytes_written = write(pipefd[1], p, len);
|
||||
if (bytes_written == -1) {
|
||||
int write_errno = errno;
|
||||
close(pipefd[1]);
|
||||
throw System_error("write", "", write_errno);
|
||||
}
|
||||
p += bytes_written;
|
||||
len -= bytes_written;
|
||||
}
|
||||
close(pipefd[1]);
|
||||
int status = 0;
|
||||
if (waitpid(child, &status, 0) == -1) {
|
||||
throw System_error("waitpid", "", errno);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
bool successful_exit (int status)
|
||||
{
|
||||
return status != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0;
|
||||
|
||||
1
util.hpp
1
util.hpp
@@ -48,6 +48,7 @@ void mkdir_parent (const std::string& path); // Create parent directories of pa
|
||||
std::string readlink (const char* pathname);
|
||||
std::string our_exe_path ();
|
||||
int exec_command (const char* command, std::ostream& output);
|
||||
int exec_command_with_input (const char* command, const char* p, size_t len);
|
||||
bool successful_exit (int status);
|
||||
void open_tempfile (std::fstream&, std::ios_base::openmode);
|
||||
std::string escape_shell_arg (const std::string&);
|
||||
|
||||
Reference in New Issue
Block a user