Skip to content

Instantly share code, notes, and snippets.

@NonLogicalDev
Last active June 15, 2023 19:06
Show Gist options
  • Save NonLogicalDev/91840c799c0301860cb20f6601ffa6b0 to your computer and use it in GitHub Desktop.
Save NonLogicalDev/91840c799c0301860cb20f6601ffa6b0 to your computer and use it in GitHub Desktop.
Utilities for fixing up stacked git stack in case of accidental modifications.
#!/bin/bash
# DEPENDENCIES:
# - git
# - stg
# - jq
# Stacked git records metadata in two commits:
#
# 1. Octopus commit that is a "merge" commit that has parent links to:
# * ^1 Simple Parent commit (this one has stack.json modifications)
# * ^2 Previous octopus commit
# * ^3 The associated commit on the branch (if this stack modifies the tree or commit metadata)
#
# $ STG_STACK_OCT=$(git rev-parse "stacks/$STG_STACK_REF")
#
# 2. Simple commit that contains the changes to the stack.json and patch files.
#
# $ STG_STACK_DAT=$(git rev-parse "$STG_STACK_OCTOPUS^1")
# ------------------------------------------------------------------------------
# COMMANDS
# ------------------------------------------------------------------------------
# __stg_fixup_rewind: (`stg-fixup rewind`)
#
# syncs the stacked git metadata to current head:
# * by marking all patches as unapplied
# * and changing the head to current HEAD
# This is the easiest fastest, and least destructive way to repair a broken stack
# if you are not immediately sure what went wrong, and would rather get yourself to
# a clean state as soon as possible.
#
# The result of this operation is that all of patches are marked unapplied
# while commits that are backing the patches are left in the branch unmodified.
#
# So in essense you are left in a similar state comparable to running `stg pop -a && stg pull`
# if the updates from upstream already include your merged patches.
#
# NOTE:
# * no modifications are applied to the branch
# * the only modifications that are done are done to stack metadata
#
# EXAMPLE:
#
# (assmuing stack):
# S SHA MSG PATCH
# ffff0 base N/A (plain git commit)
# + ffff1 commit1 (patch1)
# > ffff2 commit2 (patch2) (stg head)
# - ffff3 commit3 (patch3)
#
# (after `stg-fixup rewind`)
# S SHA MSG PATCH
# ffff0 base N/A (plain git commit)
# ffff1 commit1 N/A (plain git commit)
# ffff2 commit2 N/A (plain git commit) (stg head)
#
# - ffff1 commit1 (patch1)
# - ffff2 commit2 (patch2)
# - ffff3 commit3 (patch3)
#
__CMD_stg_fixup_rewind() {
( # set -x;
# Create a temporary file that will hold temporary git index.
# This will make it much simpler to update files in the git tree.
# Alternative purer would require fiddling with recursively rebuilding the tree with `git ls-tree` and `git mktree`.
local TMP_GIT_INDEX=$(mktemp)
export GIT_INDEX_FILE="$TMP_GIT_INDEX"
local STG_STACK_INFO=$(__stg_stack_info $(__git_current_branch))
local STG_STACK_REF=$(__json_get "$STG_STACK_INFO" .stg_stack_ref)
git read-tree "$(__json_get "$STG_STACK_INFO" .stg_stack_dat_ref)^{tree}"
local STG_STACK_JSON=$(jq -n \
--argjson stackinfo "$STG_STACK_INFO" \
'
$stackinfo.stg_stack.data
| .head = $stackinfo.git_top_sha
| .prev = $stackinfo.stg_stack_oct_ref
| .unapplied = .applied + .unapplied
| .applied = []
')
# Imperatively modify the index, by adding newly created and hashed stack.json and patch meta file.
git update-index --add --cacheinfo \
100644 "$(__git_hash "$STG_STACK_JSON")" "stack.json"
local STG_NEW_STACK_SHA=$(
__stg_stack_commit "(stg-fixup) rewind stack" $(git write-tree) "$STG_STACK_REF"
)
git update-ref "$STG_STACK_REF" "$STG_NEW_STACK_SHA"
# Clean up after ourselves.
rm "$TMP_GIT_INDEX"
)
}
__CMD_stg_fixup_align() {
local NUM=$1
# TODO:
# assert: no patches applied
#
# 1. take number `N` of commits to remap to unappied patches.
# 2. zip sha's of past N commits with next N patches.
# 3. change first N patches oids to match with commits
# 4. reset N commits back.
# 5. stg push -n N
echo "$NUM"
}
__CMD_stg_fixup_top_v1() {
CUR_SHA=$(__git_deref HEAD)
STG_TOP_SHA=$(stg id $(stg top))
git reset "${STG_TOP_SHA}"
stg refresh --force
cat <<EOS | stg edit -f -
$(stg edit --save-template=- | head -n 3)
$(git log -n 1 --format=%B $CUR_SHA)
EOS
}
# __CMD_stg_fixup_top: (`stg-fixup top`)
#
# fixes the top most applied patch to be in sync with current HEAD
# (only works if previous patch is on the stack)
#
# This is the quickest option to fixup stack metadata if you accidentally
# modified top most commit with `git amend` or something else.
#
# NOTE:
# * no modifications are applied to the branch
# * the only modifications that are done are done to stack metadata
#
# EXAMPLE:
# `ACTUAL` column stands for actual SHA if different from one known to stacked git.
#
# (assmuing stack):
# S SHA ACTUAL MSG PATCH
# ffff0 base N/A (plain git commit)
# + ffff1 commit1 (patch1)
# > ffff2 ffff5 commit2 (patch2) (stg head) 'after amend'
# - ffff3 commit3 (patch3)
#
# (after `stg-fixup top`)
# S SHA ACTUAL MSG PATCH
# ffff0 base N/A (plain git commit)
# + ffff1 commit1 (patch1)
# > ffff5 commit2 (patch2) (stg head)
# - ffff3 commit3 (patch3)
#
__CMD_stg_fixup_top() {
( # set -x;
# Create a temporary file that will hold temporary git index.
# This will make it much simpler to update files in the git tree.
# Alternative purer would require fiddling with recursively rebuilding the tree with `git ls-tree` and `git mktree`.
local TMP_GIT_INDEX=$(mktemp)
export GIT_INDEX_FILE="$TMP_GIT_INDEX"
local GIT_TOP_SHA=$(__git_deref HEAD)
local STG_STACK_INFO=$(__stg_stack_info $(__git_current_branch))
local STG_STACK_REF=$(__json_get "$STG_STACK_INFO" .stg_stack_ref)
local STG_TOP_PATCH=$(__json_get "$STG_STACK_INFO" .stg_top_patch)
local STG_TOP_PATCH_META=$(git cat-file -p "$STG_STACK_REF:patches/$STG_TOP_PATCH")
local STG_TOP_PREV=$(__json_get "$STG_STACK_INFO" '.stg_stack.data | .patches[.applied[-2]].oid // ""')
local GIT_TOP_PREV=$(__git_deref HEAD~1)
if [[ $STG_TOP_PREV != "" && $STG_TOP_PREV != $GIT_TOP_PREV ]] ; then
echo "error: last patch's parent is not on the stack (try rewind instead)" >&2
exit 1
fi
git read-tree "$(__json_get "$STG_STACK_INFO" .stg_stack_dat_ref)^{tree}"
local STG_STACK_JSON=$(jq -n \
--argjson si "$STG_STACK_INFO" \
'
$si.stg_stack.data
| .head = $si.git_top_sha
| .prev = $si.stg_stack_oct_ref
| .patches[$si.stg_top_patch].oid = $si.git_top_sha
')
local NEW_STG_PATCH_FILE=$(jq -rn \
--argjson si "$STG_STACK_INFO" \
--arg patch_meta "$STG_TOP_PATCH_META" \
--arg patch_body "$(git show -s --format=%s%n%n%b "$GIT_TOP_SHA")" \
--arg prev "$(__git_deref "$GIT_TOP_SHA^1^{tree}")" \
--arg top "$(__git_deref "$GIT_TOP_SHA^{tree}")" \
'
$patch_meta
| sub("(?<v>Top:\\s+).*"; "\(.v)\($top)")
| sub("(?<v>Bottom:\\s+).*"; "\(.v)\($prev)")
| split("\n")
| .[0:4] + ["", $patch_body]
| join("\n")
'
)
# Imperatively modify the index, by adding newly created and hashed stack.json and patch meta file.
git update-index --add --cacheinfo \
100644 "$(__git_hash "$STG_STACK_JSON")" "stack.json"
git update-index --add --cacheinfo \
100644 "$(__git_hash "$NEW_STG_PATCH_FILE")" "patches/$STG_TOP_PATCH"
local STG_NEW_STACK_SHA=$(
__stg_stack_commit \
"(stg-fixup) fixup top patch" \
$(git write-tree) \
"$STG_STACK_REF" -p "$GIT_TOP_SHA"
)
git update-ref "$STG_STACK_REF" "$STG_NEW_STACK_SHA"
# Clean up after ourselves.
rm "$TMP_GIT_INDEX"
)
}
# ------------------------------------------------------------------------------
# UTILS
# ------------------------------------------------------------------------------
__git_current_branch() {
git rev-parse --abbrev-ref HEAD
}
__git_deref() {
git rev-parse $1
}
__git_hash() {
echo "$1" | git hash-object -w --stdin
}
__json_get() {
echo "$1" | jq -r "$2"
}
__stg_stack_commit() {
local MSG=$1
local TREE=$2
local PARENT_OCT=$(__git_deref "$3")
local PARENT_DAT=$(__git_deref "$3^1")
shift 3
COMMIT_DAT=$(
echo "$MSG" | git commit-tree "$TREE" \
-p "$PARENT_DAT"
)
COMMIT_OCT=$(
echo "$MSG" | git commit-tree "$TREE" \
-p "$COMMIT_DAT" -p "$PARENT_OCT" "$@"
)
echo $COMMIT_OCT
}
__stg_stack_ref() {
local GIT_BRANCH="$1"
printf "refs/stacks/%s" "$GIT_BRANCH"
}
__stg_stack_file() {
local GIT_BRANCH=$1
local STG_STACK_REF=$(__stg_stack_ref "$GIT_BRANCH")
local STG_STACK_JSON=$STG_STACK_REF:stack.json
jq -nc \
--arg ref "$STG_STACK_JSON" \
--argjson data "$(git cat-file -p "$STG_STACK_JSON")" \
'
{
ref: $ref,
data: $data,
}
'
}
__stg_stack_info() {
local GIT_BRANCH=$1
local STG_STACK_REF=$(__stg_stack_ref "$GIT_BRANCH")
local STG_STACK_OCT_REF=$(__git_deref "$STG_STACK_REF")
local STG_STACK_DAT_REF=$(__git_deref "$STG_STACK_REF^1")
local STG_STACK_JSON=$STG_STACK_REF:stack.json
local STG_TOP_PATCH=$(stg top 2>/dev/null)
local GIT_TOP_SHA=$(git rev-parse HEAD)
jq -nc \
--arg git_branch "$GIT_BRANCH" \
--arg git_top_sha "$GIT_TOP_SHA" \
--arg stg_stack_ref "$STG_STACK_REF" \
--arg stg_stack_oct_ref "$STG_STACK_OCT_REF" \
--arg stg_stack_dat_ref "$STG_STACK_DAT_REF" \
--argjson stg_stack "$(__stg_stack_file "$GIT_BRANCH")" \
--arg stg_top_patch "$STG_TOP_PATCH" \
'
{
git_branch: $git_branch,
git_top_sha: $git_top_sha,
stg_top_patch: $stg_top_patch,
stg_stack_ref: $stg_stack_ref,
stg_stack_oct_ref: $stg_stack_oct_ref,
stg_stack_dat_ref: $stg_stack_dat_ref,
stg_stack: $stg_stack,
}
'
}
# ------------------------------------------------------------------------------
# MAIN
# ------------------------------------------------------------------------------
case $1 in
info )
__stg_stack_info $(__git_current_branch) | jq .
;;
top )
__CMD_stg_fixup_top
;;
rewind )
__CMD_stg_fixup_rewind
;;
align )
__CMD_stg_fixup_align "$@"
;;
* )
echo "available commands [ info, top, rewind ]"
;;
esac
# ------------------------------------------------------------------------------
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment