Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 77 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,30 @@
<img src="https://res.cloudinary.com/dhyii4oiw/image/upload/v1767324445/Screenshot_2026-01-02_085503_ros5gz.png" alt="Repo-lyzer Logo" width="300">
</p>

<p align="center">
<a href="https://github.com/agnivo988/Repo-lyzer/releases"><img src="https://img.shields.io/github/v/release/agnivo988/Repo-lyzer?style=flat-square" alt="Release"></a>
<a href="https://github.com/agnivo988/Repo-lyzer/blob/main/LICENSE"><img src="https://img.shields.io/github/license/agnivo988/Repo-lyzer?style=flat-square" alt="License"></a>
<a href="https://github.com/agnivo988/Repo-lyzer/issues"><img src="https://img.shields.io/github/issues/agnivo988/Repo-lyzer?style=flat-square" alt="Issues"></a>
<a href="https://github.com/agnivo988/Repo-lyzer/actions"><img src="https://img.shields.io/github/actions/workflow/status/agnivo988/Repo-lyzer/ci.yml?style=flat-square" alt="CI"></a>
</p>

**Repo-lyzer** is a modern, terminal-based CLI tool written in **Golang** that analyzes GitHub repositories and presents insights in a beautifully formatted, interactive dashboard.

---

## Table of Contents
- [Features](#features)
- [Quick Start](#quick-start)
- [Installation](#installation)
- [Basic Usage](#basic-usage)
- [Docker Usage](#docker-usage)
- [Architecture Overview](#architecture-overview)
- [Documentation](#documentation)
- [Contributing](#contributing)
- [License](#license)

---

## Features
- **Deep Analytics** – Repository health, maturity scores, and bus factor.
- **Contribution Friendliness** – Assess how easy it is to contribute to a repository using weighted metrics (`--contribute` flag).
Expand All @@ -19,10 +39,18 @@

## Quick Start

### Prerequisites
- **Go 1.21+** (for `go install`) or **Docker** (for containerized usage)
- A [GitHub Personal Access Token](https://github.com/settings/tokens) (required for API calls)

### Installation
```bash
go install [github.com/agnivo988/Repo-lyzer@v1.0.6](https://github.com/agnivo988/Repo-lyzer@v1.0.6)
repo-lyzer
go install github.com/agnivo988/Repo-lyzer@latest
```

Verify the installation:
```bash
repo-lyzer version
```

### Basic Usage
Expand All @@ -33,7 +61,6 @@ repo-lyzer summary golang/go
# Run full interactive analysis
repo-lyzer analyze microsoft/vscode

# Run analysis with contribution scoring enabled
# Run analysis with contribution scoring enabled
repo-lyzer analyze microsoft/vscode --contribute
```
Expand Down Expand Up @@ -113,14 +140,57 @@ The `docker-compose.yml` mounts a local `./data` directory to persist settings a

---

## Maintainers & Contributors
### Maintainer: @agnivo988
## Contributing

We welcome contributions from the community! Here's how to get started:

1. **Fork** the repository
2. **Clone** your fork:
```bash
git clone https://github.com/your-username/Repo-lyzer.git
```
3. **Create a feature branch**:
```bash
git checkout -b feat/your-feature-name
```
4. **Make your changes** and ensure tests pass:
```bash
go test ./...
```
5. **Commit** with a clear message:
```bash
git commit -m "feat: add your feature"
```
6. **Push** and open a Pull Request

### Development Setup
```bash
# Install dependencies
go mod download

# Run tests
go test ./...

# Build from source
go build -o repo-lyzer .
```

### Code Style
- Run `go fmt ./...` before committing
- Follow standard Go conventions
- Add tests for new functionality

<a href="https://github.com/Aamod007"><img src="https://github.com/Aamod007.png" width="50" height="50" alt="Aamod007" title="Contributor"></a> <a href="https://github.com/Aditya8369"><img src="https://github.com/Aditya8369.png" width="50" height="50" alt="Aditya8369" title="Contributor"></a> <a href="https://github.com/agnivo988"><img src="https://github.com/agnivo988.png" width="50" height="50" alt="agnivo988" title="Project Maintainer"></a> <a href="https://github.com/Gupta-02"><img src="https://github.com/Gupta-02.png" width="50" height="50" alt="Gupta-02" title="Contributor"></a> <a href="https://github.com/GauravKarakoti"><img src="https://github.com/GauravKarakoti.png" width="50" height="50" alt="GauravKarakoti" title="Contributor"></a> <a href="https://github.com/Sappymukherjee214"><img src="https://github.com/Sappymukherjee214.png" width="50" height="50" alt="Sappymukherjee214" title="Contributor"></a> <a href="https://github.com/ItsMeArm00n"><img src="https://github.com/ItsMeArm00n.png" width="50" height="50" alt="ItsMeArm00n" title="Contributor"></a> <a href="https://github.com/MuktaRedij"><img src="https://github.com/MuktaRedij.png" width="50" height="50" alt="MuktaRedij" title="Contributor"></a> <a href="https://github.com/Kiran95021"><img src="https://github.com/Kiran95021.png" width="50" height="50" alt="Kiran95021" title="Contributor"></a> <a href="https://github.com/Shriii19"><img src="https://github.com/Shriii19.png" width="50" height="50" alt="Shriii19" title="Contributor"></a> <a href="https://github.com/KUMARI-SONALIUPADHYAY"><img src="https://github.com/KUMARI-SONALIUPADHYAY.png" width="50" height="50" alt="KUMARI-SONALIUPADHYAY" title="Contributor"></a> <a href="https://github.com/magic-peach"><img src="https://github.com/magic-peach.png" width="50" height="50" alt="magic-peach" title="Contributor"></a> <a href="https://github.com/coderabbitai"><img src="https://github.com/coderabbitai.png" width="50" height="50" alt="coderabbitai[bot]" title="Bot Contributor"></a> <a href="https://github.com/sahoo-tech"><img src="https://github.com/sahoo-tech.png" width="50" height="50" alt="sahoo-tech" title="Contributor"></a> <a href="https://github.com/Abhijeet-980"><img src="https://github.com/Abhijeet-980.png" width="50" height="50" alt="Abhijeet-980" title="Contributor"></a> <a href="https://github.com/Diksha78-bot"><img src="https://github.com/Diksha78-bot.png" width="50" height="50" alt="Diksha78-bot" title="Bot Contributor"></a> <a href="https://github.com/Shivani-Meena07"><img src="https://github.com/Shivani-Meena07.png" width="50" height="50" alt="Shivani-Meena07" title="Contributor"></a> <a href="https://github.com/ShashankSaga"><img src="https://github.com/ShashankSaga.png" width="50" height="50" alt="ShashankSaga" title="Contributor"></a>
---

## Maintainers & Contributors

### Project Maintainer
- [@agnivo988](https://github.com/agnivo988)

### Contributors
<a href="https://github.com/Aamod007"><img src="https://github.com/Aamod007.png" width="50" height="50" alt="Aamod007" title="Contributor"></a> <a href="https://github.com/Aditya8369"><img src="https://github.com/Aditya8369.png" width="50" height="50" alt="Aditya8369" title="Contributor"></a> <a href="https://github.com/agnivo988"><img src="https://github.com/agnivo988.png" width="50" height="50" alt="agnivo988" title="Project Maintainer"></a> <a href="https://github.com/Gupta-02"><img src="https://github.com/Gupta-02.png" width="50" height="50" alt="Gupta-02" title="Contributor"></a> <a href="https://github.com/GauravKarakoti"><img src="https://github.com/GauravKarakoti.png" width="50" height="50" alt="GauravKarakoti" title="Contributor"></a> <a href="https://github.com/Sappymukherjee214"><img src="https://github.com/Sappymukherjee214.png" width="50" height="50" alt="Sappymukherjee214" title="Contributor"></a> <a href="https://github.com/ItsMeArm00n"><img src="https://github.com/ItsMeArm00n.png" width="50" height="50" alt="ItsMeArm00n" title="Contributor"></a> <a href="https://github.com/MuktaRedij"><img src="https://github.com/MuktaRedij.png" width="50" height="50" alt="MuktaRedij" title="Contributor"></a> <a href="https://github.com/Kiran95021"><img src="https://github.com/Kiran95021.png" width="50" height="50" alt="Kiran95021" title="Contributor"></a> <a href="https://github.com/Shriii19"><img src="https://github.com/Shriii19.png" width="50" height="50" alt="Shriii19" title="Contributor"></a> <a href="https://github.com/KUMARI-SONALIUPADHYAY"><img src="https://github.com/KUMARI-SONALIUPADHYAY.png" width="50" height="50" alt="KUMARI-SONALIUPADHYAY" title="Contributor"></a> <a href="https://github.com/magic-peach"><img src="https://github.com/magic-peach.png" width="50" height="50" alt="magic-peach" title="Contributor"></a> <a href="https://github.com/coderabbitai"><img src="https://github.com/coderabbitai.png" width="50" height="50" alt="coderabbitai[bot]" title="coderabbitai[bot]"></a>

---

## License
**MIT License © 2026 Agniva Mukherjee**
**MIT License © 2026 Agniva Mukherjee**. See [LICENSE](LICENSE) for details.
5 changes: 4 additions & 1 deletion cmd/analyze.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,14 +315,17 @@ var analyzeCmd = &cobra.Command{
busFactor, busRisk := analyzer.BusFactor(contributors)
overallProgress.CompleteStep("Bus factor analysis complete")

// Check releases
hasReleases, _ := client.HasReleases(owner, repo)

// Calculate repository maturity score and level
overallProgress.StartStep("📈 Calculating repository maturity")
maturityScore, maturityLevel :=
analyzer.RepoMaturityScore(
repoInfo,
len(commits),
len(contributors),
false, // Assuming no releases check for simplicity
hasReleases,
)
overallProgress.CompleteStep("Maturity score calculated")

Expand Down
6 changes: 6 additions & 0 deletions cmd/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,17 @@ func fetchCompareInput(client *github.Client, owner, repo string) (output.Compar
return output.CompareInput{}, fmt.Errorf("error fetching contributors for %s/%s: %w", owner, repo, err)
}

hasReleases, err := client.HasReleases(owner, repo)
if err != nil {
return output.CompareInput{}, fmt.Errorf("error checking releases for %s/%s: %w", owner, repo, err)
}

return output.CompareInput{
Repo: repoInfo,
Commits: commits,
Contributors: contributors,
Languages: languages,
HasReleases: hasReleases,
}, nil
}

Expand Down
5 changes: 4 additions & 1 deletion internal/analyzer/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,12 @@ func GenerateCertificate(owner, repo string, client *github.Client) (*Certificat
return nil, fmt.Errorf("failed to get contributors: %w", err)
}

// Check releases
hasReleases, _ := client.HasReleases(owner, repo)

// Calculate scores
healthScore := CalculateHealth(repoInfo, commits)
maturityScore, maturityLevel := RepoMaturityScore(repoInfo, len(commits), len(contributors), false)
maturityScore, maturityLevel := RepoMaturityScore(repoInfo, len(commits), len(contributors), hasReleases)
busFactor, busRisk := BusFactor(contributors)

// Determine primary language
Expand Down
18 changes: 15 additions & 3 deletions internal/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ type Cache struct {
func DefaultConfig() CacheConfig {
return CacheConfig{
Enabled: true,
TTL: 24 * time.Hour,
TTL: clampTTL(24 * time.Hour),
MaxSize: 100,
AutoCache: true,
}
Expand Down Expand Up @@ -174,9 +174,10 @@ func (c *Cache) loadConfig() {
configPath := filepath.Join(c.cacheDir, "config.json")
data, err := os.ReadFile(configPath)
if err != nil {
return // Use defaults
return
}
json.Unmarshal(data, &c.config)
c.config.TTL = clampTTL(c.config.TTL)
}

// SaveConfig saves cache configuration
Expand Down Expand Up @@ -420,10 +421,21 @@ func (c *Cache) SetEnabled(enabled bool) {

// SetTTL sets the cache time-to-live
func (c *Cache) SetTTL(ttl time.Duration) {
c.config.TTL = ttl
c.config.TTL = clampTTL(ttl)
c.SaveConfig()
}

// clampTTL ensures TTL stays within reasonable bounds: 1 minute to 7 days.
func clampTTL(ttl time.Duration) time.Duration {
if ttl < time.Minute {
return time.Minute
}
if ttl > 7*24*time.Hour {
return 7 * 24 * time.Hour
}
return ttl
}

// SetAutoCache enables or disables auto-caching
func (c *Cache) SetAutoCache(enabled bool) {
c.config.AutoCache = enabled
Expand Down
38 changes: 38 additions & 0 deletions internal/github/releases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package github

import (
"fmt"

gocache "github.com/patrickmn/go-cache"
)

func (c *Client) HasReleases(owner, repo string) (bool, error) {
cacheKey := "releases:" + owner + "/" + repo
if cached, found := c.cache.Get(cacheKey); found {
return cached.(bool), nil
}

v, err, _ := c.sf.Do(cacheKey, func() (interface{}, error) {
if cached, found := c.cache.Get(cacheKey); found {
return cached.(bool), nil
}

url := fmt.Sprintf(
"https://api.github.com/repos/%s/%s/releases?per_page=1&page=1",
owner, repo,
)

var releases []struct{}
if err := c.get(url, &releases); err != nil {
return false, err
}

hasReleases := len(releases) > 0
c.cache.Set(cacheKey, hasReleases, gocache.DefaultExpiration)
return hasReleases, nil
})
if err != nil {
return false, err
}
return v.(bool), nil
}
3 changes: 2 additions & 1 deletion internal/output/compare.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type CompareInput struct {
Commits []github.Commit
Contributors []github.Contributor
Languages map[string]int
HasReleases bool
}

// CompareRepository contains the computed metrics for one repository.
Expand Down Expand Up @@ -179,7 +180,7 @@ func buildCompareRepository(input CompareInput) CompareRepository {
}

busFactor, busRisk := analyzer.BusFactor(input.Contributors)
maturityScore, maturityLevel := analyzer.RepoMaturityScore(input.Repo, len(input.Commits), len(input.Contributors), false)
maturityScore, maturityLevel := analyzer.RepoMaturityScore(input.Repo, len(input.Commits), len(input.Contributors), input.HasReleases)
healthScore := analyzer.CalculateHealth(input.Repo, input.Commits)

primaryLanguage := input.Repo.Language
Expand Down
5 changes: 4 additions & 1 deletion internal/scheduler/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,13 @@ func (s *Scheduler) executeJob(job config.ScheduledJob) error {
return fmt.Errorf("failed to get contributors: %w", err)
}

// Check releases
hasReleases, _ := client.HasReleases(job.Owner, job.Repo)

// Calculate metrics
healthScore := analyzer.CalculateHealth(repoInfo, commits)
busFactor, busRisk := analyzer.BusFactor(contributors)
maturityScore, maturityLevel := analyzer.RepoMaturityScore(repoInfo, len(commits), len(contributors), false)
maturityScore, maturityLevel := analyzer.RepoMaturityScore(repoInfo, len(commits), len(contributors), hasReleases)

// Build compact config for export
compactCfg := output.CompactConfig{
Expand Down
12 changes: 9 additions & 3 deletions internal/ui/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -1058,7 +1058,11 @@ func (m MainModel) analyzeRepo(ctx context.Context, repoName string) tea.Cmd {
// Stage 5: Compute metrics
score := analyzer.CalculateHealth(repo, commits)
busFactor, busRisk := analyzer.BusFactor(contributors)
maturityScore, maturityLevel := analyzer.RepoMaturityScore(repo, len(commits), len(contributors), false)
hasReleases, hasRelErr := client.HasReleases(parts[0], parts[1])
if hasRelErr != nil {
hasReleases = false
}
maturityScore, maturityLevel := analyzer.RepoMaturityScore(repo, len(commits), len(contributors), hasReleases)
contributorInsights := analyzer.AnalyzeContributors(contributors)

commitsLast90Days := 0
Expand Down Expand Up @@ -1426,7 +1430,8 @@ func (m MainModel) compareRepos(repo1Name, repo2Name string) tea.Cmd {
fileTree1, _ := client.GetFileTree(parts1[0], parts1[1], repo1.DefaultBranch)
score1 := analyzer.CalculateHealth(repo1, commits1)
busFactor1, busRisk1 := analyzer.BusFactor(contributors1)
maturityScore1, maturityLevel1 := analyzer.RepoMaturityScore(repo1, len(commits1), len(contributors1), false)
hasReleases1, _ := client.HasReleases(parts1[0], parts1[1])
maturityScore1, maturityLevel1 := analyzer.RepoMaturityScore(repo1, len(commits1), len(contributors1), hasReleases1)

result1 := AnalysisResult{
Repo: repo1,
Expand All @@ -1452,7 +1457,8 @@ func (m MainModel) compareRepos(repo1Name, repo2Name string) tea.Cmd {
fileTree2, _ := client.GetFileTree(parts2[0], parts2[1], repo2.DefaultBranch)
score2 := analyzer.CalculateHealth(repo2, commits2)
busFactor2, busRisk2 := analyzer.BusFactor(contributors2)
maturityScore2, maturityLevel2 := analyzer.RepoMaturityScore(repo2, len(commits2), len(contributors2), false)
hasReleases2, _ := client.HasReleases(parts2[0], parts2[1])
maturityScore2, maturityLevel2 := analyzer.RepoMaturityScore(repo2, len(commits2), len(contributors2), hasReleases2)

result2 := AnalysisResult{
Repo: repo2,
Expand Down
Loading