Advanced git diff analysis, file pattern matching, and change detection capabilities for comprehensive code review automation.
Basic file change information available in all Dangerfiles.
interface GitDSL {
readonly modified_files: string[];
readonly created_files: string[];
readonly deleted_files: string[];
readonly commits: GitCommit[];
readonly base: string;
readonly head: string;
}Usage Examples:
// Check for specific file changes
const hasPackageChanges = danger.git.modified_files.includes("package.json");
const hasLockfileChanges = danger.git.modified_files.includes("yarn.lock");
if (hasPackageChanges && !hasLockfileChanges) {
fail("package.json changed but yarn.lock wasn't updated. Run `yarn install`.");
}
// Check new files
const newTSFiles = danger.git.created_files.filter(f => f.endsWith(".ts"));
if (newTSFiles.length > 0) {
message(`π Created ${newTSFiles.length} new TypeScript files`);
}
// Check deletions
if (danger.git.deleted_files.length > 10) {
warn(`Deleting ${danger.git.deleted_files.length} files. Ensure this is intentional.`);
}
// Access git references
message(`Comparing ${danger.git.base} to ${danger.git.head}`);Advanced file pattern matching with glob support.
/**
* Pattern matching utility for file paths
* @param patterns - Glob patterns to match against
* @returns Object with boolean flags for different change types
*/
fileMatch: (...patterns: string[]) => GitMatchResult;
interface GitMatchResult {
readonly modified: boolean;
readonly created: boolean;
readonly edited: boolean; // created + modified
readonly deleted: boolean;
getKeyedPaths(): KeyedPaths<GitMatchResult>;
}
// Supporting types for file matching
type Pattern = string;
type Path = string;
type KeyedPatterns<T> = {
readonly [K in keyof T]: Pattern[];
};
type KeyedPaths<T> = {
readonly [K in keyof T]: Path[];
};
type Chainsmoker<T> = (...patterns: Pattern[]) => GitMatchResult;Usage Examples:
// Simple pattern matching
const packageFiles = danger.git.fileMatch("package.json", "yarn.lock");
if (packageFiles.modified) {
message("π¦ Package files were updated");
}
// Glob patterns
const sourceFiles = danger.git.fileMatch("src/**/*.ts", "src/**/*.tsx");
const testFiles = danger.git.fileMatch("**/*.test.ts", "**/*.spec.ts");
if (sourceFiles.edited && !testFiles.edited) {
warn("Source files changed but no tests were updated");
}
// Multiple patterns with different logic
const configFiles = danger.git.fileMatch(
"*.json",
"*.yml",
"*.yaml",
".env*",
"Dockerfile*"
);
if (configFiles.modified) {
warn("βοΈ Configuration files changed. Review deployment impact.");
}
// Get actual file paths
const docsFiles = danger.git.fileMatch("docs/**/*.md", "*.md");
if (docsFiles.edited) {
const paths = docsFiles.getKeyedPaths();
const fileList = [...paths.modified, ...paths.created].join(", ");
message(`π Documentation updated: ${fileList}`);
}
// Complex pattern combinations
const frontendFiles = danger.git.fileMatch(
"src/components/**/*",
"src/pages/**/*",
"public/**/*"
);
const backendFiles = danger.git.fileMatch(
"src/api/**/*",
"src/lib/**/*",
"src/utils/**/*"
);
if (frontendFiles.edited && backendFiles.edited) {
message("π Full-stack changes detected");
} else if (frontendFiles.edited) {
message("π¨ Frontend-only changes");
} else if (backendFiles.edited) {
message("βοΈ Backend-only changes");
}Get detailed text differences for specific files.
/**
* Get text diff for a specific file
* @param filename - Path to the file
* @returns Promise resolving to diff information or null
*/
diffForFile(filename: string): Promise<TextDiff | null>;
interface TextDiff {
before: string;
after: string;
diff: string;
added: string;
removed: string;
}Usage Examples:
// Analyze specific file changes
schedule(async () => {
const readmeDiff = await danger.git.diffForFile("README.md");
if (readmeDiff) {
const addedLines = readmeDiff.added.split('\n').length;
const removedLines = readmeDiff.removed.split('\n').length;
message(`README updated: +${addedLines} lines, -${removedLines} lines`);
// Check for specific patterns in changes
if (readmeDiff.added.includes("BREAKING")) {
warn("β οΈ Breaking changes mentioned in README");
}
}
});
// Check for sensitive data in diffs
schedule(async () => {
for (const file of danger.git.modified_files) {
const diff = await danger.git.diffForFile(file);
if (diff && diff.added.match(/(password|secret|key|token)/i)) {
fail(`π Potential sensitive data in ${file}`);
}
}
});Get parsed git diff with chunk information.
/**
* Get structured diff with chunk information
* @param filename - Path to the file
* @returns Promise resolving to structured diff or null
*/
structuredDiffForFile(filename: string): Promise<StructuredDiff | null>;
interface StructuredDiff {
chunks: File["chunks"]; // From parse-diff library
fromPath: string | undefined;
}Usage Examples:
// Analyze code structure changes
schedule(async () => {
const diff = await danger.git.structuredDiffForFile("src/main.ts");
if (diff) {
let functionsAdded = 0;
let functionsRemoved = 0;
diff.chunks.forEach(chunk => {
chunk.changes.forEach(change => {
if (change.type === 'add' && change.content.includes('function ')) {
functionsAdded++;
}
if (change.type === 'del' && change.content.includes('function ')) {
functionsRemoved++;
}
});
});
if (functionsAdded > 0 || functionsRemoved > 0) {
message(`Functions: +${functionsAdded}, -${functionsRemoved}`);
}
}
});Advanced JSON file comparison with semantic understanding.
/**
* Simplified JSON diff showing semantic changes
* @param filename - Path to JSON file
* @returns Promise resolving to JSON diff object
*/
JSONDiffForFile(filename: string): Promise<JSONDiff>;
interface JSONDiff {
[name: string]: JSONDiffValue;
}
interface JSONDiffValue {
before: any;
after: any;
added?: any[];
removed?: any[];
}Usage Examples:
// Analyze package.json changes
schedule(async () => {
const packageDiff = await danger.git.JSONDiffForFile("package.json");
if (packageDiff.dependencies) {
const { added = [], removed = [] } = packageDiff.dependencies;
if (added.length > 0) {
message(`π¦ Added dependencies: ${added.join(", ")}`);
}
if (removed.length > 0) {
message(`ποΈ Removed dependencies: ${removed.join(", ")}`);
}
}
if (packageDiff.version) {
message(`π Version: ${packageDiff.version.before} β ${packageDiff.version.after}`);
}
if (packageDiff.scripts?.added?.length > 0) {
message(`β‘ New scripts: ${packageDiff.scripts.added.join(", ")}`);
}
});
// Analyze configuration changes
schedule(async () => {
const configDiff = await danger.git.JSONDiffForFile("config/app.json");
// Check for environment-specific changes
Object.keys(configDiff).forEach(key => {
if (key.includes("production") || key.includes("prod")) {
warn(`π¨ Production config changed: ${key}`);
}
});
// Check for security-related changes
if (configDiff.security || configDiff.auth || configDiff.encryption) {
fail("π Security configuration changed. Requires additional review.");
}
});RFC6902 JSON Patch operations for precise change tracking.
/**
* Get RFC6902 JSON patch operations
* @param filename - Path to JSON file
* @returns Promise resolving to JSON patch operations or null
*/
JSONPatchForFile(filename: string): Promise<JSONPatch | null>;
interface JSONPatch {
before: any;
after: any;
diff: JSONPatchOperation[];
}
interface JSONPatchOperation {
op: string; // "add", "remove", "replace", "move", "copy", "test"
path: string; // JSON pointer path
value: string; // New value for operation
}Usage Examples:
// Detailed change tracking
schedule(async () => {
const patch = await danger.git.JSONPatchForFile("package.json");
if (patch) {
const majorChanges = patch.diff.filter(op =>
op.path.includes('/version') ||
op.path.includes('/main') ||
op.path.includes('/exports')
);
if (majorChanges.length > 0) {
warn("π¨ Major package.json changes detected:");
majorChanges.forEach(change => {
message(` - ${change.op} ${change.path}: ${change.value}`);
});
}
}
});/**
* Calculate lines of code changes with optional filtering
* @param pattern - Optional glob pattern to filter files
* @returns Promise resolving to net line change count
*/
linesOfCode(pattern?: string): Promise<number | null>;Usage Examples:
// Overall change analysis
schedule(async () => {
const totalChanges = await danger.git.linesOfCode();
if (totalChanges !== null) {
if (totalChanges > 1000) {
warn(`π¨ Large PR: ${totalChanges} net lines changed`);
} else if (totalChanges > 500) {
message(`π Medium PR: ${totalChanges} net lines changed`);
} else {
message(`β
Small PR: ${totalChanges} net lines changed`);
}
}
});
// Pattern-specific analysis
schedule(async () => {
const srcChanges = await danger.git.linesOfCode("src/**/*.ts");
const testChanges = await danger.git.linesOfCode("**/*.test.ts");
const ratio = testChanges && srcChanges ? testChanges / srcChanges : 0;
if (srcChanges && srcChanges > 50 && ratio < 0.3) {
warn(`π Test coverage ratio is low: ${(ratio * 100).toFixed(1)}%`);
}
});
// File type analysis
schedule(async () => {
const [jsChanges, tsChanges, styleChanges] = await Promise.all([
danger.git.linesOfCode("**/*.js"),
danger.git.linesOfCode("**/*.ts"),
danger.git.linesOfCode("**/*.{css,scss,less}")
]);
const breakdown = [
jsChanges && `JS: ${jsChanges}`,
tsChanges && `TS: ${tsChanges}`,
styleChanges && `Styles: ${styleChanges}`
].filter(Boolean).join(", ");
if (breakdown) {
message(`π Changes by type: ${breakdown}`);
}
});interface GitCommit {
sha: string;
author: GitCommitAuthor;
committer: GitCommitAuthor;
message: string;
tree: any;
parents?: string[];
url: string;
}
interface GitCommitAuthor {
name: string;
email: string;
date: string; // ISO8601 date string
}Usage Examples:
// Commit message validation
const invalidCommits = danger.git.commits.filter(commit => {
const msg = commit.message;
return !msg.match(/^(feat|fix|docs|style|refactor|test|chore)(\(.+\))?: .{1,50}/);
});
if (invalidCommits.length > 0) {
fail("β Some commits don't follow conventional commit format:");
invalidCommits.forEach(commit => {
fail(` - ${commit.sha.substring(0, 7)}: ${commit.message.split('\n')[0]}`);
});
}
// Author analysis
const authors = new Set(danger.git.commits.map(c => c.author.email));
if (authors.size > 1) {
message(`π₯ Multiple authors: ${Array.from(authors).join(", ")}`);
}
// Commit timing analysis
const commitDates = danger.git.commits.map(c => new Date(c.author.date));
const daySpan = Math.ceil(
(Math.max(...commitDates) - Math.min(...commitDates)) / (1000 * 60 * 60 * 24)
);
if (daySpan > 7) {
message(`π
Commits span ${daySpan} days - consider breaking into smaller PRs`);
}
// Find large commits
schedule(async () => {
for (const commit of danger.git.commits) {
const commitFiles = danger.git.modified_files.length; // Approximation
if (commitFiles > 20) {
warn(`π¨ Large commit ${commit.sha.substring(0, 7)}: ~${commitFiles} files`);
}
}
});// Ensure related files are updated together
const schemaFiles = danger.git.fileMatch("**/*.schema.json");
const migrationFiles = danger.git.fileMatch("migrations/**/*.ts");
const modelFiles = danger.git.fileMatch("src/models/**/*.ts");
if (schemaFiles.edited && !migrationFiles.edited) {
warn("Schema changed but no migration added");
}
if (modelFiles.edited && !schemaFiles.edited) {
warn("Models changed but schema wasn't updated");
}// Enforce architecture boundaries
const uiFiles = danger.git.fileMatch("src/ui/**/*");
const dbFiles = danger.git.fileMatch("src/database/**/*");
if (uiFiles.edited && dbFiles.edited) {
fail("UI components shouldn't directly access database layer");
}
// Check for circular dependencies
schedule(async () => {
const importRegex = /import.*from ['"]([^'"]+)['"]/g;
for (const file of danger.git.modified_files.filter(f => f.endsWith('.ts'))) {
const content = await danger.github.utils.fileContents(file);
const imports = [...content.matchAll(importRegex)].map(m => m[1]);
// Check for relative imports going up too many levels
const deepImports = imports.filter(imp => imp.startsWith('../../../'));
if (deepImports.length > 0) {
warn(`Deep relative imports in ${file}: ${deepImports.join(', ')}`);
}
}
});