git-town
git-town copied to clipboard
stack refactoring
motivation
Not sure if this is something people do a lot or even should should do a lot. But if they do it, they should do it using Git Town. Because Git Town
- provides a workflow engine to do these complex operations step-by-step, with redoing steps after problems
- updates associated proposals
- provides safe undo in case of unexpected issues (which are likely with this type of operation)
how it works
- all branches to refactor must be in sync
- the changes made here are complex enough, let's not make them more complex by also syncing
- all branches to refactor must have simple commits
- no merge commits
- people who use the
mergesync-strategy need tocompresstheir branch first
- update associated proposals
Branch commands
Some of these commands could be top-level commands, but they aren't used often enough to justify being a top-level command.
-
[ ]
git town move- moves the current branch to a different position in the lineage- does what the
up,down,first, andlastcommands do, and more (moving to a different stack) - That would be useful because it's debatable what "up" and "down" mean in a stack. Arguments can be made either way depending whether the main branch is at the top or bottom.
- Using "up" and "down" repeatedly becomes slow because it always has to update the proposals as well.
UI
The UI is similar to
git town switch, but moves the highlighted branch up and down.main branch-1 * branch-2 branch-3 - does what the
-
[ ]
git town split- split the current branch into two stacked branchesexample
Given this stackmain \ branch-1And
branch-1contains multiple commits When I rungit town refactor split branch-1 branch-2Then it asks me to select the commits to go into the new branch And I have this stack:main \ branch-1 \ branch-2UI
The user can move the split here line up and down using cursor keys.
=== parent branch name === commit 1 commit 2 ----- split here ----- commit 3 commit 4 === child branch name === -
[ ]
git town split --all- split all commits in the current branch into dedicated branchesexample
Given I am on a branch with these commits:
$ git log commit-1 commit-2 commit-3When I run
git town refactor explodeThen this branch is gone and instead I have this stack:
main \ commit-1 \ commit-2 \ commit-3 -
[ ]
git town merge- merge two consecutive branches in a stackexample
Given this stack:main \ branch-1 \ branch-2 \ branch-3And I am on
branch-2When I rungit town refactor merge-with-parentThen I have this stack:main \ branch-1 \ branch-3And
branch-1contains the changes ofbranch-1andbranch-2. -
[ ]
git town merge --stack- convert a stack into a single branch with multiple commits- all child branches must have only one child branch
example
Given this stack:
main \ branch-1 \ branch-2 \ branch-3And I am on any branch in this stack When I run
git town refactor condense new-branchThen the stack is gone and instead I have a branchnew-branchwith these commits:$ git log branch-1 branch-2 branch-3If the branches contain only one commit, then the commit messages in the new branch are the commit messages of the commits in the original branches. If the branches contain multiple commits, the commit messages are the commit messages of all commits from the branch concatenated.
-
[ ]
git town detach- detach the current branch from its stack (make it an independent top-level branch)example
Given this stack:
main \ branch-1 \ branch-2 \ branch-3And I am on
branch-2When I rungit town refactor detachThen I have this stack:main \ branch-1 \ branch-3 \ branch-2
Archive (old ideas)
-
[ ]
git town branch up- move the current branch one position up in its stackexample
Given this stack:main \ branch-1 \ branch-2 \ branch-3And I am on
branch-3When I rungit town refactor upThen I have this stack:main \ branch-1 \ branch-3 \ branch-2 -
[ ]
git town down- move the current branch one position down in its stackexample
Given this stack:
main \ branch-1 \ branch-2 \ branch-3And I am on
branch-1When I rungit town refactor downThen I have this stack:main \ branch-2 \ branch-1 \ branch-3 -
[ ]
git town first- move the current branch to the beginning of the stackexample
Given this stack:main \ branch-1 \ branch-2 \ branch-3And I am on
branch-3When I rungit town refactor firstThen I have this stack:main \ branch-3 \ branch-1 \ branch-2 -
[ ]
git town last- move the current branch to the end of the stackexample
Given this stack:main \ branch-1 \ branch-2 \ branch-3And I am on
branch-1When I rungit town refactor lastThen I have this stack:main \ branch-2 \ branch-3 \ branch-1
We could also use set-parent here: https://github.com/git-town/git-town/issues/3473
Decision needed on the behavior of the git town split command.
Consider this stack:
main
\
branch-1
Currently, branch-1 is active, and I run git town split branch-2. The question is: how should the new branch hierarchy look like?
Option A:
main
\
branch-1
\
branch-2
This structure supports a scenario where I’m working on branch-1 but realize that the last few commits would be better isolated in a separate branch. I then "extract" those commits into branch-2.
Option B:
main
\
branch-2
\
branch-1
This approach could be useful when splitting branch-1 and inserting a new branch (branch-2) to contain initial changes.
@ruudk you proposed this feature - which of these use cases did you have in mind?
@stephenwade curious about your take on this as well. Thanks!
Reading my original issue https://github.com/git-town/git-town/issues/3667 and thinking about my day to day, I often start hacking on a feature, and fix things along the way (scouts rule). These fixes are always done in separate commits, and are often independent of the feature. In that case I want to split the branch and move some of the commits to a parent (option B).
Instead of creating a new split command, why not add a --split option to git town prepend and git town append? In that way, you can do option A + B, and decide per use case how you want it. Because prepend/append is basically already what this is, the only difference is to move/split commits.
That's a pretty brilliant idea, thanks for sharing! :pray: I love that this allows us to implement this without introducing a new top-level command, as Git Town has a lot of top-level commands already.
We could also add this --split option to git town hack. I (personally) prefer implementing unrelated cleanups in independent top-level feature branches rather than as a parent branch of a larger change. Makes proposing, reviewing, shipping, and/or discarding the change easier.
Yeah depending on the use case doing a split on hack makes also sense. So let's add it to prepend append and hack.
Regarding the naming, we're no longer "splitting" a branch mid-way; instead, we're selectively cherry-picking specific commits into the new branch. Let's find a better name for this workflow. Some ideas:
--cherry-pick--move-commits--move
My script is called hack-pick. So maybe just --pick?
In my opinion:
For prepend/append, we shouldn't call it --pick/--cherry-pick because it should not cherry-pick, just update refs.
For hack, we shouldn't call it --pick/--cherry-pick because that implies cherry-picking but not removing the commits from another branch.
I suggest --move/--move-commits for hack and --split for prepend/append, since they are really different operations: in hack, you're cherry-picking commits which you don't need to do in prepend or append.
Another decision we have to make, which might also help with naming the CLI option: we now have two alternative approaches for selecting commits to move to the new branch.
Approach A (the initial concept outlined earlier in this ticket): Split the branch at a specified commit, then move either all commits before or after this split point into the new branch.
Approach B (suggested by @ruudk in #3667): Allow users to select individual commits to move into the new branch using checkboxes, enabling non-sequential commits to be moved.
Approach A is much easier to implement and will cause fewer merge conflicts along the way. Approach B seems more flexible.
When using approach A, it would make sense to name the CLI option --split. When using approach B, --move or --pick makes more sense.
There is also the option of implementing commit-moving as a standalone command, separate from creating new branches. This approach seems the most flexible, as it also allows moving commits between existing branches without creating any new branches. It comes at the cost of having to run two separate Git Town commands when extracting commits into a new branch.
Approach A is much easier to implement and will cause fewer merge conflicts along the way. Approach B seems more flexible.
I think it would be nice to have both approaches. Sometimes I just want to split up what I have but keep them in a stack; sometimes I want to move a few commits out of this stack into an isolated branch.
Also, after thinking about it some more, I think these should both be their own commands.
Splitting a branch: When I realize that a PR is too big, I want to split it up. prepend and append are for adding new branches, so they aren't going to come to mind when I think of what action to take. I would expect a split command.
Having a separate command could also allow for a more flexible workflow, like picking multiple split points to create more than two branches or renaming both branches (e.g. start with branch feature and end with branches feature-1 and feature-2). This is how Graphite's split --by-commit feature worked and I miss it.
Graphite split command documentation: https://graphite.dev/docs/squash-fold-split#split-a-branch-into-multiple-branches
Moving commits: This should be its own command to allow moving commits between existing branches. I've definitely been in the situation of creating a commit for feature A on feature B's existing branch.
It comes at the cost of having to run two separate Git Town commands when extracting commits into a new branch.
It would be nice to have one command to create branch + move commits, because that's probably the most common case for moving commits. Here are some convenience shortcut ideas:
- Add a
--moveflag to prepend/append/hack that means "prepend/append/hack, then move commits". - Add
--prepend,--append,--hackflags to move-commits that mean "prepend/append/hack a new branch instead of asking me what branch to move to". - Add "new branch" as an option in the branch selector when running move-commits.
@stephenwade I think this makes a lot of sense indeed 👍
git town split is basically what I need for Git Town to be useful to me. I tend to do many things at once but I usually separate them into commits already before pushing.
Currently I manually turn each commit into a separate branch and PR. spr is a tool which automates exactly this workflow but it also automates the PR creation on GitHub which makes it difficult to later overwrite the PR description with screenshots and other things that can't be part of the commit message beforehand. I also had some other issues with it and dislike not having the individual branches also locally.
Git Town looks much better overall but lacks support for splitting a branch apart later in the process. I know this is open source but still I would like to ask if there are any plans on when this feature will be implemented (a few weeks, months, years away)?
Thanks, everyone, for your thoughtful input—this discussion has been incredibly insightful! Here’s a quick recap of where we stand based on the feedback so far:
Splitting a branch: not practical; extracting commits: promising
The idea of splitting a branch initially sounded intriguing but seems to address only a narrow set of use cases mentioned in this thread. Given its limited applicability and the additional complexity it would introduce, we’re shelving this concept for now.
Conversely, enabling users to view all commits in a branch and select specific ones (via checkboxes) to move to another branch—either new or existing—addresses all of the discussed scenarios.
Extracting commits into a new branch: highly requested
A key takeaway is the widespread need for extracting specific commits from an existing branch into a new one. The most natural fit for Git Town would be to introduce a --move flag to the existing hack, prepend, and append commands, which already create new branches.
Moving commits to an existing branch: fills a gap
A dedicated git town move command could be useful when someone accidentally commits to the wrong branch. Handling this with vanilla Git commands is notoriously cumbersome, so adding a streamlined solution could fill a gap here.
Let me know if I’ve missed anything or if there’s more we should consider!
Feedback requested: It would be great if somebody who is already manually moving commits between branches shares how they do it. What’s the sequence of Git commands to move a single commit to another branch and remove it from its original branch?
Context
Consider my workspace contains branches branch-1 and branch-2, and branch-1 contains these commits:
commit-1
commit-2
commit-3
I run git town hack --move branch-2 and select the following commit to move:
[ ] commit-1
[x] commit-2
[ ] commit-3
Desired end state
-
branch-1should contain these commits:commit-1 commit-3 -
branch-2should contain this commit:commit-2
possible approaches
ChatGPT suggests the following Git commands:
SHA=<SHA of commit-2>
git checkout branch-2
git cherry-pick $SHA
git checkout branch-1
git rebase --onto "$SHA^" "$SHA"
Open questions
- Does this approach work?
- Does it cover all edge cases?
- Is there a better way to achieve this?
- If any descendant branches exist, would they need to be rebased to remove references to commit-2?
I use cherry-pick (see https://github.com/git-town/git-town/issues/3667#issuecomment-2222804895).
As you can see, if you have the SHA's of the commits, you don't need to checkout to the old branch anymore.
Move down:
- Append a new branch
- Cherry-pick commits to new branch
- Remove commits from source branch
- Rebase (sync stack)
Move up:
- Prepend a new branch
- Cherry-pick commits to new branch
- Rebase to make commits disappear from source (sync stack)
Feedback requested: It would be great if somebody who is already manually moving commits between branches shares how they do it. What’s the sequence of Git commands to move a single commit to another branch and remove it from its original branch?
I use cherry-pick to move commits, then interactive rebase to remove the commits from the previous branch.
ChatGPT suggests the following Git commands:
SHA=<SHA of commit-2> git checkout branch-2 git cherry-pick $SHA git checkout branch-1 git rebase --onto "$SHA^" "$SHA"
- Does this approach work?
This works if you are moving only one commit.
- Does it cover all edge cases?
It would require multiple rebases if you are moving multiple commits, and I think that might mean multiple resolutions of the same conflict.
- Is there a better way to achieve this?
I think a better approach would be to cherry-pick any commits after the first moved one. This would replicate a single interactive rebase, and only require resolving any conflicts once.
- If any descendant branches exist, would they need to be rebased to remove references to commit-2?
Yes, any time a rebase happens any descendant branches need to be rebased so they have the new branch tip in their ancestry.
Feedback requested: It would be great if somebody who is already manually moving commits between branches shares how they do it.
@kevgo Hope I'm not too late to the party! I have two use cases that frequently come up at the moment that I think Git Town could help automate:
- Making small changes & PRs for work unrelated to my current branch
- Cherry-picking & creating PRs for hotfixes
I'll run you through each use case, the workflow that I use to currently handle them, and how I think Git Town could help.
Making small changes & PRs for work unrelated to my current branch
Let's say I'm working on a feature and my local stack looks like this:
main
\
feature
What happens a lot of the time is that a small issue comes up that is unrelated to the current work I'm doing. The current process I use looks like this:
- Address the issue on
featurebranch git-town hack fix: Create a new branch offmainin preparation to commit & PR changes. This sometimes creates merge conflicts when Git Town applies untracked/unstaged changes from the temporary stash it creates.git commit && git-town propose: Commit & propose changes. Will sometimes have to stash untracked/unstaged changes manually asgit-town proposewill do a sync, pulling in fresh changes frommaingit-town switch: Switch back tofeaturebranch to continue work
This is pretty cumbersome & leaves a lot to be desired. I'm really excited for git town hack --beam for this use case as on paper it would do exactly what I want! I'd just need to commit changes I want to beam onto another branch before running the command. Hopefully untracked/unstaged changes won't get in the way of this as well!
Cherry-picking & creating PRs for hotfixes
My team and I do our development on the main branch and cut perennial branches for each release (e.g. release-v1.0) once we're ready to ship. Hotfixes for us are commits that are cherry picked from main onto the latest release branch. The process of creating a hotfix usually looks something like this:
main
\
fix/urgent-bug
\
release-v1.0
\
hotfix/urgent-bug
git-town hack fix/urgent-bug- Fix issue, propose to
main& merge PR git-town switchtorelease-v1.0git-town append hotfix/urgent-buggit log main: Grab commit hash of merged fixgit cherry-pick <commit>git-town propose
This is a lot of manual steps/commands for shipping hotfixes. I'm not sure if this would be in the scope of stack refactoring, but it would be awesome if Git Town could help out with steps 3-6. I don't think the --beam flag would be appropriate here, as we're not moving commits from main to hotfix/urgent-bug, we're cherry-picking them over instead. Perhaps we could introduce a --copy flag to handle scenarios like this where we want to keep changes on the source branch?
@tranhl in Git Town the party never stops!
Your first use case might also benefit from https://github.com/git-town/git-town/issues/4376. With that in place, your workflow would look like this:
- Implement the unrelated issue on your
featurebranch git town hack <branch name> --commit --message "my changes" --proposeto commit the currently uncommitted changes into a new top-level branch and propose that branch, while never changing away from yourfeaturebranch.
@tranhl I’m having trouble understanding the purpose of steps 4, 6, and 7 in your second example. Since anything merged into main must be reviewed, I assume the hotfix was already reviewed in step 2. If that’s the case, why not skip directly to step 5 and cherry-pick the hotfix commit into release-v1.0? The only remaining concern would be ensuring the hotfix doesn’t break the release branch, which CI should handle. Am I overlooking something?
@kevgo
@tranhl I’m having trouble understanding the purpose of steps 4, 6, and 7 in your second example. Since anything merged into
mainmust be reviewed, I assume the hotfix was already reviewed in step 2. If that’s the case, why not skip directly to step 5 and cherry-pick the hotfix commit intorelease-v1.0? The only remaining concern would be ensuring the hotfix doesn’t break the release branch, which CI should handle. Am I overlooking something?
Not at all, you're on the right track! We have a rule that restricts direct pushes to release branches, meaning hotfixes can only be shipped via PR. We also prefer to cherry pick the fix made on main to carry over Git metadata when shipping the hotfix (with the help of rebase merges). Hope that clarifies things!
@kevgo
@tranhl in Git Town the party never stops!
Your first use case might also benefit from #4376. With that in place, your workflow would look like this:
- Implement the unrelated issue on your
featurebranchgit town hack <branch name> --commit --message "my changes" --proposeto commit the currently uncommitted changes into a new top-level branch and propose that branch, while never changing away from yourfeaturebranch.
This would be amazing TBH! Everything I need in a one-liner 😄
2. git town hack
--commit --message "my changes" --propose
This would be epic 🤩
I'm hacking in a feature branch, and made a few commits. Then I realize I want to extract a single commit and ship that separately. I would really expect a git town hack --beam to exist, but it does not. Just wanted to say this, because I feel it's counterintuitive.
Thanks for the feedback! One of the challenges with implementing git town hack --beam has been removing the beamed commit from the original branch in a way that's automation-friendly. Most folks handle this manually via an interactive rebase, which doesn't play well with tools like Git Town.
Your comment prompted me to dig a bit deeper, and I came across this Stack Overflow answer, which suggests using git rebase -p --onto SHA^ SHA. I'm going to give that approach a shot and see if it works.
This is now available as part of Git Town v19.0.
Does hack now also support beam? That wasn't in the release notes. Amazing
Yes it does! It's in the release notes ... the first item in the "New Features" section. Look for "git town append and git town hack now also have a --beam flag".
Ugh I complete read that as append and prepend. Sweet! Thanks again!