Rubbing
Learn how to use the rub command to apply changes to branches.
As we saw in the Branching and Committing section, the but stage command can be used to assign changes to branch lanes.
However, we can also do this with a command called but rub. Not only can it stage changes though, it can be used to do so much more. Rubbing is essentially combining two things. Since there are lots of things in the tool, combining them together can do lots of different operations. Most of them should be fairly intuitive once you understand the concept.
Let’s take a look at what is possible with this very straightforward command.
Unassigning Changes
We already showed how you can use rub to assign a file change or set of changes to a branch for later committing (rubbing a file and a branch), but what if you want to undo that? Move assignments to a different lane or revert them to being unassigned for later?
As you may have noticed in the but status output, there is a special identifier zz which is always the “unassigned” ID. If you rub anything to zz then it will move it to unassigned.
So given this status:
╭┄zz [unstaged changes] ┊ g0 M README-es.md 🔒 da42d06 ┊ h0 A README.new.md ┊ i0 A app/views/bookmarks/index.html.erb ┊ ┊╭┄ge [gemfile-fixes] ┊● da42d06 Add Spanish README and bookmarks feature ┊● fdbd753 just the one ┊│ ┊├┄at [feature-bookmarks] ┊● 223fdd6 feat: Add bookmarks table to store user-tweet rela ├╯ ┊ ┊╭┄sc [sc-branch-26] ┊● f55a30e add bookmark model and associations ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but diff` to see uncommitted changes and `but stage <file>` to stage them to a branch
We can re-unassign the README.new.md file with but rub h0 zz. Or, we can re-assign that file to the sc-branch-26 parallel branch with but rub h0 sc-branch-26.
Amending Commits
However, branch assignment is not all we can do with rubbing. We can also use it to move things to and from commits. A common example would be to amend a commit with new work.
Let’s say that we sent commits out for review and got feedback and instead of creating new commits to address the review, we wanted to actually fix up our commits to be better. This is somewhat complicated to do in Git (something something fixup commit, autosquash, etc).
However, with rub it’s incredibly simple. Just rub the new changes into the target commit rather than a branch.
Let’s say that we have a branch with some commits in it, we’ve made changes to two files and want to amend two different commits with the new changes.
╭┄zz [unstaged changes] ┊ g0 M README-es.md 🔒 da42d06 ┊ h0 A README.new.md ┊ i0 A app/views/bookmarks/index.html.erb ┊ ┊╭┄ge [gemfile-fixes] ┊● da42d06 Add Spanish README and bookmarks feature ┊● fdbd753 just the one ┊│ ┊├┄at [feature-bookmarks] ┊● 223fdd6 feat: Add bookmarks table to store user-tweet rela ├╯ ┊ ┊╭┄sc [sc-branch-26] ┊● f55a30e add bookmark model and associations ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but diff` to see uncommitted changes and `but stage <file>` to stage them to a branch
If we want to update the first commit (da42d06) with the README-es.md changes and the last commit (fdbd753) with the app/views/bookmarks/index.html.erb changes, we can run the following two rub commands:
Amended the only hunk in README.new.md in the unassigned area → fa2544d
Amended the only hunk in app/views/bookmarks/index.html.erb in the unassigned area → 4128746
╭┄zz [unstaged changes] ┊ g0 M README-es.md 🔒 27118eb ┊ ┊╭┄ge [gemfile-fixes] ┊● 27118eb Add Spanish README and bookmarks feature ┊│ 27:0 M Gemfile ┊│ 27:1 A README-es.md ┊│ 27:2 A README.new.md ┊│ 27:3 A app/controllers/bookmarks_controller.rb ┊│ 27:4 M app/views/dashboard/index.html.erb ┊● 4128746 just the one ┊│ 41:0 M app/models/user.rb ┊│ 41:1 A app/views/bookmarks/index.html.erb ┊│ ┊├┄at [feature-bookmarks] ┊● 223fdd6 feat: Add bookmarks table to store user-tweet rela ┊│ 22:0 A app/models/bookmark.rb ┊│ 22:1 M config/routes.rb ┊│ 22:2 A spacer.txt ┊│ 22:3 A testing.md ├╯ ┊ ┊╭┄sc [sc-branch-26] ┊● f55a30e add bookmark model and associations ┊│ f5:0 M app/models/tweet.rb ┊│ f5:1 A db/migrate/20250925000001_create_bookmarks.rb ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but diff` to see uncommitted changes and `but stage <file>` to stage them to a branch
Notice that the SHAs have changed for those commits. It has rewritten the commits to have the same messages but incorporated the changes you rubbed into those patches.
Also notice that you can use either the commit SHA or file path instead of the short ID if you prefer more typing.
If you wanted to rub all the unassigned changes into a specific commit, you could also do that by rubbing the unstaged section to a commit, for example but rub zz f55a30e which would take all unstaged changes (if there were any) and amend commit f55a30e with them.
Squashing Commits
File changes are not the only thing that you can rub. You can also rub commits into things. To squash two commits together, you simply rub them together. Let’s look at a simple example of a branch with two commits on it. We'll squash them into one:
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄sq [squash-example] ┊● 0fa2965 the second commit ┊● 080a970 the original commit ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
We can absorb the top commit into the bottom one by running but rub <commit-squash> <commit-target>:
Squashed 0fa2965 → 9f1384c
Now we can see that we only have one commit in our branch:
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄sq [squash-example] ┊● 9f1384c the original commit ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
You probably want to edit the commit message after this too, since it will simply combine the two commit messages.
Uncommitting
Let’s say that we want to just undo a commit - that is, pretend that we had not made that commit and instead put the changes back to unassigned status. In this case we would use the special 00 ID that we talked about earlier, just like unassigning changes, we can unassign commits.
So, if we’re back to this status:
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄sq [squash-example] ┊● 0fa2965 the second commit ┊● 080a970 the original commit ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
And we want to un-commit the first commit (0fa2965) as though we had never made it, you can rub to zz:
Uncommitted 0fa2965
Now if we look at our status again, we will see that commit removed and those files back in the unassigned status:
╭┄zz [unstaged changes] ┊ g0 M app/models/user.rb ┊ h0 A app/views/bookmarks/index.html.erb ┊ i0 M app/views/dashboard/index.html.erb ┊ j0 M config/routes.rb ┊ k0 A spacer.txt ┊ l0 A testing.md ┊ ┊╭┄sq [squash-example] ┊● 080a970 the original commit ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but diff` to see uncommitted changes and `but stage <file>` to stage them to a branch
Moving Commits
We can also use rubbing to move a commit from one branch to another branch if we have multiple active branches and committed to the wrong one, or otherwise decide that we want to split up independent work.
Let’s say that we have two commits on one branch and created a second parallel branch to move one of the commits to so it's not dependent.
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄sq [squash-example] ┊● 0fa2965 the second commit ┊● 080a970 the original commit ├╯ ┊ ┊╭┄mo [move-second-commit] (no commits) ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
We can move the “second commit” commit to the move-second-commit branch with but rub:
Moved 0fa2965 → [move-second-commit]
Now we can see that the commit has been moved to the move-second-commit branch, breaking up the series into two independent branches with one commit each.
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄sq [squash-example] ┊● 080a970 the original commit ├╯ ┊ ┊╭┄mo [move-second-commit] ┊● 5be95a8 the second commit ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
Notice that the only SHA that changed was the one that moved, since nothing else needed to be rebased. Rubbing a commit to another branch always adds it to the top of that branch.
As you might imagine, you can also simultaneously move and squash by rubbing a commit in one branch on a commit in another branch too.
Moving Files between Commits
You can also move specific file changes from one commit to another.
To do that, you need identifiers for the files and hunks in an existing commit, which you can get via a but status -f, or but status --files that tells status to also list commit file IDs.
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄sq [squash-example] ┊● 080a970 the original commit ┊│ 08:0 M Gemfile ┊│ 08:1 A README-es.md ┊│ 08:2 A README.new.md ┊│ 08:3 A app/controllers/bookmarks_controller.rb ┊│ 08:4 A app/models/bookmark.rb ├╯ ┊ ┊╭┄mo [move-second-commit] ┊● 2ef4df5 the second commit ┊│ 2e:0 M app/models/user.rb ┊│ 2e:1 A app/views/bookmarks/index.html.erb ┊│ 2e:2 M app/views/dashboard/index.html.erb ┊│ 2e:3 M config/routes.rb ┊│ 2e:4 A spacer.txt ┊│ 2e:5 A testing.md ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
So now we can move the changes from one commit to another by rubbing pretty easily. Let’s take the app/controllers/bookmarks_controller.rb change and move it down to the "second commit" commit on the other branch:
Moved files between commits!
Now the change is in the "second commit" on the other branch:
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄sq [squash-example] ┊● 95b1d19 the original commit ┊│ 95:0 M Gemfile ┊│ 95:1 A README-es.md ┊│ 95:2 A README.new.md ┊│ 95:3 A app/models/bookmark.rb ├╯ ┊ ┊╭┄mo [move-second-commit] ┊● 6ba5abc the second commit ┊│ 6b:0 A app/controllers/bookmarks_controller.rb ┊│ 6b:1 M app/models/user.rb ┊│ 6b:2 A app/views/bookmarks/index.html.erb ┊│ 6b:3 M app/views/dashboard/index.html.erb ┊│ 6b:4 M config/routes.rb ┊│ 6b:5 A spacer.txt ┊│ 6b:6 A testing.md ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
Also notice that the SHAs of both commits were changed, as they both needed to have content modified.
Splitting Commits
Ok, so now we can be pretty specifc about moving changes around to all these different states. The last thing we’ll cover here is splitting commits, which requires a new command that creates a new empty commit called but commit empty.
The general strategy here is that to split a commit, you would make a new empty commit above or below it, then rub changes from the one commit into the empty commit until it's how you want it to look, then you're done.
Let’s say we have a branch with a single commit on it and want to split it into two commits.
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄us [user-bookmarks] ┊● 6a62ad5 all of the changes ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
Now we want to split the "add bookmark model and associations" into two separate commits. The way we do this is to insert a blank commit in between 06 and f5 and then rub changes into it (then probably edit the commit message).
We can insert a blank commit by running but commit empty --after 6a which inserts a blank commit above the specified commit.
Created blank commit after commit 6a62ad5
Now we have a blank commit:
╭┄zz [unstaged changes] ┊ no changes ┊ ┊╭┄us [user-bookmarks] ┊● 19fd384 (no commit message) (no changes) ┊● 6a62ad5 all of the changes ┊│ 6a:0 M Gemfile ┊│ 6a:1 A app/controllers/bookmarks_controller.rb ┊│ 6a:2 A app/models/bookmark.rb ┊│ 6a:3 M app/models/user.rb ┊│ 6a:4 A app/views/bookmarks/index.html.erb ┊│ 6a:5 M config/routes.rb ├╯ ┊ ┊● 32a2175 (upstream) ⏫ 2 new commits ├╯ 204e309 (common base) [origin/main] 2025-07-06 Merge pull request #10 from schacon/sc-description Hint: run `but help` for all commands
Now we can use the previous method of moving file changes from other commits into it, then edit the commit message with but reword 54 (for more on the reword command, see Editing Commits, coming up next).
Last updated on