This guide explains how to implement new features and modifications in the DGT tool. It covers the existing codebase patterns, extension points, and development workflows.
- Dart SDK 3.9.2 or later
- Git (for repository operations)
- Access to Gerrit server (for testing API integration)
- Clone the repository
- Install dependencies:
dart pub get - Run analysis:
dart analyze - Run tests:
dart test
The project follows Dart's official style guide with additional linting rules defined in analysis_options.yaml.
Key Rules:
- Use single quotes for strings
- Always declare return types
- Prefer
constconstructors where possible - Lines should not exceed 80 characters
- Use relative imports within the package
All external integrations (Git, Gerrit, file system) are encapsulated in service classes.
Example: GitService handles all Git operations.
class GitService {
static Future<List<String>> getAllBranches() async {
// Implementation
}
}Use immutable data classes with named constructors for domain objects.
Example: BranchInfo combines Git and Gerrit data.
class BranchInfo {
const BranchInfo({
required this.branchName,
required this.localHash,
// ... other properties
});
final String branchName;
final String localHash;
}Use a three-layer precedence system: CLI arguments > Config file > Defaults.
Pattern:
factory DisplayOptions.resolve({
required ArgResults results,
required DgtConfig? config,
}) {
return DisplayOptions(
showGerrit: config.resolveFlag(results, 'gerrit', true),
// ... other options
);
}Commands are handled in bin/dgt.dart with a switch statement on the first argument.
// In main() function
case 'new-command':
await runNewCommand(/* parameters */);Future<void> runNewCommand(
String? repositoryPath,
// Other parameters
) async {
// Command implementation
}// In CliOptions class
static const String newOption = 'new-option';Add command description in lib/print_usage.dart.
// 1. Add command case
case 'status':
final branch = statusResults.option('branch');
await runStatusCommand(repositoryPath, branch);
// 2. Implement command
Future<void> runStatusCommand(String? path, String? branch) async {
// Change directory if needed
if (path != null) {
Directory.current = path;
}
// Get branch info
final targetBranch = branch ?? await GitService.getCurrentBranch();
final branchInfo = await getBranchInfo(targetBranch);
// Display results
Terminal.info('Status for $targetBranch: ${branchInfo.getDisplayStatus()}');
}class FilterOptions {
FilterOptions({
this.statuses,
this.since,
this.before,
this.diverged,
this.newFilter, // Add new filter
});
final bool? newFilter;
}// In CliOptions.addCommonOptions()
parser.addFlag(
'new-filter',
help: 'Description of new filter',
);// In applyFilters() function
if (filters.newFilter == true) {
filtered = filtered.where((branch) {
// Filter logic here
return /* condition */;
}).toList();
}// In DgtConfig class
final bool? filterNewFilter;
// In fromArgResults factory
filterNewFilter: _extractFlag(results, 'new-filter'),// In CliOptions class
static const List<String> allowedSortFields = [
'local-date',
'gerrit-date',
'status',
'divergences',
'name',
'new-field', // Add new field
];static const Map<String, String> sortFieldDescriptions = {
// ... existing fields
'new-field': 'Description of new sort field',
};// In applySort() function
switch (sortOptions.field!.toLowerCase()) {
// ... existing cases
case 'new-field':
sorted.sort((a, b) {
// Sort comparison logic
final valueA = /* extract value from a */;
final valueB = /* extract value from b */;
return multiplier * valueA.compareTo(valueB);
});Always use batch operations when possible:
// Good: Batch operation
final commitInfoMap = await GitServiceBatch.getBatchCommitInfo(branches);
// Avoid: Individual operations
for (final branch in branches) {
final info = await GitService.getCommitHashAndDate(branch);
}Git operations are automatically cached within GitService. Clear cache when repository state changes:
GitService.clearCache(); // Call when switching repositoriesAlways handle Git errors gracefully:
try {
final result = await GitService.someOperation();
// Process result
} catch (e) {
VerboseOutput.instance.warning('Git operation failed: $e');
// Provide fallback or continue processing
}Always use batch queries for multiple changes:
// Collect issue numbers
final issueNumbers = <String>[];
for (final branch in branches) {
if (branch.gerritConfig.hasGerritConfig) {
issueNumbers.add(branch.gerritConfig.gerritIssue!);
}
}
// Batch query
final changes = await GerritService.getBatchChangesByIssueNumbers(issueNumbers);Handle API failures gracefully:
try {
final changes = await GerritService.getBatchChangesByIssueNumbers(issues);
// Process successful results
} catch (e) {
VerboseOutput.instance.warning('Gerrit API failed: $e');
// Continue with local data only
}Remember to handle Gerrit's XSSI protection:
String cleanResponse(String response) {
// Remove Gerrit's XSSI protection prefix
if (response.startsWith(")]}'")) {
return response.substring(4);
}
return response;
}class DgtConfig {
DgtConfig({
// ... existing properties
this.newOption,
});
final String? newOption;
}// In fromJson factory
newOption: json['newOption'] as String?,
// In toJson method
if (newOption != null) 'newOption': newOption,// In fromArgResults factory
newOption: _extractOption(results, 'new-option'),// In DgtConfigExtensions
String resolveOption(ArgResults argResults, String optionName, String defaultValue) {
if (argResults.wasParsed(optionName)) {
return argResults.option(optionName) ?? defaultValue;
}
final configValue = /* extract from config */;
return configValue ?? defaultValue;
}Use PerformanceTracker to measure new operations:
Future<void> expensiveOperation() async {
tracker?.startTimer('operation_name');
try {
// Perform operation
} finally {
tracker?.endTimer('operation_name');
}
}Use Future.wait() for independent operations:
final results = await Future.wait([
GitService.getCommitHash(branch1),
GitService.getCommitHash(branch2),
GerritService.getChangeByIssue(issue1),
]);For CPU-intensive processing, use isolates:
// Define isolate function
static Future<Map<String, dynamic>> processInIsolate(String data) async {
return await Isolate.run(() {
// CPU-intensive processing
return processData(data);
});
}
// Use in main thread
final result = await processInIsolate(jsonData);Use Terminal class for consistent output:
Terminal.info('Informational message'); // White
Terminal.error('Error message'); // Red
Terminal.warning('Warning message'); // YellowDefine status-specific colors:
String colorizeStatus(String status) {
return switch (status) {
'Active' => AnsiPen()..green(),
'WIP' => AnsiPen()..yellow(),
'Merged' => AnsiPen()..cyan(),
_ => AnsiPen()..white(),
}(status);
}Use OutputFormatter patterns for tabular data:
class CustomFormatter {
void displayTable(List<Data> items) {
// Calculate column widths
final maxWidth = items.map((item) => item.name.length).reduce(max);
// Print headers
_printHeader(['Name', 'Value'], [maxWidth, 20]);
// Print rows
for (final item in items) {
_printRow([item.name, item.value], [maxWidth, 20]);
}
}
}Always provide fallbacks:
String getDisplayValue(BranchInfo branch) {
try {
return branch.gerritChange?.status ?? '-';
} catch (e) {
VerboseOutput.instance.warning('Failed to get status: $e');
return '?';
}
}Provide actionable error messages:
void validateInput(String input) {
if (!isValidDate(input)) {
throw FormatException(
'Invalid date format: "$input".\n'
'Expected ISO 8601 format (e.g., 2025-10-10 or 2025-10-10T14:30:00)'
);
}
}Continue processing when possible:
final results = <BranchInfo>[];
for (final branch in branches) {
try {
final info = await processBranch(branch);
results.add(info);
} catch (e) {
VerboseOutput.instance.warning('Failed to process $branch: $e');
// Add placeholder or skip
}
}Use VerboseOutput for debugging:
VerboseOutput.instance.info('[VERBOSE] Processing branch: $branchName');
VerboseOutput.instance.warning('[VERBOSE] API request failed, retrying...');Add timing for new operations:
tracker?.startTimer('new_operation');
try {
await performOperation();
} finally {
tracker?.endTimer('new_operation');
}Log important state changes:
VerboseOutput.instance.info(
'[VERBOSE] Found ${branches.length} branches, '
'${issueNumbers.length} with Gerrit config'
);final resolvedValue = config.resolveFlag(argResults, 'option-name', defaultValue);final batchSize = 10;
for (var i = 0; i < items.length; i += batchSize) {
final batch = items.skip(i).take(batchSize).toList();
await processBatch(batch);
}Future<void> withTempDirectory(Future<void> Function(Directory) action) async {
final tempDir = await Directory.systemTemp.createTemp('dgt_');
try {
await action(tempDir);
} finally {
await tempDir.delete(recursive: true);
}
}- Run
dart analyzeand fix all issues - Run
dart testand ensure all tests pass - Test manually with various repository states
- Update documentation if adding public APIs
- Keep functions focused and single-purpose
- Use descriptive variable and function names
- Add comments for complex logic
- Handle errors appropriately
- Follow existing code patterns
- Profile new operations with
PerformanceTracker - Use batch operations when possible
- Avoid unnecessary Git process spawns
- Consider memory usage for large repositories
This implementation guide provides the foundation for extending the DGT tool while maintaining code quality and performance standards.