brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
dev-loop-git-workflowUse when the user asks about Git workflows, branching strategies, git hooks, merge strategies, git history management, or team git conventions. Do NOT use for: code review (dev-loop-code-review), changelogs (dev-loop-changelog-generator), or CI/CD configuration.
Independent GitHub snapshot · Automated classification · Original documentation governs
Establish and maintain effective Git workflows — branching strategy, commit conventions, merge patterns, and history management — that enable team collaboration, continuous integration, and reliable releases.
Exact user phrases: "git workflow", "branching strategy", "git flow", "trunk-based", "commit convention", "merge strategy", "git hooks", "git history", "rebase vs merge", "git best practices", "commit message format".
Git workflow specification with branching model, commit convention, merge strategy, and hook configuration.
200 lines.
What is the team's release model?
├── Continuous deployment (multiple deploys per day)
│ → Trunk-based development: short-lived feature branches → main
│ → Feature flags for incomplete features
│ → No release branches (main is always deployable)
├── Scheduled releases (weekly/monthly)
│ → GitHub Flow: feature → main → release branch → tag
│ → Release branches for patch backports
│ → Version tags on release commits
├── Multiple versions in support (LTS, enterprise)
│ → Git Flow: develop + main + release branches + hotfix
│ → Long-lived release branches, cherry-pick fixes
└── Open source / community
→ Forking workflow: fork → PR → maintainer review → merge
→ Maintainer has squash + rebase options
Strategy Complexity Best For Release Model
──────────────────────────────────────────────────────────────────────────
Trunk-based Low Small teams, high CI Continuous
GitHub Flow Low-Med Most teams, standard On-demand
Git Flow High Multi-version, enterprise Scheduled
Forking High Open source, external PR-driven
convention: "scoped names with forward slashes"
patterns:
features: "feat/<issue-number>-<short-description>"
# e.g. feat/142-add-user-avatar
fixes: "fix/<issue-number>-<short-description>"
# e.g. fix/87-null-pointer-on-login
chores: "chore/<description>"
# e.g. chore/update-dependencies
releases: "release/<version>"
# e.g. release/v2.1.0
hotfixes: "hotfix/<version>-<description>"
# e.g. hotfix/v2.0.1-security-patch
team_rules:
- "All branches must reference an issue number (if applicable)"
- "Use kebab-case, no uppercase"
- "Delete remote branch after merge"
- "Maximum branch lifetime: 3 days (trunk-based) or 1 week"
format: |
<type>(<scope>): <description>
[optional body]
[optional footer(s)]
types:
feat: "New feature (bumps minor)"
fix: "Bug fix (bumps patch)"
docs: "Documentation"
style: "Formatting, no logic change"
refactor: "Code restructuring"
perf: "Performance improvement"
test: "Adding/fixing tests"
chore: "Maintenance, deps, build"
ci: "CI/CD changes"
rules:
- "Subject line: max 72 characters, imperative mood, lowercase"
- "Body: wrap at 72 characters, describe WHY not WHAT"
- "Footer: BREAKING CHANGE: or references (closes #142)"
- "Breaking change: append ! after type/scope"
# Good commit messages
feat(api): add user avatar upload endpoint
feat(api)!: remove deprecated v1 endpoints
BREAKING CHANGE: The `/api/v1/users` endpoint is removed.
Use `/api/v2/users` instead.
fix(auth): handle null token on session refresh
Closes #142
docs(readme): update installation instructions
# Bad commit messages
"fix stuff"
"update"
"WIP"
"asdf"
"final_final_v2_REAL"
# GitHub branch protection rules (for main branch)
branch_protection:
main:
required_status_checks:
- "Lint / lint (ubuntu-latest)"
- "Test / test (ubuntu-latest)"
- "Test / test (windows-latest)"
- "Build / build"
- "CodeQL / Analyze"
required_pull_request_review:
required_approving_review_count: 1
dismiss_stale_reviews: true
require_code_owner_reviews: false
require_last_push_approval: true
restrictions:
push_restrictions: []
allow_force_pushes: false
allow_deletions: false
other:
linear_history: false # Allow merge commits, but prefer squash
require_conversation_resolution: true
delete_branch_on_merge: true
required_linear_history: false
# Recommended: squash merge for feature branches
default: "squash merge"
rationale: |
Clean history on main. Each feature or fix becomes a single
well-described commit. Easy to revert. Simple git bisect.
strategies:
- name: "Squash merge"
when: "Standard feature branch or bugfix"
what: "All commits in feature branch become one commit on main"
commit_message: "<type>(<scope>): <description> (#PR-number)"
pros:
- "Clean linear history on main"
- "Easy to revert (one commit per feature)"
- "bisect-friendly"
cons:
- "Loses individual commit history of feature branch"
- name: "Rebase and merge"
when: "Small change with clean commits, open source PR"
what: "Individual commits preserved, no merge commit"
commit_message: "Original commits kept"
pros:
- "Preserves detailed commit history"
- "No merge commits"
cons:
- "Requires clean commit history from contributor"
- name: "Merge commit"
when: "Complex merge, multiple collaborators on branch"
what: "Merge commit created, all commits preserved"
commit_message: "Merge branch 'feat/...' into main"
pros:
- "Preserves full history and co-author attribution"
cons:
- "Cluttered history, harder to bisect"
# .husky/commit-msg
npx --no -- commitlint --edit $1
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [2, 'always', [
'feat', 'fix', 'docs', 'style', 'refactor',
'perf', 'test', 'chore', 'ci'
]],
'subject-case': [2, 'always', 'lower-case'],
'subject-max-length': [2, 'always', 72],
'header-max-length': [2, 'always', 72],
'scope-case': [2, 'always', 'lower-case'],
}
};
# .husky/pre-commit - lint staged files
npx lint-staged
// package.json
{
"lint-staged": {
"*.{ts,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md,yaml}": ["prettier --write"]
}
}
release_workflow:
steps:
- "Create release branch from main: release/v2.1.0"
- "Bump version in package.json, changelog"
- "Run full CI suite on release branch"
- "Create PR from release/v2.1.0 → main"
- "After approval, merge using merge commit (preserve release commit)"
- "Tag the merge commit: git tag v2.1.0 && git push --tags"
- "Deploy from tag or release branch"
hotfix_workflow:
- "Create branch from tag: hotfix/v2.0.1-security-fix"
- "Apply fix, bump patch version"
- "Create PR → main (merge via squash)"
- "Cherry-pick to older release branches if needed"
- "Tag: git tag v2.0.1 && git push --tags"
rules:
- "Never commit directly to main"
- "Never force push to shared branches"
- "Always squash merge feature/fix branches"
- "Always delete branch after merge"
- "Rebase feature branch before creating PR"
# Resolving merge conflicts
git checkout main
git pull origin main
git checkout feature/my-feature
git rebase main
# Fix conflicts in each commit
git add <resolved-files>
git rebase --continue
# Force push (if not shared with others)
git push --force-with-lease
| Pitfall | Description | Prevention |
|---|---|---|
| Long-lived branches | Diverged from main, huge merge conflicts | Trunk-based: max 3 days per branch |
| Force push to shared branches | Losing teammates' work | Use --force-with-lease, ban force push on shared |
| Messy history with WIP commits | Unhelpful commit messages | Squash on merge, use meaningful commits locally |
| Cherry-pick duplication | Same commit cherry-picked to multiple branches | Note cherry-pick source, use git cherry |
| No hooks → inconsistent commits | Formatting, commit messages not enforced | pre-commit + commitlint in CI and hooks |
| Rebase instead of merge at wrong time | Rebasing already-pushed branch | Don't rebase public branches; use merge |
| Ignoring .gitignore patterns | Committing node_modules, .env, secrets | Audit .gitignore, use .gitattributes |
| Large binary files in repo | Repo size balloons, slow clones | Git LFS for binaries, exclude build artifacts |
| Practice | Rationale |
|---|---|
| One feature = one branch = one PR | Clean separation, atomic changes |
| Squash merge feature branches | Clean linear history on main |
| Conventional Commits | Enables automated tooling (changelog, versioning) |
| Trunk-based development for CI | Short branches, frequent integration, less conflict |
| Branch protection on main | Prevents accidental pushes, enforces review |
| Git hooks for commit quality | Catches issues before CI runs |
| Delete branches after merge | Reduces clutter, no stale branches |
| Use .gitattributes for line endings | Users on any OS get correct line endings |
| Prefer --force-with-lease over --force | Won't overwrite others' pushes |
| Keep commits small and focused | Easier review, revert, bisect |
import subprocess
import re
from typing import List, Dict, Optional
from datetime import datetime
class GitWorkflowManager:
def __init__(self, repo_path: str = "."):
self.repo_path = repo_path
def create_feature_branch(self, feature_name: str, base: str = "main") -> bool:
branch = f"feat/{feature_name.lower().replace(' ', '-')}"
try:
subprocess.run(["git", "checkout", base], check=True, cwd=self.repo_path)
subprocess.run(["git", "pull", "origin", base], check=True, cwd=self.repo_path)
subprocess.run(["git", "checkout", "-b", branch], check=True, cwd=self.repo_path)
return True
except subprocess.CalledProcessError as e:
print(f"Failed to create branch: {e}")
return False
def squash_commits(self, message: str) -> bool:
try:
log = subprocess.run(
["git", "log", "--oneline"], capture_output=True, text=True, cwd=self.repo_path
)
commits = log.stdout.strip().split("\n")
if len(commits) <= 1:
return True
subprocess.run(["git", "reset", "--soft", f"HEAD~{len(commits)-1}"], check=True, cwd=self.repo_path)
subprocess.run(["git", "commit", "-m", message], check=True, cwd=self.repo_path)
return True
except subprocess.CalledProcessError as e:
print(f"Failed to squash: {e}")
return False
def detect_diverged(self, branch: str, base: str = "main") -> int:
try:
result = subprocess.run(
["git", "rev-list", "--count", f"{base}..{branch}"],
capture_output=True, text=True, cwd=self.repo_path
)
ahead = int(result.stdout.strip())
result = subprocess.run(
["git", "rev-list", "--count", f"{branch}..{base}"],
capture_output=True, text=True, cwd=self.repo_path
)
behind = int(result.stdout.strip())
return ahead - behind
except subprocess.CalledProcessError:
return 0
def suggest_rebase(self, branch: str, base: str = "main") -> Optional[str]:
behind = self.detect_diverged(branch, base)
if behind < 0:
return f"Branch is {abs(behind)} commits behind {base}. Suggested: git rebase {base}"
elif behind > 5:
return f"Branch is {behind} commits ahead — consider squashing or splitting PR"
return None
def get_latest_tag(self) -> Optional[str]:
try:
result = subprocess.run(
["git", "describe", "--tags", "--abbrev=0"],
capture_output=True, text=True, cwd=self.repo_path
)
return result.stdout.strip() if result.returncode == 0 else None
except subprocess.CalledProcessError:
return None
def changelog_between(self, from_ref: str, to_ref: str = "HEAD") -> List[Dict]:
try:
result = subprocess.run(
["git", "log", f"{from_ref}..{to_ref}", "--oneline", "--format=%H|%s|%an|%ad", "--date=short"],
capture_output=True, text=True, cwd=self.repo_path
)
entries = []
for line in result.stdout.strip().split("\n"):
parts = line.split("|")
if len(parts) >= 4:
entries.append({
"hash": parts[0][:8],
"message": parts[1],
"author": parts[2],
"date": parts[3],
})
return entries
except subprocess.CalledProcessError:
return []
class GitHookInstaller:
def __init__(self, repo_path: str = "."):
self.repo_path = repo_path
self.hooks_dir = f"{repo_path}/.git/hooks"
def install_pre_commit_hook(self):
hook_content = """#!/bin/sh
# Pre-commit hook: validate commit message format
commit_msg_file=$1
msg=$(cat "$commit_msg_file")
# Conventional Commits regex
conventional_regex="^(feat|fix|docs|refactor|perf|test|chore|ci|build|style|revert)(\\\\(.+\\\\))?(!)?: .+"
if ! echo "$msg" | grep -qE "$conventional_regex"; then
echo ""
echo "ERROR: Commit message must follow Conventional Commits format."
echo "Examples:"
echo " feat(api): add user endpoint"
echo " fix: resolve null pointer in auth"
echo " refactor(core): extract validation logic"
echo ""
exit 1
fi
"""
hook_path = f"{self.hooks_dir}/commit-msg"
with open(hook_path, "w") as f:
f.write(hook_content)
import os
os.chmod(hook_path, 0o755)
print(f"Installed commit-msg hook at {hook_path}")
def install_pre_push_hook(self):
hook_content = """#!/bin/sh
# Pre-push hook: prevent force push to protected branches
protected_branches="main master develop"
while read local_ref local_sha remote_ref remote_sha; do
for branch in $protected_branches; do
if echo "$remote_ref" | grep -q "refs/heads/$branchquot;; then
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
echo "ERROR: Deleting protected branch $branch is not allowed"
exit 1
fi
# Check if force push
zero_commit="0000000000000000000000000000000000000000"
if [ "$remote_sha" != "$zero_commit" ]; then
merge_base=$(git merge-base $local_sha $remote_sha 2>/dev/null)
if [ "$merge_base" != "$remote_sha" ]; then
echo "ERROR: Force push to $branch is not allowed. Use --force-with-lease and create a PR instead."
exit 1
fi
fi
fi
done
done
exit 0
"""
hook_path = f"{self.hooks_dir}/pre-push"
with open(hook_path, "w") as f:
f.write(hook_content)
import os
os.chmod(hook_path, 0o755)
print(f"Installed pre-push hook at {hook_path}")
What's the team size and release cadence?
├── Small team (1-5), continuous deployment
│ └── Trunk-based development
│ ├── Short-lived feature branches (< 1 day)
│ ├── Direct commits to main (with review)
│ └── Feature flags for incomplete work
│
├── Medium team (5-20), weekly releases
│ └── GitHub Flow
│ ├── feature/* branches from main
│ ├── PR → main with squash merge
│ └── Release tags from main
│
├── Large team (20+), scheduled releases
│ └── Git Flow
│ ├── develop branch for integration
│ ├── feature/* from develop → develop
│ ├── release/* for release candidates
│ └── hotfix/* from main → main + develop
│
└── Mono repo with multiple services
└── Trunk-based + release branches per service
├── service-* prefixes
├── Independent release cadence
└── Per-service CI/CD pipelines
What type of conflict?
├── Same file, different sections
│ └── Auto-merge handles it — verify with build
│
├── Same file, same section
│ ├── One side is trivial (rename, whitespace)
│ │ └── Accept the meaningful change
│ └── Both sides are meaningful
│ ├── Understand both changes → combine
│ └── If incompatible → talk to the other author
│
├── Deleted file vs modified file
│ └── Usually want the deletion — confirm intent
│
└── Binary file conflict
└── Pick one version, re-apply the other's intent
husky or pre-commit to distribute hooks via git to the whole team. Ensure consistency without manual setup steps.| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Giant mega-branches | Merge hell, integration nightmare | Keep branches < 3 days, use feature flags |
| Rebased published branches | Creates divergent history for collaborators | Only rebase unpushed commits |
| Squash merging everything | Loses context of individual commits | Squash features, merge commit for releases |
| Long-lived release branches | Cherry-pick chaos, never fully synced | Short release branches or trunk-based |
| No commit message convention | Impossible to auto-generate changelog | Enforce Conventional Commits in hooks/CI |
| Force pushing to shared branches | Destroys co-authors' work | Use --force-with-lease, protect branches |
| Multiple merges from main into feature | Pollutes feature branch history | Rebase feature onto main once, before PR |
| Not pulling before rebase | Rebasing onto outdated main | Always git pull origin main before rebase |
| Stashing unfinished work for too long | Context lost, conflicts pile up | Commit WIP with fixup!, rebase later |
| .gitignore updated too late | Secrets committed, repo bloated | Setup .gitignore before first commit |
git clone --depth 1 in CI pipelines. Reduces clone time by 80%+ for large repositories. Use git fetch --unshallow only when full history is needed.git clone --filter=blob:none for developer machines. Downloads only commit metadata initially, fetches file contents on demand.git sparse-checkout set <path> to only checkout relevant subdirectories. Reduces working tree size for monorepos with many packages.git rebase --interactive --autosquash with fixup! commits. Reduces manual squash effort. Use git rebase --update-refs to handle stacked branches.Hand off to dev-loop-code-review for PR review process. Hand off to dev-loop-changelog-generator for release notes from commits.