GitButler ⧓

GuidesCLI Tutorial

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:

$ but status
╭┄zz [unstaged changes] 
┊   g0 M README-es.md 🔒 da42d06h0 A README.new.mdi0 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.

$ but status
╭┄zz [unstaged changes] 
┊   g0 M README-es.md 🔒 da42d06h0 A README.new.mdi0 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:

$ but rub h0 da42d06
Amended the only hunk in README.new.md in the unassigned area → fa2544d
$ but rub app/views/bookmarks/index.html.erb fdbd753
Amended the only hunk in app/views/bookmarks/index.html.erb in the unassigned area → 4128746
$ but status --files
╭┄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:

$ but 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

We can absorb the top commit into the bottom one by running but rub <commit-squash> <commit-target>:

$ but rub 0f 08
Squashed 0fa29659f1384c

Now we can see that we only have one commit in our branch:

$ but status
╭┄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:

$ but 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:

$ but rub 0f 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:

$ but status
╭┄zz [unstaged changes] 
┊   g0 M app/models/user.rbh0 A app/views/bookmarks/index.html.erbi0 M app/views/dashboard/index.html.erbj0 M config/routes.rbk0 A spacer.txtl0 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.

$ but status
╭┄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:

$ but rub 0f mo
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.

$ but status
╭┄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.

$ but status -f
╭┄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:

$ but rub 08:3 2e
Moved files between commits!

Now the change is in the "second commit" on the other branch:

$ but status -f
╭┄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.

$ but status
╭┄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.

$ but commit empty --after 6a
Created blank commit after commit 6a62ad5

Now we have a blank commit:

$ but status -f
╭┄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

On this page

Edit on GitHubGive us feedback