1

I have a hacky shell script that I use to clean up the history on a feature branch when I'm working with people who don't understand how rebasing works and have hundreds of messy commits and multiple merge commits, meaning it is not possible to do a single squash or fixup interactive rebase.

This script basically creates a single squashed commit out of their branch vs what they are merging to, regardless of how they got there.

But it's really hacky, is there an existing git command that does this? Or is there a more idiomatic way to write the script?

#!/bin/bash  
set -e
if [ -n "$(git status -s)" ] ; then
   echo "ERROR: uncommitted changes"
   exit 1
fi
if [ -z "$1" ] ; then
   echo "ERROR: you must provide a base branch"
   exit 1
fi
NAME=`git rev-parse --abbrev-ref HEAD`
CUR=`git rev-parse HEAD`
TO=`git rev-parse $1`
echo "backing up your branch in bak/$CUR"
git checkout -b bak/$CUR
git checkout -b tmp/$CUR
git reset --hard $TO
git merge --squash $CUR
git commit --no-edit
git checkout $NAME
git reset --hard tmp/$CUR
git branch -D tmp/$CUR
echo "created a squash commit against $1 and rewrote your history"
4
  • I guess the best approach would be to fix your coworkers instead of fixing their messy commit history,... If that is not possible, I would do this: git-log all messages into a tempfile, git-checkout their latest commit, git-reset --soft to git merge-base HEAD <branch to merge to> and then git-commit with the contents from the tempfile. Not you can merge the single commit. Of course you lose all commit history with this approach. Another one would be to git-log --no-merges to merge-base and cherry-pick each commit... but merges are lost this way, which could break things.
    – musicmatze
    Commented Nov 16, 2017 at 11:16
  • that sounds like a multi-step alternative to the single script call that I'm currently using... why is that better?
    – fommil
    Commented Nov 16, 2017 at 12:10
  • You can put that into a script as well, of course. And it is even more simple (the reset-soft-approach). Also: No hard reset, so less likely to screw up things.
    – musicmatze
    Commented Nov 16, 2017 at 12:19
  • I already know about that approach, I linked to it in the ticket... I don't think it's as easily automated. Plus my script takes a backup of the branch before doing anything.
    – fommil
    Commented Nov 16, 2017 at 12:31

1 Answer 1

2

... is there an existing git command that does this?

No: it takes at least three to do what you are doing, in terms of the final commits and positions of branch names:

  1. git branch to create a new branch name pointing to the commit identified by HEAD;
  2. git reset --soft to move the current branch name to the target commit without changing the index and work-tree;
  3. git commit to make the new commit.

To obtain the desired log message for the new commit (the one built by git merge --squash) takes additional commands. To get git merge --squash to build the message for you, you must use git merge --squash or repeat its logic (check merge.branchdesc and merge.log settings, run various Git commands to extract appropriate strings based on those settings).

Or is there a more idiomatic way to write the script?

Well, yes if you wanted to submit it as a contributed script to Git: then you would want to use git-sh-setup and a lot of lower level plumbing commands directly, and insert considerably more error checking (e.g., what should the script do when $1 cannot be git rev-parsed correctly? what should it do in a completely empty repository where even HEAD is invalid?). But that would make the script a lot more verbose—not quite a ~2000 line monster like the rebase code:

$ wc -l git-rebase*sh
     103 git-rebase--am.sh
    1042 git-rebase--interactive.sh
     169 git-rebase--merge.sh
     648 git-rebase.sh
    1962 total

but still a lot longer than it is now.

Besides that: In general, Git commands that do this sort of thing don't set up a backup branch name at all. Instead, they rely on Git's reflogs for the branch names, and the ORIG_HEAD name that retains the immediately-previous setting. If you were to omit the backup branch and use git reset --soft && git commit -F $msg_temp_file, you would get this same behavior—but once again you would have to construct the merge message in a temporary file. Or, since both git reset and git merge --squash set ORIG_HEAD, if you were to use git merge --squash to create the new merge, you would have to use git update-ref to set ORIG_HEAD back to the correct value.

Both would change the observable behavior significantly, of course.

Not the answer you're looking for? Browse other questions tagged or ask your own question.