From 111a6e4d6300cf4c7bfdf1d2e8c679945be209f5 Mon Sep 17 00:00:00 2001 From: Rahul Yadav Date: Fri, 4 Oct 2024 17:10:48 +0530 Subject: [PATCH] feat: git integration - Initilize git repository - List untracked files - Add files to staging area --- package-lock.json | 178 +++++++++++-- package.json | 1 + .../git/ManageGit/ManageGit.module.scss | 79 ++++++ src/components/git/ManageGit/ManageGit.tsx | 236 ++++++++++++++++++ src/components/git/ManageGit/index.ts | 1 + src/components/git/index.ts | 1 + src/components/ui/icon/index.tsx | 11 +- .../workspace/WorkSpace/WorkSpace.module.scss | 2 +- .../workspace/WorkSpace/WorkSpace.tsx | 8 +- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 7 +- src/constant/AppData.ts | 2 +- src/interfaces/git.interface.ts | 11 + src/lib/fs.ts | 4 + src/lib/git.ts | 141 +++++++++++ src/workers/git.ts | 88 +++++++ 15 files changed, 741 insertions(+), 29 deletions(-) create mode 100644 src/components/git/ManageGit/ManageGit.module.scss create mode 100644 src/components/git/ManageGit/ManageGit.tsx create mode 100644 src/components/git/ManageGit/index.ts create mode 100644 src/components/git/index.ts create mode 100644 src/interfaces/git.interface.ts create mode 100644 src/lib/git.ts create mode 100644 src/workers/git.ts diff --git a/package-lock.json b/package-lock.json index 3a0eb90..61a996b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,7 @@ "eslint": "^8.56.0", "eslint-config-next": "13.1.6", "eventemitter3": "^5.0.1", + "isomorphic-git": "^1.27.1", "jsonwebtoken": "^9.0.0", "lodash.clonedeep": "^4.5.0", "mixpanel-browser": "^2.47.0", @@ -2177,6 +2178,11 @@ "version": "0.0.8", "license": "MIT" }, + "node_modules/async-lock": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" + }, "node_modules/async-validator": { "version": "4.2.5", "license": "MIT" @@ -2517,6 +2523,11 @@ "version": "2.3.2", "license": "MIT" }, + "node_modules/clean-git-ref": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", + "integrity": "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw==" + }, "node_modules/client-only": { "version": "0.0.1", "license": "MIT" @@ -2722,6 +2733,20 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-equal": { "version": "2.2.3", "license": "MIT", @@ -2834,6 +2859,11 @@ "react": ">=16" } }, + "node_modules/diff3": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", + "integrity": "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g==" + }, "node_modules/dir-glob": { "version": "3.0.1", "license": "MIT", @@ -4284,18 +4314,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/ipfs-unixfs-importer/node_modules/readable-stream": { - "version": "3.6.2", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/is-alphabetical": { "version": "2.0.1", "license": "MIT", @@ -4676,6 +4694,30 @@ "whatwg-fetch": "^3.4.1" } }, + "node_modules/isomorphic-git": { + "version": "1.27.1", + "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.27.1.tgz", + "integrity": "sha512-X32ph5zIWfT75QAqW2l3JCIqnx9/GWd17bRRehmn3qmWc34OYbSXY6Cxv0o9bIIY+CWugoN4nQFHNA+2uYf2nA==", + "dependencies": { + "async-lock": "^1.4.1", + "clean-git-ref": "^2.0.1", + "crc-32": "^1.2.0", + "diff3": "0.0.3", + "ignore": "^5.1.4", + "minimisted": "^2.0.0", + "pako": "^1.0.10", + "pify": "^4.0.1", + "readable-stream": "^3.4.0", + "sha.js": "^2.4.9", + "simple-get": "^4.0.1" + }, + "bin": { + "isogit": "cli.cjs" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/isomorphic-textencoder": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/isomorphic-textencoder/-/isomorphic-textencoder-1.0.1.tgz", @@ -5609,6 +5651,17 @@ "node": ">= 0.6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "license": "ISC", @@ -5626,6 +5679,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minimisted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minimisted/-/minimisted-2.0.1.tgz", + "integrity": "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA==", + "dependencies": { + "minimist": "^1.2.5" + } + }, "node_modules/mixpanel-browser": { "version": "2.47.0", "license": "Apache-2.0" @@ -6037,6 +6098,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, "node_modules/param-case": { "version": "3.0.4", "license": "MIT", @@ -6146,6 +6212,14 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "engines": { + "node": ">=6" + } + }, "node_modules/popmotion": { "version": "11.0.3", "license": "MIT", @@ -6340,18 +6414,6 @@ "ieee754": "^1.2.1" } }, - "node_modules/rabin-wasm/node_modules/readable-stream": { - "version": "3.6.2", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -7094,6 +7156,19 @@ "node": ">=10" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "license": "MIT", @@ -7588,6 +7663,18 @@ "node": ">=6.9" } }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "license": "MIT", @@ -7621,6 +7708,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "3.0.0", "license": "MIT", diff --git a/package.json b/package.json index e05af45..2fdeca3 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "eslint": "^8.56.0", "eslint-config-next": "13.1.6", "eventemitter3": "^5.0.1", + "isomorphic-git": "^1.27.1", "jsonwebtoken": "^9.0.0", "lodash.clonedeep": "^4.5.0", "mixpanel-browser": "^2.47.0", diff --git a/src/components/git/ManageGit/ManageGit.module.scss b/src/components/git/ManageGit/ManageGit.module.scss new file mode 100644 index 0000000..e44a2be --- /dev/null +++ b/src/components/git/ManageGit/ManageGit.module.scss @@ -0,0 +1,79 @@ +.root { + .collapse, + .collapsePanel { + background-color: transparent !important; + user-select: none; + } + .collapsePanel { + border: 0; + + > div { + padding: 0 !important; + background: transparent !important; + } + .collapseHeader { + display: flex; + align-items: center; + justify-content: space-between; + position: relative; + } + .action { + display: inline-flex; + cursor: pointer; + padding: 0.2rem; + background-color: var(--light-grey); + border-radius: 50%; + position: absolute; + right: -0.5rem; + &:hover { + background-color: var(--light-grey); + } + } + .fileItem { + opacity: 0.9; + display: flex; + align-items: center; + gap: 0.5rem; + justify-content: space-between; + position: relative; + &:hover { + .action { + visibility: visible; + } + } + .fileDetails { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .action { + visibility: hidden; + &:hover { + visibility: visible; + } + } + } + .filePath { + opacity: 0.6; + font-size: 0.8rem; + } + ul { + padding: 0; + list-style-type: none; + li { + border-left: 1px solid #d9d9d9; + padding-left: 0.9rem; + margin-left: 0.3rem; + } + } + div[class*='ant-collapse-content-box'] { + padding: 0; + } + div[class*='ant-collapse-header'] { + font-weight: 600; + } + div[class*='ant-collapse-expand-icon'] { + padding-inline-end: 5px; + } + } +} diff --git a/src/components/git/ManageGit/ManageGit.tsx b/src/components/git/ManageGit/ManageGit.tsx new file mode 100644 index 0000000..51f3989 --- /dev/null +++ b/src/components/git/ManageGit/ManageGit.tsx @@ -0,0 +1,236 @@ +import { Tooltip } from '@/components/ui'; +import AppIcon from '@/components/ui/icon'; +import { useProject } from '@/hooks/projectV2.hooks'; +import { IGitWorkerMessage, InitRepo } from '@/interfaces/git.interface'; +import GitManager from '@/lib/git'; +import EventEmitter from '@/utility/eventEmitter'; +import { Button, Collapse } from 'antd'; +import { FC, useEffect, useRef, useState } from 'react'; +import s from './ManageGit.module.scss'; + +interface IFileCollection { + path: string; + status: 'U' | 'A' | 'M' | 'D'; + staged: boolean; +} + +const ManageGit: FC = () => { + const workerRef = useRef(); + const { activeProject } = useProject(); + const [isGitInitialized, setIsGitInitialized] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [fileCollection, setFileCollection] = useState([]); + + const initGit = () => { + if (!activeProject?.path) { + console.log('Project path not found'); + return; + } + + const workerMessage: IGitWorkerMessage = { + type: 'init', + payload: { + data: { + projectPath: activeProject.path, + }, + }, + }; + if (workerRef.current) { + workerRef.current.postMessage(workerMessage); + } + }; + + const getFilesToCommit = () => { + workerRef.current?.postMessage({ + type: 'getFilesToCommit', + payload: { data: { projectPath: activeProject?.path } }, + }); + }; + + const handleFiles = ( + filePath: string | 'none', + action: 'add' | 'unstage', + all = false, + ) => { + if (!activeProject?.path) return; + + let files = []; + if (all) { + const filterType = action === 'add' ? 'changed' : 'staged'; + files = filterdFiles(filterType).map((file) => ({ path: file.path })); + if (files.length === 0) return; + } else { + files = [{ path: filePath }]; + } + + workerRef.current?.postMessage({ + type: action === 'add' ? 'addFiles' : 'unstageFile', + payload: { + data: { files, projectPath: activeProject.path }, + }, + }); + }; + + const filterdFiles = (type: 'staged' | 'changed') => { + if (type === 'staged') { + return fileCollection.filter((file) => file.staged); + } + return fileCollection.filter((file) => !file.staged); + }; + + const onMount = async () => { + if (!activeProject?.path) return; + const git = new GitManager(); + const _isInitialized = await git.isInitialized(activeProject.path); + setIsGitInitialized(_isInitialized); + if (_isInitialized) { + getFilesToCommit(); + } + setIsLoading(false); + }; + + useEffect(() => { + onMount(); + workerRef.current = new Worker( + new URL('@/workers/git.ts', import.meta.url), + { + type: 'module', + }, + ); + workerRef.current.onmessage = (e) => { + const { type, projectPath } = e.data; + + if (type === 'GIT_INITIALIZED') { + EventEmitter.emit('RELOAD_PROJECT_FILES', projectPath); + setIsGitInitialized(true); + } else if (type === 'FILES_TO_COMMIT') { + setFileCollection(e.data.payload); + } + const actionsForReload = [ + 'GIT_INITIALIZED', + 'FILES_ADDED', + 'FILE_UNSTAGED', + ]; + if (actionsForReload.includes(type)) { + getFilesToCommit(); + } + }; + workerRef.current.onerror = (error) => { + console.error('Worker error:', error); + }; + return () => { + if (workerRef.current) { + workerRef.current.terminate(); + } + }; + }, []); + + if (!activeProject?.path || isLoading) { + return <>; + } + + const renderCategoryWiseFiles = ( + files: IFileCollection[], + staged: boolean, + ) => { + return ( +
    + {files.map((file) => ( +
  • +
    + {file.path.split('/').pop()}{' '} + + {file.path.split('/').slice(0, -1).join('/')} + +
    + + { + handleFiles(file.path, staged ? 'unstage' : 'add'); + }} + > + + + + +
    + {file.status && `${file.status}`} +
    +
  • + ))} +
+ ); + }; + + const renderFiles = () => { + if (!fileCollection.length) { + return
No changes
; + } + const stagedFiles = filterdFiles('staged'); + const unstagedFiles = filterdFiles('changed'); + const header = (staged = false) => { + return ( +
+ {staged ? 'Staged' : 'Changes'} + + { + handleFiles('none', staged ? 'unstage' : 'add', true); + }} + > + + + +
+ ); + }; + return ( +
+ + {stagedFiles.length > 0 && ( + + {renderCategoryWiseFiles(stagedFiles, true)} + + )} + + + {renderCategoryWiseFiles(unstagedFiles, false)} + + +
+ ); + }; + + return ( +
+ {!isGitInitialized && ( + + )} + {isGitInitialized && renderFiles()} +
+ ); +}; + +export default ManageGit; diff --git a/src/components/git/ManageGit/index.ts b/src/components/git/ManageGit/index.ts new file mode 100644 index 0000000..22d9ef0 --- /dev/null +++ b/src/components/git/ManageGit/index.ts @@ -0,0 +1 @@ +export { default } from './ManageGit'; diff --git a/src/components/git/index.ts b/src/components/git/index.ts new file mode 100644 index 0000000..e32f9dd --- /dev/null +++ b/src/components/git/index.ts @@ -0,0 +1 @@ +export { default as ManageGit } from './ManageGit'; diff --git a/src/components/ui/icon/index.tsx b/src/components/ui/icon/index.tsx index c966a10..f16941d 100644 --- a/src/components/ui/icon/index.tsx +++ b/src/components/ui/icon/index.tsx @@ -1,10 +1,13 @@ import { FC } from 'react'; import { + AiOutlineBranches, AiOutlineDelete, AiOutlineDownload, AiOutlineGoogle, AiOutlineHome, AiOutlineLogout, + AiOutlineMinus, + AiOutlinePlus, AiOutlineProject, AiOutlineReload, } from 'react-icons/ai'; @@ -42,6 +45,8 @@ export type AppIconType = | 'Code' | 'Beaker' | 'Plus' + | 'Plus2' + | 'Minus' | 'Home' | 'AngleUp' | 'AngleDown' @@ -64,7 +69,8 @@ export type AppIconType = | 'Clear' | 'Download' | 'Import' - | 'Reload'; + | 'Reload' + | 'GitBranch'; export interface AppIconInterface { name: AppIconType; @@ -90,6 +96,8 @@ const Components = { Setting, Clone: FaRegClone, Plus, + Plus2: AiOutlinePlus, + Minus: AiOutlineMinus, Delete: AiOutlineDelete, Play: BsFillPlayFill, Document: HiDocumentText, @@ -103,6 +111,7 @@ const Components = { Download: AiOutlineDownload, Import, Reload: AiOutlineReload, + GitBranch: AiOutlineBranches, }; const AppIcon: FC = ({ name, className = '' }) => { diff --git a/src/components/workspace/WorkSpace/WorkSpace.module.scss b/src/components/workspace/WorkSpace/WorkSpace.module.scss index 42f7884..76b8ef5 100644 --- a/src/components/workspace/WorkSpace/WorkSpace.module.scss +++ b/src/components/workspace/WorkSpace/WorkSpace.module.scss @@ -90,7 +90,7 @@ justify-content: flex-end; } } - .testCaseArea { + .commonContainer { padding: 1rem; height: 100%; } diff --git a/src/components/workspace/WorkSpace/WorkSpace.tsx b/src/components/workspace/WorkSpace/WorkSpace.tsx index 6640b72..7a296dd 100644 --- a/src/components/workspace/WorkSpace/WorkSpace.tsx +++ b/src/components/workspace/WorkSpace/WorkSpace.tsx @@ -1,5 +1,6 @@ 'use client'; +import { ManageGit } from '@/components/git'; import { DownloadProject } from '@/components/project'; import { ProjectTemplate } from '@/components/template'; import { AppConfig } from '@/config/AppConfig'; @@ -220,10 +221,15 @@ const WorkSpace: FC = () => { /> )} {activeMenu === 'test-cases' && ( -
+
)} + {activeMenu === 'git' && ( +
+ +
+ )}
{isLoaded && ( diff --git a/src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx b/src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx index af01b9d..172afad 100644 --- a/src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/src/components/workspace/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -7,7 +7,7 @@ import Link from 'next/link'; import { FC } from 'react'; import s from './WorkspaceSidebar.module.scss'; -export type WorkSpaceMenu = 'code' | 'build' | 'test-cases' | 'setting'; +export type WorkSpaceMenu = 'code' | 'build' | 'test-cases' | 'setting' | 'git'; interface MenuItem { label: string; value: WorkSpaceMenu; @@ -57,6 +57,11 @@ const WorkspaceSidebar: FC = ({ value: 'test-cases', icon: 'Test', }, + { + label: 'Git', + value: 'git', + icon: 'GitBranch', + }, ]; const settingContent = () => ( diff --git a/src/constant/AppData.ts b/src/constant/AppData.ts index 7dd049c..f46d979 100644 --- a/src/constant/AppData.ts +++ b/src/constant/AppData.ts @@ -1,7 +1,7 @@ export const AppData = { socials: [ { - label: 'GitHub', + label: 'Project GitHub', icon: 'GitHub', url: 'https://github.com/nujan-io/nujan-ide', }, diff --git a/src/interfaces/git.interface.ts b/src/interfaces/git.interface.ts new file mode 100644 index 0000000..ab6c116 --- /dev/null +++ b/src/interfaces/git.interface.ts @@ -0,0 +1,11 @@ +export interface IGitWorkerMessage { + type: 'init' | 'data' | 'error'; + payload?: { + id?: string; + data: T; + }; +} + +export interface InitRepo { + projectPath: string; +} diff --git a/src/lib/fs.ts b/src/lib/fs.ts index 9455a60..1e70fc3 100644 --- a/src/lib/fs.ts +++ b/src/lib/fs.ts @@ -6,6 +6,10 @@ class FileSystem { this.fs = fs; } + get fsInstance() { + return this.fs; + } + async readFile(path: string) { if (!(await this.exists(path))) { throw new Error(`File not found: ${path}`); diff --git a/src/lib/git.ts b/src/lib/git.ts new file mode 100644 index 0000000..56aa008 --- /dev/null +++ b/src/lib/git.ts @@ -0,0 +1,141 @@ +import { PromisifiedFS } from '@isomorphic-git/lightning-fs'; +import git from 'isomorphic-git'; +import fileSystem from './fs'; + +class GitManager { + private fs: PromisifiedFS; + + constructor() { + this.fs = fileSystem.fsInstance; + } + + async init(dest: string) { + await git.init({ fs: this.fs, dir: dest, defaultBranch: 'main' }); + } + + async isInitialized(dest: string) { + // It only checks if .git/HEAD file exists. It doesn't check if the repo has any commits + try { + await this.fs.readFile(`${dest}/.git/HEAD`, 'utf8'); + return true; + } catch (error) { + return false; + } + } + + async addFiles(files: { path: string }[], dest: string) { + for (const file of files) { + // TODO: check if git.writeBlob is necessary + await git.add({ + fs: this.fs, + dir: dest, + filepath: file.path, + }); + } + } + + async unstageFile(files: { path: string }[], dest: string) { + for (const file of files) { + await git.remove({ + fs: this.fs, + dir: dest, + filepath: file.path, + }); + } + // console.log( + // 'unstaged', + // await git.status({ fs: this.fs, dir: dest, filepath: file }), + // ); + } + + async commit( + message: string, + dest: string, + author: { name: string; email: string }, + ) { + const sha = await git.commit({ + fs: this.fs, + dir: dest, + message, + author, + }); + console.log('commit sha', sha); + } + + async log(dest: string) { + const commits = await git.log({ fs: this.fs, dir: dest }); + console.log('commits', commits); + } + + async status(dest: string, filepath: string) { + const status = await git.status({ fs: this.fs, dir: dest, filepath }); + console.log('status', status); + } + + async addRemote(repo: string, dest: string) { + await git.addRemote({ + fs: this.fs, + dir: dest, + remote: 'origin', + url: repo, + }); + } + + async getFileCollection( + dest: string, + ): Promise> { + try { + const statusMatrix = await git.statusMatrix({ fs: this.fs, dir: dest }); + type Status = 0 | 1 | 2; + + // Map over the status matrix to get the status for each file + const filesWithStatus = statusMatrix.map( + ([filePath, workDirStatus, stageStatus, headStatus]) => { + let status = ''; + let isStaged = false; + + // TypeScript expects workDirStatus and others to be '0 | 1' in some cases, so we assert it as 'Status' (0 | 1 | 2) + const workDir = workDirStatus as Status; + const stage = stageStatus as Status; + const head = headStatus as Status; + + // Determine status based on workDir, stage, and head + if (workDir === 2 && stage === 0 && head === 0) { + status = 'U'; // Untracked file + } else if (workDir === 2 && stage === 0 && head === 1) { + status = 'M'; // Modified but not staged + } else if (workDir === 2 && stage === 2 && head === 1) { + status = 'M'; // Modified and staged + // isStaged = true; + } else if (workDir === 0 && stage === 2 && head === 0) { + status = 'A'; // Added (new file staged but not yet committed) + // isStaged = true; + } else if (workDir === 0 && stage === 0 && head === 1) { + status = 'D'; // Deleted (file deleted but not staged) + } else if (workDir === 0 && stage === 2 && head === 1) { + status = 'D'; // Deleted and staged + // isStaged = true; + } + if (head === 2) { + isStaged = true; + } + + return { path: filePath, status, staged: isStaged }; + }, + ); + + return filesWithStatus; + } catch (error) { + console.error('Error getting files to commit:', error); + throw error; + } + } + + // async clone(repo, dest) { + // } + + // async pull(repo, dest) { + // } +} + +export default GitManager; diff --git a/src/workers/git.ts b/src/workers/git.ts new file mode 100644 index 0000000..e4f2372 --- /dev/null +++ b/src/workers/git.ts @@ -0,0 +1,88 @@ +import GitManager from '@/lib/git'; + +self.onmessage = async (e) => { + const gitManager = new GitManager(); + + const { projectPath } = e.data.payload.data; + switch (e.data.type) { + case 'init': { + try { + console.log('Initializing repo projectPath', projectPath); + await gitManager.init(projectPath); + // as we cannot use EventEmitter here, we will post message to main thread + self.postMessage({ type: 'GIT_INITIALIZED', projectPath }); + } catch (error) { + console.log('error', error); + } + break; + } + + case 'getFilesToCommit': { + try { + const files = await gitManager.getFileCollection(projectPath); + // Post the list of files back to the main thread + postMessage({ type: 'FILES_TO_COMMIT', payload: files }); + } catch (error) { + postMessage({ + type: 'ERROR', + message: 'Error getting files to commit', + error, + }); + } + break; + } + + case 'addFiles': { + const { files } = e.data.payload.data; + try { + await gitManager.addFiles(files, projectPath); + postMessage({ type: 'FILES_ADDED' }); + } catch (error) { + postMessage({ + type: 'ERROR', + message: 'Error adding files', + error, + }); + } + break; + } + + case 'unstageFile': { + const { files } = e.data.payload.data; + try { + await gitManager.unstageFile(files, projectPath); + postMessage({ type: 'FILE_UNSTAGED' }); + } catch (error) { + postMessage({ + type: 'ERROR', + message: 'Error unstaging file', + error, + }); + } + break; + } + + case 'commit': { + const { message, author } = e.data.payload.data; + try { + await gitManager.commit(message, projectPath, author); + postMessage({ type: 'COMMIT_COMPLETE' }); + } catch (error) { + postMessage({ + type: 'ERROR', + message: 'Error committing changes', + error, + }); + } + break; + } + + default: + // Handle any cases that are not explicitly mentioned + console.error('Unhandled message type:', e.data.type); + } +}; + +self.onerror = (e) => { + console.error('Worker error:', e); +};