├── public ├── CNAME ├── favicon.ico ├── robots.txt ├── icon-512x512.png └── index.html ├── .env.production ├── .prettierignore ├── repo-img ├── sample-std.png ├── component-structure.png ├── sample-branch-selector.png ├── sample-commit-selector.png └── sample-raw-object-data.png ├── src ├── components │ ├── ObjectArea │ │ ├── ObjectArea.css │ │ └── ObjectArea.jsx │ ├── GraphArea │ │ ├── GraphArea.css │ │ └── GraphArea.jsx │ ├── Loader │ │ ├── loader.svg │ │ ├── Loader.jsx │ │ └── loader.css │ ├── BranchSelector │ │ ├── BranchSelector.css │ │ └── BranchSelector.jsx │ ├── BackToTop │ │ ├── BackToTop.css │ │ ├── up-arrow.svg │ │ └── BackToTop.jsx │ ├── ErrorMsg │ │ ├── ErrorMsg.css │ │ └── ErrorMsg.jsx │ ├── CommitSelector │ │ ├── cross.svg │ │ ├── CommitSelector.css │ │ └── CommitSelector.jsx │ ├── RawDataDisplay │ │ ├── cross.svg │ │ ├── RawDataDisplay.jsx │ │ └── RawDataDisplay.css │ ├── IntroMsg │ │ ├── IntroMsg.css │ │ └── IntroMsg.jsx │ ├── GitObject │ │ ├── GitObject.css │ │ └── GitObject.jsx │ └── App │ │ ├── App.css │ │ └── App.jsx ├── util │ ├── generateId.js │ ├── generateConnections.js │ ├── generateBranchInfo.js │ ├── commonFns.js │ ├── objectRawData.js │ ├── formatObjects.js │ ├── coloring.js │ └── generateObjects.js ├── index.js └── index.css ├── .gitignore ├── .prettierrc ├── patches └── react-scripts+5.0.0.patch ├── .gitpod.yml ├── package.json ├── LICENSE ├── .github └── workflows │ └── build-web-app.yaml ├── README.md └── CONTRIBUTING.md /public/CNAME: -------------------------------------------------------------------------------- 1 | git-graph.harshkapadia.me 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | GENERATE_SOURCEMAP=false 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarshKapadia2/git-graph/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarshKapadia2/git-graph/HEAD/public/icon-512x512.png -------------------------------------------------------------------------------- /repo-img/sample-std.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarshKapadia2/git-graph/HEAD/repo-img/sample-std.png -------------------------------------------------------------------------------- /src/components/ObjectArea/ObjectArea.css: -------------------------------------------------------------------------------- 1 | .object-area { 2 | display: flex; 3 | flex-direction: column; 4 | } 5 | -------------------------------------------------------------------------------- /repo-img/component-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarshKapadia2/git-graph/HEAD/repo-img/component-structure.png -------------------------------------------------------------------------------- /repo-img/sample-branch-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarshKapadia2/git-graph/HEAD/repo-img/sample-branch-selector.png -------------------------------------------------------------------------------- /repo-img/sample-commit-selector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarshKapadia2/git-graph/HEAD/repo-img/sample-commit-selector.png -------------------------------------------------------------------------------- /repo-img/sample-raw-object-data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HarshKapadia2/git-graph/HEAD/repo-img/sample-raw-object-data.png -------------------------------------------------------------------------------- /src/components/GraphArea/GraphArea.css: -------------------------------------------------------------------------------- 1 | #graph-area { 2 | display: flex; 3 | justify-content: center; 4 | width: 90vw; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Loader/loader.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/util/generateId.js: -------------------------------------------------------------------------------- 1 | function getId(hash = "", name = "") { 2 | let id = hash + "-"; 3 | 4 | name = name.split("."); 5 | 6 | for (let i = 0; i < name.length; i++) id += name[i]; 7 | 8 | return id; 9 | } 10 | 11 | export default getId; 12 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./components/App/App"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.querySelector("body") 11 | ); 12 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.jsx: -------------------------------------------------------------------------------- 1 | import "./loader.css"; 2 | import loader from "./loader.svg"; 3 | 4 | const Loader = () => { 5 | return ( 6 |
7 | Loading... 8 |
Loading...
9 |
10 | ); 11 | }; 12 | 13 | export default Loader; 14 | -------------------------------------------------------------------------------- /src/components/BranchSelector/BranchSelector.css: -------------------------------------------------------------------------------- 1 | select { 2 | padding: 0.5em 1em; 3 | margin: 0.5em; 4 | border-radius: 5px; 5 | background-color: #242424; 6 | border: none; 7 | color: #e0e0e0; 8 | cursor: pointer !important; 9 | text-align: center; 10 | } 11 | 12 | select:disabled { 13 | cursor: auto !important; 14 | } 15 | -------------------------------------------------------------------------------- /src/components/BackToTop/BackToTop.css: -------------------------------------------------------------------------------- 1 | .back-to-top-btn { 2 | display: grid; 3 | place-content: center; 4 | padding: 1em; 5 | position: fixed; 6 | bottom: 2.5vh; 7 | right: 2vw; 8 | border-radius: 50%; 9 | z-index: 1; 10 | } 11 | 12 | .back-to-top-btn img { 13 | width: 1em; 14 | height: 1em; 15 | } 16 | 17 | .hidden { 18 | display: none; 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Loader/loader.css: -------------------------------------------------------------------------------- 1 | #loader-wrapper { 2 | margin-top: 10em; 3 | } 4 | 5 | #loader { 6 | animation: spin infinite linear 1.5s; 7 | margin-bottom: 1em; 8 | width: 100%; 9 | height: 100%; 10 | } 11 | 12 | @keyframes spin { 13 | 0% { 14 | transform: rotate(0deg); 15 | } 16 | 17 | 100% { 18 | transform: rotate(360deg); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ErrorMsg/ErrorMsg.css: -------------------------------------------------------------------------------- 1 | .error-msg { 2 | padding: 1em 2em 2em 2em; 3 | margin: 5em 1em; 4 | display: grid; 5 | place-content: center; 6 | text-align: center; 7 | border: 1px solid #242424; 8 | border-radius: 5px; 9 | } 10 | 11 | .error-msg h2 { 12 | color: red; 13 | margin-bottom: 0.5em; 14 | } 15 | 16 | .error-msg p { 17 | margin-bottom: 1em; 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/components/BackToTop/up-arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/CommitSelector/cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/RawDataDisplay/cross.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/IntroMsg/IntroMsg.css: -------------------------------------------------------------------------------- 1 | .intro-msg { 2 | margin: 5em 1em 2em 1em; 3 | } 4 | 5 | .intro-msg p { 6 | margin-bottom: 1em; 7 | } 8 | 9 | .intro-msg li { 10 | margin-bottom: 1em; 11 | list-style: inside; 12 | padding-left: 0.5em; 13 | } 14 | 15 | .intro-msg h2 { 16 | margin: 1.5em 0 1em 0; 17 | } 18 | 19 | .intro-msg ul ul { 20 | margin-left: 3em; 21 | margin-top: 1em; 22 | } 23 | 24 | .intro-msg ul ul li { 25 | list-style: circle; 26 | } 27 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | } 6 | 7 | /* ======================================================== SCROLLBAR ================================================== */ 8 | 9 | body::-webkit-scrollbar { 10 | width: 0.7em; 11 | } 12 | 13 | body::-webkit-scrollbar-track { 14 | background-color: #212121; 15 | } 16 | 17 | body::-webkit-scrollbar-thumb { 18 | background-color: #616161; 19 | border-radius: 2px; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/BackToTop/BackToTop.jsx: -------------------------------------------------------------------------------- 1 | import "./BackToTop.css"; 2 | import upArrow from "./up-arrow.svg"; 3 | 4 | const BackToTop = ({ backToTopBtn, scrollToTopTriggerDiv }) => { 5 | const scrollToTop = () => { 6 | scrollToTopTriggerDiv.current.scrollIntoView(true); 7 | }; 8 | 9 | return ( 10 | 17 | ); 18 | }; 19 | 20 | export default BackToTop; 21 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": true, 4 | "cursorOffset": -1, 5 | "embeddedLanguageFormatting": "auto", 6 | "endOfLine": "auto", 7 | "filepath": "", 8 | "htmlWhitespaceSensitivity": "css", 9 | "insertPragma": false, 10 | "jsxBracketSameLine": false, 11 | "jsxSingleQuote": false, 12 | "overrides": [], 13 | "plugins": [], 14 | "pluginSearchDirs": [], 15 | "printWidth": 80, 16 | "proseWrap": "preserve", 17 | "quoteProps": "preserve", 18 | "rangeEnd": -1, 19 | "rangeStart": 0, 20 | "requirePragma": false, 21 | "semi": true, 22 | "singleQuote": false, 23 | "tabWidth": 4, 24 | "trailingComma": "none", 25 | "useTabs": true, 26 | "vueIndentScriptAndStyle": false 27 | } 28 | -------------------------------------------------------------------------------- /patches/react-scripts+5.0.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/react-scripts/config/webpack.config.js b/node_modules/react-scripts/config/webpack.config.js 2 | index 2b1b3bb..1ec4f2c 100644 3 | --- a/node_modules/react-scripts/config/webpack.config.js 4 | +++ b/node_modules/react-scripts/config/webpack.config.js 5 | @@ -343,6 +343,13 @@ module.exports = function (webpackEnv) { 6 | babelRuntimeRegenerator, 7 | ]), 8 | ], 9 | + fallback: { 10 | + assert: require.resolve('assert'), 11 | + buffer: require.resolve('buffer'), 12 | + stream: require.resolve('stream-browserify'), 13 | + util: require.resolve('util'), 14 | + zlib: require.resolve('browserify-zlib'), 15 | + }, 16 | }, 17 | module: { 18 | strictExportPresence: true, 19 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | tasks: 2 | - name: Setup 3 | 4 | init: | 5 | # Install npm dependencies 6 | # npm installs dependencies to the working folder, which are persistent across restarts 7 | npm install 8 | 9 | command: | 10 | # Start the development server 11 | npm run start 12 | 13 | vscode: 14 | extensions: 15 | - dsznajder.es7-react-js-snippets 16 | - esbenp.prettier-vscode 17 | - dbaeumer.vscode-eslint 18 | - ritwickdey.liveserver 19 | - mikestead.dotenv 20 | - redhat.vscode-yaml 21 | - streetsidesoftware.code-spell-checker 22 | - vscode-icons-team.vscode-icons 23 | 24 | ports: 25 | - port: 3000 26 | onOpen: open-browser 27 | visibility: public 28 | - port: 5000 29 | onOpen: open-browser 30 | visibility: public 31 | -------------------------------------------------------------------------------- /src/components/BranchSelector/BranchSelector.jsx: -------------------------------------------------------------------------------- 1 | import "./BranchSelector.css"; 2 | 3 | const BranchSelector = ({ branchInfo, handleBranchChange, isDisabled }) => { 4 | return ( 5 | 25 | ); 26 | }; 27 | 28 | export default BranchSelector; 29 | -------------------------------------------------------------------------------- /src/components/ObjectArea/ObjectArea.jsx: -------------------------------------------------------------------------------- 1 | import getId from "../../util/generateId"; 2 | import GitObject from "../GitObject/GitObject"; 3 | import "./ObjectArea.css"; 4 | 5 | const ObjectArea = ({ objectType, objects, sendRawObjDetails }) => { 6 | return ( 7 |
8 | {objectType === "commit" && ( 9 | 10 | )} 11 | 12 | {objects !== [] && 13 | objects.map((obj, index) => ( 14 | 28 | ))} 29 |
30 | ); 31 | }; 32 | 33 | export default ObjectArea; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-graph", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "assert": "^2.0.0", 7 | "browser-fs-access": "^0.24.0", 8 | "browserify-zlib": "^0.2.0", 9 | "buffer": "^6.0.3", 10 | "patch-package": "^6.4.7", 11 | "react": "^17.0.2", 12 | "react-dom": "^17.0.2", 13 | "react-scripts": "5.0.0", 14 | "react-xarrows": "^2.0.2", 15 | "stream-browserify": "^3.0.0", 16 | "util": "^0.12.4" 17 | }, 18 | "scripts": { 19 | "start": "react-scripts start", 20 | "build": "react-scripts build", 21 | "test": "react-scripts test", 22 | "eject": "react-scripts eject", 23 | "postinstall": "patch-package" 24 | }, 25 | "eslintConfig": { 26 | "extends": [ 27 | "react-app", 28 | "react-app/jest" 29 | ] 30 | }, 31 | "browserslist": { 32 | "production": [ 33 | ">0.2%", 34 | "not dead", 35 | "not op_mini all" 36 | ], 37 | "development": [ 38 | "last 1 chrome version", 39 | "last 1 firefox version", 40 | "last 1 safari version" 41 | ] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Harsh Kapadia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/GitObject/GitObject.css: -------------------------------------------------------------------------------- 1 | .git-object { 2 | background-color: #242424; 3 | padding: 0.5em; 4 | border-radius: 5px; 5 | text-align: center; 6 | margin: 2em 0; 7 | font-size: 18px; 8 | border: 1px solid black; 9 | position: relative; 10 | } 11 | 12 | .commit-object { 13 | background-color: #ff8a80; 14 | color: black; 15 | margin-left: 0.5em; 16 | } 17 | 18 | .tree-object { 19 | margin-right: 30vw; 20 | margin-left: 15vw; 21 | background-color: #82b1ff; 22 | color: black; 23 | } 24 | 25 | .blob-object { 26 | background-color: #ea80fc; 27 | color: black; 28 | margin-right: 0.5em; 29 | } 30 | 31 | .HEAD-object { 32 | margin-left: 0.5em; 33 | } 34 | 35 | .HEAD-object, 36 | .git-object-no-color, 37 | .git-object-no-color .branch-head { 38 | background-color: #242424; 39 | color: #e0e0e0; 40 | } 41 | 42 | .raw-btn { 43 | position: absolute; 44 | right: 0; 45 | top: 0; 46 | padding: 0.125em 0.25em; 47 | display: none; 48 | } 49 | 50 | .git-object:hover > .raw-btn { 51 | display: block; 52 | } 53 | 54 | .branch-head { 55 | position: absolute; 56 | left: -15px; 57 | top: -20px; 58 | background-color: yellow; 59 | padding: 0.125em 0.25em; 60 | width: max-content; 61 | border-radius: 5px; 62 | } 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Git Graph 9 | 10 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 29 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/components/GitObject/GitObject.jsx: -------------------------------------------------------------------------------- 1 | import "./GitObject.css"; 2 | 3 | const GitObject = ({ 4 | objectType, 5 | objId, 6 | hash, 7 | name, 8 | isToBeColored, 9 | branchHead, 10 | sendRawObjDetails 11 | }) => { 12 | const objectClass = isToBeColored 13 | ? "git-object " + objectType + "-object" 14 | : "git-object " + objectType + "-object git-object-no-color"; 15 | 16 | const sendObjDetails = () => { 17 | let hash; 18 | 19 | if (objectType === "blob") hash = objId.split("-")[0]; 20 | else hash = objId; 21 | 22 | sendRawObjDetails({ objHash: hash, objName: name }); 23 | }; 24 | 25 | return ( 26 |
27 | {objectType === "commit" && branchHead !== "" && ( 28 |
29 | {branchHead.length <= 40 30 | ? branchHead 31 | : branchHead.slice(0, 40) + "..."} 32 |
33 | )} 34 | 35 | {objectType} 36 | {hash && " " + hash.slice(0, 7)} 37 | {name && ( 38 | <> 39 |
40 | {name.length <= 40 ? name : name.slice(0, 40) + "..."} 41 | 42 | )} 43 | 44 | {objectType !== "HEAD" && ( 45 | 48 | )} 49 |
50 | ); 51 | }; 52 | 53 | export default GitObject; 54 | -------------------------------------------------------------------------------- /src/components/RawDataDisplay/RawDataDisplay.jsx: -------------------------------------------------------------------------------- 1 | import "./RawDataDisplay.css"; 2 | import cross from "./cross.svg"; 3 | 4 | const RawDataDisplay = ({ objDetails, rawData, dismissRawDataDisplay }) => { 5 | return ( 6 |
7 | 10 | 11 |

Raw Object Data

12 | 13 |
14 |
15 | Object Name:{" "} 16 | {objDetails.objName !== "" ? objDetails.objName : " —"} 17 |
18 |
19 | Object Hash:{" "} 20 | {objDetails.objHash} 21 |
22 |
23 | Object Type:{" "} 24 | {rawData.objType} 25 |
26 |
27 | Object Length:{" "} 28 | {rawData.objLength} 29 |
30 |
31 |
Object Content
32 |
event.stopPropagation()} 35 | > 36 | {rawData.objContent} 37 |
38 |
39 |
40 |
41 | ); 42 | }; 43 | 44 | export default RawDataDisplay; 45 | -------------------------------------------------------------------------------- /src/components/ErrorMsg/ErrorMsg.jsx: -------------------------------------------------------------------------------- 1 | import "./ErrorMsg.css"; 2 | 3 | const ErrorMsg = ({ errorType }) => { 4 | let errorMsg = ""; 5 | 6 | if (errorType === "packed-repo") 7 | errorMsg = ( 8 |
9 |

10 | Looks like the repository is{" "} 11 | 16 | packed 17 | 18 | . 19 |

20 |

21 | Please{" "} 22 | 27 | unpack ALL the Packfiles 28 | {" "} 29 | and then re-select the .git directory. 30 |

31 |
32 | ); 33 | else if (errorType === "no-branches") 34 | errorMsg = ( 35 |
36 |

No branches were found.

37 |

38 | Please upload the correct directory (a .git{" "} 39 | directory) or make commits and then re-select the{" "} 40 | .git directory. 41 |

42 |

43 | If the .git directory is not visible in the 44 | directory picker, please enable hidden file viewing on the 45 | local machine. 46 |

47 |
48 | ); 49 | 50 | return ( 51 |
52 |

Oh no!

53 | 54 | {errorMsg} 55 |
56 | ); 57 | }; 58 | 59 | export default ErrorMsg; 60 | -------------------------------------------------------------------------------- /src/components/App/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: black; 3 | color: #e0e0e0; 4 | font-family: sans-serif; 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | } 9 | 10 | html { 11 | scroll-padding: 12vh 0 0 0; 12 | } 13 | 14 | header { 15 | height: 13vh; 16 | width: 100%; 17 | display: grid; 18 | place-content: center; 19 | position: sticky; 20 | top: 0; 21 | left: 0; 22 | z-index: 2; 23 | background-color: #000000aa; 24 | backdrop-filter: blur(4px); 25 | -webkit-backdrop-filter: blur(4px); 26 | text-align: center; 27 | transition: 0.3s; 28 | } 29 | 30 | h1 { 31 | padding: 0.25em 0.5em; 32 | } 33 | 34 | .header-border-bottom { 35 | border-bottom: 1px solid #242424; 36 | } 37 | 38 | .header-dismiss { 39 | transform: translateY(-13vh); 40 | } 41 | 42 | main { 43 | min-height: 77vh; 44 | display: flex; 45 | flex-direction: column; 46 | align-items: center; 47 | } 48 | 49 | h1 { 50 | margin: 0 0.5em; 51 | } 52 | 53 | button { 54 | padding: 0.5em 1em; 55 | margin: 0.5em; 56 | border-radius: 5px; 57 | background-color: #242424; 58 | border: none; 59 | color: #e0e0e0; 60 | cursor: pointer !important; 61 | } 62 | 63 | button:disabled { 64 | cursor: auto !important; 65 | opacity: 0.7; 66 | } 67 | 68 | footer { 69 | height: 10vh; 70 | text-align: center; 71 | display: flex; 72 | justify-content: center; 73 | align-items: center; 74 | padding: 1em 0; 75 | margin: 0 0.5em; 76 | flex-wrap: wrap; 77 | } 78 | 79 | a, 80 | a:hover { 81 | color: #e0e0e0; 82 | text-decoration-thickness: 1px; 83 | text-underline-offset: 4px; 84 | } 85 | -------------------------------------------------------------------------------- /src/util/generateConnections.js: -------------------------------------------------------------------------------- 1 | import getId from "./generateId"; 2 | 3 | let CONNECTIONS = []; 4 | let OBJECT_DATA = {}; 5 | 6 | function getConnections(objectData = {}) { 7 | OBJECT_DATA = objectData; 8 | const gitObjects = OBJECT_DATA.objects; 9 | 10 | for (let objKey in gitObjects) { 11 | CONNECTIONS.push({ 12 | start: gitObjects[objKey].commit, 13 | end: gitObjects[objKey].parentCommit, 14 | color: true 15 | }); 16 | 17 | CONNECTIONS.push({ 18 | start: gitObjects[objKey].commit, 19 | end: gitObjects[objKey].tree, 20 | color: true 21 | }); 22 | 23 | const gitBlobs = gitObjects[objKey].blobs; 24 | for (let blobKey in gitBlobs) { 25 | if (gitBlobs[blobKey].type === "tree") { 26 | CONNECTIONS.push({ 27 | start: gitObjects[objKey].tree, 28 | end: gitBlobs[blobKey].hash, 29 | color: true 30 | }); 31 | addRecursiveTrees(gitBlobs[blobKey].hash); 32 | } else { 33 | CONNECTIONS.push({ 34 | start: gitObjects[objKey].tree, 35 | end: getId(gitBlobs[blobKey].hash, gitBlobs[blobKey].name), 36 | color: true 37 | }); 38 | } 39 | } 40 | } 41 | 42 | const objectConnections = CONNECTIONS; 43 | 44 | CONNECTIONS = []; 45 | OBJECT_DATA = {}; 46 | 47 | return objectConnections; 48 | } 49 | 50 | function addRecursiveTrees(treeHash = "") { 51 | const obj = OBJECT_DATA.recursiveTrees[treeHash]; 52 | 53 | for (let objKey in obj) { 54 | if (obj[objKey].type === "tree") { 55 | CONNECTIONS.push({ 56 | start: treeHash, 57 | end: obj[objKey].hash, 58 | color: true 59 | }); 60 | addRecursiveTrees(obj[objKey].hash); 61 | } else 62 | CONNECTIONS.push({ 63 | start: treeHash, 64 | end: getId(obj[objKey].hash, obj[objKey].name), 65 | color: true 66 | }); 67 | } 68 | } 69 | 70 | export default getConnections; 71 | -------------------------------------------------------------------------------- /src/components/RawDataDisplay/RawDataDisplay.css: -------------------------------------------------------------------------------- 1 | #raw-data-display { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: flex-start; 5 | width: 50vw; 6 | min-height: 100vh; 7 | background-color: #000000aa; 8 | backdrop-filter: blur(4px); 9 | -webkit-backdrop-filter: blur(4px); 10 | border-left: 1px solid #242424; 11 | position: fixed; 12 | top: 0; 13 | right: 0; 14 | z-index: 2; 15 | padding: 1em 2em; 16 | } 17 | 18 | #raw-data-display h2 { 19 | margin: 0.75em 0.5em 0.55em 0; 20 | text-align: center; 21 | } 22 | 23 | #raw-data { 24 | width: 100%; 25 | margin-top: 1em; 26 | } 27 | 28 | .raw-data-field { 29 | margin-bottom: 1em; 30 | white-space: pre-wrap; 31 | backdrop-filter: blur(10px); 32 | -webkit-backdrop-filter: blur(10px); 33 | border: 1px solid #242424; 34 | border-radius: 5px; 35 | overflow: auto; 36 | } 37 | 38 | .raw-data-field:not(:last-child) { 39 | padding: 0.75em 1em; 40 | } 41 | 42 | .raw-data-field:last-child { 43 | padding: 0.75em; 44 | } 45 | 46 | #raw-data-content-field-name { 47 | position: sticky; 48 | padding: 0 0.5em 0.5em 0.5em; 49 | margin-bottom: 0.5em; 50 | border-bottom: 1px solid #242424; 51 | } 52 | 53 | #raw-data-file-content { 54 | max-height: 43vh; 55 | overflow: auto; 56 | padding: 0 0.5em; 57 | } 58 | 59 | /* ======================================================== SCROLLBAR ================================================== */ 60 | 61 | .raw-data-field::-webkit-scrollbar, 62 | #raw-data-file-content::-webkit-scrollbar { 63 | width: 0.7em; 64 | } 65 | 66 | .raw-data-field::-webkit-scrollbar-track, 67 | #raw-data-file-content::-webkit-scrollbar-track { 68 | background-color: #212121; 69 | } 70 | 71 | .raw-data-field::-webkit-scrollbar-thumb, 72 | #raw-data-file-content::-webkit-scrollbar-thumb { 73 | background-color: #616161; 74 | border-radius: 2px; 75 | } 76 | -------------------------------------------------------------------------------- /src/util/generateBranchInfo.js: -------------------------------------------------------------------------------- 1 | import { getHeadCommit, readFile } from "./commonFns"; 2 | 3 | async function getAllBranches(fileObjects = []) { 4 | let allBranches = []; 5 | 6 | // Get branch names and their hashes from the `.git/refs/heads` directory 7 | for (let i = 0; i < fileObjects.length; i++) { 8 | if (fileObjects[i].directoryHandle.name === "heads") { 9 | let branchName = fileObjects[i].handle.name; 10 | 11 | let branchHeadTemp = await readFile( 12 | fileObjects, 13 | `.git/refs/heads/${branchName}`, 14 | "binary" 15 | ); 16 | let branchHeadHash = branchHeadTemp.slice(0, -1); 17 | 18 | allBranches.push({ branchName, branchHeadHash }); 19 | } 20 | } 21 | 22 | // Get branch names from packed refs if the `packed-refs` file exists 23 | let packedRefs = await readFile(fileObjects, ".git/packed-refs", "binary"); 24 | if (packedRefs !== undefined) { 25 | packedRefs = packedRefs.split("\n"); 26 | 27 | for (let i = 1; i < packedRefs.length; i++) { 28 | const refEntry = packedRefs[i].split(" "); 29 | 30 | if (refEntry[1]?.indexOf("refs/heads/") >= 0) { 31 | const index = refEntry[1].lastIndexOf("/"); 32 | const branchName = refEntry[1].slice(index + 1); 33 | const branchHeadHash = refEntry[0]; 34 | 35 | if ( 36 | !allBranches.some( 37 | (branchInArr) => branchInArr.branchName === branchName 38 | ) 39 | ) 40 | allBranches.push({ branchName, branchHeadHash }); 41 | } 42 | } 43 | } 44 | 45 | if (allBranches.length === 0) throw new Error("Branches not found."); 46 | 47 | // Get current branch name and hash 48 | let currentBranchRef = await readFile(fileObjects, ".git/HEAD", "binary"); 49 | let currentBranchName = currentBranchRef.slice(16, -1); 50 | let currentBranchHeadHash = await getHeadCommit( 51 | fileObjects, 52 | currentBranchName 53 | ); 54 | 55 | return { 56 | currentBranch: { 57 | name: currentBranchName, 58 | headHash: currentBranchHeadHash 59 | }, 60 | allBranches 61 | }; 62 | } 63 | 64 | export default getAllBranches; 65 | -------------------------------------------------------------------------------- /src/components/IntroMsg/IntroMsg.jsx: -------------------------------------------------------------------------------- 1 | import "./IntroMsg.css"; 2 | 3 | const IntroMsg = () => { 4 | return ( 5 |
6 |

7 | A visualizer for the Directed Acyclic Graph that Git creates to 8 | connect{" "} 9 | 14 | Commit, Tree and Blob objects 15 | {" "} 16 | internally. 17 |

18 |

19 | 24 | Learn the internals of Git. 25 | 26 |

27 | 28 |

Usage Instructions

29 | 52 | 53 |

Note

54 | 76 |
77 | ); 78 | }; 79 | 80 | export default IntroMsg; 81 | -------------------------------------------------------------------------------- /src/components/GraphArea/GraphArea.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import Xarrow from "react-xarrows"; 3 | import ObjectArea from "../ObjectArea/ObjectArea"; 4 | import "./GraphArea.css"; 5 | 6 | const GraphArea = ({ objectData, sendRawObjDetails }) => { 7 | const [gitObjectData, setGitObjectData] = useState(objectData); 8 | 9 | useEffect(() => { 10 | setGitObjectData(objectData); 11 | }, [objectData]); 12 | 13 | const randomColor = () => { 14 | const colors = [ 15 | "yellow", 16 | "red", 17 | "lawngreen", 18 | "aqua", 19 | "white", 20 | "violet", 21 | "coral" 22 | ]; 23 | const index = Math.floor(Math.random() * colors.length); 24 | return colors[index]; 25 | }; 26 | 27 | return ( 28 |
29 | 39 | 49 | 59 | 60 | {gitObjectData.objectConnections !== undefined && 61 | gitObjectData.objectConnections.map((connection, index) => { 62 | if (connection.end !== "") 63 | return ( 64 | 75 | ); 76 | else return ""; 77 | })} 78 |
79 | ); 80 | }; 81 | 82 | export const MemoizedGraphArea = React.memo(GraphArea); 83 | -------------------------------------------------------------------------------- /.github/workflows/build-web-app.yaml: -------------------------------------------------------------------------------- 1 | name: Build and deploy web app 2 | on: 3 | push: 4 | branches: 5 | - main 6 | paths: 7 | - ".github/workflows/**" 8 | - "patches/**" 9 | - "public/**" 10 | - "src/**" 11 | - ".env.production" 12 | - "package-lock.json" 13 | - "package.json" 14 | - "yarn.lock" 15 | pull_request: 16 | branches: 17 | - main 18 | paths: 19 | - ".github/workflows/**" 20 | - "patches/**" 21 | - "public/**" 22 | - "src/**" 23 | - ".env.production" 24 | - "package-lock.json" 25 | - "package.json" 26 | - "yarn.lock" 27 | workflow_dispatch: # Allows manual execution of workflow 28 | 29 | jobs: 30 | build: 31 | if: startsWith(github.ref, 'refs/pull/') 32 | runs-on: ubuntu-latest 33 | container: node:17 34 | steps: 35 | - name: Checkout code 36 | uses: actions/checkout@v3 37 | 38 | - name: Build web app 39 | env: 40 | CI: false # To prevent taking warnings as errors 41 | run: | 42 | npm install 43 | npm run build 44 | 45 | build-and-deploy: 46 | if: startsWith(github.ref, 'refs/heads/') 47 | runs-on: ubuntu-latest 48 | container: node:17 49 | steps: 50 | - name: Checkout code 51 | uses: actions/checkout@v3 52 | 53 | - name: Build web app 54 | env: 55 | CI: false # To prevent taking warnings as errors 56 | run: | 57 | npm install 58 | npm run build 59 | 60 | - name: Install package for deploy Action 61 | run: | 62 | apt-get update 63 | apt-get install rsync -y 64 | 65 | - name: Deploy to GitHub Pages 66 | uses: JamesIves/github-pages-deploy-action@v4 67 | with: 68 | branch: gh-pages 69 | folder: build 70 | clean: true 71 | -------------------------------------------------------------------------------- /src/util/commonFns.js: -------------------------------------------------------------------------------- 1 | import { inflateSync } from "browserify-zlib"; 2 | import { Buffer } from "buffer"; 3 | 4 | async function getObjectType(files = [], hash = "") { 5 | if (hash === "") 6 | throw new Error("Hash parameter for getObjectType() missing."); 7 | 8 | const filePath = ".git/objects/" + hash.slice(0, 2) + "/" + hash.slice(2); 9 | const fileContent = await getDecompressedFileBuffer(files, filePath); 10 | 11 | const objectType = fileContent.toString("utf-8").split(" ")[0]; 12 | return objectType; 13 | } 14 | 15 | async function getHeadCommit(files = [], branchName) { 16 | let head = await readFile(files, `.git/refs/heads/${branchName}`, "binary"); 17 | 18 | if (head === undefined) 19 | head = await getHeadCommitFromPackedRefs(files, branchName); 20 | else head = head.slice(0, -1); 21 | 22 | return head; 23 | } 24 | 25 | async function getHeadCommitFromPackedRefs(files = [], branchName) { 26 | let packedRefs = await readFile(files, ".git/packed-refs", "binary"); 27 | packedRefs = packedRefs.split("\n"); 28 | 29 | for (let i = 1; i < packedRefs.length; i++) { 30 | const refEntry = packedRefs[i].split(" "); 31 | 32 | if (refEntry[1] === `refs/heads/${branchName}`) return refEntry[0]; 33 | } 34 | 35 | return ""; 36 | } 37 | 38 | async function getDecompressedFileBuffer(files = [], path = "") { 39 | let fileBuffer = await readFile(files, path, "buffer"); 40 | 41 | if (fileBuffer === undefined) return undefined; 42 | 43 | fileBuffer = Buffer.from(new Uint8Array(fileBuffer)); 44 | fileBuffer = inflateSync(fileBuffer); 45 | 46 | return fileBuffer; 47 | } 48 | 49 | async function readFile(files = [], path = "", readType = "") { 50 | return new Promise((resolve, reject) => { 51 | const readFile = new FileReader(); 52 | 53 | readFile.addEventListener("error", () => reject()); 54 | readFile.addEventListener("load", (event) => 55 | resolve(event.target.result) 56 | ); 57 | 58 | const fileArr = files.filter( 59 | (file) => file.webkitRelativePath === path 60 | ); 61 | 62 | if (fileArr[0] === undefined) resolve(undefined); 63 | 64 | if (readType === "buffer") readFile.readAsArrayBuffer(fileArr[0]); 65 | else if (readType === "binary") readFile.readAsBinaryString(fileArr[0]); 66 | }); 67 | } 68 | 69 | export { 70 | getObjectType, 71 | getHeadCommit, 72 | getHeadCommitFromPackedRefs, 73 | getDecompressedFileBuffer, 74 | readFile 75 | }; 76 | -------------------------------------------------------------------------------- /src/util/objectRawData.js: -------------------------------------------------------------------------------- 1 | import { getObjectType, getDecompressedFileBuffer } from "./commonFns"; 2 | 3 | async function getObjRawData(files = [], hash = "") { 4 | let objRawData = {}; 5 | const objType = await getObjectType(files, hash); 6 | 7 | const objFilePath = 8 | ".git/objects/" + hash.slice(0, 2) + "/" + hash.slice(2); 9 | 10 | const objFileBuffer = await getDecompressedFileBuffer(files, objFilePath); 11 | 12 | if (objType === "tree") objRawData = await getTreeRawData(files, hash); 13 | else { 14 | const rawData = objFileBuffer.toString("utf-8"); 15 | const firstNullIndex = rawData.indexOf("\0"); 16 | const [objectType, objectLength] = rawData 17 | .slice(0, firstNullIndex) 18 | .split(" "); 19 | const rawContent = rawData.slice(firstNullIndex + 1); 20 | 21 | objRawData = { 22 | objType: objectType, 23 | objLength: objectLength, 24 | objContent: rawContent 25 | }; 26 | } 27 | 28 | return objRawData; 29 | } 30 | 31 | async function getTreeRawData(files = [], treeHash = "") { 32 | if (treeHash === "" || files.length === 0) 33 | throw new Error("Parameters missing."); 34 | 35 | const treeFilePath = 36 | ".git/objects/" + treeHash.slice(0, 2) + "/" + treeHash.slice(2); 37 | 38 | const treeContent = await getDecompressedFileBuffer(files, treeFilePath); 39 | 40 | let treeRawData = {}; 41 | 42 | let index = treeContent.indexOf("\0"); 43 | let [objType, objLength] = treeContent 44 | .toString("utf-8", 0, index) 45 | .split(" "); 46 | const length = parseInt(objLength); 47 | 48 | treeRawData = { objType, objLength }; 49 | 50 | let rawContent = ""; 51 | for (let nullIndex = index; nullIndex < length; index = nullIndex) { 52 | nullIndex = treeContent.indexOf("\0", index + 1); 53 | 54 | let [fileMode, fileName] = treeContent 55 | .toString("utf-8", index, nullIndex) 56 | .split(" "); 57 | 58 | nullIndex++; 59 | 60 | const objHash = treeContent.toString( 61 | "hex", 62 | nullIndex, 63 | (nullIndex += 20) 64 | ); // '20' since the SHA1 hash is 20 bytes (40 hexadecimal characters per SHA1 hash) 65 | 66 | const tempObjType = await getObjectType(files, objHash); 67 | 68 | rawContent += `${fileMode}\t${tempObjType}\t\t${objHash}\t\t${fileName}\n`; 69 | } 70 | 71 | rawContent = rawContent.slice(1); 72 | treeRawData["objContent"] = rawContent; 73 | 74 | return treeRawData; 75 | } 76 | 77 | export default getObjRawData; 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Git Graph 2 | 3 | Visualize the Directed Acyclic Graph that Git creates to connect Commit, Tree and Blob objects internally. 4 | 5 | Hosted at [git-graph.harshkapadia.me](https://git-graph.harshkapadia.me). 6 | 7 | > NOTE: 8 | > 9 | > - [Learn about Git Objects.](https://git.harshkapadia.me/#_git_objects) 10 | > - This web app is a part of [my Git Internals talks](https://talks.harshkapadia.me/git_internals). 11 | > - Special thanks to [@KartikSoneji](https://github.com/KartikSoneji) for his help with parsing Tree files and [@tusharnankani](https://github.com/tusharnankani) for his UI/UX suggestions. 12 | > - Please report errors and bugs by [raising issues](https://github.com/HarshKapadia2/git-graph/issues). 13 | 14 | ## Usage 15 | 16 | - Select the `.git` directory of a repository for the graph to render. 17 | - If the `.git` directory is not visible in the directory picker, please enable hidden file viewing on the local machine. 18 | - Extremely huge repositories might not load due to browser memory constraints. 19 | - 'Packed repo' error: Please unpack **all** the [packfiles](https://git.harshkapadia.me/#_the_pack_directory) (`.pack` files) in the repository. ([Tutorial](https://www.youtube.com/watch?v=cauIy20JhFs)) 20 | - Using the Branch Selector, any local branch can be visualized. 21 | - 'No branch' error: No branches could be found. Either the directory uploaded is not a `.git` directory or the repository has no commits in the checked out branch. 22 | - Using the Commit Selector, one or more Commits and their corresponding Trees and Blobs can be highlighted. 23 | - Hover over objects and click on the 'Raw' button to view the raw contents of that Git Object. 24 | 25 | ## Screenshots 26 | 27 | ![](repo-img/sample-std.png) 28 | 29 | Select the branch to render 👇 30 | 31 | ![](repo-img/sample-branch-selector.png) 32 | 33 | Select commit(s) to highlight 👇 34 | 35 | ![](repo-img/sample-commit-selector.png) 36 | 37 | View the raw contents of any Git Object 👇 38 | 39 | ![](repo-img/sample-raw-object-data.png) 40 | 41 | ## Contribution 42 | 43 | Contributors are most welcome! Please go through the [`CONTRIBUTING.md` file](CONTRIBUTING.md) for local project setup instructions and the component and object structures. 44 | 45 |
46 | 47 | 48 | The Git logo by Jason Long is licensed under the Creative Commons Attribution 3.0 Unported License. 49 | 50 | -------------------------------------------------------------------------------- /src/components/CommitSelector/CommitSelector.css: -------------------------------------------------------------------------------- 1 | #commit-selector { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | justify-content: space-between; 6 | width: 20vw; 7 | min-height: 100vh; 8 | background-color: #000000aa; 9 | backdrop-filter: blur(4px); 10 | -webkit-backdrop-filter: blur(4px); 11 | border-right: 1px solid #242424; 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | z-index: 2; 16 | } 17 | 18 | #commit-selector h2 { 19 | margin: 0.5em 0.5em 1em 0.5em; 20 | text-align: center; 21 | } 22 | 23 | #commit-selector-commit-wrapper, 24 | #commit-selector-shortcut-btn-wrapper { 25 | width: 100%; 26 | display: flex; 27 | flex-direction: column; 28 | align-items: center; 29 | } 30 | 31 | #commit-selector-shortcut-btn-wrapper { 32 | margin-bottom: 1em; 33 | } 34 | 35 | #commit-selector-dismiss { 36 | width: 100%; 37 | display: flex; 38 | justify-content: flex-end; 39 | } 40 | 41 | #selector-commit-wrapper { 42 | max-height: 71vh; 43 | overflow: auto; 44 | width: 100%; 45 | display: flex; 46 | flex-direction: column; 47 | } 48 | 49 | .selector-commit { 50 | margin: 0.5em; 51 | display: flex; 52 | align-items: center; 53 | backdrop-filter: blur(10px); 54 | -webkit-backdrop-filter: blur(10px); 55 | padding: 0.25em 0.5em; 56 | border: 1px solid #242424; 57 | border-radius: 5px; 58 | cursor: pointer; 59 | position: relative; 60 | } 61 | 62 | .selector-commit:first-child { 63 | margin-top: 0; 64 | } 65 | 66 | .selector-commit:last-child { 67 | margin-bottom: 0; 68 | } 69 | 70 | .selector-commit-text-msg { 71 | color: #616161; 72 | } 73 | 74 | .selector-commit input { 75 | cursor: pointer; 76 | } 77 | 78 | .selector-commit-text { 79 | margin-left: 0.5em; 80 | overflow: auto; 81 | } 82 | 83 | .commit-selector-branch-head { 84 | position: absolute; 85 | right: 0; 86 | bottom: 0; 87 | background-color: yellow; 88 | padding: 0.125em 0.25em; 89 | margin: 0.25em; 90 | width: max-content; 91 | border-radius: 5px; 92 | color: black; 93 | } 94 | 95 | /* ======================================================== SCROLLBAR ================================================== */ 96 | 97 | .selector-commit-text::-webkit-scrollbar, 98 | #selector-commit-wrapper::-webkit-scrollbar { 99 | width: 0.7em; 100 | } 101 | 102 | .selector-commit-text::-webkit-scrollbar-track, 103 | #selector-commit-wrapper::-webkit-scrollbar-track { 104 | background-color: #212121; 105 | } 106 | 107 | .selector-commit-text::-webkit-scrollbar-thumb, 108 | #selector-commit-wrapper::-webkit-scrollbar-thumb { 109 | background-color: #616161; 110 | border-radius: 2px; 111 | } 112 | -------------------------------------------------------------------------------- /src/util/formatObjects.js: -------------------------------------------------------------------------------- 1 | let OBJECT_DATA = {}; 2 | let COMMITS = []; 3 | let TREES = []; 4 | let BLOBS = []; 5 | 6 | function formatObjects(objectData = {}) { 7 | OBJECT_DATA = objectData; 8 | const gitObjects = OBJECT_DATA.objects; 9 | 10 | for (let objKey in gitObjects) { 11 | COMMITS.push({ 12 | hash: gitObjects[objKey].commit, 13 | name: gitObjects[objKey].commitMsg, 14 | branchHead: gitObjects[objKey].branchHead, 15 | color: true 16 | }); 17 | 18 | if (!TREES.some((obj) => obj.hash === gitObjects[objKey].tree)) 19 | TREES.push({ 20 | hash: gitObjects[objKey].tree, 21 | name: "", 22 | color: true 23 | }); 24 | 25 | const gitBlobs = gitObjects[objKey].blobs; 26 | for (let blobKey in gitBlobs) { 27 | if (gitBlobs[blobKey].type === "tree") { 28 | if ( 29 | !TREES.some( 30 | (treeObject) => 31 | treeObject.hash === gitBlobs[blobKey].hash 32 | ) 33 | ) { 34 | TREES.push({ 35 | hash: gitBlobs[blobKey].hash, 36 | name: gitBlobs[blobKey].name, 37 | color: true 38 | }); 39 | addRecursiveTrees(gitBlobs[blobKey].hash); 40 | } 41 | } else { 42 | if ( 43 | !BLOBS.some( 44 | (blobObject) => 45 | blobObject.hash === gitBlobs[blobKey].hash && 46 | blobObject.name === gitBlobs[blobKey].name 47 | ) 48 | ) 49 | BLOBS.push({ 50 | hash: gitBlobs[blobKey].hash, 51 | name: gitBlobs[blobKey].name, 52 | color: true 53 | }); 54 | } 55 | } 56 | } 57 | 58 | // OBJECT_DATA.objects.forEach((gitObject) => { 59 | // COMMITS.push({ hash: gitObject.commit, name: "" }); 60 | 61 | // if (!TREES.some((obj) => obj.hash === gitObject.tree)) 62 | // TREES.push({ hash: gitObject.tree, name: "" }); 63 | 64 | // gitObject.blobs.forEach((obj) => { 65 | // if (obj.type === "tree") { 66 | // if (!TREES.some((treeObject) => treeObject.hash === obj.hash)) { 67 | // TREES.push({ hash: obj.hash, name: obj.name }); 68 | // addRecursiveTrees(obj.hash); 69 | // } 70 | // } else { 71 | // if (!BLOBS.some((blobObject) => blobObject.hash === obj.hash)) 72 | // BLOBS.push({ hash: obj.hash, name: obj.name }); 73 | // } 74 | // }); 75 | // }); 76 | 77 | const formattedObjectData = { 78 | commits: COMMITS, 79 | trees: TREES, 80 | blobs: BLOBS 81 | }; 82 | 83 | OBJECT_DATA = []; 84 | COMMITS = []; 85 | TREES = []; 86 | BLOBS = []; 87 | 88 | return formattedObjectData; 89 | } 90 | 91 | function addRecursiveTrees(treeHash = "") { 92 | const obj = OBJECT_DATA.recursiveTrees[treeHash]; 93 | 94 | for (let objKey in obj) { 95 | if (obj[objKey].type === "tree") { 96 | if ( 97 | !TREES.some( 98 | (treeObject) => treeObject.hash === obj[objKey].hash 99 | ) 100 | ) { 101 | TREES.push({ 102 | hash: obj[objKey].hash, 103 | name: obj[objKey].name, 104 | color: true 105 | }); 106 | addRecursiveTrees(obj[objKey].hash); 107 | } 108 | } else { 109 | if ( 110 | !BLOBS.some( 111 | (blobObject) => 112 | blobObject.hash === obj[objKey].hash && 113 | blobObject.name === obj[objKey].name 114 | ) 115 | ) 116 | BLOBS.push({ 117 | hash: obj[objKey].hash, 118 | name: obj[objKey].name, 119 | color: true 120 | }); 121 | } 122 | } 123 | 124 | // objArr.forEach((object) => { 125 | // if (object.type === "tree") { 126 | // if (!TREES.some((treeObject) => treeObject.hash === object.hash)) { 127 | // TREES.push({ hash: object.hash, name: object.name }); 128 | // addRecursiveTrees(object.hash); 129 | // } 130 | // } else { 131 | // if (!BLOBS.some((blobObject) => blobObject.hash === object.hash)) 132 | // BLOBS.push({ hash: object.hash, name: object.name }); 133 | // } 134 | // }); 135 | } 136 | 137 | export default formatObjects; 138 | -------------------------------------------------------------------------------- /src/components/CommitSelector/CommitSelector.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useRef } from "react"; 2 | import "./CommitSelector.css"; 3 | import cross from "./cross.svg"; 4 | 5 | const CommitSelector = ({ 6 | commits, 7 | selectorDisplayState, 8 | selectCommits, 9 | selectedCommits 10 | }) => { 11 | const [isChecked, setIsChecked] = useState( 12 | new Array(commits.length).fill(false) 13 | ); 14 | 15 | const timeoutInstance = useRef(); 16 | 17 | useEffect(() => { 18 | if (selectedCommits.length !== 0) { 19 | let updatedCheckedState = []; 20 | for (let i = 0; i < commits.length; i++) { 21 | if ( 22 | selectedCommits.some( 23 | (element) => element === commits[i].hash 24 | ) 25 | ) 26 | updatedCheckedState.push(true); 27 | else updatedCheckedState.push(false); 28 | } 29 | 30 | setIsChecked(updatedCheckedState); 31 | } 32 | }, []); 33 | 34 | const handleCheckboxChange = (index) => { 35 | let checkboxState = [...isChecked]; // Can't directly assign, as passing by reference isn't considered as a change 36 | checkboxState[index] = !checkboxState[index]; 37 | 38 | setIsChecked(checkboxState); 39 | debouncedCommitSubmission(checkboxState); 40 | }; 41 | 42 | const debouncedCommitSubmission = (checkboxState) => { 43 | clearTimeout(timeoutInstance.current); 44 | 45 | timeoutInstance.current = setTimeout( 46 | () => submitSelectedCommits(checkboxState), 47 | 800 48 | ); 49 | }; 50 | 51 | const submitSelectedCommits = (checkboxState) => { 52 | let commitArr = []; 53 | for (let i = 0; i < commits.length; i++) 54 | if (checkboxState[i] === true) commitArr.push(commits[i].hash); 55 | 56 | selectCommits(commitArr); 57 | }; 58 | 59 | const submitAllCommits = () => { 60 | const updatedCheckedState = new Array(commits.length).fill(true); 61 | let commitArr = []; 62 | 63 | for (let i = 0; i < commits.length; i++) 64 | commitArr.push(commits[i].hash); 65 | 66 | setIsChecked(updatedCheckedState); 67 | selectCommits(commitArr); 68 | }; 69 | 70 | const submitNoCommit = () => { 71 | setIsChecked(new Array(commits.length).fill(false)); 72 | selectCommits([]); 73 | }; 74 | 75 | return ( 76 |
77 |
78 |
79 | 85 |
86 | 87 |

Highlight Commits

88 | 89 |
90 | 93 | 94 | 97 |
98 | 99 |
event.stopPropagation()} 102 | > 103 | {commits.map((commit, index) => ( 104 |
handleCheckboxChange(index)} 108 | > 109 | 114 | 115 |
116 | commit {commit.hash.slice(0, 7)} 117 |
118 | 119 | {commit.name.length <= 30 120 | ? commit.name 121 | : commit.name.slice(0, 30) + "..."} 122 | 123 |
124 | 125 | {commit.branchHead !== "" && ( 126 |
127 | {commit.branchHead.length > 7 128 | ? commit.branchHead.slice(0, 7) + "..." 129 | : commit.branchHead} 130 |
131 | )} 132 |
133 | ))} 134 |
135 |
136 |
137 | ); 138 | }; 139 | 140 | export default CommitSelector; 141 | -------------------------------------------------------------------------------- /src/util/coloring.js: -------------------------------------------------------------------------------- 1 | import getId from "./generateId"; 2 | 3 | let COMMIT_OBJECTS = []; 4 | let TREE_OBJECTS = []; 5 | let BLOB_OBJECTS = []; 6 | let CONNECTIONS = []; 7 | 8 | let COLORED_TREES = []; 9 | let COLORED_BLOBS = []; 10 | 11 | function colorObjectsAndConnections(objectData, selectedCommits) { 12 | COMMIT_OBJECTS = objectData.objects.commits; 13 | TREE_OBJECTS = objectData.objects.trees; 14 | BLOB_OBJECTS = objectData.objects.blobs; 15 | CONNECTIONS = objectData.objectConnections; 16 | 17 | if (selectedCommits.length === 0) return removeAllColors(); 18 | if (selectedCommits.length === COMMIT_OBJECTS.length) return addAllColors(); 19 | 20 | removeAllColors(); 21 | 22 | for (let i = 0; i < COMMIT_OBJECTS.length; i++) { 23 | if (isPresentInArr(COMMIT_OBJECTS[i].hash, selectedCommits)) { 24 | COMMIT_OBJECTS[i].color = true; 25 | handleCommitConnections(COMMIT_OBJECTS[i].hash); 26 | } 27 | } 28 | 29 | for (let i = 0; i < COLORED_TREES.length; i++) { 30 | for (let j = 0; j < TREE_OBJECTS.length; j++) { 31 | if (TREE_OBJECTS[j].hash === COLORED_TREES[i]) { 32 | TREE_OBJECTS[j].color = true; 33 | handleTreeConnections(TREE_OBJECTS[j].hash); 34 | break; 35 | } 36 | } 37 | } 38 | 39 | if (COLORED_BLOBS.length > 0) { 40 | for (let i = 0; i < BLOB_OBJECTS.length; i++) { 41 | let id = getId(BLOB_OBJECTS[i].hash, BLOB_OBJECTS[i].name); 42 | 43 | if (isPresentInArr(id, COLORED_BLOBS)) BLOB_OBJECTS[i].color = true; 44 | } 45 | } 46 | 47 | COLORED_TREES = []; 48 | COLORED_BLOBS = []; 49 | 50 | return { 51 | objects: { 52 | commits: COMMIT_OBJECTS, 53 | trees: TREE_OBJECTS, 54 | blobs: BLOB_OBJECTS 55 | }, 56 | objectConnections: CONNECTIONS 57 | }; 58 | } 59 | 60 | function handleCommitConnections(commitHash) { 61 | for (let i = 0; i < CONNECTIONS.length; i++) { 62 | if (CONNECTIONS[i].start === commitHash) { 63 | CONNECTIONS[i].color = true; 64 | if (!isPresentInArr(CONNECTIONS[i].end, COLORED_TREES)) 65 | COLORED_TREES.push(CONNECTIONS[i].end); 66 | } 67 | } 68 | } 69 | 70 | function handleTreeConnections(treeHash) { 71 | for (let i = 0; i < CONNECTIONS.length; i++) { 72 | if (CONNECTIONS[i].start === treeHash) { 73 | CONNECTIONS[i].color = true; 74 | 75 | if (CONNECTIONS[i].end.length === 40) { 76 | if (!isPresentInArr(CONNECTIONS[i].end, COLORED_TREES)) 77 | // Implies a Tree Object not present in the tree array 78 | COLORED_TREES.push(CONNECTIONS[i].end); 79 | } else if (!isPresentInArr(CONNECTIONS[i].end, COLORED_BLOBS)) 80 | COLORED_BLOBS.push(CONNECTIONS[i].end); 81 | } 82 | } 83 | } 84 | 85 | function removeAllColors() { 86 | for (let i = 0; i < COMMIT_OBJECTS.length; i++) 87 | COMMIT_OBJECTS[i].color = false; 88 | for (let i = 0; i < TREE_OBJECTS.length; i++) TREE_OBJECTS[i].color = false; 89 | for (let i = 0; i < BLOB_OBJECTS.length; i++) BLOB_OBJECTS[i].color = false; 90 | for (let i = 0; i < CONNECTIONS.length; i++) CONNECTIONS[i].color = false; 91 | 92 | COLORED_TREES = []; 93 | COLORED_BLOBS = []; 94 | 95 | return { 96 | objects: { 97 | commits: COMMIT_OBJECTS, 98 | trees: TREE_OBJECTS, 99 | blobs: BLOB_OBJECTS 100 | }, 101 | objectConnections: CONNECTIONS 102 | }; 103 | } 104 | 105 | function addAllColors() { 106 | for (let i = 0; i < COMMIT_OBJECTS.length; i++) 107 | COMMIT_OBJECTS[i].color = true; 108 | for (let i = 0; i < TREE_OBJECTS.length; i++) TREE_OBJECTS[i].color = true; 109 | for (let i = 0; i < BLOB_OBJECTS.length; i++) BLOB_OBJECTS[i].color = true; 110 | for (let i = 0; i < CONNECTIONS.length; i++) CONNECTIONS[i].color = true; 111 | 112 | COLORED_TREES = []; 113 | COLORED_BLOBS = []; 114 | 115 | return { 116 | objects: { 117 | commits: COMMIT_OBJECTS, 118 | trees: TREE_OBJECTS, 119 | blobs: BLOB_OBJECTS 120 | }, 121 | objectConnections: CONNECTIONS 122 | }; 123 | } 124 | 125 | function isPresentInArr(target, arr) { 126 | return arr.some((element) => element === target); 127 | } 128 | 129 | export default colorObjectsAndConnections; 130 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Git Graph 2 | 3 | - Please check the [issues tab](https://github.com/HarshKapadia2/git-graph/issues) for things to work on. 4 | - Please [raise an issue](https://github.com/HarshKapadia2/git-graph/issues) to request a feature/modification or for reporting a bug, if it has not already been raised. 5 | 6 | ## Tech Stack 7 | 8 | - React.js ([Create React App](https://create-react-app.dev)) 9 | 10 | ## Gitpod Setup 11 | 12 | [![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/HarshKapadia2/git-graph) 13 | 14 | - On clicking on the above 'Open in Gitpod' button, a Gitpod Workspace with all dependencies installed and the development server started will open up. 15 | - Make sure to do a GitHub integration and grant permissions on the [Integrations page](https://gitpod.io/integrations) on Gitpod if not already done. 16 | - [Make a new Git branch](https://harshkapadia2.github.io/git_basics/#_branch_name_2) on Gitpod and [switch to the newly created branch](https://harshkapadia2.github.io/git_basics/#_git_switch). 17 | - The development server should already be started, but in case it isn't, run `npm run start` in the root directory of the project to start it. 18 | - Run `npm run build` to generate a production build in the `build` directory to test it out. Do not commit this directory. 19 | - Make the required contribution(s) on Gitpod in the new branch. 20 | - Please follow the [commit message format](https://harshkapadia2.github.io/git_basics/#_commit_messagetitle). 21 | - To open a PR 22 | - Fork this repo. (Top right corner on GitHub.) 23 | - On Gitpod, [add the forked repo as a remote](https://harshkapadia2.github.io/git_basics/#_add_connecting_repo_alias_connecting_repo_url_git). 24 | - [Push the new branch to the forked repo.](https://harshkapadia2.github.io/git_basics/#_git_push) 25 | - [Open a PR as usual](https://github.com/firstcontributions/first-contributions#submit-your-changes-for-review) from the forked repo on GitHub. 26 | 27 | ## Local Setup 28 | 29 | - Fork this repo. (Top right corner.) 30 | - Clone the forked repo using the [`git clone` command](https://harshkapadia2.github.io/git_basics/#_git_clone). 31 | - `cd` into the cloned repo directory. 32 | - Execute `npm install` in the root directory of the project to install all dependencies. 33 | - Execute `npm run start` in the root directory of the project to start the development server. 34 | - Run `npm run build` to generate a production build in the `build` directory to test it out. Do not commit this directory. 35 | - Make contribution(s). 36 | - Write meaningful commit messages and include the number (#) of the issue being resolved (if any) at the end of the commit message. 37 | 38 | Example: `:bug: fix: Resolve 'isCorrect' function error (#0)` 39 | 40 | [Commit message format](https://harshkapadia2.github.io/git_basics/#_commit_messagetitle) 41 | 42 | - Open a Pull Request (PR). 43 | - [Learn how to open a PR.](https://github.com/firstcontributions/first-contributions) 44 | - Solve one issue per PR, without any extra changes. 45 | - Include extra changes in a separate PR. 46 | 47 | ## Component Structure 48 | 49 | ![](repo-img/component-structure.png) 50 | 51 | ## `objectData` Structure 52 | 53 | ```json 54 | { 55 | "objects": { 56 | "commits": [ 57 | { 58 | "hash": "", 59 | "name": "|''", 60 | "branchHead": "|''", 61 | "color": 62 | }, 63 | ... 64 | ], 65 | "trees": [ 66 | { 67 | "hash": "", 68 | "name": "|''", 69 | "color": 70 | }, 71 | ... 72 | ], 73 | "blobs": [ 74 | { 75 | "hash": "", 76 | "name": "", 77 | "color": 78 | }, 79 | ... 80 | ] 81 | }, 82 | "objectConnections": [ 83 | { 84 | "start": "", 85 | "end": "", 86 | "color": 87 | }, 88 | ... 89 | ] 90 | } 91 | ``` 92 | 93 | ## `rawObjects` Structure 94 | 95 | ```json 96 | { 97 | "objects": [ 98 | { 99 | "commit": "", 100 | "branchHead": "|''", 101 | "commitMsg": "|''", 102 | "parentCommit": "|''", 103 | "tree": "", 104 | "blobs": [ 105 | { 106 | "type": "blob|tree", 107 | "name": "|", 108 | "hash": "" 109 | }, 110 | ... 111 | ] 112 | }, 113 | ... 114 | ], 115 | "recursiveTrees": { 116 | "": [ 117 | { 118 | "type": "blob|tree", 119 | "name": "|", 120 | "hash": "" 121 | }, 122 | ... 123 | ], 124 | ... 125 | } 126 | } 127 | ``` 128 | 129 | ## `branchInfo` Structure 130 | 131 | ```json 132 | { 133 | "currentBranch": { 134 | "name": "", 135 | "headHash": "" 136 | }, 137 | "allBranches": [ 138 | { 139 | "branchName": "", 140 | "branchHeadHash": "" 141 | }, 142 | ... 143 | ] 144 | } 145 | ``` 146 | 147 | ## Further Help 148 | 149 | If any further help is needed, do not hesitate to contact the author ([Harsh Kapadia](https://harshkapadia.me)) via Twitter [@harshgkapadia](https://twitter.com/harshgkapadia), [LinkedIn](https://www.linkedin.com/in/harshgkapadia) or e-mail ([contact@harshkapadia.me](mailto:contact@harshkapadia.me)). An [issue](https://github.com/HarshKapadia2/git-graph/issues) can be raised as well. 150 | -------------------------------------------------------------------------------- /src/util/generateObjects.js: -------------------------------------------------------------------------------- 1 | import { 2 | getObjectType, 3 | getHeadCommit, 4 | getDecompressedFileBuffer 5 | } from "./commonFns"; 6 | 7 | let FILE_ARR = []; 8 | let OBJECT_ARR = []; 9 | let RECURSED_DATA = {}; 10 | 11 | async function getObjects(fileObjects, branchInfo) { 12 | FILE_ARR = fileObjects; 13 | const head = await getHeadCommit(FILE_ARR, branchInfo.currentBranch.name); 14 | let commit = head; 15 | let parentCommit = ""; 16 | let tree = ""; 17 | 18 | // await recurseCommits(head); 19 | 20 | do { 21 | const commitFilePath = 22 | ".git/objects/" + commit.slice(0, 2) + "/" + commit.slice(2); 23 | 24 | let commitContent = await getDecompressedFileBuffer( 25 | FILE_ARR, 26 | commitFilePath 27 | ); 28 | if (commitContent === undefined) { 29 | FILE_ARR = []; 30 | OBJECT_ARR = []; 31 | RECURSED_DATA = {}; 32 | 33 | throw new Error("File not found."); 34 | } 35 | commitContent = commitContent.toString("utf-8").split("\n"); 36 | 37 | let commitMsg = getCommitMsg(commitContent); 38 | 39 | const treeArr = commitContent[0].split(" "); 40 | tree = treeArr[2]; 41 | 42 | const blobs = await getBlobs(tree); 43 | 44 | const parentCommitArr = commitContent[1].split(" "); 45 | if (parentCommitArr[0] === "parent") parentCommit = parentCommitArr[1]; 46 | else parentCommit = ""; 47 | 48 | let branchHead = ""; 49 | for (let i = 0; i < branchInfo.allBranches.length; i++) { 50 | if (branchInfo.allBranches[i].branchHeadHash === commit) { 51 | if (branchHead === "") 52 | branchHead = branchInfo.allBranches[i].branchName; 53 | else 54 | branchHead = 55 | branchHead + 56 | ", " + 57 | branchInfo.allBranches[i].branchName; 58 | } 59 | } 60 | 61 | OBJECT_ARR.push({ 62 | commit, 63 | branchHead, 64 | commitMsg, 65 | parentCommit, 66 | tree, 67 | blobs 68 | }); 69 | 70 | commit = parentCommit; 71 | } while (parentCommit !== ""); 72 | 73 | const objectData = { objects: OBJECT_ARR, recursiveTrees: RECURSED_DATA }; 74 | 75 | FILE_ARR = []; 76 | OBJECT_ARR = []; 77 | RECURSED_DATA = {}; 78 | 79 | return objectData; 80 | } 81 | 82 | // async function recurseCommits(startCommit = "") { 83 | // let commit = startCommit; 84 | // let parentCommits = []; 85 | // let tree = ""; 86 | 87 | // do { 88 | // const commitFilePath = 89 | // ".git/objects/" + commit.slice(0, 2) + "/" + commit.slice(2); 90 | 91 | // let commitContent = await getDecompressedFileBuffer(commitFilePath); 92 | // commitContent = commitContent.toString("utf-8").split("\n"); 93 | 94 | // const treeArr = commitContent[0].split(" "); 95 | // tree = treeArr[2]; 96 | 97 | // const blobs = await getBlobs(tree); 98 | 99 | // parentCommits = []; 100 | // let parentCommitArr = commitContent[1].split(" "); 101 | // if (parentCommitArr[0] === "parent") 102 | // parentCommits.push(parentCommitArr[1]); 103 | // else parentCommits[0] = ""; 104 | 105 | // parentCommitArr = commitContent[2].split(" "); 106 | // if (parentCommitArr[0] === "parent") { 107 | // parentCommits.push(parentCommitArr[1]); 108 | // OBJECT_ARR.push({ 109 | // commit, 110 | // commitMsg: "", 111 | // parentCommits, 112 | // tree, 113 | // blobs 114 | // }); 115 | // await recurseCommits(parentCommitArr[1]); 116 | // } else 117 | // OBJECT_ARR.push({ 118 | // commit, 119 | // commitMsg: "", 120 | // parentCommits, 121 | // tree, 122 | // blobs 123 | // }); 124 | 125 | // commit = parentCommits[0]; 126 | // } while (parentCommits[0] !== "" && !isCommitInObjArr(commit)); 127 | // } 128 | 129 | async function getBlobs(treeHash = "") { 130 | if (treeHash === "") throw new Error("Tree hash parameter missing."); 131 | 132 | const treeFilePath = 133 | ".git/objects/" + treeHash.slice(0, 2) + "/" + treeHash.slice(2); 134 | const treeContent = await getDecompressedFileBuffer(FILE_ARR, treeFilePath); 135 | if (treeContent === undefined) { 136 | FILE_ARR = []; 137 | OBJECT_ARR = []; 138 | RECURSED_DATA = {}; 139 | 140 | throw new Error("File not found."); 141 | } 142 | 143 | let index = treeContent.indexOf("\0"); 144 | let [type, length] = treeContent.toString("utf-8", 0, index).split(" "); 145 | length = parseInt(length); 146 | let blobArr = []; 147 | 148 | for (let nullIndex = index; nullIndex < length; index = nullIndex) { 149 | nullIndex = treeContent.indexOf("\0", index + 1); 150 | 151 | let [mode, name] = treeContent 152 | .toString("utf-8", index, nullIndex) 153 | .split(" "); 154 | 155 | nullIndex++; 156 | 157 | const hash = treeContent.toString("hex", nullIndex, (nullIndex += 20)); // '20' since the SHA1 hash is 20 bytes (40 hexadecimal characters per SHA1 hash) 158 | 159 | const objectType = await getObjectType(FILE_ARR, hash); 160 | 161 | blobArr.push({ type: objectType, name, hash }); 162 | 163 | if (objectType === "tree") { 164 | const newData = await getBlobs(hash); 165 | RECURSED_DATA[hash] = newData; 166 | } 167 | } 168 | 169 | return blobArr; 170 | } 171 | 172 | function getCommitMsg(commitObjContent = []) { 173 | let i = 0; 174 | let firstParent = commitObjContent[1].split(" "); 175 | let secondParent = commitObjContent[2].split(" "); 176 | 177 | if (firstParent[0] === "parent") { 178 | i++; 179 | if (secondParent[0] === "parent") i++; 180 | } 181 | 182 | i += 3; 183 | 184 | let temp = commitObjContent[i].split(" "); 185 | if (temp[0] === "gpgsig") { 186 | let j = 0; 187 | for (j = i; j < commitObjContent.length; j++) { 188 | if (commitObjContent[j] === " -----END PGP SIGNATURE-----") { 189 | break; 190 | } 191 | } 192 | i = j + 2; 193 | } 194 | 195 | i++; 196 | 197 | return commitObjContent[i]; 198 | } 199 | 200 | // function isCommitInObjArr(commitHash = "") { 201 | // return OBJECT_ARR.some((obj) => obj.commit === commitHash); 202 | // } 203 | 204 | export default getObjects; 205 | -------------------------------------------------------------------------------- /src/components/App/App.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState, useRef, useCallback } from "react"; 2 | import { directoryOpen } from "browser-fs-access"; 3 | import { MemoizedGraphArea } from "../GraphArea/GraphArea"; 4 | import ErrorMsg from "../ErrorMsg/ErrorMsg"; 5 | import CommitSelector from "../CommitSelector/CommitSelector"; 6 | import Loader from "../Loader/Loader"; 7 | import RawDataDisplay from "../RawDataDisplay/RawDataDisplay"; 8 | import BackToTop from "../BackToTop/BackToTop"; 9 | import IntroMsg from "../IntroMsg/IntroMsg"; 10 | import BranchSelector from "../BranchSelector/BranchSelector"; 11 | import getObjects from "../../util/generateObjects"; 12 | import formatObjects from "../../util/formatObjects"; 13 | import getConnections from "../../util/generateConnections"; 14 | import colorObjectsAndConnections from "../../util/coloring"; 15 | import getObjRawData from "../../util/objectRawData"; 16 | import getAllBranches from "../../util/generateBranchInfo"; 17 | import "./App.css"; 18 | 19 | function App() { 20 | const [fileBlobs, setFileBlobs] = useState([]); 21 | const [objectData, setObjectData] = useState({}); 22 | const [errorType, setErrorType] = useState(""); 23 | const [isLoading, setIsLoading] = useState(false); 24 | const [showCommitSelector, setShowCommitSelector] = useState(false); 25 | const [selectedCommits, setSelectedCommits] = useState([]); 26 | const [rawDataObjDetails, setRawDataObjDetails] = useState({}); 27 | const [objRawData, setObjRawData] = useState({}); 28 | const [branchInfo, setBranchInfo] = useState({}); 29 | 30 | const backToTopBtn = useRef(); 31 | const scrollToTopTriggerDiv = useRef(); 32 | const headerRef = useRef(); 33 | 34 | useEffect(() => { 35 | async function getBranchNames() { 36 | if (fileBlobs.length !== 0) { 37 | try { 38 | const branchInfoTemp = await getAllBranches(fileBlobs); 39 | setBranchInfo(branchInfoTemp); 40 | } catch (err) { 41 | if (err.message === "Branches not found.") 42 | setErrorType("no-branches"); 43 | } 44 | } 45 | } 46 | getBranchNames(); 47 | }, [fileBlobs]); 48 | 49 | useEffect(() => { 50 | if (branchInfo.currentBranch !== undefined) { 51 | setIsLoading(true); 52 | setShowCommitSelector(false); 53 | setObjectData({}); 54 | setSelectedCommits([]); 55 | parseObjects(); 56 | } 57 | }, [branchInfo]); 58 | 59 | useEffect(() => { 60 | if (objectData.objects !== undefined && selectedCommits.length !== 0) { 61 | const updatedObjectData = colorObjectsAndConnections( 62 | objectData, 63 | selectedCommits 64 | ); 65 | setObjectData(updatedObjectData); 66 | } 67 | }, [selectedCommits]); 68 | 69 | useEffect(() => { 70 | if (Object.keys(rawDataObjDetails).length !== 0) handleObjRawData(); 71 | }, [rawDataObjDetails]); 72 | 73 | useEffect(() => { 74 | if (errorType) { 75 | if (isLoading) setIsLoading(false); 76 | if (showCommitSelector) setShowCommitSelector(false); 77 | if (Object.keys(objectData).length !== 0) setObjectData({}); 78 | if (Object.keys(rawDataObjDetails).length !== 0) 79 | setRawDataObjDetails({}); 80 | if (Object.keys(objRawData).length !== 0) setObjRawData({}); 81 | if (fileBlobs.length !== 0) setFileBlobs([]); 82 | if (selectedCommits.length !== 0) setSelectedCommits([]); 83 | if (branchInfo.currentBranch !== undefined) setBranchInfo({}); 84 | } 85 | }, [errorType]); 86 | 87 | useEffect(() => { 88 | const observer = new IntersectionObserver(scrollToTop, { 89 | rootMargin: "-12%" 90 | }); 91 | observer.observe(scrollToTopTriggerDiv.current); 92 | }, []); 93 | 94 | useEffect(() => { 95 | if (Object.keys(objectData).length !== 0) { 96 | window.addEventListener("wheel", (event) => handleScroll(event), { 97 | passive: true 98 | }); 99 | } 100 | 101 | return () => 102 | window.removeEventListener( 103 | "wheel", 104 | (event) => handleScroll(event), 105 | { 106 | passive: true 107 | } 108 | ); 109 | }, [objectData]); 110 | 111 | const showDirectoryPicker = async () => { 112 | const skippedDirectories = [ 113 | "gitweb", 114 | "hooks", 115 | "lfs", 116 | "logs", 117 | "rebase-merge", 118 | "remotes", 119 | "info", 120 | "pack", 121 | "modules", 122 | "worktrees" 123 | ]; 124 | 125 | await directoryOpen({ 126 | recursive: true, 127 | skipDirectory: (dir) => { 128 | return skippedDirectories.some( 129 | (skippedDir) => skippedDir === dir.name 130 | ); 131 | } 132 | }) 133 | .then((blobs) => { 134 | if (showCommitSelector) setShowCommitSelector(false); 135 | 136 | setIsLoading(true); 137 | setFileBlobs(blobs); 138 | setBranchInfo({}); 139 | setObjectData({}); 140 | 141 | if (errorType) setErrorType(""); 142 | }) 143 | .catch((err) => console.error("ERROR: ", err)); 144 | }; 145 | 146 | const parseObjects = async () => { 147 | try { 148 | let rawObjects = await getObjects(fileBlobs, branchInfo); 149 | let objects = formatObjects(rawObjects); 150 | let objectConnections = getConnections(rawObjects); 151 | 152 | const headObj = { 153 | start: "head", 154 | end: objectConnections[0].start, 155 | color: true 156 | }; 157 | objectConnections.unshift(headObj); 158 | 159 | if (showCommitSelector) setShowCommitSelector(false); 160 | setObjectData({ objects, objectConnections }); 161 | setIsLoading(false); 162 | if (errorType) setErrorType(""); 163 | } catch (err) { 164 | if (err.message === "File not found.") setErrorType("packed-repo"); 165 | } 166 | }; 167 | 168 | const handleBranchChange = (chosenBranchName) => { 169 | let chosenBranchHeadHash = ""; 170 | for (let i = 0; i < branchInfo.allBranches.length; i++) { 171 | if (branchInfo.allBranches[i].branchName === chosenBranchName) { 172 | chosenBranchHeadHash = branchInfo.allBranches[i].branchHeadHash; 173 | break; 174 | } 175 | } 176 | 177 | const branchNamesTemp = { 178 | currentBranch: { 179 | name: chosenBranchName, 180 | headHash: chosenBranchHeadHash 181 | }, 182 | allBranches: branchInfo.allBranches 183 | }; 184 | 185 | setBranchInfo(branchNamesTemp); 186 | }; 187 | 188 | const handleObjRawData = async () => { 189 | const rawObjData = await getObjRawData( 190 | fileBlobs, 191 | rawDataObjDetails.objHash, 192 | branchInfo 193 | ); 194 | setObjRawData(rawObjData); 195 | }; 196 | 197 | const handleRawDataObjDetails = useCallback((objDetails) => { 198 | if (objDetails.objHash !== rawDataObjDetails.objHash) 199 | setRawDataObjDetails(objDetails); 200 | }, []); 201 | 202 | const dismissRawDataDisplay = () => { 203 | setRawDataObjDetails({}); 204 | setObjRawData({}); 205 | }; 206 | 207 | const scrollToTop = (entries, observer) => { 208 | entries.forEach((entry) => { 209 | if (entry.isIntersecting) { 210 | backToTopBtn.current.classList.add("hidden"); 211 | headerRef.current.classList.remove("header-border-bottom"); 212 | headerRef.current.classList.remove("header-dismiss"); 213 | } else { 214 | backToTopBtn.current.classList.remove("hidden"); 215 | headerRef.current.classList.add("header-border-bottom"); 216 | } 217 | }); 218 | }; 219 | 220 | const handleScroll = (event) => { 221 | if (event.deltaY > 0) headerRef.current.classList.add("header-dismiss"); 222 | else if (event.deltaY < 0) 223 | headerRef.current.classList.remove("header-dismiss"); 224 | }; 225 | 226 | return ( 227 | <> 228 |
229 |

Git Graph

230 | 231 |
232 | 235 | 236 | {branchInfo.currentBranch !== undefined ? ( 237 | isLoading ? ( 238 | <> 239 | 244 | 247 | 248 | ) : ( 249 | <> 250 | 255 | 260 | 261 | ) 262 | ) : null} 263 |
264 |
265 | 266 |
267 |
268 | 269 | {errorType !== "" ? ( 270 | 271 | ) : objectData.objects !== undefined ? ( 272 | 276 | ) : isLoading ? null : ( 277 | 278 | )} 279 | 280 | {showCommitSelector && ( 281 | 287 | )} 288 | 289 | {Object.keys(rawDataObjDetails).length !== 0 && ( 290 | 295 | )} 296 | 297 | {isLoading && } 298 | 299 | 303 |
304 | 305 | 346 | 347 | ); 348 | } 349 | 350 | export default App; 351 | --------------------------------------------------------------------------------