Created
October 16, 2023 03:49
-
-
Save apainintheneck/ddc87043a645e87f2d9e02b69be155b6 to your computer and use it in GitHub Desktop.
An experimental wrapper for Git that acts like a shell.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env gawk -f | |
# Inspired by zbg | |
# We use gawk to allow next inside a function. | |
BEGIN { | |
if(!is_git_installed()) { | |
die("Git is not installed!") | |
} | |
if(!is_git_repo()) { | |
die("This is not a Git repository!") | |
} | |
# Initialize constants | |
init_shortcuts() | |
NO_COLOR = ENVIRON["NO_COLOR"] | |
if(NO_COLOR) | |
GIT_COMMAND = "git -c color.ui=false " | |
else | |
GIT_COMMAND = "git -c color.ui=always " | |
print "# Welcome to gitsh!" | |
print "# This is a simple wrapper around Git that acts like a shell." | |
print "#" | |
print "# Type any Git subcommand to run it without prefixing 'git'." | |
print "#" | |
print "# Flags can even be passed using the dot syntax." | |
print "# Dot syntax: grep.v.no:index 'service do'" | |
print "# Old syntax: grep -v --no-index 'service do'" | |
print "#" | |
print "# To run normal shell commands add the '$' or 'sh' prefix" | |
print "#############################################################" | |
shell_prompt() | |
} | |
is_cmd("now") { | |
sub($1, "status") | |
git($0) | |
next_command() | |
} | |
is_cmd("twig") { | |
if(NF == 1) | |
too_few_args(2, NF) | |
else if(NF == 2) | |
git("switch -c " $2) | |
else | |
too_many_args(2, NF) | |
next_command() | |
} | |
is_cmd("apply") { | |
if(NF == 1) | |
git("stash apply") | |
else | |
too_many_args(1, NF) | |
next_command() | |
} | |
is_cmd("amend") { | |
if(NF == 1) | |
git("commit --amend") | |
else | |
too_many_args(1, NF) | |
next_command() | |
} | |
is_cmd("drop") || | |
$1 == "drop.f" || | |
$1 == "drop.force" { | |
command = ($1 == "drop") ? "reset" : "reset --hard" | |
if(NF == 1) | |
git(command " HEAD~1") | |
else if(NF == 2) { | |
if($2 ~ /^[1-9][0-9]*$/) | |
git(command " HEAD~" $2) | |
else | |
invalid_args("a positive integer", $2) | |
} else | |
too_many_args(1, NF) | |
next_command() | |
} | |
is_cmd("clear") { | |
if(NF == 1) { | |
git("add .") && | |
git("reset --hard") | |
} else | |
too_many_args(1, NF) | |
next_command() | |
} | |
is_cmd("sync") { | |
if(NF == 1) { | |
if(branch_name = current_branch()) | |
git("pull --ff-only origin " branch_name) | |
} else | |
too_many_args(1, NF) | |
next_command() | |
} | |
$1 == "sync.f" || | |
$1 == "sync.force" { | |
if(NF == 1) { | |
if(branch_name = current_branch()) { | |
git("fetch origin " branch_name) && | |
git("reset --hard origin/" branch_name) | |
} | |
} else | |
too_many_args(1, NF) | |
next_command() | |
} | |
$1 == "sh" || | |
$1 == "$" { | |
if(NF >= 2) { | |
sub(/sh|\$/, "") | |
sh($0) | |
} else | |
too_few_args("2 or more", NF) | |
next_command() | |
} | |
is_cmd("help") || | |
is_cmd("man") { | |
if(NF == 1) | |
git("help") | |
else if(NF == 2) | |
git("help " $2) | |
else | |
too_many_args("1 or 2", NF) | |
next_command() | |
} | |
# Note: We don't use is_cmd() here because we don't want to shortcut these commands. | |
$1 == "exit" || | |
$1 == "quit" { | |
print "Have a nice day!" | |
exit(0) | |
} | |
NF > 0 { | |
parse_dot_options() | |
if(is_cmd() || !print_longcuts()) | |
git($0) | |
} | |
{ | |
next_command() | |
} | |
## Error Helpers | |
function err(message) { | |
print("Error: " message) | |
} | |
function die(message) { | |
err(message) | |
exit(1) | |
} | |
function expectation(error, expected, received) { | |
err(error ": expected " expected " but received " received) | |
} | |
function too_many_args(expected, received) { | |
expectation("too many args", expected, received) | |
} | |
function too_few_args(expected, received) { | |
expectation("too few args", expected, received) | |
} | |
function invalid_args(expected, received) { | |
expectation("invalid args", expected, received) | |
} | |
## Shell Helpers | |
function shell_prompt( prompt_string) { | |
printf(blue("gitsh(") cyan(silent_current_branch()) blue(")> ")) | |
} | |
function blue(string) { | |
return "\033[34m" string "\033[0m" | |
} | |
function cyan(string) { | |
return "\033[36m" string "\033[0m" | |
} | |
function next_command() { | |
shell_prompt() | |
next | |
} | |
# Before: "grep.v.no:index -A 10 'service do'" | |
# After: "grep -v --no-index -A 10 'service do'" | |
function parse_dot_options( first, array, idx, option_type) { | |
if($1 !~ /[.:]/) return | |
first = gsub(":", "-", $1) | |
split(first, array, ".") | |
first = array[1] | |
for(idx = 2; idx <= length(array); idx++) { | |
if(length(array[idx]) == 0) continue | |
# Check for long and short options | |
option_type = length(array[idx]) == 1 ? "-" : "--" | |
first = first " " option_type array[idx] | |
} | |
sub($1, first) | |
} | |
function sh(args) { | |
sub(/^ */, "", args) | |
print("$ " args) | |
return system(args) == 0 | |
} | |
function git(args) { | |
sub(/^ */, "", args) | |
print("$ git " args) | |
return system(GIT_COMMAND args) == 0 | |
} | |
## Git Commands | |
function is_git_installed() { | |
return system("git --version 1> /dev/null 2> /dev/null") == 0 | |
} | |
function is_git_repo( cmd, res) { | |
cmd = "git rev-parse --is-inside-work-tree 2> /dev/null" | |
cmd | getline res | |
close(cmd) | |
return res == "true" | |
} | |
function current_branch( cmd, res) { | |
cmd = "git branch --show-current" | |
cmd | getline res | |
close(cmd) | |
return res | |
} | |
function silent_current_branch( cmd, res) { | |
cmd = "git branch --show-current 2> /dev/null" | |
cmd | getline res | |
close(cmd) | |
return length(res) == 0 ? "..." : res | |
} | |
## Shortcuts | |
# Checks if the command matches exactly or is the unique shortcut for a command. | |
# If no command is given, it tries to find a valid command in the CMD_LIST array. | |
function is_cmd(cmd) { | |
if(cmd) { | |
if($1 == cmd) { | |
return 1 | |
} else if($1 in SHORTCUTS && SHORTCUTS[$1] == cmd) { | |
sub($1, cmd) | |
return 1 | |
} else { | |
return 0 | |
} | |
} else { | |
if($1 in CMD_LIST) { | |
return 1 | |
} else if($1 in SHORTCUTS) { | |
print SHORTCUTS[$1] | |
sub($1, SHORTCUTS[$1]) | |
return 1 | |
} else { | |
return 0 | |
} | |
} | |
} | |
# Print the possible commands that a prefix might be valid for. | |
# Returns the number of valid commands for a prefix. | |
function print_longcuts( idx, len) { | |
len = LONGCUTS[$1] | |
if(len > 0) { | |
print "Did you mean..." | |
for(idx = 1; idx <= len; idx++) | |
printf("- %s\n", LONGCUTS[$1, idx]) | |
} | |
return len | |
} | |
# Init the SHORTCUTS, LONGCUTS and CMD_LIST arrays. | |
# These are used when searching for commands based on the command prefix. | |
# | |
# SHORTCUTS : Command prefixes that map to only one unique command. | |
# LONGCUTS : Command prefixes that map to several commands. | |
# CMD_LIST : A list of all valid internal and normal Git commands. | |
function init_shortcuts( tmp_array, freq, cmd_string, cmd, len, prefix, idx, shell_cmd) { | |
cmd_string = "now twig apply amend drop clear sync sh help man exit quit" | |
split(cmd_string, tmp_array) | |
for(idx in tmp_array) | |
CMD_LIST[tmp_array[idx]] = 1 | |
shell_cmd = "git --list-cmds=main,nohelpers" | |
while((shell_cmd | getline cmd) > 0) | |
CMD_LIST[cmd] = 1 | |
close(shell_cmd) | |
for(cmd in CMD_LIST) { | |
len = length(cmd) | |
for(idx = 1; idx < len; idx++) { | |
prefix = substr(cmd, 1, idx) | |
if(prefix in CMD_LIST) continue | |
freq = ++LONGCUTS[prefix] | |
if(1 == freq) { | |
SHORTCUTS[prefix] = cmd | |
} else if(2 == freq) { | |
LONGCUTS[prefix, 1] = SHORTCUTS[prefix] | |
LONGCUTS[prefix, 2] = cmd | |
delete SHORTCUTS[prefix] | |
} else { | |
LONGCUTS[prefix, freq] = cmd | |
} | |
} | |
} | |
for(prefix in LONGCUTS) { | |
if(1 == LONGCUTS[prefix]) | |
delete LONGCUTS[prefix] | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment