Skip to content

Commit

Permalink
Merge pull request #7 from Incognito/0.2
Browse files Browse the repository at this point in the history
Simplified and documented
  • Loading branch information
Incognito committed Jan 22, 2022
2 parents dd38c5e + 7db2ead commit 49b725c
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 87 deletions.
89 changes: 61 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,66 @@
# What is this

A tool to help you when you accidentally wrote too much code in too many places
in a feature branch. This tool lets you preserve history and break the code
down by folders into individual branches. This is really useful for people who
work on decoupled component architectures.

It seems it is a frequent pattern for engineers to get so focused on making
changes they often need a way to break it up and release it in smaller chunks
to make reviews easier for their peers.

# Usage

```
git exfiltrator <against-ref> <subject-ref> <pathspec>
1. Be sure you have the branch that needs to be broken up checked out
2. Be sure you are in the project root
3. Ensure that your branch was not already merged into your project's primary branch (if those changes were already applied, there is nothing to split)

- <against> defines the target to merge towards.
- <subject> defines the subject branch that files will be extracted from
- <pathspec> same as in `git add`, except only able to add files that exist
between the <subject> and the <against> target found in each commit.
```

# Known Uses
- Breaking up a large branch or Pull Request into many smaller ones.
- Releasing (or moving) code that is "ready" without waiting for code that is
"not ready"
- Wanting to remove some feature or migrate development to another branch
without losing history.
Usage: git-exfiltrator[-h] [-b] against-ref subject-ref pathspec
<against> The branch you will merge changes into
<new-branch> The branch you want to create
<pathspec> the path you wish to split ("some/path/*")
<base> (optional) The tool will attempt to auto-detect the common ancestor
between your branch and the against target. If your branch histories
are complicated you can manually provide the original ancestor commit.
Break a big feature branch into a smaller specific branch with the changes from
one specific folder. Also, preserve your commit history.
```

# Benefits
- A late stage fix to development processes which generate massive PRs instead
of many small PRs.
- A late stage fix to development processes (or mindset) which generate massive
branches instead of many small ones.
- Keeping history of work migrated to a new branch.
- Avoid using primitive cut+paste techniques.
- Avoid using primitive cut+paste techniques from one branch to another which
remove history and are error prone.

# Known Drawbacks
- Commit messages will be duplicated into the extracted branch.
- Operator should know how a git-tree works.
- The user should know how a git-tree works (or have access to someone who
does) if there are serious complications.
- Possibility of introducing multiple root ancestors (unknown if there are practical drawbacks).

# Installation
You can install the script using this repo to keep things versioned correctly,
or you can try and do it the hack way.

You can install the script using this repo to keep things versioned correctly, or you can try and do it the hack way.
## Hacky way:
Cut+paste the script to your home folder and run the bash script whenever you
want to use it.

Hacky way: Cut+paste the script to your home folder and run the bash script whenever you want to use it.
## Supported way:
1. Clone the repo to your home folder.
2. Make your `$PATH` envvar able to resolve this script. Once you do, git will
understand `git exfiltrate` (without the `-`) anywhere you want to use it.
3. Then add this repo to your watchlist/subscriptions on GitHub so you know
when/if I provide any updates or useful improvements, and you can git-pull
the update as needed.

Supported way: Clone the repo to your home folder. Make your `$PATH` envvar able to resolve this script. Once you do, git will understand `git exfiltrate` (without the `-`) anywhere you want to use it. Then add this repo to your watchlist/subscriptions on GitHub so you know when/if I provide any updates or useful improvements.
(If someone would like to maintain a `.deb` or `brew` dependency I'd be happy
to link to it.)

(If someone would like to maintain a `.deb` or `brew` dependency I'd be happy to link to it.)

# Example

[![asciicast](https://asciinema.org/a/OwPrRxKT0IHauAdgXZ5p74naT.svg)](https://asciinema.org/a/OwPrRxKT0IHauAdgXZ5p74naT)
# Example usage

Imagine you're faced with a large `feature-branch` and it's making too many
changes to too many parts of the code (See "uses" below). Consider this trivial
Expand All @@ -65,13 +87,15 @@ maintain a logical commit history.
a/1 | 1 +
```

Run git exfiltrator to extract the `b` folder into a new "extract" branch:
Run git exfiltrate to extract the `b` folder into a new "extract" branch:

```
./git-exfiltrate master feature-branch "b/*"
git checkout feature-branch
./git-exfiltrate master feature-branch-extracted "b/*"
```

The `-extracted` branch will be merged into the `subject` argument provided.
The `-extracted` branch will be created with just the contents of `b` from the branch.

Note that the extracted branch has an unrelated history from the original
`feature-branch`. This means master has all of folder `b`, and a complete
timeline of changes related to `b`, but none of the work from `a` or `c` is
Expand Down Expand Up @@ -123,3 +147,12 @@ branch.

I hope one day our entire industry (not just some pockets of it) will fully
embrace and be capable of working only in small incremental changes.

There was a previous "Stateless" version that let you operate on bare repos but
it proved too unintuitive in practice for a wide audience, so this new version
requires that you are checked out on the right branch and in the root folder.

And yes, the heart of this program is a 1-liner but the real value here is
documenting how to do that one liner and making it dead-simple to an audience
who typically not want to figure it out while needing to deliver more code
quickly.
61 changes: 48 additions & 13 deletions git-exfiltrate
Original file line number Diff line number Diff line change
@@ -1,18 +1,53 @@
#!/bin/sh
#!/usr/bin/env bash

destinationRef=$1
originRef=$2
pathspec=$3
test -z "$destinationRef" && echo "ERROR: Please provide the destination reference." 1>&2 && exit 1
test -z "$originRef" && echo "ERROR: Please provide the origin reference." 1>&2 && exit 1
test -z "$pathspec" && echo "ERROR: Please provide the paths to be extracted." 1>&2 && exit 1
set -Eeuo pipefail

git checkout "$originRef"
usage() {
cat <<EOF
Usage: $(basename "${BASH_SOURCE[0]}") [-h] [-b] <against> <new-branch> <pathspec> (<base>)
<against> The branch you will merge changes into
<new-branch> The branch you want to create
<pathspec> the path you wish to split ("some/path/*")
<base> (optional) The tool will attempt to auto-detect the common ancestor
between your branch and the against target. If your branch histories
are complicated you can manually provide the original ancestor commit.
git checkout -b "$originRef-extracted"
Break a big feature branch into a smaller specific branch with the changes from
one specific folder. Also, preserve your commit history.
git filter-branch --force --prune-empty --index-filter \
"git ls-files | grep -Ev $pathspec | xargs git rm --cached" "$(git merge-base "$destinationRef" "$originRef")..HEAD"
EOF
exit
}

git checkout "$destinationRef"
git merge "$originRef-extracted" --no-edit --allow-unrelated-histories
AGAINST=${1:-}
NEW_BRANCH=${2:-}
PATHSPEC=${3:-}
if [ "$#" -lt 3 ]; then
usage
fi

BASE=${4:-$(git merge-base "$AGAINST" HEAD)}

git checkout -b "$NEW_BRANCH" || exit

FILTER_BRANCH_SQUELCH_WARNING=1 git filter-branch --force --prune-empty --index-filter \
"git ls-files | grep -Ev $PATHSPEC | xargs git rm --cached" "$BASE..HEAD"


cat <<EOF
--------------------------------------------------------------------------------
Your new branch is created and checked out.
You should be able to merge the branch as you normally would. In some
situations git may not permit merges due to the "history" being "unrelated"
(for example, if you introduce a new folder in your branch it has no common
ancestor in the branch you wish to merge into. In this situation you must tell
git to merge a branch with unrelated history: the unrelated history:
git checkout $AGAINST
git merge $NEW_BRANCH --no-edit --allow-unrelated-histories
A regular git merge is usually all you need. You only need to do this if it
complains.
--------------------------------------------------------------------------------
EOF
46 changes: 0 additions & 46 deletions test_repo.sh

This file was deleted.

0 comments on commit 49b725c

Please sign in to comment.