A safe workflow from start to finish
The interactive lesson needs JavaScript. The text below is the complete lesson.
After this lesson you can
- carry out a whole day in a team: update
main, work on a branch, read the diff, commit and push apply - handle a rejected push by fetching, integrating and pushing again, without
--forceapply - explain why rebasing is safe for commits only you have, but not for commits you have already shared with others understand
- make a risky step reversible: check status and how fresh
origin/mainis, and create a branch where you stand before you move a branch apply
Understand: The loop you repeat every day
Before a command that changes something, ask yourself: where am I, what does git status say, and what do I actually know about origin? In the figure, Ada has pushed a new commit. Your origin/main does not know about it until you fetch.
The day starts by updating main. Then you create a branch for the task, make small commits and read the diff before each one.
Push your branch early. Then there is a copy on the server, and others can see the work and give feedback. When it is done, you integrate it into main and push main.
Sometimes the server rejects your push. Someone else pushed first, here Ada with commit D, and your commit E is not a continuation of the server's history. The rejection is information, not something to force away.
The answer is always the same: fetch, integrate and push again. git pull --rebase fetches Ada's commit and replays your unpushed commit E on top of it. The copy gets a new ID, and the push becomes an ordinary fast-forward.
Only rebase commits nobody else has. And never use --force on a shared branch to get past a rejection, because that removes other people's commits from the branch on the server. If you must publish rewritten history on your own branch, agree on it and use --force-with-lease.
Keep this checklist. Before a command that changes something, ask: Where am I? What is in the three areas? How fresh is origin/main? And how do I get back? If you are unsure about the last one, first create a branch where you stand.
Watch: Morning: from updated main to published branch
A new day. Ada pushed a commit last night. Before we start, we update main. We have no commits of our own here. What does git pull --rebase do then?
$ git pull --rebase
Today's task is a contact page. It gets a branch of its own.
$ git switch -c contact-page
We write the contact page. One small, coherent change.
$ echo "Email: team@example.com" > contact.txt
We stage the file for the commit.
$ git add contact.txt
Before committing, we read exactly what goes in. git diff --staged shows the staging area against the last commit. This is where you catch typos, secrets and files that do not belong.
$ git diff --staged
The diff is what we wanted, so we commit.
$ git commit -m "Add contact page"
We push the branch and set its upstream with -u. Watch the graph: origin/contact-page appears. The server has a copy, and Ada can see the work.
$ git push -u origin contact-page
Tasks
- Update
mainwith what is on origin. - Create and switch to a branch
contact-page. - Read exactly what will go into the next commit.
- Commit the change with a descriptive message.
- Publish your branch on origin, with an upstream.
Watch: Afternoon: rejected push and integration
We are on contact-page and add a phone number in a new commit. What we do not know is that Ada has pushed a commit to the same branch.
$ echo "Phone: 555 0100" >> contact.txt && git commit -am "Add phone number"
We try to push. What does the server answer?
$ git push
We fetch Ada's commit and replay our unpushed commit on top of it. That is safe, because nobody else has seen our commit yet. In the graph it gets a new ID and now sits after Ada's.
$ git pull --rebase
Now our history continues the server's, and the push goes through without force.
$ git push
The contact page is done and should go into main. We switch there.
$ git switch main
Fetch before you integrate. Nobody has pushed to main here, so Git says everything is up to date. We only know that now.
$ git pull --rebase
main is an ancestor of contact-page, so the merge is a fast-forward, and main moves forward to the newest commit. In a team with a protected main, the hosting service does this step when the pull request is merged, with the strategy the team has chosen.
$ git merge contact-page
Finally we push main. In the graph, main, origin/main and origin/contact-page are on the same commit as contact-page. No force, and no work lost.
$ git push
Tasks
- Try to push your commit, and read what the server answers.
- Fetch Ada's work and put your unpushed commit on top of it.
- Push the branch again.
- Make sure
mainis up to date with origin before you integrate. - Integrate
contact-pageintomain. - Publish
main.
Practise: Transfer: a messy morning
Yesterday you committed a FAQ draft straight onto main, and it is not pushed. Ada has pushed to main since you last fetched. Tidy up and deliver, using everything you have learned. After this you are ready for the final assignment, From solo to team.
Tasks
- Move the FAQ commit onto a new branch
faq, and setmainback so it has no unpushed commits. - Publish
faqon origin, so there is a copy on the server. - Get
faqintomainon origin. Ada has pushed tomainin the meantime. Do not overwrite her work, and do not use force.
Check yourself: Recall it
git push is rejected with the message “fetch first”. What do you do?
git push --force, since it is my commit after all- Fetch, integrate what is new and push again
- Delete the clone and clone again
You are on a feature branch and run git pull --rebase origin main. When is that safe?
- When the commits on the branch exist only on your machine
- Always, because it only touches commits that are not on
mainyet - Only when nobody has pushed to
mainsince you last fetched
You have run git add. Which command shows exactly what goes into the next commit?
git diffgit diff --stagedgit log -p
The graph in your tool looks tidy. Is that enough to push safely?
- Yes, the graph shows the whole state
- No. Also check status, and fetch first, because
origin/mainmay be out of date - Yes, as long as
git statussays “up to date with origin/main”
Review from “Undo shared history”: yesterday you pushed a commit to main that broke the build. Ada has already fetched it. How do you undo it?
git reset --hard HEAD~1and thengit push --force-with-leasegit restore ., which takes you back to before the commitgit reverton the commit, and then an ordinarygit push
Common misconceptions
- “A rejected push is an error to force through.” The rejection protects other people's commits. Fetch, integrate and push again.
- “
git pull --rebaseis always safe.” It rewrites your local commits that the upstream does not have. If you have already shared them, for example pushed them to another branch, others end up with two versions of the same work. - “A tidy graph means all is well.” The graph does not show uncommitted changes, and
origin/mainis only as fresh as your last fetch.
Read more
Four questions before every command that changes something
- Where am I? Which repository, which branch, is HEAD detached? The first line of
git status -sbanswers. - What is in the three areas? What is modified, what is staged, what is untracked? A tidy graph does not show uncommitted work.
- What do I actually know about origin?
origin/mainis only as fresh as your last fetch. Rungit fetchbefore you decide anything that depends on the server. - What will this command change, and how do I get back? If the answer involves moving a branch or rewriting history, first create a branch where you stand:
git branch backup-before-rebase. It costs nothing and keeps the committed history within reach. Save uncommitted work first, with a commit or a stash.
The daily loop
git switch main
git pull --rebase
git switch -c contact-page
git add contact.txt
git diff --staged
git commit
git push -u origin contact-page
Keep commits small and coherent: one reason per commit. Write the messages as “Good commits” describes, and read git diff --staged before every commit; that is where you catch debug output, secrets and files that do not belong. Push your branch early so the work exists in more than one place and others can review it. Before integrating, update again and test the combined result. In a team with a protected main, the integration itself happens through a reviewed pull request; without one, you merge locally and push main without force.
A rejected push is information
When the server rejects a push, it is protecting commits you do not have:
$ git push
To ../origin.git
! [rejected] contact-page -> contact-page (fetch first)
error: failed to push some refs to '../origin.git'
hint: Updates were rejected because the remote contains work that you do not
hint: have locally. This is usually caused by another repository pushing to
hint: the same ref. If you want to integrate the remote changes, use
hint: 'git pull' before pushing again.
...
"fetch first" means the server has commits you have not even fetched. If you have fetched but not integrated, it says "non-fast-forward" instead. The answer is the same: fetch, look at what arrived with git log --oneline HEAD..origin/contact-page, integrate with git pull --rebase (for unpushed commits of your own) or git pull --no-rebase (to merge), run the tests, and push again. Never use --force to get past this message on a shared branch. If you have rewritten your own branch and must publish it, agree on it and use git push --force-with-lease; its weak spots are covered in “Rebase”. Never force-push main.
Show that it really arrived
"It works on my machine" and "I pushed" are claims. Back them with evidence:
$ git rev-parse HEAD
59c03df2da960ea31603edf622cfc25bfa0584c3
$ git ls-remote origin refs/heads/contact-page
59c03df2da960ea31603edf622cfc25bfa0584c3 refs/heads/contact-page
The same ID locally and on the server proves that the commit you tested is the one that was published. git log --oneline --graph --decorate --all shows the history around it, and git status that nothing is left uncommitted.
A team agreement template
Copy this into CONTRIBUTING.md and fill it in together. The point is not the specific answers, but that everyone knows them.
Branches
mainis always releasable. Nobody commits or force-pushes tomaindirectly.- One branch per task, named type/short-description, e.g. feature/contact-page.
Commits
- Small and coherent. Subject <= 50 chars, imperative. Body explains why.
- Never commit secrets, build output or personal editor files (
.gitignore).
Integration
- Update
mainand run the tests before integrating. - Review: at least one other person reads the diff before merge.
- Strategy: rebase your own unpushed commits; integrate feature branches one agreed way (merge commit, squash or rebase).
Shared history
- Never rewrite commits others have. Undo shared commits with
git revert. Force only on your own branch, with--force-with-lease, after telling anyone else using it.
When something goes wrong
- Stop. Run
git statusandgit log. Create a backup branch before experimenting. Commits that look lost are usually still ingit reflog. - Ask in the team channel before any command you cannot explain.
Next: the final challenge
The next lesson is the final challenge. There you work without a demo: collaborate through a shared remote, resolve a conflict, justify a merge or a rebase, recover from a mistake and deliver with evidence. Use the four questions above at every step.
Per-Arne's Git Good!