├── 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 |

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 |
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 |
30 | -
31 | Select the
.git directory of a repository for
32 | the graph to render.
33 |
34 | -
35 | Once the graph is rendered, one can
36 |
37 | -
38 | Use the Branch Selector to visualize any local
39 | branch.
40 |
41 | -
42 | Use the Commit Selector to highlight one or more
43 | Commits and their corresponding Trees and Blobs.
44 |
45 | -
46 | Hover over the objects and click on the 'Raw' button
47 | to view the contents of that Git Object.
48 |
49 |
50 |
51 |
52 |
53 |
Note
54 |
55 | -
56 | If the
.git directory is not visible in the
57 | directory picker, please enable hidden file viewing on the
58 | local machine.
59 |
60 | -
61 | Extremely huge repositories might not load due to browser
62 | memory constraints.
63 |
64 | -
65 | Please give suggestions and report errors and bugs by{" "}
66 |
71 | raising issues
72 |
73 | .
74 |
75 |
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 | 
28 |
29 | Select the branch to render 👇
30 |
31 | 
32 |
33 | Select commit(s) to highlight 👇
34 |
35 | 
36 |
37 | View the raw contents of any Git Object 👇
38 |
39 | 
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 | [](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 | 
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 |
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 |
--------------------------------------------------------------------------------