Three simple rules prevent both drift and contamination:
- Merging FROM
developis NEVER allowed - Develop is dirty, so block it as a merge source - Merging TO
developis ALLOWED EXCEPT FOR UNFINSHED features and epics - This makes merging to develop easier since no intermediate branch is needed for this task - Auto-merge main → develop BEFORE Git Stream merges anything TO develop - Keeps develop current
This approach:
- Prevents drift: Develop always has latest production baseline before receiving new code
- Prevents contamination: Develop's unreleased code never leaks to other branches
- Simplifies workflow: No intermediate branches needed, direct merges to develop work safely
In Git Stream's workflow model, develop serves as a "dirty integration branch" where features and epics are merged for testing before being selectively included in releases. This creates a fundamental problem: develop accumulates unreleased code while main continues to evolve through releases and hotfixes.
Timeline:
T1: Feature A merges to develop
T2: Feature B merges to develop
T3: Release 1.0 (Feature A only) → main
T4: Feature C merges to develop
T5: Hotfix 1.0.1 → main
T6: Feature D merges to develop
At T6, develop contains:
- ✅ Feature A (released in 1.0)
⚠️ Feature B (unreleased - only in develop)- ❌ Missing hotfix 1.0.1 changes
⚠️ Feature C (unreleased - only in develop)⚠️ Feature D (unreleased - only in develop)
Meanwhile, main (production) contains:
- ✅ Release 1.0 (Feature A)
- ✅ Hotfix 1.0.1
Result: develop has diverged significantly from the production baseline. When Feature B is eventually selected for a release, it may conflict with hotfix changes that were never merged back to develop.
This drift compounds over time:
- Merge Conflicts: Features tested on an outdated
developmay conflict with currentmain - Integration Issues: Code tested against stale dependencies fails in production
- Duplicate Work: Fixes applied to
main(via hotfix) must be manually reapplied to features indevelop - Release Delays: Unexpected conflicts during release creation block deployments
- Lost Context: Weeks-old drift makes conflict resolution harder as team members forget implementation details
Current practice relies on developers remembering to sync:
- ❌ Inconsistent: Developers forget during feature work
- ❌ Too Late: Conflicts discovered during release creation
- ❌ Incomplete: Only syncs when problems are already visible
- ❌ Reactive: Fixes symptoms rather than prevents drift
The solution is elegantly simple: allow merging TO develop from anywhere, but NEVER merge FROM develop.
Since develop is dirty (contains unreleased code), we prevent it from polluting other branches by:
- Allowing all branches to merge TO develop: main, releases, hotfixes, finished features/epics
- Blocking all merges FROM develop: Nothing should ever branch from or merge from develop
This approach:
- Keeps develop current: Any branch can sync changes into develop
- Prevents contamination: Develop's unreleased code never leaks out
- Simplifies implementation: No complex sync timing or conflict resolution needed
- Maintains safety: Develop remains isolated as a testing ground
- One-Way Flow: All branches → develop (never develop → anywhere)
- Develop is Dirty: Accept that develop contains unreleased code
- Isolation: Keep develop's unreleased code contained
- Simple Rules: Easy to understand and enforce
- Production-First:
mainis source of truth, develop tracks it
The implementation is straightforward: allow merges TO develop, block merges FROM develop.
| Source Branch | Target Branch | Allowed? | Rationale |
|---|---|---|---|
main |
develop |
✅ YES | Keep develop current with production |
release/* |
develop |
✅ YES | Sync released code back to develop |
hotfix/* |
develop |
✅ YES | Propagate fixes to develop |
feature/* |
develop |
✅ YES | Merge finished features for testing |
epic/* |
develop |
✅ YES | Merge finished epics for testing |
epic-feature/* |
develop |
❌ NO | Must merge to parent epic first |
develop |
ANY | ❌ NO | Develop is dirty, never merge FROM it |
Git Stream automatically syncs main → develop BEFORE merging anything TO develop:
| Workflow Stage | Command | Sync Action | Purpose |
|---|---|---|---|
| Feature Finish | feature finish |
1. Merge main → develop 2. Merge feature → develop |
Ensure develop is current before adding feature |
| Epic Finish | epic finish |
1. Merge main → develop 2. Merge epic → develop |
Ensure develop is current before adding epic |
| Release Finish | release finish |
1. Merge to main 2. Merge main → develop |
Propagate released code back to develop |
| Hotfix Finish | hotfix finish |
1. Merge to main 2. Merge main → develop |
Propagate fixes immediately to develop |
Key Insight: Auto-syncing main → develop BEFORE each merge ensures:
- Develop always has the latest production baseline
- No drift accumulates between operations
- Conflicts are detected early with full context
- Direct merges to develop are safe (no intermediate branches needed)
graph LR
main[main<br/>production] -->|merge| develop[develop<br/>dirty testing]
release[release/*] -->|merge| develop
hotfix[hotfix/*] -->|merge| develop
feature[feature/*] -->|merge| develop
epic[epic/*] -->|merge| develop
develop -.->|BLOCKED| main
develop -.->|BLOCKED| release
develop -.->|BLOCKED| feature
develop -.->|BLOCKED| epic
style develop fill:#f9f,stroke:#333,stroke-width:2px
style main fill:#9f9,stroke:#333,stroke-width:2px
Conflicts only occur when merging TO develop:
- Source: The branch being merged (feature, epic, main, etc.)
- Target: develop (dirty, but that's okay)
- Resolution: Standard git conflict resolution
- Impact: Limited to develop only (doesn't affect production)
Prevent any merges FROM develop:
// In SafetyValidator or Repository class
/**
* Validate that develop is never used as a merge source
*
* @throws ValidationException if attempting to merge from develop
*/
protected function validateDevelopNotSource(string $sourceBranch): void
{
$developBranch = $this->configManager->get('branches.develop', 'develop');
if ($sourceBranch === $developBranch) {
throw new ValidationException(
"Cannot merge FROM {$developBranch}. " .
"Develop is a dirty testing branch and should never be used as a merge source. " .
"All branches should branch from 'main' instead."
);
}
}Add reusable sync method in PromptsBaseCommand:
/**
* Sync develop with main before merging to develop
* Prevents drift by ensuring develop has latest production baseline
*
* @return bool Success status
*/
protected function syncDevelopBeforeMerge(): bool
{
$developBranch = $this->configManager->get('branches.develop', 'develop');
$mainBranch = $this->configManager->get('branches.main', 'main');
$this->step("Syncing {$developBranch} with {$mainBranch}");
try {
$currentBranch = $this->repository->getCurrentBranch();
$this->repository->checkout($developBranch);
$this->repository->merge($mainBranch, "Auto-sync: Update develop with main before merge");
if ($currentBranch !== $developBranch) {
$this->repository->checkout($currentBranch);
}
$this->stepSuccess("Develop is now current with production");
return true;
} catch (RepositoryException $e) {
$this->stepError("Sync failed: " . $e->getMessage());
return false;
}
}// In finishFeature() method
// 1. Sync develop with main first
if (!$this->syncDevelopBeforeMerge()) {
throw new RepositoryException("Cannot merge feature: develop sync failed");
}
// 2. Merge feature to develop
$this->step("Merging feature to {$developBranch}");
$this->repository->checkout($developBranch);
$this->repository->merge($featureBranch, "Merge feature/{$featureName}");// In finishEpic() method
// 1. Sync develop with main first
if (!$this->syncDevelopBeforeMerge()) {
throw new RepositoryException("Cannot merge epic: develop sync failed");
}
// 2. Merge epic to develop
$this->step("Merging epic to {$developBranch}");
$this->repository->checkout($developBranch);
$this->repository->merge($epicBranch, "Merge epic/{$epicKey}");// In finishRelease() method
// 1. Merge release to main
$this->step("Merging release to {$mainBranch}");
// ... existing merge logic
// 2. Sync develop with updated main
$this->step("Syncing {$developBranch} with {$mainBranch}");
$this->repository->checkout($developBranch);
$this->repository->merge($mainBranch, "Sync develop with released code");
// 3. Update deployment tags
// ... existing tag logic// In finishHotfix() method
// 1. Merge hotfix to main
$this->step("Merging hotfix to {$mainBranch}");
// ... existing merge logic
// 2. Sync develop with main (includes hotfix)
$this->step("Syncing {$developBranch} with {$mainBranch}");
$this->repository->checkout($developBranch);
$this->repository->merge($mainBranch, "Sync develop with hotfix");
// 3. Update deployment tags
// ... existing tag logicAdd git hook to prevent accidental merges FROM develop:
#!/bin/bash
# .git/hooks/pre-merge-commit
# Get the branch being merged FROM
MERGE_HEAD=$(git rev-parse MERGE_HEAD 2>/dev/null)
if [ -n "$MERGE_HEAD" ]; then
MERGE_BRANCH=$(git name-rev --name-only $MERGE_HEAD)
# Check if merging from develop
if [[ "$MERGE_BRANCH" == *"develop"* ]]; then
echo "ERROR: Cannot merge FROM develop branch"
echo "Develop is a dirty testing branch and should never be used as a merge source"
echo "All branches should branch from 'main' instead"
exit 1
fi
fiThis hook prevents developers from accidentally using git merge develop on any branch.
Auto-syncing main → develop before each merge ensures develop always has the latest production baseline.
Blocking merges FROM develop ensures unreleased code never leaks to production branches.
Two clear rules: "Auto-sync main → develop before merging" and "Never merge FROM develop."
Direct merges to develop work safely because drift is prevented automatically.
Hotfixes and improvements in main automatically flow to develop before new features are added.
Conflicts are discovered when merging to develop (with full context), not weeks later during release.
Developers don't need to remember to sync—Git Stream handles it automatically.
# Developer works on feature-login
git checkout feature/login
# ... make changes ...
gstr commit "Implement login form"
# Finish feature
gstr feature finish login
# Git Stream automatically:
# 1. Syncs develop with main (ensures current baseline)
# 2. Merges feature to develop
# 3. Handles any conflicts (standard git resolution)
# Result: feature in develop, which is current with productionKey Point: Auto-sync ensures develop has latest production baseline before receiving the feature. No drift accumulates.
# Emergency production fix
gstr hotfix start security-patch
# ... fix vulnerability ...
gstr hotfix finish security-patch
# Git Stream automatically:
# 1. Merges hotfix to main
# 2. Merges main to develop (propagates fix)
# 3. Updates deploy/production tag
# Result: fix in production AND in develop for future featuresBenefit: Security fix immediately available in develop for all unreleased features.
# Release finishes
gstr release finish 2.0
# Git Stream automatically:
# 1. Merges release to main
# 2. Merges main to develop (propagates released code)
# 3. Updates deploy/production tag
# Result: develop immediately reflects production stateKey Point: Released code flows back to develop automatically.
# Developer accidentally tries to merge from develop
git checkout feature/new-feature
git merge develop
# Git Stream hook blocks this:
# ERROR: Cannot merge FROM develop branch
# Develop is a dirty testing branch and should never be used as a merge source
# All branches should branch from 'main' insteadProtection: Prevents contamination of clean branches with unreleased code.
-
Add validation in SafetyValidator
- Block any merge operation where source branch is
develop - Clear error message explaining the rule
- Block any merge operation where source branch is
-
Add helper method in PromptsBaseCommand
syncDevelopBeforeMerge()- auto-sync main → develop- Handle conflicts with standard resolution
-
Update FeatureCommand.finishFeature()
- Call
syncDevelopBeforeMerge()before merging feature - Then merge feature → develop
- Call
-
Update EpicCommand.finishEpic()
- Call
syncDevelopBeforeMerge()before merging epic - Then merge epic → develop
- Call
-
Update ReleaseCommand.finishRelease()
- After merging to main, merge main → develop
- Handle conflicts with standard resolution
-
Update HotfixCommand.finishHotfix()
- After merging to main, merge main → develop
- Handle conflicts with standard resolution
-
Install git hook
- Add pre-merge-commit hook to block
git merge develop - Provide clear error message
- Add pre-merge-commit hook to block
-
Update documentation
- Explain both rules (auto-sync + block FROM develop)
- Update workflow diagrams
- Add examples of correct usage
-
Test blocked merges FROM develop
- Attempt
git merge developfrom feature branch → should fail - Attempt release from develop → should fail
- Attempt
-
Test allowed merges TO develop
- Merge feature → develop → should succeed
- Merge main → develop → should succeed
- Merge hotfix → develop → should succeed
-
Test conflict handling
- Create conflicting changes in main and feature
- Merge feature → develop → should prompt for resolution
- Verify develop contains resolved changes
- Existing repositories: Install new git hook during next
gstr initorgstr sync - Documentation: Update all examples to reflect new approach
- User communication: Explain simplified model in release notes
Preventing develop branch drift requires two complementary rules:
✅ Auto-sync main → develop BEFORE Git Stream merges anything TO develop ❌ Never merge FROM develop (it's dirty)
This dual approach:
- Prevents drift: Develop always has latest production baseline before receiving new code
- Prevents contamination: Develop's unreleased code never leaks to production branches
- Enables direct merges: No intermediate branches needed—develop stays current automatically
- Reduces cognitive load: Git Stream handles synchronization automatically
- Validation: Block any merge operation where source is
develop - Auto-sync helper:
syncDevelopBeforeMerge()merges main → develop - Feature/Epic finish: Auto-sync, then merge to develop
- Release/Hotfix finish: Merge to main, then sync develop
- Git hooks: Prevent accidental
git merge developcommands
Remember: develop is dirty by design. It's a testing ground for unreleased features. The key is:
- Keep it current (auto-sync main → develop before each merge)
- Keep it isolated (never merge FROM it)