Skip to content

Instantly share code, notes, and snippets.

@apainintheneck
Created October 16, 2023 03:49
Show Gist options
  • Save apainintheneck/ddc87043a645e87f2d9e02b69be155b6 to your computer and use it in GitHub Desktop.
Save apainintheneck/ddc87043a645e87f2d9e02b69be155b6 to your computer and use it in GitHub Desktop.
An experimental wrapper for Git that acts like a shell.
#!/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