Git And Collaboration Workflows
Resolving Git Merge Conflicts
A merge conflict means Git cannot safely decide how to combine changes. It is not proof that either branch is wrong. It is a request for a developer to understand the intended final behavior.
Conflicts can occur during merge, rebase, cherry-pick, pull, or branch updates performed by hosting tools. The correct continuation and abort commands depend on which operation stopped.
Start With A Clean Working Tree
Before integrating another branch, finish, commit, or deliberately stash unrelated local work.
git status --short
Git can preserve some local modifications, but conflict recovery is harder when pre-existing edits are mixed with the integration. A clean starting point makes --abort more reliable and the final diff easier to understand.
Inspect The Operation And Files
When Git stops, read its output and run:
git status
git diff --name-only --diff-filter=U
git diff
git status identifies the operation in progress and suggests the appropriate next commands. The U filter lists unmerged paths.
A textual conflict contains markers like these:
<<<<<<< HEAD
code from the currently checked-out side
=======
code from the other side
>>>>>>> other-branch
The labels provide context, but do not assume the top or bottom block is automatically correct. During rebase, the meaning of “ours” and “theirs” can be surprising because commits are being replayed.
Resolve The Intended Behavior
A correct resolution may:
- keep the current side
- keep the incoming side
- combine both changes
- replace both with a clearer implementation
- delete code that neither side should retain
Read the surrounding function, tests, task, and related commits. Conflict resolution is a small design decision, not marker deletion.
After editing, search for leftover markers and inspect the result:
git diff --check
git diff
Then mark each resolved path:
git add src/ProductName.php
git status
For a resolved deletion, use git rm instead.
Continue The Correct Operation
For a merge:
git merge --continue
A normal git commit can also conclude a conflicted merge after all paths are staged, but git merge --continue communicates the active operation clearly.
For a rebase:
git rebase --continue
A rebase may stop more than once as later commits are replayed. Resolve and verify each stop independently.
For a cherry-pick:
git cherry-pick --continue
Do not run a continuation command copied from a different operation. Let git status identify the state.
Abort When The Resolution Is Unclear
You can return to the pre-operation state:
git merge --abort
git rebase --abort
git cherry-pick --abort
Aborting is appropriate when the integration target was wrong, the working tree contains unexpected changes, or the conflict requires product or architecture input. It is better to restart with understanding than commit a guessed resolution.
Test The Combined Result
Each branch may pass on its own while the combined code fails. After resolving:
- run syntax checks and focused tests
- run static analysis and formatting checks
- inspect migrations and configuration
- exercise the behavior both branches changed
- review the final diff against the base branch
For a conflict in a conditional or validation rule, add boundary tests rather than trusting visual inspection.
PHP-Specific Conflict Cases
composer.json And composer.lock
Reconcile the intended dependency constraints in composer.json, then use the project's Composer workflow to regenerate or update the lock file. Avoid hand-selecting arbitrary lock-file sections without understanding package resolution.
Database Migrations
Two migrations may share a timestamp, sequence, or schema assumption even when the files do not textually conflict. Check execution order and test upgrading from the current deployed schema.
Generated Files
If a generated artifact is intentionally tracked, resolve the source files first and regenerate it with the documented tool. Manual conflict-marker editing can leave output inconsistent with its source.
Configuration
Combining both sides may create duplicate service registrations, routes, environment keys, or incompatible defaults. Validate the resulting application configuration, not only the syntax.
Do Not Hide The Resolution
A conflict resolution can change behavior beyond either original branch. Mention significant decisions in the commit or pull request and ask for re-review when the resolved code differs materially from what reviewers previously saw.
What To Remember
Conflicts require semantic decisions. Identify the active Git operation, inspect every unmerged path, build the intended final behavior, stage the resolution, run the correct continuation command, and test the combined result. Abort when you do not yet have enough information.
Before moving on, make sure you can explain why deleting markers is insufficient, how merge and rebase continuation differ, and why a lock-file conflict should be resolved through the dependency workflow.
A Conflict Is Competing Edits, Not A Git Failure
A merge conflict means Git could not automatically combine changes. It does not mean either branch is wrong. Two people may edit the same line, one branch may rename a file while another modifies it, or two migrations may choose the same ordering. Your job is to produce the version that preserves the intended behavior from both sides.
Start by understanding both branches. Read the pull request, commit messages, and surrounding code. Do not resolve conflicts by blindly choosing "ours" or "theirs" unless you know that one side is intentionally discarded. The correct resolution is often a third version that combines ideas.
Read Conflict Markers Carefully
Conflict markers show the current branch, the incoming branch, and the separator between them. Remove the markers only after you understand the code. In PHP files, confirm syntax after resolution. In Markdown, check headings and links. In JSON, YAML, or XML, validate structure because a visually reasonable merge can still be invalid.
Use git status to list conflicted files. Use git diff to inspect unresolved conflicts and git diff --ours, git diff --theirs, or mergetool views when helpful. For complex conflicts, open the base version too so you can see what each branch changed from.
Resolve Behavior, Not Text
A conflict in a service method may require updating tests, documentation, or configuration. If one branch adds a validation rule and another adds a new caller, the resolution must ensure the new caller follows the rule. If one branch changes a constructor and another creates an instance, the constructor call must be updated intentionally.
Database migrations need extra care. Two branches may create migrations with timestamps that run in a surprising order, or both may alter the same table. Do not simply rename files until the migration order, rollback behavior, and deployed database state make sense. When migrations have already run in shared environments, coordinate before rewriting them.
Composer lock-file conflicts should usually be resolved by rerunning the appropriate Composer command after the source conflict is resolved. Hand-editing composer.lock is error-prone. Know whether the intended operation is install, targeted update, or broader update.
Use Tools Without Surrendering Judgment
Merge tools can show three panes and make text selection easier. They do not know the product rule. After using a tool, review the final file as if you had written it. Run formatters only after the semantic resolution is complete, otherwise formatting noise can hide mistakes.
git rerere can remember how you resolved a conflict and replay it later. This is useful during long rebases or repeated integration work, but it should not be trusted blindly. Inspect reused resolutions when the surrounding code has changed.
Test The Resolution
After resolving, run the smallest meaningful checks first: syntax checks, unit tests for affected code, migration validation, or a focused build. Then run the repository's required checks. A conflict resolution can compile while losing behavior from one side, so include tests or manual verification that exercise both changes.
Search for conflict markers before committing. git diff --check catches leftover markers and whitespace problems. Also inspect git diff --staged; staged content is what the merge commit or rebased commit will contain.
Conflicts During Rebase
During a rebase, Git replays commits one at a time. You may resolve similar conflicts repeatedly because each commit is being copied onto a new base. After resolving one step, use git rebase --continue. If the rebase becomes confusing, git rebase --abort returns to the pre-rebase state.
Because rebase creates new commits, avoid rebasing shared branches without team agreement. If reviewers already saw the old commit series, use git range-diff to show how the rewritten series compares.
Conflicts In Generated Files
Generated files should be regenerated from source after resolving the source conflict. If generated output is committed, run the documented generator and review that the output matches the combined source. If there is no documented generator, treat that as a project maintenance problem and fix the documentation.
Lock files, snapshots, API clients, and compiled assets all fall into this category. The resolution should be reproducible, not a hand-merged artifact nobody can recreate.
Collaboration During Difficult Conflicts
Some conflicts require the other author. Ask when the behavior is unclear, when migrations interact, or when both branches modify a subtle security or money-handling rule. Pairing for fifteen minutes is cheaper than shipping a resolution that silently removes a guard.
Document the final decision in the pull request if the conflict was non-obvious. Future reviewers may wonder why a branch's original line disappeared. A short note prevents the same issue from being reopened.
After this lesson, you should be able to inspect conflict markers, understand both sides of a conflict, combine behavior rather than text, handle migrations and lock files safely, use merge tools carefully, test resolutions, and collaborate when the correct result is not obvious from the diff alone.
Practice
Task: Resolve A Product-Name Conflict
- trim surrounding whitespace
- return
Unknown productwhen the key is missing or the trimmed name is empty - return the trimmed name otherwise
The conflicted function body is:
<<<<<<< HEAD
return trim($product['name'] ?? 'Unknown product');
=======
$name = $product['name'] ?? null;
return $name === null ? 'Unknown product' : $name;
>>>>>>> main
Write the resolved PHP function and example calls for a normal name, a whitespace-only name, and a missing key. Include expected output comments.
Then give the commands to:
- inspect unmerged paths
- stage the resolved file
- continue a merge
- abort instead if the requirements remain unclear
Show solution
<?php
declare(strict_types=1);
/**
* @param array{name?: string} $product
*/
function productName(array $product): string
{
$name = trim($product['name'] ?? '');
return $name === '' ? 'Unknown product' : $name;
}
echo productName(['name' => ' Notebook ']) . PHP_EOL;
echo productName(['name' => ' ']) . PHP_EOL;
echo productName([]) . PHP_EOL;
// Prints:
// Notebook
// Unknown product
// Unknown product
The resolution combines the trimming requirement with a fallback that also handles whitespace-only names.
git status
git diff --name-only --diff-filter=U
git add src/ProductName.php
git merge --continue
If the intended behavior is still unclear, do not stage a guess. Use git merge --abort to restore the pre-merge state, clarify the requirement, and retry deliberately.