complete rewrite in zig...
upon release v5.0 I introduced a regression through my caching system in which I naively thought that the mtime of a directory was updated by child directories when it actually only updates for child files. This rewrite introduces a new caching system where instead of relying on mtime of the index file I simply store the path to, at most, 10 untracked paths and check those before anything else. This results in slower, but more reliable caching. However due to Zig's ease of use I was able to bring down the overall use when statically linked to libgit2 so overall XD should be faster \o/. I also added a visor? (O) to represent an in progress rebase.
This commit is contained in:
parent
a205314c69
commit
eb0f31daa7
17 changed files with 909 additions and 724 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -1,4 +1,2 @@
|
|||
*.o
|
||||
XD
|
||||
compile_commands.json
|
||||
.cache
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
|
|
|
|||
24
Makefile
24
Makefile
|
|
@ -1,24 +0,0 @@
|
|||
include config.mk
|
||||
|
||||
# flags and incs
|
||||
PKGS = $(GITLIB)
|
||||
CFLAGS = -DVERSION=\"$(VERSION)\" -Wall -pedantic -O3 $(GIT) $(GITHASH) $(ERR) $(PERF) $(EXPLAIN)
|
||||
LIBS = `$(PKG_CONFIG) --libs --cflags $(PKGS)`
|
||||
|
||||
all: XD
|
||||
XD: XD.o hash.o helpers.o
|
||||
$(CC) *.o $(CFLAGS) $(LIBS) -o $@
|
||||
|
||||
clean:
|
||||
rm -f XD *.o
|
||||
|
||||
install: XD
|
||||
mkdir -p $(PREFIX)/bin
|
||||
cp -f XD $(PREFIX)/bin
|
||||
chmod 755 $(PREFIX)/bin/XD
|
||||
mkdir -p $(MANDIR)/man1
|
||||
cp -f XD.1 $(MANDIR)/man1
|
||||
chmod 644 $(MANDIR)/man1/XD.1
|
||||
|
||||
uninstall: XD
|
||||
rm -f $(PREFIX)/bin/XD $(MANDIR)/man1/XD.1
|
||||
12
README.md
Normal file
12
README.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# XD
|
||||
Smiley face to explain info on the shell.
|
||||
---
|
||||
More info on XD can be found in the manpage.
|
||||
|
||||
## Building
|
||||
When packaging XD it's recommended to use the following flags:
|
||||
`zig build -Doptimize=ReleaseFast`
|
||||
which will won't be linked to libgit2, if you instead wish to link to libgit2:
|
||||
`zig build -Doptimize=ReleaseFast -Ddynamic=true`
|
||||
Statically linking results in marginally faster, but significantly larger
|
||||
executables.
|
||||
1
XD.1
1
XD.1
|
|
@ -28,6 +28,7 @@ c;l.
|
|||
\;;in a git repo
|
||||
8;in a git repo with stashed changes
|
||||
X;in a git repo during a merge
|
||||
O;in a git repo during a rebase
|
||||
B;in a git repo with no commits
|
||||
.TE
|
||||
.Ss Nose
|
||||
|
|
|
|||
397
XD.c
397
XD.c
|
|
@ -1,397 +0,0 @@
|
|||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
|
||||
#ifdef ERR
|
||||
#include <errno.h>
|
||||
#endif
|
||||
#if defined(EXPLAIN) || defined(ERR)
|
||||
#include <stdarg.h>
|
||||
#endif
|
||||
|
||||
#ifdef GIT
|
||||
#include <sys/stat.h>
|
||||
#include <dirent.h>
|
||||
#include <git2.h>
|
||||
#endif
|
||||
|
||||
#include "helpers.h"
|
||||
#include "hash.h"
|
||||
|
||||
#define P(X) fwrite(X, 1, 1, stdout)
|
||||
|
||||
#ifdef GIT
|
||||
/**
|
||||
* @brief search all parent directories for a git repo
|
||||
*
|
||||
* @return absolute path to git repo
|
||||
*/
|
||||
char
|
||||
*find_git_repo(void)
|
||||
{
|
||||
PS();
|
||||
char path[PATH_MAX] = ".", fstr[PATH_MAX], *rpath, *res;
|
||||
struct stat s;
|
||||
FILE *f;
|
||||
int i, c;
|
||||
|
||||
/* find the number of jumps to the root of the fs */
|
||||
rpath = realpath(path, NULL);
|
||||
if (!rpath) {
|
||||
L("realpath: %s", strerror(errno));
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
for (i = c = 0; i < strlen(rpath); i++) {
|
||||
if (rpath[i] == '/') {
|
||||
c++;
|
||||
}
|
||||
}
|
||||
free(rpath);
|
||||
|
||||
/* start searching */
|
||||
for (i = c; i > 0; i--) {
|
||||
strcat(path, "/.git");
|
||||
|
||||
/* if there seems to be a git directory return the directory it was found in */
|
||||
if (stat(path, &s) == 0) {
|
||||
if (S_ISDIR(s.st_mode)) {
|
||||
PE();
|
||||
return realpath(path, NULL);
|
||||
} else if (S_ISREG(s.st_mode)) {
|
||||
/* we do some special magic here to check if we're in a submodule */
|
||||
f = fopen(path, "r");
|
||||
if (!f) {
|
||||
L("fopen: %s", strerror(errno));
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
res = fgets(fstr, PATH_MAX, f);
|
||||
fclose(f);
|
||||
if (!res) {
|
||||
L("fgets: %s", strerror(errno));
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
if (strncmp(fstr, "gitdir: ", strlen("gitdir: ")) == 0) {
|
||||
fstr[strlen(fstr) - 1] = '\0';
|
||||
PE();
|
||||
return realpath(fstr + strlen("gitdir: "), NULL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* reset contents of gpath, and go up a directory */
|
||||
memset(&path[strlen(path) - 4], '.', 2);
|
||||
memset(&path[strlen(path) - 2], 0, 2);
|
||||
}
|
||||
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief open git repo if one is available at the current path
|
||||
*
|
||||
* @return a pointer to the git repo object
|
||||
*/
|
||||
git_repository
|
||||
*init_git(void)
|
||||
{
|
||||
PS();
|
||||
char *buf;
|
||||
git_repository *repo;
|
||||
|
||||
/* check for a repo before loading libgit2 */
|
||||
if ((buf = find_git_repo()) == NULL) {
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* disable a bunch of git options to hopefully speed things up */
|
||||
git_libgit2_opts(GIT_OPT_ENABLE_CACHING, 0);
|
||||
git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_GLOBAL, "");
|
||||
git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_XDG, "");
|
||||
git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, GIT_CONFIG_LEVEL_SYSTEM, "");
|
||||
git_libgit2_opts(GIT_OPT_SET_TEMPLATE_PATH, "");
|
||||
git_libgit2_opts(GIT_OPT_DISABLE_PACK_KEEP_FILE_CHECKS, 1);
|
||||
git_libgit2_opts(GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, 0);
|
||||
|
||||
/* initialize the git library and repository */
|
||||
if (git_libgit2_init() < 0) {
|
||||
L("Failed to initalize libgit2, proceeding without git functionality enabled.");
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (git_repository_open(&repo, buf) < 0) {
|
||||
L("Failed to open git repo: %s", git_error_last()->message);
|
||||
free(buf);
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* get rid of object containing git repo path and return the repo */
|
||||
free(buf);
|
||||
PE();
|
||||
return repo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief check for any existing stashes in the provided git repo
|
||||
*
|
||||
* @param repo git repo to check for existing stashes
|
||||
* @return 1 if stashes found 0 otherwise
|
||||
*/
|
||||
int
|
||||
has_stashes(git_repository *repo)
|
||||
{
|
||||
PS();
|
||||
git_reference *stash = NULL;
|
||||
int e;
|
||||
|
||||
e = git_reference_lookup(&stash, repo, "refs/stash");
|
||||
if (e == GIT_ENOTFOUND) {
|
||||
PE();
|
||||
return 0;
|
||||
} else if (e < 0) {
|
||||
L("Error looking up stash reference: %s", git_error_last()->message);
|
||||
PE();
|
||||
return 0;
|
||||
} else {
|
||||
e = 1;
|
||||
}
|
||||
|
||||
git_reference_free(stash);
|
||||
PE();
|
||||
return e;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief check for any untracked changes in the current git repo
|
||||
*
|
||||
* @param repo git repository object
|
||||
* @return 1 if any untracked changes have been found 0 otherwise
|
||||
*/
|
||||
int
|
||||
has_untracked(git_repository *repo)
|
||||
{
|
||||
PS();
|
||||
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
|
||||
git_status_list *list = NULL;
|
||||
repohash *storedhash;
|
||||
uint64_t newhash = generate_hash(repo);
|
||||
int r = 0;
|
||||
|
||||
#ifdef GITHASH
|
||||
if ((storedhash = read_hash(repo))
|
||||
&& storedhash->hash == newhash) {
|
||||
r = storedhash->changes;
|
||||
free(storedhash);
|
||||
PE();
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
|
||||
/* if we need to regen the hash then we need to do a hard check on the real
|
||||
* git repository */
|
||||
opts.show = GIT_STATUS_SHOW_INDEX_AND_WORKDIR;
|
||||
opts.flags = GIT_STATUS_OPT_INCLUDE_UNTRACKED |
|
||||
GIT_STATUS_OPT_RENAMES_HEAD_TO_INDEX |
|
||||
GIT_STATUS_OPT_UPDATE_INDEX |
|
||||
GIT_STATUS_OPT_DISABLE_PATHSPEC_MATCH;
|
||||
|
||||
if (git_status_list_new(&list, repo, &opts) < 0) {
|
||||
L("Error checking for untracked changes: %s", git_error_last()->message);
|
||||
PE();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* if any changes are found return 1 */
|
||||
r = git_status_list_entrycount(list) > 0;
|
||||
#ifdef GITHASH
|
||||
write_hash(repo, (repohash){ .hash = newhash, .changes = r });
|
||||
#endif
|
||||
|
||||
git_status_list_free(list);
|
||||
PE();
|
||||
return r;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief check for staged changes
|
||||
*
|
||||
* @param repo git repository object
|
||||
* @return 1 if any staged changes found 0 otherwise
|
||||
*/
|
||||
int
|
||||
has_staged(git_repository *repo)
|
||||
{
|
||||
PS();
|
||||
git_status_entry entry;
|
||||
git_status_list *list = NULL;
|
||||
git_status_options opts = GIT_STATUS_OPTIONS_INIT;
|
||||
int i, c, r = 0;
|
||||
|
||||
opts.show = GIT_STATUS_SHOW_INDEX_ONLY;
|
||||
opts.flags = GIT_STATUS_INDEX_NEW;
|
||||
|
||||
if (git_status_list_new(&list, repo, &opts) < 0) {
|
||||
L("Error checking for staged changes: %s", git_error_last()->message);
|
||||
PE();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* if any staged changes are found return 1 */
|
||||
if ((c = git_status_list_entrycount(list)) > 0) {
|
||||
for (i = 0; i < c; i++) {
|
||||
entry = *git_status_byindex(list, i);
|
||||
|
||||
if (entry.status & (GIT_STATUS_INDEX_NEW
|
||||
| GIT_STATUS_INDEX_DELETED
|
||||
| GIT_STATUS_INDEX_MODIFIED)) {
|
||||
r = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
git_status_list_free(list);
|
||||
PE();
|
||||
return r;
|
||||
}
|
||||
#endif
|
||||
|
||||
inline unsigned
|
||||
numcat(unsigned x, unsigned y)
|
||||
{
|
||||
PS();
|
||||
unsigned pow = 10;
|
||||
while(y >= pow) {
|
||||
pow *= 10;
|
||||
}
|
||||
|
||||
PE();
|
||||
return (x * pow) + y;
|
||||
}
|
||||
|
||||
int
|
||||
str_to_int(char *str)
|
||||
{
|
||||
PS();
|
||||
int res = -1;
|
||||
char *c;
|
||||
|
||||
for (c = str; (*c != '\0') && isdigit(*c); c++) {
|
||||
if (res == -1) {
|
||||
res = *c - '0';
|
||||
} else {
|
||||
res = numcat(res, *c - '0');
|
||||
}
|
||||
}
|
||||
|
||||
PE();
|
||||
return res;
|
||||
}
|
||||
|
||||
int
|
||||
main(int argc, char *argv[])
|
||||
{
|
||||
PS();
|
||||
int code = -1;
|
||||
|
||||
/* print version information */
|
||||
if (argc > 1 && strcmp(argv[1], "-v") == 0) {
|
||||
printf("XD [number] %s\n", VERSION);
|
||||
PE();
|
||||
return 0;
|
||||
#ifdef EXPLAIN
|
||||
} else if (argc > 1 && strcmp(argv[1], "-e") == 0) {
|
||||
explain = 1;
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef GIT
|
||||
git_repository *repo;
|
||||
|
||||
if ((repo = init_git())) {
|
||||
/* change the eyes depending on the current git repo's status */
|
||||
if (has_stashes(repo)) {
|
||||
E("The current git repo has stashed changes.")
|
||||
P("8"); /* goggle eyes if we have some stashed changes */
|
||||
} else if (git_repository_state(repo) == GIT_REPOSITORY_STATE_MERGE) {
|
||||
E("The current git repo is in the middle of a merge.")
|
||||
P("X"); /* laughing eyes cause the user is fucked */
|
||||
} else if (git_repository_is_empty(repo)) {
|
||||
E("This is a new git repo.")
|
||||
P("B"); /* sunglasses if we're in a new repo with no HEAD */
|
||||
} else {
|
||||
E("In a git repository.")
|
||||
P(";"); /* wink when we're in a git repo */
|
||||
}
|
||||
|
||||
/* change the nose depending on the current git repo's status */
|
||||
if (has_staged(repo)) {
|
||||
E("There staged changes.")
|
||||
P("*"); /* change to broken nose for staged changes */
|
||||
} else if (has_untracked(repo)) {
|
||||
E("There are untracked changes in the repository.")
|
||||
P("^"); /* add a little nose when there are untracked changes in the repo */
|
||||
} else if (git_repository_head_detached(repo)) {
|
||||
E("The HEAD is detached.")
|
||||
P("-"); /* add a minus nose when the HEAD is detached */
|
||||
}
|
||||
git_repository_free(repo);
|
||||
git_libgit2_shutdown();
|
||||
} else
|
||||
#endif
|
||||
if (1) {
|
||||
E("Not in a git repository.")
|
||||
P(":");
|
||||
}
|
||||
|
||||
/* get exit code from user args */
|
||||
if (argc > 1 && argv[argc - 1]) {
|
||||
code = str_to_int(argv[argc - 1]);
|
||||
if (code < 0) {
|
||||
L("Return code, %d, not valid", code);
|
||||
PE();
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/* change mouth based on exit code */
|
||||
if (code >= 0) {
|
||||
switch (code) {
|
||||
case 0: /* all good */
|
||||
E("Return code of %d, no errors.", code)
|
||||
P(")");
|
||||
break;
|
||||
case 130: /* Ctrl-c pressed (SIGTERM) */
|
||||
E("Return code of %d, ctrl-c was pressed, or SIGTERM sent.", code)
|
||||
P("O");
|
||||
break;
|
||||
case 126: /* permission denied */
|
||||
E("Return code of %d, permission denied.", code)
|
||||
P("P");
|
||||
break;
|
||||
case 127: /* command not found */
|
||||
E("Return code of %d, command not found.", code)
|
||||
P("/");
|
||||
break;
|
||||
default: /* all other codes (usually the program saying it has failed) */
|
||||
E("Return code of %d, probably an error with the program.", code)
|
||||
P("(");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
E("No code information passed in.")
|
||||
P("|"); /* no code info */
|
||||
}
|
||||
|
||||
PE();
|
||||
return code;
|
||||
}
|
||||
67
build.zig
Normal file
67
build.zig
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
const std = @import("std");
|
||||
const pkg = @import("build.zig.zon");
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
|
||||
const git = b.option(bool, "git", "Enable git support") orelse true;
|
||||
const explain = b.option(bool, "explain", "Explain what the emojitcon means") orelse true;
|
||||
const perf = b.option(bool, "perf", "Monitor performance of functions") orelse false;
|
||||
const err = b.option(bool, "err", "Print errors") orelse false;
|
||||
const dynamic = b.option(bool, "dynamic", "Link XD dynamically") orelse false;
|
||||
const cleanup = b.option(bool, "cleanup", "removes the old XDhash file from your git directories") orelse false;
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption(bool, "git", git);
|
||||
options.addOption(bool, "explain", explain);
|
||||
options.addOption(bool, "perf", perf);
|
||||
options.addOption(bool, "err", err);
|
||||
options.addOption(bool, "dynamic", dynamic);
|
||||
options.addOption(bool, "cleanup", cleanup);
|
||||
options.addOption([]const u8, "version", pkg.version);
|
||||
|
||||
var mod = b.addModule("XD", .{
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
const exe = b.addExecutable(.{ .name = "XD", .root_module = mod });
|
||||
b.installArtifact(exe);
|
||||
|
||||
// add our buildtime options as a module called config
|
||||
exe.root_module.addOptions("config", options);
|
||||
|
||||
if (git) {
|
||||
if (dynamic) {
|
||||
exe.root_module.linkSystemLibrary("git2", .{});
|
||||
mod.link_libc = true;
|
||||
} else {
|
||||
// not sure if there's an easier way to do this, there's a type in
|
||||
// the libgit2 bindings build.zig but I'm not sure how to access it
|
||||
const libgit2_dep = if (optimize == .Debug) b.dependency("libgit2", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.@"enable-ssh" = false,
|
||||
.@"tls-backend" = .mbedtls, // openssl doesn't work in debug mode
|
||||
}) else b.dependency("libgit2", .{
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.@"enable-ssh" = false,
|
||||
.@"tls-backend" = .openssl, // much much faster than mbedtls
|
||||
});
|
||||
exe.linkLibrary(libgit2_dep.artifact("git2"));
|
||||
}
|
||||
}
|
||||
|
||||
const run_step = b.step("run", "Run the app");
|
||||
const run_cmd = b.addRunArtifact(exe);
|
||||
run_step.dependOn(&run_cmd.step);
|
||||
run_cmd.step.dependOn(b.getInstallStep());
|
||||
if (b.args) |args| run_cmd.addArgs(args);
|
||||
|
||||
const tests = b.addTest(.{ .root_module = mod });
|
||||
const test_step = b.step("test", "Run tests");
|
||||
const test_cmd = b.addRunArtifact(tests);
|
||||
test_step.dependOn(&test_cmd.step);
|
||||
}
|
||||
19
build.zig.zon
Normal file
19
build.zig.zon
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.{
|
||||
.name = .XD,
|
||||
.version = "6.0.0",
|
||||
.fingerprint = 0x420a402a481c77c1, // Changing this has security and trust implications.
|
||||
.minimum_zig_version = "0.15.2",
|
||||
.dependencies = .{
|
||||
.libgit2 = .{
|
||||
.url = "git+https://github.com/allyourcodebase/libgit2#58dfd002d47a8c9fbd99d4a939cb343172590e1b",
|
||||
.hash = "libgit2-1.9.0-uizqTQnZAADyPOmBklxzj_lrnfJoRLRDBbbTvi28SrZX",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
"LICENSE",
|
||||
"README.md",
|
||||
},
|
||||
}
|
||||
54
config.mk
54
config.mk
|
|
@ -1,54 +0,0 @@
|
|||
VERSION := `git describe --tags --dirty`
|
||||
|
||||
PKG_CONFIG = pkg-config
|
||||
|
||||
# paths
|
||||
PREFIX = /usr/local
|
||||
MANDIR = $(PREFIX)/share/man
|
||||
|
||||
GIT =
|
||||
GITHASH =
|
||||
GITLIB =
|
||||
# comment to disable git support
|
||||
GIT = -DGIT
|
||||
GITHASH = -DGITHASH
|
||||
GITLIB = libgit2
|
||||
|
||||
ERR =
|
||||
# uncomment to enable errors
|
||||
# ERR = -DERR
|
||||
|
||||
PERF =
|
||||
# uncomment to enable performance logging
|
||||
# PERF = -DPERF
|
||||
|
||||
EXPLAIN =
|
||||
# comment to disable explinations
|
||||
EXPLAIN = -DEXPLAIN
|
||||
|
||||
# add compilation details to VERSION variable
|
||||
ifneq ($(GIT),)
|
||||
VERSION := $(VERSION)"\\nlibgit2 "`$(PKG_CONFIG) --modversion $(GITLIB)`
|
||||
endif
|
||||
ifeq ($(GITHASH),)
|
||||
VERSION := $(VERSION)"\\ngit hashing disabled"
|
||||
else
|
||||
VERSION := $(VERSION)"\\ngit hashing enabled"
|
||||
endif
|
||||
ifeq ($(EXPLAIN),)
|
||||
VERSION := $(VERSION)"\\nexplinations disabled"
|
||||
else
|
||||
VERSION := $(VERSION)"\\nexplinations enabled"
|
||||
endif
|
||||
ifeq ($(ERR),)
|
||||
VERSION := $(VERSION)"\\nerrors disabled"
|
||||
else
|
||||
VERSION := $(VERSION)"\\nerrors enabled"
|
||||
endif
|
||||
ifeq ($(PERF),)
|
||||
VERSION := $(VERSION)"\\nperformance logging disabled"
|
||||
else
|
||||
VERSION := $(VERSION)"\\nperformance logging enabled"
|
||||
endif
|
||||
|
||||
CC = cc
|
||||
152
hash.c
152
hash.c
|
|
@ -1,152 +0,0 @@
|
|||
#include <git2/repository.h>
|
||||
#include <git2/types.h>
|
||||
#include <linux/limits.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include "hash.h"
|
||||
#include "helpers.h"
|
||||
|
||||
#ifdef GITHASH
|
||||
static uint64_t
|
||||
murmur64(uint64_t k)
|
||||
{
|
||||
PS();
|
||||
k ^= k >> 33;
|
||||
k *= 0xff51afd7ed558ccdLLU;
|
||||
k ^= k >> 33;
|
||||
k *= 0xc4ceb9fe1a85ec53LLU;
|
||||
k ^= k >> 33;
|
||||
|
||||
PE();
|
||||
return k;
|
||||
}
|
||||
|
||||
uint64_t
|
||||
generate_hash(git_repository *repo)
|
||||
{
|
||||
PS();
|
||||
struct stat dir, index;
|
||||
char path[PATH_MAX] = { 0 };
|
||||
|
||||
const char *gitpath = git_repository_path(repo);
|
||||
if (strlen(gitpath) + strlen("/..") > PATH_MAX - 1) {
|
||||
L("strlen: %s", strerror(errno));
|
||||
PE();
|
||||
return -1;
|
||||
}
|
||||
|
||||
strcat(path, gitpath);
|
||||
if (stat(path, &index) < 0) {
|
||||
PE();
|
||||
return -1;
|
||||
}
|
||||
strcat(path, "/..");
|
||||
if (stat(path, &dir) < 0) {
|
||||
PE();
|
||||
return -1;
|
||||
}
|
||||
PE();
|
||||
return murmur64(dir.st_mtim.tv_nsec ^ index.st_mtim.tv_nsec);
|
||||
}
|
||||
|
||||
repohash
|
||||
*read_hash(git_repository *repo)
|
||||
{
|
||||
PS();
|
||||
FILE *f;
|
||||
uint64_t data;
|
||||
uint8_t changes;
|
||||
repohash *hash = malloc(sizeof(repohash));
|
||||
char path[PATH_MAX] = { 0 };
|
||||
|
||||
const char *gitpath = git_repository_path(repo);
|
||||
if (strlen(gitpath) + strlen(XD_HASH_PATH) > PATH_MAX - 1) {
|
||||
L("strlen: %s", strerror(errno));
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
strcat(path, gitpath);
|
||||
strcat(path, XD_HASH_PATH);
|
||||
|
||||
f = fopen(path, "r");
|
||||
if (!f) {
|
||||
L("fopen: %s", strerror(errno));
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fread(&data, sizeof(uint64_t), 1, f) != 1) {
|
||||
L("fread: %s", strerror(errno));
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fseek(f, sizeof(uint64_t), SEEK_SET) < 0) {
|
||||
L("fseek: %s", strerror(errno));
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (fread(&changes, sizeof(uint8_t), 1, f) != 1) {
|
||||
L("fread: %s", strerror(errno));
|
||||
PE();
|
||||
return NULL;
|
||||
}
|
||||
fclose(f);
|
||||
|
||||
hash->hash = data;
|
||||
hash->changes = changes;
|
||||
PE();
|
||||
return hash;
|
||||
}
|
||||
|
||||
int
|
||||
write_hash(git_repository *repo, repohash hash)
|
||||
{
|
||||
PS();
|
||||
FILE *f;
|
||||
char path[PATH_MAX] = { 0 };
|
||||
|
||||
const char *gitpath = git_repository_path(repo);
|
||||
if (strlen(gitpath) + strlen(XD_HASH_PATH) > PATH_MAX - 1) {
|
||||
L("strlen: %s", strerror(errno));
|
||||
PE();
|
||||
return 1;
|
||||
}
|
||||
strcat(path, gitpath);
|
||||
strcat(path, XD_HASH_PATH);
|
||||
|
||||
f = fopen(path, "wb");
|
||||
if (!f) {
|
||||
L("fopen: %s", strerror(errno));
|
||||
PE();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (fwrite(&hash.hash, sizeof(uint64_t), 1, f) != 1) {
|
||||
L("fwrite: %s", strerror(errno));
|
||||
PE();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (fseek(f, sizeof(uint64_t), SEEK_SET) < 0) {
|
||||
L("fseek: %s", strerror(errno));
|
||||
PE();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (fwrite(&hash.changes, sizeof(uint8_t), 1, f) != 1) {
|
||||
L("fwrite: %s", strerror(errno));
|
||||
PE();
|
||||
return 1;
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
PE();
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
37
hash.h
37
hash.h
|
|
@ -1,37 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <git2/repository.h>
|
||||
|
||||
#ifdef GIT
|
||||
typedef struct {
|
||||
uint64_t hash;
|
||||
bool changes;
|
||||
} repohash;
|
||||
|
||||
#define XD_HASH_PATH "/XDhash"
|
||||
/**
|
||||
* @brief generate a hash from the repository state
|
||||
*
|
||||
* @param repo the git repository
|
||||
* @return the hash
|
||||
*/
|
||||
uint64_t generate_hash(git_repository *repo);
|
||||
|
||||
/**
|
||||
* @brief read the hash from the git repo
|
||||
*
|
||||
* @param repo the git repository
|
||||
* @return the hash
|
||||
*/
|
||||
repohash *read_hash(git_repository *repo);
|
||||
|
||||
/**
|
||||
* @brief write a new hash to the repository
|
||||
*
|
||||
* @param repo the repository
|
||||
* @param hash the hash to write
|
||||
*/
|
||||
int write_hash(git_repository *repo, repohash hash);
|
||||
#endif
|
||||
28
helpers.c
28
helpers.c
|
|
@ -1,28 +0,0 @@
|
|||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "helpers.h"
|
||||
|
||||
#ifdef EXPLAIN
|
||||
int explain = 0;
|
||||
#endif
|
||||
|
||||
#if defined(ERR) || defined(EXPLAIN)
|
||||
void
|
||||
l(const char *fmt, ...)
|
||||
{
|
||||
va_list ap;
|
||||
|
||||
va_start(ap, fmt);
|
||||
vfprintf(stderr, fmt, ap);
|
||||
va_end(ap);
|
||||
|
||||
if (fmt[0] && fmt[strlen(fmt) - 1] == ':') {
|
||||
fputc(' ', stderr);
|
||||
perror(NULL);
|
||||
} else {
|
||||
fputc('\n', stderr);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
28
helpers.h
28
helpers.h
|
|
@ -1,28 +0,0 @@
|
|||
#pragma once
|
||||
|
||||
#if defined(ERR) || defined(EXPLAIN)
|
||||
void l(const char *fmt, ...);
|
||||
#endif
|
||||
|
||||
#ifdef ERR
|
||||
#define L(...) l(__VA_ARGS__)
|
||||
#else
|
||||
#define L(...)
|
||||
#endif
|
||||
|
||||
#ifdef EXPLAIN
|
||||
extern int explain;
|
||||
#define E(...) if (explain) { \
|
||||
l(__VA_ARGS__); \
|
||||
} else
|
||||
#else
|
||||
#define E(...)
|
||||
#endif
|
||||
|
||||
#ifdef PERF
|
||||
#define PS() long __start = clock()
|
||||
#define PE() l("%s: %fs", __func__, ((double) (clock() - __start)) / CLOCKS_PER_SEC)
|
||||
#else
|
||||
#define PS()
|
||||
#define PE()
|
||||
#endif
|
||||
305
src/Cache.zig
Normal file
305
src/Cache.zig
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
const Cache = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const config = @import("config");
|
||||
const util = @import("util.zig");
|
||||
const perf = @import("perf.zig");
|
||||
|
||||
/// The cache file name
|
||||
const file_name = "XDcache";
|
||||
|
||||
/// this is previous name of the file that I would like to remove if we
|
||||
/// encounter it
|
||||
const old_file_name = "XDhash";
|
||||
|
||||
/// Storage of features and version data
|
||||
pub const Data = struct {
|
||||
untracked_file_paths: struct {
|
||||
data: []const []const u8,
|
||||
comptime version: std.SemanticVersion = .{.major = 1, .minor = 0, .patch = 0},
|
||||
},
|
||||
|
||||
const new_entry_marker = ":)";
|
||||
const version: std.SemanticVersion = .{.major = 1, .minor = 0, .patch = 0};
|
||||
|
||||
/// Generate an enum of features at comptime
|
||||
fn Features() type {
|
||||
const lib_fields = @typeInfo(Data).@"struct".fields;
|
||||
var fields: [lib_fields.len]std.builtin.Type.EnumField = undefined;
|
||||
for (lib_fields, 0..) |f, i| fields[i] = .{ .name = f.name, .value = i };
|
||||
|
||||
return @Type(.{.@"enum" = .{
|
||||
.decls = &.{},
|
||||
.tag_type = i16,
|
||||
.fields = &fields,
|
||||
.is_exhaustive = true,
|
||||
}});
|
||||
}
|
||||
};
|
||||
const Features = Data.Features();
|
||||
|
||||
allocator: std.mem.Allocator,
|
||||
path: []const u8,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, path: []const u8) Cache {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
const self: Cache = .{
|
||||
.allocator = allocator,
|
||||
.path = path,
|
||||
};
|
||||
|
||||
if (config.cleanup) removeOldFile(self);
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/// remove the old cache file from the git directory
|
||||
fn removeOldFile(self: *Cache) void {
|
||||
const delete_path = std.fs.path.join(
|
||||
self.allocator,
|
||||
&[_][]const u8{ self.path, old_file_name },
|
||||
) catch return;
|
||||
defer self.allocator.free(delete_path);
|
||||
std.fs.deleteFileAbsolute(delete_path) catch return;
|
||||
}
|
||||
|
||||
pub const ReadError = error{ EmptyFile };
|
||||
pub fn readFromFile(self: *Cache) !Data {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
const path = try std.fs.path.join(
|
||||
self.allocator,
|
||||
&[_][]const u8{ self.path, file_name },
|
||||
);
|
||||
defer self.allocator.free(path);
|
||||
|
||||
const file = try std.fs.openFileAbsolute(path, .{.mode = .read_only});
|
||||
defer file.close();
|
||||
|
||||
var file_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var file_reader = file.reader(&file_buffer);
|
||||
const reader: *std.Io.Reader = &file_reader.interface;
|
||||
if (try file.getEndPos() <= try file.getPos()) {
|
||||
return ReadError.EmptyFile;
|
||||
}
|
||||
|
||||
return try read(self.allocator, reader);
|
||||
}
|
||||
|
||||
fn read(allocator: std.mem.Allocator, reader: *std.Io.Reader) !Data {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
var data: Data = .{
|
||||
.untracked_file_paths = undefined,
|
||||
};
|
||||
|
||||
var feature: ?Features = null;
|
||||
var feature_len: u16 = 0;
|
||||
var untracked_file_paths: ?[][]const u8 = null;
|
||||
|
||||
var i: u32 = 0;
|
||||
while (reader.takeDelimiterInclusive('\n') catch null) |line| {
|
||||
if (std.mem.eql(u8, line[0..2], Data.new_entry_marker)) {
|
||||
i = 0;
|
||||
feature = null; // we need to determine the feature
|
||||
const version_end_idx = std.mem.indexOf(u8, line, " ");
|
||||
if (version_end_idx == null) continue; // not found
|
||||
const feature_version = try std.SemanticVersion.parse(line[2..version_end_idx.?]);
|
||||
if (Data.version.order(feature_version) == .lt) {
|
||||
// unsupported feature
|
||||
continue;
|
||||
}
|
||||
|
||||
var feature_name_end_idx = std.mem.indexOf(u8, line[version_end_idx.? + 1..], " ");
|
||||
if (feature_name_end_idx == null) continue; // not found
|
||||
feature_name_end_idx.? += version_end_idx.? + 1; // make the idx relative to the start of the line
|
||||
|
||||
const feature_name = line[version_end_idx.? + 1..feature_name_end_idx.?];
|
||||
feature_len = try std.fmt.parseInt(u16, line[feature_name_end_idx.? + 1..line.len - 1], 10);
|
||||
|
||||
inline for (@typeInfo(Features).@"enum".fields) |field| {
|
||||
if (std.mem.eql(u8, field.name, feature_name)) {
|
||||
feature = @enumFromInt(field.value);
|
||||
}
|
||||
}
|
||||
continue; // skip to the next line with the actual data
|
||||
}
|
||||
|
||||
if (feature == null) continue;
|
||||
switch (feature.?) {
|
||||
.untracked_file_paths => {
|
||||
if (untracked_file_paths == null) {
|
||||
untracked_file_paths = try allocator.alloc([]const u8, feature_len);
|
||||
}
|
||||
untracked_file_paths.?[i] = line;
|
||||
},
|
||||
}
|
||||
|
||||
i += 1;
|
||||
}
|
||||
|
||||
data.untracked_file_paths = .{
|
||||
.data = untracked_file_paths.?,
|
||||
};
|
||||
return data;
|
||||
}
|
||||
|
||||
pub fn writeToFile(self: *Cache, data: Data) !void {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
const path = try std.fs.path.join(
|
||||
self.allocator,
|
||||
&[_][]const u8{ self.path, file_name },
|
||||
);
|
||||
defer self.allocator.free(path);
|
||||
|
||||
const file = try std.fs.createFileAbsolute(path, .{ .truncate = true });
|
||||
defer file.close();
|
||||
|
||||
var file_buffer: [std.fs.max_path_bytes]u8 = undefined;
|
||||
var file_writer = file.writer(&file_buffer);
|
||||
const writer: *std.io.Writer = &file_writer.interface;
|
||||
|
||||
return try write(writer, data);
|
||||
}
|
||||
|
||||
fn write(writer: *std.Io.Writer, data: Data) !void {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
inline for (@typeInfo(Data).@"struct".fields) |field| {
|
||||
const feature = @field(data, field.name);
|
||||
const feature_enum = @field(Features, field.name);
|
||||
const feature_version: std.SemanticVersion = @field(feature, "version");
|
||||
|
||||
// write feature string
|
||||
try writer.writeAll(Data.new_entry_marker);
|
||||
try feature_version.format(writer);
|
||||
try writer.writeAll(" " ++ field.name ++ " ");
|
||||
try writer.print("{}", .{ feature.data.len });
|
||||
try writer.writeAll("\n");
|
||||
switch (feature_enum) {
|
||||
.untracked_file_paths => {
|
||||
for (feature.data) |d| {
|
||||
if (std.mem.eql(u8, d, "")) break;
|
||||
try writer.writeAll(d);
|
||||
try writer.writeAll("\n");
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
try writer.flush();
|
||||
}
|
||||
|
||||
test "write" {
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const file = try std.fs.cwd().createFile(
|
||||
"junk_file.txt",
|
||||
.{ .read = true },
|
||||
);
|
||||
defer std.fs.cwd().deleteFile("junk_file.txt") catch undefined;
|
||||
|
||||
var file_writer = file.writer(&buf);
|
||||
const writer: *std.Io.Writer = &file_writer.interface;
|
||||
|
||||
var file_reader = file.reader(&buf);
|
||||
const reader: *std.Io.Reader = &file_reader.interface;
|
||||
|
||||
const expected_message = ":)1.0.0 untracked_file_paths 1\ntest\n";
|
||||
try Cache.write(writer, .{
|
||||
.untracked_file_paths = .{ .data = &[_][]const u8{ "test" } },
|
||||
});
|
||||
|
||||
const contents = try reader.readAlloc(
|
||||
std.testing.allocator,
|
||||
expected_message.len,
|
||||
);
|
||||
defer std.testing.allocator.free(contents);
|
||||
|
||||
std.testing.expect(std.mem.eql(u8, contents, expected_message)) catch |err| {
|
||||
std.log.err("got: \n{s}", .{ contents });
|
||||
std.log.err("expected: \n{s}", .{ expected_message });
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
test "read" {
|
||||
var arena_alloc: std.heap.ArenaAllocator = .init(std.testing.allocator);
|
||||
defer arena_alloc.deinit();
|
||||
const allocator = arena_alloc.allocator();
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const file = try std.fs.cwd().createFile(
|
||||
"junk_file.txt",
|
||||
.{ .read = true },
|
||||
);
|
||||
defer std.fs.cwd().deleteFile("junk_file.txt") catch undefined;
|
||||
|
||||
var file_writer = file.writer(&buf);
|
||||
const writer: *std.Io.Writer = &file_writer.interface;
|
||||
|
||||
var file_reader = file.reader(&buf);
|
||||
const reader: *std.Io.Reader = &file_reader.interface;
|
||||
|
||||
const expected_message = ":)1.0.0 untracked_file_paths 1\ntest\n";
|
||||
try writer.writeAll(expected_message);
|
||||
try writer.flush();
|
||||
|
||||
const data = try Cache.read(allocator, reader);
|
||||
try std.testing.expectEqualDeep(Data {
|
||||
.untracked_file_paths = .{ .data = &[_][]const u8{ "test\n" } },
|
||||
}, data);
|
||||
}
|
||||
|
||||
test "read long cache" {
|
||||
var arena_alloc: std.heap.ArenaAllocator = .init(std.testing.allocator);
|
||||
defer arena_alloc.deinit();
|
||||
const allocator = arena_alloc.allocator();
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const file = try std.fs.cwd().createFile(
|
||||
"junk_file.txt",
|
||||
.{ .read = true },
|
||||
);
|
||||
defer std.fs.cwd().deleteFile("junk_file.txt") catch undefined;
|
||||
|
||||
var file_writer = file.writer(&buf);
|
||||
const writer: *std.Io.Writer = &file_writer.interface;
|
||||
|
||||
var file_reader = file.reader(&buf);
|
||||
const reader: *std.Io.Reader = &file_reader.interface;
|
||||
|
||||
const expected_message = ":)1.0.0 untracked_file_paths 10\n"
|
||||
++ "test\n"
|
||||
++ "test/a/really\n"
|
||||
++ "test/a/really/really\n"
|
||||
++ "test/a/really/really/really\n"
|
||||
++ "test/a/really/really/really/really\n"
|
||||
++ "test/a/really/really/really/really/really\n"
|
||||
++ "test/a/really/really/really/really/really/really\n"
|
||||
++ "test/a/really/really/really/really/really/really/really\n"
|
||||
++ "test/a/really/really/really/really/really/really/really/really\n"
|
||||
++ "test/a/really/really/really/really/really/really/really/really/long/path\n";
|
||||
try writer.writeAll(expected_message);
|
||||
try writer.flush();
|
||||
|
||||
const data = try Cache.read(allocator, reader);
|
||||
try std.testing.expectEqualDeep(Data {
|
||||
.untracked_file_paths = .{ .data = &[_][]const u8{
|
||||
"test\n",
|
||||
"test/a/really\n",
|
||||
"test/a/really/really\n",
|
||||
"test/a/really/really/really\n",
|
||||
"test/a/really/really/really/really\n",
|
||||
"test/a/really/really/really/really/really\n",
|
||||
"test/a/really/really/really/really/really/really\n",
|
||||
"test/a/really/really/really/really/really/really/really\n",
|
||||
"test/a/really/really/really/really/really/really/really/really\n",
|
||||
"test/a/really/really/really/really/really/really/really/really/long/path\n",
|
||||
} },
|
||||
}, data);
|
||||
}
|
||||
285
src/Git.zig
Normal file
285
src/Git.zig
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
const Git = @This();
|
||||
|
||||
const Cache = @import("Cache.zig");
|
||||
pub const c = @cImport({
|
||||
@cInclude("git2.h");
|
||||
});
|
||||
const config = @import("config");
|
||||
const perf = @import("perf.zig");
|
||||
const std = @import("std");
|
||||
const util = @import("util.zig");
|
||||
|
||||
repo: ?*c.git_repository,
|
||||
git_path: []const u8,
|
||||
repo_path: []const u8,
|
||||
allocator: std.mem.Allocator,
|
||||
repo_state: c_int,
|
||||
cache: Cache,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator) !?*Git {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
// ensure we're in a git repo
|
||||
const repo_root = findRepoRoot(allocator) catch |err| switch (err) {
|
||||
error.OutOfMemory => util.oom(),
|
||||
else => if (config.err) return err else null,
|
||||
};
|
||||
if (repo_root == null) return null;
|
||||
|
||||
// disable some libgit2 features
|
||||
_ = c.git_libgit2_opts(c.GIT_OPT_ENABLE_CACHING, @as(u32, 0));
|
||||
_ = c.git_libgit2_opts(c.GIT_OPT_ENABLE_STRICT_HASH_VERIFICATION, @as(u32, 0));
|
||||
|
||||
if (c.git_libgit2_init() < 0) {
|
||||
util.logErr("Failed to initalize libgit2, proceeding without git functionality enabled.", .{});
|
||||
return null;
|
||||
}
|
||||
|
||||
const self = try allocator.create(Git);
|
||||
self.* = .{
|
||||
.repo = undefined,
|
||||
.git_path = repo_root.?,
|
||||
.allocator = allocator,
|
||||
.repo_state = undefined,
|
||||
.repo_path = undefined,
|
||||
.cache = .init(self.allocator, self.git_path),
|
||||
};
|
||||
|
||||
if (c.git_repository_open(&self.repo, repo_root.?.ptr) != 0) {
|
||||
util.logErr("Failed to open git repo: {s}", .{
|
||||
c.git_error_last().*.message,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
self.repo_state = c.git_repository_state(self.repo);
|
||||
self.repo_path = try std.fs.path.resolve(allocator, &[_][]const u8{
|
||||
self.git_path,
|
||||
"..",
|
||||
});
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Git) void {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
self.allocator.free(self.git_path);
|
||||
self.allocator.free(self.repo_path);
|
||||
c.git_repository_free(self.repo);
|
||||
self.allocator.destroy(self);
|
||||
_ = c.git_libgit2_shutdown();
|
||||
}
|
||||
|
||||
pub fn hasStashes(self: *Git) bool {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
var stash: ?*c.struct_git_reference = null;
|
||||
const res = c.git_reference_lookup(&stash, self.repo, "refs/stash");
|
||||
defer c.git_reference_free(stash);
|
||||
|
||||
if (res == c.GIT_ENOTFOUND) {
|
||||
return false;
|
||||
} else if (res < 0) {
|
||||
util.logErr("Error looking up stash reference: {s}", .{
|
||||
c.git_error_last().*.message,
|
||||
});
|
||||
return false;
|
||||
} else return true;
|
||||
}
|
||||
|
||||
pub fn inNewRepo(self: *Git) bool {
|
||||
return c.git_repository_is_empty(self.repo) == 1;
|
||||
}
|
||||
|
||||
pub fn inMerge(self: *Git) bool {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
return self.repo_state == c.GIT_REPOSITORY_STATE_MERGE;
|
||||
}
|
||||
|
||||
pub fn inRebase(self: *Git) bool {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
return switch (self.repo_state) {
|
||||
c.GIT_REPOSITORY_STATE_REBASE,
|
||||
c.GIT_REPOSITORY_STATE_REBASE_INTERACTIVE,
|
||||
c.GIT_REPOSITORY_STATE_REBASE_MERGE => true,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn hasStaged(self: *Git) bool {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
var entry: c.git_status_entry = undefined;
|
||||
var list: ?*c.git_status_list = undefined;
|
||||
var opts: c.git_status_options = .{
|
||||
.show = c.GIT_STATUS_SHOW_INDEX_ONLY,
|
||||
.flags = c.GIT_STATUS_INDEX_NEW
|
||||
| c.GIT_STATUS_OPT_NO_REFRESH,
|
||||
.version = c.GIT_STATUS_OPTIONS_VERSION,
|
||||
};
|
||||
|
||||
if (c.git_status_list_new(&list, self.repo, &opts) < 0) {
|
||||
util.logErr("Error checking for staged changes: {s}", .{
|
||||
c.git_error_last().*.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
defer c.git_status_list_free(list);
|
||||
|
||||
const count = c.git_status_list_entrycount(list);
|
||||
for (0..count) |i| {
|
||||
const entry_ptr = c.git_status_byindex(list, i);
|
||||
entry = entry_ptr.*;
|
||||
if ((entry.status & c.GIT_STATUS_INDEX_NEW
|
||||
| c.GIT_STATUS_INDEX_DELETED
|
||||
| c.GIT_STATUS_INDEX_MODIFIED) != 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
pub fn hasUntracked(self: *Git) bool {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
var list: ?*c.git_status_list = undefined;
|
||||
var opts: c.git_status_options = .{
|
||||
.show = c.GIT_STATUS_SHOW_INDEX_AND_WORKDIR,
|
||||
.flags = c.GIT_STATUS_OPT_INCLUDE_UNTRACKED
|
||||
| c.GIT_STATUS_OPT_NO_REFRESH,
|
||||
.version = c.GIT_STATUS_OPTIONS_VERSION,
|
||||
};
|
||||
|
||||
read_cache: {
|
||||
const data = self.cache.readFromFile() catch |err| {
|
||||
util.logErr("Failed to read cache from disk: {}", .{err});
|
||||
switch (err) {
|
||||
error.OutOfMemory => util.oom(),
|
||||
else => break :read_cache, // leave caching code early
|
||||
}
|
||||
};
|
||||
for (data.untracked_file_paths.data) |path| {
|
||||
const file_path: [:0]u8 = @ptrCast(@constCast(path));
|
||||
file_path[file_path.len - 1] = 0;
|
||||
|
||||
// TODO: combine the paths into one pathspec and diff all files at once
|
||||
var pathspec_paths = self.allocator.alloc([*c]u8, 2) catch return false;
|
||||
pathspec_paths[0] = file_path;
|
||||
pathspec_paths[1] = null;
|
||||
var cache_opts: c.git_diff_options = .{
|
||||
.flags = c.GIT_DIFF_INCLUDE_UNTRACKED
|
||||
| c.GIT_DIFF_RECURSE_UNTRACKED_DIRS,
|
||||
.version = c.GIT_DIFF_OPTIONS_VERSION,
|
||||
.pathspec = .{
|
||||
.count = 1,
|
||||
.strings = pathspec_paths.ptr,
|
||||
},
|
||||
};
|
||||
var diff: ?*c.git_diff = undefined;
|
||||
if (c.git_diff_index_to_workdir(&diff, self.repo, null, &cache_opts) < 0) {
|
||||
util.logErr("Error diffing file '{s}': {s}", .{
|
||||
path,
|
||||
c.git_error_last().*.message,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
defer c.git_diff_free(diff);
|
||||
if (c.git_diff_num_deltas(diff) > 0) return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (c.git_status_list_new(&list, self.repo, &opts) < 0) {
|
||||
util.logErr("Error checking for staged changes: {s}", .{
|
||||
c.git_error_last().*.message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
defer c.git_status_list_free(list);
|
||||
|
||||
const max_entries = 10;
|
||||
const untracked_entries = @min(c.git_status_list_entrycount(list), max_entries);
|
||||
if (untracked_entries > 0) write_cache: {
|
||||
var paths: [][]const u8 = self.allocator.alloc(
|
||||
[]const u8,
|
||||
untracked_entries,
|
||||
) catch return false;
|
||||
for (0..untracked_entries) |i| {
|
||||
const entry = c.git_status_byindex(list, i);
|
||||
if (entry) |e| if (e.*.index_to_workdir) |itw| {
|
||||
paths[i] = std.mem.span(itw.*.new_file.path);
|
||||
};
|
||||
}
|
||||
self.cache.writeToFile(.{
|
||||
.untracked_file_paths = .{ .data = paths },
|
||||
}) catch |err| {
|
||||
util.logErr("Failed to write cache to disk: {}", .{err});
|
||||
break :write_cache;
|
||||
};
|
||||
}
|
||||
|
||||
return untracked_entries > 0;
|
||||
}
|
||||
|
||||
pub fn headIsDetached(self: *Git) bool {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
return c.git_repository_head_detached(self.repo) == 1;
|
||||
}
|
||||
|
||||
fn findRepoRoot(allocator: std.mem.Allocator) !?[]u8 {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
var buf: [std.fs.max_path_bytes]u8 = undefined;
|
||||
const path = std.fs.realpath(".", &buf) catch return null;
|
||||
|
||||
// count the number of jumps back to the root of the fs
|
||||
var root_steps: u32 = 0;
|
||||
for (path) |ch| {
|
||||
if (ch == std.fs.path.sep) root_steps += 1;
|
||||
}
|
||||
|
||||
var cur_path = std.fs.cwd();
|
||||
for (path) |_| {
|
||||
const stat = cur_path.statFile(".git") catch {
|
||||
// go up a directory if the current one doesn't work
|
||||
cur_path = try cur_path.openDir("..", .{});
|
||||
continue;
|
||||
};
|
||||
|
||||
return switch (stat.kind) {
|
||||
.directory => try allocator.dupe(u8, try cur_path.realpathZ(
|
||||
".git",
|
||||
&buf,
|
||||
)),
|
||||
.file => {
|
||||
@branchHint(.cold);
|
||||
const f = try cur_path.openFile(".git", .{ .mode = .read_only });
|
||||
const file_reader = f.reader(&buf);
|
||||
var reader = file_reader.interface;
|
||||
const read = try reader.readSliceShort(&buf);
|
||||
const prefix = "gitdir: ";
|
||||
if (std.mem.startsWith(u8, &buf, prefix)) {
|
||||
buf[read - 1] = 0; // remove the newline
|
||||
return try allocator.dupe(u8, buf[prefix.len..]);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
185
src/main.zig
Normal file
185
src/main.zig
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
const std = @import("std");
|
||||
const config = @import("config");
|
||||
const perf = @import("perf.zig");
|
||||
const util = @import("util.zig");
|
||||
|
||||
const FaceParts = enum(u32) { eyes, nose, mouth };
|
||||
const FacePart = struct { symbol: u8, explanation: []const u8 };
|
||||
|
||||
pub fn main() u8 {
|
||||
const s = perf.s();
|
||||
defer perf.e(s, @src());
|
||||
|
||||
// state
|
||||
var explain = false;
|
||||
var face: [@typeInfo(FaceParts).@"enum".fields.len]?FacePart = .{
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
};
|
||||
|
||||
errdefer |err| {
|
||||
util.logErr("{}", .{ err });
|
||||
std.process.exit(1);
|
||||
}
|
||||
|
||||
var stdout_buffer: [1024]u8 = undefined;
|
||||
var stdout_writer = std.fs.File.stdout().writer(&stdout_buffer);
|
||||
const stdout = &stdout_writer.interface;
|
||||
|
||||
var signal: ?u8 = null;
|
||||
const argv = std.os.argv;
|
||||
if (argv.len > 1) {
|
||||
if (std.mem.eql(u8, std.mem.span(argv[1]), "-v")) {
|
||||
try stdout.writeAll("XD v" ++ config.version ++ "\n"
|
||||
// I tried to make this into a function but the zig compiler wouldn't
|
||||
// accept that the return type was comptime known, this sucks
|
||||
++ "git " ++ b: { if (config.git) break :b "enabled" else break :b "disabled"; } ++ "\n"
|
||||
++ "explinations " ++ b: { if (config.explain) break :b "enabled" else break :b "disabled"; } ++ "\n"
|
||||
++ "v5 cleanup " ++ b: { if (config.cleanup) break :b "enabled" else break :b "disabled"; } ++ "\n"
|
||||
++ "dynamic linking " ++ b: { if (config.dynamic) break :b "enabled" else break :b "disabled"; } ++ "\n"
|
||||
++ "performance monitoring " ++ b: { if (config.perf) break :b "enabled" else break :b "disabled"; } ++ "\n"
|
||||
++ "error logs " ++ b: { if (config.err) break :b "enabled" else break :b "disabled"; } ++ "\n"
|
||||
);
|
||||
try stdout.flush();
|
||||
std.process.exit(0);
|
||||
} else if (config.explain and std.mem.eql(u8, std.mem.span(argv[1]), "-e")) {
|
||||
explain = true;
|
||||
}
|
||||
signal = std.fmt.parseInt(u8, std.mem.span(argv[argv.len - 1]), 10) catch {
|
||||
util.logErr("The last arument must be a number!", .{});
|
||||
return 1;
|
||||
};
|
||||
}
|
||||
|
||||
var arena_allocator: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
|
||||
defer arena_allocator.deinit();
|
||||
const allocator = arena_allocator.allocator();
|
||||
|
||||
var git = if (config.git) try @import("Git.zig").init(allocator) orelse null;
|
||||
defer if (config.git and git != null) git.?.deinit();
|
||||
if (config.git and git != null) {
|
||||
if (git.?.hasStashes()) {
|
||||
face[@intFromEnum(FaceParts.eyes)] = .{
|
||||
.symbol = '8',
|
||||
.explanation = "The current git repo has stashed changes.",
|
||||
};
|
||||
} else if (git.?.inMerge()) {
|
||||
@branchHint(.unlikely);
|
||||
face[@intFromEnum(FaceParts.eyes)] = .{
|
||||
.symbol = 'X',
|
||||
.explanation = "The current git repo is in the middle of a merge.",
|
||||
};
|
||||
} else if (git.?.inRebase()) {
|
||||
@branchHint(.cold);
|
||||
face[@intFromEnum(FaceParts.eyes)] = .{
|
||||
.symbol = 'O',
|
||||
.explanation = "The current git repo is in the middle of a rebase.",
|
||||
};
|
||||
} else if (git.?.inNewRepo()) {
|
||||
@branchHint(.cold);
|
||||
face[@intFromEnum(FaceParts.eyes)] = .{
|
||||
.symbol = 'B',
|
||||
.explanation = "The current git repo is has no commits.",
|
||||
};
|
||||
} else {
|
||||
face[@intFromEnum(FaceParts.eyes)] = .{
|
||||
.symbol = ';',
|
||||
.explanation = "In a git repository.",
|
||||
};
|
||||
}
|
||||
|
||||
if (git.?.hasStaged()) {
|
||||
face[@intFromEnum(FaceParts.nose)] = .{
|
||||
.symbol = '*',
|
||||
.explanation = "There are staged changes.",
|
||||
};
|
||||
} else if (git.?.hasUntracked()) {
|
||||
@branchHint(.likely);
|
||||
face[@intFromEnum(FaceParts.nose)] = .{
|
||||
.symbol = '^',
|
||||
.explanation = "There are untracked changes in the repository.",
|
||||
};
|
||||
} else if (git.?.headIsDetached()) {
|
||||
face[@intFromEnum(FaceParts.nose)] = .{
|
||||
.symbol = '-',
|
||||
.explanation = "The HEAD is detached.",
|
||||
};
|
||||
}
|
||||
} else {
|
||||
@branchHint(.likely);
|
||||
face[@intFromEnum(FaceParts.eyes)] = .{
|
||||
.symbol = ':',
|
||||
.explanation = "Not in a git repository.",
|
||||
};
|
||||
}
|
||||
|
||||
if (signal) |sig| {
|
||||
face[@intFromEnum(FaceParts.mouth)] = blk: switch (sig) {
|
||||
0 => {
|
||||
@branchHint(.likely);
|
||||
break :blk .{
|
||||
.symbol = ')',
|
||||
.explanation = "Return code of 0, no errors.",
|
||||
};
|
||||
},
|
||||
130 => {
|
||||
@branchHint(.likely);
|
||||
break :blk .{
|
||||
.symbol = 'O',
|
||||
.explanation = "Return code of 130, ctrl-c was pressed, or SIGTERM sent.",
|
||||
};
|
||||
},
|
||||
126 => {
|
||||
@branchHint(.cold);
|
||||
break :blk .{
|
||||
.symbol = 'P',
|
||||
.explanation = "Return code of 126, permission denied.",
|
||||
};
|
||||
},
|
||||
127 => {
|
||||
@branchHint(.unlikely);
|
||||
break :blk .{
|
||||
.symbol = '/',
|
||||
.explanation = "Return code of 127, command not found.",
|
||||
};
|
||||
},
|
||||
else => {
|
||||
@branchHint(.likely);
|
||||
var buf: [60]u8 = undefined;
|
||||
break :blk .{
|
||||
.symbol = '(',
|
||||
.explanation = try std.fmt.bufPrint(
|
||||
&buf,
|
||||
"Return code of {}, probably an error with the program.",
|
||||
.{ sig }
|
||||
),
|
||||
};
|
||||
},
|
||||
};
|
||||
} else {
|
||||
@branchHint(.cold);
|
||||
face[@intFromEnum(FaceParts.mouth)] = .{
|
||||
.symbol = '|',
|
||||
.explanation = "No code information passed in.",
|
||||
};
|
||||
}
|
||||
|
||||
// print out face parts
|
||||
for (face) |part| {
|
||||
if (part == null) continue;
|
||||
if (explain) {
|
||||
@branchHint(.cold);
|
||||
try stdout.print("{s}\n", .{ part.?.explanation });
|
||||
} else {
|
||||
_ = try stdout.write(@ptrCast(&part.?.symbol));
|
||||
}
|
||||
}
|
||||
|
||||
try stdout.flush();
|
||||
return signal orelse 0;
|
||||
}
|
||||
|
||||
test "main" {
|
||||
_ = @import("Cache.zig");
|
||||
}
|
||||
19
src/perf.zig
Normal file
19
src/perf.zig
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
const Perf = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const config = @import("config");
|
||||
|
||||
pub fn s() ?i128 {
|
||||
if (!config.perf) return null;
|
||||
return std.time.nanoTimestamp();
|
||||
}
|
||||
|
||||
pub fn e(start: ?i128, src: std.builtin.SourceLocation) void {
|
||||
if (!config.perf or start == null) return;
|
||||
const elapsed = std.time.nanoTimestamp() - start.?;
|
||||
std.debug.print("{s}.{s}(): {}ms\n", .{
|
||||
src.file[0..src.file.len - 4], // - 4 removes ".zig" from the file name
|
||||
src.fn_name,
|
||||
@divExact(@as(f128, @floatFromInt(elapsed)), std.time.ns_per_ms),
|
||||
});
|
||||
}
|
||||
14
src/util.zig
Normal file
14
src/util.zig
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const util = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const config = @import("config");
|
||||
|
||||
pub fn logErr(comptime format: []const u8, args: anytype) void {
|
||||
if (!config.err) return;
|
||||
std.debug.print("err: " ++ format ++ "\n", args);
|
||||
}
|
||||
|
||||
pub fn oom() noreturn {
|
||||
logErr("Ran out of memory, whoops.", .{});
|
||||
std.process.exit(1);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue