summaryrefslogtreecommitdiff
path: root/packages/core/src/services/gitService.ts
diff options
context:
space:
mode:
authorLouis Jimenez <[email protected]>2025-06-11 15:33:09 -0400
committerGitHub <[email protected]>2025-06-11 15:33:09 -0400
commite0f4f428fc6bef4f81db379ce1e0368004079c76 (patch)
tree4f9be0dc0f8fe11de7b83c32e56bf55314de9d93 /packages/core/src/services/gitService.ts
parentf75c48323ce65f651381c74ae75a1795e7cc5c45 (diff)
Restore Checkpoint Feature (#934)
Diffstat (limited to 'packages/core/src/services/gitService.ts')
-rw-r--r--packages/core/src/services/gitService.ts132
1 files changed, 132 insertions, 0 deletions
diff --git a/packages/core/src/services/gitService.ts b/packages/core/src/services/gitService.ts
new file mode 100644
index 00000000..8cd6b887
--- /dev/null
+++ b/packages/core/src/services/gitService.ts
@@ -0,0 +1,132 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as fs from 'fs/promises';
+import * as path from 'path';
+import { isNodeError } from '../utils/errors.js';
+import { isGitRepository } from '../utils/gitUtils.js';
+import { exec } from 'node:child_process';
+import { simpleGit, SimpleGit, CheckRepoActions } from 'simple-git';
+
+export const historyDirName = '.gemini_cli_history';
+
+export class GitService {
+ private projectRoot: string;
+
+ constructor(projectRoot: string) {
+ this.projectRoot = path.resolve(projectRoot);
+ }
+
+ async initialize(): Promise<void> {
+ if (!isGitRepository(this.projectRoot)) {
+ throw new Error('GitService requires a Git repository');
+ }
+ const gitAvailable = await this.verifyGitAvailability();
+ if (!gitAvailable) {
+ throw new Error('GitService requires Git to be installed');
+ }
+ this.setupHiddenGitRepository();
+ }
+
+ verifyGitAvailability(): Promise<boolean> {
+ return new Promise((resolve) => {
+ exec('git --version', (error) => {
+ if (error) {
+ resolve(false);
+ } else {
+ resolve(true);
+ }
+ });
+ });
+ }
+
+ /**
+ * Creates a hidden git repository in the project root.
+ * The Git repository is used to support checkpointing.
+ */
+ async setupHiddenGitRepository() {
+ const historyDir = path.join(this.projectRoot, historyDirName);
+ const repoDir = path.join(historyDir, 'repository');
+
+ await fs.mkdir(repoDir, { recursive: true });
+ const repoInstance: SimpleGit = simpleGit(repoDir);
+ const isRepoDefined = await repoInstance.checkIsRepo(
+ CheckRepoActions.IS_REPO_ROOT,
+ );
+ if (!isRepoDefined) {
+ await repoInstance.init();
+ try {
+ await repoInstance.raw([
+ 'worktree',
+ 'add',
+ this.projectRoot,
+ '--force',
+ ]);
+ } catch (error) {
+ console.log('Failed to add worktree:', error);
+ }
+ }
+
+ const visibileGitIgnorePath = path.join(this.projectRoot, '.gitignore');
+ const hiddenGitIgnorePath = path.join(repoDir, '.gitignore');
+
+ let visibileGitIgnoreContent = ``;
+ try {
+ visibileGitIgnoreContent = await fs.readFile(
+ visibileGitIgnorePath,
+ 'utf-8',
+ );
+ } catch (error) {
+ if (isNodeError(error) && error.code !== 'ENOENT') {
+ throw error;
+ }
+ }
+
+ await fs.writeFile(hiddenGitIgnorePath, visibileGitIgnoreContent);
+
+ if (!visibileGitIgnoreContent.includes(historyDirName)) {
+ const updatedContent = `${visibileGitIgnoreContent}\n# Gemini CLI history directory\n${historyDirName}\n`;
+ await fs.writeFile(visibileGitIgnorePath, updatedContent);
+ }
+
+ const commit = await repoInstance.raw([
+ 'rev-list',
+ '--all',
+ '--max-count=1',
+ ]);
+ if (!commit) {
+ await repoInstance.add(hiddenGitIgnorePath);
+
+ await repoInstance.commit('Initial commit');
+ }
+ }
+
+ private get hiddenGitRepository(): SimpleGit {
+ const historyDir = path.join(this.projectRoot, historyDirName);
+ const repoDir = path.join(historyDir, 'repository');
+ return simpleGit(this.projectRoot).env({
+ GIT_DIR: path.join(repoDir, '.git'),
+ GIT_WORK_TREE: this.projectRoot,
+ });
+ }
+
+ async getCurrentCommitHash(): Promise<string> {
+ const hash = await this.hiddenGitRepository.raw('rev-parse', 'HEAD');
+ return hash.trim();
+ }
+
+ async createFileSnapshot(message: string): Promise<string> {
+ const repo = this.hiddenGitRepository;
+ await repo.add('.');
+ const commitResult = await repo.commit(message);
+ return commitResult.commit;
+ }
+
+ async restoreProjectFromSnapshot(commitHash: string): Promise<void> {
+ const repo = this.hiddenGitRepository;
+ await repo.raw(['restore', '--source', commitHash, '.']);
+ }
+}