What a Git Commit Hash Actually Is
A Git commit hash is the SHA-1 (or SHA-256 in newer repos) of a commit object — a small text blob containing the tree hash, parent hashes, author, committer, and message. The output is 160 bits for SHA-1, rendered as 40 lowercase hex characters, or 256 bits for SHA-256, rendered as 64 lowercase hex characters. Hashes are globally unique in practice, and Git uses the first 7 characters as a "short SHA" whenever that prefix is unambiguous in the current repo.
The Three Patterns You Need
// 1. Full SHA-1 commit hash (git log --format=%H output)
/\b[0-9a-f]{40}\b/g
// 2. Short OR full SHA-1 (git log --oneline, GitHub links)
/\b[0-9a-f]{7,40}\b/g
// 3. SHA-256 object format (git init --object-format=sha256)
/\b[0-9a-f]{64}\b/g
// 4. Both algorithms (handle mixed-format repos)
/\b[0-9a-f]{40}\b|\b[0-9a-f]{64}\b/gWhy the Word Boundary Matters
A 40-char hex string like abc1234567890abcdef1234567890abcdef12345 could be embedded in a longer hex blob — an API key, an SHA-512 digest, or a UUID without hyphens. The \b word boundary ensures the hash is a standalone token:
// Without \b — bad: matches a 40-char slice from a longer hex blob
/[0-9a-f]{40}/g
.exec("abc" + "f".repeat(60)) // ✗ matches "fffff...fff" (false positive)
// With \b — good: must be a free-standing token
/\b[0-9a-f]{40}\b/g
.exec("abc" + "f".repeat(60)) // ✓ no match (not a word boundary)
// In context
"commit a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd — fix bug"
.match(/\b[0-9a-f]{40}\b/)
// ["a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd"]Language-Specific Usage
JavaScript / TypeScript
const COMMIT_HASH = /\b[0-9a-f]{7,40}\b/g;
// Extract all commit hashes from git log output
const log = `
commit a1b2c3d fix: null pointer in auth
commit 7890abc feat: add OAuth2 support
Merge: a1b2c3d 7890abc
`;
const hashes = [...log.matchAll(COMMIT_HASH)].map(m => m[0]);
// ['a1b2c3d', '7890abc', 'a1b2c3d', '7890abc']
// Deduplicate
const unique = [...new Set(hashes)];
// ['a1b2c3d', '7890abc']
// Validate a single hash
function isCommitHash(s) {
return /^[0-9a-f]{7,40}$/.test(s) || /^[0-9a-f]{64}$/.test(s);
}
isCommitHash("abc1234"); // true
isCommitHash("not-a-hash"); // false
isCommitHash("A1B2C3D"); // false (uppercase)Python
import re
import subprocess
COMMIT_HASH = re.compile(r'\b[0-9a-f]{7,40}\b')
SHA256_HASH = re.compile(r'\b[0-9a-f]{64}\b')
# Scan a commit message body for referenced hashes
def find_referenced_commits(message: str) -> list[str]:
return COMMIT_HASH.findall(message)
# Validate strictly
def is_sha1_hash(s: str) -> bool:
return bool(re.fullmatch(r'[0-9a-f]{40}', s))
# Pull hashes directly from git log
def recent_commits(repo_path: str, n: int = 10) -> list[str]:
out = subprocess.check_output(
['git', '-C', repo_path, 'log', f'-{n}', '--format=%H'],
text=True,
)
return out.strip().split("\n")Go
package main
import (
"fmt"
"regexp"
)
var (
commitHashRe = regexp.MustCompile(`\b[0-9a-f]{7,40}\b`)
sha256Re = regexp.MustCompile(`\b[0-9a-f]{64}\b`)
)
// Extract all commit hashes from a block of text
func extractHashes(text string) []string {
return commitHashRe.FindAllString(text, -1)
}
// Validate a single hash (SHA-1 full form)
func isCommitHash(s string) bool {
return regexp.MustCompile(`^[0-9a-f]{40}$`).MatchString(s)
}
func main() {
log := "commit a1b2c3d fix issue 123"
fmt.Println(extractHashes(log)) // [a1b2c3d]
}PHP
<?php
$log = "commit a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd fix: auth bug";
// All hashes in log output
preg_match_all('/\b[0-9a-f]{7,40}\b/', $log, $matches);
print_r($matches[0]);
// Array ( [0] => a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd )
// Validate single hash
function is_commit_hash(string $s): bool {
return (bool) preg_match('/^[0-9a-f]{40}$/', $s)
|| (bool) preg_match('/^[0-9a-f]{64}$/', $s);
}Common Pitfalls
Matching uppercase accidentally
Git always writes lowercase hex, but users occasionally paste hashes from older tools or email clients that upcase everything. If your intent is to validate strictly, keep[0-9a-f]. If you want to accept user-pasted hashes, use [0-9a-fA-F] or add the /i flag.
Short-SHA ambiguity
A 7-character prefix is unique in small-to-medium repos but Git auto-grows it for large codebases — Linux kernel uses 12 characters by default. Your regex of[0-9a-f]{7,40} correctly accepts both, but downstream code that passes the short hash to git show must handle the ambiguous argument error and retry with a longer prefix.
Confusing commit hash with tree/blob hash
Git uses the same hash format for commits, trees, blobs, and tags. The regex cannot tell them apart. If you specifically need commit hashes, pair the regex with agit cat-file -t <hash> call and keep only outputs that returncommit. Pure regex can't distinguish.
UUID collision
A UUID without hyphens (a1b2c3d4e5f6789012345678901234ab) is 32 hex characters — wrong length for SHA-1 and SHA-256, so your pattern naturally rejects it. But some tools store UUIDs as 40 hex chars with zero-padding or with the version/variant bits mangled — these will match. If you're in a system that mixes both, add a stricter check against a known commit list.
Common Variants
| Context | Pattern | Example match |
|---|---|---|
| Full SHA-1 | /\b[0-9a-f]{40}\b/ | a1b2c3d4...abcd |
| Short SHA-1 | /\b[0-9a-f]{7,12}\b/g | a1b2c3d |
| GitHub commit URL | /\/commit\/([0-9a-f]{40})/ | github.com/x/y/commit/abc... |
| git log --format=%H | /^[0-9a-f]{40}$/gm | full hash per line |
| SHA-256 repo | /\b[0-9a-f]{64}\b/ | (64 hex chars) |
Testing Your Git Hash Regex
Use the live Regex Tester with this canonical test block:
$ git log --oneline -5
a1b2c3d fix: null pointer in auth guard
7890abc feat: add OAuth2 support
def4567 docs: update README
0123456 refactor: split UserService
abcdef0 chore: bump deps
$ git log -1
commit a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6abcd
Merge: a1b2c3d 7890abc
Author: [email protected]
Merge PR #42 (fixes abc1234)
SHA-256: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
Patterns to try:
/\b[0-9a-f]{40}\b/g — 1 match (full SHA-1 only)
/\b[0-9a-f]{7,40}\b/g — 8 matches (short + full)
/\b[0-9a-f]{64}\b/g — 1 match (SHA-256 line)Performance Notes
The character class [0-9a-f] is a one-table-lookup check per character. With fixed length {40} the regex engine runs linearly over the input with no backtracking. On a 10MB git log the full-hash pattern runs in milliseconds. The variable-length {7,40} form is only slightly slower because it uses greedy matching that finds the longest hex run and never retries.