
Automating Azure DevOps Work Item association with Git commit hooks
In modern software development, maintaining traceability between code changes and work items is crucial for project management, compliance, and debugging.
Azure DevOps provides excellent integration between Git commits and work items, but it relies on developers remembering to include work item references in their commit messages.
This is where Git hooks, in particular the commit-msg
hook comes to the rescue.
This article will show you how to implement a robust commit-msg
Git hook that automatically enforces Azure DevOps work item association, either by detecting existing hash number syntax (#123
) or by intelligently extracting work item numbers from branch names.
Why automate Work Item association?
Before diving into the implementation, let’s understand the benefits:
- Consistency: Ensures every commit is linked to a work item
- Traceability: Makes it easy to track which commits relate to specific features or bugs
- Compliance: Helps meet organisational requirements for change tracking
- Reporting: Enables better project metrics and allows for automatic generation of release notes
The solution: A smart commit hook
Our approach uses a commit-msg
Git hook that:
- Checks existing commit messages for work item references (e.g.,
#123
) - Extracts work item numbers from branch names when not found in the message
- Automatically appends the work item reference to the commit message
- Prevents commits that can’t be associated with any work item
Understanding the branch name patterns
The hook supports two common branch naming conventions:
- Prefix pattern:
123-feature-description
or456-bugfix-login-issue
- Feature branch pattern:
feature/789-new-dashboard
orbugfix/101-memory-leak
The commit hook implementation
Here’s the complete commit-msg
hook:
#!/bin/bash
# Get the current branch name
BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD)
# Get the commit message file
COMMIT_MSG_FILE=$1
# Validate commit message file
if [ -z "$COMMIT_MSG_FILE" ] || [ ! -f "$COMMIT_MSG_FILE" ] || [ ! -r "$COMMIT_MSG_FILE" ]; then
echo "Error: Commit message file is missing or not readable."
exit 1
fi
# Read the commit message
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE" 2>/dev/null)
if [ $? -ne 0 ]; then
echo "Error: Failed to read commit message file '$COMMIT_MSG_FILE'."
exit 1
fi
# Check if branch name starts with a number or has a number after the last forward slash
if [[ $BRANCH_NAME =~ ^([0-9]+) || $BRANCH_NAME =~ /([0-9]+)(-[^/]+)?$ ]]; then
# Extract the number (either from start or after last slash)
if [[ ${BASH_REMATCH[1]} ]]; then
WORK_ITEM_NUMBER="${BASH_REMATCH[1]}"
else
WORK_ITEM_NUMBER="${BASH_REMATCH[2]}"
fi
# Append the work item number to the commit message with a newline if not already present
if ! grep -q "#$WORK_ITEM_NUMBER" "$COMMIT_MSG_FILE"; then
echo -e "\n#$WORK_ITEM_NUMBER" >> "$COMMIT_MSG_FILE"
fi
else
# If no number is found in the branch name, check for #number in commit message
if ! echo "$COMMIT_MSG" | grep -qE '#[0-9]+'; then
echo "Error: Commit message must contain a work item number (e.g., #123) or branch name must start with a number or have a number after the last forward slash."
exit 1
fi
fi
exit 0
How It Works
Step 1: Branch analysis
The hook first examines the current branch name using git rev-parse --abbrev-ref HEAD
and applies regex patterns to detect work item numbers.
Step 2: Message validation
It reads the commit message file and validates that it exists and is readable, providing clear error messages if not.
Step 3: Smart detection
Using bash regex matching, it looks for:
- Numbers at the start of branch names:
^([0-9]+)
- Numbers after the last forward slash:
/([0-9]+)(-[^/]+)?$
Step 4: Automatic appending
If a work item number is found in the branch name but not in the commit message, it automatically appends #<number>
to the message.
Step 5: Fallback validation
If no work item number can be extracted from the branch name, it ensures the commit message already contains a #<number>
reference. If not, the hook exits with an error message, notifying the user that a work item must be associated with the commit.
Deployment across multiple repositories
Managing Git hooks across multiple repositories can be challenging. Here’s a deployment script that automates the process.
You provide the script with a local starting directory (typically your org’s Git workspace root) and it will traverse all directories below it and copy the commit-msg
hook into the relevant .git/hooks
folders.
#!/bin/bash
# Script to deploy Git hooks from the shared-git-hooks repo to all .git/hooks directories
# Usage: ./deploy-hooks.sh <start_directory> [--force|-f]
# Initialize variables
START_DIR=""
FORCE=0
# Parse arguments
while [ $# -gt 0 ]; do
case "$1" in
--force|-f)
FORCE=1
shift
;;
*)
if [ -z "$START_DIR" ]; then
START_DIR="$1"
else
echo "Error: Unexpected argument '$1'"
echo "Usage: $0 <start_directory> [--force|-f]"
exit 1
fi
shift
;;
esac
done
# Check if start directory is provided
if [ -z "$START_DIR" ]; then
echo "Usage: $0 <start_directory> [--force|-f]"
exit 1
fi
HOOKS_SRC="$(pwd)/hooks"
# Validate directories
if [ ! -d "$START_DIR" ]; then
echo "Error: Start directory '$START_DIR' does not exist."
exit 1
fi
if [ ! -d "$HOOKS_SRC" ]; then
echo "Error: Hooks directory not found in current directory ($HOOKS_SRC)."
exit 1
fi
# Find all .git directories recursively
echo "Searching for Git repositories in $START_DIR..."
find "$START_DIR" -type d -name ".git" | while read -r git_dir; do
repo_dir=$(dirname "$git_dir")
hooks_dir="$git_dir/hooks"
echo "Processing repository: $repo_dir"
# Ensure hooks directory exists
mkdir -p "$hooks_dir"
# Copy each hook from the hooks/ directory
for hook in "$HOOKS_SRC"/*; do
hook_name=$(basename "$hook")
hook_dest="$hooks_dir/$hook_name"
# Skip any hook that already exists unless force flag is set
if [ "$FORCE" -eq 0 ] && [ -f "$hook_dest" ]; then
echo " Skipping $hook_name: already exists in $repo_dir"
continue
fi
# Copy the hook and make it executable
if cp "$hook" "$hook_dest"; then
chmod +x "$hook_dest"
if [ "$FORCE" -eq 1 ] && [ -f "$hook_dest" ]; then
echo " Overwrote $hook_name in $repo_dir (force mode)"
else
echo " Installed $hook_name to $repo_dir"
fi
else
echo " Error: Failed to copy $hook_name to $repo_dir"
fi
done
done
echo "Hook deployment completed."
exit 0
Setting up your hook infrastructure
1. Create a shared hooks repository
mkdir shared-git-hooks
cd shared-git-hooks
mkdir hooks
# Place your commit-msg hook in the hooks/ directory
# Add the deploy-hooks.sh script to the root
Don’t forget to chmod +x deploy-hooks.sh
- to allow execution permissions.
2. Copy to all repositories
# Deploy to all repos in your workspace
./deploy-hooks.sh ~/workspace
# Force update existing hooks
./deploy-hooks.sh ~/workspace --force
3. Test your setup
Create a test branch and commit:
# Branch with work item number
git checkout -b 123-test-feature
git add .
git commit -m "Add new feature"
# Result: "Add new feature\n\n#123"
# Branch without work item number but commit with reference
git checkout -b feature-branch
git add .
git commit -m "Fix bug #456"
# Result: Commit succeeds with existing #456 reference
Best practices and tips
Branch naming conventions
Establish consistent patterns across your team:
<work-item>-<description>
:1234-implement-login
<type>/<work-item>-<description>
:feature/1234-implement-login
Error handling
The hook provides clear error messages when work items can’t be associated, helping developers understand what went wrong.
Customisation
Modify the regex patterns to match your team’s specific branch naming conventions. The current implementation is flexible enough for most common patterns.
Troubleshooting common issues
Hook not executing
Ensure the hook file has executable permissions: chmod +x .git/hooks/commit-msg
Regex not matching
Test your branch names against the patterns using: [[ "your-branch-name" =~ ^([0-9]+) ]] && echo "Match!"
File permission errors
The deployment script automatically sets executable permissions on the hooks, but verify manually if issues persist.
Conclusion
Implementing this Git hook system transforms work item association from a manual, error-prone process into an automatic, reliable one. Your team will benefit from better traceability, improved compliance, and reduced cognitive overhead during development.
The combination of intelligent branch name parsing and fallback validation ensures that every commit is properly associated with Azure DevOps work items, while the deployment script makes it easy to maintain consistency across all your repositories.
Start with a pilot project, gather feedback, and then roll out across your organisation. Your future self (and your project managers) will thank you for the improved traceability and reduced manual effort.