Introduction
What is it?
Git Patch Stack is really two things; a conceptual model and a tool. The conceptual model is the high level mental model used to think in terms of managing your code changes as a stack of patches rather than a series of isolated feature branches. The tool is a command line interface to aid in streamlining the process of working within this mental model.
Git Patch Stack is no different than any other tool built around a conceptual model. It provides natural benefits as side effects of their innate characteristics. However to truly unlock it's full potential it needs to be paired with a methodology that is aligned not only with it's innate characteristics but also the foundational principles.
Why use it?
The conceptual model helps you think in terms of the stack of patches instead of a series of isolated branches. The tool makes it easy for you to operate within this model. This affords you the following benefits.
- don't think about branches anymore
- never blocked by waiting on peer reviews
This might be a good start and yes it is probably better than what you were doing before. But, to really make huge differences at an application architecture level and a software maintainability level you really need to adopt the methodology which will unlock even more benefits like the following.
- develop & ship software faster
- aligns with best practices for producing more maintainable software
- less mental overhead & complexity
- smaller, logical code changes, scoped to the application architecture
- more valuable peer reviews
- makes conflict resolution easier
- facilitates continuous integration while still support pre-commit code review
The Concept
The idea of Git Patch Stack at a high level is simply that instead of managing code changes as isolated feature branches managing them as a stack of logical patches. This extends out through the peer review process as well.
Probably the biggest difference in thinking is that we have to stop thinking in terms of branches or feature branches and start thinking in terms of a stack of logical code changes, a.k.a. patches.
No Branches
The primary goal of Git Patch Stack is to eliminate the management of branches or even having to think about the concept of branches. This is the main goal because there are a lot of downsides to branching. For example feature branches make valuable peer review very difficult if not impossible due to the amount of scope they include.
Feature branches also go against software maintainability & tooling best practices due to being scoped at the product level rather than the software architecture level. Beyond that they are designed strictly to isolate code changes away from each other. We have known for a long time now that delaying integration of software changes not only makes the integration process more difficult later on, but also prevents knowledge sharing and the natural evolution of an application architecture. For more details on this and where the concept of Git Patch Stack came from please see, Journey to Small Pull Requests.
This isn't to say that branches are a horrible thing that should never be used. However, it is to say that we should be aware of the pros & cons of all our tools and processes, so we can use the ideal tools & processes for the job at hand. Git Patch Stack does not prevent the use of branches, it simply facilitates using a more ideal tool and process for the 99.99% of time where you don't need or want to have to use branches.
Mental Model
In Git Patch Stack instead of branches & commits we focus on a stack of patches.
A patch is simply a logical change to source code. From a technical standpoint a patch is simply a Git commit that exists on your patch stack. There are ideal characteristics that patches should strive to have. For example, being small, logical, build-able, testable, releasable, and having a good message. We will get into the specifics of these characteristics later and tools and techniques to help facilitate them.
A patch stack is what it sounds like, a stack of patches. Think of it like
a stack of papers. You can add new patches to the top of the stack, reorder the
patches in the stack, squash patches together, split a patch apart into
multiple patches or even drop patches you no longer want. These operations are
extremely useful for evolving patches. From a technical standpoint a patch
stack is the Git commit history on a remote tracking branch between the local
branches HEAD and the tracked remote branches HEAD. You can have as many
patch stacks as you like, however generally you work off of main
as your
primary patch stack.
Once you have iterated on a patch to the point where you feel it is ready for review you naturally request review of that patch. Let's say you get some good feedback as part of that review. Therefore you make some more changes to the patch using the operations from above. Then you simply request review of the updated patch.
After receiving a final review with an approval, you likely want to share that patch with the rest of your team. This is done by integrating the patch. From a technical standpoint integrating is pushing the particular patch up to the upstream of your patch stack.
Git Patch Stack enables you to focus on creating, evolving, reviewing, and integrating patches which aids with creating code changes that are easy to review, aligned with the application architecture, scoped logically, facilitate efficient development, and support long term software maintainability.
Tool
The Git Patch Stack command line tool is an extension of Git designed to make creating & managing your stacks of patches throughout the development and review lifecycle as easy as possible. It does this by facilitating actions that make sense within the context of the conceptual model rather than the concepts & operations of Git and tools like GitHub.
Design Principles
As we started to define Git Patch Stack as a tool we used the following design principles as guiding lights to help constrain and shape it.
- streamline working within the conceptual model and working with Git in general
- work with existing defacto peer review tooling
- support stepping outside of Git Patch Stack and back into Git
- facilitate the Continuous Integration Methodology while supporting pre-commit peer review
These design principles are the key to Git Patch Stack being such an amazing tool.
Streamline working within the conceptual model
Other tools exist that to some degree facilitate working within the mental model of a stack of patches. However using them generally feels like more work than what you normally experience with Git itself. To prevent this we have chosen this design principle to make sure that working in the conceptual model would not only, not feel like overhead, but feel even more streamlined than a normal Git workflow.
Work with existing defacto peer review tools
One of the biggest driving forces for Git Patch Stack was that we wanted a way to work within the conceptual model without having to switch whole sale to separate code hosting and peer review provider, e.g. Phabricator. Therefore, we applied the constraint that it must support defacto peer review tools that people are used to working with, e.g. GitHub, Bitbucket, GitLab, Email, etc. Having this requirement also makes it possible for Git Patch Stack to be used even in environments where a portion of the team is using Feature Branches.
Support stepping outside of Git Patch Stack
Another crucial design principle we follow is that we never want Git Patch Stack to lock you in such that you can only use it. We want you to easily be able to work inside the conceptual model but if you need for some reason to step out of that mental model and use Git itself, Git Patch Stack should make that trivial.
Facilitate the Continuous Integration Methodology
Last but not least we have chosen the requirement that we must support the Continuous Integration Methodology as closely as possible while still supporting a pre-commit peer review as that is what the majority of developers are used to. The thinking is that this requirement will help us bring developers as close to 100% pure Continuous Integration but supporting the peer review process that everyone is used to.
Note: It is important to recognize that we are talking strictly about the Methodology of Continuous Integration and not simply having a continuous integration server setup running automated tests. Many people don't even realize that Continuos Integration is and was a Methodology and that the running of automated tests was only a very small portion of it.
Installation
As Git Patch Stack is written in Rust it can be compiled and installed on many different platforms. However, currently we only provide package management of it on macOS. So if you are on another platform you will have to follow the Build from Source instructions below.
Note: In order to use the request-review
command you must set up the
request_review_post_sync
hook after installation.
macOS
To install on macOS we provide a Homebrew tap which provides the
git-ps-rs
formula. To use it first you need to add the tap as follows.
brew tap "uptech/homebrew-oss"
This basically registers our tap as another source for packages for your Homebrew. Enabling you to do things like install the Git Patch Stack command line tool as follows.
brew install uptech/oss/git-ps-rs
Because you have registered the tap you can also do useful things like upgrade your version of the Git Patch Stack command line tool as follows.
brew update
brew upgrade git-ps-rs
zsh & bash Completions
Our Homebrew formula installs the zsh & bash completion scripts into the standard Homebrew shell completions location. So you just need to make sure that path is configured in your shell configuration. For zsh it is generally something like the following:
# add the Homebrew zsh completion scripts folder so it will be searched
fpath=(/opt/homebrew/share/zsh/site-functions/ $fpath)
# enable completion in zsh
autoload -Uz compinit
compinit
Build from Source
If you are on a platform other than macOS or you just want to build from source don't fret. You will just have to make sure you have the following build dependencies installed.
- Rust (macOS:
brew install rust
) - gpgme (macOS:
brew install gpgme
, Ubuntu:apt-get install -y libgpgme-dev
)
Once you have the build dependencies installed all you should need to do is run the following command to build the release version of the command line tool.
cargo build --release
Once you have built it successfully you can use the mv
command to move the
target/release/gps
file into /usr/local/bin/
or some other location in your
PATH
environment variable.
zsh & bash completions
The zsh and bash completion scripts are generated as part of the build process
by Cargo's custom build script, build.rs
at the root of the project.
The scripts are output to the Cargo -
OUT_DIR
location, generally target/release/build/gps-*/out
where the *
is a hash
value. The files are named as follows.
gps.bash
- bash completion script_gps
- zsh completion script
Simply move the files to whatever location on your system you are sourcing for completion scripts.
Hooks
Git Patch Stack takes the stance that it shouldn't be bound to a specific source control management platform (GitHub, Bitbucket, GitLab, etc.) or a particular request review process. Even across projects.
To give our users this flexibility we have created a hooks system for a
number of the commands, e.g. the request-review
and isolate
commands. This
allows the users to configure & customize what these commands do.
A hook is simply an executable file (script, binary, etc.) that is named according to the particular hook name and located in one of the three general locations for hooks.
.git-ps/hooks/
- communal repository specific hooks.git/git-ps/hooks/
- personal repository specific hooks~/.config/git-ps/hooks/
- personal user global hooks
Communal repository specific hooks are searched first, if not found then it is followed by searching the personal repository specific hooks, and if not found then it searches in the user's global hooks. This allows a team to standardize certain hooks across the repository using the communal repository specific hooks as well configure personalized hooks in their personal repository specific hooks or general sane default hooks in their user global hooks.
The following is a list of currently supported hooks and their expected filenames.
request_review_post_sync
- hook executed byrequest-review
command after successfully syncing the patch to remote - generally used to create a pull request / patch email & send it - Note: This hook is required to be able to use therequest-review
command.isolate_post_checkout
- hook executed byisolate
command after successfully creating the temporary branch, cherry-picking the patch to it, and checking the branch outisolate_post_cleanup
- hook executed byisolate
command after doing it's cleanup, checking out stack you were on prior to isolate and deleting theps/tmp/isolate
branch. The isolate cleanup process is triggered automatically when run viarequest-review
orintegrate
commands but can also be triggered by runninggps isolate
with no patch index.integrate_post_push
- hook executed byintegrate
command after successfully pushing the patch up to the patch stacks upstream remote. This is especially useful when you need to run something after a patch has been successfully integrated but before cleanup.list_additional_information
- hook executed bylist
command for each patch in the stack. Adds a column with the output of the hook script. Could be used, for example, to add information from the remote (for example, PR status on github).
You can find examples of hooks that you can straight up use or just use as a starting point in example_hooks.
Get Started with Hooks
To get started with hooks lets set up the request_review_post_sync
hook using
the super basic GitHub CLI
implementation
that handles creating the pull request for you as part of the request-review
command.
To start we need to make sure that the Global Hook Directory is created with the following:
mkdir -p ~/.config/git-ps/hooks
Then we need to copy the example hook of your choice to the Global Hooks Directory and give it execute permissions. This can be done with the following.
curl -fsSL https://raw.githubusercontent.com/uptech/git-ps-rs/main/example_hooks/request_review_post_sync-github-cli-example --output ~/.config/git-ps/hooks/request_review_post_sync
chmod u+x ~/.config/git-ps/hooks/request_review_post_sync
This hook uses the GitHub CLI to interface with GitHub and create the pull requests. In order for this to work we need to make sure that we have the GitHub CLI installed and makes sure that we have authenticated it. This can be done as follows no macOS.
brew install gh
gh auth login
Once you have setup the hook as described above and installed the GitHub CLI
and authenticated with it to GitHub. You should be all set. When you run the
request-review
it should use the hook to create a pull request of the patch.
Configuration
Git Patch Stack supports various settings via three layers of configuration files.
- personal global settings -
~/.config/git-ps/config.toml
- intended to allow you to define default personal settings for when a repository doesn't specify a setting - personal repository settings -
repo_root/.git/git-ps/config.toml
- intended to allow you to define personal settings constrained to a repository. Note: Settings defined in here override any values defined in the personal global settings. - communal repository settings -
repo_root/.git-ps/config.toml
intended to allow a team to enforce settings for everyone working on a repository. Note: Settings defined in here override any values defined in the personal repository settings or in the personal global settings.
The following is an example of a config defining all of the settings. All sections and settings are optional so you don't need to specify them all in each config.
[pull]
show_list_post_pull = false
[request_review]
verify_isolation = true
[integrate]
verify_isolation = true
prompt_for_reassurance = true
pull_after_integrate = false
[fetch]
show_upstream_patches_after_fetch = true
[branch]
verify_isolation = true
push_to_remote = false
[list]
add_extra_patch_info = false
extra_patch_info_length = 10
reverse_order = false
The following is a breakdown of the supported settings.
pull.show_list_post_pull
- (true/false default: false) - controls whether thepull
command will show the patch list after successfully pullingrequest_review.verify_isolation
- (true/false default: true) - if true therequest-review
command will run theisolate
command & it's hooks to verify the patch is isolated prior to requesting review. If the isolation verification fails it errors preventing you from requesting review.integrate.verify_isolation
- (true/false default: true) - if true theintegrate
command will run theisolate
command & it's hooks to verify the patch is isolated prior to integrating it. If the isolation verification fails it errors preventing you from integrating the patch.integrate.prompt_for_reassurance
- (true/false default: true) - if true theintegrate
command will present the user with the patch details and prompt the user asking them if they are sure they want to integrate the patch. If they say yes, then it moves on the with integration. If not it aborts the integration.integrate.pull_after_integrate
- (true/false default: false) - if true theintegrate
command willpull
after a successful integration.fetch.show_upstream_patches_after_fetch
- (true/false default: true) - if true thefetch
command will show the upstream patches that were fetched.branch.verify_isolation
- (true/false default: true) - if true thebranch
command will run theisolate
command & it's hooks to verify the patch(es) are isolated prior to creating and possibly pushing the branch. If the isolation verification fails it errors preventing you from creating the branch.branch.push_to_remote
- (true/false default: false) - if true thebranch
command will push the branch to the remote with the same name automatically. If false it will only create the local branch.list.add_extra_patch_info
- (true/false default: false) - if true thelist
command will include extra patch information from thelist_additional_info
hook. See Hooks for more details.list.extra_patch_info_length
- (integer default: 10) - the width of the additional information column in the output ofgps list
. If the output is longer it will get truncated. See Hooks for more details.list.reverse_order
- (true/false default: false) - if set to true it will reverse the order in whichgps list
presents the patches in the stack. Some people use this option to make the patch order match the order patches are presented within interactive rebases.
Basic Usage
The basic usage pattern for Git Patch Stack is pretty straight forward and is outlined as follows.
- add one or more WIP patch(es) to your stack
- iterate and evolve a patch to a state where it is ready to be reviewed
- request review of the patch
- rinse and repeat steps 2 & 3 until that patch is approved
- integrate the patch
Add one or more WIP patch(es) to you stack
The first step is to add a WIP, Work In Progress, patch to your stack. This is effectively a stack that is incomplete in it's nature with a summary prefixed with "WIP:".
This is done simply by making some local changes, staging them with gps add
,
and then create a patch with gps c
or gps create-patch
. This will take the
staged changes and create a patch on top of your stack from them.
Iterate and evolve a patch to a state where it is ready to be reviewed
The second step is to iterate the WIP patch using various commands like gps a
or gps amend-patch
, gps rebase
, etc. to get it to a place where it is ready
to review. At this point you remove the "WIP:" prefix from the patches summary.
Request review of the patch
Then you request review of the patch with gps rr
or gps request-review
.
This requests review of the specified patch using the provided hook. This most
likely is creating a pull request on GitHub, Bitbucket, or GitLab.
Rinse and Repeat until approved
Once you have requested review of the patch you wait for feedback and make any
necessary changes using the same commands as when iterating on it before. Once
you feel it is once again ready to be reviewed you simply request review for
the patch again using gps rr
or gps request-review
. You continue this
process until you have addressed all the issues and the patch has been
approved.
Integrate the patch
Once the patch has been approved you are ready to integrate it upstream so that
the rest of the team can continue building on it. This is done with the gps int
or gps integrate
command.
Once you have integrated your patch in. You do it all over again with another patch.
Command Docs
The Git Patch Stack command line tool has integrated help for the command itself as well as all of it's subcommands.
You can get a full breakdown of all the commands by running
gps --help
You can also get detailed help about specific commands by use the --help
switch with the command. Example:
gps request-review --help
Man Pages
We also provide man pages for the Git Patch Stack command line tool as well.
The can be accessed as follows:
man gps-add
Note: The man pages are still a work in progress so there may not be the man
page you are looking for. If that is the case, no worries. Just fall back to
using the integrated --help
switch for the subcommand you are interested in.
Stuck on Feature Branches
Sometimes you are in a situation where you are stuck with Feature Branches. This could be because your team hasn't made the glorious switch over to Git Patch Stack yet. Or it could be you are dropping in as a consultant to help a team out with something. Or maybe you are working on a team that has a bunch of automation setup already around the use of Feature Branches.
Whatever your situation, don't fret! Git Patch Stack is designed such that you can use it locally and still get a lot of the benefits even if you are stuck in the world of Feature Branches.
Build up Patch Series
The big difference between the normal workflow and this one is simply that you don't request review of the individual patches as soon as you finalize them. Instead, you build up a series of finalized patches in the correct order within in your patch stack. Then you effectively request review of the series of patches.
This sadly means you don't get the benefits of near continuous integration. However, all is not lost. If you use Git Patch Stack locally and follow the best practices and methodology around creating small logical patches. You will end up with a well-defined series of patches that will stand as a logical "proof of work" for your feature. Beyond that those logical patches will be invaluable later on as part of the Git history.
Request Review of Series
Once you have built up your patch series of finalized patches. We need to create a pull request of that patch series. The best process at the moment to do this with Git Patch Stack is the following.
- build up your series of finalized patches in your stack
- create and push branch containing copy of your patch series - e.g.
gps branch -p 1 3
- open a Pull Request in whatever source control management tool you are using
Note: The gps branch
command allows you to pass the -n your-branch-name
switch to it if you want to control the name of the branch created. Also, there
is a configuration option within the branch
command section called
push_to_remote
that can be set, so you don't have to pass the -p
switch on
the command line to have it push to remote. See
Configuration for details.
Deal with Feedback
Often times you will get feedback on your Pull Request when it is reviewed. To address this process is generally as follows.
- update the patches in your patch stack to address the feedback
- overwrite local branch with updated patch series - e.g.
gps branch -p 1 3
- comment on the Pull Request letting the reviewer know what changes you have addressed and that they have been pushed up
The Future
Discussions are currently being had around how best to include the concept of a
series of patches into the request-review
command. Doing so would provide the
same functionality but with the benefit of state tracking.
Change is Hard
We understand that Change is Hard. Especially changing conceptual models and ways of thinking that you might be used to. That is why we designed Git Patch Stack to be something that you can adopt in stages. For example it is designed in such a way that you can easily use the Git Patch Stack command line tool to start getting comfortable with the tool and the process without ever knowing anything about the Methodology or the best practices.
Stage 1
An example of this might be that you use gps
to create patches, iterate on
patches, request review of patches and integrate them. But you continue to make
large patches. This seems to be how some people prefer to start. We can
consider this Stage 1 for lack of a better name.
Obviously when doing this you would be missing out on all the benefits that the Methodology provides in addition to the core benefits that the tool provides. It is worth noting that the benefits the Methodology provides are quite significant.
Stage 2
Maybe at this point you are starting to naturally break changes up into smaller patches and you have run into the concept of dependent patches. Therefore you have done some reading in our docs on Dependent Patches, the types of dependencies and the various techniques you can apply to avoid the dependencies.
This gains you even more of the benefits but still leaves you shy of a significant chunk of benefits tied to the Methodology.
Stage 3
Hopefully after living in Stage 2 for a while you have seen more value and realize that there is an extremely valuable Methodology out there that can help you understand how best to think through breaking down a problem into small logical ideally independent patches. So you read the documentation around the Methodology and start applying the practices to your work with Git Patch Stack unlocking it's full potential.
The Other Way Around
Everyone is clearly different, and how people learn and adopt things is also different. Therefore others may find it makes more sense to learn about the Methodology first and then start working with the Tool. So don't feel obligated to following these three stages.
Guides
The guides are intended to provide clear and concise step by step instructions on how to perform operations within Git Patch Stack while also providing detailed explanations of how those operations relate to Git.
Add patches to your stack
We're going to focus on how we create patches on our stack and what a
stack is in terms of git
?
TL;DR
A stack is simply a branch with an upstream, e.g. main
with an upstream of
origin/main
. You can create a patch on top of your stack with the following.
- Make a change locally in your editor of choice
- Stage your change with
gps add -p
- Create patch with
gps c
WalkThrough
Git Patch Stack is really just a layer of
tooling built directly on top of git
. So if we look at our git
tree
here, we have a small project.
It has an initial skeleton of a Rust project. That's just a Hello World program
right now, and we can see we have a branch called main
that were checked out
on. That's why HEAD
points to it, and we then have an upstream main
with a
remote of origin
. That happens to be where main
points to on our GitHub
repository for this.
Technically, in Git Patch Stack terms, any branch that has an upstream is
a Patch Stack. So you can have as many of these Patch Stacks as you want, as long
as they are branches that have a remote upstream. To add patches on to a stack,
you first have to be checked out on a branch that has an associated upstream.
In our case, main
.
Make a change
Then we just make changes like we normally would in git
. We add a commit
and that commit conceptually becomes a patch. So let's do that real quick.
Let's go look at our source code here. We have a main()
function.
fn main() { println!("Hello, world!"); }
Let's say that we want to add another function called foo()
for some reason,
and we want this function to print "Foo", nothing too crazy. So we add a
function. We're not even going to use the function. We're just going to add it
to the code base so that we can use it in the future.
fn main() { println!("Hello, world!"); } fn foo() { println!("Foo"); }
That's it. Now we just do a git diff
to verify our changes.
diff --git a/src/main.rs b/src/main.rs
index e7a11a9..0e47771 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,7 @@
fn main() {
println!("Hello, world!");
}
+
+fn foo() {
+ println!("Foo");
+}
I have a git alias of
di
setup so I can just rungit di
to get the diff quicker.
Once we verify the change is what we want, then we stage that change.
We can stage that change by doing a gps add
of that file. Note: gps add
is just a convenience mechanism for git add
so you can also use git add
directly. Now we can do a git diff --cached
and that will show us all the
changes that are staged as a diff.
diff --git a/src/main.rs b/src/main.rs
index e7a11a9..0e47771 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,7 @@
fn main() {
println!("Hello, world!");
}
+
+fn foo() {
+ println!("Foo");
+}
It looks good. Looks like the changes we want.
I have a git alias of
dc
setup so I can just rungit dc
to get the staged diff quicker.
Create Patch on Stack
If we do a gps create-patch
or gps c
for short, it pops up the configured
editor and we can enter the commit message. This is actually just a convenience
mechanism for git commit
. So technically you can create a patch by just doing
git commit
as well.
Add foo() function
So that in the future we can print the message when necessary.
Ideally, we would provide more context but because this is a contrived example. We don't really have it. Now we have a commit (a.k.a. patch).
If we look at our git
tree, you
can see we have a commit here that we're pointing to on main
, which is the
"Add foo() function" patch. And then underneath that in the tree, we have the
initial skeleton commit we had before.
Now, if we run gps ls
, which is how we list our stack of patches.
We can now see that our stack of patches consist of one patch.
Add another Patch
So let's add another one real quick just so we can see what it's like to have two. Let's just copy this and paste that, and change this word to bar, change this to bar and then we'll add another function.
fn main() { println!("Hello, world!"); } fn foo() { println!("Foo"); } fn bar() { println!("Bar"); }
Now we just gps add
, git dc
to verify our staged code. All looks good. We
create another patch using gps c
and enter the message.
Add bar() function
So that we can print the "Bar" message in the future.
Now we should have two patches in our stack.
We have patches zero, which is the index of the "Add foo() function" patch. And we have patch one which is the index of the "Add bar() function" patch.
The short SHA of these patches, a.k.a. commits, is visible to the right of the index and status space. We can also see the patch summaries to the right of the short SHA.
As you get into Git Patch Stack further, you'll see status indications here that will indicate things like whether that patch has been requested for review, the patch has been integrated, or if there's been changes since you requested review. But for now that's it.
That's how you add a patch to the top of your stack.
Hope you enjoyed.
List patches on your stack
How to list patches on your stack and their associated state information.
TL;DR
gps list
orgps ls
for short
WalkThrough
The list
command lists out your stack of patches in a format that exposes
the patch index on the far left followed by the state information, followed by
the short SHA of the git commit, and finally followed by the patch summary.
[index] [status] [sha] [summary]
The patch index value is used with other commands, e.g. gps rr <patch-index>
.
The state information is broken down into main states and modifiers. The main states are as follows.
b - local branch has been created with the patch
s - the patch has been synced to the remote
rr - you have requested review of the patch
int - you have integrated the patch into upstream
Each of the states can have any of the following modifiers.
+ - the patch in your patch stack has changed since the operation
! - the remote patch has changed since the operation
↓ - patch is behind, the patch stack base has been updated since the operation
To fully understand this lets look at an example. Let say you see the status
rr+!
when you ran the list
command. This is telling you that the current
patch is in a state of requested review, indicated by the rr
. It is also
telling you that since you last requested review of that patch changes have
been made to it, in your patch stack. This is indicated by the +
modifier.
In addition it is telling you that the patch on the remote has also changed
since you last requested review, indicated by the !
modifier.
The +
by itself is a pretty common modifier to see as it is there to simply
remind you that you have made changes to a patch and need to sync
or
request-review
for that patch again.
The !
modifier is less common as it only happens when the patch on the
remote has changed since the last operation. This is generally because someone
either force pushed up a patch out of band of Git Patch Stack to replace that
commit or because someone added a commit to the branch that Git Patch Stack
created and associated to that patch. To resolve !
modifiers you really need
to go look at the commits on the remote branch and see if there are any
changes there you want to keep. If so you should cherry-pick the ones you want
into your patch stack and squash/fix them up into the logical patch using the
rebase
command. Then you should sync
or request-review
of that patch
again to get it back in sync.
The ↓
modifier indicates that the patch is conceptually behind. What this
means is that when the last rr/sync operation was performed the base of the
patch stack was at one point in the git tree but now it has progressed forward
as someone integrated changes into it. This can be addressed by doing a gps pull
to make sure that your local stack is up to date and integrates
everything from upstream and then doing a gps sync
or gps rr
to update the
remote with newly rebased patch.
Pull integrated patches down
In this guide we're going to focus on how to pull down integrated patches from upstream and have your stack replayed on top of the pulled patches.
TL;DR
gps pull
- if conflict, resolve it
- if conflict,
gps add
resolved conflict files - if conflict,
gps rebase --continue
- repeat steps 2 to 4 for each conflicting patch
WalkThrough
The TL;DR section makes this feel trivial. And generally it is pretty trivial as you
simply run gps pull
, which effectively runs a git fetch
to update your local
repositories knowledge of the upstream repository's Git tree followed by a
git rebase --onto <upstream-branch-name> <upstream-branch-name> <head-branch-shortname>
, e.g. git rebase --onto origin/main origin/main main
.
Conflicts occurring during the rebase are what might catch you off guard the first time. Here in your stack is exactly where you want to be confronted with and resolve any conflicts though. It forces you to integrate the upstream branch's changes with your stack more often and with smaller increments which makes conflict resolution easier because the scope of changes is smaller. Beyond that having integration be bound to the pull is also beneficial because it forces you to integrate your stack when fetching any changes from upstream.
Given that this command performs a rebase it is beneficial to have a good understanding of what rebasing is and what happens during a rebase. To get a deeper understanding of rebase and gain some comfort with it you can check out ProGit - Git Branching - Rebasing.
Merge Conflicts vs Rebase Conflicts
In addition to being comfortable with the basics of rebasing it is important to
understand that merge conflicts that you might be used to with git merge
are
quite different than the conflicts you will run into with git rebase
. When
presented with a conflict from a git merge
operation you are effectively
resolving all the conflicts of all the commits involved as one singular
conflict.
The git rebase
operation works in a completely different manner. It
effectively lifts up your stack of patches and plays them back one by one on
top of the upstream branch (e.g. origin/main
). This is important because
conflicts are risen at the commit level. This means as it is going through
and replaying each commit on top of one another it is checking if there is a
conflict or not. If there isn't a conflict then it applies that commit cleanly
and moves on to playing the next commit in the stack. If there is a conflict it
pauses the rebase and leaves you in a working state with the conflict present.
You tactically resolve this the same way you would any conflict. However, you
need to understand that the scope of the conflict is specific to the commit that
was replayed and not all the commits combined together like a git merge
would
be. This means when you resolve that conflict you should resolve it as if none
of the commits above it existed yet.
This works brilliantly with Git Patch Stack because you write patches as
logical changes in alignment with the application architecture. This means when
conflicts arise they are within the scope of some application architecture
concept or within the integration of an application architecture concept. This
makes understanding and resolving conflicts much easier than with git merge
,
where the changes aren't scoped to a particular application architecture
concept.
Once you have resolved the conflict and staged the change with git add
you
can continue the rebase process with git rebase --continue
to have it pick
back up from where it was paused.
Convenience Functions
Git Patch Stack provides the gps add
and gps rebase --continue
commands as
convenience mechanisms to help provide a complete abstraction so you don't have
to bounce between Git and Git Patch Stack if you don't want to.
Reorder patches
One of the core concepts of Git Patch Stack is this idea of starting out with patches in an non-reviewable state and then iterating on them to get them to a state where they are ready for review. As we iterate on various patches it is crucial to be able to reorder the patches on your stack so that the dependencies can be moved to the bottom of the stack.
TL;DR
gps rebase
- reorder the commit lines presented, in your configured editor, into the order you want them
- save and quit the editor
WalkThrough
The gps rebase
command is a convenience function that really runs an
interactive rebase of the stack on top of it's associated upstream, e.g.
git rebase -i --onto origin/main origin/main main
.
So understanding how to reorder patches with this command is really simply learning how to reorder commits using git's interactive rebase.
Lets start with the following patch stack (gps ls
).
2 0317f6 Add function C
1 5b5f29 Add function B
0 32e6fd Add function A
Lets say that function B needs to become a dependency of function A. In order to put our stack into a state where we can actually iterate on function A adding B as a dependency we need to first reorder the patches so that "Add function B" is at the bottom of the stack.
To do this we start by running gps rebase
to kick off the interactive rebase.
It presents the following in our configured editor.
pick 32e6fd7 Add function A
pick 5b5f290 Add function B
pick 0317f6a Add function C
# Rebase a34b62b..0317f6a onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
To reorder the commits we simply reorder the lines of text in this buffer so that they are in the order we want. Note: The stack is inverted when presented in an interactive rebase. So the bottom most commit on the stack is actually the top most commit. To accomplish our goals lets swap "Add function A" and "Add function B" as follows.
pick 5b5f290 Add function B
pick 32e6fd7 Add function A
pick 0317f6a Add function C
# Rebase a34b62b..0317f6a onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
We then save and quit the editor and it reorders the commits as part of the
rebase. This leaves our patch stack as follows (gps ls
).
2 04ea78 Add function C
1 643bb7 Add function A
0 1d9185 Add function B
Exactly the state we wanted it to be in so that we can iterate on the "Add function A" patch to make it integrate with function B.
Drop a patch
As we develop software sometimes we come up with ideas that we pretty quickly decide aren't great. We generally want to throw this code away. In Git Patch Stack because our patches represent logical changes we do this by simply dropping a patch out of our stack.
TL;DR
gps rebase
- mark the commit we want to drop with
d
ordrop
- save and quit the editor
WalkThrough
The gps rebase
command is a convenience function that really runs an
interactive rebase of the stack on top of it's associated upstream, e.g.
git rebase -i --onto origin/main origin/main main
.
So understanding how to drop a patch with this command is really simply learning how to drop commits using git's interactive rebase.
Lets start with the following patch stack (gps ls
).
2 65a811 Add function C
1 a876c2 Add function B
0 f79714 Add function A
Lets say that "Add function B" is no longer need and we just want to get rid of it.
To do this we start by running gps rebase
to kick off the interactive rebase.
It presents the following in our configured editor.
pick f797149 Add function A
pick a876c29 Add function B
pick 65a811a Add function C
# Rebase a34b62b..65a811a onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
To drop the commit we simply mark it for dropping with either d
or drop
instead of it's default pick
. If you forget the marking you can always
reference the comment Git includes in the interactive rebase buffer. So in this
case we mark the "Add function B" commit with d
as follows.
Note: The stack is inverted when presented in an interactive rebase. So the bottom most commit on the stack is actually the top most commit. This can be confusing until you get used to it.
pick f797149 Add function A
d a876c29 Add function B
pick 65a811a Add function C
# Rebase a34b62b..65a811a onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
We then save and quit the editor and it drops commits marked with d
or drop
as part of the rebase. This leaves our patch stack as follows (gps ls
).
1 450768 Add function C
0 f79714 Add function A
Exactly the state we wanted it to be in.
Add a patch in the middle
Another common action you will want to perform when working with a stack of patches is being able to add a patch at a specific point in the middle of your stack.
This is beneficial over adding a patch on top of the stack and reordering it into it's correct position because it makes it so that when you are creating your patch it is based on the correct dependent code and not code that is only introduced higher up in the stack. It also has the benefit of forcing you to properly integrate changes higher up in the stack with your newly introduced patch.
This operation is really just a specific use case of a Git interactive rebase. So as with most of these operations being comfortable with Git's interactive rebase is key.
TL;DR
For those who just want a quick reminder reference here is the TL;DR. For those who need a bit more context and detail through the walk through read the sections below.
gps rebase
- do an interactive rebase of the patch stack & mark the patch you want to add a new patch after withedit
, it will drop you out into the shell at that patch- make your changes to the code
gps add
- stage changes you want in the patchgps c
- create the patchgps rebase --continue
- continue the rebase to play the other commits on top of the new commits you created
Initial State
For this example lets assume that we have a Patch Stack that has the following patches.
✔ gps ls
2 3d490f Add car() function
1 3e483a Add bar() function
0 20d08a Add foo() function
As we can see from the first patch Add foo() function
, it adds the foo()
function.
✔ gps show 0 0m main fcff7bc
commit 20d08af5aecde6126398208dc5ea16fe87eeb7e6
tree c7f299cc61fab8bcd89c7a003322779b63246b07
parent a34b62b61d1675073eb1df953f1cdfc01232cceb
author Drew De Ponte <cyphactor@gmail.com> 1657069825 -0400
committer Drew De Ponte <cyphactor@gmail.com> 1657069825 -0400
gpgsig -----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEZ4qmf6cBz6fWZrzoQcXSxuWvlEwFAmLE4QkACgkQQcXSxuWv
lEzj3Af/aeJgWK3j2RuYwRU+W5lF/oiKYukpIDGrtFkyLMPXglWD/TFqfh35Cf+q
z+GSwC4WrHcOdPXf6geJ6iXDMfu8i0WI1A16xu9d6299uPgpTRUOdC8g3IW6MO/V
Iv7CBITdGkdPrPznSN1xDjvarmYpjYJsSXravn6uvK26TKJDP0hDSREWO4LM/prI
d12TEaRm4OtWDDhCMh7HvuYyr0lJXdAsO/HpPjNadqTT1RvJ5O52RypSx79tIe7Q
cPY+N/CPLa1YaQVkbO0pl8YaIhz+bh+m51xNizgbutb/pvnMtyIALYrffVQzDPlW
e1BZRP+9ux+aw8w6CN5+0OU8BFax5w==
=3wZ5
-----END PGP SIGNATURE-----
Add foo() function
diff --git a/src/main.rs b/src/main.rs
index e7a11a9..b5a8cf4 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,3 +1,7 @@
fn main() {
println!("Hello, world!");
}
+
+fn foo() {
+ println!("Foo");
+}
The second patch, Add bar() function
, adds the bar()
function as seen in
the diff below.
✔ gps show 1
commit 3e483a19203cffae28cb8a99f989c0c1d250eda8
tree 443074d262fc9d86048becf7d6f436ef799b9ced
parent 20d08af5aecde6126398208dc5ea16fe87eeb7e6
author Drew De Ponte <cyphactor@gmail.com> 1657069869 -0400
committer Drew De Ponte <cyphactor@gmail.com> 1657070294 -0400
gpgsig -----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEZ4qmf6cBz6fWZrzoQcXSxuWvlEwFAmLE4tgACgkQQcXSxuWv
lEzdWAf9G6XI/DJmIL+Ws4mtyLP07MaGqybt0DrOdgPusjTKMrkZbj6sFD4CuSMk
LiZEsL4oAQRo3XtxBd5ggh8S4DQwusn/KZf4oJq9GvhJJylFJbg5Qe5b4/HNKMGJ
NA04+uDI1V+ZSsMgTPSBtpFq0vw/xPc7sG8uas+ESUNJ+91aAtPSBuVXFpnPYkJ1
+j7UB0I9JMyhBQtNyZ9BI+0biE5yDmepUzg/1InKQJvQOLPjHN4IbfGraAcw8Dym
6C9AU79/e8VlEHJd5OLfgqjMISh+jPEDqBsIgRuppcFTpm0F1OzVv4877RR42llL
uA6XFe5iR8ciy9mVihROZGh05X069Q==
=Weyt
-----END PGP SIGNATURE-----
Add bar() function
diff --git a/src/main.rs b/src/main.rs
index b5a8cf4..84be80e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,3 +5,7 @@ fn main() {
fn foo() {
println!("Foo");
}
+
+fn bar() {
+ println!("Bar");
+}
The third patch, Add car() function
adds the car()
function in the diff
below.
✔ gps show 2
commit 3d490f84e505c2da67d52624d84357de6ad1a746
tree 2206a333eaa4329e8762976bc9e462a29fbe48fa
parent 3e483a19203cffae28cb8a99f989c0c1d250eda8
author Drew De Ponte <cyphactor@gmail.com> 1657069940 -0400
committer Drew De Ponte <cyphactor@gmail.com> 1657070300 -0400
gpgsig -----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEZ4qmf6cBz6fWZrzoQcXSxuWvlEwFAmLE4twACgkQQcXSxuWv
lEztJwgAztuVEGj1/9pguOiV6VhPrbtEIznWIlPvik4aL4/0U2DEW7sK8ZzTnl7d
lj/JEMoKEq32CR1oAxqk8iUwiShtggyhjic+HjgKg+CYOQQ3LycEhD1c85vm8+Pk
MEWWuzzzf/vgEZqb9mIAO9IvpVqcnj7sMyrKMP/WvIhWDpw+lpyNjJeXpg2dY8Oz
dUB3m4FnUlycaxsEOxK5GlDu9AwTbtKALTkVA4eioqM3rervqcgOJnap07E4zWHH
vnDrCjfaAn4ckzHJwgNnAVLXWshKzfuq48WvNN0Bvo7wb4YwVEX+NkUE9d74BVLR
p5Y1CrJmYLF0xjsOIF3bb0Yj95NoCw==
=MnJj
-----END PGP SIGNATURE-----
Add car() function
diff --git a/src/main.rs b/src/main.rs
index 84be80e..2282948 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,3 +9,7 @@ fn foo() {
fn bar() {
println!("Bar");
}
+
+fn car() {
+ println!("Car");
+}
Add the foobar() function
Let us say for sake of discussion we want to add a new function, foobar()
that is composed of the foo()
and bar()
functions respectively. So we want
to write something like the following.
fn foobar() {
foo();
bar();
}
Edit Mode
To accomplish this we need to utilize an interactive rebase to enter "edit" mode in the correct place in the Patch Stack. In this particular case we want to rebase our Patch Stack.
gps rebase
This will bring up the following in your editor.
pick 20d08af Add foo() function
pick 3e483a1 Add bar() function
pick 3d490f8 Add car() function
# Rebase a34b62b..3d490f8 onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
In the interactive rebase buffer we want change the action for the
Add bar() function
patch to edit
so it is as follows.
pick 20d08af Add foo() function
edit 3e483a1 Add bar() function
pick 3d490f8 Add car() function
# Rebase a34b62b..3d490f8 onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
When you save & quit the editor it will run the specified interactive rebase
commands. In this case pick (meaning keep) the first patch and then stop on
the second patch allowing for editing because we specified, edit
. When it
does this it will drop you back to the console with a message similar to the
following:
Stopped at 3e483a1... Add bar() function
You can amend the commit now, with
git commit --amend '-S'
Once you are satisfied with your changes, run
git rebase --continue
Note: This drops you right after the patch (a.k.a. commit) that was marked for edit
in the interactive rebase. We can see this if we look at the Git tree and look for HEAD
as it shows which commit we are currently checked out on.
✔ git la
* 3d490f8 - G - (main) Add car() function (20 minutes ago) <Drew De Ponte>
* 3e483a1 - G - (HEAD) Add bar() function (22 minutes ago) <Drew De Ponte>
* 20d08af - G - Add foo() function (22 minutes ago) <Drew De Ponte>
* a34b62b - G - (origin/main) Initial skeleton (10 months ago) <Drew De Ponte>
Add foobar()
function patch
Now that we know that we are in the middle of a rebase and we know where we are located in terms of the patches. We are ready to simply create a new patch right where we are.
When we open the src/main.rs
file to add the new foobar()
function we see
the following.
fn main() {
println!("Hello, world!");
}
fn foo() {
println!("Foo");
}
fn bar() {
println!("Bar");
}
Note: We do NOT see the car()
function. That is because that patch is
above our current location in the stack. Which is exactly what we want.
So we add the foobar()
function so our code is as follows.
fn main() {
println!("Hello, world!");
}
fn foo() {
println!("Foo");
}
fn bar() {
println!("Bar");
}
fn foobar() {
foo();
bar();
}
Then we stage the change with gps add
or git add
and create the patch with
gps c
or git commit
as we normally would. After creating the patch if we
look at the Git tree we will see the following.
✔ git la
* 60e2c40 - G - (HEAD) Add foobar() function (11 seconds ago) <Drew De Ponte>
| * 3d490f8 - G - (main) Add car() function (30 minutes ago) <Drew De Ponte>
|/
* 3e483a1 - G - Add bar() function (31 minutes ago) <Drew De Ponte>
* 20d08af - G - Add foo() function (32 minutes ago) <Drew De Ponte>
* a34b62b - G - (origin/main) Initial skeleton (10 months ago) <Drew De Ponte>
Here we can see the new Add foobar() function
patch but we can also see that
the Add car() function
patch isn't stacked on top of it yet. This is because
we are still in the middle of the rebase.
Finish the Rebase
To replay the rest of the patches on top of the new patch(es) we just created we simply run the following.
gps rebase --continue
Potential Conflicts
Depending on the changes you made you may run into conflicts that you created with the patches above. This is actually exactly what you want because if you made the change in the correct location in your stack then you want it to force you to integrate the above patches with the new change.
In the case of our example we get the following output telling us there is a conflict.
Auto-merging src/main.rs
CONFLICT (content): Merge conflict in src/main.rs
error: could not apply 3d490f8... Add car() function
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Could not apply 3d490f8... Add car() function
Error: RebaseFailed(ExitStatus(1))
This happened because the foobar()
function definition that we introduced
into the src/main.rs
file was added in the same location that the car()
function definition was created before.
If we look at src/main.rs
right now it looks like the following.
fn main() {
println!("Hello, world!");
}
fn foo() {
println!("Foo");
}
fn bar() {
println!("Bar");
}
<<<<<<< HEAD
fn foobar() {
foo();
bar();
=======
fn car() {
println!("Car");
>>>>>>> 3d490f8 (Add car() function)
}
We can resolve this by simply moving the car()
definition down below foobar()
and remove the conflict markers like so.
fn main() {
println!("Hello, world!");
}
fn foo() {
println!("Foo");
}
fn bar() {
println!("Bar");
}
fn foobar() {
foo();
bar();
}
fn car() {
println!("Car");
}
Then we simply stage the conflict resolution state with gps add
or git add
and request it continue the rebase with gps rebase --continue
.
Since we have resolved all the conflicts we see the following out.
✔ gps rebase --continue
[detached HEAD 87903c9] Add car() function
1 file changed, 4 insertions(+)
Successfully rebased and updated refs/heads/main.
If we checkout our Patch Stack it will now look as follows.
✔ gps ls
3 87903c Add car() function
2 60e2c4 Add foobar() function
1 3e483a Add bar() function
0 20d08a Add foo() function
And we have successfully added a patch into the middle of our stack!
Edit a patch
Given that part of Git Patch Stack is iterating on patches you will likely need to modify an existing patch. One common way of modifying an existing patch is to amend it.
This is beneficial over adding a patch on top of the stack, reordering it into it's correct position, and squashing it because it makes it so that when you are amending your patch it is based on the correct dependent code and not code that is only introduced higher up in the stack. It also has the benefit of forcing you to properly integrate changes higher up in the stack with your newly introduced patch.
This operation is really just a specific use case of a Git interactive rebase. So as with most of these operations being comfortable with Git's interactive rebase is key.
TL;DR
For those who just want a quick reminder reference here is the TL;DR. For those who need a bit more context and detail, the walk through provides it in the sections below.
gps rebase
- do an interactive rebase of the patch stack & mark the patch you want to amend withedit
, it will drop you out into the shell at that patch- make your changes to the code
gps add
- stage changes you want to amend to the patch marked withedit
gps a
- amend the current patchgps rebase --continue
- continue the rebase to play the other commits on top of the new commits you created
Initial State
For this example lets assume that we have a Patch Stack that has the following patches.
✔ gps ls
3 87903c Add car() function
2 60e2c4 Add foobar() function
1 3e483a Add bar() function
0 20d08a Add foo() function
Amend the foo() function
Let us say for sake of discussion we want to amend the, Add foo() function
patch so that it instead prints out "Hello Foo"
.
Edit Mode
To accomplish this we need to utilize an interactive rebase to enter "edit" mode in the correct place in the Patch Stack. In this particular case we want to rebase our Patch Stack.
gps rebase
This will bring up the following in your editor.
pick 20d08af Add foo() function
pick 3e483a1 Add bar() function
pick 60e2c40 Add foobar() function
pick 87903c9 Add car() function
# Rebase a34b62b..87903c9 onto a34b62b (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
In the interactive rebase buffer we want change the action for the
Add foo() function
patch to edit
so it is as follows.
edit 20d08af Add foo() function
pick 3e483a1 Add bar() function
pick 60e2c40 Add foobar() function
pick 87903c9 Add car() function
# Rebase a34b62b..87903c9 onto a34b62b (4 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
When you save & quit the editor it will run the specified interactive rebase
commands. In this case editing the first patch and then stopping for editing
because we specified, edit
. When it does this it will drop you back to the
console with a message similar to the following:
Stopped at 20d08af... Add foo() function
You can amend the commit now, with
git commit --amend '-S'
Once you are satisfied with your changes, run
git rebase --continue
Note: This drops you right after the patch (a.k.a. commit) that was marked
for edit
in the interactive rebase. We can see this if we look at the Git
tree and look for HEAD
as it shows which commit we are currently checked out
on.
✔ git la
* 87903c9 - G - (main) Add car() function (2 hours ago) <Drew De Ponte>
* 60e2c40 - G - Add foobar() function (87 minutes ago) <Drew De Ponte>
* 3e483a1 - G - Add bar() function (2 hours ago) <Drew De Ponte>
* 20d08af - G - (HEAD) Add foo() function (2 hours ago) <Drew De Ponte>
* a34b62b - G - (origin/main) Initial skeleton (10 months ago) <Drew De Ponte>
Add foobar()
function patch
Now that we know that we are located on the patch we want to amend. We are ready to simply amend right where we are.
When we open the src/main.rs
file and modify the foo()
function so that it
is as follows.
fn main() {
println!("Hello, world!");
}
fn foo() {
println!("Hello Foo");
}
Note: We did NOT see the bar()
, foobar()
, or car()
functions.
This is because those patches are above our current location in the stack.
Which is exactly what we want.
Then we stage the change with gps add
or git add
and amend the patch with
gps a
or git commit --amend
as we normally would. After amending the patch
if we look at the Git tree we will see the following.
✔ git la
* ea980b0 - G - (HEAD) Add foo() function (2 hours ago) <Drew De Ponte>
| * 87903c9 - G - (main) Add car() function (2 hours ago) <Drew De Ponte>
| * 60e2c40 - G - Add foobar() function (2 hours ago) <Drew De Ponte>
| * 3e483a1 - G - Add bar() function (2 hours ago) <Drew De Ponte>
| * 20d08af - G - Add foo() function (2 hours ago) <Drew De Ponte>
|/
* a34b62b - G - (origin/main) Initial skeleton (10 months ago) <Drew De Ponte>
Here we can see the amended version of Add foo() function
patch but we can
also see that the rest of the patches aren't stacked on top of it yet. This is
because we are still in the middle of the rebase.
Finish the Rebase
To replay the rest of the patches on top of the new patch(es) we just created we simply run the following.
gps rebase --continue
Potential Conflicts
Depending on the changes you made you may run into conflicts that you created with the patches above. This is actually exactly what you want because if you made the change in the correct location in your stack then you want it to force you to integrate the above patches with the new change.
In the case of our example there are no conflicts. If we checkout our Patch Stack it will now look as follows.
✔ gps ls
3 95d601 Add car() function
2 5be80a Add foobar() function
1 44c0f6 Add bar() function
0 ea980b Add foo() function
And we have successfully amended a patch in the middle of our stack!
Combine multiple patches
Sometimes you decide that two separate patches should really be collapsed into one to make a complete logical change.
TL;DR
For those who just need a quick reference as a reminder you can do the following.
gps rebase
- do an interactive rebase of the patch stack- mark the patch(es) you want to combine up into another patch with
squash
orfixup
in the interactive rebase buffer - save the buffer and quit the editor
WalkThrough
The gps rebase
command is a convenience function that really runs an
interactive rebase of the stack on top of it's associated upstream, e.g.
git rebase -i --onto origin/main origin/main main
.
So to understanding how to combine patches with this command is really simply learning how to combine commits using git's interactive rebase.
Initial State
Lets start with the following patch stack (gps ls
).
2 1d16f9 Add function C
1 6c8104 Add function B
0 c6f715 Add function A
Mark Patches
Lets say that we need to combine the "Add function A" and "Add function B" patches together to make a logical patch.
To do this we start by running gps rebase
to kick off the interactive rebase.
It presents the following in our configured editor.
pick c6f7155 Add function A
pick 6c81046 Add function B
pick 1d16f98 Add function C
# Rebase a34b62b..1d16f98 onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
To combine a patch with the patch above it we simply mark it for squash
or
fixup
instead of it's default pick
. If you forget the marking you can always
reference the comment Git includes in the interactive rebase buffer. So in this
case we mark the "Add function B" patch with squash
as follows.
Note: The stack is inverted when presented in an interactive rebase. So the bottom most commit on the stack is actually the top most commit. This can be confusing until you get used to it.
pick c6f7155 Add function A
squash 6c81046 Add function B
pick 1d16f98 Add function C
# Rebase a34b62b..1d16f98 onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
Squash vs. Fixup
Git's interactive rebase provides two mechanisms of combining commits, squash
and fixup
. Squash combines two commits and presents you with the combined
commit messages in the rebase. Fixup on the other hand combines two commits
while throwing away the commit message of the commit marked withe fixup
.
Warning: You need to be careful when using fixup
to make sure that you
aren't accidentally throwing away a ps-id
from the commit message that you
want to keep around.
Choose a ps-id
When you are combining patches you want to be aware that depending on what
state your patches are in on, or the other, or both might have ps-id
's in
their commit message. The ps-id
in the commit message is how Git Patch Stack
uniquely identifies patches and tracks and associates state to them.
If you are in a situation where only one of the patches involved has a ps-id
you will likely want to keep that ps-id
in the resulting combined patch's
commit message.
If you are in a situation where multiple patches involved have ps-id
's you
will have to select one ps-id
to keep and use as the ps-id
for the new
combined patch.
Confirm Rebase
We then save and quit the editor and it combines the patches marked with
squash
or fixup
as part of the rebase. This leaves our patch stack as
follows (gps ls
).
1 8a6145 Add function C
0 ba2f4d Add function A & function B
Exactly the state we wanted it to be in.
Split a patch up
When you first discover the importance of logically structured Git commits (a.k.a. patches), which is by the way a core fundamental expectation & design characteristic of Git and how it is intended to be used.
It naturally leads to the question, "How do I split a patch up into multiple patches?" This is crucial because I know I am not going to get this right the first try. Below I present a simple contrived example so that you can learn the mechanics and process of doing this, as the mechanics & process don't change.
The other topic related to this which this guide does not cover is the process of taking code and splitting it up into logical chunks. This generally takes an understanding of the application architecture & the dependency relationship between the various elements.
So lets get to it.
TL;DR
For those who just want a quick reminder reference here is the TL;DR. For those who need a bit more context and detail through the walk through read the sections below.
gps rebase
- do an interactive rebase of the patch stack & mark the patch you want to split foredit
, it will drop you out into the shell at that patchgit reset HEAD^
- soft reset the patchgps add -p
- stage just the parts you want in the first patchgps c
- create the first patchgps add -p
- stage just the parts you want in the next patchgps c
- create the next patchgps rebase --continue
- continue the rebase to play the other commits on top of the new commits you created
Initial State
For this example lets assume that we have a Patch Stack that has the following patches.
2 aaaba1 Add first paragraph to README.md
1 e2e6a6 Add README.md subtitle & description
0 5a3538 Add Readme Title
As we can see from the first patch Add Readme Title
, it adds the title to the
README.
✔ gps show 0
commit 5a3538336bfa070648f4a221dd66f58d171e0372
tree ae384913fa2fd8ae03b4509817e78b8d6dc3677c
parent a34b62b61d1675073eb1df953f1cdfc01232cceb
author Drew De Ponte <cyphactor@gmail.com> 1656473449 -0400
committer Drew De Ponte <cyphactor@gmail.com> 1656473449 -0400
gpgsig -----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEZ4qmf6cBz6fWZrzoQcXSxuWvlEwFAmK7x28ACgkQQcXSxuWv
lEwG4gf/TAyEypLa/yzKcOO791jL1ew0jGS1ZzaJjQHRYkdNte3jZK/703wDrJX5
HGR/csZnpR7LV7FByeaCWzoXPtIiIr/lQ89xEHk+E8BpGKkcgxVGgF27BFJ/vAHh
LGEgbgbHNHDvesoyt85oN7trfvKbI7sm8p4NYL9rLb4rovy/2x5VMSOGvHjKL5Rn
eTCWv9AuQxVjvphZgoAa2RwPP9fWzToy8CVO2uwbXBptlzEGm9s7Rsz4J4Q3WwHk
FPAMDIlJlmpn6bPFlY7V5hnt/JT4Qkysk/nCisfQ8OwmQ7ZAhv0qwAKe4eor2OI6
+0sR5IHqUXkXv0EEuCr8Td3W2I9G9A==
=VlR/
-----END PGP SIGNATURE-----
Add Readme Title
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..db58698
--- /dev/null
+++ b/README.md
@@ -0,0 +1 @@
+# Readme Title
The second patch, Add README.md subtitle & description
, adds the
subtitle and the description as seen in the diff below.
✔ gps show 1
commit e2e6a6f470651b6a32940f57eba74582ac26fcca
tree 527436c6fdac33c0944aeb659f127ad12523a768
parent 5a3538336bfa070648f4a221dd66f58d171e0372
author Drew De Ponte <cyphactor@gmail.com> 1656473483 -0400
committer Drew De Ponte <cyphactor@gmail.com> 1656473483 -0400
gpgsig -----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEZ4qmf6cBz6fWZrzoQcXSxuWvlEwFAmK7x5YACgkQQcXSxuWv
lExNrggA2NEk3q8PqdyLcLN8wYN1QDo95HEbncjI1JrGKp8R+jbU5z5XxXM5LPgY
lyq0fsViJjEMswZajNWeM9BaQ0drNrs6GK5B/1cgjlcZkkC41zvNNcDQdYOYZr9c
pbriYf9VVXeX8l9FzjeZEGmdzCIZi1wKu6WovKFNx1bNHtTND3RQkrD8zWkHajKp
I6HsN62Wr7u4hZPa0tqXzyfqPQZ6cSXuikCzoZLbWha1Fq2q3aVyTsazW3zMsXm6
8jpEc+vge3GDDKVt+m9Tn1XrW//u13q+pyBJ44UxGsb4q6to7LetN+ttbAyCYvbZ
ws4Thz4bwjFvV9eONMbtR3VhnH4UDg==
=nFgy
-----END PGP SIGNATURE-----
Add README.md subtitle & description
diff --git a/README.md b/README.md
index db58698..b8c4d95 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,5 @@
# Readme Title
+
+## Subtitle
+
+Here is the description
The third patch, Add first paragraph to README.md
adds the first paragraph in
the diff below.
✔ gps show 2
commit aaaba12cb36e3239b90703a6777c9ee66a1ced14
tree f5ba2770179a826bb0fb63cf33dd2c6952812adf
parent e2e6a6f470651b6a32940f57eba74582ac26fcca
author Drew De Ponte <cyphactor@gmail.com> 1656473563 -0400
committer Drew De Ponte <cyphactor@gmail.com> 1656473563 -0400
gpgsig -----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEZ4qmf6cBz6fWZrzoQcXSxuWvlEwFAmK7x+MACgkQQcXSxuWv
lEwVBQgAnQl7Gzl6X4J0rz1eOVpLweO1tWCp7ScrzHiWStvNUuSNpJ8sayJAcD5E
n97CZ/olOUcC0qo+yWWOv5qSqt+zxPIgh3bCiN7FtMdGyNcr8wS6Ph4H6ZYKV0Re
J2jFvm7//8h6KxwUVvW7OPz+CFnVrSSNjF4etvmwDReyFo63TJZepczUCSsGUKvy
0PtXU2ixyvbLctVfj6XhSe/3pkAKv04H88/4lX8f+GYN7Sh/Ml4wQ6gXYSyv7S5X
vmmu7neYuwBQXF5HlCGJn72xJIEZJBKGoDkT3LdtKyiSVhdgl27Fb+9NbzkIs5e9
TN0sCffFdMiPRrVPwaeKLfAU7zbQEQ==
=kFns
-----END PGP SIGNATURE-----
Add first paragraph to README.md
diff --git a/README.md b/README.md
index b8c4d95..2ebe963 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,7 @@
## Subtitle
Here is the description
+
+## First Paragraph
+
+Here is the first paragraph.
Non-Logically Structured Patch
Looking at the patch summaries & the diffs themselves we can see that the
second commit, Add README.md subtitle & description
is actually
doing two logical things. First it is adding the subtitle. Secondly it is
adding the description to the README.
✔ gps show 1
commit e2e6a6f470651b6a32940f57eba74582ac26fcca
tree 527436c6fdac33c0944aeb659f127ad12523a768
parent 5a3538336bfa070648f4a221dd66f58d171e0372
author Drew De Ponte <cyphactor@gmail.com> 1656473483 -0400
committer Drew De Ponte <cyphactor@gmail.com> 1656473483 -0400
gpgsig -----BEGIN PGP SIGNATURE-----
iQEzBAABCAAdFiEEZ4qmf6cBz6fWZrzoQcXSxuWvlEwFAmK7x5YACgkQQcXSxuWv
lExNrggA2NEk3q8PqdyLcLN8wYN1QDo95HEbncjI1JrGKp8R+jbU5z5XxXM5LPgY
lyq0fsViJjEMswZajNWeM9BaQ0drNrs6GK5B/1cgjlcZkkC41zvNNcDQdYOYZr9c
pbriYf9VVXeX8l9FzjeZEGmdzCIZi1wKu6WovKFNx1bNHtTND3RQkrD8zWkHajKp
I6HsN62Wr7u4hZPa0tqXzyfqPQZ6cSXuikCzoZLbWha1Fq2q3aVyTsazW3zMsXm6
8jpEc+vge3GDDKVt+m9Tn1XrW//u13q+pyBJ44UxGsb4q6to7LetN+ttbAyCYvbZ
ws4Thz4bwjFvV9eONMbtR3VhnH4UDg==
=nFgy
-----END PGP SIGNATURE-----
Add README.md subtitle & description
diff --git a/README.md b/README.md
index db58698..b8c4d95 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,5 @@
# Readme Title
+
+## Subtitle
+
+Here is the description
Instead of the above what we really wanted to have was one patch that adds the subtitle and a separate patch that adds the description as two isolated logical chunks.
Edit Mode
To accomplish this we need to utilize an interactive rebase to enter "edit" mode in the correct place in the Patch Stack. In this particular case we want to rebase our Patch Stack.
gps rebase
This will bring up the following in your editor.
pick 5a35383 Add Readme Title
pick e2e6a6f Add README.md subtitle & description
pick aaaba12 Add first paragraph to README.md
# Rebase a34b62b..aaaba12 onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
In the interactive rebase buffer we can change the action for the middle patch
to edit
so it as follows.
pick 5a35383 Add Readme Title
edit e2e6a6f Add README.md subtitle & description
pick aaaba12 Add first paragraph to README.md
# Rebase a34b62b..aaaba12 onto a34b62b (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
# commit's log message, unless -C is used, in which case
# keep only this commit's message; -c is same as -C but
# opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# . create a merge commit using the original merge commit's
# . message (or the oneline, if no original merge commit was
# . specified); use -c <commit> to reword the commit message
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
When you save & quit the editor it will run the specified interactive rebase
commands. In this case pick (meaning keep) the first patch and then stop on
the second patch allowing for editing because we specified, edit
. When it
does this will drop you back to the console with a message similar to the
following:
Stopped at e2e6a6f... Add README.md subtitle & description
You can amend the commit now, with
git commit --amend '-S'
Once you are satisfied with your changes, run
git rebase --continue
Split the Patch
We want to split the changes currently held in this patch into multiple patches. To do this we need to reset the patch that we are currently on and then partially stage the changes back & create a patch, then stage the other part and create another patch, and then continue the rebase.
So first we have to reset just the patch that we are checked out on and we want to do a soft reset. So we do the following:
git reset HEAD^
Now if we run git diff
to see the now local unstaged changes we see the
following.
✔ git di
diff --git a/README.md b/README.md
index db58698..b8c4d95 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,5 @@
# Readme Title
+
+## Subtitle
+
+Here is the description
As we can see we now have the changes that add both the subtitle & the description locally.
So we first want to stage a patch with just the ## Subtitle
portion. To do
this we need to use gps add -p README.md
to do a partial stage of the README
files changes to just stage the ## Subtitle
portion. See this post, git add
patch won't split for
details on how to accomplish this. This should leave us with the following.
+
+Description of the README
If you run gps s
to check on things at this point it should look like this.
✔ gps s
interactive rebase in progress; onto a34b62b
Last commands done (2 commands done):
pick 5a35383 Add Readme Title
edit e2e6a6f Add README.md subtitle & description
Next command to do (1 remaining command):
pick aaaba12 Add first paragraph to README.md
(use "git rebase --edit-todo" to view and edit)
You are currently splitting a commit while rebasing branch 'main' on 'a34b62b'.
(Once your working directory is clean, run "git rebase --continue")
Changes to be committed:
(use "git restore --staged <file>..." to unstage)
modified: README.md
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: README.md
If we specifically check out the staged changes with git diff --staged
we get
the following.
✔ git diff --staged
diff --git a/README.md b/README.md
index db58698..b612c4b 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,3 @@
# Readme Title
+
+## Subtitle
This is exactly what we wanted. But lets make sure the unstaged changes also
represent what we want by running git diff
.
✔ git diff
diff --git a/README.md b/README.md
index b612c4b..b8c4d95 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,5 @@
# Readme Title
## Subtitle
+
+Here is the description
Yep looks like they do. So now we just need create the first of the two patches that will replace the patch we marked for edit. This can be done as follows.
gps c
When it opens the editor for the message we can give it a summary of
Add subtitle to README
.
From there we can stage the rest of the changes with gps add README.md
and
create the second patch with the following.
gps c
When it opens the editor for the message we can give it a summary of
Add description to README
.
Finish the Rebase
Now that the changes have been split up into separate patches like we wanted we now need to instruct it to finish the interactive rebase that we started with the edit. This is done as follows.
gps rebase --continue
Once it is complete if we checkout our Patch Stack it will look as follows.
✔ gps ls
3 023326 Add first paragraph to README.md
2 8a7fdc Add description to README
1 5aaf4c Add subtitle to README
0 5a3538 Add Readme Title
Exactly what we wanted!
Request Review of a patch
TL;DR
gps ls
- list out patches in patch stack to the patch index you want to request review ofgps rr <patch-index>
- request review of the patch identified by the given patch-index
WalkThrough
Get Patch Index
Before we can request review for a patch. We have to identify which patch we
want to request review of and get its associated index. The best way to do this
is to simply run gps ls
to list out the patches in the stack with their
associated indices and statuses. An example of this looks as follows.
✔ gps ls
1 8a6145 Add function C
0 ba2f4d Add function A & function B
Request Review
In the example above lets say we wanted to request review of the "Add function
A & function B" patch. We look and it's associated index is 0
. Therefore to
request review of this patch we simply run the following.
gps rr 0
The above kicks off the request review process for the "Add function A & function B" patch.
Isolation Verification
The fist step in the request review process is to run the isolation verification if the configuration for it is enabled. For specifics on the configuration checkout the Tool - Configuration chapter.
Isolation Verification is a process where a temporary branch is created that is based on the upstream base and the patch is cherry-picked into this branch. This verifies that at least from a Git perspective the patch is independent enough to successfully be cherry-picked on top of the upstream base.
This however does not verify the patch is truly independent because it doesn't
address code dependencies. To address this the Isolation Verification process
supports the isolate_post_checkout
hook. This is a hook that if present gets
executed after cherry-picking the patch into the temporary branch and checking
that branch out. It allows you to provide an isolate_post_checkout
hook
script that can run linting, test suite, build process, etc. which can help
verify that your patch is actually independent. Details on the hook can be
found in the Tool - Hooks chapter.
Request Review Branch Creation & Sync
Assuming that the isolation verification is successful it then moves onto
creating the request review branch (e.g. ps/rr/some-patch-summary
) based on
the upstream base, cherry-picking the patch into it, and then syncing that
branch with the remote.
If the isolation verification fails the command exits aborting the request review process.
Request Review Post Sync Hook
After syncing the request review branch if the request_review_post_sync
hook
is configured it will be executed. To find details about this hook and others
check out the Tool - Hooks chapter.
This hook is commonly use to automate the creation of pull requests or create a
patch email and send it to a mailing list so that all you have to do is a gps rr <patch-index>
and it will take care of the entirety of requesting review of
that patch.
Update a previously Requested Review
Generally when you request review of a patch you end up getting some feedback and need to modify the patch. This is done using all the techniques described in the other guides. Once the patch has been updated you want to update the request review with the new version of the patch. This is done by just requesting review of the patch again as follows.
gps rr <patch-index>
Thats it!
Absorb Changes
TL;DR
- discover changes were pushed up to your review branch
git cherry-pick <pushed-up-commit-sha>
- cherry-pick the changes down into your patch stackgps rebase
w/fixup/squash
- absorb the changegps rr
orgps branch
- re-request review of updated patch(es)
WalkThrough
In this example let's assume that we have a simple patch stack consisting of a
single patch. If we run gps ls
it looks as follows because we have already
requested review of our patch.
✔ gps ls
0 rr 0a301d Add foo() function
Discover Changes
We soon discover that one of the reviewers of our pull request has pushed changes up to review branch. This often happens when the reviewer is trying to give you an example or when they are trying to help you out with some changes.
We don't want just ignore their changes and do a gps rr
again as we would be
throwing their changes away. We also don't want to manually implement what they
have already done as that is more work.
Cherry-Pick Changes
So we git cherry-pick 5bca34
to cherry-pick the change down into our patch
stack. Now if we run gps ls
our patch stack looks as follows.
✔ gps ls
1 8ce58e Add bar() function
0 rr 0a301d Add foo() function
Absorb Changes
Now we simply need to absorb the change in some way. It could be that it
logically fits with our patch and therefore we fixup/squash
it with our patch
using gps rebase
.
Or, it could be that it makes sense as a separate patch.
Re-request Review
If we did the fixup/squash
approach we simply need to re-request review of
our now updated patch with gps rr
.
If we decided to keep it separate request review for it individually with gps rr
and gps rr
our originally patch and add a comment on the PR to explicitly
communicate the decision to keep them separate.
The last option would be that we chose to keep them separate but as part of a
patch series. In which case we would use the gps branch -p 0 1
command
probably also with the -n <branch-name>
switch to have it update the existing
PR branch.
That is it! We just absorbed the change that was pushed up.
Integrate a patch
TL;DR
gps ls
- list out patches in patch stack to the patch index you want to integrategps int <patch-index>
- integrate the patch identified by the given patch-index
WalkThrough
Get Patch Index
Before we can integrate a patch. We have to identify which patch we want to
integrate and get its associated index. The best way to do this is to simply
run gps ls
to list out the patches in the stack with their associated indices
and statuses. An example of this looks as follows.
✔ gps ls
1 8ce58e Add bar() function
0 0a301d Add foo() function
Integrate
In the example above lets say we wanted to integrate the Add foo() function
patch. We look and it's associated index is 0
and then run the following to
trigger the integration.
gps int 0
Safety Check
If we tried to integrate this patch with gps int 0
it would fail because as a
safety precaution it makes sure that the patch has previously been synced with
gps sync
or requested review with gps rr
and in this example thus far the
patch hasn't been synced.
This can be overriden with the -f
option as gps int -f 0
if you don't want
to have to sync or request review of a patch before integrating.
Isolation Verification
The fist step in the integration process is to run the isolation verification if the configuration for it is enabled. For specifics on the configuration checkout the Tool - Configuration chapter.
Isolation Verification is a process where a temporary branch is created that is based on the upstream base and the patch is cherry-picked into this branch. This verifies that at least from a Git perspective the patch is independent enough to successfully be cherry-picked on top of the upstream base.
This however does not verify the patch is truly independent because it doesn't
address code dependencies. To address this the Isolation Verification process
supports the isolate_post_checkout
hook. This is a hook that if present gets
executed after cherry-picking the patch into the temporary branch and checking
that branch out. It allows you to provide an isolate_post_checkout
hook
script that can run linting, test suite, build process, etc. which can help
verify that your patch is actually independent. Details on the hook can be
found in the Tool - Hooks chapter.
Prompt for Reassurance
Assuming the isolation verificatino was successful it then moves onto prompting the user for reassurance that they want to integrate the patch. This presents the details for the patch including the diff and then prompts the user to enter yes/no indicating if they want to continue with the integration.
Note: This feature can be disabled/enabled via the configuration. More details can be found in the Tool - Configuration chapter.
Patches Differ Check
If the user responded yes
to the prompt for reassurance or has it disabled
the next step in the integrate process is to check if the diff of the patch in
the patch stack matches the diff of the patch on the remote branch. This is a
precautionary check to make sure you don't accidentally integrate a patch that
is different than you think.
Note: gps ls
will show you if your patches differ.
Patch is Behind Check
Then it checks to see if the patch in the patch stack is behind the remote patch. This could occur if someone pushed up a change to the remote branch or force pushed it. This is useful to know so that you can address the difference and get the patch in your patch stack in sync with the remote prior to integrating.
Note: gps ls
will show you if your patch is behind.
Actually Integrate
Assuming the above checks pass the next step in the integration process is
doing the actual integration of the patch into upstream. At a Git level this is
done similar to the following if your stack was on main
.
git push origin origin/ps/rr/whatever-branch:main
Update State
After doing the actual integration into the upstream branch it then updates the local state store to indicate that the patch has been integrated.
Cleanup Branches
Beyond updating the local state it then cleans up the local and remote branches
that were utilized during the request review and integration process. This
branch cleanup can be prevented by passing the -k
or --keep-branch
option
to the gps int
command.
List
Now that the integration is complete we can check on the status of things again
with gps ls
.
✔ gps ls
1 8ce58e Add bar() function
0 int 0a301d Add foo() function
Collapse Integrated Patches
Now that patch is integrated so you likely don't want it in your stack of patches anymore. To collapse integrated patches out of the stack we simply perform a pull as follows.
gps pull
This fetch the latest changes from upstream and rebase main
on top of its
upstream, in this case origin/main
. During this process it detects patches
that are already in origin/main
and collapses them out of the patch stack.
Pull after Integrate
If the integration process completed successfully and the
integrate.pull_after_integrate
configuration value is set to true
then it
will initiate a gps pull
for you.
This is useful if you don't care about seeing the int
state of patches and
just want them to collapse out of your stack as soon as they are integrated.
See the Tool - Configuration chapter for more
details.
That is how a patch is integrated.
Let's Talk
We love to chat especially about Git Patch Stack. To facilitate that we support getting ahold of us in a number of a different ways.
Diff Stacking
What is it in general?
Why not stack branches instead of patches?
Well there are two primary which I think are very important reasons to choose stacking patches over stacking branches.
- stacking patches removes a layer of abstraction simplifying how you think about things as well as how you do them
This simplification and constraint is actually more valuable than what you might think. The stacking of patches and having to deal with the concepts of patch dependence/independence is an impetus driving the developer to truly understand their application architecture and write patches in relation to it. This is why I would argue that stacking patches has a more natural alignment with the application architecture than stacking branches which are generally associated with features and not the application architecture.