src — undo
src — undo
The src/undo module provides a robust checkpointing and undo/redo system for managing file state changes within a working directory. It allows Code Buddy to create snapshots of the file system, revert to previous states, and view differences between checkpoints, ensuring a safety net for potentially destructive operations.
Overview
The primary goal of this module is to offer a reliable mechanism for tracking and reverting changes made by automated tools or user actions. It acts as a local version control system, independent of Git, specifically for the operations performed by Code Buddy.
Key Features:
- File State Checkpoints: Capture the state of specified files at a given moment.
- Undo/Redo Operations: Navigate forward and backward through the history of checkpoints.
- Checkpoint Naming & Tagging: Organize checkpoints with descriptive names and searchable tags.
- Diff Viewing: Generate detailed diffs between any two checkpoints.
- Automatic Checkpoints: Configurable automatic checkpoints, especially before "dangerous" operations (e.g., file deletion, overwrites).
- Git Integration: Records current Git branch and commit hash in checkpoint metadata for context.
Core Concepts & Data Structures
The module defines several interfaces to structure the checkpoint data:
Checkpoint: Represents a single snapshot of the working directory's state.id: Unique identifier.name,description: Human-readable labels.timestamp: When the checkpoint was created.files: An array ofCheckpointFileobjects detailing the state of each tracked file.metadata: Contextual information (CheckpointMetadata).tags: String tags for categorization.parentId: Links to the previous checkpoint, forming a linear history.CheckpointFile: Describes a single file's state within a checkpoint.path,relativePath: Absolute and relative paths to the file.hash: SHA256 hash of the file's content.size,mode: File size and permissions.exists,isNew,isDeleted: Flags indicating the file's status relative to the working directory or previous checkpoints.CheckpointMetadata: Provides operational context for a checkpoint.workingDirectory: The root directory being tracked.gitBranch,gitCommit: Git information at the time of checkpoint creation.operation: A string describing the action that led to the checkpoint (e.g., 'auto', 'refactor', 'delete').tool: The tool that initiated the operation.automatic: Boolean indicating if the checkpoint was created automatically.FileChange: Used for representing differences between two checkpoints.path: Relative path of the changed file.type:'created','modified', or'deleted'.oldContent,newContent: Optional content for diffing.diff: A patch-formatted string representing the changes.CheckpointConfig: Defines the configurable behavior of theCheckpointManager.enabled: Master switch for the system.maxCheckpoints: Maximum number of checkpoints to retain.autoCheckpoint,autoCheckpointInterval: Settings for periodic automatic checkpoints.checkpointOnDangerousOps: Whether to automatically checkpoint before operations marked as dangerous.excludePatterns: Glob patterns for files/directories to ignore.maxFileSize: Maximum file size to checkpoint (in bytes).compressCheckpoints: (Currently unimplemented, but part of the interface).
The CheckpointManager Class
The CheckpointManager class is the central component of this module. It extends TypedEventEmitter, allowing it to emit type-safe events for various lifecycle actions.
Initialization and Configuration
The CheckpointManager is instantiated with a workingDirectory and an optional Partial. It merges the provided config with DEFAULT_CONFIG.
const manager = new CheckpointManager(39;/path/to/project39;, {
maxCheckpoints: 50,
autoCheckpoint: true,
});
Upon construction, the initialize() method is called, which:
- Ensures the necessary data directories exist (
~/.codebuddy/checkpoints/)./files - Loads existing checkpoints from
index.jsonwithin the data directory. - Starts the
autoCheckpointTimerifconfig.autoCheckpointis enabled.
Data Persistence
Checkpoints are stored on the local filesystem:
- Index File:
~/.codebuddy/checkpoints/stores an array of/index.json Checkpointobjects and thecurrentIndex. - File Contents: The actual content of files at each checkpoint is stored in
~/.codebuddy/checkpoints/./files/ /
The saveIndex() method is responsible for writing the current state of this.checkpoints and this.currentIndex to index.json. This method is called after any operation that modifies the checkpoint history (creation, deletion, tagging, renaming, restoration).
Checkpoint Lifecycle
Creating Checkpoints
The createCheckpoint(options) method is the core function for capturing a snapshot.
- It generates a unique ID and timestamp.
- It determines which files to checkpoint: either explicitly provided in
options.filesor all tracked files in theworkingDirectory(viagetTrackedFiles()). - For each file,
snapshotFile()is called:
- It checks
excludePatternsandmaxFileSize. - Reads the file content, calculates its SHA256 hash.
- Saves the file content to its specific checkpoint directory (
files/)./ - Determines if the file is new, deleted, or existing.
- It fetches Git information (
getGitInfoAsync()) to enrich the checkpoint metadata. - The new
Checkpointobject is added tothis.checkpoints. - Any checkpoints after the new
currentIndexare removed (clearing redo history). enforceMaxCheckpoints()is called to prune older checkpoints ifmaxCheckpointsis exceeded.- The index is saved, and a
checkpoint:createdevent is emitted.
Managing Checkpoint History (Undo/Redo)
undo(): Moves thecurrentIndexone step backward and callsrestoreCheckpoint()to revert to the previous state. Emitsundo:noopif no previous checkpoint exists, orundo:completeon success.redo(): Moves thecurrentIndexone step forward and callsrestoreCheckpoint()to apply the next state. Emitsredo:noopif no next checkpoint exists, orredo:completeon success.canUndo()/canRedo(): Simple boolean checks to determine if undo/redo operations are possible.
Restoring Checkpoints
The restoreCheckpoint(checkpoint, operation) method is central to reverting file states.
- It first creates a safety checkpoint (
"Before ${operation}") to allow reverting the restore itself. - It iterates through all files recorded in the
targetCheckpoint:
- If a file was marked as
isDeletedor!existsin the checkpoint, it removes the file from theworkingDirectory. - Otherwise, it copies the file content from the checkpoint's storage path to the
workingDirectoryand restores its file mode.
- Updates
this.currentIndexto point to the restored checkpoint. - Saves the index.
- Emits
undo:complete,redo:complete, orrestore:completebased on theoperationparameter.
graph TD
A[undo() / redo()] --> B{Target Checkpoint Exists?};
B -- No --> C[Emit :noop event];
B -- Yes --> D[restoreCheckpoint(target, operation)];
D --> E[createCheckpoint("Before restore", automatic=true)];
E --> F{For each file in target Checkpoint};
F -- File deleted/non-existent --> G[fs.remove(targetPath)];
F -- File exists --> H[fs.copy(storagePath, targetPath)];
H --> I[fs.chmod(targetPath, file.mode)];
I --> J[Add to restoredFiles];
G --> J;
J --> F;
F -- All files processed --> K[Update currentIndex];
K --> L[saveIndex()];
L --> M[Emit :complete event];
M --> N[Return UndoResult];
Diffing Checkpoints
The getDiff(fromId, toId) method calculates the differences between two specified checkpoints.
- It retrieves the
fromCheckpointandtoCheckpointby their IDs. - It compares the files present in both checkpoints:
- If a file exists in
toCheckpointbut notfromCheckpoint(or was deleted infromCheckpoint), it's a'created'file. - If a file exists in
fromCheckpointbut nottoCheckpoint(or was deleted intoCheckpoint), it's a'deleted'file. - If a file exists in both but has different
hashvalues, it's'modified'.
- For modified files, it uses the
diff_match_patchlibrary (this.dmp) to generate a semantic diff and then converts it to a patch text. - Returns an array of
FileChangeobjects.
Maintaining Checkpoints
enforceMaxCheckpoints(): Called aftercreateCheckpointto remove the oldest checkpoints ifthis.checkpoints.lengthexceedsconfig.maxCheckpoints. It also callsdeleteCheckpointFiles()for the removed checkpoints.deleteCheckpoint(id): Removes a specific checkpoint and its associated file contents from disk. AdjustscurrentIndexif the deleted checkpoint was before or at the current position. Emitscheckpoint:deleted.tagCheckpoint(id, tag): Adds a tag to a checkpoint.renameCheckpoint(id, name): Changes the name of a checkpoint.getCheckpoints(),getCurrentCheckpoint(),getCheckpoint(id),searchCheckpoints(query): Provide ways to retrieve and query checkpoint information.
Automatic Checkpointing
startAutoCheckpoint(): Initiates asetIntervaltimer to periodically callcreateCheckpoint()withautomatic: true.stopAutoCheckpoint(): Clears the auto-checkpoint timer.shouldAutoCheckpoint(operation): Checks if a givenoperationstring matches any of theDANGEROUS_OPERATIONSdefined in the configuration, indicating if an automatic checkpoint should be triggered.
Eventing
CheckpointManager extends TypedEventEmitter, allowing external components to subscribe to its events:
checkpoint:created: Emitted after a new checkpoint is successfully created.checkpoint:deleted: Emitted when a checkpoint is removed.undo:noop,redo:noop: Emitted when an undo/redo operation is attempted but no history is available.undo:complete,redo:complete,restore:complete: Emitted after a successful undo, redo, or direct restore operation.
Usage Examples (Conceptual)
import { createCheckpointManager } from 39;./undo/checkpoint-manager.js39;;
async function main() {
const workingDir = 39;/tmp/my-project39;;
class="hl-cmt">// Ensure working directory exists for demonstration
await fs.ensureDir(workingDir);
await fs.writeFile(path.join(workingDir, 39;file1.txt39;), 39;Initial content39;);
const manager = createCheckpointManager(workingDir, {
maxCheckpoints: 5,
autoCheckpoint: false, class="hl-cmt">// Disable for manual control in example
});
class="hl-cmt">// 1. Create an initial checkpoint
let cp1 = await manager.createCheckpoint({
name: 39;Initial State39;,
operation: 39;init39;,
description: 39;First checkpoint after project setup.39;,
});
console.log(`Created checkpoint: ${cp1.name} (${cp1.id})`);
class="hl-cmt">// 2. Make some changes and create another checkpoint
await fs.writeFile(path.join(workingDir, 39;file1.txt39;), 39;Modified content39;);
await fs.writeFile(path.join(workingDir, 39;new_file.txt39;), 39;Hello world39;);
let cp2 = await manager.createCheckpoint({
name: 39;Added new file39;,
operation: 39;add_feature39;,
tags: [39;feature39;, 39;v139;],
});
console.log(`Created checkpoint: ${cp2.name} (${cp2.id})`);
class="hl-cmt">// 3. View status
console.log(39;\n--- Current Status ---39;);
console.log(manager.formatStatus());
class="hl-cmt">// 4. Undo to the previous state
console.log(39;\n--- Performing Undo ---39;);
const undoResult = await manager.undo();
if (undoResult?.success) {
console.log(`Undo successful. Restored files: ${undoResult.restoredFiles.join(39;, 39;)}`);
console.log(39;Content of file1.txt after undo:39;, await fs.readFile(path.join(workingDir, 39;file1.txt39;), 39;utf-839;));
console.log(39;new_file.txt exists after undo:39;, await fs.pathExists(path.join(workingDir, 39;new_file.txt39;)));
} else {
console.error(39;Undo failed:39;, undoResult?.errors);
}
class="hl-cmt">// 5. Redo to the next state
console.log(39;\n--- Performing Redo ---39;);
const redoResult = await manager.redo();
if (redoResult?.success) {
console.log(`Redo successful. Restored files: ${redoResult.restoredFiles.join(39;, 39;)}`);
console.log(39;Content of file1.txt after redo:39;, await fs.readFile(path.join(workingDir, 39;file1.txt39;), 39;utf-839;));
console.log(39;new_file.txt exists after redo:39;, await fs.pathExists(path.join(workingDir, 39;new_file.txt39;)));
} else {
console.error(39;Redo failed:39;, redoResult?.errors);
}
class="hl-cmt">// 6. Get diff between checkpoints
console.log(39;\n--- Getting Diff between CP1 and CP2 ---39;);
const diffs = await manager.getDiff(cp1.id, cp2.id);
diffs.forEach(change => {
console.log(`File: ${change.path}, Type: ${change.type}`);
if (change.diff) {
console.log(39;Diff:\n39;, change.diff);
}
});
class="hl-cmt">// 7. Clean up
manager.dispose();
await fs.remove(workingDir);
}
main().catch(console.error);
Integration Points
The CheckpointManager is a self-contained module but interacts with several external libraries and is consumed by other parts of the Code Buddy application.
Dependencies (Outgoing Calls)
fs-extra: Heavily used for all file system operations:ensureDir,pathExists,readJSON,writeJSON,readFile,writeFile,stat,readdir,remove,copy,chmod. This is critical for managing checkpoint storage and restoring files.path: Standard Node.js module for path manipulation (join,relative,dirname,isAbsolute).os: Standard Node.js module for getting the home directory (os.homedir()).crypto: Standard Node.js module for generating unique IDs (randomBytes,createHash) and hashing file contents.child_process(spawn): Used byexecGitCommandto run Git commands (e.g.,rev-parse) to fetch branch and commit information.diff-match-patch: An external library (dmp) used bygetDiffto compute and format file differences.../types/index.js(getErrorMessage): Utility function for consistent error message formatting.../events/index.js(TypedEventEmitter,CheckpointEvents): Provides the base class for type-safe event emission.
Consumers (Incoming Calls)
The CheckpointManager is primarily consumed by command handlers, suggesting its integration into Code Buddy's command-line interface or internal operations.
commands/handlers/missing-handlers.ts:handleDiffCheckpoints: Likely usescreateCheckpointManagerandgetDiffto show differences between checkpoints.handleRestoreCheckpoint: Likely usesrestoreCheckpointto revert to a specific checkpoint.handleListCheckpoints: Likely usescreateCheckpointManagerandgetCheckpointsto display available checkpoints.tests/unit/checkpoint-manager.test.ts&tests/checkpoint-manager.test.ts: Extensive unit and integration tests validate the functionality ofCheckpointManagermethods likecreateCheckpointManager,createCheckpoint,undo,redo,restoreCheckpoint,getDiff,getCheckpoints,getCurrentCheckpoint,getCheckpoint,searchCheckpoints,tagCheckpoint,renameCheckpoint,deleteCheckpoint,canUndo,canRedo,shouldAutoCheckpoint, andformatStatus.