Last active
June 15, 2023 19:06
-
-
Save NonLogicalDev/91840c799c0301860cb20f6601ffa6b0 to your computer and use it in GitHub Desktop.
Utilities for fixing up stacked git stack in case of accidental modifications.
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
#!/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