1
0
forked from a/repotool

Compare commits

...

4 Commits

Author SHA1 Message Date
a
abe56ca25a Merge pull request 'fish' (#1) from abs3nt/repotool:fishy into master
Reviewed-on: a/repotool#1
2025-08-22 15:20:10 +00:00
abs3nt
e286bb6189 fish 2025-08-22 15:20:10 +00:00
a
47e632254f
noot 2025-07-06 16:40:46 -05:00
a
ec647555b5
noot 2025-07-06 12:57:04 -05:00
8 changed files with 3827 additions and 4 deletions

View File

@ -1,4 +1,4 @@
.PHONY: all install
.PHONY: all install test
SOURCES_SRC:=$(shell find src -type f )
LIBS_SRC:=$(shell find lib -type f )
@ -7,7 +7,7 @@ REPOTOOL_PATH ?= ${HOME}/repo
all: dist/repotool
install: dist/repotool shell/zsh/repotool.zsh shell/zsh/repotool.plugin.zsh
install: dist/repotool shell/repotool.zsh shell/repotool.plugin.zsh
mkdir -p ${REPOTOOL_PATH}/.bin/
mkdir -p ${REPOTOOL_PATH}/.shell/
install dist/repotool ${REPOTOOL_PATH}/.bin/
@ -18,3 +18,6 @@ dist/repotool: $(SOURCES_SRC) $(LIBS_SRC) main.lua
luabundler bundle main.lua -p "./src/?.lua" -p "./lib/?.lua" -o dist/repotool
@sed -i "1i#!/usr/bin/env luajit" "dist/repotool"
chmod +x dist/repotool
test:
@lua test.lua

59
fish-repotool/README.md Normal file
View File

@ -0,0 +1,59 @@
# Fish Repotool Plugin
A Fish shell plugin for the repotool repository management system.
## Installation
### Using Fisher
```fish
fisher install path/to/fish-repotool
```
### Manual Installation
1. Copy the `conf.d/repotool.fish` file to your fish config directory:
```fish
cp conf.d/repotool.fish ~/.config/fish/conf.d/
```
2. Copy the `functions/repotool.fish` file to your fish functions directory:
```fish
cp functions/repotool.fish ~/.config/fish/functions/
```
## Configuration
Set the `REPOTOOL_PATH` environment variable to specify where your repositories are stored:
```fish
set -gx REPOTOOL_PATH "$HOME/my-repos"
```
If not set, it defaults to `$HOME/repo`.
## Usage
The plugin provides a `repo` alias that sources the repotool function with the following commands:
- `repo get <url>` - Clone a repository if not found
- `repo worktree list` - List existing worktrees
- `repo worktree get <name>` - Create or go to a worktree
- `repo worktree root` - Return to the root directory
- `repo worktree remove <name>` - Remove a worktree
- `repo open` - Open the current repository in web browser
## Hooks
The plugin supports hooks that can be placed in `$REPOTOOL_PATH/.hooks/`:
- `before_<hook_name>_cd.fish` - Executed before changing directory
- `after_<hook_name>_cd.fish` - Executed after changing directory
Hooks can also have `.sh` extension or no extension at all.
## Requirements
- Fish shell 4.0 or later
- `jq` for JSON parsing
- The repotool binary installed at `$REPOTOOL_PATH/.bin/repotool`

View File

@ -0,0 +1,9 @@
#!/usr/bin/env fish
# Set default REPOTOOL_PATH if not already set
if not set -q REPOTOOL_PATH
set -gx REPOTOOL_PATH "$HOME/repo"
end
# Create alias that sources the main repotool function
alias repo="source $REPOTOOL_PATH/.shell/repotool.fish"

View File

@ -0,0 +1,79 @@
#!/usr/bin/env fish
# Set default REPOTOOL_PATH if not already set
if not set -q REPOTOOL_PATH
set -gx REPOTOOL_PATH "$HOME/repo"
end
function _activate_hook
set -l hook_name $argv[1]
set -l response $argv[2]
if test -f "$REPOTOOL_PATH/.hooks/$hook_name.fish"
source "$REPOTOOL_PATH/.hooks/$hook_name.fish" $response
return 0
else if test -f "$REPOTOOL_PATH/.hooks/$hook_name.sh"
source "$REPOTOOL_PATH/.hooks/$hook_name.sh" $response
return 0
else if test -f "$REPOTOOL_PATH/.hooks/$hook_name"
source "$REPOTOOL_PATH/.hooks/$hook_name" $response
return 0
end
return 1
end
function _parse_json
set -l response $argv[1]
set -l key $argv[2]
echo "$response" | jq -r ".$key"
end
function _handle_response
set -l response $argv[1]
# Validate JSON
if not echo "$response" | jq . >/dev/null 2>&1
# Not valid JSON, write to stderr
echo "$response" >&2
return
end
# Check if response has an echo field
set -l echo_msg (echo "$response" | jq -r '.echo // empty')
if test -n "$echo_msg"
echo "$echo_msg"
end
# Get hook name from response
set -l hook_name (echo "$response" | jq -r '.hook // empty')
# Handle before hook if hook name is provided
if test -n "$hook_name"
_activate_hook "before_{$hook_name}_cd" "$response"
end
# Check if response has a cd field
set -l cd_path (echo "$response" | jq -r '.cd // empty')
if test -n "$cd_path"
cd "$cd_path"
end
# Handle after hook if hook name is provided
if test -n "$hook_name"
_activate_hook "after_{$hook_name}_cd" "$response"
end
end
set TOOL_BIN "$REPOTOOL_PATH/.bin/repotool"
# Pass all arguments to repotool and handle response
set -l response ($TOOL_BIN $argv)
set -l exit_code $status
if test $exit_code -ne 0
echo "Command failed with exit code $exit_code" >&2
return $exit_code
end
_handle_response "$response"

3453
lib/luaunit.lua Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,9 +7,18 @@ function git.parse_url(url)
local domain, path
-- Check if it's an HTTP(S) URL
domain, path = url:match("^https?://[^/]*@?([^/]+)/(.+)%.git$")
-- First try without authentication
domain, path = url:match("^https?://([^@/]+)/(.+)%.git$")
if not domain then
domain, path = url:match("^https?://[^/]*@?([^/]+)/(.+)$")
domain, path = url:match("^https?://([^@/]+)/(.+)$")
end
-- If that didn't match, try with authentication
if not domain then
domain, path = url:match("^https?://[^@]+@([^/]+)/(.+)%.git$")
end
if not domain then
domain, path = url:match("^https?://[^@]+@([^/]+)/(.+)$")
end
-- Check if it's an SSH URL (git@host:path or ssh://...)
@ -19,6 +28,7 @@ function git.parse_url(url)
if not domain then
domain, path = url:match("^[^@]+@([^:]+):(.+)$")
end
-- SSH URLs (ssh://...)
if not domain then
domain, path = url:match("^ssh://[^@]*@?([^/]+)/(.+)%.git$")
end

111
test.lua Normal file
View File

@ -0,0 +1,111 @@
#!/usr/bin/env lua
-- Test runner for repotool
-- This script discovers and runs all test files in the test/ directory
-- Add src to package path
package.path = package.path .. ";./src/?.lua"
-- ANSI color codes
local colors = {
green = "\27[32m",
red = "\27[31m",
yellow = "\27[33m",
reset = "\27[0m"
}
-- Test statistics
local stats = {
total = 0,
passed = 0,
failed = 0,
errors = {}
}
-- Simple test framework
_G.test = {}
_G.test.current_suite = nil
_G.test.current_test = nil
function _G.test.suite(name, fn)
_G.test.current_suite = name
print("\n" .. colors.yellow .. "Testing " .. name .. colors.reset)
fn()
_G.test.current_suite = nil
end
function _G.test.case(name, fn)
_G.test.current_test = name
stats.total = stats.total + 1
local status, err = pcall(fn)
if status then
stats.passed = stats.passed + 1
print(" " .. colors.green .. "" .. colors.reset .. " " .. name)
else
stats.failed = stats.failed + 1
print(" " .. colors.red .. "" .. colors.reset .. " " .. name)
table.insert(stats.errors, {
suite = _G.test.current_suite,
test = name,
error = err
})
end
_G.test.current_test = nil
end
function _G.assert_eq(actual, expected, message)
if actual ~= expected then
error((message or "Assertion failed") .. "\n Expected: " .. tostring(expected) .. "\n Actual: " .. tostring(actual))
end
end
function _G.assert_nil(value, message)
if value ~= nil then
error((message or "Expected nil") .. "\n Got: " .. tostring(value))
end
end
function _G.assert_not_nil(value, message)
if value == nil then
error(message or "Expected non-nil value")
end
end
-- Discover and run test files
local function run_tests()
local handle = io.popen("find test -name '*.lua' -type f | sort")
local test_files = handle:read("*a")
handle:close()
for file in test_files:gmatch("[^\n]+") do
-- Skip the test.lua file itself
if not file:match("test%.lua$") then
dofile(file)
end
end
end
-- Main
print(colors.yellow .. "Running repotool tests..." .. colors.reset)
run_tests()
-- Print summary
print("\n" .. string.rep("-", 40))
print("Test Summary:")
print(" Total: " .. stats.total)
print(" " .. colors.green .. "Passed: " .. stats.passed .. colors.reset)
print(" " .. colors.red .. "Failed: " .. stats.failed .. colors.reset)
-- Print errors if any
if #stats.errors > 0 then
print("\n" .. colors.red .. "Failures:" .. colors.reset)
for _, err in ipairs(stats.errors) do
print("\n " .. err.suite .. " > " .. err.test)
print(" " .. err.error:gsub("\n", "\n "))
end
end
-- Exit with appropriate code
os.exit(stats.failed > 0 and 1 or 0)

99
test/test_git_parser.lua Normal file
View File

@ -0,0 +1,99 @@
local git = require("git")
test.suite("git.parse_url", function()
test.case("parses HTTPS URL without .git", function()
local domain, path = git.parse_url("https://gitlab.freedesktop.org/xorg/proto")
assert_eq(domain, "gitlab.freedesktop.org", "Domain should be gitlab.freedesktop.org")
assert_eq(path, "xorg/proto", "Path should be xorg/proto")
end)
test.case("parses HTTPS URL without .git", function()
local domain, path = git.parse_url("https://gitlab.freedesktop.org/xorg/proto/xcbproto")
assert_eq(domain, "gitlab.freedesktop.org", "Domain should be gitlab.freedesktop.org")
assert_eq(path, "xorg/proto/xcbproto", "Path should be xorg/proto/xcbproto")
end)
test.case("parses HTTPS URL with .git", function()
local domain, path = git.parse_url("https://github.com/user/repo.git")
assert_eq(domain, "github.com", "Domain should be github.com")
assert_eq(path, "user/repo", "Path should be user/repo")
end)
test.case("parses HTTPS URL with authentication", function()
local domain, path = git.parse_url("https://user@gitlab.freedesktop.org/xorg/proto")
assert_eq(domain, "gitlab.freedesktop.org", "Domain should be gitlab.freedesktop.org")
assert_eq(path, "xorg/proto", "Path should be xorg/proto")
end)
test.case("parses HTTPS URL with authentication and .git", function()
local domain, path = git.parse_url("https://user:token@github.com/user/repo.git")
assert_eq(domain, "github.com", "Domain should be github.com")
assert_eq(path, "user/repo", "Path should be user/repo")
end)
test.case("parses SSH URL", function()
local domain, path = git.parse_url("git@github.com:user/repo.git")
assert_eq(domain, "github.com", "Domain should be github.com")
assert_eq(path, "user/repo", "Path should be user/repo")
end)
test.case("parses SSH URL without .git", function()
local domain, path = git.parse_url("git@gitlab.com:group/project")
assert_eq(domain, "gitlab.com", "Domain should be gitlab.com")
assert_eq(path, "group/project", "Path should be group/project")
end)
test.case("parses SSH URL with port", function()
local domain, path = git.parse_url("ssh://git@github.com/user/repo.git")
assert_eq(domain, "github.com", "Domain should be github.com")
assert_eq(path, "user/repo", "Path should be user/repo")
end)
test.case("parses bare domain path", function()
local domain, path = git.parse_url("gfx.cafe/oku/trade")
assert_eq(domain, "gfx.cafe", "Domain should be gfx.cafe")
assert_eq(path, "oku/trade", "Path should be oku/trade")
end)
test.case("returns nil for invalid URLs", function()
local domain, path = git.parse_url("not-a-url")
assert_nil(domain, "Domain should be nil for invalid URL")
assert_nil(path, "Path should be nil for invalid URL")
end)
test.case("handles complex paths", function()
local domain, path = git.parse_url("https://gitlab.com/group/subgroup/project.git")
assert_eq(domain, "gitlab.com", "Domain should be gitlab.com")
assert_eq(path, "group/subgroup/project", "Path should preserve subgroups")
end)
test.case("does not parse short SSH syntax as regular domain", function()
-- This ensures git@g:xorg/proto is treated as SSH, not misinterpreted
local domain, path = git.parse_url("git@g:xorg/proto")
assert_eq(domain, "g", "Domain should be g for SSH URL")
assert_eq(path, "xorg/proto", "Path should be xorg/proto")
end)
end)
test.suite("git.valid_url", function()
test.case("identifies HTTPS URLs", function()
assert_eq(git.valid_url("https://github.com/user/repo"), 1)
assert_eq(git.valid_url("http://example.com/path"), 1)
end)
test.case("identifies SSH URLs", function()
assert_eq(git.valid_url("git@github.com:user/repo"), 2)
assert_eq(git.valid_url("ssh://git@github.com/user/repo"), 2)
end)
test.case("identifies bare domain paths", function()
assert_eq(git.valid_url("example.com/user/repo"), 3)
assert_eq(git.valid_url("sub.example.org/path/to/repo"), 3)
end)
test.case("returns -1 for invalid URLs", function()
assert_eq(git.valid_url("not-a-url"), -1)
assert_eq(git.valid_url("/local/path"), -1)
assert_eq(git.valid_url("file:///path"), -1)
end)
end)