Skip to content

Instantly share code, notes, and snippets.

@stanislaw
Last active November 16, 2024 19:59
Show Gist options
  • Save stanislaw/e31f6dd84426ef7aedb6c110d9237729 to your computer and use it in GitHub Desktop.
Save stanislaw/e31f6dd84426ef7aedb6c110d9237729 to your computer and use it in GitHub Desktop.
Keyboard shortcuts for command-line Git
#! /usr/bin/env python
import sys
import subprocess
import termios
import tty
from datetime import datetime
from pathlib import Path
from typing import Optional
class RFXCustomFormatter:
inverted = "\x1b[7;90m"
reset = "\x1b[0m"
@staticmethod
def print(message: str, color: str):
print(RFXCustomFormatter.format(message, color))
@staticmethod
def format(message: str, color: str):
return color + message + RFXCustomFormatter.reset
class GitClient:
def add_all(self):
command = "git add -A"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(command.split(" "), capture_output=False, text=True)
def branch_latest(self, all=False):
args = [
"git",
"for-each-ref",
"--sort=committerdate",
"--format=%(HEAD) %(color:yellow)%(refname:short)%(color:reset) - %(color:red)%(objectname:short)%(color:reset) - %(contents:subject) - %(authorname) (%(color:green)%(committerdate:relative)%(color:reset))"
]
if not all:
args.append("refs/heads/")
command = " ".join(args)
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(args, capture_output=False, text=True)
def commit(self, amend=False, no_edit=False, message=None):
args = ["git", "commit"]
if amend:
args.append("--amend")
if no_edit:
args.append("--no-edit")
if message is not None:
args.append("-m")
args.append(message)
command = " ".join(args)
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(args, capture_output=False, text=True)
def commit_new_or_amend_existing(self):
result = subprocess.run(["git", "show", "--summary"], capture_output=True, text=True)
if "Merge:" in result.stdout:
self.commit(message="WIP: ...")
return
self.commit(amend=True, no_edit=True)
def diff(self, target=None):
args = ["git", "diff"]
if target is not None:
args.append(target)
command = " ".join(args)
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(args, capture_output=False, text=True)
def fetch(self):
command = "git fetch"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(command.split(" "), capture_output=False, text=True)
def log(self, commits: Optional[int] = None):
args = ["git", "log"]
if commits is not None:
assert isinstance(commits, int), commits
args.append(f"-l{commits}")
command = " ".join(args)
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(args, capture_output=False, text=True)
def push(self, force=False):
args = ["git", "push"]
if force:
args.append("--force")
command_color = RFXCustomFormatter.inverted
command = " ".join(args)
RFXCustomFormatter.print(command, command_color)
subprocess.run(args, capture_output=False, text=True)
def pull(self):
args = ["git", "pull"]
command_color = RFXCustomFormatter.inverted
command = " ".join(args)
RFXCustomFormatter.print(command, command_color)
subprocess.run(args, capture_output=False, text=True)
def rebase(self, branch: str):
assert isinstance(branch, str)
command = f"git rebase {branch}"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(command.split(" "), capture_output=False, text=True)
def rebase_interactive(self):
command = "git rebase origin/main -i"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(command.split(" "), capture_output=False, text=True)
def show(self):
command = "git show"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(command.split(" "), capture_output=False, text=True)
def status(self):
status_command = "git status"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(status_command, command_color)
subprocess.run(status_command.split(" "), capture_output=False, text=True)
def switch_to_main(self):
switch_command = "git switch main"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(switch_command, command_color)
subprocess.run(switch_command.split(" "), capture_output=False, text=True)
def switch_to_previous_branch(self):
switch_command = "git switch -"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(switch_command, command_color)
subprocess.run(switch_command.split(" "), capture_output=False, text=True)
def reset_soft_unstage(self, target=None):
args = ["git", "reset"]
if target is not None:
args.append(target)
command = " ".join(args)
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(command, command_color)
subprocess.run(args, capture_output=False, text=True)
def reset_hard(self):
# First, preserve the diff.
print("")
self._apply_patch()
print("")
reset_command = "git reset --hard"
command_color = RFXCustomFormatter.inverted
RFXCustomFormatter.print(reset_command, command_color)
subprocess.run(reset_command.split(" "), capture_output=False, text=True)
def _apply_patch(self):
result = subprocess.run("git diff HEAD".split(" "), capture_output=True, text=True)
assert result.returncode == 0
if len(result.stdout) == 0:
print(
f"**********************************************************************"
)
print(f"The diff is empty. Not creating a patch.")
print(
f"**********************************************************************"
)
return
path_to_user_dir = Path.home()
patches_dir = path_to_user_dir.joinpath("patches")
patches_dir.mkdir(parents=True, exist_ok=True)
now = datetime.now()
formatted_date = now.strftime('%Y%m%d_%H%M%S_%f')
path_to_out_patch = patches_dir.joinpath(f"{formatted_date}.patch")
with open(path_to_out_patch, "w") as out_patch:
out_patch.write(result.stdout)
print(f"**********************************************************************")
print(f"Created a patch: {path_to_out_patch}")
print(f"**********************************************************************")
def get_key():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
def main():
git_client = GitClient()
debug = False
current_pattern = ""
escape = False
while True:
c = get_key()
current_pattern += c
if debug:
print(c, ord(c), end=None)
print(f"PATT: '{current_pattern}'")
if ord(c) == 3:
print("")
sys.exit(0)
# TAB
if ord(c) == 9:
git_client.fetch()
git_client.rebase("origin/main")
git_client.status()
sys.exit(0)
# Escape sequence
if ord(c) == 27:
escape = True
current_pattern = "27"
continue
# Backspace
if ord(c) == 127:
current_pattern = current_pattern[:-2]
print(f"Correcting pattern: {current_pattern}")
continue
if escape:
# Up
if current_pattern == "27[A":
git_client.push()
git_client.status()
sys.exit(0)
# Shift + Up
elif current_pattern == "27[1;2A":
git_client.push(force=True)
git_client.status()
sys.exit(0)
# Down
elif current_pattern == "27[B":
git_client.pull()
git_client.status()
sys.exit(0)
# Shift + Down
elif current_pattern == "27[1;2B":
git_client.rebase("origin/main")
git_client.status()
sys.exit(0)
# Left
elif current_pattern == "27[D":
git_client.switch_to_previous_branch()
git_client.status()
sys.exit(0)
# Shift+TAB
elif current_pattern == "27[Z":
git_client.fetch()
git_client.rebase_interactive()
git_client.status()
sys.exit(0)
else:
if c == "A":
git_client.add_all()
git_client.status()
sys.exit(0)
elif c == "B":
git_client.branch_latest()
sys.exit(0)
elif c == "C":
git_client.commit()
git_client.status()
sys.exit(0)
elif c == "D":
git_client.diff()
sys.exit(0)
elif c == "F":
git_client.fetch()
git_client.status()
sys.exit(0)
elif c == "H":
git_client.show()
sys.exit(0)
elif c == "L":
git_client.log()
sys.exit(0)
elif c == "P":
git_client.push()
git_client.status()
sys.exit(0)
elif c == "R":
git_client.rebase("origin/main")
git_client.status()
sys.exit(0)
elif c == "S":
git_client.status()
sys.exit(0)
elif c == "M":
git_client.switch_to_main()
git_client.status()
sys.exit(0)
if c == "!":
git_client.reset_hard()
git_client.status()
sys.exit(0)
elif c == '-':
git_client.reset_soft_unstage()
git_client.status()
sys.exit(0)
elif c == '_':
git_client.reset_soft_unstage("HEAD^")
git_client.status()
sys.exit(0)
elif c == "+":
git_client.add_all()
git_client.status()
git_client.commit_new_or_amend_existing()
git_client.status()
sys.exit(0)
elif current_pattern == "bl1":
git_client.branch_latest()
sys.exit(0)
elif current_pattern == "bl2":
git_client.branch_latest(all=True)
sys.exit(0)
elif current_pattern == "cm1":
git_client.commit()
git_client.status()
sys.exit(0)
elif current_pattern == "cm2":
git_client.commit(amend=True)
git_client.status()
sys.exit(0)
elif current_pattern == "cm3":
git_client.commit(amend=True, no_edit=True)
git_client.status()
sys.exit(0)
elif current_pattern == "di1":
git_client.diff()
sys.exit(0)
elif current_pattern == "di2":
git_client.diff(target="HEAD")
sys.exit(0)
elif current_pattern == "l1":
git_client.log(commits=1)
sys.exit(0)
elif current_pattern == "l2":
git_client.log(commits=2)
sys.exit(0)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment