Git And Collaboration Workflows

Git Branches, Commits, And Remotes

A Git branch gives a change a name and keeps its commits separate from the shared base until the work is reviewed or integrated. In everyday PHP work, a branch might contain one bug fix, dependency update, migration, or small feature.

A branch is not a copy of the whole repository. It is a movable reference to a commit. New commits advance the currently checked-out branch.

Start From The Shared Base

Before starting work, check the current state and update your view of the remote repository.

git status --short
git fetch origin
git switch main

git fetch origin downloads remote commits and updates remote-tracking references such as origin/main. It does not merge them into your current branch or rewrite your working files.

Integrate origin/main using the repository's documented merge, rebase, or pull policy. Do not guess when a team has a specific history convention.

Create A Focused Branch

Create and switch to a branch with git switch -c.

git switch -c fix/empty-product-name

Useful branch names describe the change rather than the developer:

fix/empty-product-name
feature/order-export
chore/update-phpstan

Keep branches short-lived. A branch that remains open for weeks accumulates more integration risk and becomes harder to review.

Make Focused Commits

After editing, inspect the changed files and diff.

git status --short
git diff

Stage only the files that belong to the commit, then inspect the staged diff.

git add src/ProductName.php tests/ProductNameTest.php
git diff --cached
git commit -m "Handle missing product names"

A useful commit represents one coherent change and leaves the repository in a reviewable state. The message should explain the outcome in imperative form rather than restating a filename.

Avoid mixing a behavior fix with unrelated formatting, dependency upgrades, or generated files. Separate concerns make review and rollback easier.

Understand Remotes

A remote is a named repository location. origin is the conventional name created by git clone, but it is not special to Git and projects may use other names.

git remote -v
git branch -vv

git remote -v shows fetch and push URLs. git branch -vv shows local branches and their upstream tracking relationships.

Common arrangements include:

  • one origin remote for a repository you can push to
  • origin for your fork and upstream for the source repository
  • separate remotes for migration or release workflows

Always check before pushing. A familiar remote name can still point to the wrong repository.

Push And Set The Upstream

The first push can create the remote branch and set its upstream:

git push --set-upstream origin fix/empty-product-name

The shorter -u form is equivalent:

git push -u origin fix/empty-product-name

After the tracking relationship exists, plain git push and git pull know which remote branch is associated with the current local branch. The repository's pull strategy still determines whether remote work is merged, rebased, or accepted only as a fast-forward.

Fetch Before Interpreting Remote State

Remote-tracking references are local snapshots. They change when you fetch or push.

git fetch origin
git log --oneline --decorate --graph --all -12

Without fetching, origin/main may be stale. This is why a developer can incorrectly believe a branch is current while the server has newer commits.

Do Not Rewrite Shared Work Casually

Amending or rebasing local commits can make a branch easier to review before it is shared. Rewriting commits other people may already use changes commit identities and can disrupt their work.

If a shared branch must be force-updated, follow team policy and prefer the guarded form:

git push --force-with-lease

--force-with-lease checks that the remote branch still points where your local repository expects. It is safer than unconditional --force, but it still rewrites published history and requires deliberate use.

A Practical Daily Loop

fetch -> switch base -> create branch -> edit -> inspect -> test
-> stage intentionally -> inspect staged diff -> commit -> push -> review

Repeat with small commits when the task genuinely contains several coherent steps. Do not create many commits merely to record every save.

What To Remember

Branches name lines of work, commits record focused snapshots, and remotes connect the local repository to other copies. Fetch before reasoning about remote state, inspect before committing, set upstream tracking on first push, and avoid rewriting shared history without agreement.

Before moving on, make sure you can explain the difference between main, origin/main, and origin, and why git fetch is safer than assuming your remote-tracking references are current.

Commits Are Snapshots With Parents

Git stores commits as snapshots linked to parent commits. A branch name is a movable pointer to one commit. main, feature/invoices, and bugfix/login are not separate copies of the repository; they are names pointing into one commit graph. Understanding that graph makes branch and remote behavior much less mysterious.

Your working tree is the files on disk. The index, or staging area, is the snapshot you are preparing. A commit records the staged content and points back to its parent. A branch moves when you create a new commit while it is checked out. If you switch branches with local changes, Git must decide whether those changes can remain safely in the working tree.

Local And Remote Names Are Different

A remote such as origin is a configured repository URL plus a set of remote-tracking refs. origin/main is your local record of where the remote's main was the last time you fetched. It is not updated continuously. git fetch updates remote-tracking refs; it does not merge them into your current branch.

git pull is fetch plus an integration step, usually merge or rebase depending on configuration. Beginners often treat pull as a harmless refresh, but it can change the current branch. Learn to fetch first when you want to inspect what changed before deciding how to integrate.

A push asks the remote to move one of its branch names. The remote may reject the push if it is not a fast-forward, if branch protection applies, or if required checks are missing. That rejection protects shared history from being overwritten accidentally.

Branches Should Carry Purpose

A good branch name communicates intent and scope: feature/order-refunds, fix/session-rotation, or chore/phpstan-baseline. The name is not permanent documentation, but it helps reviewers and CI dashboards. Avoid overloading one branch with unrelated work because each additional purpose makes review, rollback, and conflict resolution harder.

Keep feature branches short-lived when possible. Long-lived branches drift from the base branch, accumulate conflicts, and hide integration problems. If the work is large, split it into stacked or incremental branches with clear dependencies rather than one branch that cannot be reviewed until everything is done.

Fast-Forward, Merge, And Rebase

A fast-forward moves a branch name forward when no divergent commits exist. A merge creates a new commit with two parents, preserving both lines of history. A rebase copies local commits onto a new base, creating new commit IDs and a linear story. None is universally correct; the team should choose based on review workflow, audit needs, and shared-history policy.

Do not rebase commits that other people are already basing work on unless the team has explicitly agreed to that workflow. Rewriting shared history forces collaborators to reconcile their local graph. For your own unshared branch, rebase can be a clean way to incorporate the latest base or edit local commits before review.

Upstream Tracking

A local branch can track an upstream branch, such as origin/feature/order-refunds. Tracking lets Git know where to push and what to compare for ahead/behind status. The relationship is convenience, not ownership. You can change upstreams, push to a differently named remote branch, or delete a remote branch after merge.

Use git branch -vv to inspect tracking relationships. If a push goes to the wrong branch, stop and inspect configuration rather than repeatedly trying commands. Many collaboration mistakes are simply unclear branch names and upstreams.

Recovery And Inspection

Before risky branch operations, inspect the graph. git log --oneline --graph --decorate --all shows how names relate. git status shows local changes. git reflog records where local branch names and HEAD have been, which often makes recovery possible after a mistaken reset or rebase.

If you accidentally commit on the wrong branch, create the intended branch at that commit or cherry-pick the commit to the right branch before deleting anything. Avoid destructive cleanup until the commit you care about has another name pointing to it.

Team Policy

Branch policy should answer practical questions: Who may push to main? Are pull requests required? Are merge commits, squash merges, or rebases preferred? How long are release branches supported? Who deletes merged branches? How are hotfixes brought back into active development?

A written policy reduces surprise. It also makes automation easier because CI, branch protection, and merge queues can enforce the workflow the team actually wants.

After this lesson, you should be able to explain branches as movable names in a commit graph, distinguish local branches from remote-tracking refs, integrate remote changes deliberately, set upstreams, avoid unsafe history rewrites, and recover from common branch mistakes.

Inspecting The Graph During Collaboration

Graph inspection prevents many branch mistakes. Before pushing, compare your branch with its upstream using git status -sb, git log --oneline --decorate --graph --max-count=20, and a diff against the merge base. The merge base is the common ancestor from which your branch diverged; it is often the right point for reviewing only your work.

If your branch is unexpectedly behind, fetch and inspect before integrating. If it is unexpectedly ahead by commits you do not recognize, stop and identify them before pushing. A clear graph turns Git from a set of memorized commands into a visible history model.

Practice

Task: Prepare A Review Branch
  • fetch the latest state from origin
  • switch to main
  • update local main under a repository policy that permits only a fast-forward from origin/main
  • create fix/empty-product-name
  • stage src/ProductName.php and tests/ProductNameTest.php
  • inspect the staged diff
  • commit with the message Handle missing product names
  • push the branch to origin and establish upstream tracking
  • show the final branch and tracking information

Explain which command downloads remote history without merging it and why the staged diff should be read before committing.

Show solution
git fetch origin
git switch main
git merge --ff-only origin/main
git switch -c fix/empty-product-name
git add src/ProductName.php tests/ProductNameTest.php
git diff --cached
git commit -m "Handle missing product names"
git push -u origin fix/empty-product-name
git branch -vv

git fetch origin downloads objects and updates remote-tracking references without merging them into the checked-out branch. In this scenario, git merge --ff-only origin/main advances local main only when no merge commit or history rewrite is required. A repository that mandates rebase, signed merge commits, or a different base-update policy should use that documented procedure instead.

git diff --cached shows exactly what the next commit will contain. Reading it catches unrelated files, debug output, secrets, or missing tests before the snapshot is recorded and pushed for review.