├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── build.md │ ├── chore.md │ ├── ci.md │ ├── config.yml │ ├── documentation.md │ ├── feature_request.md │ ├── performance.md │ ├── refactor.md │ ├── revert.md │ ├── style.md │ └── test.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── fmt.yml │ ├── main.yml │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── Dockerfile ├── README.md ├── assest ├── compare.gif ├── contribution.png ├── loc.gif ├── main.gif ├── top_lang.png └── top_repos.png ├── index.html ├── license ├── package-lock.json ├── src ├── compare.rs ├── get_detailed_view.rs ├── github_graphql │ ├── detailed_view.rs │ └── query.graphql ├── github_logo_ascii.rs ├── graph │ ├── graph_maker.rs │ ├── query.graphql │ └── schema.graphql ├── input │ ├── menu_cli.rs │ └── mod.rs ├── lines_of_codes.rs └── main.rs └── target └── .rustc_info.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve 4 | title: "fix: " 5 | labels: bug 6 | --- 7 | 8 | **Description** 9 | 10 | A clear and concise description of what the bug is. 11 | 12 | **Steps To Reproduce** 13 | 14 | 1. Go to '...' 15 | 2. Click on '....' 16 | 3. Scroll down to '....' 17 | 4. See error 18 | 19 | **Expected Behavior** 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Additional Context** 28 | 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/build.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build System 3 | about: Changes that affect the build system or external dependencies 4 | title: "build: " 5 | labels: build 6 | --- 7 | 8 | **Description** 9 | 10 | Describe what changes need to be done to the build system and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] The build system is passing 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Chore 3 | about: Other changes that don't modify src or test files 4 | title: "chore: " 5 | labels: chore 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what change is needed and why. If this changes code then please use another issue type. 11 | 12 | **Requirements** 13 | 14 | - [ ] No functional changes to the code 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continuous Integration 3 | about: Changes to the CI configuration files and scripts 4 | title: "ci: " 5 | labels: ci 6 | --- 7 | 8 | **Description** 9 | 10 | Describe what changes need to be done to the ci/cd system and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] The ci system is passing 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Documentation 3 | about: Improve the documentation so all collaborators have a common understanding 4 | title: "docs: " 5 | labels: documentation 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what documentation you are looking to add or improve. 11 | 12 | **Requirements** 13 | 14 | - [ ] Requirements go here 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: A new feature to be added to the project 4 | title: "feat: " 5 | labels: feature 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to add. The more context the better. 11 | 12 | **Requirements** 13 | 14 | - [ ] Checklist of requirements to be fulfilled 15 | 16 | **Additional Context** 17 | 18 | Add any other context or screenshots about the feature request go here. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/performance.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Performance Update 3 | about: A code change that improves performance 4 | title: "perf: " 5 | labels: performance 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what code needs to be changed and what the performance impact is going to be. Bonus point's if you can tie this directly to user experience. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/refactor.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Refactor 3 | about: A code change that neither fixes a bug nor adds a feature 4 | title: "refactor: " 5 | labels: refactor 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what needs to be refactored and why. Please provide links to related issues (bugs or upcoming features) in order to help prioritize. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/revert.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Revert Commit 3 | about: Reverts a previous commit 4 | title: "revert: " 5 | labels: revert 6 | --- 7 | 8 | **Description** 9 | 10 | Provide a link to a PR/Commit that you are looking to revert and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] Change has been reverted 15 | - [ ] No change in test coverage has happened 16 | - [ ] A new ticket is created for any follow on work that needs to happen 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/style.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Style Changes 3 | about: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc) 4 | title: "style: " 5 | labels: style 6 | --- 7 | 8 | **Description** 9 | 10 | Clearly describe what you are looking to change and why. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/test.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Test 3 | about: Adding missing tests or correcting existing tests 4 | title: "test: " 5 | labels: test 6 | --- 7 | 8 | **Description** 9 | 10 | List out the tests that need to be added or changed. Please also include any information as to why this was not covered in the past. 11 | 12 | **Requirements** 13 | 14 | - [ ] There is no drop in test coverage. 15 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ## Status 10 | 11 | **READY/IN DEVELOPMENT/HOLD** 12 | 13 | ## Description 14 | 15 | 16 | 17 | ## Type of Change 18 | 19 | 20 | 21 | - [ ] ✨ New feature (non-breaking change which adds functionality) 22 | - [ ] 🛠️ Bug fix (non-breaking change which fixes an issue) 23 | - [ ] ❌ Breaking change (fix or feature that would cause existing functionality to change) 24 | - [ ] 🧹 Code refactor 25 | - [ ] ✅ Build configuration change 26 | - [ ] 📝 Documentation 27 | - [ ] 🗑️ Chore 28 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/fmt.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | name: Rustfmt 4 | 5 | jobs: 6 | format: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v3 10 | - uses: dtolnay/rust-toolchain@stable 11 | - uses: mbrobbel/rustfmt-check@master 12 | with: 13 | token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: OS Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | name: Build and Test 10 | runs-on: ${{ matrix.os }} 11 | strategy: 12 | matrix: 13 | os: [macos-latest, ubuntu-latest, windows-latest] 14 | rust-version: [stable] 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v3 18 | - name: Install Rust 19 | uses: actions-rs/toolchain@v1 20 | with: 21 | profile: minimal 22 | toolchain: ${{ matrix.rust-version }} 23 | - name: Build Project 24 | run: cargo build --release 25 | - name: List files in directory 26 | run: ls -R 27 | working-directory: target/release/ 28 | - name: Archive Artifact 29 | uses: actions/upload-artifact@v2 30 | with: 31 | name: gitgrab-${{ matrix.os }} 32 | path: target/release/gitgrab 33 | if: success() 34 | working-directory: release/${{ matrix.os }} 35 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v3 13 | 14 | - name: Build for Windows 15 | run: cargo build --release --target x86_64-pc-windows-gnu 16 | env: 17 | RUSTFLAGS: "-C target-feature=-crt-static" 18 | 19 | - name: Build for Linux 20 | run: cargo build --release --target x86_64-unknown-linux-gnu 21 | env: 22 | RUSTFLAGS: "-C target-feature=-crt-static" 23 | 24 | - name: Build for macOS 25 | run: cargo build --release --target x86_64-apple-darwin 26 | 27 | - name: Create release 28 | id: create_release 29 | uses: actions/create-release@v1 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | with: 33 | tag_name: ${{ github.ref }} 34 | release_name: Release ${{ github.ref }} 35 | body: | 36 | Release ${{ github.ref }} 37 | 38 | - name: Upload Windows binary 39 | id: upload_win 40 | uses: actions/upload-release-asset@v1 41 | env: 42 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 43 | with: 44 | upload_url: ${{ steps.create_release.outputs.upload_url }} 45 | asset_path: ./target/x86_64-pc-windows-gnu/release/.exe 46 | asset_name: gitgrab-windows-x86_64.exe 47 | asset_content_type: application/octet-stream 48 | 49 | - name: Upload Linux binary 50 | id: upload_linux 51 | uses: actions/upload-release-asset@v1 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | with: 55 | upload_url: ${{ steps.create_release.outputs.upload_url }} 56 | asset_path: ./target/x86_64-unknown-linux-gnu/release/gitgrab 57 | asset_name: gitgrab-linux-x86_64 58 | asset_content_type: application/octet-stream 59 | 60 | - name: Upload macOS binary 61 | id: upload_macos 62 | uses: actions/upload-release-asset@v1 63 | env: 64 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 65 | with: 66 | upload_url: ${{ steps.create_release.outputs.upload_url }} 67 | asset_path: ./target/x86_64-apple-darwin/release/gitgrab 68 | asset_name: gitgrab-macos-x86_64 69 | asset_content_type: application/octet-stream 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # File created using '.gitignore Generator' for Visual Studio Code: https://bit.ly/vscode-gig 2 | # Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,rust 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode,linux,rust 4 | 5 | ### Linux ### 6 | *~ 7 | 8 | # temporary files which can be created if a process still has a handle open of a deleted file 9 | .fuse_hidden* 10 | 11 | # KDE directory preferences 12 | .directory 13 | 14 | # Linux trash folder which might appear on any partition or disk 15 | .Trash-* 16 | 17 | # .nfs files are created when an open file is removed but is still being accessed 18 | .nfs* 19 | .vscode 20 | 21 | ### Rust ### 22 | # Generated by Cargo 23 | # will have compiled files and executables 24 | debug/ 25 | target/ 26 | 27 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 28 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 29 | Cargo.lock 30 | 31 | # These are backup files generated by rustfmt 32 | **/*.rs.bk 33 | 34 | # MSVC Windows builds of rustc generate these, which store debugging information 35 | *.pdb 36 | 37 | ### VisualStudioCode ### 38 | .vscode/* 39 | !.vscode/settings.json 40 | !.vscode/tasks.json 41 | !.vscode/launch.json 42 | !.vscode/extensions.json 43 | !.vscode/*.code-snippets 44 | 45 | # Local History for Visual Studio Code 46 | .history/ 47 | 48 | # Built Visual Studio Code Extensions 49 | *.vsix 50 | 51 | ### VisualStudioCode Patch ### 52 | # Ignore all local history of files 53 | .history 54 | .ionide 55 | 56 | # End of https://www.toptal.com/developers/gitignore/api/visualstudiocode,linux,rust 57 | 58 | # Custom rules (everything added below won't be overriden by 'Generate .gitignore File' if you use 'Update' option) 59 | 60 | TODO -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gitgrab" 3 | version = "0.2.3" 4 | edition = "2021" 5 | authors = ["ArshErgon", "arshergon@gmail.com", "https://www.linkedin.com/in/arsh-ergon/"] 6 | license = "MIT OR Apache-2.0" 7 | description = "A tool which shows github information on terminal" 8 | readme = "README.md" 9 | homepage = "https://github.com/ArshErgon/" 10 | repository = "https://github.com/ArshErgon/" 11 | keywords = ["cli", "search", "github", "open-source", "contributors"] 12 | categories = ["command-line-utilities"] 13 | 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | ansi_term = "0.12.1" 19 | anyhow = "1.0.69" 20 | cfonts = "1.1.0" 21 | clap = "4.1.8" 22 | colorful = "0.2.2" 23 | crossterm = "0.26.1" 24 | dialoguer = "0.10.3" 25 | dirs = "5.0.0" 26 | graphql_client = { version = "0.13.0", features = ["reqwest-blocking"] } 27 | reqwest = "0.11" 28 | serde = { version = "1.0", features = ["derive"] } 29 | serde_json = "1.0.95" 30 | term-table = "1.3.2" 31 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArshErgon/gitgrab/6a404816262193373833e480f60cd935dc9563a5/Dockerfile -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

made with rust 2 |

3 |
4 | 5 |
6 |
7 | 8 | # **gitGrab** 9 | 10 | An Open-Source tool for Open-Source Enthusiast that shows your GitHub Contribution on the terminal 11 | 12 |
13 | 14 | # Why its created? 15 | 16 | I was quite active on open source contributions and needed to track my progress across multiple repositories. I was constantly going back and forth between different tools to check my issues, followers, and pull requests. In the meantime, I discovered neofetch, an awesome command-line interface program that displays basic information about the operating system. I thought, why not create something similar to it? The starting phase of GitGrab was quite similar to neofetch, but later I added language bars and a contribution graph etc to it. For those unfamiliar with these terms, language bars display the percentage of code contributed in each programming language, and a contribution graph shows the frequency of contributions over time. 17 | 18 | # Features 19 | 20 | - Can see a user information without deleting the permantent user. 21 | - All viewers information 22 | - Languages Bars 23 | - Contribution Graph 24 | - Lines Of Code 25 | - Compare with 2 users 26 | 27 | ## LOC (lines of code) 28 |
29 | 30 |
31 | 32 | ## Compare two users 33 |
34 | 35 |
36 | 37 | ## Top Languages 38 |
39 | 40 |
41 | 42 | ## Contribution Graph 43 |
44 | 45 |
46 | 47 | 48 | # Commands 49 | 50 | ### Basic 51 | 52 | ```rust 53 | $ gitgrab -o 54 | 1. Create a User 55 | 2. Enter/Update the Github API key 56 | 3. Exit 57 | ``` 58 | 59 | ### For a temporary User 60 | 61 | ```rust 62 | $ gitgrab -t 63 | ``` 64 | 65 | ### For LOC (lines of code) 66 | ```rust 67 | $ gitgrab --loc 68 | ``` 69 | 70 | ### For comparing users 71 | ```rust 72 | $ gitgrab --com " " 73 | ``` 74 | 75 | ### More commands 76 | 77 | ```rust 78 | $ gitgrab -a 79 | 80 | $ gitgrab -h 81 | ``` 82 | 83 | # Installation 84 | 85 | Remember you need to add github token also: [your safety](https://github.com/ArshErgon/gitgrab#api-key-security), What are [token?](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) and where it will be [created](https://github.com/settings/tokens). Give every permission except **creating and deleting** 86 | 87 | # Downloads 88 | 89 | ## Cargo 90 | 91 | If you have rust installed on your system run these commands to install 92 | 93 | ``` 94 | $ rustup default nightly 95 | 96 | ``` 97 | Then 98 | 99 | ``` 100 | $ cargo install gitgrab --git https://github.com/ArshErgon/gitgrab 101 | ``` 102 | 103 | if you need to update you now just need to run 104 | 105 | ``` 106 | $ cargo install gitgrab --git https://github.com/ArshErgon/gitgrab --force 107 | ``` 108 | 109 | And if you want to remove it you do 110 | 111 | ``` 112 | $ cargo uninstall gitgrab 113 | ``` 114 | 115 | 116 | ## Linux 117 | 118 | Download the binary from [here](https://github.com/ArshErgon/gitgrab/releases/download/v0.2.3/gitgrab), after downloading go to the place where its download (mostly on Desktop) and enter the command below. 119 | 120 | ```rust 121 | $ sudo install -c gitgrab /usr/local/bin 122 | ``` 123 | 124 | ## Windows 125 | 126 | Download the binary from [here](https://github.com/ArshErgon/gitfetch/releases/download/v0.2.3/gitgrab.exe), 127 | There are two ways to run in windows. 128 | 129 | 1. On the CMD, add to the path 130 | 131 | ```rust 132 | C:> PATH=%PATH%;C:\path\to\gitgrab.exe 133 | ``` 134 | 135 | 2. Directly running the binary 136 | 137 | ```rust 138 | C/Downloads>./gitgrab #or add gitgrab.exe if gets an error. 139 | ``` 140 | 141 | ## MacOS 142 | 143 | Some builds gets failed as a result the installer wouldn't be made but you can still install it if you have `cargo` installed 144 | 145 | ```rust 146 | $ cargo install path/to/project/gitfetch 147 | ``` 148 | 149 | # Development 150 | 151 | ```git 152 | $ git clone https://github.com/USERNAME/gitgrab.git 153 | $ cd gitgrab 154 | $ cargo run -- -t USERNAME 155 | OR 156 | $ cargo run -- -o 157 | ``` 158 | 159 | to run your own or your friends github stats 160 | 161 | ```bash 162 | cargo run -- -t USERNAME 163 | ``` 164 | 165 | # API Key Security 166 | 167 | > As the key is save on your computer and I have no power to get it from your computer, your key is safe, but still when you are giving it the permission( as the contribution graph keeps all the ticks selected) please dont select the delete and creating; or anything you find which can harm you in data breach 168 | 169 | https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token and give it all permission (expect: deleting or creating) 170 | -------------------------------------------------------------------------------- /assest/compare.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArshErgon/gitgrab/6a404816262193373833e480f60cd935dc9563a5/assest/compare.gif -------------------------------------------------------------------------------- /assest/contribution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArshErgon/gitgrab/6a404816262193373833e480f60cd935dc9563a5/assest/contribution.png -------------------------------------------------------------------------------- /assest/loc.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArshErgon/gitgrab/6a404816262193373833e480f60cd935dc9563a5/assest/loc.gif -------------------------------------------------------------------------------- /assest/main.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArshErgon/gitgrab/6a404816262193373833e480f60cd935dc9563a5/assest/main.gif -------------------------------------------------------------------------------- /assest/top_lang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArshErgon/gitgrab/6a404816262193373833e480f60cd935dc9563a5/assest/top_lang.png -------------------------------------------------------------------------------- /assest/top_repos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArshErgon/gitgrab/6a404816262193373833e480f60cd935dc9563a5/assest/top_repos.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | gitgrab 18 | 19 | 20 | 21 | 26 |
27 |
28 |
29 | 30 |
31 |

Introducing GitGrab

32 |

An open-source CLI application that displays your GitHub contributions in the terminal. Perfect 33 | for GitHub enthusiasts!

34 | 36 | 38 |
39 |
40 |
41 | 42 | 43 | 44 |
45 |
46 |
47 |
48 |
forks
49 |
4
50 |
51 |
52 |
stars
53 |
24
54 |
55 |
56 |
Commits
57 |
276
58 |
59 |
60 |
61 |
62 |
63 |

But why gitgrab?

64 |
65 | Gitgrab started as a side project with the goal of creating a tool similar to Neofetch. 66 | Its early release received a positive response from users who found it more convenient than switching back and 67 | forth to their GitHub account or writing queries. Encouraged by this feedback, I decided to continue working on 68 | Gitgrab and further improve its features. Today, it has evolved into a more refined and robust tool. 69 |
70 |
71 |
72 | 73 |
74 |
75 |

Features

76 | 77 | 78 |
79 |
80 | 81 |
82 |

LOC

83 |

LOC (lines of code) a command which can show the total number of lines of code of different 84 | languages in tabular form. To use this feature, simply enter the URL of your GitHub repository into the Gitgrab search bar. Gitgrab will then scan your repository and generate a report that includes a table of lines of code, broken down by language, comment, blank space, and written line.

85 |
86 |
gitgrab --loc URL
87 |
gitgrab --loc https://github.com/ArshErgon/gitgrab
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | 96 |
97 |

Compare

98 |

It can compare 2 users and tells the winner by calculating total contribution made by an individual.

99 |
100 |
gitgrab --com "UserOne UserTwo"
101 |
gitgrab --com "ArshErgon torvalds"
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 | 110 |
111 |

Top languages

112 |

Quickly identify the primary programming languages used in your GitHub repository. Gain valuable insights into the distribution of languages with ease.

113 |
114 |
gitgrab
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | 123 |
124 |

Contribution Graph

125 |

Dive into your GitHub contributions with our interactive graph. Visualize your coding activity over time, displaying a comprehensive overview of your commits, pull requests, and more. Gain insights into your coding patterns, track your progress, and celebrate your accomplishments.

126 |
127 |
gitgrab
128 |
129 |
130 |
131 |
132 |
133 |
134 |

Contribution

135 |

As GitGrab is a new tool, regular updates and new features will greatly benefit the community. We welcome even the smallest pull requests, as we highly value our contributors and their contributions to our tool.

136 |
137 | 138 |

139 |

The goal is not reached yet. Far from it. But gitgrab is slowly getting better, step by step.

140 |
141 |
142 | 143 | 144 | 145 | 146 |
147 |

Copyright © 2023 - All right reserved

148 |
149 | 150 | 151 | 152 | 153 | 154 | 155 | 202 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ico277 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. -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitgrab", 3 | "lockfileVersion": 2, 4 | "requires": true, 5 | "packages": {} 6 | } 7 | -------------------------------------------------------------------------------- /src/compare.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | get_detailed_view::ascii_text, github_graphql::detailed_view::RepositoriesInformation, 5 | }; 6 | use colorful::Colorful; 7 | use term_table::{row::Row, table_cell::TableCell, Table}; 8 | 9 | struct GithubData { 10 | profile_data: HashMap, 11 | language_data: HashMap, 12 | top_repo: HashMap, 13 | } 14 | 15 | fn comparison(user_one: String, user_two: String) { 16 | ascii_text("Comparison".to_owned()); 17 | let userone_data = helper(user_one); 18 | let usertwo_data = helper(user_two.clone()); 19 | let (userone_helper, userone_sum, username_one) = hardcode_function(userone_data); 20 | let (usertwo_helper, usertwo_sum, username_two) = hardcode_function(usertwo_data); 21 | creating_table( 22 | (userone_helper, userone_sum, username_one), 23 | (usertwo_helper, usertwo_sum, user_two), 24 | ); 25 | } 26 | 27 | fn helper(user: String) -> GithubData { 28 | let (secret_key, _) = crate::input::menu_cli::get_secret_key(); 29 | let github_data = 30 | crate::github_graphql::detailed_view::get_graphql_info(user, secret_key.trim()); 31 | let github_filter = GithubData { 32 | profile_data: github_data.0, 33 | language_data: github_data.1, 34 | top_repo: github_data.2, 35 | }; 36 | github_filter 37 | } 38 | 39 | fn hardcode_function(data: GithubData) -> (String, u32, String) { 40 | let personal_detail = data.profile_data; 41 | let language_detail = data.language_data; 42 | let name = &personal_detail["name"]; 43 | let repos = &personal_detail["repo"]; 44 | let issue = &personal_detail["issues"]; 45 | let followers = &personal_detail["followers"]; 46 | let following = &personal_detail["following"]; 47 | let watcher = &personal_detail["watcher"]; 48 | let star = &personal_detail["stars"]; 49 | let pull_requests = &personal_detail["request"]; 50 | let fork = &personal_detail["fork"]; 51 | let username = &personal_detail["login"]; 52 | let mut sum = (repos.parse::().unwrap() 53 | + issue.parse::().unwrap() 54 | + followers.parse::().unwrap() 55 | + followers.parse::().unwrap() 56 | + watcher.parse::().unwrap() 57 | + star.parse::().unwrap() 58 | + pull_requests.parse::().unwrap() 59 | + fork.parse::().unwrap()); 60 | for (lang, count) in language_detail.clone() { 61 | sum += count 62 | } 63 | let repos = add_k(repos.parse::().unwrap()); 64 | let issue = add_k(issue.parse::().unwrap()); 65 | let followers = add_k(followers.parse::().unwrap()); 66 | let following = add_k(following.parse::().unwrap()); 67 | let watcher = add_k(watcher.parse::().unwrap()); 68 | let star = add_k(star.parse::().unwrap()); 69 | let pull_request = add_k(pull_requests.parse::().unwrap()); 70 | let fork = add_k(fork.parse::().unwrap()); 71 | let show_format = format!( 72 | " 73 | Name : {name} 74 | Repo count : {repos} 75 | Issue : {issue} 76 | Followers : {followers} 77 | Following : {following} 78 | Watcher : {watcher} 79 | Star : {star} 80 | PR : {pull_requests} 81 | Fork : {fork} 82 | Languages: 83 | {:#?} 84 | ", 85 | language_detail 86 | ); 87 | (show_format.to_string(), sum, username.to_string()) 88 | } 89 | 90 | fn creating_table(pair_one: (String, u32, String), pair_two: (String, u32, String)) { 91 | let (userone, userone_sum, userone_name) = (pair_one.0, pair_one.1, pair_one.2); 92 | let (usertwo, usertwo_sum, usertwo_name) = (pair_two.0, pair_two.1, pair_two.2); 93 | let mut table = Table::new(); 94 | table.max_column_width = 100; 95 | table.style = term_table::TableStyle::extended(); 96 | table.add_row(Row::new(vec![ 97 | TableCell::new_with_alignment( 98 | userone_name.clone(), 99 | 1, 100 | term_table::table_cell::Alignment::Center, 101 | ), 102 | TableCell::new_with_alignment( 103 | usertwo_name.clone(), 104 | 1, 105 | term_table::table_cell::Alignment::Center, 106 | ), 107 | ])); 108 | table.add_row(Row::new(vec![ 109 | TableCell::new_with_alignment(userone, 1, term_table::table_cell::Alignment::Left), 110 | TableCell::new_with_alignment(usertwo, 1, term_table::table_cell::Alignment::Left), 111 | ])); 112 | 113 | println!("{}", table.render()); 114 | let (winner_name, loser_name) = if userone_sum > usertwo_sum { 115 | (userone_name, usertwo_name) 116 | } else { 117 | (usertwo_name, userone_name) 118 | }; 119 | let winner_message = format!(" 120 | Okay, so I've done calculation on the basis of contribution user `{winner}` is the winner. 121 | `{loser}` failed as the user could not make the impact as `{winner}` did. 122 | 123 | NOTE: I've calculated the winner on the basis of contribution, repos data like stars, forks and so on. 124 | ", winner=winner_name.color(colorful::Color::Aquamarine1a), 125 | loser=loser_name.color(colorful::Color::Aquamarine1a), 126 | ); 127 | println!("{winner_message}"); 128 | } 129 | 130 | fn add_k(num: u32) -> String { 131 | let ans = if num >= 1000 { 132 | let decimal_star = num as f32 / 1000.0; 133 | let num = format!("{:.1}k", decimal_star); 134 | num 135 | } else { 136 | num.to_string() 137 | }; 138 | ans 139 | } 140 | 141 | pub fn start_comparison(pair: (String, String)) { 142 | let (userone, usertwo) = pair; 143 | comparison(userone, usertwo); 144 | } 145 | -------------------------------------------------------------------------------- /src/get_detailed_view.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | extern crate colorful; 3 | use colorful::{Color, Colorful, HSL}; 4 | extern crate cfonts; 5 | use cfonts::{say, Colors, Fonts, Options}; 6 | use crossterm::{ 7 | execute, 8 | terminal::{self, SetSize}, 9 | }; 10 | use term_table::{ 11 | row::Row, 12 | table_cell::{Alignment, TableCell}, 13 | }; 14 | 15 | // Contribution Graph Maker 16 | use crate::{github_graphql::detailed_view::RepositoriesInformation, graph::graph_maker, input}; 17 | 18 | use crate::github_graphql::detailed_view; 19 | 20 | fn profile_header(user: String) { 21 | ascii_text(user); 22 | } 23 | 24 | fn rainbow() { 25 | let line = "\t\t███████████████████████████████████████████████████████████████████████████\n"; 26 | line.rainbow(); 27 | } 28 | 29 | // progress bar for languages bars. 30 | fn progress_bar(data_map: HashMap) { 31 | let mut values = Vec::new(); 32 | let mut languages = Vec::new(); 33 | 34 | for (key, value) in data_map { 35 | // progress_bar(key, value); 36 | if value > 100 { 37 | values.push(100.0); 38 | } else if value == 10 { 39 | values.push((value + 10) as f64); 40 | } else { 41 | values.push(value as f64); 42 | } 43 | languages.push(key); 44 | } 45 | 46 | let bar = "█"; 47 | ascii_text(String::from("Top Language")); 48 | 49 | let c = languages.iter().max_by_key(|x| x.len()).unwrap(); 50 | 51 | for (i, value) in values.iter().enumerate() { 52 | let h = (*value as f32 * 15.0 % 360.0) / 360.0; 53 | let length = (value - 10.0) as usize; 54 | println!( 55 | " {: Result<(), Box> { 81 | // Set the new size of the terminal window 82 | // the normal size of the window is default, using this because 83 | // bar size is increasing and doing a text wrapping 84 | // decreasing the length of the bar is decreasing all the other bars also. 85 | // at now setting a new terminal height is a solution 86 | let new_width = 110; 87 | let new_height = 30; 88 | let size = SetSize(new_width, new_height); 89 | execute!(std::io::stdout(), size)?; 90 | Ok(()) 91 | } 92 | 93 | // check why does the contribution graph is not showing when using other keys. 94 | fn show_contribution_graph(user_name: String, secret_key: String) -> Result<(), anyhow::Error> { 95 | let secret_key = secret_key.trim(); 96 | graph_maker::generate_graph(user_name, secret_key) 97 | } 98 | 99 | fn top_repositories_display(repo_data: HashMap) { 100 | let mut table = term_table::Table::new(); 101 | table.max_column_width = 50; 102 | table.style = term_table::TableStyle::elegant(); 103 | 104 | for data in repo_data.values() { 105 | let ( 106 | name, 107 | star_count, 108 | description, 109 | lang, 110 | fork_count, 111 | project_url, 112 | created_at, 113 | updated_at, 114 | request, 115 | open_issues, 116 | ) = ( 117 | data.key.as_str(), 118 | data.stargazer_count.as_str(), 119 | data.description.as_str(), 120 | data.lang.as_str(), 121 | data.fork_count.as_str(), 122 | data.repo_url.as_str(), 123 | data.created_at.as_str(), 124 | data.updated_at.as_str(), 125 | data.request.as_str(), 126 | data.open_issue.as_str(), 127 | ); 128 | 129 | let formatted_data = format!( 130 | r" 131 | Project : {name} ({project_url}) 132 | Description : {description} 133 | language : {lang} 134 | Stars : {star_count} 135 | Forks : {fork_count} 136 | Open PR : {request} 137 | Open Issues : {open_issues} 138 | ", 139 | project_url = project_url.color(Color::Aquamarine1a) 140 | ); 141 | table.add_row(Row::new(vec![TableCell::new_with_alignment( 142 | formatted_data, 143 | 2, 144 | term_table::table_cell::Alignment::Left, 145 | )])); 146 | } 147 | 148 | print!("{}", table.render()); 149 | } 150 | 151 | // the main_view_start is the backbone of our tool. 152 | // the two username and secret_key grab the github username, and the API key 153 | // API key always saved in a .txt file inside the `home_dir` 154 | // same goes for the permanent user, the only time the username file will not be read when the command is starts with -t 155 | // the header_git_data: takes a vector of string which is fetching the basic information like username, repo counts etc from the file `start_header_info` 156 | // the repo_data is holding the repo details, like total stars counts etc (graphql will help me alot here, need an improment) 157 | pub fn main_view_start() { 158 | let (username, secret_key) = input::cli_input(); 159 | let (profile_data, language_data, top_repo) = 160 | detailed_view::get_graphql_info(username.clone(), secret_key.trim()); 161 | 162 | // change the size so that it can show bars and all that. 163 | 164 | set_new_terminal_size(); 165 | clean_terminal(); 166 | 167 | // An animated rainbow bar, attraction 168 | 169 | rainbow(); 170 | 171 | // profile header bar, showing information about the user 172 | // prints the github logo and the basic information 173 | profile_header(username.clone()); 174 | crate::github_logo_ascii::print_formatter(profile_data, language_data.clone()); 175 | 176 | // starting the progress bar. 177 | // for languages it will start a bar. 178 | 179 | progress_bar(language_data); 180 | 181 | // starting of the contribution graph 182 | // ascii_text converts text to ascii art for heading 183 | 184 | ascii_text("Top Repositories".to_string()); 185 | top_repositories_display(top_repo); 186 | 187 | ascii_text("Contribution Graph".to_string()); 188 | let graph = show_contribution_graph(username, secret_key); 189 | match graph { 190 | Ok(()) => print!(""), 191 | Err(error) => { 192 | eprintln!("You should change you API key, it got expires for graph contribution\nits an issue: https://github.com/ArshErgon/gitgrab/issues/17"); 193 | std::process::exit(0) 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/github_graphql/detailed_view.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use colorful::{Color, Colorful}; 3 | use graphql_client::{reqwest::post_graphql_blocking as post_graphql, GraphQLQuery}; 4 | use reqwest::blocking::Client; 5 | use std::collections::HashMap; 6 | 7 | #[derive(GraphQLQuery)] 8 | #[graphql( 9 | schema_path = "src/graph/schema.graphql", 10 | query_path = "src/github_graphql/query.graphql", 11 | response_derives = "Debug" 12 | )] 13 | 14 | struct Kusa; 15 | type URI = String; 16 | type DateTime = String; 17 | 18 | #[derive(Debug)] 19 | pub struct RepositoriesInformation { 20 | pub key: String, 21 | pub stargazer_count: String, 22 | pub description: String, 23 | pub lang: String, 24 | pub fork_count: String, 25 | pub repo_url: String, 26 | pub updated_at: String, 27 | pub created_at: String, 28 | pub request: String, 29 | pub open_issue: String, 30 | } 31 | 32 | pub fn get_graphql_info( 33 | username: String, 34 | secret_key: &str, 35 | ) -> ( 36 | HashMap, 37 | HashMap, 38 | HashMap, 39 | ) { 40 | let data = user_authentication(username, secret_key); 41 | let return_data = match data { 42 | Ok(raw_data) => filter_out_data(raw_data), 43 | Err(error) => { 44 | let error_msg = format!(" 45 | {0} 46 | Error: {error} 47 | This error can happened because of the following 48 | 1. User doesn't exists (recheck your username). 49 | 2. Organization support is not available right now 50 | 3. The token request is exceed (https://docs.github.com/en/apps/creating-github-apps/creating-github-apps/rate-limits-for-github-apps) 51 | ", "An Error Occuried".color(Color::Red)); 52 | eprint!("{error_msg}"); 53 | std::process::exit(0); 54 | } 55 | }; 56 | return_data 57 | } 58 | 59 | fn user_authentication(user_name: String, secret_key: &str) -> Result { 60 | let variables = kusa::Variables { user_name }; 61 | let client = Client::builder() 62 | .user_agent("graphql-rust/0.10.0") 63 | .default_headers( 64 | std::iter::once(( 65 | reqwest::header::AUTHORIZATION, 66 | reqwest::header::HeaderValue::from_str(&format!("Bearer {}", secret_key)).unwrap(), 67 | )) 68 | .collect(), 69 | ) 70 | .build()?; 71 | let response_body = 72 | post_graphql::(&client, "https://api.github.com/graphql", variables)?; 73 | response_body.data.context("failed to fetch data") 74 | } 75 | 76 | // getting the data out in hashmaps for easy retrival 77 | fn filter_out_data( 78 | response_data: kusa::ResponseData, 79 | ) -> ( 80 | HashMap, 81 | HashMap, 82 | HashMap, 83 | ) { 84 | const EMPTY: &str = "NA"; 85 | let mut filter_data_map: HashMap = HashMap::new(); 86 | let mut languages: HashMap = HashMap::new(); 87 | let mut fork_count = 0; 88 | let mut top_repositories: HashMap = HashMap::new(); 89 | let mut string_year = String::new(); 90 | 91 | match response_data.user { 92 | Some(user) => { 93 | filter_data_map.insert( 94 | "bio".to_string(), 95 | user.bio.unwrap_or_else(|| EMPTY.to_string()), 96 | ); 97 | filter_data_map.insert( 98 | "company".to_string(), 99 | user.company.unwrap_or_else(|| EMPTY.to_string()), 100 | ); 101 | filter_data_map.insert("email".to_string(), user.email); 102 | 103 | filter_data_map.insert( 104 | "location".to_string(), 105 | user.location.unwrap_or_else(|| EMPTY.to_string()), 106 | ); 107 | 108 | filter_data_map.insert("login".to_string(), user.login); 109 | 110 | filter_data_map.insert( 111 | "name".to_string(), 112 | user.name.unwrap_or_else(|| EMPTY.to_string()), 113 | ); 114 | filter_data_map.insert( 115 | "twitter_username".to_string(), 116 | user.twitter_username.unwrap_or_else(|| EMPTY.to_string()), 117 | ); 118 | filter_data_map.insert( 119 | "website_url".to_string(), 120 | user.website_url.unwrap_or_else(|| EMPTY.to_string()), 121 | ); 122 | 123 | match user.followers { 124 | followers => { 125 | filter_data_map 126 | .insert("followers".to_string(), followers.total_count.to_string()); 127 | } 128 | } 129 | 130 | match user.following { 131 | following => { 132 | filter_data_map 133 | .insert("following".to_string(), following.total_count.to_string()); 134 | } 135 | } 136 | 137 | match user.pull_requests { 138 | requests => { 139 | filter_data_map.insert("request".to_string(), requests.total_count.to_string()); 140 | } 141 | } 142 | 143 | match user.watching { 144 | watcher => { 145 | filter_data_map.insert("watcher".to_string(), watcher.total_count.to_string()); 146 | } 147 | } 148 | 149 | match user.issues { 150 | issues => { 151 | filter_data_map.insert("issues".to_string(), issues.total_count.to_string()); 152 | } 153 | } 154 | 155 | match user.starred_repositories { 156 | stars => { 157 | filter_data_map.insert("stars".to_string(), stars.total_count.to_string()); 158 | } 159 | } 160 | 161 | match user.updated_at { 162 | update => { 163 | filter_data_map.insert("update".to_string(), update); 164 | } 165 | } 166 | 167 | match user.contributions_collection { 168 | mut contribution => { 169 | let years = contribution.contribution_years; 170 | if years.len() > 1 { 171 | string_year = years[years.len() - 1].to_string(); 172 | } else { 173 | string_year = years[0].to_string(); 174 | } 175 | filter_data_map.insert("contribution".to_string(), string_year); 176 | } 177 | } 178 | 179 | if let Some(edges) = user.top_repositories.edges.as_ref() { 180 | for node in edges 181 | .iter() 182 | .filter_map(|edge| edge.as_ref().map(|e| e.node.as_ref()).flatten()) 183 | { 184 | if node.is_fork { 185 | continue; 186 | } 187 | let mut request_count = String::new(); 188 | let key = node.name.clone(); 189 | let description = node 190 | .description 191 | .clone() 192 | .unwrap_or_else(|| "No description given".to_string()); 193 | let stargazer_count = node.stargazer_count; 194 | let fork_count = node.fork_count; 195 | let repo_url = node 196 | .projects_url 197 | .strip_suffix("/projects") 198 | .unwrap() 199 | .to_string(); 200 | let created_at = node.created_at.clone(); 201 | let updated_at = node.updated_at.clone(); 202 | let request = node.clone().pull_requests.total_count.to_string(); 203 | let open_issue = node.clone().issues.total_count.to_string(); 204 | if let Some(lang) = &node.primary_language { 205 | let lang = lang.name.clone(); 206 | let data = RepositoriesInformation { 207 | key: key.clone(), 208 | stargazer_count: stargazer_count.to_string(), 209 | description, 210 | lang, 211 | fork_count: fork_count.to_string(), 212 | repo_url, 213 | created_at, 214 | updated_at, 215 | request, 216 | open_issue, 217 | }; 218 | top_repositories.insert(key, (data)); 219 | } 220 | } 221 | } else { 222 | println!("No repos found"); 223 | } 224 | match user.repositories { 225 | repo => { 226 | filter_data_map.insert("repo".to_string(), repo.total_count.to_string()); 227 | if let Some(ref nodes) = repo.nodes { 228 | nodes 229 | .iter() 230 | .filter_map(|node| if let Some(n) = node { Some(n) } else { None }) 231 | .filter_map(|node| node.primary_language.as_ref()) 232 | .for_each(|lang| { 233 | *languages.entry(lang.name.to_string()).or_insert(0) += 1; 234 | }); 235 | } 236 | 237 | if let Some(nodes) = repo.nodes { 238 | nodes 239 | .iter() 240 | .filter_map(|node| if let Some(n) = node { Some(n) } else { None }) 241 | .for_each(|node| { 242 | fork_count += node.forks.total_count; 243 | }); 244 | }; 245 | } 246 | } 247 | } 248 | None => { 249 | let error_msg = format!( 250 | r" 251 | Error, could not find information about the user 252 | This error can happened because of the following 253 | 1. User doesn't exists (recheck your username). 254 | 2. Organization support is not available right now 255 | 3. The token request is exceed (https://docs.github.com/en/apps/creating-github-apps/creating-github-apps/rate-limits-for-github-apps)" 256 | ); 257 | eprintln!("{error_msg}"); 258 | 259 | std::process::exit(0) 260 | } 261 | } 262 | filter_data_map.insert("fork".to_string(), fork_count.to_string()); 263 | let total_value: u32 = languages.values().sum(); 264 | for (key, val) in languages.clone() { 265 | let mut percentage = (((val + 10) as f32 / total_value as f32) * 100.0) as u32; 266 | if val == 1 { 267 | percentage = 10; 268 | } else if percentage > 100 { 269 | percentage = 100; 270 | } 271 | languages.insert(key.to_string(), percentage); 272 | } 273 | (filter_data_map, languages, top_repositories) 274 | } 275 | -------------------------------------------------------------------------------- /src/github_graphql/query.graphql: -------------------------------------------------------------------------------- 1 | query Kusa($userName: String!) { 2 | user(login: $userName) { 3 | bio 4 | company 5 | email 6 | updatedAt 7 | followers { 8 | totalCount 9 | } 10 | following { 11 | totalCount 12 | } 13 | location 14 | login 15 | name 16 | websiteUrl 17 | starredRepositories { 18 | totalCount 19 | } 20 | pullRequests { 21 | totalCount 22 | } 23 | repositories(first: 100) { 24 | totalCount 25 | nodes { 26 | forks { 27 | totalCount 28 | } 29 | primaryLanguage { 30 | name 31 | } 32 | } 33 | } 34 | twitterUsername 35 | watching { 36 | totalCount 37 | } 38 | topRepositories(orderBy: {field: STARGAZERS, direction: ASC}, first: 5) { 39 | edges { 40 | node { 41 | name 42 | isFork 43 | primaryLanguage { 44 | name 45 | } 46 | projectsUrl 47 | description 48 | stargazerCount 49 | forkCount 50 | createdAt 51 | updatedAt 52 | pullRequests(states: OPEN) { 53 | totalCount 54 | } 55 | issues(states: OPEN) { 56 | totalCount 57 | } 58 | mentionableUsers(first: 5) { 59 | edges { 60 | node { 61 | name 62 | login 63 | } 64 | } 65 | } 66 | } 67 | } 68 | } 69 | issues { 70 | totalCount 71 | } 72 | contributionsCollection { 73 | contributionCalendar { 74 | totalContributions 75 | } 76 | contributionYears 77 | hasAnyContributions 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/github_logo_ascii.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use colorful::{Color, Colorful}; 4 | use term_table::Table; 5 | 6 | fn print_logo(data_map: HashMap) { 7 | let mut table = Table::new(); 8 | table.max_column_width = 100; 9 | table.style = term_table::TableStyle::empty(); 10 | let email = &data_map["email"]; 11 | let repos = &data_map["repo"]; 12 | let issue = &data_map["issue"]; 13 | let followers = &data_map["followers"]; 14 | let company = &data_map["company"]; 15 | let watcher = &data_map["watcher"]; 16 | let star = &data_map["stars"]; 17 | let pull_requests = &data_map["request"]; 18 | let fork = &data_map["fork"]; 19 | let twitter = &data_map["twitter_username"]; 20 | let name = &data_map["name"]; 21 | let bio = &data_map["bio"]; 22 | let blog = &data_map["website_url"]; 23 | let following = &data_map["following"]; 24 | let username = &data_map["login"]; 25 | let location = &data_map["location"]; 26 | let top_lang = &data_map["top_lang"]; 27 | let update = &data_map["update"]; 28 | let contribution = &data_map["contribution"]; 29 | let msg = format!( 30 | r" 31 | {name} ({username}) has {repos} repos on GitHub, using {top_lang} as a top lang. 32 | {followers} followers, {following} following, {star} stars, {fork} forks, and {watcher} watchers. 33 | open {issue} issues, open {pull_request} pull requests and a doing contribution since {contribution}. 34 | Works at {company} in {location}. 35 | {bio} 36 | Visit blog: {blog}. 37 | Contact: {email}. 38 | Last updated: {updated}. 39 | Follow on Twitter: {twitter}. ", 40 | name = name.clone().color(Color::Aquamarine1a), 41 | username = username.clone().color(Color::Aquamarine1a), 42 | email = email.clone().color(Color::Aquamarine1a), 43 | repos = repos.clone().color(Color::Aquamarine1a), 44 | issue = issue.clone().color(Color::Aquamarine1a), 45 | followers = followers.clone().color(Color::Aquamarine1a), 46 | company = company.clone().color(Color::Aquamarine1a), 47 | watcher = watcher.clone().color(Color::Aquamarine1a), 48 | star = star.clone().color(Color::Aquamarine1a), 49 | pull_request = pull_requests.clone().color(Color::Aquamarine1a), 50 | fork = fork.clone().color(Color::Aquamarine1a), 51 | twitter = twitter.clone().color(Color::Aquamarine1a), 52 | bio = bio.clone().color(Color::Aquamarine1a), 53 | blog = blog.clone().color(Color::Aquamarine1a), 54 | following = following.clone().color(Color::Aquamarine1a), 55 | location = location.clone().color(Color::Aquamarine1a), 56 | top_lang = top_lang.clone().color(Color::Aquamarine1a), 57 | updated = update.clone().color(Color::Aquamarine1a), 58 | contribution = contribution.clone().color(Color::Aquamarine1a), 59 | ); 60 | table.add_row(term_table::row::Row::new(vec![ 61 | term_table::table_cell::TableCell::new_with_alignment( 62 | msg, 63 | 2, 64 | term_table::table_cell::Alignment::Center, 65 | ), 66 | ])); 67 | 68 | println!("{}", table.render()); 69 | } 70 | 71 | pub fn print_formatter(mut git_data: HashMap, language_map: HashMap) { 72 | let repo = add_k(git_data[&"repo".to_string()].parse::().unwrap()); 73 | git_data.entry("repo".to_string()).or_insert(repo); 74 | 75 | let followers = add_k(git_data[&"followers".to_string()].parse::().unwrap()); 76 | let following = add_k(git_data[&"following".to_string()].parse::().unwrap()); 77 | git_data.entry("followers".to_string()).or_insert(followers); 78 | git_data.entry("following".to_string()).or_insert(following); 79 | 80 | let stars = add_k(git_data[&"stars".to_string()].parse::().unwrap()); 81 | let forks = add_k(git_data[&"fork".to_string()].parse::().unwrap()); 82 | let issue = add_k(git_data[&"issues".to_string()].parse::().unwrap()); 83 | let watcher = add_k(git_data[&"watcher".to_string()].parse::().unwrap()); 84 | git_data.insert("stars".to_string(), stars); 85 | git_data.insert("fork".to_string(), forks); 86 | git_data.insert("issue".to_string(), issue); 87 | git_data.insert("watcher".to_string(), watcher); 88 | let max_key = language_map 89 | .iter() 90 | .max_by_key(|(_, &value)| value) 91 | .map(|(key, _)| key.clone()) 92 | .unwrap_or_else(|| "NA".to_string()); 93 | 94 | git_data.insert("top_lang".to_string(), max_key); 95 | 96 | print_logo(git_data); 97 | } 98 | 99 | // add a k to the number 100 | // like 1000 will become 1k 101 | fn add_k(num: u32) -> String { 102 | let ans = if num >= 1000 { 103 | let decimal_star = num as f32 / 1000.0; 104 | let num = format!("{:.1}k", decimal_star); 105 | num 106 | } else { 107 | num.to_string() 108 | }; 109 | ans 110 | } 111 | -------------------------------------------------------------------------------- /src/graph/graph_maker.rs: -------------------------------------------------------------------------------- 1 | // NOTICE: With all due respect this folder `graph` is owned by https://github.com/Ryu0118/ 2 | // he has created this script for showing the graph (https://github.com/Ryu0118/Kusa) 3 | // I just have done some changes into it, so it can run like I want it to. 4 | // I don't own this graph folder at any cost, all rights goes to https://github.com/Ryu0118/ for making it possible. 5 | 6 | use ::reqwest::blocking::Client; 7 | use ansi_term::{Colour, Style}; 8 | use anyhow::{Context, Result}; 9 | use graphql_client::{reqwest::post_graphql_blocking as post_graphql, GraphQLQuery}; 10 | use std::process; 11 | 12 | #[derive(GraphQLQuery)] 13 | #[graphql( 14 | schema_path = "src/graph/schema.graphql", 15 | query_path = "src/graph/query.graphql", 16 | response_derives = "Debug" 17 | )] 18 | 19 | struct Kusa; 20 | 21 | type Date = String; 22 | 23 | struct DailyStatus { 24 | date: String, 25 | color: String, 26 | } 27 | 28 | impl DailyStatus { 29 | fn get_month(&self) -> usize { 30 | self.date 31 | .split('-') 32 | .nth(1) 33 | .and_then(|m| m.parse().ok()) 34 | .unwrap() 35 | } 36 | } 37 | 38 | trait HexToRGB { 39 | fn get_rgb(&self) -> Colour; 40 | } 41 | 42 | impl HexToRGB for str { 43 | fn get_rgb(&self) -> Colour { 44 | // #ebedf0 -> ebedf0 45 | let v = i64::from_str_radix(&self[1..], 16).unwrap() as f64; 46 | let r: u8 = (v / 256_f64.powf(2.0) % 256.0) as u8; 47 | let g: u8 = (v / 256_f64.powf(1.0) % 256.0) as u8; 48 | let b: u8 = (v / 256_f64.powf(0.0) % 256.0) as u8; 49 | Colour::RGB(r, g, b) 50 | } 51 | } 52 | 53 | fn get_github_contributions(response_data: kusa::ResponseData) -> (i64, Vec>) { 54 | match response_data.user { 55 | Some(user) => { 56 | let contribution_calendar = user.contributions_collection.contribution_calendar; 57 | 58 | let total_contributions = contribution_calendar.total_contributions; 59 | 60 | let weekly_status = contribution_calendar 61 | .weeks 62 | .iter() 63 | .map(|weekly_status| { 64 | weekly_status 65 | .contribution_days 66 | .iter() 67 | .map(|daily_status| DailyStatus { 68 | date: daily_status.date.to_string(), 69 | color: daily_status.color.to_string(), 70 | }) 71 | .collect() 72 | }) 73 | .collect(); 74 | (total_contributions, weekly_status) 75 | } 76 | None => { 77 | println!("No users found"); 78 | process::exit(1) 79 | } 80 | } 81 | } 82 | 83 | fn post_graphql_query(user_name: String, secret_key: &str) -> Result { 84 | let variables = kusa::Variables { user_name }; 85 | let client = Client::builder() 86 | .user_agent("graphql-rust/0.10.0") 87 | .default_headers( 88 | std::iter::once(( 89 | reqwest::header::AUTHORIZATION, 90 | reqwest::header::HeaderValue::from_str(&format!("Bearer {}", secret_key)).unwrap(), 91 | )) 92 | .collect(), 93 | ) 94 | .build()?; 95 | 96 | let response_body = 97 | post_graphql::(&client, "https://api.github.com/graphql", variables)?; 98 | 99 | response_body.data.context("failed to fetch data") 100 | } 101 | 102 | fn transpose(weekly_statuses: &[Vec]) -> Vec> { 103 | let week_count = weekly_statuses.len(); 104 | let mut kusa: Vec> = Vec::new(); 105 | for column_index in 0..7 { 106 | let mut row = Vec::new(); 107 | for weekly_status in weekly_statuses.iter().take(week_count) { 108 | if let Some(contribution) = weekly_status.get(column_index) { 109 | row.push(contribution); 110 | } 111 | } 112 | kusa.push(row); 113 | } 114 | kusa 115 | } 116 | 117 | #[cfg(not(target_os = "windows"))] 118 | fn print_month(kusa: &[Vec<&DailyStatus>]) { 119 | let months = [ 120 | "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", 121 | ]; 122 | let mut month_line = "".to_string(); 123 | for (index, daily_status) in kusa[0].iter().enumerate() { 124 | if index == 0 { 125 | let month = daily_status.get_month(); 126 | month_line += months[month - 1]; 127 | continue; 128 | } 129 | 130 | let month = daily_status.get_month(); 131 | let previous_month = kusa[0][index - 1].get_month(); 132 | 133 | if month != previous_month { 134 | let require_width = index * 2; 135 | let current_width = month_line.len(); 136 | let require_space = (require_width as i64) - (current_width as i64); 137 | if require_space > 0 { 138 | let adjustment = " ".repeat(require_space as usize); 139 | month_line += &adjustment; 140 | month_line += months[month - 1]; 141 | } else { 142 | let adjustment_space = 3 + require_space; 143 | month_line.clear(); 144 | month_line += &" ".repeat(adjustment_space as usize); 145 | month_line += months[month - 1]; 146 | } 147 | } 148 | } 149 | println!("{}", month_line); 150 | } 151 | 152 | #[cfg(not(target_os = "windows"))] 153 | fn print_gradation(kusa: &[Vec<&DailyStatus>]) { 154 | let start_point = (kusa[6].len()) * 2 - 18; 155 | let colors = [ 156 | "#ebedf0", //Less 157 | "#9be9a8", "#40c463", "#30a14e", "#216e39", //More 158 | ]; 159 | let whitespaces = " ".repeat(start_point); 160 | print!("{}Less ", whitespaces); 161 | for color in colors { 162 | color 163 | .get_rgb() 164 | .paint("■ ".as_bytes()) 165 | .write_to(&mut std::io::stdout()) 166 | .unwrap(); 167 | } 168 | println!("More"); 169 | } 170 | 171 | fn print_kusa(kusa: &Vec>) { 172 | for weekly_kusa in kusa { 173 | for daily_kusa in weekly_kusa { 174 | daily_kusa 175 | .color 176 | .get_rgb() 177 | .paint("■ ".as_bytes()) 178 | .write_to(&mut std::io::stdout()) 179 | .unwrap(); 180 | } 181 | println!(); 182 | } 183 | } 184 | 185 | pub fn generate_graph(user_name: String, secret_key: &str) -> Result<()> { 186 | #[cfg(target_os = "windows")] 187 | ansi_term::enable_ansi_support(); 188 | let data = post_graphql_query(user_name, secret_key)?; 189 | let (total_contributions, weekly_statuses) = get_github_contributions(data); 190 | let kusa = transpose(&weekly_statuses); 191 | 192 | println!( 193 | "{} contributions in the last year", 194 | Style::new().bold().paint(total_contributions.to_string()) 195 | ); 196 | 197 | #[cfg(not(target_os = "windows"))] 198 | print_month(&kusa); 199 | 200 | print_kusa(&kusa); 201 | 202 | #[cfg(not(target_os = "windows"))] 203 | print_gradation(&kusa); 204 | 205 | Ok(()) 206 | } 207 | -------------------------------------------------------------------------------- /src/graph/query.graphql: -------------------------------------------------------------------------------- 1 | query Kusa($userName: String!) { 2 | user(login: $userName) { 3 | contributionsCollection { 4 | contributionCalendar { 5 | totalContributions 6 | weeks { 7 | contributionDays { 8 | contributionCount 9 | date 10 | color 11 | } 12 | } 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /src/input/menu_cli.rs: -------------------------------------------------------------------------------- 1 | use dialoguer::{theme::ColorfulTheme, Select}; 2 | 3 | pub fn menu_view() -> std::io::Result<()> { 4 | let selection = Select::with_theme(&ColorfulTheme::default()) 5 | .item("1. Create a User") 6 | .item("2. Enter/Update the Github API key") 7 | .item("3. Exit") 8 | .interact()?; 9 | 10 | match selection { 11 | 0 => create_user_file(), 12 | 1 => create_api_file(), 13 | 2 => { 14 | println!("Thanks for using gitgrab"); 15 | std::process::exit(0) 16 | } 17 | _ => println!("Not an option"), 18 | }; 19 | Ok(()) 20 | } 21 | 22 | fn create_user_file() { 23 | let mut user_name = String::new(); 24 | println!("Enter Username\n"); 25 | let user_input = std::io::stdin(); 26 | user_input.read_line(&mut user_name).unwrap(); 27 | 28 | let home_dir = match std::env::var_os("HOME") { 29 | Some(path) => path, 30 | None => { 31 | println!("Cannot get home directory!"); 32 | return; 33 | } 34 | }; 35 | let file_path = match home_dir.to_str() { 36 | Some(path) => path.to_owned() + "/gitgrab_user.txt", 37 | None => { 38 | println!("Cannot convert home directory to string!"); 39 | return; 40 | } 41 | }; 42 | 43 | let mut file = match std::fs::OpenOptions::new() 44 | .create(true) 45 | .write(true) 46 | .truncate(true) 47 | .open(&file_path) 48 | { 49 | Ok(file) => file, 50 | Err(e) => { 51 | println!("Error opening file: {:?}", e); 52 | return; 53 | } 54 | }; 55 | 56 | std::io::Write::write_all(&mut file, user_name.as_bytes()).unwrap(); 57 | 58 | let success_msg = format!( 59 | "\nUser file is successfully created at {} with a name gitgrab_user\n", 60 | home_dir.to_str().unwrap() 61 | ); 62 | 63 | let flag = get_secret_key().1; // store just the boolean value. 64 | let mut flag_msg = "You can now run `gitgrab` to see your Github information."; 65 | 66 | if !flag { 67 | flag_msg = "API key is not available. \n 68 | Please create an API key at https://github.com/settings/tokens"; 69 | } 70 | 71 | eprintln!("{0} {1}", success_msg, flag_msg); 72 | std::process::exit(0); 73 | } 74 | 75 | fn create_api_file() { 76 | let (key, flag) = get_secret_key(); 77 | if flag { 78 | println!("Your current key: {key}"); 79 | } 80 | let mut api_key = String::new(); 81 | let api_input = std::io::stdin(); 82 | api_input.read_line(&mut api_key); 83 | let home_dir = std::env::var_os("HOME").expect("Cannot get home directory!"); 84 | let file_path = home_dir.into_string().unwrap() + "/gitgrab_api.txt"; 85 | let mut file = match std::fs::OpenOptions::new() 86 | .create(true) 87 | .write(true) 88 | .truncate(true) // truncate the file to 0 bytes 89 | .open(file_path) 90 | { 91 | Ok(file) => file, 92 | Err(e) => { 93 | eprintln!("Error opening file: {:?}", e); 94 | std::process::exit(0) 95 | } 96 | }; 97 | std::io::Write::write_all(&mut file, api_key.as_bytes()).unwrap(); 98 | match flag { 99 | true => println!("\nKey successfuly updated"), 100 | false => println!("\nKey file generted"), 101 | } 102 | std::process::exit(0); 103 | } 104 | 105 | pub fn get_secret_key() -> (String, bool) { 106 | let home_dir = match dirs::home_dir() { 107 | Some(path) => path, 108 | None => { 109 | eprintln!("Cannot get home directory!"); 110 | std::process::exit(0); 111 | } 112 | }; 113 | let file_path = home_dir.join("gitgrab_api.txt"); 114 | let secret_key = match std::fs::read_to_string(&file_path) { 115 | Ok(contents) => contents, 116 | Err(e) => "NONE".to_string(), 117 | }; 118 | let flag = if secret_key == "NONE".to_string() { 119 | false 120 | } else { 121 | true 122 | }; 123 | (secret_key, flag) 124 | } 125 | -------------------------------------------------------------------------------- /src/input/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{env, path::Path}; 2 | 3 | use clap::{Arg, ArgAction, Command}; 4 | 5 | pub(crate) mod menu_cli; 6 | 7 | pub fn cli_input() -> (String, String) { 8 | let mut flag = false; 9 | let matches = Command::new("gitgrab") 10 | .version("2.0") 11 | .author("A CLI application for github users, which shows the information of a particular user in a `neofetch` style\nProudly build with the help of Rust.") 12 | .about("Neofetch but build for GitHub") 13 | .arg( 14 | Arg::new("t") 15 | .short('t') 16 | .long("temp") 17 | .help("Show information for a temporary user: gitgrab -t "), 18 | ) 19 | .arg( 20 | Arg::new("o") 21 | .short('o') 22 | .long("option") 23 | .action(ArgAction::SetTrue) 24 | .help("Option to create the user or insert the github API key"), 25 | ) 26 | .arg( 27 | Arg::new("author") 28 | .short('a') 29 | .long("author") 30 | .action(ArgAction::SetTrue) 31 | ) 32 | .arg( 33 | Arg::new("LOC") 34 | // .short("loc") 35 | .long("loc") 36 | .help("Shows line of code"), 37 | ) 38 | .arg( 39 | Arg::new("compare") 40 | .short('c') 41 | .long("com") 42 | .help("Compare two users"), 43 | ) 44 | .get_matches(); 45 | 46 | let compare_names = match matches.get_one::("compare") { 47 | None => "None", 48 | Some(val) => val, 49 | }; 50 | 51 | if compare_names != "None" { 52 | let username: Vec<&str> = compare_names.split(' ').collect(); 53 | if username.len() <= 1 { 54 | eprint!( 55 | "One another username is missing as I get only {:#?}", 56 | username 57 | ); 58 | std::process::exit(0) 59 | } 60 | crate::compare::start_comparison((username[0].to_owned(), username[1].to_owned())); 61 | std::process::exit(0); 62 | } 63 | 64 | let repo_url = match matches.get_one::("LOC") { 65 | None => "None", 66 | Some(val) => val, 67 | }; 68 | 69 | // repo_url lines of code. 70 | if repo_url != "None" { 71 | crate::lines_of_codes::start_lines(repo_url.to_string()); 72 | std::process::exit(0); 73 | } 74 | 75 | // for menu bar 76 | match matches.get_flag("o") { 77 | true => menu_cli::menu_view(), 78 | false => Ok(()), 79 | }; 80 | 81 | // a temporary user 82 | let arg_temp = match matches.get_one::("t") { 83 | None => "None", 84 | Some(val) => val, 85 | }; 86 | 87 | // to show the name of the user. 88 | if matches.get_flag("author") { 89 | about(); 90 | flag = true; 91 | }; 92 | 93 | // start the temporary user function 94 | let (mut username, mut secret_key) = (String::new(), String::new()); 95 | 96 | if arg_temp != "None" { 97 | let (key, _) = menu_cli::get_secret_key(); 98 | (username, secret_key) = (arg_temp.to_string(), key); 99 | flag = true; 100 | } 101 | 102 | // for the feature gitgrab 103 | // so that a full information about the permanet user(the home_dir/gitgrab_user one) will be displayed on the screen. 104 | if !flag { 105 | (username, secret_key) = show_user_info(String::new(), false); 106 | } 107 | (username, secret_key) 108 | } 109 | 110 | fn show_user_info(arg: String, flag: bool) -> (String, String) { 111 | let home_dir = match env::var_os("HOME") { 112 | Some(path) => path, 113 | None => { 114 | eprintln!("Cannot get home directory!"); 115 | std::process::exit(0); 116 | } 117 | }; 118 | 119 | let apifile_path = Path::new(&home_dir).join("gitgrab_api.txt"); 120 | let username_file_path = Path::new(&home_dir).join("gitgrab_user.txt"); 121 | let mut username = match std::fs::read_to_string(&username_file_path) { 122 | Ok(contents) => contents.trim().to_string(), 123 | Err(_) => { 124 | eprintln!( 125 | "Couldn't find the `gitgrab_user.txt' file here: {}\nEnter gitgrab -o to enter user or -t for temporary user", 126 | username_file_path.display() 127 | ); 128 | std::process::exit(0) 129 | } 130 | }; 131 | 132 | let secret_key_string = menu_cli::get_secret_key().0; // get the string only 133 | if !secret_key_string.is_empty() { 134 | // overwrite the contents of gitgrab_api.txt with the new API key 135 | match std::fs::write(&apifile_path, secret_key_string.clone()) { 136 | Ok(_) => (), 137 | Err(e) => { 138 | eprintln!( 139 | "Could not find `gitgrab_api.txt` file here: {}\n Have you generated the key? https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token\ngitgrab -o", 140 | apifile_path.display() 141 | ); 142 | std::process::exit(0) 143 | } 144 | } 145 | } 146 | 147 | (username, secret_key_string) 148 | } 149 | 150 | // add some information about the creator 151 | fn about() { 152 | let gitgrab_logo = format!( 153 | r" 154 | 155 | ██████╗ ██╗████████╗███████╗███████╗████████╗ ██████╗██╗ ██╗ 156 | ██╔════╝ ██║╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝██╔════╝██║ ██║ 157 | ██║ ███╗██║ ██║ █████╗ █████╗ ██║ ██║ ███████║ 158 | ██║ ██║██║ ██║ ██╔══╝ ██╔══╝ ██║ ██║ ██╔══██║ 159 | ╚██████╔╝██║ ██║ ██║ ███████╗ ██║ ╚██████╗██║ ██║ 160 | ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝v.0.2.0 161 | 162 | A CLI application which shows your github information on the terminal, started as an inspiration from `neofetch` 163 | " 164 | ); 165 | println!("{}", gitgrab_logo); 166 | std::process::exit(0) 167 | } 168 | -------------------------------------------------------------------------------- /src/lines_of_codes.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Error; 2 | use reqwest::Url; 3 | use std::collections::HashMap; 4 | use term_table::{row::Row, table_cell::TableCell, Table}; 5 | 6 | #[derive(Debug)] 7 | struct LocStruct { 8 | language: String, 9 | files: u64, 10 | lines: u64, 11 | blanks: u64, 12 | comments: u64, 13 | lines_of_code: u64, 14 | } 15 | 16 | fn find_lines(pair: (&str, &str)) -> Result<(HashMap), Error> { 17 | let (username, repo_name) = pair; 18 | let mut loc_map: HashMap = HashMap::new(); 19 | let input = format!("https://api.codetabs.com/v1/loc/?github={username}/{repo_name}"); 20 | let url = Url::parse(&input)?; 21 | let response = reqwest::blocking::get(url).map_err(|err| reqwest::Error::from(err))?; 22 | 23 | if response.status() != 200 { 24 | eprintln!("Could not find the data associated with {username}/{repo_name}"); 25 | std::process::exit(0) 26 | } 27 | 28 | crate::get_detailed_view::ascii_text("LoC".to_string()); 29 | let body = response.text()?; 30 | let data: Vec> = serde_json::from_str(&body)?; 31 | for loc_data in data { 32 | let language = loc_data 33 | .get("language") 34 | .and_then(|v| v.as_str()) 35 | .unwrap_or("") 36 | .to_string(); 37 | let files = loc_data.get("files").and_then(|v| v.as_u64()).unwrap_or(0); 38 | let lines_of_code = loc_data 39 | .get("linesOfCode") 40 | .and_then(|v| v.as_u64()) 41 | .unwrap_or(0); 42 | let blanks = loc_data.get("blanks").and_then(|v| v.as_u64()).unwrap_or(0); 43 | let comments = loc_data 44 | .get("comments") 45 | .and_then(|v| v.as_u64()) 46 | .unwrap_or(0); 47 | let lines = loc_data.get("lines").and_then(|v| v.as_u64()).unwrap_or(0); 48 | 49 | loc_map.insert( 50 | language.clone(), 51 | LocStruct { 52 | language, 53 | files, 54 | lines_of_code, 55 | blanks, 56 | comments, 57 | lines, 58 | }, 59 | ); 60 | } 61 | 62 | Ok(loc_map) 63 | } 64 | 65 | fn create_loc_table(data: HashMap, url: String) { 66 | let mut table = Table::new(); 67 | table.max_column_width = 110; 68 | table.style = term_table::TableStyle::thin(); 69 | table.add_row(Row::new(vec![ 70 | TableCell::new("Language"), 71 | TableCell::new_with_alignment("Files", 1, term_table::table_cell::Alignment::Right), 72 | TableCell::new_with_alignment("Lines", 2, term_table::table_cell::Alignment::Right), 73 | TableCell::new_with_alignment("Blanks", 3, term_table::table_cell::Alignment::Right), 74 | TableCell::new_with_alignment("Comments", 3, term_table::table_cell::Alignment::Right), 75 | TableCell::new_with_alignment("LOC", 3, term_table::table_cell::Alignment::Right), 76 | ])); 77 | 78 | for (_, loc_data) in data { 79 | let lang = loc_data.language; 80 | let file = loc_data.files; 81 | let lines_of_code = loc_data.lines_of_code; 82 | let blanks = loc_data.blanks; 83 | let comments = loc_data.comments; 84 | let lines = loc_data.lines; 85 | table.add_row(Row::new(vec![ 86 | TableCell::new(lang), 87 | TableCell::new_with_alignment(file, 1, term_table::table_cell::Alignment::Right), 88 | TableCell::new_with_alignment(lines, 2, term_table::table_cell::Alignment::Right), 89 | TableCell::new_with_alignment(blanks, 3, term_table::table_cell::Alignment::Right), 90 | TableCell::new_with_alignment(comments, 3, term_table::table_cell::Alignment::Right), 91 | TableCell::new_with_alignment( 92 | lines_of_code, 93 | 3, 94 | term_table::table_cell::Alignment::Right, 95 | ), 96 | ])); 97 | } 98 | println!("The details of the repo: {url}\n"); 99 | print!("{}", table.render()); 100 | } 101 | 102 | pub fn start_lines(para_url: String) { 103 | let url: Vec<&str> = para_url 104 | .split(|c| c == '/') 105 | .filter(|s| !s.is_empty()) 106 | .collect(); 107 | if url.len() < 1 { 108 | eprintln!("invalid url"); 109 | std::process::exit(0); 110 | } 111 | 112 | let username = url[url.len() - 2]; 113 | let project = url[url.len() - 1]; 114 | match find_lines((username, project)) { 115 | Ok(data) => { 116 | create_loc_table(data, para_url); 117 | std::process::exit(0) 118 | } 119 | Err(_) => { 120 | eprint!("Error"); 121 | std::process::exit(0); 122 | } 123 | }; 124 | } 125 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code)] 2 | #![allow(unused)] 3 | 4 | use get_detailed_view::main_view_start; 5 | 6 | mod compare; 7 | mod get_detailed_view; 8 | mod github_logo_ascii; 9 | mod input; 10 | mod github_graphql { 11 | pub mod detailed_view; 12 | } 13 | 14 | mod lines_of_codes; 15 | mod graph { 16 | pub mod graph_maker; 17 | } 18 | 19 | // the main function starts here. 20 | // 1. Inside the main_view_start 21 | // .. I'm using starting the whole project every function in different files starts there. 22 | // more information inside the function 23 | fn main() { 24 | main_view_start(); 25 | } 26 | -------------------------------------------------------------------------------- /target/.rustc_info.json: -------------------------------------------------------------------------------- 1 | {"rustc_fingerprint":10047283868261059991,"outputs":{"15729799797837862367":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.so\nlib___.so\nlib___.a\nlib___.so\n/home/arshergon/.rustup/toolchains/stable-x86_64-unknown-linux-gnu\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"gnu\"\ntarget_family=\"unix\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"linux\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"unknown\"\nunix\n","stderr":""},"4614504638168534921":{"success":true,"status":"","code":0,"stdout":"rustc 1.69.0 (84c898d65 2023-04-16)\nbinary: rustc\ncommit-hash: 84c898d65adf2f39a5a98507f1fe0ce10a2b8dbc\ncommit-date: 2023-04-16\nhost: x86_64-unknown-linux-gnu\nrelease: 1.69.0\nLLVM version: 15.0.7\n","stderr":""}},"successes":{}} --------------------------------------------------------------------------------