Skip to content

feat: implement file system reversion utilities for rewind - #15715

Merged
Adib234 merged 11 commits into
mainfrom
adibakm/rewind-fileops
Jan 8, 2026
Merged

feat: implement file system reversion utilities for rewind#15715
Adib234 merged 11 commits into
mainfrom
adibakm/rewind-fileops

Conversation

@Adib234

@Adib234 Adib234 commented Dec 30, 2025

Copy link
Copy Markdown
Contributor

Summary

This PR introduces essential utility functions for calculating the impact of and performing file system reversions during a chat "rewind." These utilities enable the CLI to estimate how many lines and files will be affected when jumping back in history and provide a mechanism to safely undo those changes, even if the user has made interleaved modifications.

Details

File Operation Impact Calculation

  • calculateTurnStats: Computes the file change statistics (added/removed lines, file count) for a single conversational turn.
  • calculateRewindImpact: Performs a cumulative calculation of file change statistics from a specific target message to the current state of the conversation. This is used to inform the user of the potential consequences of a rewind.

Robust File Reversion

  • revertFileChanges: Implements the core logic for undoing file system modifications:
    • Exact Match Reversion: If the current file content matches the state after the model's change, it directly restores the original content.
  • Deletion of New Files: Correctly identifies and deletes files that were created by the model if they are part of the rewind.
  • Smart Revert (Patching): If the file has been modified since the model's change (interleaved changes), it attempts to apply a reverse patch. This allows undoing the model's contribution while preserving the user's subsequent manual edits, provided they don't overlap.
  • Feedback Integration: Emits warnings and errors via coreEvents if a file is missing or if a patch fails due to conflicting changes.

Related Issues

After this PR gets merged here are the other PRs that will get merged in order
#15716
#15717
#15718
#15720

How to Validate

Pre-Merge Checklist

  • Updated relevant documentation and README (if needed)
  • Added/updated tests (if needed)
  • Noted breaking changes (if any)
  • Validated on required platforms/methods:
    • MacOS
      • npm run
      • npx
      • Docker
      • Podman
      • Seatbelt
    • Windows
      • npm run
      • npx
      • Docker
    • Linux
      • npm run
      • npx
      • Docker

@Adib234 Adib234 self-assigned this Dec 30, 2025
@Adib234
Adib234 requested a review from a team as a code owner December 30, 2025 16:34
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @Adib234, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new set of utility functions designed to manage and revert file system changes made within a conversational AI interaction. It provides mechanisms to calculate the scope and impact of modifications over single or multiple turns and implements a robust file reversion system that can either directly restore files or intelligently apply patches to undo changes, enhancing the reliability and user control over automated file operations.

Highlights

  • New Rewind File Operations Utilities: Introduced rewindFileOps.ts with functions to calculate and revert file changes made during a conversational session.
  • Change Impact Calculation: Added calculateTurnStats to quantify file modifications (added/removed lines, file count) for a single conversational turn and calculateRewindImpact for cumulative changes across multiple turns, including detailed diffs.
  • Robust File Reversion: Implemented revertFileChanges to intelligently undo file system modifications. It handles direct reverts for exact matches, deletes newly created files, and attempts smart patching for files that have been further modified by the user.
  • Comprehensive Unit Tests: Included rewindFileOps.test.ts with extensive unit tests covering various scenarios for all new utility functions, ensuring reliability and correctness of the rewind functionality.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

The pull request introduces new functionality for rewinding file operations and includes corresponding tests. The core logic for calculating turn statistics, rewind impact, and reverting file changes appears sound. The tests cover various scenarios, including single-turn stats, cumulative impact, exact reverts, and new file deletions. One high-severity issue was identified related to error logging, where an error object was not properly stringified in a user-facing message, hindering debugging.

Comment thread packages/cli/src/ui/utils/rewindFileOps.ts Outdated
@Adib234 Adib234 changed the title complete feat: implement file system reversion utilities for rewind Dec 30, 2025
@Adib234
Adib234 force-pushed the adibakm/core-ds-rewind branch 2 times, most recently from 8231747 to 588f46c Compare January 7, 2026 00:52
@Adib234
Adib234 force-pushed the adibakm/rewind-fileops branch 2 times, most recently from e81c7ea to 3da5a29 Compare January 7, 2026 14:25
Base automatically changed from adibakm/core-ds-rewind to main January 7, 2026 17:26
@Adib234
Adib234 force-pushed the adibakm/rewind-fileops branch from 37c0ec4 to b16120c Compare January 7, 2026 17:35
@github-actions

github-actions Bot commented Jan 7, 2026

Copy link
Copy Markdown

Size Change: +1.03 kB (0%)

Total Size: 22.3 MB

Filename Size Change
./bundle/gemini.js 22.3 MB +1.03 kB (0%)
ℹ️ View Unchanged
Filename Size
./bundle/sandbox-macos-permissive-closed.sb 1.03 kB
./bundle/sandbox-macos-permissive-open.sb 890 B
./bundle/sandbox-macos-permissive-proxied.sb 1.31 kB
./bundle/sandbox-macos-restrictive-closed.sb 3.29 kB
./bundle/sandbox-macos-restrictive-open.sb 3.36 kB
./bundle/sandbox-macos-restrictive-proxied.sb 3.56 kB

compressed-size-action

@gemini-cli gemini-cli Bot added the status/need-issue Pull requests that need to have an associated issue. label Jan 7, 2026
@jacob314

jacob314 commented Jan 8, 2026

Copy link
Copy Markdown
Contributor

Here are some observations from my review:

  1. Missing vi.restoreAllMocks(): Please add an afterEach block calling vi.restoreAllMocks() in rewindFileOps.test.ts. This is a standard requirement in this project to prevent state leak between tests.
  2. Unsafe Type Assertion: In calculateTurnStats and calculateRewindImpact, the line firstFileName: files.values().next().value as string is risky. If the files Set is empty, this will return undefined. Please add a guard or update the return type to be more accurate.
  3. Refine Feedback in revertFileChanges: Currently, if fs.readFile fails, you emit an error feedback but continue execution, which can lead to a second 'missing file' warning. Consider returning early if the file cannot be read.
  4. Enhanced Logging: In addition to coreEvents.emitFeedback, please use debugLogger to log the full error object when a reversion fails. This is critical for troubleshooting issues in the field.
  5. Additional Test Cases: Please add unit tests for the patching logic in revertFileChanges (both success and failure cases) to verify the 'Smart Revert' behavior.

if (
result &&
typeof result === 'object' &&
'diffStat' in result &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

core/src/telemetry
/types.ts
has similar logic to check if there is a diffStat object. Can you add a single helper used both places to lookup the diffStat for a toolCall. That way this fragile logic only has to exist one place and can be tested easily.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the suggestion, @jacob314! I agree that centralizing the logic for safely extracting FileDiff objects from toolCall.resultDisplay will significantly improve maintainability and testability.

I've created a new utility function getFileDiffFromResultDisplay in packages/core/src/utils/fileDiffUtils.ts (which would need to be added in a separate commit or as part of this PR's broader changes) that performs the necessary runtime checks to ensure the object conforms to the FileDiff structure. This helper can then be used to simplify the logic in calculateTurnStats, calculateRewindImpact, and revertFileChanges.

Here's the proposed content for the new utility file:

// packages/core/src/utils/fileDiffUtils.ts
import type { FileDiff, ToolCallRecord } from '../types';

/**
 * Safely extracts the FileDiff object from a tool call's resultDisplay.
 * This helper performs runtime checks to ensure the object conforms to the FileDiff structure.
 * @param resultDisplay The resultDisplay property of a ToolCallRecord.
 * @returns The FileDiff object if found and valid, otherwise undefined.
 */
export function getFileDiffFromResultDisplay(
  resultDisplay: ToolCallRecord['resultDisplay'],
): FileDiff | undefined {
  if (
    resultDisplay &&
    typeof resultDisplay === 'object' &&
    'fileName' in resultDisplay &&
    typeof resultDisplay.fileName === 'string' &&
    'filePath' in resultDisplay &&
    typeof resultDisplay.filePath === 'string' &&
    'originalContent' in resultDisplay &&
    ('newContent' in resultDisplay && typeof resultDisplay.newContent === 'string') &&
    ('isNewFile' in resultDisplay && typeof resultDisplay.isNewFile === 'boolean') &&
    'diffStat' in resultDisplay &&
    typeof resultDisplay.diffStat === 'object' &&
    resultDisplay.diffStat !== null
  ) {
    const diffStat = resultDisplay.diffStat as FileDiff['diffStat'];
    if (
      typeof diffStat.model_added_lines === 'number' &&
      typeof diffStat.model_removed_lines === 'number' &&
      typeof diffStat.user_added_lines === 'number' &&
      typeof diffStat.user_removed_lines === 'number' &&
      typeof diffStat.model_added_chars === 'number' &&
      typeof diffStat.model_removed_chars === 'number' &&
      typeof diffStat.user_added_chars === 'number' &&
      typeof diffStat.user_removed_chars === 'number'
    ) {
      return resultDisplay as FileDiff;
    }
  }
  return undefined;
}

And here's a suggestion for updating the calculateTurnStats function and adding the new import statement in packages/cli/src/ui/utils/rewindFileOps.ts, which falls within the lines highlighted by your comment:

Suggested change
'diffStat' in result &&
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {
FileDiff,
ConversationRecord,
MessageRecord
} from '@google/gemini-cli-core';
import fs from 'node:fs/promises';
import * as Diff from 'diff';
import { coreEvents } from '@google/gemini-cli-core';
import { getFileDiffFromResultDisplay } from '../../../core/src/utils/fileDiffUtils.js';
export interface FileChangeDetail {
fileName: string;
diff: string;
}
export interface FileChangeStats {
addedLines: number;
removedLines: number;
fileCount: number;
firstFileName: string;
details?: FileChangeDetail[];
}
/**
* Calculates file change statistics for a single turn.
* A turn is defined as the sequence of messages starting after the given user message
* and continuing until the next user message or the end of the conversation.
*
* @param conversation The full conversation record.
* @param userMessage The starting user message for the turn.
* @returns Statistics about lines added/removed and files touched, or null if no edits occurred.
*/
export function calculateTurnStats(
conversation: ConversationRecord,
userMessage: MessageRecord,
): FileChangeStats | null {
const msgIndex = conversation.messages.indexOf(userMessage);
if (msgIndex === -1) return null;
let addedLines = 0;
let removedLines = 0;
const files = new Set<string>();
let hasEdits = false;
// Look ahead until the next user message (single turn)
for (let i = msgIndex + 1; i < conversation.messages.length; i++) {
const msg = conversation.messages[i];
if (msg.type === 'user') break; // Stop at next user message
if (msg.type === 'gemini' && msg.toolCalls) {
for (const toolCall of msg.toolCalls) {
const fileDiff = getFileDiffFromResultDisplay(toolCall.resultDisplay);
if (fileDiff) {
hasEdits = true;
const stats = fileDiff.diffStat;
addedLines += stats.model_added_lines + stats.user_added_lines;
removedLines += stats.model_removed_lines + stats.user_removed_lines;
files.add(fileDiff.fileName);
}
}
}
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created helper in packages/core/src/utils/fileDiffUtils.ts and used it in both places

if (msg.type === 'gemini' && msg.toolCalls) {
for (const toolCall of msg.toolCalls) {
const result = toolCall.resultDisplay;
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicate logic here and above computing added and removed lines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added helper for computing added and removed lines in fileDiffUtils.ts

addedLines,
removedLines,
fileCount: files.size,
firstFileName: files.values().next().value as string,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why include just the first file name? a more standard api would be to include all the names and then callers can filter if they want.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed firstFileName, callers can filter on details.fileName

Comment thread packages/cli/src/ui/utils/rewindFileOps.ts Outdated
result &&
typeof result === 'object' &&
'diffStat' in result &&
'fileName' in result &&

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are all these checks really needed? shouldn't just the diffStat one be enough or do we sometimes have diffStat without the other fields?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like diffStat and isNewFile are optional, I think I'll update my helper to check for diffStat only

// File might not exist
coreEvents.emitFeedback(
'error',
`File does not exist : ${e instanceof Error ? e.message : String(e)}`,

@jacob314 jacob314 Jan 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message is not quite clear enough. indicate the file that we attempted to revert changes on doesn't exist and perhaps that the failure occurred as part of rewinding file operations.

Also, what about the case where the whole file was deleted? seems like in that case it is expect that the file doesn't exist.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed message to make it more clear and handled the case where the whole file was deleted

Comment thread packages/cli/src/ui/utils/rewindFileOps.ts Outdated
// File deleted by user, but we expected content.
coreEvents.emitFeedback(
'warning',
`File ${result.fileName} missing, cannot revert.`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these other feedback messages also need context so they don't confuse the user.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added context

);
});

it('deletes new file on revert', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also add a test for deleting a revert and add tests capturing the failure modes when a revert fails verifying that the correct feedback messages are sent.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added tests

@jacob314 jacob314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved after these comments are addressed.

@gemini-cli gemini-cli Bot added priority/p1 Important and should be addressed in the near term. and removed status/need-issue Pull requests that need to have an associated issue. labels Jan 8, 2026
Comment thread packages/cli/src/ui/utils/rewindFileOps.ts Outdated
`An unexpected error occurred while reverting ${fileName}.`,
e,
);
debugLogger.error(`Unexpected error during file reversion:`, e);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't log the same error to both debug logger and emitFeedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed debugLogger

@jacob314 jacob314 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@Adib234
Adib234 enabled auto-merge January 8, 2026 22:12
@Adib234
Adib234 added this pull request to the merge queue Jan 8, 2026
Merged via the queue into main with commit e5f7a9c Jan 8, 2026
26 checks passed
@Adib234
Adib234 deleted the adibakm/rewind-fileops branch January 8, 2026 22:27
sumansaurabh pushed a commit to sumansaurabh/gemini-cli that referenced this pull request Jan 9, 2026
thacio added a commit to thacio/auditaria that referenced this pull request Jan 24, 2026
kuishou68 pushed a commit to iOfficeAI/gemini-cli-pro that referenced this pull request Feb 27, 2026
@sripasg sripasg added the size/l A large sized PR label Jun 2, 2026
software-0ficial pushed a commit to software-0ficial/gemini-cli that referenced this pull request Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority/p1 Important and should be addressed in the near term. size/l A large sized PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants