├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── logo.png ├── build.sh ├── cmd ├── subcommands │ ├── calc_stats.go │ ├── check_anti_patterns.go │ ├── detect_bottlenecks.go │ ├── display_analysic.go │ └── get.go └── vc-analyze │ └── main.go ├── go.mod ├── go.sum └── pkg ├── analyzer ├── anti_patterns.go ├── bottlenecks.go ├── commit_history.go └── commit_history_test.go ├── format └── repository.go └── utils └── get_star.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | 4 | *.exe~ 5 | 6 | *.dll 7 | 8 | *.so 9 | 10 | *.dylib 11 | 12 | # Test binary, built with `go test -c` 13 | *.test 14 | 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | This document contains certain rules and guidelines that developers are expected to follow, while contributing to any repository. 4 | 5 | --- 6 | ## 1. Commit Messages 7 | 8 | * Use the `-m` flag only for minor changes. The message following the `-m` flag must be of the below format : 9 | > ` ` 10 | 11 | :white_check_mark: __Examples of valid messages:__ 12 | * Added serialisers.py for users app 13 | * Updated utils/validator.js file 14 | * Changed functionality of authentication process 15 | 16 | :x: __Examples of invalid messages:__ 17 | * Idk why this is not working 18 | * Only ui bug fixes left 19 | * All changes done, ready for production :)) 20 | 21 | * Before opening a PR, make sure you squash all your commits into one single commit using `it rebase` (squash). Instead of having 50 commits that describe 1 feature implementation, there must be one commit that describes everything that has been done so far. You can read up about it [here](https://www.internalpointers.com/post/squash-commits-into-one-git). 22 | > NOTE: While squashing your commits to write a new one, do not make use of `-m` flag. In this case, a vim editor window shall open. Write a title for the commit within 50-70 characters, leave a line and add an understandable description. 23 | 24 | ## 2. Issues 25 | * Issues __MUST__ be opened any time any of the following events occur : 26 | 1. You encounter an issue such that a major (50 lines of code or above) portion of the code needs to be changed/added. 27 | 2. You want feature enhancements 28 | 3. You encounter bugs 29 | 4. Code refactoring is required 30 | 5. Test coverage should be increased 31 | * __Open issues with the given template only.__ 32 | * Feel free to label the issues appropriately. 33 | * Do not remove the headings (questions in bold) while opening an issue with the given template. Simply append to it. 34 | 35 | 36 | ## 3. Branches and PRs 37 | 38 | * No commits must be made to the `master` branch directly. The `master` branch shall only consist of the working code. 39 | * Developers are expected to work on feature branches, and upon successful development and testing, a PR (pull request) must be opened to merge with master. 40 | * A branch must be named as either as the feature being implemented, or the issue being fixed. 41 | 42 | :white_check_mark: __Examples of valid brach names:__ 43 | * #8123 (issue number) 44 | * OAuth (feature) 45 | * questionsUtils (functionality of the questions) 46 | 47 | :x: __Examples of invalid branch names__: 48 | * ziyan-testing 49 | * attemptToFixAuth 50 | * SomethingRandom 51 | 52 | 53 | ## 4. Discussion Ethics 54 | 55 | * Developers should be clear and concise while commenting on issues or PR reviews. If needed, one should provide visual reference or a code snippet for everyone involved to properly grasp the issue. 56 | * Everyone should be respectful of everyone's opinion. Any harsh/disrespectful language is __STRICTLY__ prohibited and will not be tolerated under any circumstances. 57 | 58 | ## 5. Coding Ethics 59 | 60 | * Developers are highly encouraged to use comments wherever necessary and make the code self documented. 61 | * The project structure should be neat and organised. All folders and files should be organised semantically according to their functionality. 62 | * The name of the folders and files should not be too long but should be as self explanatory as possible. 63 | * Documentation shall __STRICTLY__ have gender neutral terms. Instead of using "he/him" or "she/her", one should use "they/them" or "the user". 64 | 65 | ## 6. Coding Style Guidelines 66 | 67 | Developers should aim to write clean, maintainable, scalable and testable code. If your code is not testable, that means, it's time to refactor it. The following guidelines might come in handy for this: 68 | 69 | * Python: [Hitchiker's Guide to Python](https://docs.python-guide.org/writing/style/), [Google](https://github.com/google/styleguide/blob/gh-pages/pyguide.md) 70 | * GoLang: [Effective-Go](https://golang.org/doc/effective_go.html) 71 | * Django: [Django-Styleguide](https://github.com/HackSoftware/Django-Styleguide) 72 | * JavaScript: [Airbnb](https://github.com/airbnb/javascript) 73 | * React.JS: [Airbnb](https://github.com/airbnb/javascript/tree/master/react) 74 | * Flutter/Dart: [Effective-Dart](https://dart.dev/guides/language/effective-dart) 75 | * Kotlin: [Kotlin Conventions](https://kotlinlang.org/docs/reference/coding-conventions.html) 76 | * Swift: [Swift Style Guide](https://github.com/github/swift-style-guide), [Google](https://google.github.io/swift/) 77 | * Docker: [Dev Best Practices](https://docs.docker.com/develop/), [Dockerfile Best Practices](https://docs.docker.com/develop/develop-images/dockerfile_best-practices/) 78 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Adi Gulalkari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Version Control Activity Analyzer 2 | 3 | [![Open Source Love svg1](https://badges.frapsoft.com/os/v1/open-source.svg?v=103)](https://github.com/adigulalkari/VC-Analyzer) 4 | [![GitHub license](https://img.shields.io/github/license/adigulalkari/VC-Analyzer.svg)](https://github.com/adigulalkari/VC-Analyzer/blob/main/LICENSE) 5 | [![GitHub go.mod Go version of a Go module](https://img.shields.io/github/go-mod/go-version/IEEE-VIT/termiboard.svg)](https://github.com/adigulalkari/VC-Analyzer) 6 | [![GitHub Open Issues](https://img.shields.io/github/issues-raw/adigulalkari/VC-Analyzer)](https://github.com/adigulalkari/VC-Analyzer/issues) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/adigulalkari/VC-Analyzer/issues/new/choose) 8 | 9 | 10 |

11 | Logo 12 |

13 | 14 | A command-line tool for analyzing version control activity in Git repositories. This tool provides insights into commit history, identifies bottlenecks, and detects anti-patterns in a project's version control workflow. 15 | 16 | ## Features 17 | 18 | - **Commit History Analysis**: Get detailed statistics about the commit history, including the total number of commits and contributions by each developer. 19 | - **Bottleneck Identification**: Identify potential bottlenecks in the workflow, such as long-lived branches and infrequent commits. 20 | - **Anti-Pattern Detection**: Detect common anti-patterns like large commits, force pushes, and other practices that may hinder collaboration and code quality. 21 | 22 | ## Prerequisites 23 | 24 | - [Git](https://git-scm.com/downloads) 25 | - [Go](https://golang.org/doc/install) (version 1.18 or higher) 26 | 27 | ## Installation 28 | 29 | Clone the repository 30 | ``` 31 | git clone https://github.com/adigulalkari/VC-Analyzer.git 32 | cd VC-Analyzer 33 | ``` 34 | Run main 35 | ``` 36 | chmod +x build.sh 37 | ./build.sh 38 | ``` 39 | 40 | ## Usage 41 | ``` 42 | vc-analyze --help 43 | ``` 44 | 45 | ## Documentation 46 | ```vc-analyze --help``` 47 |
48 | 49 | image 50 |
51 |
52 | 53 | ```vc-analyze calc-stats path/to/local/repo``` 54 | 55 | Provides the following stats: 56 | - All commit history msgs 57 | - Stats on the contributions per author 58 | - Active/Inactive branches 59 |
60 | 61 | ```vc-analyze check-anti-patterns path/to/local/repo``` 62 | 63 | Provides the following functionalities 64 | - Checking large commits 65 | - Checking for force pushes 66 | - Flag large binary files in commits that bloat the repository 67 | 68 | ## Contributing 69 | Contributions are welcome! Please follow these steps to contribute to the project: 70 | 71 | - Fork the repository. 72 | - Create a new branch: ```git checkout -b feature-branch``` 73 | - Make your changes and commit them: ```git commit -m "Add new feature"``` 74 | - Push to the branch: ```git push origin feature-branch``` 75 | - Open a pull request. 76 | 77 | Refer to [CONTRIBUTING.md](https://github.com/adigulalkari/VC-Analyzer/blob/main/CONTRIBUTING.md) for more guidelines! 78 | 79 | ## LICENSE 80 | See the [LICENSE](https://github.com/adigulalkari/VC-Analyzer/blob/main/LICENSE) file for license rights and limitations (MIT). 81 | 82 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adigulalkari/VC-Analyzer/3cd978ab6575c741c92759d7ccd551fee2b2c6e9/assets/logo.png -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Name of the binary 4 | BINARY_NAME=vc-analyze 5 | 6 | # Detect OS type 7 | if [[ "$OSTYPE" == "linux-gnu"* || "$OSTYPE" == "darwin"* ]]; then 8 | # Linux or macOS 9 | INSTALL_DIR="/usr/local/bin" 10 | echo "Detected Unix-based OS (Linux/macOS)" 11 | elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then 12 | # Windows with Git Bash (msys) 13 | INSTALL_DIR="/c/Program Files/VC-Analyze" 14 | echo "Detected Windows with Git Bash" 15 | else 16 | echo "Unsupported OS: $OSTYPE" 17 | exit 1 18 | fi 19 | 20 | echo "Building $BINARY_NAME..." 21 | GO111MODULE=on go build -o $BINARY_NAME ./cmd/vc-analyze 22 | 23 | # Create installation directory if it doesn't exist (on Windows) 24 | if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then 25 | mkdir -p "$INSTALL_DIR" 26 | fi 27 | 28 | echo "Installing $BINARY_NAME to $INSTALL_DIR..." 29 | if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then 30 | mv "$BINARY_NAME" "$INSTALL_DIR/$BINARY_NAME" 31 | else 32 | sudo mv "$BINARY_NAME" "$INSTALL_DIR" 33 | fi 34 | 35 | echo "Installation completed successfully!" 36 | -------------------------------------------------------------------------------- /cmd/subcommands/calc_stats.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/MakeNowJust/heredoc/v2" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/adigulalkari/VC-Analyzer/pkg/analyzer" 12 | ) 13 | 14 | var ( 15 | authorStats bool 16 | commitSize bool 17 | activeBranch bool 18 | ) 19 | 20 | var CalcStatsCmd = &cobra.Command{ 21 | Use: "calc-stats ", 22 | Short: "Calculate statistics for the local repo", 23 | Long: `This command allows you to calculate various statistics for a local Git repository, including author statistics and commit size statistics.`, 24 | Aliases: []string{"c"}, 25 | Example: heredoc.Doc(` 26 | # Calculate author statistics 27 | $ vc-analyze calc-stats --author-stats path/to/local/repo 28 | 29 | # Calculate commit size statistics 30 | $ vc-analyze calc-stats --commit-size path/to/local/repo 31 | 32 | # Calculate branch statistics 33 | $ vc-analyze calc-stats --active-branch path/to/local/repo 34 | `), 35 | Args: func(cmd *cobra.Command, args []string) error { 36 | if len(args) < 1 { 37 | return errors.New("requires a path to the repository") 38 | } 39 | return nil 40 | }, 41 | RunE: func(cmd *cobra.Command, args []string) error { 42 | repoPath := args[0] // Get the repository path from the arguments 43 | 44 | // Check if the repository exists (optional) 45 | if _, err := os.Stat(repoPath); os.IsNotExist(err) { 46 | return fmt.Errorf("repository path does not exist: %s", repoPath) 47 | } 48 | 49 | // Check which flag is set and call the appropriate function 50 | if authorStats { 51 | // Call a function to calculate author statistics 52 | analyzer.AnalyzeCommitHistory(repoPath) 53 | } else if commitSize { 54 | // Call a function to calculate commit size statistics 55 | analyzer.AnalyzeCommitSize(repoPath) 56 | } else if activeBranch { 57 | // Call function to show branch statistics 58 | analyzer.AnalyzeBranchStats(repoPath) 59 | } else { 60 | return errors.New("no valid flag provided, use --author-stats , --commit-size or --active-branch") 61 | } 62 | 63 | return nil 64 | }, 65 | } 66 | 67 | func init() { 68 | CalcStatsCmd.Flags().BoolVarP(&authorStats, "author-stats", "a", false, "Calculate statistics for each author") 69 | CalcStatsCmd.Flags().BoolVarP(&commitSize, "commit-size", "s", false, "Calculate the size of commits") 70 | CalcStatsCmd.Flags().BoolVarP(&activeBranch, "active-branch", "b", false, "Show branch statistics") 71 | 72 | // Custom help function to display help message clearly 73 | CalcStatsCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { 74 | helpMessage := ` 75 | Usage: 76 | vc-analyze calc-stats [flags] 77 | 78 | This command allows you to calculate various statistics for a local Git repository. 79 | 80 | Flags: 81 | -a, --author-stats Calculate statistics for each author 82 | -s, --commit-size Calculate the size of commits 83 | -b, --active-branch Show branch statistics 84 | -h, --help help for calc-stats 85 | 86 | Examples: 87 | # Calculate author statistics 88 | $ vc-analyze calc-stats --author-stats path/to/local/repo 89 | 90 | # Calculate commit size statistics 91 | $ vc-analyze calc-stats --commit-size path/to/local/repo 92 | 93 | # Calculate branch statistics 94 | $ vc-analyze calc-stats --active-branch path/to/local/repo 95 | ` 96 | fmt.Fprint(cmd.OutOrStdout(), helpMessage) 97 | }) 98 | } -------------------------------------------------------------------------------- /cmd/subcommands/check_anti_patterns.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/MakeNowJust/heredoc/v2" 9 | "github.com/spf13/cobra" 10 | 11 | "github.com/adigulalkari/VC-Analyzer/pkg/analyzer" 12 | ) 13 | 14 | var AntiPatternsCmd = &cobra.Command{ 15 | Use: "check-anti-patterns ", 16 | Short: "Find out the anti-patterns present in your repository", 17 | Aliases: []string{"p"}, 18 | Example: heredoc.Doc(` 19 | Find out the anti-patterns present in your repository 20 | $ vc-analyze check-anti-patterns path/to/local/repo 21 | `), 22 | Args: func(cmd *cobra.Command, args []string) error { 23 | if len(args) < 1 { 24 | return errors.New("requires a detail argument") 25 | } 26 | return nil 27 | }, 28 | RunE: func(cmd *cobra.Command, args []string) error { 29 | repoPath := args[0] // Get the repository path from the arguments 30 | 31 | // Check if the repository exists (optional) 32 | if _, err := os.Stat(repoPath); os.IsNotExist(err) { 33 | return fmt.Errorf("repository path does not exist: %s", repoPath) 34 | } 35 | 36 | // Call the AnalyzeCommitHistory function 37 | analyzer.DetectAntiPatterns(repoPath) 38 | 39 | return nil 40 | }, 41 | } 42 | -------------------------------------------------------------------------------- /cmd/subcommands/detect_bottlenecks.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/MakeNowJust/heredoc/v2" 7 | "github.com/adigulalkari/VC-Analyzer/pkg/analyzer" 8 | "github.com/spf13/cobra" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | ) 13 | 14 | // Function to gather commit history data 15 | func getCommitHistory(repoPath string) ([]analyzer.CommitInfo, error) { 16 | // Navigate to the repository path 17 | cmd := exec.Command("git", "-C", repoPath, "log", "--pretty=format:%H,%an,%ad,%s", "--numstat") 18 | output, err := cmd.CombinedOutput() 19 | if err != nil { 20 | return nil, fmt.Errorf("failed to run git command: %v", err) 21 | } 22 | 23 | // Parse the output 24 | lines := strings.Split(string(output), "\n") 25 | var commits []analyzer.CommitInfo 26 | var currentCommit *analyzer.CommitInfo 27 | for _, line := range lines { 28 | if strings.Contains(line, ",") { 29 | // New commit entry 30 | fields := strings.SplitN(line, ",", 4) 31 | currentCommit = &analyzer.CommitInfo{ 32 | Hash: fields[0], 33 | Author: fields[1], 34 | Date: fields[2], 35 | Message: fields[3], 36 | Files: make(map[string]int), 37 | } 38 | commits = append(commits, *currentCommit) 39 | } else if currentCommit != nil { 40 | // File changes for the current commit 41 | fileFields := strings.Fields(line) 42 | if len(fileFields) == 3 { 43 | // Update file change details 44 | currentCommit.Files[fileFields[2]] = 1 // Mark file as changed 45 | } 46 | } 47 | } 48 | return commits, nil 49 | } 50 | 51 | var DetectBottlenecksCmd = &cobra.Command{ 52 | Use: "detect-bottlenecks ", 53 | Short: "Find bottlenecks in the commit history of a local repository", 54 | Aliases: []string{"d"}, 55 | Example: heredoc.Doc(` 56 | $ vc-analyze detect-bottlenecks path/to/local/repo 57 | `), 58 | Args: func(cmd *cobra.Command, args []string) error { 59 | if len(args) < 1 { 60 | return errors.New("requires a repository path argument") 61 | } 62 | return nil 63 | }, 64 | RunE: func(cmd *cobra.Command, args []string) error { 65 | repoPath := args[0] // Get the repository path from the arguments 66 | 67 | // Check if the repository exists 68 | if _, err := os.Stat(repoPath); os.IsNotExist(err) { 69 | return fmt.Errorf("repository path does not exist: %s", repoPath) 70 | } 71 | 72 | // Get the commit history for the repository 73 | commits, err := getCommitHistory(repoPath) 74 | if err != nil { 75 | return fmt.Errorf("error fetching commit history: %v", err) 76 | } 77 | 78 | // Detect bottlenecks based on the commit history 79 | // detectBottlenecks(commits) 80 | analyzer.DetectBottlenecks(commits) 81 | return nil 82 | }, 83 | } 84 | -------------------------------------------------------------------------------- /cmd/subcommands/display_analysic.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/MakeNowJust/heredoc/v2" 8 | "github.com/olekukonko/tablewriter" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var CmdCmd = &cobra.Command{ 13 | Use: "cmd", 14 | Short: "Display help commands in table format", 15 | Long: `Displays all available commands along with their flags and shorthand in table.`, 16 | Example: heredoc.Doc(`#Display available commands in table format 17 | $vc-analyze cmd 18 | 19 | # Display specific help for a command, e.g., calc-stats 20 | $vc-analyze c -h 21 | 22 | #Display specific help command, e.g., check-anti-patterns 23 | $vc-analyze p -h`), 24 | Run: func(cmd *cobra.Command, args []string) { 25 | printCmdHelpTable() 26 | }, 27 | } 28 | 29 | func printCmdHelpTable() { 30 | fmt.Println("Usage: vc-analyze [command/short-hand] [-h/--help]") 31 | table := tablewriter.NewWriter(os.Stdout) 32 | table.SetHeader([]string{"command", "short-hand", "Description"}) 33 | //Set minimum column width for 2nd column 34 | table.SetColMinWidth(2, 35) 35 | table.SetAutoWrapText(false) 36 | 37 | table.Append([]string{"check-anti-patterns", "p", "Find out the anti-patterns present in your repository"}) 38 | table.Append([]string{"calc-stats", "c", "Calculate statistics for the local repository"}) 39 | table.Append([]string{"detect-bottlenecks", "d", "Find bottlenecks in the commit history of a local repository"}) 40 | table.Append([]string{"get", "g", "Display one or many repositories details"}) 41 | 42 | table.SetBorder(true) 43 | table.SetHeaderColor( 44 | tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiGreenColor}, // Color for "Flag" header 45 | tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiGreenColor}, // Color for "Short-hand" header 46 | tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiGreenColor}, // Color for Description header 47 | ) 48 | table.SetColumnColor( 49 | tablewriter.Colors{tablewriter.FgHiBlackColor}, // Color for "Flag" column 50 | tablewriter.Colors{tablewriter.FgHiBlackColor}, // Color for "Description" column 51 | tablewriter.Colors{tablewriter.FgHiBlackColor}, // Color for Short-hand column 52 | ) 53 | 54 | // Render the table to the console 55 | table.Render() 56 | } 57 | 58 | func init() { 59 | CmdCmd.SetHelpFunc(func(cmd *cobra.Command, args []string) { 60 | printCmdHelpTable() // Custom help function in table format 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /cmd/subcommands/get.go: -------------------------------------------------------------------------------- 1 | package subcommands 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "github.com/MakeNowJust/heredoc/v2" 8 | "github.com/spf13/cobra" 9 | 10 | "github.com/adigulalkari/VC-Analyzer/pkg/format" 11 | "github.com/adigulalkari/VC-Analyzer/pkg/utils" 12 | ) 13 | 14 | var ( 15 | Repository string // Exported to be accessible outside the package 16 | ) 17 | 18 | var GetCmd = &cobra.Command{ // Exported to be accessible outside the package 19 | Use: "get ", 20 | Short: "Display one or many repositories details", 21 | Aliases: []string{"g"}, 22 | Example: heredoc.Doc(` 23 | Get stars count for a given repository 24 | $ vc-analyze get stars -r golang/go 25 | `), 26 | Args: func(cmd *cobra.Command, args []string) error { 27 | if len(args) < 1 { 28 | return errors.New("requires a detail argument") 29 | } 30 | 31 | possibleDetails := []string{"stars"} 32 | validResource := false 33 | 34 | for _, resource := range possibleDetails { 35 | if args[0] == resource { 36 | validResource = true 37 | break 38 | } 39 | } 40 | 41 | if !validResource { 42 | return fmt.Errorf(`detail "%s" is invalid`, args[0]) 43 | } 44 | 45 | return nil 46 | }, 47 | RunE: func(cmd *cobra.Command, args []string) error { 48 | if !format.IsRepositoryValid(Repository) { 49 | return errors.New("repository flag is required and must be in the format 'owner/repo'") 50 | } 51 | 52 | detail := args[0] 53 | 54 | switch detail { 55 | case "stars": 56 | { 57 | starsCount, err := utils.GetStarCount(Repository) 58 | if err != nil { 59 | return fmt.Errorf("could not get stars count for this repository: %w", err) 60 | } 61 | 62 | fmt.Printf("%s has %d stars\n", Repository, *starsCount) 63 | } 64 | } 65 | 66 | return nil 67 | }, 68 | } 69 | -------------------------------------------------------------------------------- /cmd/vc-analyze/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/adigulalkari/VC-Analyzer/cmd/subcommands" 7 | "github.com/common-nighthawk/go-figure" 8 | "github.com/fatih/color" 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | var rootCmd = &cobra.Command{ 13 | Use: "vc-analyze", 14 | Version: "0.1.0", 15 | Short: "A command-line tool for analyzing version control activity in Git repositories.", 16 | 17 | CompletionOptions: cobra.CompletionOptions{ 18 | DisableDefaultCmd: true, // This line disables the 'completion' command 19 | }, 20 | } 21 | 22 | func init() { 23 | subcommands.GetCmd.Flags().StringVarP(&subcommands.Repository, "repository", "r", "", "The GitHub repository in the format 'owner/repo'") 24 | 25 | rootCmd.AddCommand(subcommands.CmdCmd) 26 | rootCmd.AddCommand(subcommands.GetCmd) 27 | rootCmd.AddCommand(subcommands.CalcStatsCmd) 28 | rootCmd.AddCommand(subcommands.AntiPatternsCmd) 29 | rootCmd.AddCommand(subcommands.DetectBottlenecksCmd) 30 | } 31 | 32 | func main() { 33 | myFigure := figure.NewFigure("VC-Analyze", "", true) 34 | 35 | blue := color.New(color.FgBlue) 36 | blue.Println(myFigure.String()) 37 | 38 | err := rootCmd.Execute() 39 | if err != nil { 40 | os.Exit(1) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/adigulalkari/VC-Analyzer 2 | 3 | go 1.23.1 4 | 5 | require ( 6 | github.com/MakeNowJust/heredoc/v2 v2.0.1 7 | github.com/fatih/color v1.17.0 8 | github.com/go-git/go-billy/v5 v5.5.0 9 | github.com/olekukonko/tablewriter v0.0.5 10 | github.com/spf13/cobra v1.8.1 11 | ) 12 | 13 | require ( 14 | dario.cat/mergo v1.0.0 // indirect 15 | github.com/Microsoft/go-winio v0.6.1 // indirect 16 | github.com/ProtonMail/go-crypto v1.0.0 // indirect 17 | github.com/cloudflare/circl v1.3.7 // indirect 18 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 19 | github.com/emirpasic/gods v1.18.1 // indirect 20 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 21 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 22 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 23 | github.com/kevinburke/ssh_config v1.2.0 // indirect 24 | github.com/mattn/go-colorable v0.1.13 // indirect 25 | github.com/mattn/go-isatty v0.0.20 // indirect 26 | github.com/mattn/go-runewidth v0.0.9 // indirect 27 | github.com/pjbgf/sha1cd v0.3.0 // indirect 28 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 29 | github.com/skeema/knownhosts v1.2.2 // indirect 30 | github.com/xanzy/ssh-agent v0.3.3 // indirect 31 | golang.org/x/crypto v0.21.0 // indirect 32 | golang.org/x/mod v0.12.0 // indirect 33 | golang.org/x/net v0.23.0 // indirect 34 | golang.org/x/sys v0.18.0 // indirect 35 | golang.org/x/tools v0.13.0 // indirect 36 | gopkg.in/warnings.v0 v0.1.2 // indirect 37 | ) 38 | 39 | require ( 40 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 41 | github.com/go-git/go-git/v5 v5.12.0 42 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 43 | github.com/spf13/pflag v1.0.5 // indirect 44 | ) 45 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/MakeNowJust/heredoc/v2 v2.0.1 h1:rlCHh70XXXv7toz95ajQWOWQnN4WNLt0TdpZYIR/J6A= 4 | github.com/MakeNowJust/heredoc/v2 v2.0.1/go.mod h1:6/2Abh5s+hc3g9nbWLe9ObDIOhaRrqsyY9MWy+4JdRM= 5 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 6 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 7 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 8 | github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= 9 | github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 10 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 11 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 12 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 13 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 14 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 15 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 16 | github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= 17 | github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= 18 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= 19 | github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= 20 | github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 21 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 22 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= 27 | github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= 28 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 29 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 30 | github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= 31 | github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= 32 | github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= 33 | github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= 34 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 35 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 36 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 37 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 38 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 39 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 40 | github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= 41 | github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= 42 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 43 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 44 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 45 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 46 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 47 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 48 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 49 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 50 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 51 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 52 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 53 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 54 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 57 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 58 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 59 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 60 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 61 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 62 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 63 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 64 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 65 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 66 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 67 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 68 | github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= 69 | github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= 70 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 71 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 72 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 73 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 74 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 75 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 76 | github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= 77 | github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 78 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 79 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 80 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 81 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 82 | github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= 83 | github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 84 | github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 85 | github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 86 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 87 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 88 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 89 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 90 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 91 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 92 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 93 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 94 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 95 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 96 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 97 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 98 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 99 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 100 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 101 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= 102 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= 103 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 104 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 105 | golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= 106 | golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 107 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 109 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 110 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 111 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 112 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 113 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 114 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 115 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 116 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 117 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 118 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 119 | golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= 120 | golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= 121 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 122 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 127 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 129 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 130 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 131 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 132 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 133 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 134 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 135 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= 136 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 137 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 138 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 139 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 140 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 141 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 142 | golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= 143 | golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= 144 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 145 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 146 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 147 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 148 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 149 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 150 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 151 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 152 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 153 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 154 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 155 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 156 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 157 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 158 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 159 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 160 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 161 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 162 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 163 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 164 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 165 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 166 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 167 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 168 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 169 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 170 | -------------------------------------------------------------------------------- /pkg/analyzer/anti_patterns.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | "github.com/go-git/go-git/v5" 9 | "github.com/go-git/go-git/v5/plumbing/object" 10 | ) 11 | 12 | // DetectAntiPatterns looks for common version control anti-patterns. 13 | func DetectAntiPatterns(repoPath string) { 14 | // Open the Git repository 15 | repo, err := git.PlainOpen(repoPath) 16 | if err != nil { 17 | log.Fatalf("Error opening repository: %v", err) 18 | } 19 | 20 | // Get the HEAD reference 21 | ref, err := repo.Head() 22 | if err != nil { 23 | log.Fatalf("Error getting repository HEAD: %v", err) 24 | } 25 | 26 | // Get the commit history 27 | commitIter, err := repo.Log(&git.LogOptions{From: ref.Hash()}) 28 | if err != nil { 29 | log.Fatalf("Error getting commit history: %v", err) 30 | } 31 | 32 | fmt.Println("Detecting anti-patterns...") 33 | 34 | // Initialize anti-pattern counters 35 | var largeCommitsCount int 36 | var forcePushesDetected bool 37 | var infrequentCommits bool 38 | 39 | // Iterate through the commits 40 | err = commitIter.ForEach(func(c *object.Commit) error { 41 | // Detect large commits 42 | if len(c.Message) > 1000 { 43 | largeCommitsCount++ 44 | } 45 | 46 | // Simulate detecting force pushes (for demo purposes) 47 | // You can improve this logic by tracking ref changes 48 | if c.NumParents() > 1 { 49 | forcePushesDetected = true 50 | } 51 | // FIXME: The logic here needs to be improved 52 | 53 | return nil 54 | }) 55 | if err != nil { 56 | log.Fatalf("Error iterating through commits: %v", err) 57 | } 58 | 59 | // Simulate checking for infrequent commits 60 | // Check the time between the most recent and older commits 61 | recentCommit, _ := repo.CommitObject(ref.Hash()) 62 | recentTime := recentCommit.Committer.When 63 | 64 | err = commitIter.ForEach(func(c *object.Commit) error { 65 | if recentTime.Sub(c.Committer.When) > (7 * 24 * time.Hour) { 66 | infrequentCommits = true 67 | } 68 | return nil 69 | }) 70 | if err != nil { 71 | log.Fatalf("Error iterating through commits: %v", err) 72 | } 73 | 74 | // Display anti-pattern results 75 | if largeCommitsCount > 0 { 76 | fmt.Printf("Detected %d large commit(s).\n", largeCommitsCount) 77 | } else { 78 | fmt.Println("No large commits detected.") 79 | } 80 | 81 | if forcePushesDetected { 82 | fmt.Println("Force pushes detected.") 83 | } else { 84 | fmt.Println("No force pushes detected.") 85 | } 86 | 87 | if infrequentCommits { 88 | fmt.Println("Detected infrequent commits (more than 7 days between commits).") 89 | } else { 90 | fmt.Println("No infrequent commit patterns detected.") 91 | } 92 | 93 | fmt.Println("Anti-pattern detection complete.") 94 | } 95 | -------------------------------------------------------------------------------- /pkg/analyzer/bottlenecks.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os/exec" 7 | "regexp" 8 | "strings" 9 | ) 10 | 11 | // CommitInfo represents the details of a commit, including the commit hash, author, date, 12 | // commit message, and a map of files changed in the commit along with their respective change counts. 13 | type CommitInfo struct { 14 | Hash string 15 | Author string 16 | Date string 17 | Message string 18 | Files map[string]int 19 | } 20 | 21 | // MergeCommit holds the information of a merge commit, including the commit hash, 22 | // the commit message, and a list of files affected by the merge. This structure is used 23 | // to track merge conflicts and the files involved. 24 | type MergeCommit struct { 25 | CommitHash string 26 | Message string 27 | Files []string 28 | } 29 | 30 | func DetectBottlenecks(commits []CommitInfo){ 31 | fileChangeCounts := make(map[string]int) 32 | authorChangeCounts := make(map[string]int) 33 | 34 | for _, commit := range commits { 35 | for file := range commit.Files { 36 | fileChangeCounts[file]++ 37 | } 38 | authorChangeCounts[commit.Author]++ 39 | } 40 | 41 | // Arbitarily choosing fileChange threshold as 5 42 | fmt.Println("Potential bottleneck files (most frequently changed):") 43 | for file, count := range fileChangeCounts { 44 | if count > 2 { 45 | fmt.Printf("%s: %d changes\n", file, count) 46 | } 47 | } 48 | 49 | // Arbitarily choosing authorChange threshold as 5 50 | fmt.Println("\nPotential bottlenecks caused by contributors:") 51 | for author, count := range authorChangeCounts { 52 | if count > 5 { 53 | fmt.Printf("%s: %d changes\n", author, count) 54 | } 55 | } 56 | 57 | fmt.Println("\nPotential bottlenecks caused by frequent rollbacks:") 58 | mergeCommits := findMergeConflicts() 59 | for _, merge := range mergeCommits { 60 | fmt.Printf("Merge Commit: %s, Message: %s, Affected Files: %v\n", merge.CommitHash, merge.Message, merge.Files) 61 | } 62 | } 63 | 64 | // findMergeConflicts retrieves the list of merge commits from the Git history, 65 | // checks their commit messages for conflict indicators, and collects details 66 | // about the files affected by these merges. This function aids in identifying 67 | // problematic merges that could slow down development. 68 | func findMergeConflicts() []MergeCommit { 69 | cmd := exec.Command("git", "log", "--merges", "--pretty=format:%H %s") 70 | var out bytes.Buffer 71 | cmd.Stdout = &out 72 | err := cmd.Run() 73 | if err != nil { 74 | fmt.Println("Error executing git command:", err) 75 | return nil 76 | } 77 | 78 | commitLines := strings.Split(out.String(), "\n") 79 | var mergeCommits []MergeCommit 80 | 81 | for _, line := range commitLines { 82 | parts := strings.SplitN(line, " ", 2) 83 | if len(parts) < 2 { 84 | continue 85 | } 86 | commitHash := parts[0] 87 | commitMessage := parts[1] 88 | 89 | if isMergeConflict(commitMessage) { 90 | files := getChangedFiles(commitHash) 91 | mergeCommits = append(mergeCommits, MergeCommit{ 92 | CommitHash: commitHash, 93 | Message: commitMessage, 94 | Files: files, 95 | }) 96 | } 97 | } 98 | 99 | return mergeCommits 100 | } 101 | 102 | // isMergeConflict checks if a given commit message indicates a merge conflict 103 | // by searching for the keyword "conflict". This helps to filter out merge commits 104 | // that are likely to have encountered issues, facilitating focused analysis on 105 | // problematic areas during conflict resolution. 106 | func isMergeConflict(message string) bool { 107 | conflictPattern := `(?i)conflict` 108 | return regexp.MustCompile(conflictPattern).FindString(message) != "" 109 | } 110 | 111 | // getChangedFiles retrieves a list of files that were changed in a specific 112 | // merge commit identified by its hash. This is crucial for understanding 113 | // the impact of a merge and identifying which files may require attention 114 | // during conflict resolution. 115 | func getChangedFiles(commitHash string) []string { 116 | cmd := exec.Command("git", "diff-tree", "--no-commit-id", "--name-only", "-r", commitHash) 117 | var out bytes.Buffer 118 | cmd.Stdout = &out 119 | err := cmd.Run() 120 | if err != nil { 121 | fmt.Println("Error executing git command:", err) 122 | return nil 123 | } 124 | 125 | files := strings.Split(out.String(), "\n") 126 | return files[:len(files)-1] 127 | } 128 | 129 | -------------------------------------------------------------------------------- /pkg/analyzer/commit_history.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sort" 7 | 8 | "time" 9 | 10 | "github.com/fatih/color" 11 | "github.com/go-git/go-git/v5" // Core Go-git library 12 | "github.com/go-git/go-git/v5/plumbing" 13 | "github.com/go-git/go-git/v5/plumbing/object" // Used for commit objects 14 | ) 15 | 16 | type authorCommit struct { 17 | Author string 18 | Count int 19 | } 20 | 21 | var ( 22 | InActive string = "Inactive" 23 | Active string = "Active" 24 | ) 25 | 26 | // AnalyzeCommitHistory analyzes and prints commit history of the given repository 27 | func AnalyzeCommitHistory(repoPath string) { 28 | repo, err := git.PlainOpen(repoPath) 29 | if err != nil { 30 | log.Fatalf("Error opening repository: %v", err) 31 | } 32 | 33 | commitHistory(repo) 34 | } 35 | 36 | func commitHistory(repo *git.Repository) { 37 | // Get all authors and their commit count 38 | commitCounts, commitCount, err := getCommitCounts(repo) 39 | if err != nil { 40 | log.Fatalf("Error getting commit counts: %v", err) 41 | } 42 | authorCommits := getSortedAuthorCommits(commitCounts) 43 | printCommitHistoryAnalysis(commitCount, authorCommits) 44 | } 45 | 46 | func getSortedAuthorCommits(commitCounts map[string]int) []authorCommit { 47 | // Create a slice of author commits 48 | // for sorting by the number of commits in descending order 49 | var authorCommits []authorCommit 50 | for author, count := range commitCounts { 51 | authorCommits = append(authorCommits, authorCommit{Author: author, Count: count}) 52 | } 53 | 54 | // Sort the slice by commit count in descending order 55 | sort.Slice(authorCommits, func(i, j int) bool { 56 | return authorCommits[i].Count > authorCommits[j].Count 57 | }) 58 | 59 | return authorCommits 60 | } 61 | 62 | func getCommitCounts(repo *git.Repository) (map[string]int, int, error) { 63 | // Get the HEAD reference 64 | ref, err := repo.Head() 65 | if err != nil { 66 | return nil, 0, fmt.Errorf("Error getting HEAD reference: %w", err) 67 | } 68 | 69 | // Iterate over the commit history starting from HEAD 70 | commitIter, err := repo.Log(&git.LogOptions{From: ref.Hash()}) 71 | if err != nil { 72 | return nil, 0, fmt.Errorf("Error getting commit log: %w", err) 73 | } 74 | 75 | // Map to track the number of commits by each author 76 | commitCounts := make(map[string]int) 77 | commitCount := 0 78 | 79 | err = commitIter.ForEach(func(c *object.Commit) error { 80 | 81 | // Increment commit count for the author 82 | commitCounts[c.Author.Name]++ 83 | commitCount++ 84 | return nil 85 | }) 86 | 87 | if err != nil { 88 | return nil, 0, fmt.Errorf("Error iterating over commits: %w", err) 89 | } 90 | 91 | return commitCounts, commitCount, nil 92 | } 93 | 94 | func printCommitHistoryAnalysis(commitCount int, authorCommits []authorCommit) { 95 | fmt.Println("Commit history analysis:") 96 | 97 | // Print total number of commits 98 | fmt.Printf("\nTotal number of commits: %d\n", commitCount) 99 | 100 | // Print the sorted list of authors and their commit counts 101 | fmt.Println("\nNumber of commits by each author (in decreasing order):") 102 | for _, ac := range authorCommits { 103 | fmt.Printf("%s: %d commits\n", ac.Author, ac.Count) 104 | } 105 | } 106 | 107 | func AnalyzeCommitSize(repoPath string) { 108 | repo, err := git.PlainOpen(repoPath) 109 | if err != nil { 110 | log.Fatalf("Error opening repository: %v", err) 111 | } 112 | 113 | commitSize(repo) 114 | } 115 | 116 | func commitSize(repo *git.Repository) { 117 | commitCount, totalSize, err := getCommitStats(repo) 118 | if err != nil { 119 | log.Fatalf("Error getting commit stats: %v", err) 120 | } 121 | 122 | // Print commit size statistics 123 | printCommitSize(commitCount, totalSize) 124 | } 125 | 126 | func getCommitStats(repo *git.Repository) (int, int, error) { 127 | // Get the HEAD reference 128 | ref, err := repo.Head() 129 | if err != nil { 130 | return 0, 0, fmt.Errorf("Error getting HEAD reference: %w", err) 131 | } 132 | 133 | // Iterate over the commit history starting from HEAD 134 | commitIter, err := repo.Log(&git.LogOptions{From: ref.Hash()}) 135 | if err != nil { 136 | return 0, 0, fmt.Errorf("Error getting commit log: %w", err) 137 | } 138 | 139 | totalSize := 0 140 | commitCount := 0 141 | 142 | err = commitIter.ForEach(func(c *object.Commit) error { 143 | totalSize += len(c.Message) // Approximate commit size as the message length 144 | commitCount++ 145 | return nil 146 | }) 147 | 148 | if err != nil { 149 | return 0, 0, fmt.Errorf("Error iterating over commits: %w", err) 150 | } 151 | return commitCount, totalSize, nil 152 | } 153 | 154 | func printCommitSize(commitCount int, totalSize int) { 155 | fmt.Printf("\nTotal number of commits: %d\n", commitCount) 156 | fmt.Printf("Total commit message size: %d bytes\n", totalSize) 157 | fmt.Printf("Average commit size: %.2f bytes\n", float64(totalSize)/float64(commitCount)) 158 | } 159 | 160 | // AnalyzeBranchStats analyzes and prints branch stats of the given repository 161 | func AnalyzeBranchStats(repoPath string) { 162 | repo, err := git.PlainOpen(repoPath) 163 | if err != nil { 164 | log.Fatalf("Error opening repository: %v", err) 165 | } 166 | 167 | branchStats(repo, false) 168 | } 169 | 170 | func branchStats(repo *git.Repository, noColor bool) { 171 | // List all branches and their activity status 172 | branchesMap, activeBranchCount, inactiveBranchCount, err := getBranchCounts(repo) 173 | if err != nil { 174 | log.Fatalf("Error getting branch history: %v", err) 175 | } 176 | printBranchHistory(branchesMap, activeBranchCount, inactiveBranchCount, noColor) 177 | } 178 | 179 | func getBranchCounts(repo *git.Repository) (map[string]string, int, int, error) { 180 | branches, err := repo.Branches() 181 | if err != nil { 182 | return nil, 0, 0, fmt.Errorf("Error getting branches: %w", err) 183 | } 184 | 185 | branchesMap := make(map[string]string) 186 | activeBranchCount := 0 187 | inactiveBranchCount := 0 188 | 189 | err = branches.ForEach(func(ref *plumbing.Reference) error { 190 | commit, err := repo.CommitObject(ref.Hash()) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | branchName := ref.Name().Short() 196 | 197 | // Determine if the branch is active based on last commit date 198 | isActive := time.Since(commit.Committer.When) < 90*24*time.Hour 199 | 200 | branchStatus := InActive 201 | if isActive { 202 | branchStatus = Active 203 | activeBranchCount++ 204 | } else { 205 | inactiveBranchCount++ 206 | } 207 | 208 | branchesMap[branchName] = branchStatus 209 | return nil 210 | }) 211 | 212 | if err != nil { 213 | return nil, 0, 0, fmt.Errorf("Error getting commit object: %w", err) 214 | } 215 | 216 | return branchesMap, activeBranchCount, inactiveBranchCount, nil 217 | } 218 | 219 | func printBranchHistory(branchesMap map[string]string, activeBranchCount int, inactiveBranchCount int, noColor bool) { 220 | fmt.Println("Branch analysis:") 221 | fmt.Println("\nBranches:") 222 | for branchName, branchStatus := range branchesMap { 223 | fmt.Printf("%s: ", branchName) 224 | statusColor := color.New(color.FgRed) 225 | if branchStatus == Active { 226 | statusColor = color.New(color.FgGreen) 227 | } 228 | if noColor { 229 | fmt.Printf("%s\n", branchStatus) 230 | } else { 231 | statusColor.Printf("%s\n", branchStatus) 232 | } 233 | } 234 | fmt.Printf("\nActive branches: %d\n", activeBranchCount) 235 | fmt.Printf("Inactive branches: %d\n", inactiveBranchCount) 236 | } 237 | -------------------------------------------------------------------------------- /pkg/analyzer/commit_history_test.go: -------------------------------------------------------------------------------- 1 | package analyzer 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "testing" 7 | 8 | "time" 9 | 10 | "github.com/go-git/go-billy/v5/memfs" 11 | "github.com/go-git/go-billy/v5/util" 12 | "github.com/go-git/go-git/v5" 13 | "github.com/go-git/go-git/v5/plumbing" 14 | "github.com/go-git/go-git/v5/plumbing/object" 15 | "github.com/go-git/go-git/v5/storage/memory" 16 | ) 17 | 18 | func Example_printCommitHistoryAnalysis() { 19 | commitCount := 5 20 | authorCommits := []authorCommit{ 21 | {"Alice", 3}, 22 | {"Bob", 2}, 23 | } 24 | 25 | printCommitHistoryAnalysis(commitCount, authorCommits) 26 | 27 | // Output: 28 | // Commit history analysis: 29 | // 30 | // Total number of commits: 5 31 | // 32 | // Number of commits by each author (in decreasing order): 33 | // Alice: 3 commits 34 | // Bob: 2 commits 35 | } 36 | 37 | func Example_printBranchHistory() { 38 | branchesMap := map[string]string{ 39 | "main": Active, 40 | } 41 | activeBranchCount := 1 42 | inactiveBranchCount := 0 43 | 44 | printBranchHistory(branchesMap, activeBranchCount, inactiveBranchCount, true) 45 | 46 | // Output: 47 | // 48 | // Branch analysis: 49 | // 50 | // Branches: 51 | // main: Active 52 | // 53 | // Active branches: 1 54 | // Inactive branches: 0 55 | } 56 | 57 | func Example_commitHistory() { 58 | // Create a new in-memory repository 59 | fs := memfs.New() 60 | repo, err := git.Init(memory.NewStorage(), fs) 61 | if err != nil { 62 | fmt.Printf("Failed to initialize in-memory repository: %v\n", err) 63 | return 64 | } 65 | _, err = createCommit(repo, time.Now()) 66 | if err != nil { 67 | fmt.Printf("Failed to create commit for tests: %v\n", err) 68 | return 69 | } 70 | 71 | commitHistory(repo) 72 | 73 | // Output: 74 | // Commit history analysis: 75 | // 76 | // Total number of commits: 1 77 | // 78 | // Number of commits by each author (in decreasing order): 79 | // Test Author: 1 commits 80 | } 81 | 82 | func TestListBranches(t *testing.T) { 83 | // Create a new in-memory repository 84 | fs := memfs.New() 85 | repo, err := git.Init(memory.NewStorage(), fs) 86 | if err != nil { 87 | t.Fatalf("Failed to initialize in-memory repository: %v", err) 88 | } 89 | expectedBranches := map[string]string{ 90 | "master": "Active", 91 | "foo": "Inactive", 92 | } 93 | // Create a new commit in master branch 94 | lastCommit, err := createCommit(repo, time.Now()) 95 | if err != nil { 96 | t.Fatalf("Failed to create commit for tests: %v", err) 97 | } 98 | 99 | wt, err := repo.Worktree() 100 | if err != nil { 101 | t.Fatalf("Failed to get worktree: %v", err) 102 | } 103 | 104 | targetBranch := plumbing.NewBranchReferenceName("foo") 105 | err = wt.Checkout(&git.CheckoutOptions{ 106 | Hash: lastCommit, 107 | Create: true, 108 | Branch: targetBranch, 109 | }) 110 | if err != nil { 111 | t.Fatalf("Failed to create and checkout branch: %v", err) 112 | } 113 | 114 | // Create a new commit in foo branch 115 | _, err = createCommit(repo, time.Now().Add(-91*24*time.Hour)) 116 | if err != nil { 117 | t.Fatalf("Failed to create commit for tests: %v", err) 118 | } 119 | 120 | branchesMap, activeBranchCount, inactiveBranchCount, err := getBranchCounts(repo) 121 | if err != nil { 122 | t.Fatalf("Failed to get branches for tests: %v", err) 123 | } 124 | 125 | // Check local branches 126 | if len(branchesMap) != 2 { 127 | t.Errorf("Expected 2 local branch , got %v", branchesMap) 128 | } 129 | if activeBranchCount != 1 { 130 | t.Errorf("Expected 1 active branch , got %v", activeBranchCount) 131 | } 132 | if inactiveBranchCount != 1 { 133 | t.Errorf("Expected 1 inactive branch , got %v", inactiveBranchCount) 134 | } 135 | if !reflect.DeepEqual(branchesMap, expectedBranches) { 136 | t.Errorf("Expected branch %v, got %v", expectedBranches, branchesMap) 137 | } 138 | } 139 | 140 | func createCommit(repo *git.Repository, when time.Time) (plumbing.Hash, error) { 141 | wt, err := repo.Worktree() 142 | if err != nil { 143 | return plumbing.ZeroHash, fmt.Errorf("Failed to get worktree: %w", err) 144 | } 145 | util.WriteFile(wt.Filesystem, "foo", []byte("foo"), 0644) 146 | _, err = wt.Add("foo") 147 | if err != nil { 148 | return plumbing.ZeroHash, fmt.Errorf("Failed to add file: %w", err) 149 | } 150 | 151 | h, err := wt.Commit("Initial commit ", &git.CommitOptions{ 152 | Author: &object.Signature{ 153 | Name: "Test Author", 154 | Email: "test@example.com", 155 | When: when, 156 | }, 157 | }) 158 | if err != nil { 159 | return plumbing.ZeroHash, fmt.Errorf("Failed to create commit: %w", err) 160 | } 161 | return h, nil 162 | } 163 | 164 | func TestCountCommits(t *testing.T) { 165 | // Create a new in-memory repository 166 | fs := memfs.New() 167 | repo, err := git.Init(memory.NewStorage(), fs) 168 | if err != nil { 169 | t.Fatalf("Failed to initialize in-memory repository: %v", err) 170 | } 171 | 172 | _, err = createCommit(repo, time.Now()) 173 | if err != nil { 174 | t.Fatalf("Failed to create commit for tests: %v", err) 175 | } 176 | 177 | // Count commits 178 | commitsPerAuthor, totalCommits, err := getCommitCounts(repo) 179 | if err != nil { 180 | t.Fatalf("Failed to count commits: %v", err) 181 | } 182 | 183 | // Check total commits 184 | if totalCommits != 1 { 185 | t.Errorf("Expected 1 commit, got %d", totalCommits) 186 | } 187 | 188 | if len(commitsPerAuthor) != 1 { 189 | t.Errorf("Expected 1 author, got %v", len(commitsPerAuthor)) 190 | } 191 | // Check commits per author 192 | if commitsPerAuthor["Test Author"] != 1 { 193 | t.Errorf("Expected 1 commit for 'Test Author', got %v", commitsPerAuthor) 194 | } 195 | } 196 | 197 | func TestGetSortedAuthorCommits(t *testing.T) { 198 | tests := []struct { 199 | name string 200 | commitCounts map[string]int 201 | expected []authorCommit 202 | }{ 203 | { 204 | name: "basic test", 205 | commitCounts: map[string]int{ 206 | "Alice": 5, 207 | "Bob": 3, 208 | "Carol": 8, 209 | }, 210 | expected: []authorCommit{ 211 | {Author: "Carol", Count: 8}, 212 | {Author: "Alice", Count: 5}, 213 | {Author: "Bob", Count: 3}, 214 | }, 215 | }, 216 | { 217 | name: "empty map", 218 | commitCounts: map[string]int{}, 219 | expected: nil, 220 | }, 221 | { 222 | name: "single author", 223 | commitCounts: map[string]int{ 224 | "Alice": 5, 225 | }, 226 | expected: []authorCommit{ 227 | {Author: "Alice", Count: 5}, 228 | }, 229 | }, 230 | } 231 | 232 | for _, tt := range tests { 233 | t.Run(tt.name, func(t *testing.T) { 234 | result := getSortedAuthorCommits(tt.commitCounts) 235 | if !reflect.DeepEqual(result, tt.expected) { 236 | t.Errorf("got %v, want %v", result, tt.expected) 237 | } 238 | }) 239 | } 240 | } 241 | 242 | func Example_printCommitSize() { 243 | printCommitSize(5, 250) 244 | // Output: 245 | // Total number of commits: 5 246 | // Total commit message size: 250 bytes 247 | // Average commit size: 50.00 bytes 248 | } 249 | 250 | func Example_commitSize() { 251 | // Create a new in-memory repository 252 | fs := memfs.New() 253 | repo, err := git.Init(memory.NewStorage(), fs) 254 | if err != nil { 255 | fmt.Printf("Failed to initialize in-memory repository: %v\n", err) 256 | return 257 | } 258 | 259 | _, err = createCommit(repo, time.Now()) 260 | if err != nil { 261 | fmt.Printf("Failed to create commit for tests: %v\n", err) 262 | return 263 | } 264 | commitSize(repo) 265 | 266 | // Output: 267 | // Total number of commits: 1 268 | // Total commit message size: 15 bytes 269 | // Average commit size: 15.00 bytes 270 | } 271 | 272 | func TestCommitStats(t *testing.T) { 273 | // Create a new in-memory repository 274 | fs := memfs.New() 275 | repo, err := git.Init(memory.NewStorage(), fs) 276 | if err != nil { 277 | t.Fatalf("Failed to initialize in-memory repository: %v", err) 278 | } 279 | 280 | _, err = createCommit(repo, time.Now()) 281 | if err != nil { 282 | t.Fatalf("Failed to create commit for tests: %v", err) 283 | } 284 | 285 | _, err = createCommit(repo, time.Now()) 286 | if err != nil { 287 | t.Fatalf("Failed to create commit for tests: %v", err) 288 | } 289 | 290 | commitCount, totalSize, err := getCommitStats(repo) 291 | if err != nil { 292 | t.Errorf("Expected nil Error, got: %v", err) 293 | } 294 | // Check total commits 295 | if commitCount != 2 { 296 | t.Errorf("Expected 2 commits, got %d", commitCount) 297 | } 298 | 299 | // Check total commits size 300 | if totalSize != 30 { 301 | t.Errorf("Expected size 30, got %d", totalSize) 302 | } 303 | } 304 | 305 | func Example_branchStats() { 306 | // Create a new in-memory repository 307 | fs := memfs.New() 308 | repo, err := git.Init(memory.NewStorage(), fs) 309 | if err != nil { 310 | fmt.Printf("Failed to initialize in-memory repository: %v\n", err) 311 | return 312 | } 313 | 314 | _, err = createCommit(repo, time.Now()) 315 | if err != nil { 316 | fmt.Printf("Failed to create commit for tests: %v\n", err) 317 | return 318 | } 319 | branchStats(repo, true) 320 | 321 | // Output: 322 | // Branch analysis: 323 | // 324 | // Branches: 325 | // master: Active 326 | // 327 | // Active branches: 1 328 | // Inactive branches: 0 329 | } 330 | -------------------------------------------------------------------------------- /pkg/format/repository.go: -------------------------------------------------------------------------------- 1 | package format 2 | 3 | import "strings" 4 | 5 | func IsRepositoryValid(name string) bool { 6 | if len(name) == 0 { 7 | return false 8 | } 9 | 10 | parts := strings.Split(name, "/") 11 | 12 | return len(parts) == 2 13 | } -------------------------------------------------------------------------------- /pkg/utils/get_star.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | type RepositoryResponse struct { 10 | StargazersCount int64 `json:"stargazers_count"` 11 | } 12 | 13 | func GetStarCount(repoName string)(*int64, error){ 14 | response, err := http.Get("https://api.github.com/repos/" + repoName) 15 | 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | body, err := io.ReadAll(response.Body) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | var responseStruct RepositoryResponse 26 | err = json.Unmarshal(body, &responseStruct) 27 | if err != nil { 28 | return nil, err 29 | } 30 | 31 | return &responseStruct.StargazersCount, nil 32 | } --------------------------------------------------------------------------------