or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

cli-commands.mddangerfile-api.mdgit-utilities.mdgithub-integration.mdindex.mdplatform-integrations.md
tile.json

git-utilities.mddocs/

Git Analysis & Utilities

Advanced git diff analysis, file pattern matching, and change detection capabilities for comprehensive code review automation.

Capabilities

File Change Detection

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}`);

Pattern Matching

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");
}

Diff Analysis

Text Diff

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}`);
    }
  }
});

Structured Diff

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}`);
    }
  }
});

JSON Analysis

Advanced JSON file comparison with semantic understanding.

JSON Diff

/**
 * 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.");
  }
});

JSON Patch

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}`);
      });
    }
  }
});

Code Metrics

Lines of Code Analysis

/**
 * 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}`);
  }
});

Commit Analysis

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`);
    }
  }
});

Advanced Pattern Examples

Multi-file Validation

// 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");
}

Architecture Validation

// 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(', ')}`);
    }
  }
});