Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
A basic git workflow for centralized repositories.

Basic Workflow

Description

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, and git push.
    • Pull from collaborators using git pull --rebase and git push.
    • Pull from parent using git pull origin <parent> and git push.
  • 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.
    • 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.

Workflow summary

  1. Cut a feature branch from a "parent" branch like master or a long-running integration branch.
  2. Commit work into that feature branch
  3. While working, integrate changes from two places:
    1. Those made by others on the same feature branch, whenever necessary, and
    2. Those added to the parent branch, periodically, but remember to
    3. Push immediately after performing either of those integrations.
  4. 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.

Checkout (create or switch to) a feature branch (feature/foo)

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

a new branch based on parent

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.

a new branch "here"

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.

an existing branch

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

Iterate this process until your code is complete.

1. stage, commit and push local changes

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

2. pull --rebase collaborators' commits

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

3. pull origin <parent> to avoid diverging from parent

# 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

Integrate

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

Problems

My feature is based on or requires code from another feature branch.

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.

There's junk commits in my branch's history that I want to clean up with a rebase before I merge!

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

Initial configuration

Only push the branch you're working on

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

Undo configuration from previous rebase-based workflows

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.

To do

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 to running in production. we could have a deployed branch that represents the state of code currently in production.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment