Skip to main content

Agent Skills Framework Extension

Git Workflow Patterns Skill

When to Use This Skill

Use this skill when implementing git workflow patterns patterns in your codebase.

How to Use This Skill

  1. Review the patterns and examples below
  2. Apply the relevant patterns to your implementation
  3. Follow the best practices outlined in this skill

Bottom-up submodule synchronization, conventional commits, branching strategies, and multi-repository coordination.

Core Capabilities

  1. Bottom-Up Sync - Submodule-first synchronization workflow
  2. Conventional Commits - Structured commit message format
  3. Branch Strategies - Git-flow, trunk-based, GitHub flow
  4. Submodule Management - Multi-repository coordination
  5. Conflict Resolution - Automated and manual merge strategies

Bottom-Up Submodule Sync

#!/usr/bin/env python3
"""
Bottom-up git synchronization - submodules first, then master repo.
Prevents detached HEAD and ensures all changes are committed.
"""

import subprocess
import json
from pathlib import Path
from typing import List, Dict, Optional
from dataclasses import dataclass

@dataclass
class SubmoduleStatus:
path: str
branch: str
has_changes: bool
ahead_behind: tuple[int, int]
commit_hash: str

class GitWorkflowManager:
def __init__(self, root_path: Path):
self.root_path = root_path
self.sync_report = {
'timestamp': '',
'submodules_synced': [],
'master_synced': False,
'conflicts': [],
'errors': []
}

def discover_submodules(self) -> List[str]:
"""Discover all submodules in repository."""
result = subprocess.run(
['git', 'submodule', 'status'],
cwd=self.root_path,
capture_output=True,
text=True
)

submodules = []
for line in result.stdout.strip().split('\n'):
if line:
# Parse: " hash path (branch)"
parts = line.split()
if len(parts) >= 2:
submodules.append(parts[1])

return submodules

def get_submodule_status(self, submodule_path: str) -> Optional[SubmoduleStatus]:
"""Get detailed status of a submodule."""
full_path = self.root_path / submodule_path

# Check if on a branch
branch_result = subprocess.run(
['git', 'branch', '--show-current'],
cwd=full_path,
capture_output=True,
text=True
)
branch = branch_result.stdout.strip() or 'DETACHED'

# Check for uncommitted changes
status_result = subprocess.run(
['git', 'status', '--porcelain'],
cwd=full_path,
capture_output=True,
text=True
)
has_changes = bool(status_result.stdout.strip())

# Check ahead/behind
if branch != 'DETACHED':
ahead_behind_result = subprocess.run(
['git', 'rev-list', '--left-right', '--count', f'origin/{branch}...HEAD'],
cwd=full_path,
capture_output=True,
text=True
)
if ahead_behind_result.returncode == 0:
behind, ahead = map(int, ahead_behind_result.stdout.strip().split())
ahead_behind = (ahead, behind)
else:
ahead_behind = (0, 0)
else:
ahead_behind = (0, 0)

# Get current commit
commit_result = subprocess.run(
['git', 'rev-parse', 'HEAD'],
cwd=full_path,
capture_output=True,
text=True
)
commit_hash = commit_result.stdout.strip()[:8]

return SubmoduleStatus(
path=submodule_path,
branch=branch,
has_changes=has_changes,
ahead_behind=ahead_behind,
commit_hash=commit_hash
)

def sync_submodule(self, submodule_path: str) -> bool:
"""
Sync a single submodule: checkout main, pull, commit, push.
Returns True if sync successful, False otherwise.
"""
full_path = self.root_path / submodule_path
print(f"\n🔄 Syncing submodule: {submodule_path}")

try:
# 1. Checkout main branch
subprocess.run(
['git', 'checkout', 'main'],
cwd=full_path,
check=True,
capture_output=True
)
print(f" ✓ Checked out main branch")

# 2. Pull latest changes
pull_result = subprocess.run(
['git', 'pull', 'origin', 'main'],
cwd=full_path,
capture_output=True,
text=True
)

if pull_result.returncode != 0:
if 'CONFLICT' in pull_result.stdout or 'CONFLICT' in pull_result.stderr:
self.sync_report['conflicts'].append({
'path': submodule_path,
'message': 'Merge conflict detected'
})
print(f" ⚠️ Merge conflict - manual resolution required")
return False

print(f" ✓ Pulled latest changes")

# 3. Check for uncommitted changes
status = self.get_submodule_status(submodule_path)
if status.has_changes:
# Auto-commit with conventional format
commit_msg = self.generate_commit_message(full_path)
subprocess.run(
['git', 'add', '.'],
cwd=full_path,
check=True
)
subprocess.run(
['git', 'commit', '-m', commit_msg],
cwd=full_path,
check=True
)
print(f" ✓ Committed changes: {commit_msg}")

# 4. Push to remote
push_result = subprocess.run(
['git', 'push', 'origin', 'main'],
cwd=full_path,
capture_output=True,
text=True
)

if push_result.returncode == 0:
print(f" ✓ Pushed to remote")
self.sync_report['submodules_synced'].append(submodule_path)
return True
else:
print(f" ⚠️ Push failed: {push_result.stderr}")
return False

except subprocess.CalledProcessError as e:
self.sync_report['errors'].append({
'path': submodule_path,
'error': str(e)
})
print(f" ❌ Error: {e}")
return False

def generate_commit_message(self, repo_path: Path) -> str:
"""Generate conventional commit message from git status."""
status_result = subprocess.run(
['git', 'status', '--porcelain'],
cwd=repo_path,
capture_output=True,
text=True
)

changes = status_result.stdout.strip().split('\n')

# Classify changes
new_files = [c for c in changes if c.startswith('A') or c.startswith('??')]
modified_files = [c for c in changes if c.startswith('M')]
deleted_files = [c for c in changes if c.startswith('D')]

# Determine commit type
if new_files and not modified_files:
commit_type = 'feat'
scope = 'core'
subject = f'Add {len(new_files)} new file(s)'
elif deleted_files and not new_files:
commit_type = 'chore'
scope = 'cleanup'
subject = f'Remove {len(deleted_files)} file(s)'
elif modified_files:
commit_type = 'chore'
scope = 'update'
subject = f'Update {len(modified_files)} file(s)'
else:
commit_type = 'chore'
scope = 'sync'
subject = 'Sync repository state'

return f"{commit_type}({scope}): {subject}"

def sync_master_repo(self) -> bool:
"""Update master repo submodule pointers and commit."""
print(f"\n🔄 Syncing master repository...")

try:
# Check for submodule pointer updates
status_result = subprocess.run(
['git', 'status', '--porcelain'],
cwd=self.root_path,
capture_output=True,
text=True
)

if status_result.stdout.strip():
# Commit submodule pointer updates
subprocess.run(
['git', 'add', 'submodules/'],
cwd=self.root_path,
check=True
)

commit_msg = "chore(submodules): Update submodule pointers to latest"
subprocess.run(
['git', 'commit', '-m', commit_msg],
cwd=self.root_path,
check=True
)
print(f" ✓ Committed submodule pointer updates")

# Push to remote
subprocess.run(
['git', 'push', 'origin', 'main'],
cwd=self.root_path,
check=True
)
print(f" ✓ Pushed to remote")

self.sync_report['master_synced'] = True
return True
else:
print(f" ℹ️ No submodule pointer updates needed")
return True

except subprocess.CalledProcessError as e:
self.sync_report['errors'].append({
'path': 'master',
'error': str(e)
})
print(f" ❌ Error: {e}")
return False

def execute_bottom_up_sync(self) -> Dict:
"""
Execute complete bottom-up sync workflow.
Returns sync report with results.
"""
from datetime import datetime

self.sync_report['timestamp'] = datetime.utcnow().isoformat()

print("🚀 Starting bottom-up git synchronization...")

# 1. Discover all submodules
submodules = self.discover_submodules()
print(f"📦 Found {len(submodules)} submodule(s)")

# 2. Sync each submodule
for submodule_path in submodules:
self.sync_submodule(submodule_path)

# 3. Sync master repository
self.sync_master_repo()

# 4. Save report
report_path = self.root_path / 'git-sync-report.json'
with open(report_path, 'w') as f:
json.dump(self.sync_report, f, indent=2)

print(f"\n✅ Sync complete! Report saved to: {report_path}")
print(f" Submodules synced: {len(self.sync_report['submodules_synced'])}")
print(f" Conflicts: {len(self.sync_report['conflicts'])}")
print(f" Errors: {len(self.sync_report['errors'])}")

return self.sync_report

# Usage
if __name__ == '__main__':
manager = GitWorkflowManager(Path.cwd())
report = manager.execute_bottom_up_sync()

Conventional Commit Validator

#!/usr/bin/env python3
"""
Validate and enforce conventional commit format.
Install as git commit-msg hook for automatic validation.
"""

import re
import sys
from pathlib import Path
from typing import Optional, Tuple

class ConventionalCommitValidator:
# Conventional Commits 1.0.0 format
PATTERN = re.compile(
r'^(?P<type>build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)'
r'(?:\((?P<scope>[a-z0-9-]+)\))?'
r'(?P<breaking>!)?'
r': '
r'(?P<subject>.{1,72})$',
re.MULTILINE
)

TYPES = {
'feat': 'New feature',
'fix': 'Bug fix',
'docs': 'Documentation only',
'style': 'Code style (formatting, missing semicolons, etc)',
'refactor': 'Code change that neither fixes a bug nor adds a feature',
'perf': 'Performance improvement',
'test': 'Adding missing tests or correcting existing tests',
'build': 'Changes affecting build system or dependencies',
'ci': 'Changes to CI configuration files and scripts',
'chore': 'Other changes that don\'t modify src or test files',
'revert': 'Reverts a previous commit'
}

def validate(self, commit_msg: str) -> Tuple[bool, Optional[str]]:
"""
Validate commit message against conventional commits format.
Returns (is_valid, error_message).
"""
lines = commit_msg.strip().split('\n')

if not lines:
return False, "Commit message is empty"

# Validate header (first line)
header = lines[0]
match = self.PATTERN.match(header)

if not match:
return False, self._format_error_message(header)

# Validate type
commit_type = match.group('type')
if commit_type not in self.TYPES:
return False, f"Invalid type '{commit_type}'. Valid types: {', '.join(self.TYPES.keys())}"

# Validate subject length
subject = match.group('subject')
if len(subject) < 1:
return False, "Subject is required"
if len(subject) > 72:
return False, f"Subject too long ({len(subject)} chars). Max 72 characters."

# Validate subject doesn't end with period
if subject.endswith('.'):
return False, "Subject should not end with a period"

# Validate subject starts with lowercase (except BREAKING CHANGE)
if not match.group('breaking') and subject[0].isupper():
return False, "Subject should start with lowercase letter"

# Validate blank line after header if body present
if len(lines) > 1 and lines[1] != '':
return False, "Blank line required between header and body"

return True, None

def _format_error_message(self, header: str) -> str:
"""Generate helpful error message with examples."""
return f"""
Invalid conventional commit format.

Your header: {header}

Expected format:
type(scope): subject

Examples:
feat(api): add user authentication endpoint
fix(ui): resolve button alignment issue
docs(readme): update installation instructions
chore(deps): update dependencies

Valid types:
{chr(10).join(f' {k}: {v}' for k, v in self.TYPES.items())}

Rules:
- type: one of the valid types above
- scope: optional, lowercase alphanumeric with hyphens
- subject: max 72 chars, lowercase start, no period
- Breaking change: add ! before colon (e.g., feat!: breaking change)
""".strip()

def install_commit_hook():
"""Install as git commit-msg hook."""
git_dir = Path('.git')
if not git_dir.exists():
print("Error: Not a git repository")
sys.exit(1)

hook_path = git_dir / 'hooks' / 'commit-msg'
hook_path.parent.mkdir(exist_ok=True)

hook_script = f"""#!/usr/bin/env python3
import sys
from pathlib import Path

# Read this validator script
validator_content = Path(__file__).read_text()

# Execute validator
exec(validator_content)

# Validate commit message
commit_msg_file = sys.argv[1]
commit_msg = Path(commit_msg_file).read_text()

validator = ConventionalCommitValidator()
is_valid, error = validator.validate(commit_msg)

if not is_valid:
print(f"❌ Commit message validation failed:\\n{{error}}")
sys.exit(1)

print("✅ Commit message is valid")
"""

hook_path.write_text(hook_script)
hook_path.chmod(0o755)
print(f"✅ Installed commit-msg hook at: {hook_path}")

# CLI usage
if __name__ == '__main__':
if len(sys.argv) > 1:
# Called as git hook
commit_msg_file = sys.argv[1]
commit_msg = Path(commit_msg_file).read_text()

validator = ConventionalCommitValidator()
is_valid, error = validator.validate(commit_msg)

if not is_valid:
print(f"❌ {error}")
sys.exit(1)

print("✅ Commit message is valid")
else:
# Install hook
install_commit_hook()

Branch Strategy Configuration

#!/usr/bin/env bash
# setup-git-flow.sh - Configure Git Flow branching strategy

set -euo pipefail

# Configuration
MAIN_BRANCH="${MAIN_BRANCH:-main}"
DEVELOP_BRANCH="${DEVELOP_BRANCH:-develop}"
FEATURE_PREFIX="${FEATURE_PREFIX:-feature/}"
RELEASE_PREFIX="${RELEASE_PREFIX:-release/}"
HOTFIX_PREFIX="${HOTFIX_PREFIX:-hotfix/}"

log_info() { echo -e "\033[0;32m[INFO]\033[0m $1"; }
log_error() { echo -e "\033[0;31m[ERROR]\033[0m $1" >&2; }

setup_git_flow() {
log_info "Setting up Git Flow branching strategy..."

# Ensure main branch exists
if ! git rev-parse --verify "$MAIN_BRANCH" >/dev/null 2>&1; then
log_info "Creating $MAIN_BRANCH branch..."
git checkout -b "$MAIN_BRANCH"
git push -u origin "$MAIN_BRANCH"
fi

# Create develop branch
if ! git rev-parse --verify "$DEVELOP_BRANCH" >/dev/null 2>&1; then
log_info "Creating $DEVELOP_BRANCH branch from $MAIN_BRANCH..."
git checkout "$MAIN_BRANCH"
git checkout -b "$DEVELOP_BRANCH"
git push -u origin "$DEVELOP_BRANCH"
fi

# Set up branch protections (requires GitHub CLI)
if command -v gh >/dev/null 2>&1; then
log_info "Configuring branch protection rules..."

# Protect main branch
gh api "repos/:owner/:repo/branches/$MAIN_BRANCH/protection" \
--method PUT \
--field required_status_checks='{"strict":true,"contexts":["ci/tests"]}' \
--field enforce_admins=true \
--field required_pull_request_reviews='{"dismissal_restrictions":{},"dismiss_stale_reviews":true,"require_code_owner_reviews":true,"required_approving_review_count":1}' \
--field restrictions=null || log_error "Failed to protect $MAIN_BRANCH"

# Protect develop branch
gh api "repos/:owner/:repo/branches/$DEVELOP_BRANCH/protection" \
--method PUT \
--field required_status_checks='{"strict":true,"contexts":["ci/tests"]}' \
--field enforce_admins=false \
--field required_pull_request_reviews='{"dismissal_restrictions":{},"dismiss_stale_reviews":true,"require_code_owner_reviews":false,"required_approving_review_count":1}' \
--field restrictions=null || log_error "Failed to protect $DEVELOP_BRANCH"
fi

log_info "✅ Git Flow setup complete!"
log_info " Main branch: $MAIN_BRANCH"
log_info " Develop branch: $DEVELOP_BRANCH"
log_info " Feature prefix: $FEATURE_PREFIX"
log_info " Release prefix: $RELEASE_PREFIX"
log_info " Hotfix prefix: $HOTFIX_PREFIX"
}

create_feature() {
local feature_name="$1"
local branch_name="${FEATURE_PREFIX}${feature_name}"

log_info "Creating feature branch: $branch_name"

git checkout "$DEVELOP_BRANCH"
git pull origin "$DEVELOP_BRANCH"
git checkout -b "$branch_name"

log_info "✅ Feature branch created: $branch_name"
log_info " To push: git push -u origin $branch_name"
}

finish_feature() {
local current_branch
current_branch=$(git branch --show-current)

if [[ ! "$current_branch" =~ ^$FEATURE_PREFIX ]]; then
log_error "Not on a feature branch. Current branch: $current_branch"
exit 1
fi

log_info "Finishing feature: $current_branch"

# Ensure develop is up to date
git checkout "$DEVELOP_BRANCH"
git pull origin "$DEVELOP_BRANCH"

# Merge feature
git merge --no-ff "$current_branch" -m "Merge $current_branch into $DEVELOP_BRANCH"
git push origin "$DEVELOP_BRANCH"

# Delete feature branch
git branch -d "$current_branch"
git push origin --delete "$current_branch" || true

log_info "✅ Feature merged and deleted: $current_branch"
}

create_release() {
local version="$1"
local branch_name="${RELEASE_PREFIX}${version}"

log_info "Creating release branch: $branch_name"

git checkout "$DEVELOP_BRANCH"
git pull origin "$DEVELOP_BRANCH"
git checkout -b "$branch_name"

# Update version in package files
if [[ -f "package.json" ]]; then
sed -i.bak "s/\"version\": \".*\"/\"version\": \"$version\"/" package.json
rm package.json.bak
git add package.json
git commit -m "chore(release): bump version to $version"
fi

git push -u origin "$branch_name"

log_info "✅ Release branch created: $branch_name"
}

finish_release() {
local current_branch
current_branch=$(git branch --show-current)

if [[ ! "$current_branch" =~ ^$RELEASE_PREFIX ]]; then
log_error "Not on a release branch. Current branch: $current_branch"
exit 1
fi

local version="${current_branch#$RELEASE_PREFIX}"

log_info "Finishing release: $version"

# Merge to main
git checkout "$MAIN_BRANCH"
git pull origin "$MAIN_BRANCH"
git merge --no-ff "$current_branch" -m "Release $version"
git tag -a "v$version" -m "Release $version"
git push origin "$MAIN_BRANCH"
git push origin "v$version"

# Merge back to develop
git checkout "$DEVELOP_BRANCH"
git pull origin "$DEVELOP_BRANCH"
git merge --no-ff "$current_branch" -m "Merge release $version back to develop"
git push origin "$DEVELOP_BRANCH"

# Delete release branch
git branch -d "$current_branch"
git push origin --delete "$current_branch" || true

log_info "✅ Release finished: $version"
}

# CLI
case "${1:-}" in
setup)
setup_git_flow
;;
feature)
if [[ -z "${2:-}" ]]; then
log_error "Usage: $0 feature <name>"
exit 1
fi
create_feature "$2"
;;
finish-feature)
finish_feature
;;
release)
if [[ -z "${2:-}" ]]; then
log_error "Usage: $0 release <version>"
exit 1
fi
create_release "$2"
;;
finish-release)
finish_release
;;
*)
echo "Usage: $0 {setup|feature|finish-feature|release|finish-release}"
exit 1
;;
esac

Usage Examples

Bottom-Up Sync All Submodules

Apply git-workflow-patterns skill to execute bottom-up sync across all submodules with conventional commits

Validate Conventional Commits

Apply git-workflow-patterns skill to install commit-msg hook for automatic commit message validation

Setup Git Flow Strategy

Apply git-workflow-patterns skill to configure Git Flow with protected branches and automated workflows

Success Output

When successful, this skill MUST output:

✅ SKILL COMPLETE: git-workflow-patterns

Completed:
- [x] Bottom-up submodule sync executed successfully (all submodules synced)
- [x] Conventional commit validation installed as git hook
- [x] Git Flow or trunk-based branching strategy configured
- [x] Branch protection rules applied (main, develop)
- [x] All submodule pointers updated in master repository

Outputs:
- git-sync-report.json (sync summary with conflicts, errors)
- .git/hooks/commit-msg (conventional commit validator)
- Branch protection rules (via GitHub API or git config)
- Conventional commit messages in git log
- Submodule sync scripts (sync-all-submodules.sh)

Completion Checklist

Before marking this skill as complete, verify:

  • All submodules on main branch (no detached HEAD)
  • All submodule changes committed and pushed
  • Master repository submodule pointers updated
  • Conventional commit hook installed and working
  • Test commit blocked if message format invalid
  • Branch protection rules active (require PR for main)
  • Git Flow branches created (main, develop) if applicable
  • Sync report shows no errors or conflicts
  • All commits follow conventional format (type(scope): subject)

Failure Indicators

This skill has FAILED if:

  • ❌ Submodules still in detached HEAD state
  • ❌ Uncommitted changes remain in submodules
  • ❌ Master repository submodule pointers not updated
  • ❌ Conventional commit hook not installed or not firing
  • ❌ Invalid commit messages not blocked
  • ❌ Branch protection rules not applied
  • ❌ Sync report shows unresolved conflicts
  • ❌ Sync failed to push to remote (permission issues)
  • ❌ Commits don't follow conventional format

When NOT to Use

Do NOT use this skill when:

  • Single repository (no submodules) - use standard git workflow
  • No need for conventional commits (internal project only)
  • Squash-merge workflow (conventional commits lost on merge)
  • No CI/CD integration (branch protection not needed)
  • Small team (<3 people) with informal process
  • Monorepo with no submodules (use monorepo-patterns skill)
  • Legacy project migrating to git (use git-migration-patterns skill)

Use these alternatives instead:

  • Single repo: Standard git workflow (feature branches, PR reviews)
  • Monorepo: monorepo-patterns skill
  • Migration: git-migration-patterns skill

Anti-Patterns (Avoid)

Anti-PatternProblemSolution
Top-down sync onlyDetached HEAD in submodulesAlways sync submodules first, then master
No commit validationInconsistent commit messagesInstall commit-msg hook for validation
Force push to mainLost commits, broken historyUse branch protection to block force push
Manual submodule updatesForgotten submodules, inconsistent stateAutomate with bottom-up sync script
No conflict resolutionSync fails silentlyDetect conflicts, report, require manual resolution
Uppercase commit subjectsViolates conventional commitsEnforce lowercase in validation hook
Missing scope in commitsUnclear what changedEncourage scope usage (e.g., feat(api): ...)

Principles

This skill embodies:

  • #3 Complete Execution - Bottom-up sync automates entire workflow (detect, sync, commit, push)
  • #4 Separation of Concerns - Separate submodule sync from master repo sync
  • #5 Eliminate Ambiguity - Conventional commits make commit purpose clear
  • #7 Verification Protocol - Sync report verifies all submodules synced successfully
  • #8 No Assumptions - Check for detached HEAD, uncommitted changes before syncing

Full Standard: CODITECT-STANDARD-AUTOMATION.md

Integration Points

  • orchestration-patterns - Multi-agent git coordination
  • project-organization-patterns - Repository structure
  • cicd-pipeline-design - Automated testing and deployment