├── src-tauri
├── rust-toolchain
├── build.rs
├── icons
│ ├── OxidizedGitAppIcon.ico
│ ├── OxidizedGitAppIcon.png
│ ├── OxidizedGitAppIcon.icns
│ ├── OxidizedGitMainLogo.png
│ ├── OxidizedGitAppIcon_Old.png
│ ├── OxidizedGitAppIconShrunk.png
│ └── OxidizedGitAppIconEnlarged.png
├── Cargo.toml
├── tauri.conf.json
└── src
│ ├── config_manager.rs
│ ├── svg_row.rs
│ ├── parseable_info.rs
│ └── main.rs
├── .gitignore
├── .github
├── FUNDING.yml
├── dependabot.yml
└── workflows
│ └── main.yml
├── ui
├── import_jquery.js
├── treeView.css
├── mainStyle.css
├── svg_manager.js
└── index.html
├── screenshots
├── ChangesScreenshot.png
└── GraphScreenshot.png
├── package.json
├── vite.config.ts
├── current_version.json
├── README.md
└── LICENSE
/src-tauri/rust-toolchain:
--------------------------------------------------------------------------------
1 | [toolchain]
2 | channel = "1.70.0"
3 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | src-tauri/target/
2 | .idea/
3 | node_modules/
4 | ui/dist/
5 | .envrc
6 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [jlpatter]
4 |
--------------------------------------------------------------------------------
/ui/import_jquery.js:
--------------------------------------------------------------------------------
1 | import jQuery from "jquery";
2 |
3 | export default (window.$ = window.jQuery = jQuery);
4 |
--------------------------------------------------------------------------------
/screenshots/ChangesScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/screenshots/ChangesScreenshot.png
--------------------------------------------------------------------------------
/screenshots/GraphScreenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/screenshots/GraphScreenshot.png
--------------------------------------------------------------------------------
/src-tauri/icons/OxidizedGitAppIcon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/src-tauri/icons/OxidizedGitAppIcon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/OxidizedGitAppIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/src-tauri/icons/OxidizedGitAppIcon.png
--------------------------------------------------------------------------------
/src-tauri/icons/OxidizedGitAppIcon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/src-tauri/icons/OxidizedGitAppIcon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/OxidizedGitMainLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/src-tauri/icons/OxidizedGitMainLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/OxidizedGitAppIcon_Old.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/src-tauri/icons/OxidizedGitAppIcon_Old.png
--------------------------------------------------------------------------------
/src-tauri/icons/OxidizedGitAppIconShrunk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/src-tauri/icons/OxidizedGitAppIconShrunk.png
--------------------------------------------------------------------------------
/src-tauri/icons/OxidizedGitAppIconEnlarged.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jlpatter/oxidized_git/HEAD/src-tauri/icons/OxidizedGitAppIconEnlarged.png
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |
4 | # Maintain dependencies for GitHub Actions
5 | - package-ecosystem: "github-actions"
6 | # Workflow files stored in the
7 | # default location of `.github/workflows`
8 | directory: "/"
9 | schedule:
10 | interval: "daily"
11 |
12 | # Maintain dependencies for npm
13 | - package-ecosystem: "npm"
14 | directory: "/"
15 | schedule:
16 | interval: "weekly"
17 |
18 | # Maintain dependencies for Composer
19 | - package-ecosystem: "cargo"
20 | directory: "/src-tauri/"
21 | schedule:
22 | interval: "weekly"
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oxidized_git",
3 | "private": true,
4 | "scripts": {
5 | "dev": "vite",
6 | "build": "vite build",
7 | "preview": "vite preview",
8 | "tauri": "tauri"
9 | },
10 | "dependencies": {
11 | "@tauri-apps/api": "~1.5.3",
12 | "bootstrap": "~5.3.2",
13 | "@fortawesome/fontawesome-free": "~6.5.1",
14 | "highlight.js": "~11.9.0",
15 | "jquery": "~3.7.1",
16 | "resizable": "~1.2.1",
17 | "events": "~3.3.0"
18 | },
19 | "devDependencies": {
20 | "@tauri-apps/cli": "~1.5.9",
21 | "@types/node": "~20.11.5",
22 | "vite": "~4.5.2"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ui/treeView.css:
--------------------------------------------------------------------------------
1 | /* Remove default bullets */
2 | .sub-tree-view, .tree-view {
3 | list-style-type: none;
4 | }
5 |
6 | /* Remove margins and padding from the parent ul */
7 | .tree-view {
8 | margin: 0;
9 | padding: 0;
10 | }
11 |
12 | .sub-tree-view {
13 | padding-left: 20px;
14 | }
15 |
16 | .fa-caret-down {
17 | display: inline-block;
18 | transform: rotate(270deg);
19 | }
20 |
21 | .parent-tree, .inner-branch-item {
22 | white-space: nowrap;
23 | cursor: pointer;
24 | }
25 |
26 | /* Rotate the caret/arrow icon when clicked on (using JavaScript) */
27 | .rotated-caret {
28 | transform: rotate(0deg);
29 | }
30 |
31 | /* Hide the nested list */
32 | .nested {
33 | display: none;
34 | }
35 |
36 | /* Show the nested list when the user clicks on the caret/arrow (with JavaScript) */
37 | .active-tree {
38 | display: block;
39 | }
40 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 |
3 | export default defineConfig({
4 | // prevent vite from obscuring rust errors
5 | clearScreen: false,
6 | // Tauri expects a fixed port, fail if that port is not available
7 | server: {
8 | strictPort: true,
9 | },
10 | // to make use of `TAURI_PLATFORM`, `TAURI_ARCH`, `TAURI_FAMILY`,
11 | // `TAURI_PLATFORM_VERSION`, `TAURI_PLATFORM_TYPE` and `TAURI_DEBUG`
12 | // env variables
13 | envPrefix: ['VITE_', 'TAURI_'],
14 | root: 'ui',
15 | build: {
16 | // Tauri supports es2021
17 | target: ['es2021', 'chrome100', 'safari13'],
18 | // don't minify for debug builds
19 | minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
20 | // produce sourcemaps for debug builds
21 | sourcemap: !!process.env.TAURI_DEBUG,
22 | },
23 | })
24 |
--------------------------------------------------------------------------------
/src-tauri/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "oxidized_git"
3 | version = "1.2.3"
4 | description = "A Tauri Git Client Application"
5 | authors = ["Joshua Patterson"]
6 | license = "GPL-3.0-or-later"
7 | build = "build.rs"
8 | default-run = "oxidized_git"
9 | edition = "2021"
10 | rust-version = "1.69"
11 |
12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
13 |
14 | [build-dependencies]
15 | tauri-build = { version = "1.5.*", features = [] }
16 |
17 | [dependencies]
18 | serde_json = "1.0.*"
19 | serde = { version = "1.0.*", features = ["derive"] }
20 | serde_with = "3.5.*"
21 | tauri = { version = "1.5.*", features = ["clipboard-write-text", "dialog-open", "icon-ico", "icon-png", "path-all", "process-relaunch", "updater"] }
22 | git2 = "0.18.*"
23 | directories = "5.0.*"
24 | keytar = "0.1.*"
25 | html-escape = "0.2.*"
26 | anyhow = { version = "1.0.*", features = ["backtrace"] }
27 | time = { version = "0.3.*", features = ["local-offset", "formatting"] }
28 | # This is a hack so MacOS doesn't try to use homebrew's openssl. It should work with just the "native-tls-vendored" feature, but it doesn't for some reason...
29 | [target.'cfg(target_os = "macos")'.dependencies]
30 | openssl = { version = "*", features = ["vendored"] }
31 | openssl-sys = { version = "*", features = ["vendored"] }
32 |
33 | [features]
34 | # by default Tauri runs in production mode
35 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
36 | default = [ "custom-protocol" ]
37 | # this feature is used used for production builds where `devPath` points to the filesystem
38 | # DO NOT remove this
39 | custom-protocol = [ "tauri/custom-protocol" ]
40 |
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "beforeBuildCommand": "npm run build",
4 | "beforeDevCommand": "npm run dev",
5 | "devPath": "http://localhost:5173",
6 | "distDir": "../ui/dist"
7 | },
8 | "package": {
9 | "productName": "Oxidized Git",
10 | "version": "1.2.3"
11 | },
12 | "tauri": {
13 | "allowlist": {
14 | "clipboard": {
15 | "writeText": true
16 | },
17 | "dialog": {
18 | "open": true
19 | },
20 | "path": {
21 | "all": true
22 | },
23 | "process": {
24 | "relaunch": true
25 | }
26 | },
27 | "bundle": {
28 | "active": true,
29 | "category": "DeveloperTool",
30 | "copyright": "",
31 | "deb": {
32 | "depends": ["gnome-keyring", "libsecret"]
33 | },
34 | "externalBin": [],
35 | "icon": [
36 | "icons/OxidizedGitAppIcon.png",
37 | "icons/OxidizedGitAppIcon.icns",
38 | "icons/OxidizedGitAppIcon.ico"
39 | ],
40 | "identifier": "com.patterson.og",
41 | "longDescription": "",
42 | "macOS": {
43 | "entitlements": null,
44 | "exceptionDomain": "",
45 | "frameworks": [],
46 | "providerShortName": null,
47 | "signingIdentity": null
48 | },
49 | "resources": [],
50 | "shortDescription": "",
51 | "targets": "all",
52 | "windows": {
53 | "certificateThumbprint": null,
54 | "digestAlgorithm": "sha256",
55 | "timestampUrl": ""
56 | }
57 | },
58 | "security": {
59 | "csp": null
60 | },
61 | "updater": {
62 | "active": true,
63 | "endpoints": [
64 | "https://raw.githubusercontent.com/jlpatter/oxidized_git/master/current_version.json"
65 | ],
66 | "dialog": false,
67 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEIyREY1M0Q3NzBDN0NDNjIKUldSaXpNZHcxMVBmc21PWUxSMURmU1hsKzVTcks0NmdkMnZhZWlFSHcxOU9hR3hDYys1TUxoOXEK"
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/current_version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "v1.2.3",
3 | "notes": "Update dependencies",
4 | "platforms": {
5 | "darwin-x86_64": {
6 | "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSaXpNZHcxMVBmc3NQWmRWSTEvSVlyeGxhd1JOMUhHNCs4aEgrZy9ZWFpJWE9EOUs0Q1RpSUdoS0gxeFhxS3BiQUI3YUtUVXdZMWNYYmxMaHBMeEdwcUpkNjZtbVBINlFJPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjgyOTA2MDI4CWZpbGU6T3hpZGl6ZWQgR2l0LmFwcC50YXIuZ3oKVlpZNlNwNFRoVGlJOXJqQ3VRN3lXWGZ5SFNzQlJUdHljem1ELytESElSVzRpMlgyemgrbXhrdENnUWJxU1Vid3R1YmYzMmNTTTRORGMwUlVUSGhaRGc9PQo=",
7 | "url": "https://github.com/jlpatter/oxidized_git/releases/download/v1.2.3/oxidized_git_x64.app.tar.gz"
8 | },
9 | "darwin-aarch64": {
10 | "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSaXpNZHcxMVBmc2tybUtXQmVxNW1RZlNYaC9oVVFuSDlSUkI2Vk9hSUNWMFIwaGRrczVzVkpIUUZoSjlzOTlqUzdjclM3c05nTW1SZ2N3ZXVBblRDdVpZb0VVZmdac0F3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjgyOTc4NzkzCWZpbGU6T3hpZGl6ZWQgR2l0LmFwcC50YXIuZ3oKYjRvY1ZGZi9kVWZ0MFc5ZkJhQjlURXJMVkhrVWIvczFOL2FycmxQYXoySGNPbTZka3JBZmh3REI2c1NHZldsSWd3QjJzL1B3L0hEdThnZHVHMTJIQXc9PQo=",
11 | "url": "https://github.com/jlpatter/oxidized_git/releases/download/v1.2.3/oxidized_git_aarch64.app.tar.gz"
12 | },
13 | "linux-x86_64": {
14 | "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSaXpNZHcxMVBmc3JUbnlBUWQ1S1BoWXJIR1N1cy9KRjZwb1RJeWVrZEdlUnFYTzU2OG82bUJaQ3hha2RJWjQ3M2VhT3JGeEhxNUpSaWp0WjhsN2psc2c2T3pIbzF6SEFZPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjgyOTA1NjY3CWZpbGU6b3hpZGl6ZWQtZ2l0XzEuMi4zX2FtZDY0LkFwcEltYWdlLnRhci5negp2MksxeEJMamJib3doZjNyWVFMYTBVOTFjcTFGbHZnQjVvdmZKOVEwNCtiNFAwamx2eEl0alNrcFpRaU4wSVdMRzRCOFQweGZMQ29SODh2emtJK2RBdz09Cg==",
15 | "url": "https://github.com/jlpatter/oxidized_git/releases/download/v1.2.3/oxidized-git.AppImage.tar.gz"
16 | },
17 | "windows-x86_64": {
18 | "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVSaXpNZHcxMVBmc2xEOUdHWUR0bmJjaUpBMkZ0bTFZRkUxWnBPMExDTldKRmg1RXJXc0VBTG5GMUV1UnBhVnVENWtoYktLbVhWNTZDNC80U21vTkJaSjN2OExSQTB4TWd3PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNjgyOTA1OTQ2CWZpbGU6T3hpZGl6ZWQgR2l0XzEuMi4zX3g2NF9lbi1VUy5tc2kuemlwClF3TnZ4cXd2ZHFqZ2NaSk1EazNhcS84Nnk4MW9PQjJoSGRCY3FlbFM0K21JeXJMTkNqTytRcmw0MitBK2tVUUtybFFIekpvdHBXbW4rTlBTMm13ckNnPT0K",
19 | "url": "https://github.com/jlpatter/oxidized_git/releases/download/v1.2.3/oxidized_git.msi.zip"
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ## Features
6 | ### Easily view your commit history and perform various Git operations
7 |
8 |
9 |
10 |
11 | ### Stage changes before you commit them
12 |
13 |
14 |
15 |
16 | * Create/Delete Branches
17 | * Checkout branches (similar to git switch)
18 | * Commit
19 | * Fetch
20 | * Pull (automatically detects and performs either a fast-forward merge or a rebase!)
21 | * Push (with option to Force Push)
22 | * Save and Apply Stashes
23 | * View File Diffs in a Commit
24 | * Stage/Unstage and View Changes
25 | * Discard Changes
26 | * Merge
27 | * Rebase
28 | * Cherrypick
29 | * Revert
30 | * Reset (Soft, Mixed, or Hard)
31 |
32 | ## Usage
33 | Download and install the desired version from the "Releases"
34 | ### Linux
35 | * You may need to install the equivalent of WebView2 on Linux (if you're having trouble getting it work, maybe try installing dependencies listed here: https://tauri.app/v1/guides/getting-started/prerequisites#setting-up-linux)
36 | * Make sure you have gnome-keyring installed and libsecret. If it isn't working, make sure you've created a default keyring in it!
37 |
38 | ## For Development
39 | ### Windows
40 | * Install the Microsoft Visual Studio C++ build tools https://visualstudio.microsoft.com/visual-cpp-build-tools/
41 | * Install WebView2 https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section
42 | * Install Rust https://www.rust-lang.org/tools/install
43 | * Install NodeJS https://nodejs.org/en/
44 | * Install Strawberry Perl https://strawberryperl.com/
45 | * Continue to 'All' below
46 | ### Mac
47 | * Install xcode
48 | * Install homebrew
49 | * Install Rust: https://www.rust-lang.org/tools/install
50 | * Install NodeJS: `brew install node`
51 | * Continue to 'All' below
52 | ### Linux
53 | #### Debian-based distros (Untested)
54 | * Install Tauri dependencies: https://tauri.app/v1/guides/getting-started/prerequisites
55 | * Install Rust: https://www.rust-lang.org/tools/install
56 | * Install NodeJS: `sudo apt install nodejs npm`
57 | * Continue to 'All' below
58 | #### Arch-based distros
59 | * Install Tauri dependencies: https://tauri.app/v1/guides/getting-started/prerequisites
60 | * Install Rust: https://www.rust-lang.org/tools/install
61 | * Install NodeJS: `sudo pacman -S nodejs npm`
62 | * Continue to 'All' below
63 | ### All
64 | * Run `npm install` in the project root
65 | * (Optional) Consider setting the environment variable `RUST_BACKTRACE` to `1` if you want a backtrace when an error occurs
66 | * If `webkit2gtk` is showing a blank screen, try setting `WEBKIT_DISABLE_COMPOSITING_MODE` to `1`.
67 | * Run `npm run tauri dev` in the project root to run the dev environment or `npm run tauri build` to package the application
68 | ### Making a Release
69 | For creating release packages, you will need:
70 |
71 | * `TAURI_PRIVATE_KEY` and `TAURI_KEY_PASSWORD` set to sign updates for all versions
72 | * `APPLE_CERTIFICATE`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_ID`, `APPLE_PASSWORD`, `APPLE_PROVIDER_SHORT_NAME`, and `APPLE_SIGNING_IDENTITY` set to sign and notarize Apple versions
73 |
74 | There are 3 places that the version number needs to be updated BEFORE pushing the version tag (which should kick off the pipelines that create a GitHub release):
75 | * `src-tauri/Cargo.toml`
76 | * `src-tauri/Cargo.lock`
77 | * `src-tauri/tauri.conf.json`
78 |
79 | Once the GitHub release has been created and published (which you have to do manually), you'll need to update the `version`
80 | field and the versions in the urls and the `signature` fields (by copying the signatures generated in the associated `.sig` files) in `current_version.json`
81 | and push it up (so that the tauri updater will automatically download from the new release).
82 |
--------------------------------------------------------------------------------
/src-tauri/src/config_manager.rs:
--------------------------------------------------------------------------------
1 | use std::fs;
2 | use std::fs::create_dir_all;
3 | use std::path::PathBuf;
4 | use anyhow::{bail, Result};
5 | use serde::{Serialize, Deserialize};
6 | use directories::ProjectDirs;
7 |
8 | #[serde_with::skip_serializing_none]
9 | #[derive(Clone, Serialize, Deserialize)]
10 | pub struct Config {
11 | limit_commits: Option,
12 | commit_count: Option,
13 | cred_type: Option,
14 | https_username: Option,
15 | public_key_path: Option,
16 | private_key_path: Option,
17 | uses_passphrase: Option,
18 | }
19 |
20 | impl Config {
21 | pub fn new_default() -> Self {
22 | Self {
23 | limit_commits: Some(true),
24 | commit_count: Some(2000),
25 | cred_type: None,
26 | https_username: None,
27 | public_key_path: None,
28 | private_key_path: None,
29 | uses_passphrase: None,
30 | }
31 | }
32 |
33 | pub fn borrow_limit_commits(&self) -> &Option {
34 | &self.limit_commits
35 | }
36 |
37 | pub fn borrow_commit_count(&self) -> &Option {
38 | &self.commit_count
39 | }
40 |
41 | pub fn borrow_cred_type(&self) -> &Option {
42 | &self.cred_type
43 | }
44 |
45 | pub fn borrow_https_username(&self) -> &Option {
46 | &self.https_username
47 | }
48 |
49 | pub fn borrow_public_key_path(&self) -> &Option {
50 | &self.public_key_path
51 | }
52 |
53 | pub fn borrow_private_key_path(&self) -> &Option {
54 | &self.private_key_path
55 | }
56 |
57 | pub fn borrow_uses_passphrase(&self) -> &Option {
58 | &self.uses_passphrase
59 | }
60 |
61 | pub fn set_cred_type(&mut self, cred_type: String) {
62 | self.cred_type = Some(cred_type);
63 | }
64 |
65 | pub fn set_https_username(&mut self, new_username: String) {
66 | self.https_username = Some(new_username);
67 | }
68 |
69 | pub fn set_public_key_path(&mut self, public_key_path: PathBuf) {
70 | self.public_key_path = Some(public_key_path);
71 | }
72 |
73 | pub fn set_private_key_path(&mut self, private_key_path: PathBuf) {
74 | self.private_key_path = Some(private_key_path);
75 | }
76 |
77 | pub fn set_uses_passphrase(&mut self, uses_passphrase: bool) {
78 | self.uses_passphrase = Some(uses_passphrase);
79 | }
80 |
81 | pub fn save(&self) -> Result<()> {
82 | let config_path_buf = get_config_path()?;
83 | let config_path = config_path_buf.as_path();
84 | if !config_path.exists() {
85 | let prefix = match config_path.parent() {
86 | Some(p) => p,
87 | None => bail!("Config path prefix not defined. This should never happen if the library is working."),
88 | };
89 | if !prefix.exists() {
90 | create_dir_all(prefix)?;
91 | }
92 | }
93 | fs::write(config_path, serde_json::to_string_pretty(&self)?)?;
94 | Ok(())
95 | }
96 | }
97 |
98 | fn get_config_path() -> Result {
99 | let pd = match ProjectDirs::from("com", "Oxidized Git", "Oxidized Git") {
100 | Some(pd) => pd,
101 | None => bail!("Failed to determine HOME directory on your OS"),
102 | };
103 | let config_path = pd.config_dir();
104 | let mut config_path_buf = config_path.to_path_buf();
105 | config_path_buf.push(PathBuf::from("config.json"));
106 | Ok(config_path_buf)
107 | }
108 |
109 | fn save_default_config() -> Result<()> {
110 | let config = Config::new_default();
111 | config.save()?;
112 | Ok(())
113 | }
114 |
115 | pub fn save_config_from_json(payload: &str) -> Result<()> {
116 | let config: Config = serde_json::from_str(payload)?;
117 | config.save()?;
118 | Ok(())
119 | }
120 |
121 | pub fn get_config() -> Result {
122 | let config_path_buf = get_config_path()?;
123 | let config_path = config_path_buf.as_path();
124 | if !config_path.exists() {
125 | save_default_config()?;
126 | }
127 | let data_string = fs::read_to_string(config_path)?;
128 | let config: Config = serde_json::from_str(&*data_string)?;
129 | Ok(config)
130 | }
131 |
--------------------------------------------------------------------------------
/ui/mainStyle.css:
--------------------------------------------------------------------------------
1 | body {
2 | display: block;
3 | height: 100vh;
4 | width: 100vw;
5 | overflow: hidden;
6 | font-size: 15px;
7 | }
8 |
9 | textarea {
10 | width: 100%;
11 | }
12 |
13 | #welcomeView {
14 | justify-content: center;
15 | align-content: center;
16 | }
17 |
18 | .nav-tabs .nav-link.active {
19 | color: white;
20 | background-color: black;
21 | }
22 |
23 | .tab-btn-xsm {
24 | padding: 2px 4px;
25 | }
26 |
27 | .display-flex-column {
28 | display: flex;
29 | flex-direction: column;
30 | }
31 |
32 | .display-flex-row {
33 | display: flex;
34 | flex-direction: row;
35 | }
36 |
37 | .full-height {
38 | height: 100%;
39 | }
40 |
41 | .half-height {
42 | height: 50%;
43 | }
44 |
45 | .full-width {
46 | width: 100% !important; /* important is required to override Resizable's width */
47 | }
48 |
49 | .flex-auto-in-column {
50 | flex: auto;
51 | min-height: 0;
52 | }
53 |
54 | .flex-auto-in-row {
55 | flex: auto;
56 | min-width: 0;
57 | }
58 |
59 | .resizable-column {
60 | border-color: white;
61 | border-right: 1px solid;
62 | position: relative;
63 | flex: none;
64 | width: 16.66%; /* This is the initial width */
65 | }
66 |
67 | .resizable-row {
68 | border-color: white;
69 | border-bottom: 1px solid;
70 | position: relative;
71 | flex: none;
72 | }
73 |
74 | .text-align-center {
75 | text-align: center;
76 | }
77 |
78 | .little-padding-top {
79 | padding-top: 10px;
80 | }
81 |
82 | .little-padding-bottom {
83 | padding-bottom: 10px;
84 | }
85 |
86 | .little-padding-left {
87 | padding-left: 10px;
88 | }
89 |
90 | .little-padding-right {
91 | padding-right: 10px;
92 | }
93 |
94 | #spinnerContainer {
95 | position: absolute;
96 | top: 0;
97 | left: 0;
98 | width: 100vw;
99 | text-align: center;
100 | }
101 |
102 | .file-path-txt, .no-margin-bottom {
103 | margin-bottom: 0;
104 | }
105 |
106 | .white-space-nowrap {
107 | white-space: nowrap;
108 | }
109 |
110 | .hoverable-row, .svg-hoverable-row {
111 | cursor: pointer;
112 | }
113 |
114 | .hoverable-row:hover {
115 | background-color: rgba(255, 255, 255, 0.075);
116 | }
117 |
118 | .selected-row {
119 | background-color: rgba(255, 255, 255, 0.1);
120 | }
121 |
122 | .added-code-line {
123 | background-color: rgba(0, 255, 0, 0.2);
124 | }
125 |
126 | .removed-code-line {
127 | background-color: rgba(255, 0, 0, 0.2);
128 | }
129 |
130 | .hljs {
131 | background-color: transparent;
132 | }
133 |
134 | pre {
135 | margin-bottom: 0;
136 | padding-left: 10px;
137 | }
138 |
139 | pre code.hljs {
140 | padding: 0;
141 | }
142 |
143 | .line-no {
144 | text-align: right;
145 | padding-right: 5px;
146 | }
147 |
148 | .line-content {
149 | width: 100%;
150 | }
151 |
152 | .controls {
153 | flex: initial;
154 | padding-left: 10px;
155 | padding-right: 10px;
156 | padding-bottom: 10px;
157 | }
158 |
159 | .right {
160 | float: right;
161 | }
162 |
163 | .right-padding {
164 | float: right;
165 | padding-right: 10px;
166 | }
167 |
168 | .cm-item {
169 | text-align: left;
170 | }
171 |
172 | .text-red {
173 | color: red;
174 | }
175 |
176 | .text-grey {
177 | color: grey;
178 | }
179 |
180 | .text-overflow-ellipsis {
181 | white-space: nowrap;
182 | overflow: hidden;
183 | text-overflow: ellipsis;
184 | }
185 |
186 | .text-unselectable {
187 | -webkit-user-select: none;
188 | -moz-user-select: none;
189 | -ms-user-select: none;
190 | user-select: none;
191 | }
192 |
193 | #contextMenu {
194 | display: grid;
195 | position: absolute;
196 | max-width: 500px;
197 | margin: 10px;
198 | -webkit-border-radius: 15px;
199 | border-radius: 15px;
200 | }
201 |
202 | #errorModalDialog {
203 | max-width: 80vw;
204 | }
205 |
206 | svg text {
207 | font-family: monospace;
208 | font-size: 12px;
209 | }
210 |
211 | svg .svg-hoverable-row {
212 | opacity: 0;
213 | }
214 |
215 | svg .svg-hoverable-row:hover {
216 | opacity: 0.75;
217 | }
218 |
219 | svg .svg-selected-row {
220 | opacity: 1.0;
221 | }
222 |
223 | ::-webkit-scrollbar {
224 | width: 12px;
225 | }
226 |
227 | ::-webkit-scrollbar-track {
228 | -webkit-box-shadow: inset 0 0 6px rgba(200,200,200,1);
229 | border-radius: 10px;
230 | }
231 |
232 | ::-webkit-scrollbar-thumb {
233 | border-radius: 10px;
234 | background-color: #fff;
235 | -webkit-box-shadow: inset 0 0 6px rgba(90, 90, 90, 0.7);
236 | }
237 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | # This is a basic workflow to help you get started with Actions
2 |
3 | name: CI
4 |
5 | env:
6 | bundle_app_name_prefix: oxidized-git
7 | bundle_app_name_prefix_underscore: oxidized_git
8 | bundle_app_name_prefix_verbose: Oxidized Git
9 |
10 | # Controls when the workflow will run
11 | on:
12 | # Triggers the workflow on version tag pushes
13 | push:
14 | tags:
15 | - 'v*.*.*'
16 |
17 | # Allows you to run this workflow manually from the Actions tab
18 | workflow_dispatch:
19 |
20 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
21 | jobs:
22 | build:
23 | env:
24 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
25 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
26 | APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_SIGNING_IDENTITY }}
27 | APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
28 | APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
29 | APPLE_ID: ${{ secrets.APPLE_ID }}
30 | APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
31 | APPLE_PROVIDER_SHORT_NAME: ${{ secrets.APPLE_PROVIDER_SHORT_NAME }}
32 |
33 | strategy:
34 | matrix:
35 | # Note that "macos-latest" has been removed as my Apple developer subscription has expired :(
36 | os: [ubuntu-latest, windows-latest]
37 | build: [release]
38 | # The type of runner that the job will run on
39 | runs-on: ${{ matrix.os }}
40 |
41 | # Steps represent a sequence of tasks that will be executed as part of the job
42 | steps:
43 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
44 | - uses: actions/checkout@v3
45 |
46 | - uses: actions/cache@v3
47 | with:
48 | path: |
49 | ~/.cargo/bin/
50 | ~/.cargo/registry/index/
51 | ~/.cargo/registry/cache/
52 | ~/.cargo/git/db/
53 | src-tauri/target/
54 | !src-tauri/target/bundle
55 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
56 |
57 | - name: Install system dependencies for Tauri
58 | run: |
59 | if [ "$RUNNER_OS" == "Linux" ]; then
60 | sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev \
61 | build-essential \
62 | curl \
63 | wget \
64 | libssl-dev \
65 | libgtk-3-dev \
66 | libayatana-appindicator3-dev \
67 | librsvg2-dev
68 | elif [ "$RUNNER_OS" == "Windows" ]; then
69 | echo "No need for additional Tauri dependencies for $RUNNER_OS"
70 | exit 0
71 | elif [ "$RUNNER_OS" == "macOS" ]; then
72 | echo "No need for additional Tauri dependencies for $RUNNER_OS"
73 | exit 0
74 | else
75 | echo "$RUNNER_OS not supported"
76 | exit 1
77 | fi
78 | shell: bash
79 |
80 | - name: Fetch Node Dependencies
81 | run: npm install
82 |
83 | - name: Run Tauri Build
84 | run: npm run tauri build
85 |
86 | - name: Rename AppImage Artifacts
87 | working-directory: ./src-tauri/target/release/bundle/appimage
88 | if: matrix.os == 'ubuntu-latest'
89 | run: |
90 | mv $(find . -name '${{ env.bundle_app_name_prefix }}_*.AppImage') ${{ env.bundle_app_name_prefix }}.AppImage && \
91 | mv $(find . -name '${{ env.bundle_app_name_prefix }}_*.AppImage.tar.gz') ${{ env.bundle_app_name_prefix }}.AppImage.tar.gz && \
92 | mv $(find . -name '${{ env.bundle_app_name_prefix }}_*.AppImage.tar.gz.sig') ${{ env.bundle_app_name_prefix }}.AppImage.tar.gz.sig
93 |
94 | - name: Upload AppImage Artifacts
95 | uses: ncipollo/release-action@v1
96 | if: matrix.os == 'ubuntu-latest'
97 | with:
98 | artifacts: "src-tauri/target/release/bundle/appimage/${{ env.bundle_app_name_prefix }}.AppImage,src-tauri/target/release/bundle/appimage/${{ env.bundle_app_name_prefix }}.AppImage.tar.gz,src-tauri/target/release/bundle/appimage/${{ env.bundle_app_name_prefix }}.AppImage.tar.gz.sig"
99 | allowUpdates: true
100 | draft: true
101 | token: ${{ secrets.GITHUB_TOKEN }}
102 |
103 | - name: Rename MacOS Artifacts
104 | working-directory: ./src-tauri/target/release/bundle/macos
105 | if: matrix.os == 'macos-latest'
106 | run: |
107 | mv "${{ env.bundle_app_name_prefix_verbose }}.app.tar.gz" ${{ env.bundle_app_name_prefix_underscore }}_x64.app.tar.gz && \
108 | mv "${{ env.bundle_app_name_prefix_verbose }}.app.tar.gz.sig" ${{ env.bundle_app_name_prefix_underscore }}_x64.app.tar.gz.sig
109 |
110 | - name: Rename MacOS DMG Artifact
111 | working-directory: ./src-tauri/target/release/bundle/dmg
112 | if: matrix.os == 'macos-latest'
113 | run: |
114 | mv "$(find . -name '${{ env.bundle_app_name_prefix_verbose }}_*.dmg')" ${{ env.bundle_app_name_prefix_underscore }}_x64.dmg
115 | shell: bash
116 |
117 | - name: Upload MacOS Artifacts
118 | uses: ncipollo/release-action@v1
119 | if: matrix.os == 'macos-latest'
120 | with:
121 | artifacts: "src-tauri/target/release/bundle/dmg/${{ env.bundle_app_name_prefix_underscore }}_x64.dmg,src-tauri/target/release/bundle/macos/${{ env.bundle_app_name_prefix_underscore }}_x64.app.tar.gz,src-tauri/target/release/bundle/macos/${{ env.bundle_app_name_prefix_underscore }}_x64.app.tar.gz.sig"
122 | allowUpdates: true
123 | draft: true
124 | token: ${{ secrets.GITHUB_TOKEN }}
125 |
126 | - name: Rename MSI Artifacts
127 | working-directory: ./src-tauri/target/release/bundle/msi
128 | if: matrix.os == 'windows-latest'
129 | run: |
130 | mv "$(find . -name '${{ env.bundle_app_name_prefix_verbose }}_*.msi')" ${{ env.bundle_app_name_prefix_underscore }}.msi && \
131 | mv "$(find . -name '${{ env.bundle_app_name_prefix_verbose }}_*.msi.zip')" ${{ env.bundle_app_name_prefix_underscore }}.msi.zip && \
132 | mv "$(find . -name '${{ env.bundle_app_name_prefix_verbose }}_*.msi.zip.sig')" ${{ env.bundle_app_name_prefix_underscore }}.msi.zip.sig
133 | shell: bash
134 |
135 | - name: Upload MSI Artifacts
136 | uses: ncipollo/release-action@v1
137 | if: matrix.os == 'windows-latest'
138 | with:
139 | artifacts: "src-tauri/target/release/bundle/msi/${{ env.bundle_app_name_prefix_underscore }}.msi,src-tauri/target/release/bundle/msi/${{ env.bundle_app_name_prefix_underscore }}.msi.zip,src-tauri/target/release/bundle/msi/${{ env.bundle_app_name_prefix_underscore }}.msi.zip.sig"
140 | allowUpdates: true
141 | draft: true
142 | token: ${{ secrets.GITHUB_TOKEN }}
143 |
--------------------------------------------------------------------------------
/ui/svg_manager.js:
--------------------------------------------------------------------------------
1 | import {writeText} from "@tauri-apps/api/clipboard";
2 | import {emit} from "@tauri-apps/api/event";
3 |
4 | /**
5 | * A class to manage the svg element.
6 | */
7 | export class SVGManager {
8 | Y_SPACING = 24; // If changing, be sure to update on backend-end too
9 | Y_OFFSET = 20; // If changing, be sure to update on backend-end too
10 | BRANCH_TEXT_SPACING = 5;
11 | RIGHT_TEXT_SPACING = 10;
12 | SCROLL_RENDERING_MARGIN = 100;
13 | SCROLLBAR_WIDTH = 12; // If changing, be sure to update in CSS!
14 | /**
15 | * Constructs the svg manager.
16 | */
17 | constructor(mainJS) {
18 | this.commitColumn = document.getElementById('commitColumn');
19 | this.commitTableSVG = document.getElementById('commitTableSVG');
20 | this.rows = [];
21 | this.commitsTop = -99;
22 | this.commitsBottom = -99;
23 | this.selectedSHA = '';
24 | this.mainJS = mainJS;
25 | this.setScrollEvent();
26 | }
27 |
28 | getSingleCharWidth() {
29 | const self = this,
30 | $textSizeTestContainer = $(''),
31 | textSizeTest = self.makeSVG('text', {id: 'textSizeTest', x: 0, y: 0, fill: 'white'});
32 | textSizeTest.textContent = 'a';
33 | $textSizeTestContainer.append(textSizeTest);
34 | $('#mainBody').append($textSizeTestContainer);
35 | const singleCharWidth = textSizeTest.getBBox().width;
36 | $textSizeTestContainer.remove();
37 | return singleCharWidth;
38 | }
39 |
40 | /**
41 | * Refreshes the commit table. Can be called on its own for a passive refresh.
42 | */
43 | updateGraph(commitsInfo, headSHA) {
44 | const self = this,
45 | singleCharWidth = self.getSingleCharWidth();
46 |
47 | for (let i = 0; i < self.rows.length; i++) {
48 | self.removeBranchLabels(self.rows[i]);
49 | }
50 |
51 | const graphWidth = Number(self.commitTableSVG.getAttribute('width'));
52 | if (commitsInfo['svg_row_draw_properties'].length > 0) {
53 | self.rows = [];
54 |
55 | for (let i = 0; i < commitsInfo['svg_row_draw_properties'].length; i++) {
56 | const commit = commitsInfo['svg_row_draw_properties'][i];
57 | const elements = commit['elements'];
58 | let row = {'sha': commit['sha'], 'pixel_y': commit['pixel_y'], 'lines': [], 'branches': [], 'circle': null, 'summaryTxt': null, 'authorName': null, 'authorTime': null, 'backRect': null};
59 | for (const childLine of elements['child_lines']) {
60 | const line = self.makeSVG(childLine['tag'], childLine['attrs']);
61 | if (childLine['row-y'] < i) {
62 | self.rows[childLine['row-y']]['lines'].push(line);
63 | } else if (childLine['row-y'] === i) {
64 | row['lines'].push(line);
65 | } else {
66 | console.error("ERROR: A child line is trying to be added after the current node!");
67 | }
68 | }
69 | row['circle'] = self.makeSVG(elements['circle']['tag'], elements['circle']['attrs']);
70 |
71 | const summaryTxt = self.makeSVG(elements['summary_text']['tag'], elements['summary_text']['attrs']);
72 | summaryTxt.textContent = elements['summary_text']['textContent'];
73 | row['summaryTxt'] = summaryTxt;
74 |
75 | const authorTimeX = graphWidth - (elements['author_time']['textContent'].length * singleCharWidth) - self.RIGHT_TEXT_SPACING;
76 | elements['author_time']['attrs']['x'] = authorTimeX;
77 | const authorTime = self.makeSVG(elements['author_time']['tag'], elements['author_time']['attrs']);
78 | authorTime.textContent = elements['author_time']['textContent'];
79 | row['authorTime'] = authorTime;
80 |
81 | elements['author_name']['attrs']['x'] = authorTimeX - (elements['author_name']['textContent'].length * singleCharWidth) - self.RIGHT_TEXT_SPACING;
82 | const authorName = self.makeSVG(elements['author_name']['tag'], elements['author_name']['attrs']);
83 | authorName.textContent = elements['author_name']['textContent'];
84 | row['authorName'] = authorName;
85 |
86 | elements['back_rect']['attrs']['width'] = graphWidth - elements['circle']['attrs']['cx'];
87 | const backRect = self.makeSVG(elements['back_rect']['tag'], elements['back_rect']['attrs']);
88 | backRect.onclick = self.getClickFunction(commit['sha']);
89 | backRect.ondblclick = self.getDblClickFunction(commit['sha']);
90 | backRect.oncontextmenu = self.getContextFunction(commit['sha']);
91 | row['backRect'] = backRect;
92 |
93 | self.rows.push(row);
94 | }
95 | }
96 |
97 | self.addBranchLabels(commitsInfo['branch_draw_properties'], singleCharWidth);
98 | for (let i = 0; i < self.rows.length; i++) {
99 | self.truncateSummaryTxt(i, singleCharWidth);
100 | }
101 |
102 | self.setVisibleCommits();
103 | self.commitTableSVG.setAttribute('height', ((self.rows.length + 1) * self.Y_SPACING).toString());
104 | self.selectRowOnRefresh(headSHA);
105 | }
106 |
107 | selectRowOnRefresh(headSHA) {
108 | const self = this;
109 |
110 | self.mainJS.commitFileDiffTableScrollTop = $('#commitFileDiffTableContainer').scrollTop();
111 |
112 | let selectedIndex = 0;
113 | let foundOldSelected = false;
114 | if (self.selectedSHA !== '') {
115 | const tempIndex = self.rows.findIndex(function(row) {
116 | return row['sha'] === self.selectedSHA;
117 | });
118 | if (tempIndex !== -1) {
119 | selectedIndex = tempIndex;
120 | foundOldSelected = true;
121 | } else {
122 | self.selectedSHA = '';
123 | }
124 | }
125 | if (!foundOldSelected && headSHA !== '') {
126 | const tempIndex = self.rows.findIndex(function(row) {
127 | return row['sha'] === headSHA;
128 | });
129 | if (tempIndex !== -1) {
130 | selectedIndex = tempIndex;
131 | }
132 | }
133 | if (selectedIndex >= 0 && selectedIndex < self.rows.length) {
134 | self.selectRow(self.rows[selectedIndex]['backRect'], self.rows[selectedIndex]['sha']);
135 | }
136 | }
137 |
138 | truncateSummaryTxt(rowIndex, singleCharWidth) {
139 | const self = this;
140 |
141 | let summaryTxtContent = self.rows[rowIndex]['summaryTxt'].getAttribute('data-original-txt');
142 | const summaryEndX = Number(self.rows[rowIndex]['summaryTxt'].getAttribute('x')) + summaryTxtContent.length * singleCharWidth;
143 | const authorNameX = Number(self.rows[rowIndex]['authorName'].getAttribute('x'));
144 | if (summaryEndX > authorNameX) {
145 | const numOfCharsToRemove = Math.ceil((summaryEndX - authorNameX) / singleCharWidth) + 3;
146 | summaryTxtContent = summaryTxtContent.slice(0, -numOfCharsToRemove);
147 | summaryTxtContent += '...';
148 | self.rows[rowIndex]['summaryTxt'].textContent = summaryTxtContent;
149 | }
150 | }
151 |
152 | addBranchLabels(branchDrawProperties, singleCharWidth) {
153 | const self = this;
154 |
155 | for (let i = 0; i < branchDrawProperties.length; i++) {
156 | const rowIndex = self.rows.findIndex(function (row) {
157 | return row['sha'] === branchDrawProperties[i][0];
158 | });
159 | if (rowIndex !== -1) {
160 | const summaryTxtElem = self.rows[rowIndex]['summaryTxt'];
161 | const pixel_y = Number(self.rows[rowIndex]['circle'].getAttribute('cy'));
162 | let currentPixelX = Number(summaryTxtElem.getAttribute('x'));
163 | for (let j = 0; j < branchDrawProperties[i][1].length; j++) {
164 | const branch = branchDrawProperties[i][1][j];
165 | branch[0]['attrs']['x'] = currentPixelX;
166 | branch[0]['attrs']['y'] += pixel_y;
167 | const txtElem = self.makeSVG(branch[0]['tag'], branch[0]['attrs']);
168 | const box_width = singleCharWidth * branch[0]['textContent'].length + 10;
169 |
170 | branch[1]['attrs']['x'] = currentPixelX - 5;
171 | branch[1]['attrs']['y'] += pixel_y;
172 | branch[1]['attrs']['width'] = box_width;
173 | const rectElem = self.makeSVG(branch[1]['tag'], branch[1]['attrs']);
174 | txtElem.textContent = branch[0]['textContent'];
175 |
176 | self.rows[rowIndex]['branches'].push(rectElem);
177 | self.rows[rowIndex]['branches'].push(txtElem);
178 |
179 | currentPixelX += box_width + self.BRANCH_TEXT_SPACING;
180 | summaryTxtElem.setAttribute('x', currentPixelX.toString());
181 | }
182 | }
183 | }
184 | }
185 |
186 | removeBranchLabels(row) {
187 | if (row['branches'].length > 0) {
188 | const startX = Number(row['branches'][1].getAttribute('x'));
189 | row['branches'] = [];
190 | row['summaryTxt'].setAttribute('x', startX.toString());
191 | }
192 | }
193 |
194 | renderVisibleCommits() {
195 | const self = this;
196 |
197 | let df = document.createDocumentFragment();
198 | for (let i = self.commitsTop; i <= self.commitsBottom; i++) {
199 | self.rows[i]['lines'].forEach((line) => {
200 | df.appendChild(line);
201 | });
202 | }
203 |
204 | for (let i = self.commitsTop; i <= self.commitsBottom; i++) {
205 | df.appendChild(self.rows[i]['circle']);
206 | df.appendChild(self.rows[i]['summaryTxt']);
207 | df.appendChild(self.rows[i]['authorName']);
208 | df.appendChild(self.rows[i]['authorTime']);
209 | self.rows[i]['branches'].forEach((branch) => {
210 | df.appendChild(branch);
211 | });
212 | df.appendChild(self.rows[i]['backRect']);
213 | }
214 |
215 | self.commitTableSVG.innerHTML = '';
216 |
217 | self.commitTableSVG.appendChild(df);
218 | }
219 |
220 | setGraphWidth() {
221 | const self = this,
222 | singleCharWidth = self.getSingleCharWidth(),
223 | newGraphWidth = $('#mainBody').width() - self.commitTableSVG.getBoundingClientRect().left - self.SCROLLBAR_WIDTH;
224 |
225 | self.commitTableSVG.setAttribute('width', newGraphWidth.toString());
226 | for (let i = 0; i < self.rows.length; i++) {
227 | const authorTimeX = newGraphWidth - (self.rows[i]['authorTime'].textContent.length * singleCharWidth) - self.RIGHT_TEXT_SPACING;
228 | self.rows[i]['authorTime'].setAttribute('x', authorTimeX.toString());
229 | self.rows[i]['authorName'].setAttribute('x', (authorTimeX - (self.rows[i]['authorName'].textContent.length * singleCharWidth) - self.RIGHT_TEXT_SPACING).toString());
230 | self.rows[i]['backRect'].setAttribute('width', (newGraphWidth - Number(self.rows[i]['circle'].getAttribute('cx'))).toString());
231 | self.truncateSummaryTxt(i, singleCharWidth);
232 | }
233 | }
234 |
235 | setVisibleCommits() {
236 | const self = this;
237 | if (self.rows.length > 0) {
238 | const renderingAreaTop = self.commitColumn.scrollTop - self.SCROLL_RENDERING_MARGIN,
239 | renderingAreaBottom = self.commitColumn.scrollTop + self.commitColumn.clientHeight + self.SCROLL_RENDERING_MARGIN;
240 |
241 | // Convert from pixels to index.
242 | self.commitsTop = Math.max(Math.round((renderingAreaTop - self.Y_OFFSET) / self.Y_SPACING), 0);
243 | self.commitsBottom = Math.min(Math.round((renderingAreaBottom - self.Y_OFFSET) / self.Y_SPACING), self.rows.length - 1);
244 |
245 | self.renderVisibleCommits();
246 | }
247 | }
248 |
249 | scrollToCommit(sha) {
250 | const self = this;
251 | if (sha !== '') {
252 | const rowIndex = self.rows.findIndex(function(row) {
253 | return row['sha'] === sha;
254 | });
255 | if (rowIndex !== -1) {
256 | const rowPixelY = rowIndex * self.Y_SPACING + self.Y_OFFSET;
257 | const halfClientHeight = self.commitColumn.clientHeight / 2;
258 | // scrollTop automatically bounds itself for negative numbers or numbers greater than the max scroll position.
259 | self.commitColumn.scrollTop = rowPixelY - halfClientHeight;
260 | self.setVisibleCommits();
261 | }
262 | }
263 | }
264 |
265 | setScrollEvent() {
266 | const self = this;
267 | self.commitColumn.addEventListener('scroll', () => {
268 | self.setVisibleCommits();
269 | });
270 | }
271 |
272 | /**
273 | * Makes an SVG element
274 | * @param {string} tag
275 | * @param {Object} attrs
276 | * @return {SVGElement|SVGGraphicsElement}
277 | */
278 | makeSVG(tag, attrs) {
279 | const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
280 | // eslint-disable-next-line guard-for-in
281 | for (const k in attrs) {
282 | el.setAttribute(k, attrs[k]);
283 | }
284 | return el;
285 | }
286 |
287 | unselectAllRows() {
288 | const svgRowElements = document.querySelectorAll('.svg-selected-row');
289 | svgRowElements.forEach((svgRowElement) => {
290 | svgRowElement.classList.remove('svg-selected-row');
291 | svgRowElement.classList.add('svg-hoverable-row');
292 | });
293 |
294 | $('#commit-info').empty();
295 | $('#commitChanges').empty();
296 | $('#commitFileDiffTable').empty();
297 | }
298 |
299 | selectRow(backRectElement, sha) {
300 | const self = this;
301 | self.unselectAllRows();
302 | backRectElement.classList.add('svg-selected-row');
303 | backRectElement.classList.remove('svg-hoverable-row');
304 | self.selectedSHA = sha;
305 | // Will call start-process from back-end
306 | emit("get-commit-info", sha).then();
307 | }
308 |
309 | selectRowViaSha(sha) {
310 | const self = this;
311 | const row = self.rows.find(function(row) {
312 | return row['sha'] === sha;
313 | });
314 | if (row !== undefined) {
315 | self.selectRow(row['backRect'], sha);
316 | }
317 | }
318 |
319 | getClickFunction(sha) {
320 | const self = this;
321 | return function(event) {
322 | self.selectRow(event.target, sha);
323 | };
324 | }
325 |
326 | getDblClickFunction(sha) {
327 | return function(event) {
328 | emit("checkout-detached-head", sha).then();
329 | }
330 | }
331 |
332 | /**
333 | * Gets the function to be called by oncontextmenu
334 | * @return {(function(*): void)|*}
335 | */
336 | getContextFunction(sha) {
337 | const self = this;
338 | return function(event) {
339 | event.preventDefault();
340 | const $contextMenu = $('#contextMenu');
341 | $contextMenu.empty();
342 | $contextMenu.css('left', event.pageX + 'px');
343 | $contextMenu.css('top', event.pageY + 'px');
344 |
345 | const $tagBtn = $('');
346 | $tagBtn.click(function() {
347 | $('#tagSha').text(sha);
348 | $('#tagModal').modal('show');
349 | });
350 | $contextMenu.append($tagBtn);
351 |
352 | const $mergeBtn = $('');
353 | $mergeBtn.click(function() {
354 | emit("merge", sha).then();
355 | });
356 | $contextMenu.append($mergeBtn);
357 |
358 | const $rebaseBtn = $('');
359 | $rebaseBtn.click(function() {
360 | self.mainJS.addProcessCount();
361 | emit("rebase", sha).then();
362 | });
363 | $contextMenu.append($rebaseBtn);
364 |
365 | const $cherrypickBtn = $('');
366 | $cherrypickBtn.click(function() {
367 | $('#cherrypickSha').text(sha);
368 | $('#commitCherrypickCheckBox').prop('checked', true);
369 | $('#cherrypickModal').modal('show');
370 | });
371 | $contextMenu.append($cherrypickBtn);
372 |
373 | const $revertBtn = $('');
374 | $revertBtn.click(function() {
375 | $('#revertSha').text(sha);
376 | $('#commitRevertCheckBox').prop('checked', true);
377 | $('#revertModal').modal('show');
378 | });
379 | $contextMenu.append($revertBtn);
380 |
381 | const $copyShaBtn = $('');
382 | $copyShaBtn.click(function() {
383 | writeText(sha).then();
384 | });
385 | $contextMenu.append($copyShaBtn);
386 |
387 | const $softResetBtn = $('');
388 | $softResetBtn.click(function() {
389 | emit("reset", {sha: sha, type: "soft"}).then();
390 | });
391 | $contextMenu.append($softResetBtn);
392 |
393 | const $mixedResetBtn = $('');
394 | $mixedResetBtn.click(function() {
395 | emit("reset", {sha: sha, type: "mixed"}).then();
396 | });
397 | $contextMenu.append($mixedResetBtn);
398 |
399 | const $hardResetBtn = $('');
400 | $hardResetBtn.click(function() {
401 | emit("reset", {sha: sha, type: "hard"}).then();
402 | });
403 | $contextMenu.append($hardResetBtn);
404 |
405 | $contextMenu.show();
406 | };
407 | }
408 | }
409 |
--------------------------------------------------------------------------------
/src-tauri/src/svg_row.rs:
--------------------------------------------------------------------------------
1 | use std::cell::RefCell;
2 | use std::collections::HashMap;
3 | use std::rc::Rc;
4 | use anyhow::{bail, Result};
5 | use serde::{Serialize, Serializer};
6 | use crate::parseable_info::ParseableCommitInfo;
7 |
8 | #[derive(Clone)]
9 | pub enum SVGPropertyAttrs {
10 | SomeString(String),
11 | SomeInt(isize),
12 | }
13 |
14 | impl Serialize for SVGPropertyAttrs {
15 | fn serialize(&self, serializer: S) -> Result where S: Serializer {
16 | match &self {
17 | SVGPropertyAttrs::SomeString(st) => st.serialize(serializer),
18 | SVGPropertyAttrs::SomeInt(i) => i.serialize(serializer),
19 | }
20 | }
21 | }
22 |
23 | #[derive(Clone)]
24 | pub enum SVGProperty {
25 | SomeInt(isize),
26 | SomeString(String),
27 | SomeHashMap(HashMap),
28 | }
29 |
30 | impl Serialize for SVGProperty {
31 | fn serialize(&self, serializer: S) -> Result where S: Serializer {
32 | match &self {
33 | SVGProperty::SomeInt(i) => i.serialize(serializer),
34 | SVGProperty::SomeString(st) => st.serialize(serializer),
35 | SVGProperty::SomeHashMap(hm) => hm.serialize(serializer),
36 | }
37 | }
38 | }
39 |
40 | #[derive(Clone)]
41 | pub enum DrawProperty {
42 | SomeHashMap(HashMap),
43 | SomeVector(Vec>),
44 | SomeVectorVector(Vec>>),
45 | }
46 |
47 | impl Serialize for DrawProperty {
48 | fn serialize(&self, serializer: S) -> Result where S: Serializer {
49 | match &self {
50 | DrawProperty::SomeHashMap(hm) => hm.serialize(serializer),
51 | DrawProperty::SomeVector(v) => v.serialize(serializer),
52 | DrawProperty::SomeVectorVector(v) => v.serialize(serializer),
53 | }
54 | }
55 | }
56 |
57 | #[derive(Clone)]
58 | pub enum RowProperty {
59 | SomeInt(isize),
60 | SomeString(String),
61 | SomeHashMap(HashMap),
62 | }
63 |
64 | impl Serialize for RowProperty {
65 | fn serialize(&self, serializer: S) -> Result where S: Serializer {
66 | match &self {
67 | RowProperty::SomeInt(i) => i.serialize(serializer),
68 | RowProperty::SomeString(s) => s.serialize(serializer),
69 | RowProperty::SomeHashMap(hm) => hm.serialize(serializer),
70 | }
71 | }
72 | }
73 |
74 | const Y_SPACING: isize = 24; // If changing, be sure to update on front-end too
75 | const Y_OFFSET: isize = 20; // If changing, be sure to update on front-end too
76 | const X_SPACING: isize = 15;
77 | const X_OFFSET: isize = 10;
78 | const TEXT_Y_OFFSET: isize = 5;
79 | const CIRCLE_RADIUS: isize = 5;
80 | const LINE_STROKE_WIDTH: isize = 2;
81 | const RECT_HEIGHT: isize = 18;
82 | const RECT_Y_OFFSET: isize = -(RECT_HEIGHT / 2);
83 |
84 | #[derive(Clone)]
85 | pub struct SVGRow {
86 | sha: String,
87 | author_name: String,
88 | author_time: String,
89 | summary: String,
90 | parent_oids: Vec,
91 | child_oids: Vec,
92 | has_parent_child_svg_rows_set: bool,
93 | parent_svg_rows: Vec>>,
94 | child_svg_rows: Vec>>,
95 | x: isize,
96 | y: isize,
97 | }
98 |
99 | impl SVGRow {
100 | pub fn from_commit_info(commit_info: &ParseableCommitInfo) -> Self {
101 | Self {
102 | sha: commit_info.borrow_sha().clone(),
103 | author_name: commit_info.borrow_author_name().clone(),
104 | author_time: commit_info.borrow_author_time().clone(),
105 | summary: commit_info.borrow_summary().clone(),
106 | parent_oids: commit_info.borrow_parent_shas().clone(),
107 | child_oids: commit_info.borrow_child_shas().clone(),
108 | has_parent_child_svg_rows_set: false,
109 | parent_svg_rows: vec![],
110 | child_svg_rows: vec![],
111 | x: commit_info.borrow_x().clone(),
112 | y: commit_info.borrow_y().clone(),
113 | }
114 | }
115 |
116 | pub fn set_parent_and_child_svg_row_values(&mut self, all_svg_rows: &HashMap>>) {
117 | for sha in &self.parent_oids {
118 | match all_svg_rows.get(&*sha) {
119 | Some(svg_row_rc) => {
120 | self.parent_svg_rows.push(svg_row_rc.clone());
121 | },
122 | // If a parent is not present, ignore it. It may be outside the revwalk range.
123 | None => (),
124 | };
125 | }
126 |
127 | for sha in &self.child_oids {
128 | match all_svg_rows.get(&*sha) {
129 | Some(svg_row_rc) => {
130 | self.child_svg_rows.push(svg_row_rc.clone());
131 | },
132 | // If a child is not present, ignore it. It may be outside the revwalk range.
133 | None => (),
134 | };
135 | }
136 |
137 | self.has_parent_child_svg_rows_set = true;
138 | }
139 |
140 | fn get_color_string(x: isize) -> String {
141 | let color_num = x % 4;
142 | if color_num == 0 {
143 | String::from("#00CC19")
144 | } else if color_num == 1 {
145 | String::from("#0198A6")
146 | } else if color_num == 2 {
147 | String::from("#FF7800")
148 | } else {
149 | String::from("#FF0D00")
150 | }
151 | }
152 |
153 | pub fn get_occupied_table(svg_rows: &Vec>>) -> Result>> {
154 | let mut main_table: Vec> = vec![];
155 |
156 | for svg_row_rc in svg_rows.iter() {
157 | let mut svg_row = svg_row_rc.borrow_mut();
158 |
159 | if !svg_row.has_parent_child_svg_rows_set {
160 | bail!("SVGRow object didn't have parents or children set. Make sure 'set_parent_and_child_svg_row_values' is run before 'get_occupied_table'!");
161 | }
162 |
163 | // Set the current node position as occupied (or find a position that's unoccupied and occupy it).
164 | if svg_row.y < main_table.len() as isize {
165 | while main_table[svg_row.y as usize].contains(&svg_row.x) {
166 | svg_row.x += 1;
167 | }
168 | main_table[svg_row.y as usize].push(svg_row.x);
169 | } else {
170 | main_table.push(vec![svg_row.x]);
171 | }
172 |
173 | // Set the space of the line from the current node to its parents as occupied.
174 | for parent_svg_row_rc in &svg_row.parent_svg_rows {
175 | let mut parent_svg_row = parent_svg_row_rc.borrow_mut();
176 | let mut moved_x_val = 0;
177 | for i in (svg_row.y + 1)..parent_svg_row.y {
178 | let mut x_val = svg_row.x;
179 | if i < main_table.len() as isize {
180 | while main_table[i as usize].contains(&x_val) {
181 | x_val += 1;
182 | // Note: this has to stay in the loop so it's only set when x changes!
183 | // and not just to svg_row.x
184 | moved_x_val = x_val;
185 | }
186 | main_table[i as usize].push(x_val);
187 | } else {
188 | main_table.push(vec![x_val]);
189 | }
190 | }
191 | // This is used particularly for merging lines
192 | parent_svg_row.x = moved_x_val;
193 | }
194 | }
195 |
196 | // Loop through after everything's set in order to properly occupy spaces by curved lines just for summary text positions.
197 | for svg_row_rc in svg_rows {
198 | let svg_row = svg_row_rc.borrow();
199 |
200 | for parent_svg_row_rc in &svg_row.parent_svg_rows {
201 | let parent_svg_row = parent_svg_row_rc.borrow();
202 | if svg_row.x < parent_svg_row.x {
203 | let x_val = parent_svg_row.x;
204 | main_table[svg_row.y as usize].push(x_val);
205 | } else if svg_row.x > parent_svg_row.x {
206 | let x_val = svg_row.x;
207 | main_table[parent_svg_row.y as usize].push(x_val);
208 | }
209 | }
210 | }
211 |
212 | Ok(main_table)
213 | }
214 |
215 | pub fn get_draw_properties(&mut self, main_table: &Vec>) -> HashMap {
216 | let mut row_properties: HashMap = HashMap::new();
217 | let mut draw_properties: HashMap = HashMap::new();
218 |
219 | row_properties.insert(String::from("sha"), RowProperty::SomeString(self.sha.clone()));
220 |
221 | let pixel_x = self.x * X_SPACING + X_OFFSET;
222 | let pixel_y = self.y * Y_SPACING + Y_OFFSET;
223 | row_properties.insert(String::from("pixel_y"), RowProperty::SomeInt(pixel_y));
224 | let color = SVGRow::get_color_string(self.x);
225 | let mut child_lines: Vec> = vec![];
226 | // Draw the lines from the current node's children to itself.
227 | for child_svg_row_rc in &self.child_svg_rows {
228 | let child_svg_row = child_svg_row_rc.borrow();
229 | let child_pixel_x = child_svg_row.x * X_SPACING + X_OFFSET;
230 | let child_pixel_y = child_svg_row.y * Y_SPACING + Y_OFFSET;
231 | let before_y = self.y - 1;
232 | let before_pixel_y = before_y * Y_SPACING + Y_OFFSET;
233 | if before_pixel_y != child_pixel_y {
234 | let start_index;
235 | let end_index;
236 | let line_pixel_x;
237 | if self.x > child_svg_row.x {
238 | line_pixel_x = pixel_x;
239 | start_index = child_svg_row.y + 1;
240 | end_index = before_y;
241 | } else {
242 | line_pixel_x = child_pixel_x;
243 | start_index = child_svg_row.y;
244 | end_index = before_y - 1;
245 | }
246 | for i in start_index..=end_index {
247 | let top_pixel_y = i * Y_SPACING + Y_OFFSET;
248 | let bottom_pixel_y = (i + 1) * Y_SPACING + Y_OFFSET;
249 |
250 | let style_str = String::from("stroke:") + SVGRow::get_color_string((line_pixel_x - X_OFFSET) / X_SPACING).as_str() +
251 | ";stroke-width:" + LINE_STROKE_WIDTH.to_string().as_str();
252 | let line_attrs: HashMap = HashMap::from([
253 | (String::from("x1"), SVGPropertyAttrs::SomeInt(line_pixel_x)),
254 | (String::from("y1"), SVGPropertyAttrs::SomeInt(top_pixel_y)),
255 | (String::from("x2"), SVGPropertyAttrs::SomeInt(line_pixel_x)),
256 | (String::from("y2"), SVGPropertyAttrs::SomeInt(bottom_pixel_y)),
257 | (String::from("style"), SVGPropertyAttrs::SomeString(style_str)),
258 | ]);
259 | child_lines.push(HashMap::from([
260 | (String::from("tag"), SVGProperty::SomeString(String::from("line"))),
261 | (String::from("attrs"), SVGProperty::SomeHashMap(line_attrs)),
262 | (String::from("row-y"), SVGProperty::SomeInt(i + 1)),
263 | ]));
264 | }
265 | }
266 | let mut style_str = String::from("stroke:");
267 | let row_y = self.y;
268 | if child_svg_row.x >= self.x {
269 | // Sets the color for "branching" lines and straight lines
270 | style_str += SVGRow::get_color_string(child_svg_row.x).as_str();
271 | } else {
272 | // Sets the color for "merging" lines
273 | style_str += SVGRow::get_color_string(self.x).as_str();
274 | }
275 | style_str += ";fill:transparent;stroke-width:";
276 | style_str += LINE_STROKE_WIDTH.to_string().as_str();
277 | if child_pixel_x == pixel_x {
278 | let line_attrs: HashMap = HashMap::from([
279 | (String::from("x1"), SVGPropertyAttrs::SomeInt(child_pixel_x)),
280 | (String::from("y1"), SVGPropertyAttrs::SomeInt(before_pixel_y)),
281 | (String::from("x2"), SVGPropertyAttrs::SomeInt(pixel_x)),
282 | (String::from("y2"), SVGPropertyAttrs::SomeInt(pixel_y)),
283 | (String::from("style"), SVGPropertyAttrs::SomeString(style_str)),
284 | ]);
285 | child_lines.push(HashMap::from([
286 | (String::from("tag"), SVGProperty::SomeString(String::from("line"))),
287 | (String::from("attrs"), SVGProperty::SomeHashMap(line_attrs)),
288 | (String::from("row-y"), SVGProperty::SomeInt(row_y)),
289 | ]));
290 | } else {
291 | let d_str;
292 | if child_pixel_x < pixel_x {
293 | let after_child_pixel_y = (child_svg_row.y + 1) * Y_SPACING + Y_OFFSET;
294 | let start_control_point_x = child_pixel_x + X_SPACING * 3 / 4;
295 | let end_control_point_y = after_child_pixel_y - Y_SPACING * 3 / 4;
296 | d_str = format!("M {child_pixel_x} {child_pixel_y} C {start_control_point_x} {child_pixel_y}, {pixel_x} {end_control_point_y}, {pixel_x} {after_child_pixel_y}");
297 | } else {
298 | let start_control_point_y = before_pixel_y + Y_SPACING * 3 / 4;
299 | let end_control_point_x = pixel_x + X_SPACING * 3 / 4;
300 | d_str = format!("M {child_pixel_x} {before_pixel_y} C {child_pixel_x} {start_control_point_y}, {end_control_point_x} {pixel_y}, {pixel_x} {pixel_y}");
301 | }
302 | let path_attrs: HashMap = HashMap::from([
303 | (String::from("d"), SVGPropertyAttrs::SomeString(d_str)),
304 | (String::from("style"), SVGPropertyAttrs::SomeString(style_str)),
305 | ]);
306 | child_lines.push(HashMap::from([
307 | (String::from("tag"), SVGProperty::SomeString(String::from("path"))),
308 | (String::from("attrs"), SVGProperty::SomeHashMap(path_attrs)),
309 | (String::from("row-y"), SVGProperty::SomeInt(row_y)),
310 | ]));
311 | }
312 | }
313 | draw_properties.insert(String::from("child_lines"), DrawProperty::SomeVector(child_lines));
314 |
315 | // Now get the circle
316 | let circle_attrs: HashMap = HashMap::from([
317 | (String::from("cx"), SVGPropertyAttrs::SomeInt(pixel_x)),
318 | (String::from("cy"), SVGPropertyAttrs::SomeInt(pixel_y)),
319 | (String::from("r"), SVGPropertyAttrs::SomeInt(CIRCLE_RADIUS)),
320 | (String::from("stroke"), SVGPropertyAttrs::SomeString(color.clone())),
321 | (String::from("stroke-width"), SVGPropertyAttrs::SomeInt(1)),
322 | (String::from("fill"), SVGPropertyAttrs::SomeString(color.clone())),
323 | ]);
324 | draw_properties.insert(String::from("circle"), DrawProperty::SomeHashMap(HashMap::from([
325 | (String::from("tag"), SVGProperty::SomeString(String::from("circle"))),
326 | (String::from("attrs"), SVGProperty::SomeHashMap(circle_attrs)),
327 | ])));
328 |
329 | let largest_occupied_x = main_table[self.y as usize].iter().max().unwrap_or(&0);
330 |
331 | // Get summary text
332 | let text_attrs: HashMap = HashMap::from([
333 | (String::from("x"), SVGPropertyAttrs::SomeInt((largest_occupied_x + 1) * X_SPACING + X_OFFSET)),
334 | (String::from("y"), SVGPropertyAttrs::SomeInt(pixel_y + TEXT_Y_OFFSET)),
335 | (String::from("fill"), SVGPropertyAttrs::SomeString(String::from("white"))),
336 | (String::from("data-original-txt"), SVGPropertyAttrs::SomeString(self.summary.clone())),
337 | ]);
338 | draw_properties.insert(String::from("summary_text"), DrawProperty::SomeHashMap(HashMap::from([
339 | (String::from("tag"), SVGProperty::SomeString(String::from("text"))),
340 | (String::from("attrs"), SVGProperty::SomeHashMap(text_attrs)),
341 | (String::from("textContent"), SVGProperty::SomeString(self.summary.clone())),
342 | ])));
343 |
344 | // Get author name
345 | let text_attrs: HashMap = HashMap::from([
346 | (String::from("x"), SVGPropertyAttrs::SomeInt(0)),
347 | (String::from("y"), SVGPropertyAttrs::SomeInt(pixel_y + TEXT_Y_OFFSET)),
348 | (String::from("fill"), SVGPropertyAttrs::SomeString(String::from("white"))),
349 | ]);
350 | draw_properties.insert(String::from("author_name"), DrawProperty::SomeHashMap(HashMap::from([
351 | (String::from("tag"), SVGProperty::SomeString(String::from("text"))),
352 | (String::from("attrs"), SVGProperty::SomeHashMap(text_attrs)),
353 | (String::from("textContent"), SVGProperty::SomeString(self.author_name.clone())),
354 | ])));
355 |
356 | // Get author time
357 | let text_attrs: HashMap = HashMap::from([
358 | (String::from("x"), SVGPropertyAttrs::SomeInt(0)),
359 | (String::from("y"), SVGPropertyAttrs::SomeInt(pixel_y + TEXT_Y_OFFSET)),
360 | (String::from("fill"), SVGPropertyAttrs::SomeString(String::from("white"))),
361 | ]);
362 | draw_properties.insert(String::from("author_time"), DrawProperty::SomeHashMap(HashMap::from([
363 | (String::from("tag"), SVGProperty::SomeString(String::from("text"))),
364 | (String::from("attrs"), SVGProperty::SomeHashMap(text_attrs)),
365 | (String::from("textContent"), SVGProperty::SomeString(self.author_time.clone())),
366 | ])));
367 |
368 | // Get background rectangle
369 | let rect_attrs: HashMap = HashMap::from([
370 | (String::from("class"), SVGPropertyAttrs::SomeString(String::from("svg-hoverable-row"))),
371 | (String::from("x"), SVGPropertyAttrs::SomeInt(pixel_x)),
372 | (String::from("y"), SVGPropertyAttrs::SomeInt(pixel_y + RECT_Y_OFFSET)),
373 | (String::from("width"), SVGPropertyAttrs::SomeInt(0)),
374 | (String::from("height"), SVGPropertyAttrs::SomeInt(RECT_HEIGHT)),
375 | (String::from("style"), SVGPropertyAttrs::SomeString(String::from("fill:white;fill-opacity:0.1;"))),
376 | ]);
377 | draw_properties.insert(String::from("back_rect"), DrawProperty::SomeHashMap(HashMap::from([
378 | (String::from("tag"), SVGProperty::SomeString(String::from("rect"))),
379 | (String::from("attrs"), SVGProperty::SomeHashMap(rect_attrs)),
380 | ])));
381 |
382 | row_properties.insert(String::from("elements"), RowProperty::SomeHashMap(draw_properties));
383 |
384 | row_properties
385 | }
386 |
387 | pub fn get_branch_draw_properties(branches_and_tags: Vec<(String, String)>) -> Vec>> {
388 | // Get the branch text
389 | let mut branch_and_tags: Vec>> = vec![];
390 | for (branch_name, branch_type) in branches_and_tags.clone().into_iter() {
391 | let mut branch_and_tag_properties: Vec> = vec![];
392 | let text_attrs: HashMap = HashMap::from([
393 | (String::from("x"), SVGPropertyAttrs::SomeInt(0)),
394 | (String::from("y"), SVGPropertyAttrs::SomeInt(TEXT_Y_OFFSET)),
395 | (String::from("fill"), SVGPropertyAttrs::SomeString(String::from("white"))),
396 | ]);
397 | branch_and_tag_properties.push(HashMap::from([
398 | (String::from("tag"), SVGProperty::SomeString(String::from("text"))),
399 | (String::from("attrs"), SVGProperty::SomeHashMap(text_attrs)),
400 | (String::from("textContent"), SVGProperty::SomeString(branch_name.clone())),
401 | ]));
402 |
403 | let mut branch_rect_color = "yellow";
404 | if branch_type == "local" {
405 | branch_rect_color = "red";
406 | } else if branch_type == "remote" {
407 | branch_rect_color = "green";
408 | } else if branch_type == "tag" {
409 | branch_rect_color = "grey";
410 | }
411 |
412 | let style_str = String::from("fill:") + branch_rect_color + ";fill-opacity:0.5;";
413 | let rect_attrs: HashMap = HashMap::from([
414 | (String::from("x"), SVGPropertyAttrs::SomeInt(0)),
415 | (String::from("y"), SVGPropertyAttrs::SomeInt(RECT_Y_OFFSET)),
416 | (String::from("rx"), SVGPropertyAttrs::SomeInt(10)),
417 | (String::from("ry"), SVGPropertyAttrs::SomeInt(10)),
418 | (String::from("width"), SVGPropertyAttrs::SomeInt(0)),
419 | (String::from("height"), SVGPropertyAttrs::SomeInt(RECT_HEIGHT)),
420 | (String::from("style"), SVGPropertyAttrs::SomeString(style_str)),
421 | ]);
422 | branch_and_tag_properties.push(HashMap::from([
423 | (String::from("tag"), SVGProperty::SomeString(String::from("rect"))),
424 | (String::from("attrs"), SVGProperty::SomeHashMap(rect_attrs)),
425 | ]));
426 | branch_and_tags.push(branch_and_tag_properties);
427 | }
428 | branch_and_tags
429 | }
430 | }
431 |
--------------------------------------------------------------------------------
/src-tauri/src/parseable_info.rs:
--------------------------------------------------------------------------------
1 | use std::cell::RefCell;
2 | use std::collections::{HashMap, VecDeque};
3 | use std::rc::Rc;
4 | use anyhow::{bail, Result};
5 | use git2::{BranchType, Diff, ErrorCode, Oid, RepositoryState};
6 | use serde::{Serialize, Deserialize, Serializer};
7 | use time::{format_description, OffsetDateTime};
8 | use crate::git_manager::GitManager;
9 | use crate::svg_row::{RowProperty, SVGProperty, SVGRow};
10 |
11 | #[derive(Clone)]
12 | pub enum SVGCommitInfoValue {
13 | SomeString(String),
14 | SomeStringVec(Vec),
15 | SomeStringTupleVec(Vec<(String, String)>),
16 | SomeInt(isize),
17 | }
18 |
19 | impl Serialize for SVGCommitInfoValue {
20 | fn serialize(&self, serializer: S) -> Result where S: Serializer {
21 | match &self {
22 | SVGCommitInfoValue::SomeString(st) => st.serialize(serializer),
23 | SVGCommitInfoValue::SomeStringVec(v) => v.serialize(serializer),
24 | SVGCommitInfoValue::SomeStringTupleVec(v) => v.serialize(serializer),
25 | SVGCommitInfoValue::SomeInt(i) => i.serialize(serializer),
26 | }
27 | }
28 | }
29 |
30 | #[derive(Clone, Serialize)]
31 | pub struct CommitsInfo {
32 | branch_draw_properties: Vec<(String, Vec>>)>,
33 | svg_row_draw_properties: Vec>,
34 | }
35 |
36 | impl CommitsInfo {
37 | pub fn new(branch_draw_properties: Vec<(String, Vec>>)>, svg_row_draw_properties: Vec>) -> Self {
38 | Self {
39 | branch_draw_properties,
40 | svg_row_draw_properties,
41 | }
42 | }
43 | }
44 |
45 | #[derive(Clone, Serialize)]
46 | pub struct ParseableCommitInfo {
47 | sha: String,
48 | author_name: String,
49 | author_time: String,
50 | x: isize,
51 | y: isize,
52 | summary: String,
53 | parent_shas: Vec,
54 | child_shas: Vec,
55 | }
56 |
57 | impl ParseableCommitInfo {
58 | pub fn new(sha: String, author_name: String, author_time: String, x: isize, y: isize, summary: String, parent_shas: Vec, child_shas: Vec) -> Self {
59 | Self {
60 | sha,
61 | author_name,
62 | author_time,
63 | x,
64 | y,
65 | summary,
66 | parent_shas,
67 | child_shas,
68 | }
69 | }
70 |
71 | pub fn borrow_sha(&self) -> &String {
72 | &self.sha
73 | }
74 |
75 | pub fn borrow_author_name(&self) -> &String {
76 | &self.author_name
77 | }
78 |
79 | pub fn borrow_author_time(&self) -> &String {
80 | &self.author_time
81 | }
82 |
83 | pub fn borrow_x(&self) -> &isize {
84 | &self.x
85 | }
86 |
87 | pub fn borrow_y(&self) -> &isize {
88 | &self.y
89 | }
90 |
91 | pub fn borrow_summary(&self) -> &String {
92 | &self.summary
93 | }
94 |
95 | pub fn borrow_parent_shas(&self) -> &Vec {
96 | &self.parent_shas
97 | }
98 |
99 | pub fn borrow_child_shas(&self) -> &Vec {
100 | &self.child_shas
101 | }
102 | }
103 |
104 | #[derive(Clone)]
105 | pub enum RepoInfoValue {
106 | SomeCommitInfo(CommitsInfo),
107 | SomeBranchInfo(BranchesInfo),
108 | SomeRemoteInfo(Vec),
109 | SomeGeneralInfo(HashMap),
110 | SomeFilesChangedInfo(FilesChangedInfo),
111 | }
112 |
113 | impl Serialize for RepoInfoValue {
114 | fn serialize(&self, serializer: S) -> Result where S: Serializer {
115 | match &self {
116 | RepoInfoValue::SomeCommitInfo(c) => c.serialize(serializer),
117 | RepoInfoValue::SomeBranchInfo(b) => b.serialize(serializer),
118 | RepoInfoValue::SomeRemoteInfo(v) => v.serialize(serializer),
119 | RepoInfoValue::SomeGeneralInfo(hm) => hm.serialize(serializer),
120 | RepoInfoValue::SomeFilesChangedInfo(f) => f.serialize(serializer),
121 | }
122 | }
123 | }
124 |
125 | #[derive(Clone, Serialize, Deserialize)]
126 | pub struct ParseableDiffDelta {
127 | status: u8,
128 | path: String,
129 | }
130 |
131 | impl ParseableDiffDelta {
132 | pub fn new(status: u8, path: String) -> Self {
133 | Self {
134 | status,
135 | path,
136 | }
137 | }
138 |
139 | pub fn get_status(&self) -> u8 {
140 | self.status
141 | }
142 |
143 | pub fn get_path(&self) -> &String {
144 | &self.path
145 | }
146 | }
147 |
148 | #[derive(Clone, Serialize)]
149 | pub struct FilesChangedInfo {
150 | files_changed: usize,
151 | unstaged_files: Vec,
152 | staged_files: Vec,
153 | }
154 |
155 | impl FilesChangedInfo {
156 | pub fn new(files_changed: usize, unstaged_files: Vec, staged_files: Vec) -> Self {
157 | Self {
158 | files_changed,
159 | unstaged_files,
160 | staged_files,
161 | }
162 | }
163 | }
164 |
165 | #[derive(Clone, Serialize)]
166 | pub struct BranchInfo {
167 | target_sha: String,
168 | branch_shorthand: String,
169 | full_branch_name: String,
170 | is_head: bool,
171 | branch_type: String,
172 | ahead: usize,
173 | behind: usize,
174 | has_upstream: bool,
175 | }
176 |
177 | impl BranchInfo {
178 | pub fn new(target_sha: String, branch_shorthand: String, full_branch_name: String, is_head: bool, branch_type: String, ahead: usize, behind: usize, has_upstream: bool) -> Self {
179 | Self {
180 | target_sha,
181 | branch_shorthand,
182 | full_branch_name,
183 | is_head,
184 | branch_type,
185 | ahead,
186 | behind,
187 | has_upstream,
188 | }
189 | }
190 | }
191 |
192 | #[derive(Clone, Serialize)]
193 | pub struct StashInfo {
194 | index: usize,
195 | message: String,
196 | }
197 |
198 | impl StashInfo {
199 | pub fn new(index: usize, message: String) -> Self {
200 | Self {
201 | index,
202 | message,
203 | }
204 | }
205 | }
206 |
207 | #[derive(Clone, Serialize)]
208 | pub struct BranchInfoTreeNode {
209 | text: String,
210 | branch_info: Option,
211 | children: Vec,
212 | }
213 |
214 | impl BranchInfoTreeNode {
215 | fn new(text: String, branch_info: Option) -> Self {
216 | Self {
217 | text,
218 | branch_info,
219 | children: vec![],
220 | }
221 | }
222 |
223 | pub fn insert_split_shorthand(&mut self, split_shorthand: VecDeque, branch_info: Option) {
224 | // self should be the root node in this case.
225 | assert_eq!(self.text, String::from(""));
226 | let mut current_tree_node = self;
227 |
228 | for (i, string_ref) in split_shorthand.iter().enumerate() {
229 | let s = string_ref.clone();
230 | let child_index = current_tree_node.children.iter().position(|child| {
231 | child.text == s
232 | });
233 | match child_index {
234 | Some(j) => {
235 | current_tree_node = &mut current_tree_node.children[j];
236 | },
237 | None => {
238 | if i == split_shorthand.len() - 1 {
239 | current_tree_node.children.push(BranchInfoTreeNode::new(s, branch_info.clone()));
240 | } else {
241 | current_tree_node.children.push(BranchInfoTreeNode::new(s, None));
242 | }
243 | let last_index = current_tree_node.children.len() - 1;
244 | current_tree_node = &mut current_tree_node.children[last_index];
245 | },
246 | };
247 | }
248 | }
249 | }
250 |
251 | #[derive(Clone, Serialize)]
252 | pub struct BranchesInfo {
253 | local_branch_info_tree: BranchInfoTreeNode,
254 | remote_branch_info_tree: BranchInfoTreeNode,
255 | tag_branch_info_tree: BranchInfoTreeNode,
256 | stash_info_list: Vec,
257 | }
258 |
259 | impl BranchesInfo {
260 | pub fn new(local_branch_info_tree: BranchInfoTreeNode, remote_branch_info_tree: BranchInfoTreeNode, tag_branch_info_tree: BranchInfoTreeNode, stash_info_list: Vec) -> Self {
261 | Self {
262 | local_branch_info_tree,
263 | remote_branch_info_tree,
264 | tag_branch_info_tree,
265 | stash_info_list,
266 | }
267 | }
268 | }
269 |
270 | fn get_oid_refs(git_manager: &GitManager) -> Result>> {
271 | let repo = git_manager.borrow_repo()?;
272 |
273 | // Get HashMap of Oids and their refs based on type (local, remote, or tag)
274 | let mut oid_refs: HashMap> = HashMap::new();
275 |
276 | // Iterate over branches
277 | for branch_result in repo.branches(None)? {
278 | let (branch, _) = branch_result?;
279 | let mut branch_string = String::new();
280 | if branch.is_head() {
281 | branch_string += "* ";
282 | }
283 |
284 | let reference = branch.get();
285 | branch_string += GitManager::get_utf8_string(reference.shorthand(), "Ref Name")?;
286 | match reference.target() {
287 | Some(oid) => {
288 | let branch_type;
289 | if reference.is_remote() {
290 | branch_type = "remote".to_string();
291 | } else {
292 | branch_type = "local".to_string();
293 | }
294 | match oid_refs.get_mut(&*oid.to_string()) {
295 | Some(oid_ref_vec) => {
296 | oid_ref_vec.push((branch_string, branch_type));
297 | },
298 | None => {
299 | oid_refs.insert(oid.to_string(), vec![(branch_string, branch_type)]);
300 | },
301 | }
302 | },
303 | None => (),
304 | };
305 | }
306 |
307 | // If HEAD is detached, add it too
308 | if repo.head_detached()? {
309 | match repo.head()?.target() {
310 | Some(oid) => {
311 | match oid_refs.get_mut(&*oid.to_string()) {
312 | Some(oid_ref_vec) => {
313 | oid_ref_vec.push((String::from("* HEAD"), String::from("local")));
314 | },
315 | None => {
316 | oid_refs.insert(oid.to_string(), vec![(String::from("* HEAD"), String::from("local"))]);
317 | },
318 | }
319 | },
320 | None => (),
321 | };
322 | }
323 |
324 | // Iterate over tags
325 | for reference_result in repo.references()? {
326 | let reference = reference_result?;
327 | if reference.is_tag() {
328 | let ref_name = GitManager::get_utf8_string(reference.shorthand(), "Tag Name")?;
329 |
330 | let oid = reference.peel_to_commit()?.id();
331 | match oid_refs.get_mut(&*oid.to_string()) {
332 | Some(oid_ref_vec) => {
333 | oid_ref_vec.push((ref_name.to_string(), "tag".to_string()));
334 | }
335 | None => {
336 | oid_refs.insert(oid.to_string(), vec![(ref_name.to_string(), "tag".to_string())]);
337 | },
338 | };
339 | }
340 | }
341 | Ok(oid_refs)
342 | }
343 |
344 | fn get_general_info(git_manager: &GitManager) -> Result> {
345 | let repo = git_manager.borrow_repo()?;
346 |
347 | let mut general_info: HashMap = HashMap::new();
348 |
349 | let project_name = match repo.workdir() {
350 | Some(p) => {
351 | match p.file_name() {
352 | Some(d) => d,
353 | None => bail!("Working directory path is empty?"),
354 | }
355 | },
356 | None => bail!("Repo doesn't have a working directory?"),
357 | };
358 | general_info.insert(String::from("project_name"), String::from(GitManager::get_utf8_string(project_name.to_str(), "Project Containing Directory")?));
359 |
360 | general_info.insert(String::from("head_sha"), String::new());
361 | match repo.head() {
362 | Ok(head_ref) => {
363 | if let Some(oid) = head_ref.target() {
364 | general_info.insert(String::from("head_sha"), oid.to_string());
365 | }
366 |
367 | match repo.find_branch(GitManager::get_utf8_string(head_ref.shorthand(), "Branch Name")?, BranchType::Local) {
368 | Ok(head_branch) => {
369 | match head_branch.upstream() {
370 | Ok(_) => {
371 | general_info.insert(String::from("head_has_upstream"), true.to_string());
372 | },
373 | Err(e) => {
374 | if e.code() == ErrorCode::NotFound {
375 | general_info.insert(String::from("head_has_upstream"), false.to_string());
376 | } else {
377 | return Err(e.into());
378 | }
379 | },
380 | }
381 | },
382 | Err(e) => {
383 | if e.code() == ErrorCode::NotFound {
384 | general_info.insert(String::from("head_has_upstream"), false.to_string());
385 | } else {
386 | return Err(e.into());
387 | }
388 | },
389 | };
390 | },
391 | Err(e) => {
392 | if e.code() == ErrorCode::UnbornBranch {
393 | general_info.insert(String::from("head_has_upstream"), false.to_string());
394 | } else {
395 | return Err(e.into());
396 | }
397 | },
398 | };
399 |
400 | // Check if an operation is in progress (this means that conflicts occurred during the operation).
401 | let repo_state = repo.state();
402 | general_info.insert(String::from("is_cherrypicking"), (repo_state == RepositoryState::CherryPick).to_string());
403 | general_info.insert(String::from("is_reverting"), (repo_state == RepositoryState::Revert).to_string());
404 | general_info.insert(String::from("is_merging"), (repo_state == RepositoryState::Merge).to_string());
405 | general_info.insert(String::from("is_rebasing"), (repo_state == RepositoryState::Rebase || repo_state == RepositoryState::RebaseMerge || repo_state == RepositoryState::RebaseInteractive).to_string());
406 |
407 | Ok(general_info)
408 | }
409 |
410 | fn get_commit_info_list(git_manager: &GitManager, oid_list: Vec) -> Result> {
411 | let mut commit_list: Vec = vec![];
412 |
413 | let repo = git_manager.borrow_repo()?;
414 | let mut children_oids_hm: HashMap> = HashMap::new();
415 | for (i, oid) in oid_list.iter().enumerate() {
416 | let commit = repo.find_commit(*oid)?;
417 |
418 | // Get commit summary
419 | let commit_summary = GitManager::get_utf8_string(commit.summary(), "Commit Summary")?;
420 |
421 | // Get parent Oids
422 | let mut parent_shas: Vec = vec![];
423 | for parent in commit.parents() {
424 | parent_shas.push(parent.id().to_string());
425 | match children_oids_hm.get_mut(&*parent.id().to_string()) {
426 | Some(children_oid_vec) => children_oid_vec.push(oid.to_string()),
427 | None => {
428 | children_oids_hm.insert(parent.id().to_string(), vec![oid.to_string()]);
429 | },
430 | };
431 | }
432 |
433 | let author_signature = commit.author();
434 | let author_name = String::from(GitManager::get_utf8_string(author_signature.name(), "Author Name")?);
435 |
436 | let author_time = author_signature.when().seconds();
437 | let author_utc_datetime = OffsetDateTime::from_unix_timestamp(author_time)?;
438 | let author_local_datetime = author_utc_datetime.to_offset(git_manager.borrow_current_local_offset().clone());
439 |
440 | let now_utc = OffsetDateTime::now_utc();
441 | let now_local = now_utc.to_offset(git_manager.borrow_current_local_offset().clone());
442 | let diff = now_local.date() - author_local_datetime.date();
443 |
444 | let time_format = format_description::parse("[hour repr:12]:[minute]:[second] [period case:upper]")?;
445 | let datetime_format = format_description::parse("[year]-[month]-[day] [hour repr:12]:[minute]:[second] [period case:upper]")?;
446 | let formatted_datetime;
447 | if diff.whole_days() == 0 {
448 | formatted_datetime = format!("Today {}", author_local_datetime.time().format(&time_format)?);
449 | } else if diff.whole_days() == 1 {
450 | formatted_datetime = format!("Yesterday {}", author_local_datetime.time().format(&time_format)?);
451 | } else {
452 | formatted_datetime = format!("{}", author_local_datetime.format(&datetime_format)?);
453 | }
454 |
455 | commit_list.push(ParseableCommitInfo::new(
456 | oid.to_string(),
457 | author_name,
458 | formatted_datetime,
459 | 0,
460 | i as isize,
461 | String::from(commit_summary),
462 | parent_shas,
463 | vec![])
464 | );
465 | }
466 |
467 | // Gather the child commits after running through the commit graph once in order
468 | // to actually have populated entries.
469 | for commit_info in commit_list.iter_mut() {
470 | match children_oids_hm.get(&*commit_info.sha) {
471 | Some(v) => {
472 | commit_info.child_shas = v.clone();
473 | },
474 | None => (),
475 | };
476 | }
477 |
478 | Ok(commit_list)
479 | }
480 |
481 | fn get_commit_svg_draw_properties_list(git_manager: &mut GitManager, force_refresh: bool) -> Result {
482 | let mut commit_info_list = vec![];
483 | if let Some(oid_vec) = git_manager.git_revwalk(force_refresh)? {
484 | commit_info_list = get_commit_info_list(git_manager, oid_vec)?;
485 | }
486 |
487 | let mut svg_row_draw_properties: Vec> = vec![];
488 | if commit_info_list.len() > 0 {
489 | let mut svg_rows: Vec>> = vec![];
490 | let mut svg_row_hm: HashMap>> = HashMap::new();
491 | for commit_info in commit_info_list {
492 | let svg_row_rc: Rc> = Rc::new(RefCell::new(SVGRow::from_commit_info(&commit_info)));
493 | svg_row_hm.insert(commit_info.sha.clone(), svg_row_rc.clone());
494 | svg_rows.push(svg_row_rc);
495 | }
496 |
497 | for svg_row_rc in &svg_rows {
498 | svg_row_rc.borrow_mut().set_parent_and_child_svg_row_values(&svg_row_hm);
499 | }
500 |
501 | let main_table = SVGRow::get_occupied_table(&svg_rows)?;
502 | for svg_row_rc in svg_rows {
503 | svg_row_draw_properties.push(svg_row_rc.borrow_mut().get_draw_properties(
504 | &main_table,
505 | ));
506 | }
507 | }
508 |
509 | let oid_refs_hm = get_oid_refs(git_manager)?;
510 | let mut branch_draw_properties: Vec<(String, Vec>>)> = vec![];
511 | for (k, v) in oid_refs_hm {
512 | branch_draw_properties.push((k, SVGRow::get_branch_draw_properties(v)));
513 | }
514 |
515 | Ok(CommitsInfo::new(branch_draw_properties, svg_row_draw_properties))
516 | }
517 |
518 | fn get_branch_info_list(git_manager: &mut GitManager) -> Result {
519 | let repo = git_manager.borrow_repo_mut()?;
520 |
521 | // Get all remote heads to be excluded from branches info
522 | let remotes = repo.remotes()?;
523 | let mut remote_heads: Vec = vec![];
524 | for remote in remotes.iter() {
525 | let remote_head_name = String::from(GitManager::get_utf8_string(remote, "Remote Name")?) + "/HEAD";
526 | remote_heads.push(remote_head_name);
527 | }
528 |
529 | let mut local_branch_info_tree = BranchInfoTreeNode::new(String::from(""), None);
530 | let mut remote_branch_info_tree = BranchInfoTreeNode::new(String::from(""), None);
531 | let mut tag_branch_info_tree = BranchInfoTreeNode::new(String::from(""), None);
532 | for reference_result in repo.references()? {
533 | let reference = reference_result?;
534 |
535 | let target_sha = match reference.peel_to_commit() {
536 | Ok(c) => c.id().to_string(),
537 | Err(_) => String::new(),
538 | };
539 |
540 | // Get branch name
541 | let branch_shorthand = String::from(GitManager::get_utf8_string(reference.shorthand(), "Branch Name")?);
542 |
543 | // If this is the remote head, don't add it to the branches info
544 | if remote_heads.contains(&branch_shorthand) {
545 | continue;
546 | }
547 |
548 | // Get full branch name
549 | let full_branch_name = String::from(GitManager::get_utf8_string(reference.name(), "Branch Name")?);
550 |
551 | // Get if branch is head
552 | let mut is_head = false;
553 | if reference.is_branch() {
554 | let local_branch = repo.find_branch(branch_shorthand.as_str(), BranchType::Local)?;
555 | if local_branch.is_head() {
556 | is_head = true;
557 | }
558 | }
559 |
560 | // Get branch type
561 | let mut branch_type = String::from("");
562 | if reference.is_branch() {
563 | branch_type = String::from("local");
564 | } else if reference.is_remote() {
565 | branch_type = String::from("remote");
566 | } else if reference.is_tag() {
567 | branch_type = String::from("tag");
568 | }
569 |
570 | // Get ahead/behind counts
571 | let mut ahead = 0;
572 | let mut behind = 0;
573 | let mut has_upstream = false;
574 | if reference.is_branch() {
575 | let local_branch = repo.find_branch(branch_shorthand.as_str(), BranchType::Local)?;
576 | match local_branch.upstream() {
577 | Ok(remote_branch) => {
578 | has_upstream = true;
579 | match local_branch.get().target() {
580 | Some(local_oid) => {
581 | match remote_branch.get().target() {
582 | Some(remote_oid) => {
583 | let (a, b) = repo.graph_ahead_behind(local_oid, remote_oid)?;
584 | ahead = a;
585 | behind = b;
586 | },
587 | None => (),
588 | };
589 | },
590 | None => (),
591 | };
592 | },
593 | Err(e) => {
594 | if e.code() != ErrorCode::NotFound {
595 | return Err(e.into());
596 | }
597 | },
598 | };
599 | }
600 |
601 | let mut split_shorthand = VecDeque::new();
602 | for s in branch_shorthand.split("/") {
603 | split_shorthand.push_back(String::from(s));
604 | }
605 | let branch_info = BranchInfo::new(target_sha, branch_shorthand, full_branch_name, is_head, branch_type.clone(), ahead, behind, has_upstream);
606 | if branch_type == String::from("local") {
607 | local_branch_info_tree.insert_split_shorthand(split_shorthand, Some(branch_info));
608 | } else if branch_type == String::from("remote") {
609 | remote_branch_info_tree.insert_split_shorthand(split_shorthand, Some(branch_info));
610 | } else if branch_type == String::from("tag") {
611 | tag_branch_info_tree.insert_split_shorthand(split_shorthand, Some(branch_info));
612 | }
613 | }
614 |
615 | // Add remote names in case a remote is present but has no branches.
616 | for remote in remotes.iter() {
617 | let remote_name = String::from(GitManager::get_utf8_string(remote, "Remote Name")?);
618 | let mut split_shorthand = VecDeque::new();
619 | split_shorthand.push_back(remote_name);
620 | remote_branch_info_tree.insert_split_shorthand(split_shorthand, None);
621 | }
622 |
623 | let mut stash_info_list = vec![];
624 | repo.stash_foreach(|stash_index, stash_message, _stash_oid| {
625 | let stash_info = StashInfo::new(stash_index, format!("{}: {}", stash_index, stash_message));
626 | stash_info_list.push(stash_info);
627 | true
628 | })?;
629 |
630 | Ok(BranchesInfo::new(local_branch_info_tree, remote_branch_info_tree, tag_branch_info_tree, stash_info_list))
631 | }
632 |
633 | fn get_remote_info_list(git_manager: &GitManager) -> Result> {
634 | let repo = git_manager.borrow_repo()?;
635 |
636 | let mut remote_info_list = vec![];
637 | let remote_string_array = repo.remotes()?;
638 |
639 | for remote_name_opt in remote_string_array.iter() {
640 | let remote_name = GitManager::get_utf8_string(remote_name_opt, "Remote Name")?;
641 | remote_info_list.push(String::from(remote_name));
642 | }
643 | Ok(remote_info_list)
644 | }
645 |
646 | pub fn get_parseable_diff_delta(diff: Diff) -> Result> {
647 | let mut files: Vec = vec![];
648 | for delta in diff.deltas() {
649 | let status = delta.status() as u8;
650 | let path = match delta.new_file().path() {
651 | Some(p) => {
652 | match p.to_str() {
653 | Some(s) => s,
654 | None => bail!("File Path uses invalid unicode. Not sure how your file system isn't corrupted..."),
655 | }
656 | },
657 | None => bail!("Possible invalid file path? I'm not actually sure why this error would occur. It looks like git didn't store a file path with a file or something."),
658 | };
659 | files.push(ParseableDiffDelta::new(status, String::from(path)));
660 | }
661 | Ok(files)
662 | }
663 |
664 | pub fn get_files_changed_info_list(git_manager: &GitManager) -> Result