commit
5f87d9846c
@ -0,0 +1,12 @@ |
||||
.*.sw* |
||||
.DS_Store |
||||
*.sqlite3 |
||||
*.sqlite3-wal |
||||
*.sqlite3-shm |
||||
debug |
||||
coverage/ |
||||
.coverage |
||||
builddir |
||||
subprojects |
||||
*.csv |
||||
*.exe |
@ -0,0 +1,38 @@ |
||||
A game I'm working. It's a weird one. |
||||
|
||||
Right now I've got a command that watches your files for changes, and runs a build command. It's |
||||
entirely overengineered for this purpose, but it has what I'll need later. To build it do this: |
||||
|
||||
```shell |
||||
./scripts/reset_build.ps1 |
||||
meson compile -C builddir |
||||
``` |
||||
|
||||
If you get a bad compile because of libgit2's `src/util/process.h` then do this: |
||||
|
||||
```shell |
||||
cp patches/process.h subprojects/libgit2/src/util/ |
||||
meson compile -C builddir |
||||
``` |
||||
|
||||
I don't know why it fails at this, and only on Windows but I've talked to them repeatedly and it's |
||||
mostly "works for me" responses. I also can't figure out how Meson exactly applies patches. I've |
||||
generated every possible patch I can and Meson just can't apply them. |
||||
|
||||
Once it's running, we have even more annoying BS to deal with, and that's because Meson doesn't |
||||
actually statically compile efsw or libgit2. Even though I said `--default-librart=static --prefer-static` in the setup it just ignores that and makes DLLs. The "fix" for this asinine stupidity is this: |
||||
|
||||
```shell |
||||
meson devenv -C builddir |
||||
cd .. |
||||
cp builddir/watchgit . |
||||
./watchgit . "meson compile -C builddir |
||||
``` |
||||
|
||||
Do all that garbage and yay, this little program runs. Finally. |
||||
|
||||
## Future Changes |
||||
|
||||
1. If I can't figure out why libgit2 has compilation errors in `src/util/process.h` then I may just rip it out and just shell out to `git` directly. |
||||
2. Ultimately I can jsut use efsw to watch the directory for all changes, but I wanted to only focus on what's in git. Oh well. |
||||
3. I also have to find out why efsw and libgit2 refuse to compile statically. Once I do that I can static compile `watchgit` and then work on the next part of the game. |
@ -0,0 +1,49 @@ |
||||
#include <string> |
||||
#include <fmt/core.h> |
||||
|
||||
using namespace std; |
||||
|
||||
namespace dbc { |
||||
class Error { |
||||
public: |
||||
const string message; |
||||
Error(string m) : message{m} {} |
||||
Error(const char *m) : message{m} {} |
||||
}; |
||||
|
||||
class CheckError : public Error {}; |
||||
|
||||
class SentinelError : public Error {}; |
||||
class PreCondError : public Error {}; |
||||
class PostCondError : public Error {}; |
||||
|
||||
void log(const string &message) { |
||||
fmt::print("{}\n", message); |
||||
} |
||||
|
||||
void sentinel(const string &message) { |
||||
string err = fmt::format("[SENTINEL!] {}\n", message); |
||||
throw SentinelError{err}; |
||||
} |
||||
|
||||
void pre(const string &message, std::function<bool()> tester) { |
||||
if(!tester()) { |
||||
string err = fmt::format("[PRE!] {}\n", message); |
||||
throw PreCondError{err}; |
||||
} |
||||
} |
||||
|
||||
void post(const string &message, std::function<bool()> tester) { |
||||
if(!tester()) { |
||||
string err = fmt::format("[POST!] {}\n", message); |
||||
throw PostCondError{err}; |
||||
} |
||||
} |
||||
|
||||
void check(bool test, const string &message) { |
||||
if(!test) { |
||||
string err = fmt::format("[CHECK!] {}\n", message); |
||||
throw CheckError{err}; |
||||
} |
||||
} |
||||
} |
@ -0,0 +1,10 @@ |
||||
[wrap-git] |
||||
url = https://github.com/SpartanJ/efsw.git |
||||
revision = 1.3.1 |
||||
depth = 1 |
||||
# patch_filename = |
||||
# patch_hash = |
||||
|
||||
|
||||
[provide] |
||||
efsw = efsw_dep |
@ -0,0 +1,12 @@ |
||||
[wrap-file] |
||||
directory = libgit2-1.8.1 |
||||
source_url = https://github.com/libgit2/libgit2/archive/refs/tags/v1.8.1.tar.gz |
||||
source_filename = v1.8.1.tar.gz |
||||
source_hash = 8c1eaf0cf07cba0e9021920bfba9502140220786ed5d8a8ec6c7ad9174522f8e |
||||
wrapdb_version = 2.4.1-3 |
||||
# patch_filename = |
||||
# patch_hash = |
||||
|
||||
|
||||
[provide] |
||||
libgit2 = libgit2_dep |
@ -0,0 +1,26 @@ |
||||
project('lcppthw', 'cpp', |
||||
default_options: ['cpp_std=c++20']) |
||||
|
||||
cmake = import('cmake') |
||||
opts = cmake.subproject_options() |
||||
opts.add_cmake_defines({ |
||||
'USE_ICONV': false, |
||||
'USE_SSH': false, |
||||
'USE_NTLMCLIENT': false, |
||||
'BUILD_SHARED_LIBS': true, |
||||
'BUILD_TESTS': false, |
||||
}) |
||||
libgit2_proj = cmake.subproject('libgit2', options: opts) |
||||
libgit2package_dep = libgit2_proj.dependency('libgit2package') |
||||
|
||||
efsw_proj = cmake.subproject('efsw') |
||||
efsw_dep = efsw_proj.dependency('efsw') |
||||
|
||||
fmt = dependency('fmt') |
||||
|
||||
dependencies = [ |
||||
fmt, libgit2package_dep, efsw_dep, |
||||
] |
||||
|
||||
executable('watchgit', 'watchgit.cpp', |
||||
dependencies: dependencies) |
@ -0,0 +1,223 @@ |
||||
/*
|
||||
* Copyright (C) the libgit2 contributors. All rights reserved. |
||||
* |
||||
* This file is part of libgit2, distributed under the GNU GPL v2 with |
||||
* a Linking Exception. For full terms see the included COPYING file. |
||||
*/ |
||||
|
||||
#ifndef INCLUDE_process_h__ |
||||
#define INCLUDE_process_h__ |
||||
|
||||
typedef struct git_str git_str; |
||||
typedef struct git_process git_process; |
||||
|
||||
typedef struct { |
||||
unsigned int capture_in : 1, |
||||
capture_out : 1, |
||||
capture_err : 1, |
||||
exclude_env : 1; |
||||
|
||||
char *cwd; |
||||
} git_process_options; |
||||
|
||||
typedef enum { |
||||
GIT_PROCESS_STATUS_NONE, |
||||
GIT_PROCESS_STATUS_NORMAL, |
||||
GIT_PROCESS_STATUS_ERROR |
||||
} git_process_result_status; |
||||
|
||||
#define GIT_PROCESS_RESULT_INIT { GIT_PROCESS_STATUS_NONE } |
||||
|
||||
typedef struct { |
||||
git_process_result_status status; |
||||
int exitcode; |
||||
int signal; |
||||
} git_process_result; |
||||
|
||||
#define GIT_PROCESS_OPTIONS_INIT { 0 } |
||||
|
||||
#ifdef GIT_WIN32 |
||||
# define p_pid_t DWORD |
||||
#else |
||||
# define p_pid_t pid_t |
||||
#endif |
||||
|
||||
/**
|
||||
* Create a new process. The command to run should be specified as the |
||||
* element of the `arg` array, execv-style. This should be the full path |
||||
* to the command to run, the PATH is not obeyed. |
||||
* |
||||
* This function will add the given environment variables (in `env`) |
||||
* to the current environment. Operations on environment variables |
||||
* are not thread safe, so you may not modify the environment during |
||||
* this call. You can avoid this by setting `exclude_env` in the |
||||
* options and providing the entire environment yourself. |
||||
* |
||||
* @param out location to store the process |
||||
* @param args the command (with arguments) to run |
||||
* @param args_len the length of the args array |
||||
* @param env environment variables to add (or NULL) |
||||
* @param env_len the length of the env len |
||||
* @param opts the options for creating the process |
||||
* @return 0 or an error code |
||||
*/ |
||||
extern int git_process_new( |
||||
git_process **out, |
||||
const char **args, |
||||
size_t args_len, |
||||
const char **env, |
||||
size_t env_len, |
||||
git_process_options *opts); |
||||
|
||||
/**
|
||||
* Create a new process. The command to run should be specified as the |
||||
* `cmdline` option - which is the full text of the command line as it |
||||
* would be specified or run by a user. The command to run will be |
||||
* looked up in the PATH. |
||||
* |
||||
* On Unix, this will be executed by the system's shell (`/bin/sh`) |
||||
* and may contain _Bourne-style_ shell quoting rules. On Windows, |
||||
* this will be passed to `CreateProcess`, and similarly, may |
||||
* contain _Windows-style_ shell quoting rules. |
||||
* |
||||
* This function will add the given environment variables (in `env`) |
||||
* to the current environment. Operations on environment variables |
||||
* are not thread safe, so you may not modify the environment during |
||||
* this call. You can avoid this by setting `exclude_env` in the |
||||
* options and providing the entire environment yourself. |
||||
*/ |
||||
extern int git_process_new_from_cmdline( |
||||
git_process **out, |
||||
const char *cmdline, |
||||
const char **env, |
||||
size_t env_len, |
||||
git_process_options *opts); |
||||
|
||||
#ifdef GIT_WIN32 |
||||
|
||||
extern int git_process__appname( |
||||
git_str *out, |
||||
const char *cmdline); |
||||
|
||||
/* Windows path parsing is tricky; this helper function is for testing. */ |
||||
extern int git_process__cmdline( |
||||
git_str *out, |
||||
const char **in, |
||||
size_t in_len); |
||||
|
||||
#endif |
||||
|
||||
/*
|
||||
* Whether the given string looks like a command line option (starts |
||||
* with a dash). This is useful for examining strings that will become |
||||
* cmdline arguments to ensure that they are not erroneously treated |
||||
* as an option. For example, arguments to `ssh`. |
||||
*/ |
||||
static __inline__ bool git_process__is_cmdline_option(const char *str) |
||||
{ |
||||
return (str && str[0] == '-'); |
||||
} |
||||
|
||||
/**
|
||||
* Start the process. |
||||
* |
||||
* @param process the process to start |
||||
* @return 0 or an error code |
||||
*/ |
||||
extern int git_process_start(git_process *process); |
||||
|
||||
/**
|
||||
* Returns the process id of the process. |
||||
* |
||||
* @param out pointer to a pid_t to store the process id |
||||
* @param process the process to query |
||||
* @return 0 or an error code |
||||
*/ |
||||
extern int git_process_id(p_pid_t *out, git_process *process); |
||||
|
||||
/**
|
||||
* Read from the process's stdout. The process must have been created with |
||||
* `capture_out` set to true. |
||||
* |
||||
* @param process the process to read from |
||||
* @param buf the buf to read into |
||||
* @param count maximum number of bytes to read |
||||
* @return number of bytes read or an error code |
||||
*/ |
||||
extern ssize_t git_process_read(git_process *process, void *buf, size_t count); |
||||
|
||||
/**
|
||||
* Read from the process's stderr. The process must have been created with |
||||
* `capture_err` set to true. |
||||
* |
||||
* @param process the process to read from |
||||
* @param buf the buf to read into |
||||
* @param count maximum number of bytes to read |
||||
* @return number of bytes read or an error code |
||||
*/ |
||||
extern ssize_t git_process_read_err(git_process *process, void *buf, size_t count); |
||||
|
||||
/**
|
||||
* Write to the process's stdin. The process must have been created with |
||||
* `capture_in` set to true. |
||||
* |
||||
* @param process the process to write to |
||||
* @param buf the buf to write |
||||
* @param count maximum number of bytes to write |
||||
* @return number of bytes written or an error code |
||||
*/ |
||||
extern ssize_t git_process_write(git_process *process, const void *buf, size_t count); |
||||
|
||||
/**
|
||||
* Wait for the process to finish. |
||||
* |
||||
* @param result the result of the process or NULL |
||||
* @param process the process to wait on |
||||
*/ |
||||
extern int git_process_wait(git_process_result *result, git_process *process); |
||||
|
||||
/**
|
||||
* Close the input pipe from the child. |
||||
* |
||||
* @param process the process to close the pipe on |
||||
*/ |
||||
extern int git_process_close_in(git_process *process); |
||||
|
||||
/**
|
||||
* Close the output pipe from the child. |
||||
* |
||||
* @param process the process to close the pipe on |
||||
*/ |
||||
extern int git_process_close_out(git_process *process); |
||||
|
||||
/**
|
||||
* Close the error pipe from the child. |
||||
* |
||||
* @param process the process to close the pipe on |
||||
*/ |
||||
extern int git_process_close_err(git_process *process); |
||||
|
||||
/**
|
||||
* Close all resources that are used by the process. This does not |
||||
* wait for the process to complete. |
||||
* |
||||
* @parma process the process to close |
||||
*/ |
||||
extern int git_process_close(git_process *process); |
||||
|
||||
/**
|
||||
* Place a human-readable error message in the given git buffer. |
||||
* |
||||
* @param msg the buffer to store the message |
||||
* @param result the process result that produced an error |
||||
*/ |
||||
extern int git_process_result_msg(git_str *msg, git_process_result *result); |
||||
|
||||
/**
|
||||
* Free a process structure |
||||
* |
||||
* @param process the process to free |
||||
*/ |
||||
extern void git_process_free(git_process *process); |
||||
|
||||
#endif |
@ -0,0 +1,14 @@ |
||||
mv .\subprojects\packagecache . |
||||
rm -recurse -force .\subprojects\,.\builddir\ |
||||
mkdir subprojects |
||||
mv .\packagecache .\subprojects\ |
||||
cp *.wrap subprojects |
||||
# cp -recurse -force packagefiles subprojects |
||||
mkdir builddir |
||||
meson wrap install fmt |
||||
meson wrap install sqlite3 |
||||
meson wrap install sqlitecpp |
||||
meson wrap install ftxui |
||||
# $env:CC="clang" |
||||
# $env:CXX="clang++" |
||||
meson setup --default-library=static --prefer-static builddir |
@ -0,0 +1,14 @@ |
||||
#!/usr/bin/env bash |
||||
set -e |
||||
|
||||
mv -f ./subprojects/packagecache . |
||||
rm -rf subprojects builddir |
||||
mkdir subprojects |
||||
mv packagecache ./subprojects/ |
||||
mkdir builddir |
||||
cp *.wrap subprojects |
||||
meson wrap install fmt |
||||
meson wrap install sqlite3 |
||||
meson wrap install sqlitecpp |
||||
meson wrap install ftxui |
||||
meson setup builddir |
@ -0,0 +1,208 @@ |
||||
#include <iostream> |
||||
#include <iomanip> |
||||
#include <fstream> |
||||
#include <fmt/core.h> |
||||
#include <regex> |
||||
#include <string> |
||||
#include <iterator> |
||||
#include <ctime> |
||||
#include "dbc.h" |
||||
#include <unistd.h> |
||||
#include <stdio.h> |
||||
#include "test.hpp" |
||||
#include <git2.h> |
||||
#include <efsw/efsw.hpp> |
||||
#include <regex> |
||||
|
||||
using namespace std; |
||||
using namespace fmt; |
||||
|
||||
#define BUF_MAX 1024 |
||||
|
||||
/*
|
||||
* No idea what the semantics of this are. Will need |
||||
* to research git's dumb terminology to figure out why |
||||
* they have 4 different versions of the path for status. |
||||
*/ |
||||
const char *unfuck_path(const git_status_entry *entry) { |
||||
if(entry->head_to_index != nullptr) { |
||||
if(entry->head_to_index->new_file.path) { |
||||
return entry->head_to_index->new_file.path; |
||||
} else { |
||||
return entry->head_to_index->old_file.path; |
||||
} |
||||
} |
||||
|
||||
if(entry->index_to_workdir != nullptr) { |
||||
if(entry->index_to_workdir->new_file.path) { |
||||
return entry->index_to_workdir->new_file.path; |
||||
} else { |
||||
return entry->index_to_workdir->old_file.path; |
||||
} |
||||
} |
||||
|
||||
return nullptr; |
||||
} |
||||
|
||||
void add_status(const git_status_entry *entry, unsigned int status_flags, vector<string> &updates) { |
||||
const char *path = unfuck_path(entry); |
||||
|
||||
if(status_flags & GIT_STATUS_WT_NEW |
||||
|| status_flags & GIT_STATUS_INDEX_NEW) |
||||
{ |
||||
updates.push_back(string{path}); |
||||
} |
||||
|
||||
if(status_flags & GIT_STATUS_WT_MODIFIED |
||||
|| status_flags & GIT_STATUS_INDEX_MODIFIED) |
||||
{ |
||||
updates.push_back(string{path}); |
||||
} |
||||
|
||||
// need to confirm this gets the new name
|
||||
if(status_flags & GIT_STATUS_WT_RENAMED |
||||
|| status_flags & GIT_STATUS_INDEX_RENAMED) |
||||
{ |
||||
updates.push_back(string{path}); |
||||
} |
||||
} |
||||
|
||||
class UpdateListener : public efsw::FileWatchListener { |
||||
public: |
||||
bool changes = false; |
||||
|
||||
void handleFileAction(efsw::WatchID watchid, |
||||
const std::string& dir, |
||||
const std::string& filename, |
||||
efsw::Action action, |
||||
std::string oldFilename) override |
||||
{ |
||||
changes = true; |
||||
|
||||
switch(action) { |
||||
case efsw::Actions::Add: |
||||
print("ADD {} {} {}\n", dir, filename, oldFilename); |
||||
break; |
||||
case efsw::Actions::Delete: |
||||
print("DEL {} {} {}\n", dir, filename, oldFilename); |
||||
break; |
||||
case efsw::Actions::Modified: |
||||
print("MOD {} {} {}\n", dir, filename, oldFilename); |
||||
break; |
||||
case efsw::Actions::Moved: |
||||
print("MOV {} {} {}\n", dir, filename, oldFilename); |
||||
break; |
||||
default: |
||||
dbc::sentinel("Unknown efsw action."); |
||||
} |
||||
|
||||
} |
||||
|
||||
void reset_state() { |
||||
changes = false; |
||||
} |
||||
}; |
||||
|
||||
void list_git_changes(git_repository* repo) { |
||||
git_status_options opts = GIT_STATUS_OPTIONS_INIT; |
||||
git_status_list *statuses = nullptr; |
||||
|
||||
//TODO: does this leak?
|
||||
int err = git_status_list_new(&statuses, repo, &opts); |
||||
dbc::check(err == 0, git_error_last()->message); |
||||
size_t count = git_status_list_entrycount(statuses); |
||||
|
||||
vector<string> updates; |
||||
|
||||
for(size_t i=0; i < count; i++) { |
||||
const git_status_entry *entry = git_status_byindex(statuses, i); |
||||
add_status(entry, entry->status, updates); |
||||
} |
||||
|
||||
for(string path : updates) { |
||||
print("PATH {}\n", path); |
||||
} |
||||
} |
||||
|
||||
void run_build(const char* command) { |
||||
regex err_re("(.*?):([0-9]+):([0-9]+):\\s*(.*?):\\s*(.*)"); |
||||
char buffer[BUF_MAX]; // BUF_MAX is a define already?
|
||||
smatch err; |
||||
ofstream stats_out; |
||||
stats_out.open("stats.csv", ios::out | ios::app); |
||||
auto t = time(nullptr); |
||||
auto tm = *std::gmtime(&t); |
||||
|
||||
dbc::check(stats_out.good(), "Error opening stats.csv file."); |
||||
dbc::pre("simple test", [&]() { return stats_out.good(); }); |
||||
|
||||
FILE *build_out = popen(command, "r"); |
||||
dbc::check(build_out != nullptr, "Failed to run command."); |
||||
|
||||
while(fgets(buffer, BUF_MAX, build_out) != nullptr) { |
||||
string line(buffer); // yeah, that's probably a problem
|
||||
|
||||
print("{}\n", line); |
||||
if(regex_match(line, err, err_re)) { |
||||
string file_name = err[1].str(); |
||||
string line = err[2].str(); |
||||
string col = err[3].str(); |
||||
string type = err[4].str(); |
||||
string message = err[5].str(); |
||||
|
||||
stats_out << put_time(&tm, "%FT%TZ"); |
||||
stats_out << format(",{},{},{},{},{}\n", file_name, line, col, type, message); |
||||
} |
||||
} |
||||
|
||||
stats_out.close(); |
||||
dbc::post("a post test", [&]() { return !stats_out.is_open(); }); |
||||
} |
||||
|
||||
|
||||
int main(int argc, char *argv[]) |
||||
{ |
||||
git_repository* repo = nullptr; |
||||
|
||||
try { |
||||
dbc::check(argc == 3, "USAGE: watchgit PATH BUILD_CMD"); |
||||
const char *git_path = argv[1]; |
||||
const char *build_cmd = argv[2]; |
||||
|
||||
print("Using build command: {}", build_cmd); |
||||
efsw::FileWatcher* fileWatcher = new efsw::FileWatcher(); |
||||
dbc::check(fileWatcher != nullptr, "Failed to create filewatcher."); |
||||
|
||||
git_libgit2_init(); |
||||
|
||||
int err = git_repository_open(&repo, git_path); |
||||
dbc::check(err == 0, git_error_last()->message); |
||||
|
||||
UpdateListener* listener = new UpdateListener(); |
||||
dbc::check(listener != nullptr, "Failed to create listener."); |
||||
|
||||
print("Watching directory {} for changes...\n", git_path); |
||||
efsw::WatchID wid = fileWatcher->addWatch(git_path, listener, true); |
||||
|
||||
while(true) { |
||||
fileWatcher->watch(); |
||||
|
||||
if(listener->changes) { |
||||
sleep(1); |
||||
list_git_changes(repo); |
||||
listener->reset_state(); |
||||
run_build(build_cmd); |
||||
} |
||||
|
||||
sleep(1); |
||||
} |
||||
|
||||
git_libgit2_shutdown(); |
||||
} catch(dbc::Error &err) { |
||||
print("ERROR: {}\n", err.message); |
||||
if(repo != nullptr) git_repository_free(repo); |
||||
return 1; |
||||
} |
||||
|
||||
git_libgit2_shutdown(); |
||||
} |
Loading…
Reference in new issue