Different uses of git will have a different optimal workflow. This document describes a git workflow with the following characteristics:
-
It is a centralized workflow, meaning multiple developers all push to a shared repository. (As opposed to a fork-and-pull-request Github-style workflow.)
-
It is safe to use when collaborating on a feature branch with others, but requires no workflow changes when working alone. (So there's only one set of steps to remember.)
-
Some git users rely heavily on rebases, using them for any and all conflicts. This is an unhealthy coping mechanism born of past git merge trauma. This workflow relies heavily on merges, using rebase only when it is safe to do so (with commits that have yet to be pushed).
-
push --force
is never needed (and may be disallowed by the remote repository.) -
During your main development iteration, there are only three steps to remember, and with some judicious configuration, no additional arguments need to be specified:
- Create commits using
git add
,git commit
, andgit push
. - Pull from collaborators using
git pull --rebase
andgit push
. - Pull from parent using
git pull origin <parent>
andgit push
.
- Create commits using
-
We have some preferences about the history graph:
- All commits in the first-parent history of major branches represent "complete" features -- this may mean "working," "tested," "deployable," or possibly "have been deployed to production."
- Some prefer
git merge --squash
to keep history simple, others prefer to keep all commits to show the development of a feature. You may use either method; we only insist that feature-in-progress commits stay in branches where we can hide them from history diagrams when desired.
- Some prefer
- When needed, we prefer to undo an entire feature, or a whole set of features that are part of an integration, by reverting at most single merge commit.
- This avoids having to carefully track down whole commit ranges or having to create a revert commit for each of several commits in a feature.
- All commits in the first-parent history of major branches represent "complete" features -- this may mean "working," "tested," "deployable," or possibly "have been deployed to production."
- Cut a feature branch from a "parent" branch like
master
or a long-running integration branch. - Commit work into that feature branch
- While working, integrate changes from two places:
- Those made by others on the same feature branch, whenever necessary, and
- Those added to the parent branch, periodically, but remember to
- Push immediately after performing either of those integrations.
- Ask integration manager to merge your feature into its parent branch.
If you're good at git there's probably some tweaks you prefer; it's probably okay to use them as long as the results are the same: merge commits to bring feature branches into integration branches.
Use one of the following steps to get started on a feature branch. Everywhere in this document, I call this "feature/foo" as an example. Swap that out with your feature branch name.
Features are started at, and later merge back into a long-running integration branch, that we call its parent branch. Simpler workflows have only one possible parent: master
. Even if you begin your branch at (or "base your branch upon") another feature branch, your parent remains the long-running integration branch that you will eventually merge into.
Note: what this document calls "parent" is different from what git calls "upstream".
Create a new feature branch of development from the tip of parent:
git fetch
git checkout -b feature/foo origin/<parent>
git push -u
- Always use **origin/**parent here, not simply parent.
push -u
is an easy way to get "upstream tracking" set up correctly.
Create a new branch of development at the current commit (HEAD):
git checkout -b feature/foo
git push -u
push -u
is an easy way to get the "upstream tracking" set up correctly.
Share an existing feature branch with another developer:
git fetch
git checkout feature/foo
- If you don't already have a feature/foo branch, then this is equivalent to: git checkout origin/feature/foo -b feature/foo
- If you already have a feature/foo branch, this just switches to it.
Iterate this process until your code is complete.
Make some changes, get them staged and committed to your local git clone. Occasionally, push them up to the remote repository.
# edit files...
git add <file> <file> ...
git commit
# loop until complete
Once you have some commits ready, push them into the remote repository.
git push
# If git complains: go to (2) pull --rebase collaborator's commits
git push
is equivalent to:git push origin feature/foo
- It's good to do
push
often since that means you have a remote backup in case of a disaster, and others can see and collaborate on your work. - On the other hand, once a commit is pushed, it can't be (easily) rewritten, so if you habitually reorganize or clean up your commits after the fact you might want to push less frequently. (More on this later.)
# first, commit (or stash) all your local changes. Then,
git pull --rebase
# resolve any conflicts
git push
# If git complains: repeat (2) pull --rebase collaborator's commits
- This will cause any commits that have not been pushed to be rebased on the updated branch.
- Using --rebase is not strictly necessary here, but doing so will keep the feature branch history cleaner.
- Git will force you to take this step whenever someone else changes your branch, by forbidding you to push until you do.
- If nobody else ever changes your branch, this command is idempotent; a no-op. It's safe to run just out of habit.
- Don't forget to push at the end of this step!
- Otherwise you'll end up either re-resolving the same conflicts, or building a long chain of merge commits.
# Ensure you've recently done step (2) pull --rebase collaborator's commits
git pull origin <parent>
# resolve any conflicts
git push
# If git complains: repeat from `git pull origin <parent>`
- Never use --rebase here. If you do accidentally, git will forbid your push.
- Always do step (2) before this step.
- Don't forget to push at the end of this step!
- Otherwise the next time you do step (2) pull --rebase your collaborator's commits, it could cause problems.
- How often you do this step is a tradeoff: more often means smaller conflicts to resolve, but messy history; less often means cleaner history but potentially difficult conflict resolution.
- You should do this at least once to ensure a clean merge with parent.
- If time passes before your branch is merged with parent, you may have to repeat this step to resolve any conflicts with new code added to parent.
When your feature branch is complete, and if you're allowed to perform the merge into it:
# Ensure you've recently done step (4) pull origin <parent>
git checkout <parent>
git pull --no-ff --no-rebase origin feature/foo
git push
Using --no-ff causes all commits along the "first-parent" ancestry in parent to be merge commits that bring in feature branches. This means:
- whole features may be reverted with a single revert commit
- individual work-in-progress commits remain in the history
- but, we can exclude them easily with
git log --first-parent
Begin as usual, from the dependent branch's parent. Then, pull from the other feature branch the same way you would pull from parent:
`git pull origin <other feature branch>`
You may want to repeat that step, as you would with parent, whenever that branch changes, to avoid future conflicts.
Your repo may not allow the force-push necessary to push a branch after being rebased. But even if it does, if you have any collaborators sharing the branch, this is dangerous. You must communicate and coordinate with them to avoid losing work:
- "I'm going to perform a rebase. Make sure all your work is pushed to the branch!"
- Perform step (2) to ensure you have the most recent copy of their work
- Perform the rebase and force-push the result
- "I'm done rebasing, do a
git fetch; git reset --hard origin/<branch>
"
If this situation happens frequently, consider:
- develop on a throwaway branch like "tmp-feature/foo-1" until you are getting close to completion.
- Then, rebase to clean up your branch and
- push it as the finalized branch name: git push origin HEAD:feature/foo
- delete the messy temporary upstream branch, if it exists: git push origin :tmp-feature/foo-1
This step is needed only for git prior to version 2.0.
Beginning with git 2.0, its push.default
defaults to simple
which does what we want and expect.
git config --global push.default current
This configures git to always push the current branch to update a branch with the same name on the receiving end. Git's default of "matching" can cause unintentional modification of other branches.
Note: The use of --global sets this as the default for every repo, but it's a good idea to check any pre-existing repos to ensure it's not overridden there:
git config push.default
If this is a pre-existing clone, you may need to unset some settings:
git config --unset branch.master.rebase
git config --unset branch.feature/foo.rebase
git config --unset branch.autosetupmerge
git config --unset branch.autosetuprebase
git config --unset pull.rebase
Note: These commands are only sometimes needed in existing repos, and never need to be run on fresh clones.
Questions:
- How do I build (and manage) an integration branch containing other branches?
Clean up:
- Move some of the notes and asides into footnotes or something, make the document less wordy
- Improve "variations"
Automation:
- Githooks to guide folks about next-steps
- Local githooks to check that the user is up to date with upstream before they attempt to pull origin master.
- Git subcommand scripts or aliases to simplify command-line arguments for pull; enable "git p" to always do the right thing: check for unpushed merge commits; pull --rebase; figure out parent; check if parent should be merged; (optionally, according to policy) merge it
master
does not always equate torunning in production
. we could have adeployed
branch that represents the state of code currently in production.