Skip to content

Instantly share code, notes, and snippets.

@Lightfire228
Last active September 13, 2022 20:00
Show Gist options
  • Save Lightfire228/a4070939ffae712dbe4962101ec0ce0a to your computer and use it in GitHub Desktop.
Save Lightfire228/a4070939ffae712dbe4962101ec0ce0a to your computer and use it in GitHub Desktop.
Git Hop

Git Hop

This is a python script for "hopping" branches. Hopping is defined as preserving the current working changes with the current branch. This will add all current changes to the working directory to a temp commit, then switch git branches, and pop any temp commits detected on the new branch.

Alias

You can alias this script as a git sub-command

git config --global alias.hop "!python /path/to/git_hop.py"

Hook injection

By default, the script will inject a post-checkout hook into the .git/hooks folder. This hook will check for a temp commit in the branch you just switched to (without using hop), and print a warning message if one is found. You can disable this behaviour by toggling EMBED_HOOK to false

Example

These are the equivalent git commands

# git hop branch_2

git add -A
git commit -m "!!hop_temp_commit!!"
git switch branch_2

# --- 
# git hop branch_1
git add -A
git commit -m "!!hop_temp_commit!!"
git switch branch_1
# if last commit is temp commit
git reset HEAD~1
from pathlib import Path
import argparse
import os
import subprocess
import sys
COMMIT_MSG = '!!hop_temp_commit!!'
CLEAN_MSG = 'nothing to commit, working tree clean'
SELF_CHECK = '_GIT_HOP_DISABLE'
MSG_PREFIX = 'Git Hop:'
# set this to false if you don't want git_hop to automatically embed a
# post-checkout hook checking for the temp commit on switch/checkout
EMBED_HOOK = True
def main():
if os.environ.get(SELF_CHECK, False):
return
if EMBED_HOOK:
_embed_hook()
args = _parse_args()
if args.check:
check()
elif args.pop:
pop()
else:
hop(args.branch)
def hop(branch: str):
if not branch:
_log('No branch specified!')
return
if not _is_clean():
_push_commit()
_switch(branch)
if _has_temp_commit():
_pop_commit()
def check():
if _has_temp_commit():
_log('Temp hop commit found, but not popped! (pop with "git hop -p")')
def pop():
if _has_temp_commit():
_pop_commit()
else:
_log('Temp hop commit not found', error = False)
#region helpers
def _push_commit():
_run(['git', 'add', '-A'])
_run(['git', 'commit', '-m', COMMIT_MSG])
def _pop_commit():
_run(['git', 'reset', 'HEAD~1'])
def _switch(branch: str):
env = {
**os.environ,
SELF_CHECK: str(1)
}
_run(['git', 'switch', branch], env=env)
def _has_temp_commit():
p = _run([
'git', 'log',
'-n', '1',
'--format=%B'
], capture_output = True)
msg = p.stdout.decode('utf-8').strip()
return msg == COMMIT_MSG
def _is_clean():
p = _run(['git', 'status'], capture_output = True)
return CLEAN_MSG in str(p.stdout).strip()
def _embed_hook():
cwd = Path(os.getcwd()).resolve()
me = Path(__file__).resolve()
hook_dir = cwd / '.git/hooks'
# not a git repo
if not hook_dir.exists():
return
hook = hook_dir / 'post-checkout'
if hook.exists():
# check for our script in existing hook
if me.name not in hook.read_text():
_log(f'existing "post-checkout" hook found, but does not call "{me.name}"')
# hook already injected previously
else:
return
else:
_log(f'"post-checkout" not found. Adding temp commit check hook')
hook.write_text('\n'.join([
f'#!/bin/sh',
f'python "{me}" -c'
f''
]))
#endregion
#region utilities
def _run(cmd: list | str, **kwargs) -> subprocess.CompletedProcess:
cmd = (
cmd.split(' ') if isinstance(cmd, str)
else [str(c) for c in cmd]
)
return subprocess.run(cmd, **kwargs)
def _log(msg, error = True):
file_ = sys.stderr if error else sys.stdout
print(MSG_PREFIX, msg, file = file_)
def _parse_args():
parser = argparse.ArgumentParser(
description="Usage: add this script as an alias to git (eg. git hop develop)"
)
parser.add_argument('branch', nargs='?', default='', help = 'The branch to hop to')
parser.add_argument('-c', '--check', action = 'store_true', help = 'Check if temp commit exists')
parser.add_argument('-p', '--pop', action = 'store_true', help = 'Pop temp commit of current branch')
return parser.parse_args()
#endregion
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment