Skip to content

Commit be54b55

Browse files
committed
feat: open coverage to a github remote origin in a web browser
0 parents  commit be54b55

File tree

9 files changed

+282
-0
lines changed

9 files changed

+282
-0
lines changed

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
use flake

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Development
2+
.direnv
3+
.zig-cache
4+
5+
# Build
6+
zig-out

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog][changelog], and this project adheres
6+
to [Semantic Versioning][semver].
7+
8+
## [0.1.0] - 2025-05-03
9+
10+
### Added
11+
12+
- Open coverage to a GitHub remote origin in a web browser with `coverage`.
13+
14+
<!-- a collection of links -->
15+
16+
[changelog]: https://keepachangelog.com/en/1.1.0/
17+
[semver]: https://semver.org/spec/v2.0.0.html
18+
19+
<!-- a collection of releases -->
20+
21+
[Unreleased]: https://github.com/zimeg/git-coverage/compare/v0.1.0...HEAD
22+
[0.1.0]: https://github.com/zimeg/git-coverage/releases/tag/v0.1.0

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 Eden Zimbelman
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# git-coverage
2+
3+
Open test coverage uploaded to [`codecov`][codecov] in a web browser.
4+
5+
## Usage
6+
7+
The following command uses `git` details:
8+
9+
```sh
10+
$ git coverage # https://app.codecov.io/gh/zimeg/git-coverage
11+
```
12+
13+
## Installation
14+
15+
Add a build to `$PATH` for automatic detection.
16+
17+
[codecov]: https://about.codecov.io

build.zig

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const std = @import("std");
2+
3+
pub fn build(b: *std.Build) void {
4+
const target = b.standardTargetOptions(.{});
5+
const optimize = b.standardOptimizeOption(.{});
6+
const exe_mod = b.createModule(.{
7+
.root_source_file = b.path("src/main.zig"),
8+
.target = target,
9+
.optimize = optimize,
10+
});
11+
const exe = b.addExecutable(.{
12+
.name = "git-coverage",
13+
.root_module = exe_mod,
14+
});
15+
b.installArtifact(exe);
16+
17+
const run_step = b.step("run", "Run the command");
18+
const run_cmd = b.addRunArtifact(exe);
19+
run_cmd.step.dependOn(b.getInstallStep());
20+
if (b.args) |args| {
21+
run_cmd.addArgs(args);
22+
}
23+
run_step.dependOn(&run_cmd.step);
24+
25+
const test_step = b.step("test", "Run unit tests");
26+
const exe_unit_tests = b.addTest(.{
27+
.root_module = exe_mod,
28+
});
29+
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
30+
test_step.dependOn(&run_exe_unit_tests.step);
31+
}

flake.lock

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
description = "open test coverage in a web browser";
3+
inputs = {
4+
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
5+
flake-utils.url = "github:numtide/flake-utils";
6+
};
7+
outputs =
8+
{
9+
nixpkgs,
10+
flake-utils,
11+
...
12+
}:
13+
flake-utils.lib.eachDefaultSystem (
14+
system:
15+
let
16+
pkgs = import nixpkgs { inherit system; };
17+
in
18+
{
19+
packages.default = pkgs.stdenv.mkDerivation {
20+
pname = "git-coverage";
21+
version = "unversioned";
22+
src = ./.;
23+
nativeBuildInputs = [
24+
pkgs.zig.hook
25+
];
26+
zigBuildFlags = [ "-Doptimize=ReleaseSmall" ];
27+
};
28+
devShells.default = pkgs.mkShell {
29+
packages = [
30+
pkgs.git # https://github.com/git/git
31+
pkgs.zig # https://github.com/ziglang/zig
32+
];
33+
};
34+
}
35+
);
36+
}

src/main.zig

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
const std = @import("std");
2+
3+
pub fn main() !void {
4+
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
5+
var aa = std.heap.ArenaAllocator.init(gpa.allocator());
6+
defer aa.deinit();
7+
const allocator = aa.allocator();
8+
const proc = try std.process.Child.run(.{
9+
.allocator = allocator,
10+
.argv = &.{ "git", "remote", "-v" },
11+
});
12+
if (proc.stdout.len <= 0) {
13+
return error.GitRemoteMissing;
14+
}
15+
const remote = try origin(proc.stdout);
16+
const project = try repo(remote);
17+
const url = try coverage(allocator, project);
18+
_ = std.process.Child.run(.{
19+
.allocator = allocator,
20+
.argv = &[_][]const u8{ "open", url },
21+
}) catch {
22+
try std.io.getStdOut().writer().print("{s}\n", .{ url });
23+
};
24+
}
25+
26+
fn origin(remotes: []const u8) ![]const u8 {
27+
var remote = std.mem.splitScalar(u8, remotes, '\n');
28+
while (true) {
29+
const line = remote.next();
30+
if (line) |val| {
31+
if (std.mem.startsWith(u8, val, "origin") and std.mem.endsWith(u8, val, "(push)")) {
32+
return std.mem.trim(u8, val[7..val.len-6], " ");
33+
}
34+
} else {
35+
return error.GitOriginMissing;
36+
}
37+
}
38+
}
39+
40+
test "origin http" {
41+
const remotes = "origin https://github.com/zimeg/git-coverage.git (fetch)\norigin https://github.com/zimeg/git-coverage.git (push)";
42+
const remote = try origin(remotes);
43+
try std.testing.expectEqualStrings(remote, "https://github.com/zimeg/git-coverage.git");
44+
}
45+
46+
test "origin ssh" {
47+
const remotes = "origin git@github.com:zimeg/git-coverage.git (fetch)\norigin git@github.com:zimeg/git-coverage.git (push)";
48+
const remote = try origin(remotes);
49+
try std.testing.expectEqualStrings(remote, "git@github.com:zimeg/git-coverage.git");
50+
}
51+
52+
fn repo(remote: []const u8) ![]const u8 {
53+
if (std.mem.startsWith(u8, remote, "https://github.com/")) {
54+
return remote[19..remote.len-4];
55+
}
56+
if (std.mem.startsWith(u8, remote, "git@github.com:")) {
57+
return remote[15..remote.len-4];
58+
}
59+
return error.GitProtocolMissing;
60+
}
61+
62+
test "repo http" {
63+
const remote = "https://github.com/zimeg/git-coverage.git";
64+
const project = try repo(remote);
65+
try std.testing.expectEqualStrings(project, "zimeg/git-coverage");
66+
}
67+
68+
test "repo ssh" {
69+
const remote = "git@github.com:zimeg/git-coverage.git";
70+
const project = try repo(remote);
71+
try std.testing.expectEqualStrings(project, "zimeg/git-coverage");
72+
}
73+
74+
fn coverage(allocator: std.mem.Allocator, project: []const u8) ![]const u8 {
75+
return std.fmt.allocPrint(
76+
allocator,
77+
"https://app.codecov.io/gh/{s}",
78+
.{ project },
79+
);
80+
}
81+
82+
test "coverage project" {
83+
const project = "zimeg/git-coverage";
84+
const url = try coverage(std.testing.allocator, project);
85+
defer std.testing.allocator.free(url);
86+
try std.testing.expectEqualStrings(url, "https://app.codecov.io/gh/zimeg/git-coverage");
87+
}

0 commit comments

Comments
 (0)