diff --git a/.gitignore b/.gitignore index a6d30c82..2d7e18ff 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # Directories src/.bin +cxx/build .cache/ **/__pycache__ .pytest_cache diff --git a/cxx/.ycm_extra_conf.py b/cxx/.ycm_extra_conf.py new file mode 100644 index 00000000..2580ab30 --- /dev/null +++ b/cxx/.ycm_extra_conf.py @@ -0,0 +1,137 @@ +""" +YCM Config for gitstatus. +""" +import os +import ycm_core + +ROOT = os.path.abspath(os.path.dirname(__file__)) +if os.path.dirname(__file__) == '': + ROOT = os.getcwd() + +# Extensions that will be looked for. +SOURCE_EXTENSIONS = ['.c', '.cpp', '.cxx', '.cc', '.m', '.mm'] +HEADER_EXTENSIONS = ['.h', '.hpp', '.hxx', '.hh'] + +# These are the compilation flags that will be used by YCM to check c files. +FLAGS = [ + # C Flags + # Debug options: https://gcc.gnu.org/onlinedocs/gcc/Debugging-Options.html + '-ggdb', + '-Og', + '-Wall', + '-Wextra', + '-Werror=format-security', + '-Wundef', + '-Winline', + '-Wshadow', + '-Wunused', + '-Winit-self', + '-pedantic', # Warn if violating std, pedantic-errors makes it an error + '-pipe', + '-fexceptions', + '-Weffc++', + # Defines + '-D_FORTIFY_SOURCE=2', + '-D_GLIBCXX_ASSERTIONS', + '-std=c++11', + # Need to tell clang language of headers. + # For a C project set to 'c' instead of 'c++'. + '-x', + 'c++', + # Includes. + '-isystem', + '/usr/include', + '-isystem', + '/usr/include/c++/8', + '-isystem', + '/usr/local/include', +] + +# Set this to the absolute path to the folder (NOT the file!) containing the +# compile_commands.json file to use that instead of 'flags'. See here for +# more details: http://clang.llvm.org/docs/JSONCompilationDatabase.html +# +# Most projects will NOT need to set this to anything; you can just change the +# 'flags' list of compilation flags. Notice that YCM itself uses that approach. +COMPILATION_DB_FOLDER = os.path.join(ROOT, 'build') +DB = None +if os.path.isdir(COMPILATION_DB_FOLDER): + DB = ycm_core.CompilationDatabase(COMPILATION_DB_FOLDER) + + +def MakeRelativePathsInFlagsAbsolute(flags, working_directory): + """ Processes paths in FLAGS var and makes them absolute. """ + if not working_directory: + return list(flags) + new_flags = [] + make_next_absolute = False + path_flags = ['-isystem', '-I', '-iquote', '--sysroot='] + for flag in flags: + new_flag = flag + + if make_next_absolute: + make_next_absolute = False + if not flag.startswith('/'): + new_flag = os.path.join(working_directory, flag) + + for path_flag in path_flags: + if flag == path_flag: + make_next_absolute = True + break + + if flag.startswith(path_flag): + path = flag[len(path_flag):] + new_flag = path_flag + os.path.join(working_directory, path) + break + + if new_flag: + new_flags.append(new_flag) + return new_flags + + +def IsHeaderFile(filename): + """ Return true only if filename ends in a header extension. """ + extension = os.path.splitext(filename)[1] + return extension in HEADER_EXTENSIONS + + +def GetCompilationInfoForFile(filename): + """ The compilation_commands.json file generated by CMake does not have + entries for header files. So we do our best by asking the db for + flags for a corresponding source file, if any. If one exists, the + flags for that file should be good enough. + """ + if IsHeaderFile(filename): + basename = os.path.splitext(filename)[0] + for extension in SOURCE_EXTENSIONS: + replacement_file = basename + extension + if os.path.exists(replacement_file): + compilation_info = DB.GetCompilationInfoForFile( + replacement_file) + if compilation_info.compiler_flags_: + return compilation_info + return None + return DB.GetCompilationInfoForFile(filename) + + +def FlagsForFile(filename, **kwargs): + """ Given a filename, return the flags to compile it. """ + with open("/tmp/ttt", 'w') as fout: + if DB: + # Bear in mind that compilation_info.compiler_flags_ does NOT return a + # python list, but a "list-like" StringVec object + compilation_info = GetCompilationInfoForFile(filename) + fout.write("comp_info" + str(compilation_info) + "\n") + if not compilation_info: + return None + + final_flags = MakeRelativePathsInFlagsAbsolute( + compilation_info.compiler_flags_, + compilation_info.compiler_working_dir_) + fout.write("final_flags" + str(final_flags)) + + else: + relative_to = os.path.dirname(os.path.abspath(__file__)) + final_flags = MakeRelativePathsInFlagsAbsolute(FLAGS, relative_to) + + return {'flags': final_flags, 'do_cache': True} diff --git a/cxx/CMakeLists.txt b/cxx/CMakeLists.txt new file mode 100644 index 00000000..d5ceee51 --- /dev/null +++ b/cxx/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required (VERSION 3.1.0) +project(gitstatus) + +# Pregenerate step in build, generate constants before compilation +add_executable(gen_const src/gen_const.cc) +# add the command to generate the source code +add_custom_command ( + OUTPUT ${CMAKE_BINARY_DIR}/src/const.h + COMMAND gen_const ${CMAKE_BINARY_DIR}/src/const.h + DEPENDS gen_const +) + +# Build core project +add_library(gstat src/gstat.cc ${CMAKE_BINARY_DIR}/src/const.h) +add_executable(gitstatus src/main.cc) +target_link_libraries(gitstatus gstat) + +# Define ${CMAKE_INSTALL_...} variables +include(GNUInstallDirs) + +# Specify directories required +link_directories(${CMAKE_BINARY_DIR}/lib/installed/${CMAKE_INSTALL_LIBDIR}) +include_directories( + ${CMAKE_BINARY_DIR}/lib/installed/${CMAKE_INSTALL_INCLUDEDIR} + ${CMAKE_BINARY_DIR} + ${CMAKE_CURRENT_SOURCE_DIR} # Make project includes from root +) + +# Optional requirement for test suite +find_package(Threads) +if(${Threads_FOUND}) + # Fetch googletest inside build dir + include(ExternalProject) + ExternalProject_Add(googletest + PREFIX "${CMAKE_BINARY_DIR}/lib" + GIT_REPOSITORY "https://github.com/google/googletest.git" + GIT_TAG "master" + CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/lib/installed + ) + + # Prevent build on all targets build + set_target_properties(googletest PROPERTIES EXCLUDE_FROM_ALL TRUE) + + # Test + add_executable(tests test/test_gstat.cc) + target_link_libraries(tests gstat gtest Threads::Threads) + + # Make sure third-party is built before executable + add_dependencies(tests googletest) + set_target_properties(tests PROPERTIES EXCLUDE_FROM_ALL TRUE) + + # Add post build running of tests + add_custom_command( + TARGET tests + COMMENT "Run google tests post build" + POST_BUILD + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMAND "./tests" "--gtest_color=yes" + ) +endif() + +# Flags & compiler +set(CMAKE_CXX_FLAGS "-std=c++11 -pedantic -pipe -fexceptions -Winline") +if(CMAKE_BUILD_TYPE STREQUAL "Release") + add_definitions(-D_FORTIFY_SOURCE=2) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Ofast") +else() + + add_definitions(-D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS) + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -ggdb -Og -Wall -Wextra -Werror=format-security") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wshadow -Wunused -Winit-self -Weffc++") + # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pg") # GProf + # set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fprofile-arcs -ftest-coverage") # GCov +endif() + +# Will export build commands to build dir, for YCM users +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Just for coverage, still not working +# TODO: Make it go! +# set(CMAKE_CXX_FLAGS_COVERAGE "-ggdb -O0 --coverage -fprofile-arcs -ftest-coverage -Wall -Wextra") +# if(CMAKE_BUILD_TYPE STREQUAL "Coverage") + # set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/scripts/cmake) + # include(CodeCoverage) + # APPEND_COVERAGE_COMPILER_FLAGS() + # setup_target_for_coverage(NAME coverage EXECUTABLE tests DEPENDENCIES googletest) + + # # set(CMAKE_CXX_FLAGS_COVERAGE "-ggdb -O0 --coverage -fprofile-arcs -ftest-coverage -Wall -Wextra") + # # SET(CMAKE_CXX_FLAGS "-g -O0 --coverage -fprofile-arcs -ftest-coverage") + # # SET(CMAKE_C_FLAGS "-g -O0 --coverage -fprofile-arcs -ftest-coverage") +# endif(CMAKE_BUILD_TYPE STREQUAL "Coverage") + +# TODO: Better way? +# set(CMAKE_CXX_STANDARD 11) +# set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/cxx/LICENSE.md b/cxx/LICENSE.md new file mode 100644 index 00000000..171dd512 --- /dev/null +++ b/cxx/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License + +Copyright (c) 2018 Jeremy Pallats/starcraft.man + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/cxx/README.md b/cxx/README.md new file mode 100644 index 00000000..d3c1dc71 --- /dev/null +++ b/cxx/README.md @@ -0,0 +1,21 @@ +# Gitstatus (C++ Implementation) + +Default build is debug. + +## Requirements + +1. `cmake`, version >= 3.1.0 +2. C++ compiler implementing c++11 standard, for example GCC version >= 5.0 +3. pthread library for google tests + +## To Build + +1. `mkdir ./build && cd build` +1. `cmake -DCMAKE_BUILD_TYPE=Release .. && make` + +For debug version, just omit -DCMAKE_BUILD_TYPE flag. + +## To Test + +1. `mkdir ./build && cd build` +1. `cmake .. && make tests` diff --git a/cxx/build.sh b/cxx/build.sh new file mode 100755 index 00000000..4ef1a92b --- /dev/null +++ b/cxx/build.sh @@ -0,0 +1,16 @@ +#!/bin/sh +BUILD_D="$(dirname "$(readlink -f "$0")")/build" + +echo "This will build the c++ based version of gitstatus." +echo "To build, both cmake and a C++11 compliant compiler are required." +echo "-----------------------------------------------------------------" + +command rm -rf "$BUILD_D" +command mkdir -p "$BUILD_D" +cd "$BUILD_D" +cmake .. -DCMAKE_BUILD_TYPE=Release +make + +echo "-----------------------------------------------------------------" +echo "To use the cxx version, put following in your ~/.zshrc" +echo " export GIT_PROMPT_EXECUTABLE=cxx" diff --git a/cxx/src/gen_const.cc b/cxx/src/gen_const.cc new file mode 100644 index 00000000..d633e140 --- /dev/null +++ b/cxx/src/gen_const.cc @@ -0,0 +1,67 @@ +/** + * The MIT License + * + * Copyright (c) 2018 Jeremy Pallats/starcraft.man + * + * Generate a simple header with predefined hashes for runtime use. + */ +#include + +#include +#include +#include +#include +#include +#include + +#include "src/gstat.h" + +#define HEADER_GUARD "CXX_BUILD_SRC_CONST_H_" + +/** + * Just a simple mkdir clone, recurse until exists. + * + * Raises: std::runtime_error - Unable to complete mkdir + */ +void mkdir_recurse(const std::string &path) { + if (path.length() == 0 || path == ROOT_DIR || gstat::file_is_dir(path)) { + return; + } + + mkdir_recurse(gstat::dirname(path)); + + uint_fast8_t err = mkdir(path.c_str(), S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH); + if (err != 0) { + throw std::runtime_error("Unable to create dir: " + path); + } +} + +int main(int argc, char *argv[]) { + if (argc != 2) { + std::cout << "Usage: " << argv[0] << " filename" << std::endl; + return 1; + } + + std::string fname(argv[1]); + mkdir_recurse(gstat::dirname(fname)); + + std::ofstream fout(fname.c_str()); + if (!fout.good()) { + std::cout << "CRITIAL ERROR: Cannot open file!" << std::endl; + return 1; + } + + fout << "#ifndef " << HEADER_GUARD << std::endl + << "#define " << HEADER_GUARD << std::endl < vals({ "AA", "AU", "DD", "DU", "UA", "UD", "UU" }); + for (std::initializer_list::const_iterator itr = vals.begin(); itr != vals.end(); ++itr) { + fout << "#define HASH_CASE_" << *itr << " " + << gstat::hash_two_places(*itr) << std::endl; + } + + fout << std::endl << "#endif // " << HEADER_GUARD << std::endl; + fout.close(); + + return 0; +} diff --git a/cxx/src/gstat.cc b/cxx/src/gstat.cc new file mode 100644 index 00000000..88e39c1c --- /dev/null +++ b/cxx/src/gstat.cc @@ -0,0 +1,366 @@ +/** + * The MIT License + * + * Copyright (c) 2018 Jeremy Pallats/starcraft.man + * + */ +#include "src/gstat.h" + +// Linux only +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "src/const.h" + +namespace gstat { + +std::ostream & operator<<(std::ostream& os, const GBranch &info) { + os << info.branch << " " + << info.upstream << " " + << info.local << " "; + + return os; +} + +std::ostream & operator<<(std::ostream& os, const GRemote &info) { + os << info.ahead << " " + << info.behind << " "; + + return os; +} + +std::ostream & operator<<(std::ostream& os, const GStats &info) { + os << info.staged << " " + << info.conflicts << " " + << info.changed << " " + << info.untracked << " "; + + return os; +} + +/** + * This sets the tree_d when inside a git worktree. + * In this case, the .git folder is instead a file with the path to the + * worktree folder in the original repository. + * Relative this new folder, scan up until we hit the root. + * + * Returns: std::string - The path to the tree directory. + */ +void GPaths::set_tree_d() { + // Format of the file: + // gitdir: /tmp/g/.git/worktrees/wg + std::ifstream fin(this->tree_d.c_str()); + if (!fin.good()) { + throw std::runtime_error("Could not open wotkree file: " + this->tree_d); + } + fin >> this->tree_d >> this->tree_d; + fin.close(); + + this->git_root = this->tree_d; + while (basename(this->git_root) != ".git") { + this->git_root = dirname(this->git_root); + } +} + +std::string GPaths::head() { + return join({this->tree_d, "HEAD"}); +} + +std::string GPaths::merge() { + return join({this->tree_d, "MERGE_HEAD"}); +} + +std::string GPaths::rebase() { + return join({this->tree_d, "rebase-apply"}); +} + +std::string GPaths::stash() { + return join({this->git_root, "logs", "refs", "stash"}); +} + +/** + * Does the current STDIN file have __ANY__ input. + * + * Returns: Bool + */ +bool stdin_has_input() { + struct pollfd fds; + fds.fd = 0; + fds.events = POLLIN; + + return poll(&fds, 1, 0) == 1; +} + +/** + * Run a command on the system in the current directory. + * + * Return: std:string of output captured + * + * Raises: runtime_error : Popen failed, returned NULL. + */ +std::string run_cmd(const char *cmd) { + FILE *pfile = popen(cmd, "r"); + if (pfile == nullptr) { + throw std::runtime_error("Could not execute cmd: " + std::string(cmd)); + } + + std::array buffer; + std::string text; + while (fgets(buffer.data(), 1000, pfile) != nullptr) { + text.append(buffer.data()); + } + pclose(pfile); + + return text; +} + +/** + * Get the current working directory. + * + * Returns: A string of the path. + * + * Raises: runtime_error: Failed to get CWD. + */ +std::string get_cwd() { + std::array buffer; + + if ((getcwd(buffer.data(), PATH_MAX)) == nullptr) { + throw std::runtime_error("Unable to get CWD"); + } + + return std::string(buffer.data()); +} + +/** + * Move upward from the current directory until a directory + * with `.git` is found. + * + * Returns: std::string - The path with git. + */ +std::string find_git_root() { + std::string cwd = get_cwd(); + std::string git_leaf = ".git"; + std::string git_root = join({cwd, git_leaf}); + + while (cwd != ROOT_DIR) { + if (file_exists(git_root)) { + return git_root; + } + + cwd = dirname(cwd); + git_root = join({cwd, git_leaf}); + } + + throw std::runtime_error("Could not find a git directory!"); +} + +/** + * Parse the branch of a string line. + * + * Returns: GBranch structure. + */ +GBranch parse_branch(const std::string &branch_line, + const std::string &head_file) { + GBranch result; + result.local = 1; + + std::string temp = branch_line.substr(3); + std::size_t found = temp.rfind(" ["); + if (found != std::string::npos) { + temp = temp.substr(0, found); + } + + found = temp.find("..."); + if (found != std::string::npos) { + result.branch = temp.substr(0, found); + result.upstream = temp.substr(found + 3); + result.local = 0; + } else if (temp.find("(no branch)") != std::string::npos) { + result.local = 0; + std::ifstream fin(head_file.c_str()); + if (fin.good()) { + fin >> result.branch; + } else { + throw std::runtime_error("Failed to get hash!"); + } + fin.close(); + } else if (temp.find("Initial commit") != std::string::npos || + temp.find("No commits yet") != std::string::npos) { + result.branch = temp.substr(temp.rfind(" ") + 1); + } else { + result.branch = temp; + } + + return result; +} + +/* + * Parse the remote tracking portion of git status. + * + * Returns: GRemote structure. + */ +GRemote parse_remote(const std::string &branch_line) { + GRemote remote; + std::string temp = branch_line; + + std::size_t found = branch_line.find(" ["); + // Check for remote tracking (example: [ahead 2]) + if (found == std::string::npos || + branch_line.at(branch_line.length() - 1) != ']') { + return remote; + } + + // Only the remote tracking section remains + temp = temp.substr(found + 2, temp.length() -1); + + if (temp.length() != 0 && temp.find("ahead") != std::string::npos) { + temp = temp.replace(temp.begin(), temp.begin() + 6, ""); + std::string part; + while (temp.length() != 0) { + part += temp.at(0); + temp = temp.substr(1); + if (temp.length() == 0 || std::isdigit(temp.at(0)) == 0) { + break; + } + } + + remote.ahead = std::stoi(part); + } + + if (temp.length() != 0 && temp.at(0) == ',') { + temp = temp.substr(1); + } + + if (temp.length() != 0 && temp.find("behind") != std::string::npos) { + temp = temp.replace(temp.begin(), temp.begin() + 7, ""); + remote.behind = std::stoi(temp); + } + + return remote; +} + +/** + * Parses the status information from porcelain output. + * + * Returns: GStats structure. + */ +GStats parse_stats(const lines_iter_t &start, const lines_iter_t &end) { + GStats stats; + + for (lines_iter_t itr = start; itr != end; ++itr) { + if (itr->at(0) == '?') { + stats.untracked++; + continue; + } + + switch (hash_two_places(*itr)) { + case HASH_CASE_AA: + case HASH_CASE_AU: + case HASH_CASE_DD: + case HASH_CASE_DU: + case HASH_CASE_UA: + case HASH_CASE_UD: + case HASH_CASE_UU: + stats.conflicts++; + continue; + } + + switch (itr->at(0)) { + case 'A': + case 'C': + case 'D': + case 'M': + case 'R': + stats.staged++; + } + + switch (itr->at(1)) { + case 'C': + case 'D': + case 'M': + case 'R': + stats.changed++; + } + } + + return stats; +} + +/** + * Returns: std:string - The # of stashes on repo + */ +std::string stash_count(const std::string &stash_file) { + std::uint_fast32_t count = 0; + + std::ifstream fin(stash_file.c_str()); + while (fin.good()) { + std::string buffer; + std::getline(fin, buffer); + if (buffer != "") { + ++count; + } + } + fin.close(); + + return std::to_string(count); +} + +/** + * Returns: + * - "0": No active rebase + * - "1/4": Rebase in progress, commit 1 of 4 + */ +std::string rebase_progress(const std::string &rebase_d) { + std::string result = "0"; + std::string temp; + + std::ifstream next(join({rebase_d, "next"}).c_str()); + std::ifstream last(join({rebase_d, "last"}).c_str()); + if (next.good() && last.good()) { + result.clear(); + last >> temp; + next >> result; + result += "/" + temp; + } + last.close(); + next.close(); + + return result; +} + +/** + * Take input and produce the required output per specification. + */ +std::string current_gitstatus(const std::vector &lines) { + GPaths path(gstat::find_git_root()); + GBranch info = parse_branch(lines.front(), path.head()); + GRemote remote = parse_remote(lines.front()); + GStats stats = parse_stats(lines.begin() + 1, lines.end()); + std::string stashes = stash_count(path.stash()); + std::string merge = std::to_string( + static_cast(file_exists(path.merge()))); + std::string rebase = rebase_progress(path.rebase()); + + std::ostringstream ss; + ss << info.branch << " " << remote << stats << stashes << " " + << std::to_string(info.local) << " " << info.upstream << " " + << merge << " " << rebase;; + + return ss.str(); +} + +} // namespace gstat diff --git a/cxx/src/gstat.h b/cxx/src/gstat.h new file mode 100644 index 00000000..0246e8d4 --- /dev/null +++ b/cxx/src/gstat.h @@ -0,0 +1,150 @@ +/** + * The MIT License + * + * Copyright (c) 2018 Jeremy Pallats/starcraft.man + */ +#ifndef CXX_SRC_GSTAT_H_ +#define CXX_SRC_GSTAT_H_ + +#include + +#include +#include +#include + +#define PATH_SEP "/" +#define ROOT_DIR "/" + +namespace gstat { + +typedef std::vector::const_iterator lines_iter_t; + +class GBranch { + public: + GBranch() noexcept: branch(""), upstream(".."), local(0) {} + ~GBranch() noexcept {}; // Suppress warning + + friend std::ostream & operator<<(std::iostream& os, const GBranch &info); + + std::string branch; + std::string upstream; + std::uint_fast8_t local; +}; + +class GRemote { + public: + GRemote() noexcept: ahead(0), behind(0) {} + + friend std::ostream & operator<<(std::iostream& os, const GRemote &info); + + std::uint_fast32_t ahead; + std::uint_fast32_t behind; +}; + +class GStats { + public: + GStats() noexcept: changed(0), conflicts(0), staged(0), untracked(0) {} + + friend std::ostream & operator<<(std::iostream& os, const GRemote &info); + + std::uint_fast32_t changed; + std::uint_fast32_t conflicts; + std::uint_fast32_t staged; + std::uint_fast32_t untracked; +}; + +bool file_is_dir(const std::string &path); // Inlined below +class GPaths { + public: + explicit GPaths(const std::string &_git_root): git_root(_git_root), tree_d(_git_root) { + if (!file_is_dir(this->tree_d)) { + this->set_tree_d(); + } + } + void set_tree_d(); + + std::string head(); + std::string merge(); + std::string rebase(); + std::string stash(); + + std::string git_root; + std::string tree_d; +}; + +GBranch parse_branch(const std::string &branch_line, + const std::string &head_file); +GRemote parse_remote(const std::string &branch_line); +GStats parse_stats(const lines_iter_t &start, const lines_iter_t &end); +std::string current_gitstatus(const std::vector &lines); +std::string stash_count(const std::string &stash_file); +std::string rebase_progress(const std::string &rebase_d); +std::string run_cmd(const char *cmd); +std::string get_cwd(); +std::string find_git_root(); +bool stdin_has_input(); + +/** + * Returns: bool - Does that file exist? + */ +inline bool file_exists(const std::string &path) { + struct stat buffer; + + return (stat(path.c_str(), &buffer) == 0); +} + +/** + * Returns: bool - Is the file a directory? (implies exists) + */ +inline bool file_is_dir(const std::string &path) { + struct stat buffer; + + return (stat(path.c_str(), &buffer) == 0) && S_ISDIR(buffer.st_mode); +} + +/** + * Simple copy of os.path.join, takes variable args as initializer_list + * Condition: Must be called with at least 1 element. + * + * Returns: std::string - The new joined path + */ +inline std::string join(const std::initializer_list &list) { + std::initializer_list::const_iterator itr = list.begin(); + std::string result(*itr); + + for (++itr; itr != list.end(); ++itr) { + result += PATH_SEP + *itr; + } + + return result; +} + +/** + * Simple copy of os.path.basename + * + * Returns: std::string - The last leaf of the path + */ +inline std::string basename(const std::string &path) { + return path.substr(path.rfind(PATH_SEP) + 1); +} + +/** + * Simple copy of os.path.dirname + * + * Returns: std::string - The new path without last leaf + */ +inline std::string dirname(const std::string &path) { + return path.substr(0, path.rfind(PATH_SEP)); +} + +/** + * Simple hash, both characters are important. + */ +inline std::uint_fast32_t hash_two_places(const std::string &word) { + return static_cast(word.at(0)) * 1000 + + static_cast(word.at(1)); +} + +} // namespace gstat + +#endif // CXX_SRC_GSTAT_H_ diff --git a/cxx/src/main.cc b/cxx/src/main.cc new file mode 100644 index 00000000..3a1b5694 --- /dev/null +++ b/cxx/src/main.cc @@ -0,0 +1,51 @@ +/** + * The MIT License + * + * Copyright (c) 2018 Jeremy Pallats/starcraft.man + * + * A simple reimplementation in c++ for gitstatus. + * Main just handles the input and delegates to library. + */ +#include +#include +#include +#include +#include +#include + +#include "src/gstat.h" + +/* + * Main entry, this program works in two modes: + * 1) If STDIN data present, read that and parse it. Assume it is the + * output of `git status --branch --porcelain`. + * + * 2) Otherwise, run the command yourself and parse it internally. + */ +int main() { + std::vector lines; + std::string part; + + if (gstat::stdin_has_input()) { + while (std::getline(std::cin, part)) { + lines.push_back(part); + } + } else { + std::stringstream ssin(gstat::run_cmd("git status --porcelain --branch 2>&1")); + while (std::getline(ssin, part)) { + lines.push_back(part); + } + } + + if (lines.front().find("fatal: ") == 0 && + lines.front().find("ot a git repository") != std::string::npos) { + return 0; + } + + // Must be in a git repository past here + try { + std::cout << gstat::current_gitstatus(lines); + } catch (const std::runtime_error &e) { + // pass + } +} diff --git a/cxx/test/test_gstat.cc b/cxx/test/test_gstat.cc new file mode 100644 index 00000000..aec14f4b --- /dev/null +++ b/cxx/test/test_gstat.cc @@ -0,0 +1,246 @@ +/** + * The MIT License + * + * Copyright (c) 2018 Jeremy Pallats/starcraft.man + * + * A simple test suite for the gstat library. + */ +#include + +#include +#include +#include +#include +#include + +#include "src/gstat.h" + +// Capture stdout +// testing::internal::CaptureStdout(); +// std::cout << "My test" +// std::string output = testing::internal::GetCapturedStdout(); +// std::cout << testing::internal::GetCapturedStdout() << std::endl; +// +// TODO(starcraft.man) Setup test coverage build. + +/** + * A simple tempfile generate. Write the text + * passed in to a new file selected. + */ +class TFile { + public: + explicit TFile(const std::string &text): fname(select_temp()) { + std::ofstream fout(fname.c_str()); + fout << text; + } + TFile(const std::string &text, const std::string &_fname): fname(_fname) { + std::ofstream fout(fname.c_str()); + fout << text; + } + ~TFile() { + std::remove(this->fname.c_str()); + } + + std::string select_temp() { + std::string prefix = "/tmp/tempGT"; + std::uint_fast16_t count = 0; + std::string temp = prefix + std::to_string(count); + + while (gstat::file_exists(temp)) { + ++count; + temp = prefix + std::to_string(count); + } + + return temp; + } + + std::string fname; +}; + + +TEST(GStatParseBranch, BranchLocal) { + std::string temp_dummy; + std::string input("## master"); + gstat::GBranch result = gstat::parse_branch(input, temp_dummy); + EXPECT_EQ(result.branch, "master"); +} + +TEST(GStatParseBranch, BranchUpstream) { + std::string temp_dummy; + std::string input("## master...up/master"); + gstat::GBranch result = gstat::parse_branch(input, temp_dummy); + EXPECT_EQ(result.branch, "master"); + EXPECT_EQ(result.upstream, "up/master"); +} + +TEST(GStatParseBranch, BranchOnHash) { + TFile temp(std::string("54321")); + std::string input("## HEAD (no branch)"); + gstat::GBranch result = gstat::parse_branch(input, temp.fname); + EXPECT_EQ(result.branch, "54321"); + EXPECT_EQ(result.upstream, ".."); +} + +TEST(GStatParseBranch, BranchInitGitLess217) { + std::string temp_dummy; + std::string input("## Initial commit on master"); + gstat::GBranch result = gstat::parse_branch(input, temp_dummy); + EXPECT_EQ(result.branch, "master"); + EXPECT_EQ(result.upstream, ".."); +} + +TEST(GStatParseBranch, BranchInitGitGreater217) { + std::string temp_dummy; + std::string input("## No commits yet on master"); + gstat::GBranch result = gstat::parse_branch(input, temp_dummy); + EXPECT_EQ(result.branch, "master"); + EXPECT_EQ(result.upstream, ".."); +} + +TEST(GStatParseRemote, AheadTwo) { + std::string input("## master...up/master [ahead 2]"); + gstat::GRemote result = gstat::parse_remote(input); + EXPECT_EQ(result.ahead, 2); + EXPECT_EQ(result.behind, 0); +} + +TEST(GStatParseRemote, BehindOne) { + std::string input("## master...up/master [behind 1]"); + gstat::GRemote result = gstat::parse_remote(input); + EXPECT_EQ(result.ahead, 0); + EXPECT_EQ(result.behind, 1); +} + +TEST(GStatParseRemote, AheadTwoBehindOne) { + std::string input("## master...up/master [ahead 2, behind 1]"); + gstat::GRemote result = gstat::parse_remote(input); + EXPECT_EQ(result.ahead, 2); + EXPECT_EQ(result.behind, 1); +} + +TEST(GStatParseRemote, RemoteBranchGone) { + std::string input("## master...up/master [gone]"); + gstat::GRemote result = gstat::parse_remote(input); + EXPECT_EQ(result.ahead, 0); + EXPECT_EQ(result.behind, 0); +} + +TEST(GStatParseStats, AllTestCases) { + const char *possible_strings[] = { + "?? untracked1", + "?? untracked2", + "?? untracked3", + "AA conflicts1", + "AU conflicts2", + "DD conflicts3", + "DU conflicts4", + "UA conflicts5", + "UD conflicts6", + "UD conflicts7", + "A_ staged1", + "C_ staged2", + "D_ staged3", + "M_ staged4", + "R_ staged5", + "_C changed1", + "_D changed2", + "_M changed3", + "_R changed4", + }; + std::vector lines(possible_strings, possible_strings + 19); + + gstat::GStats result = gstat::parse_stats(lines.begin(), lines.end()); + EXPECT_EQ(result.changed, 4); + EXPECT_EQ(result.conflicts, 7); + EXPECT_EQ(result.staged, 5); + EXPECT_EQ(result.untracked, 3); +} + +TEST(GStatPath, Join) { + std::string result = gstat::join({"/usr/bin", "gcc"}); + EXPECT_EQ(result, "/usr/bin/gcc"); + result = gstat::join({std::string("/usr/bin"), "gcc"}); + EXPECT_EQ(result, "/usr/bin/gcc"); + result = gstat::join({std::string("/usr/bin"), std::string("gcc")}); + EXPECT_EQ(result, "/usr/bin/gcc"); +} + +TEST(GStatPath, Basename) { + std::string input("/usr/bin/gcc"); + std::string result = gstat::basename(input); + EXPECT_EQ(result, "gcc"); +} + +TEST(GStatPath, Dirname) { + std::string input("/usr/bin/gcc"); + std::string result = gstat::dirname(input); + EXPECT_EQ(result, "/usr/bin"); +} + +TEST(GStatPath, FileExists) { + std::string input = gstat::join({gstat::dirname(gstat::find_git_root()), "zshrc.sh"}); + EXPECT_TRUE(gstat::file_exists(input)); + EXPECT_FALSE(gstat::file_is_dir(input)); +} + +TEST(GStatPath, FileIsDir) { + std::string input = gstat::join({gstat::dirname(gstat::find_git_root()), "src"}); + EXPECT_TRUE(gstat::file_exists(input)); + EXPECT_TRUE(gstat::file_is_dir(input)); +} + +TEST(GStatPath, GetCWD) { + std::string result = gstat::get_cwd(); + EXPECT_EQ(gstat::basename(result), "build"); +} + +TEST(GStatPath, FindGitRoot) { + std::string expect = gstat::get_cwd(); + while (!gstat::file_exists(gstat::join({expect, ".git"}))) { + expect = gstat::dirname(expect); + } + expect = gstat::join({expect, ".git"}); + + std::string result = gstat::find_git_root(); + EXPECT_EQ(result, expect); +} + +TEST(GStatHash, SimpleHash) { + std::string input = "AA"; + int val = static_cast('A'); + EXPECT_EQ(gstat::hash_two_places(input), val * 1000 + val); +} + +TEST(GStatRunCmd, SimpleEcho) { + std::string input = "echo \"Hello!\""; + std::string result = gstat::run_cmd(input.c_str()); + EXPECT_EQ(result, "Hello!\n"); +} + +TEST(GStatStashCount, NoFile) { + std::string temp = "tempzzzz"; + EXPECT_EQ(gstat::stash_count(temp), "0"); +} + +TEST(GStatStashCount, FileTwoEntries) { + std::string text("This is a first stash\nSecond stash"); + TFile temp(text); + EXPECT_EQ(gstat::stash_count(temp.fname), "2"); +} + +TEST(GStatRebaseProgress, NoFile) { + std::string temp = "tempzzzz"; + EXPECT_EQ(gstat::rebase_progress(temp), "0"); +} + +TEST(GStatRebaseProgress, ShowProgress) { + std::string cwd = gstat::get_cwd(); + TFile next(std::string("2"), gstat::join({cwd, "next"})); + TFile last(std::string("5"), gstat::join({cwd, "last"})); + EXPECT_EQ(gstat::rebase_progress(cwd), "2/5"); +} + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/zshrc.sh b/zshrc.sh index 4c1c9b3e..9d61069c 100644 --- a/zshrc.sh +++ b/zshrc.sh @@ -23,11 +23,13 @@ chpwd_update_git_vars() { update_current_git_vars() { unset __CURRENT_GIT_STATUS - if [ "$GIT_PROMPT_EXECUTABLE" = "python" ]; then + if [ "$GIT_PROMPT_EXECUTABLE" = "haskell" ]; then + __GIT_CMD=$(git status --porcelain --branch &> /dev/null | $__GIT_PROMPT_DIR/src/.bin/gitstatus) + elif [ "$GIT_PROMPT_EXECUTABLE" = "cxx" ]; then + __GIT_CMD=$("$__GIT_PROMPT_DIR/cxx/build/gitstatus") + else local py_bin=${ZSH_GIT_PROMPT_PYBIN:-"python"} __GIT_CMD=$(git status --porcelain --branch &> /dev/null 2>&1 | ZSH_THEME_GIT_PROMPT_HASH_PREFIX=$ZSH_THEME_GIT_PROMPT_HASH_PREFIX $py_bin "$__GIT_PROMPT_DIR/gitstatus.py") - else - __GIT_CMD=$(git status --porcelain --branch &> /dev/null | $__GIT_PROMPT_DIR/src/.bin/gitstatus) fi __CURRENT_GIT_STATUS=("${(@s: :)__GIT_CMD}") unset __GIT_CMD