git-town icon indicating copy to clipboard operation
git-town copied to clipboard

stack refactoring

Open kevgo opened this issue 1 year ago • 1 comments

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 merge sync-strategy need to compress their 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, and last commands 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 
    
  • [ ] git town split - split the current branch into two stacked branches

    example Given this stack
    main
     \
      branch-1
    

    And branch-1 contains multiple commits When I run git town refactor split branch-1 branch-2 Then it asks me to select the commits to go into the new branch And I have this stack:

    main
     \
      branch-1
       \
        branch-2
    
    UI

    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 branches

    example

    Given I am on a branch with these commits:

    $ git log
    commit-1
    commit-2
    commit-3
    

    When I run git town refactor explode

    Then 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 stack

    example Given this stack:
    main
     \
      branch-1
       \
        branch-2
         \
          branch-3
    

    And I am on branch-2 When I run git town refactor merge-with-parent Then I have this stack:

    main
     \
      branch-1
       \
        branch-3
    

    And branch-1 contains the changes of branch-1 and branch-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-3
    

    And I am on any branch in this stack When I run git town refactor condense new-branch Then the stack is gone and instead I have a branch new-branch with these commits:

    $ git log
    branch-1
    branch-2
    branch-3
    

    If 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-3
    

    And I am on branch-2 When I run git town refactor detach Then 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 stack

    example Given this stack:
    main
     \
      branch-1
       \
        branch-2
         \
          branch-3
    

    And I am on branch-3 When I run git town refactor up Then I have this stack:

    main
     \
      branch-1
       \
        branch-3
         \
          branch-2
    
  • [ ] git town down - move the current branch one position down in its stack

    example

    Given this stack:

    main
     \
      branch-1
       \
        branch-2
         \
          branch-3
    

    And I am on branch-1 When I run git town refactor down Then I have this stack:

    main
     \
      branch-2
       \
        branch-1
         \
          branch-3
    
  • [ ] git town first - move the current branch to the beginning of the stack

    example Given this stack:
    main
     \
      branch-1
       \
        branch-2
         \
          branch-3
    

    And I am on branch-3 When I run git town refactor first Then I have this stack:

    main
     \
      branch-3
       \
        branch-1
         \
          branch-2
    
  • [ ] git town last - move the current branch to the end of the stack

    example Given this stack:
    main
     \
      branch-1
       \
        branch-2
         \
          branch-3
    

    And I am on branch-1 When I run git town refactor last Then I have this stack:

    main
     \
      branch-2
       \
        branch-3
         \
          branch-1
    

kevgo avatar Apr 13 '24 12:04 kevgo

We could also use set-parent here: https://github.com/git-town/git-town/issues/3473

kevgo avatar May 04 '24 22:05 kevgo

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!

kevgo avatar Nov 12 '24 00:11 kevgo

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.

ruudk avatar Nov 12 '24 10:11 ruudk

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.

kevgo avatar Nov 12 '24 13:11 kevgo

Yeah depending on the use case doing a split on hack makes also sense. So let's add it to prepend append and hack.

ruudk avatar Nov 12 '24 13:11 ruudk

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

kevgo avatar Nov 12 '24 13:11 kevgo

My script is called hack-pick. So maybe just --pick?

ruudk avatar Nov 12 '24 13:11 ruudk

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.

stephenwade avatar Nov 12 '24 19:11 stephenwade

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.

kevgo avatar Nov 14 '24 01:11 kevgo

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:

  1. Add a --move flag to prepend/append/hack that means "prepend/append/hack, then move commits".
  2. Add --prepend, --append, --hack flags to move-commits that mean "prepend/append/hack a new branch instead of asking me what branch to move to".
  3. Add "new branch" as an option in the branch selector when running move-commits.

stephenwade avatar Nov 14 '24 03:11 stephenwade

@stephenwade I think this makes a lot of sense indeed 👍

ruudk avatar Nov 22 '24 07:11 ruudk

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)?

levrik avatar Dec 13 '24 14:12 levrik

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!

kevgo avatar Dec 16 '24 22:12 kevgo

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-1 should contain these commits:

    commit-1
    commit-3
    
  • branch-2 should 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

  1. Does this approach work?
  2. Does it cover all edge cases?
  3. Is there a better way to achieve this?
  4. If any descendant branches exist, would they need to be rebased to remove references to commit-2?

kevgo avatar Dec 17 '24 00:12 kevgo

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.

ruudk avatar Dec 17 '24 07:12 ruudk

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)

davidolrik avatar Dec 17 '24 07:12 davidolrik

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"
  1. Does this approach work?

This works if you are moving only one commit.

  1. 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.

  1. 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.

  1. 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.

stephenwade avatar Dec 17 '24 17:12 stephenwade

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:

  1. Address the issue on feature branch
  2. git-town hack fix: Create a new branch off main in preparation to commit & PR changes. This sometimes creates merge conflicts when Git Town applies untracked/unstaged changes from the temporary stash it creates.
  3. git commit && git-town propose: Commit & propose changes. Will sometimes have to stash untracked/unstaged changes manually as git-town propose will do a sync, pulling in fresh changes from main
  4. git-town switch: Switch back to feature branch 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
  1. git-town hack fix/urgent-bug
  2. Fix issue, propose to main & merge PR
  3. git-town switch to release-v1.0
  4. git-town append hotfix/urgent-bug
  5. git log main: Grab commit hash of merged fix
  6. git cherry-pick <commit>
  7. 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 avatar Mar 10 '25 23:03 tranhl

@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:

  1. Implement the unrelated issue on your feature branch
  2. git town hack <branch name> --commit --message "my changes" --propose to commit the currently uncommitted changes into a new top-level branch and propose that branch, while never changing away from your feature branch.

kevgo avatar Mar 21 '25 11:03 kevgo

@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 avatar Mar 21 '25 11:03 kevgo

@kevgo

@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?

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!

tranhl avatar Mar 24 '25 23:03 tranhl

@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:

  1. Implement the unrelated issue on your feature branch
  2. git town hack <branch name> --commit --message "my changes" --propose to commit the currently uncommitted changes into a new top-level branch and propose that branch, while never changing away from your feature branch.

This would be amazing TBH! Everything I need in a one-liner 😄

tranhl avatar Mar 24 '25 23:03 tranhl

2. git town hack --commit --message "my changes" --propose

This would be epic 🤩

ruudk avatar Mar 25 '25 10:03 ruudk

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.

ruudk avatar Apr 04 '25 12:04 ruudk

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.

kevgo avatar Apr 04 '25 15:04 kevgo

This is now available as part of Git Town v19.0.

kevgo avatar Apr 18 '25 19:04 kevgo

Does hack now also support beam? That wasn't in the release notes. Amazing

ruudk avatar Apr 19 '25 05:04 ruudk

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".

kevgo avatar Apr 19 '25 12:04 kevgo

Ugh I complete read that as append and prepend. Sweet! Thanks again!

ruudk avatar Apr 19 '25 12:04 ruudk