Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,18 @@ on:
jobs:
build:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
# macos + 0.15.2 is omitted: zig 0.15.2 cannot link against the
# macOS 26 SDK on current runner images (and current Macs).
# 0.16 support comes via src/compat.zig.
include:
- os: ubuntu-latest
zig: "0.15.2"
- os: ubuntu-latest
zig: "0.16.0"
- os: macos-latest
zig: "0.16.0"

runs-on: ${{ matrix.os }}

Expand All @@ -19,7 +29,7 @@ jobs:

- uses: mlugg/setup-zig@v2
with:
version: 0.15.2
version: ${{ matrix.zig }}

- name: Install libgit2 (Linux)
if: runner.os == 'Linux'
Expand All @@ -41,3 +51,6 @@ jobs:
./zig-out/bin/nit status
./zig-out/bin/nit log -n 5
./zig-out/bin/nit show

- name: Conformance tests
run: ./tests/conformance.sh ./zig-out/bin/nit
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ We ran [experiments](experiments/) to validate our choice of 1 context line (vs

## Install

Requires [libgit2](https://libgit2.org/) and [zig](https://ziglang.org/) 0.14+.
Requires [libgit2](https://libgit2.org/) and [zig](https://ziglang.org/) 0.15.2 or 0.16.0. On recent macOS (26+ SDK) use 0.16.0 - zig 0.15.2 cannot link against that SDK.

```sh
# From source
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
.name = .nit,
.version = "0.1.0",
.fingerprint = 0x5e5f5af3d86f98f2,
.minimum_zig_version = "0.14.0",
.minimum_zig_version = "0.15.2",

.paths = .{
"build.zig",
Expand Down
12 changes: 5 additions & 7 deletions src/cli.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const compat = @import("compat.zig");
const git = @import("git.zig");
const status = @import("cmd/status.zig");
const log = @import("cmd/log.zig");
Expand Down Expand Up @@ -26,15 +27,14 @@ const usage =
\\
;

pub fn run() !void {
pub fn run(init: compat.ProcessInit) !void {
color.init();

var buf: [8192]u8 = undefined;
var file_writer = std.fs.File.stdout().writer(&buf);
var file_writer = compat.stdoutWriter(init, &buf);
const w = &file_writer.interface;

const args = try std.process.argsAlloc(std.heap.page_allocator);
defer std.process.argsFree(std.heap.page_allocator, args);
const args = try compat.argsSlice(init);

if (args.len < 2) {
try w.writeAll(usage);
Expand Down Expand Up @@ -163,7 +163,5 @@ fn passthrough(args: []const [:0]const u8) !void {
argv[args.len] = null;

const argv_ptr: [*:null]const ?[*:0]const u8 = @ptrCast(argv.ptr);
const envp: [*:null]const ?[*:0]const u8 = @ptrCast(std.c.environ);

return std.posix.execvpeZ("git", argv_ptr, envp);
return compat.execGit(argv_ptr);
}
5 changes: 3 additions & 2 deletions src/color.zig
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const std = @import("std");
const compat = @import("compat.zig");

// Default ANSI colors (respects terminal theme)
const defaults = struct {
Expand Down Expand Up @@ -54,7 +55,7 @@ fn makeEscape(sgr: []const u8) []const u8 {
}

pub fn init() void {
const env = std.posix.getenv("NIT_COLORS") orelse return;
const env = compat.getenv("NIT_COLORS") orelse return;

// Parse "key=value:key=value:..."
var iter = std.mem.splitScalar(u8, env, ':');
Expand Down Expand Up @@ -88,5 +89,5 @@ pub fn init() void {
}

pub fn isTty() bool {
return std.posix.isatty(std.fs.File.stdout().handle);
return compat.stdoutIsTty();
}
57 changes: 57 additions & 0 deletions src/compat.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Zig version compatibility layer.
//!
//! nit supports both Zig 0.15.x and 0.16.x (where the std I/O and process
//! APIs changed shape). Everything version-sensitive lives here so the rest
//! of the codebase can stay on the shared std.Io.Writer interface, which is
//! identical across both versions.
const std = @import("std");

/// Zig 0.16 introduced std.process.Init and moved File I/O behind std.Io.
pub const is_modern = @hasDecl(std.process, "Init");

/// On 0.16 main receives std.process.Init (which carries the Io instance
/// and args); on 0.15 main takes no arguments and we reach for globals.
pub const ProcessInit = if (is_modern) std.process.Init else void;

pub const StdoutWriter = if (is_modern) std.Io.File.Writer else std.fs.File.Writer;

pub fn stdoutWriter(init: ProcessInit, buffer: []u8) StdoutWriter {
if (is_modern) {
// Streaming, not positional: positional mode pwrites from offset 0,
// which overwrites earlier output when stdout is redirected to a file.
return std.Io.File.stdout().writerStreaming(init.io, buffer);
} else {
return std.fs.File.stdout().writer(buffer);
}
}

/// Argument slices live for the whole process (nit is a short-lived CLI):
/// arena-backed on 0.16, page-allocated and never freed on 0.15.
pub fn argsSlice(init: ProcessInit) ![]const [:0]const u8 {
if (is_modern) {
return init.minimal.args.toSlice(init.arena.allocator());
} else {
const raw = try std.process.argsAlloc(std.heap.page_allocator);
return @ptrCast(raw);
}
}

/// libc getenv: identical on both versions (std.posix.getenv was removed in 0.16).
pub fn getenv(name: [*:0]const u8) ?[]const u8 {
const val = std.c.getenv(name) orelse return null;
return std.mem.span(val);
}

pub fn stdoutIsTty() bool {
return std.c.isatty(std.posix.STDOUT_FILENO) == 1;
}

// std.posix.execvpeZ was removed in 0.16; libc execvp (which inherits the
// process environment) covers both versions since we always link libc.
extern "c" fn execvp(file: [*:0]const u8, argv: [*:null]const ?[*:0]const u8) c_int;

/// Replace the current process with `git`. Only returns on failure.
pub fn execGit(argv: [*:null]const ?[*:0]const u8) error{ExecFailed} {
_ = execvp("git", argv);
return error.ExecFailed;
}
10 changes: 3 additions & 7 deletions src/git.zig
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,9 @@ pub const Repository = struct {

pub fn openFromCwd() !Repository {
var repo: ?*c.git_repository = null;
const cwd_dir = std.fs.cwd();
var buf: [std.fs.max_path_bytes]u8 = undefined;
const path = try cwd_dir.realpath(".", &buf);
const path_z = try std.heap.page_allocator.dupeZ(u8, path);
defer std.heap.page_allocator.free(path_z);

try check(c.git_repository_open_ext(&repo, path_z, 0, null));
// libgit2 resolves the relative path and discovers upward itself;
// no need to realpath the cwd first.
try check(c.git_repository_open_ext(&repo, ".", 0, null));
return .{ .repo = repo.? };
}

Expand Down
13 changes: 11 additions & 2 deletions src/main.zig
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
const cli = @import("cli.zig");
const compat = @import("compat.zig");

pub fn main() !void {
try cli.run();
// Zig 0.16 passes std.process.Init to main; 0.15 requires a zero-arg main.
// compat.ProcessInit keeps both signatures valid on both versions.
pub const main = if (compat.is_modern) mainModern else mainLegacy;

fn mainModern(init: compat.ProcessInit) !void {
try cli.run(init);
}

fn mainLegacy() !void {
try cli.run({});
}
39 changes: 25 additions & 14 deletions tests/conformance.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,22 @@ fi
# Resolve to absolute path
NIT="$(cd "$(dirname "$NIT")" && pwd)/$(basename "$NIT")"

# Run a command under a pseudo-TTY. BSD/macOS script takes the command as
# positional args; util-linux script needs -c (and %q-quoting, since color
# values contain semicolons).
if script -qec true /dev/null >/dev/null 2>&1; then
run_tty() { script -qec "$(printf '%q ' "$@")" /dev/null; }
else
run_tty() { script -q /dev/null "$@"; }
fi

TMPDIR=$(mktemp -d -t nit-conformance-XXXXXX)
trap "rm -rf $TMPDIR" EXIT

cd "$TMPDIR"
git init -q
# -b main: the merge-commit tests checkout main by name, so don't depend
# on the host's init.defaultBranch
git init -q -b main
git config user.email "test@test.com"
git config user.name "Test User"

Expand Down Expand Up @@ -731,13 +742,13 @@ check "show HEAD:binary.dat matches git" \
echo ""
echo "NIT_COLORS:"

# On macOS, script -q /dev/null forces a pseudo-TTY
# On macOS, run_tty forces a pseudo-TTY
# Verify that NIT_COLORS changes output when running in a TTY
if command -v script >/dev/null 2>&1; then
# Without NIT_COLORS: default colors in TTY
default_out=$(script -q /dev/null $NIT show -H 2>&1 | cat -v | head -3)
default_out=$(run_tty $NIT show -H 2>&1 | cat -v | head -3)
# With NIT_COLORS: custom hash color (magenta = 35)
custom_out=$(script -q /dev/null env NIT_COLORS="hash=35" $NIT show -H 2>&1 | cat -v | head -3)
custom_out=$(run_tty env NIT_COLORS="hash=35" $NIT show -H 2>&1 | cat -v | head -3)

if [ "$default_out" != "$custom_out" ]; then
PASS=$((PASS + 1))
Expand Down Expand Up @@ -775,7 +786,7 @@ if command -v script >/dev/null 2>&1; then
"NIT_COLORS='hash=38;2;255;165;0' $NIT log -n 1 2>&1"

# Verify true-color actually produces escape sequence in TTY
truecolor_out=$(script -q /dev/null env NIT_COLORS="hash=38;2;255;0;0" $NIT show -H 2>&1 | cat -v | head -1)
truecolor_out=$(run_tty env NIT_COLORS="hash=38;2;255;0;0" $NIT show -H 2>&1 | cat -v | head -1)
if echo "$truecolor_out" | grep -q '38;2;255;0;0'; then
PASS=$((PASS + 1))
echo " PASS: NIT_COLORS true-color escape in TTY output"
Expand All @@ -797,8 +808,8 @@ if command -v script >/dev/null 2>&1; then
"NIT_COLORS='boguskey=32' $NIT log -n 1 2>&1"

# Duplicate keys (last wins) - verify output changes with second value
dup_out1=$(script -q /dev/null env NIT_COLORS="hash=35" $NIT show -H 2>&1 | cat -v | head -1)
dup_out2=$(script -q /dev/null env NIT_COLORS="hash=33:hash=35" $NIT show -H 2>&1 | cat -v | head -1)
dup_out1=$(run_tty env NIT_COLORS="hash=35" $NIT show -H 2>&1 | cat -v | head -1)
dup_out2=$(run_tty env NIT_COLORS="hash=33:hash=35" $NIT show -H 2>&1 | cat -v | head -1)
if [ "$dup_out1" = "$dup_out2" ]; then
PASS=$((PASS + 1))
echo " PASS: NIT_COLORS duplicate keys (last wins)"
Expand Down Expand Up @@ -836,7 +847,7 @@ if command -v script >/dev/null 2>&1; then
"NIT_COLORS='add=31:del=32:hunk=33:context=34:staged=35:unstaged=36:hash=37:date=38:add=39:del=40:hunk=41:context=42' $NIT log -n 1 2>&1"

# NIT_COLORS affects show -H (color slot: hash, date used in show)
custom_show=$(script -q /dev/null env NIT_COLORS="hash=35" $NIT show -H 2>&1 | cat -v | head -1)
custom_show=$(run_tty env NIT_COLORS="hash=35" $NIT show -H 2>&1 | cat -v | head -1)
if [ "$default_out" != "$custom_show" ] || echo "$custom_show" | grep -q '35m'; then
PASS=$((PASS + 1))
echo " PASS: NIT_COLORS affects show -H"
Expand All @@ -847,8 +858,8 @@ if command -v script >/dev/null 2>&1; then
fi

# NIT_COLORS affects diff -H (color slot: add, del, hunk, context)
default_diff=$(script -q /dev/null $NIT diff -H 2>&1 | cat -v | head -3)
custom_diff=$(script -q /dev/null env NIT_COLORS="add=35:del=36" $NIT diff -H 2>&1 | cat -v | head -3)
default_diff=$(run_tty $NIT diff -H 2>&1 | cat -v | head -3)
custom_diff=$(run_tty env NIT_COLORS="add=35:del=36" $NIT diff -H 2>&1 | cat -v | head -3)
if [ "$default_diff" != "$custom_diff" ]; then
PASS=$((PASS + 1))
echo " PASS: NIT_COLORS affects diff -H"
Expand All @@ -859,8 +870,8 @@ if command -v script >/dev/null 2>&1; then
fi

# NIT_COLORS affects log -H (color slot: hash, date)
default_log=$(script -q /dev/null $NIT log -H -n 1 2>&1 | cat -v | head -1)
custom_log=$(script -q /dev/null env NIT_COLORS="hash=35:date=36" $NIT log -H -n 1 2>&1 | cat -v | head -1)
default_log=$(run_tty $NIT log -H -n 1 2>&1 | cat -v | head -1)
custom_log=$(run_tty env NIT_COLORS="hash=35:date=36" $NIT log -H -n 1 2>&1 | cat -v | head -1)
if [ "$default_log" != "$custom_log" ]; then
PASS=$((PASS + 1))
echo " PASS: NIT_COLORS affects log -H"
Expand All @@ -871,8 +882,8 @@ if command -v script >/dev/null 2>&1; then
fi

# NIT_COLORS affects status -H (color slot: staged, unstaged)
default_status=$(script -q /dev/null $NIT status -H 2>&1 | cat -v)
custom_status=$(script -q /dev/null env NIT_COLORS="staged=35:unstaged=36" $NIT status -H 2>&1 | cat -v)
default_status=$(run_tty $NIT status -H 2>&1 | cat -v)
custom_status=$(run_tty env NIT_COLORS="staged=35:unstaged=36" $NIT status -H 2>&1 | cat -v)
if [ "$default_status" != "$custom_status" ]; then
PASS=$((PASS + 1))
echo " PASS: NIT_COLORS affects status -H"
Expand Down
Loading