Last active
November 16, 2024 19:59
-
-
Save stanislaw/e31f6dd84426ef7aedb6c110d9237729 to your computer and use it in GitHub Desktop.
Keyboard shortcuts for command-line Git
This file contains hidden or 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 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