diff --git a/.github/workflows/branch-analysis.yml b/.github/workflows/branch-analysis.yml
new file mode 100644
index 0000000..57903cf
--- /dev/null
+++ b/.github/workflows/branch-analysis.yml
@@ -0,0 +1,62 @@
+name: Branch Comparison Analysis and Deployment
+
+on:
+ schedule:
+ - cron: '*/5 * * * *' # Run every 5 minutes
+ workflow_dispatch: # Allow manual trigger
+ push:
+ branches:
+ - release # Run on pushes to release branch
+
+jobs:
+ analyze-and-deploy:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # Needed for pushing to gh-pages
+ pages: write # Needed for GitHub Pages deployment
+ id-token: write # Needed for GitHub Pages deployment
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: '3.10'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install PyGithub
+
+ - name: Configure Git
+ run: |
+ git config user.name "GitHub Actions Bot"
+ git config user.email "actions@github.com"
+
+ - name: Run branch analysis for latest
+ env:
+ GITHUB_TOKEN: ${{ secrets.GH_PAT }}
+ GITHUB_REPO: ${{ github.repository }}
+ MAIN_BRANCH: main
+ RELEASE_BRANCH: release
+ run: |
+ # Run analysis for latest
+ python github_branch_analyzer_with_date.py
+ python analyze_branch_data_with_date.py
+
+ - name: Setup visualization directory
+ run: |
+ # Create visualization directory if it doesn't exist
+ mkdir -p visualization
+
+ # Copy analysis results
+ cp analyzed_branch_data*.json visualization/
+
+ - name: Deploy to GitHub Pages
+ uses: JamesIves/github-pages-deploy-action@v4
+ with:
+ folder: visualization
+ branch: gh-pages
+ clean: true
diff --git a/.gitignore copy b/.gitignore copy
new file mode 100644
index 0000000..4c49bd7
--- /dev/null
+++ b/.gitignore copy
@@ -0,0 +1 @@
+.env
diff --git a/DEPLOYMENT_GUIDE.md b/DEPLOYMENT_GUIDE.md
new file mode 100644
index 0000000..5d7ed3d
--- /dev/null
+++ b/DEPLOYMENT_GUIDE.md
@@ -0,0 +1,168 @@
+# GitHub Branch Analyzer - Deployment Guide
+
+This guide explains how to deploy the GitHub Branch Analyzer with date filtering to GitHub Pages using GitHub Actions.
+
+## Overview
+
+This deployment method will:
+1. Automatically run branch analysis on a schedule (daily by default)
+2. Generate visualizations for multiple dates
+3. Deploy the results to GitHub Pages
+4. Make the visualization accessible via a public URL
+
+## Prerequisites
+
+- A GitHub repository where you want to deploy the visualization
+- Admin access to the repository to configure GitHub Pages and secrets
+- A GitHub Personal Access Token (PAT) with appropriate permissions
+
+## Step 1: Set Up Your Repository
+
+1. Create a new repository or use an existing one
+2. Clone this repository to your local machine:
+ ```bash
+ git clone https://github.com/yourusername/your-repository.git
+ cd your-repository
+ ```
+
+3. Copy all the files from the GitHub Branch Analyzer into your repository:
+ - `.github/workflows/branch-analysis.yml`
+ - `_config.yml`
+ - `github_branch_analyzer_for_actions.py`
+ - `analyze_branch_data_for_actions.py`
+ - All files from the `git-branch-comparison-website` directory
+
+## Step 2: Create a Personal Access Token
+
+1. Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
+2. Click "Generate new token"
+3. Give it a name like "Branch Analyzer Deployment"
+4. Set the expiration as needed (e.g., 1 year)
+5. Select the repository you want to analyze
+6. Under "Repository permissions", grant:
+ - Contents: Read and write
+ - Pull requests: Read
+ - Metadata: Read
+ - Pages: Read and write
+7. Click "Generate token" and copy the token value
+
+## Step 3: Configure Repository Secrets
+
+1. Go to your repository on GitHub
+2. Click on "Settings" → "Secrets and variables" → "Actions"
+3. Click "New repository secret"
+4. Add a secret with name `GH_PAT` and paste your Personal Access Token as the value
+5. Click "Add secret"
+
+## Step 4: Configure GitHub Pages
+
+1. Go to your repository on GitHub
+2. Click on "Settings" → "Pages"
+3. Under "Source", select "GitHub Actions" as the build and deployment source
+4. This will allow the workflow to handle the deployment
+
+## Step 5: Customize the Workflow (Optional)
+
+You can customize the workflow by editing `.github/workflows/branch-analysis.yml`:
+
+1. Change the schedule:
+ ```yaml
+ schedule:
+ - cron: '0 0 * * *' # Default: Run daily at midnight
+ ```
+
+2. Modify the branches to analyze:
+ ```yaml
+ env:
+ MAIN_BRANCH: main # Change to your main branch name
+ RELEASE_BRANCH: release # Change to your release branch name
+ ```
+
+3. Adjust the number of historical dates:
+ ```yaml
+ # Run analysis for past 7 days
+ for i in {1..7}; do # Change 7 to your desired number of days
+ ```
+
+## Step 6: Push Changes and Trigger the Workflow
+
+1. Commit and push all changes to your repository:
+ ```bash
+ git add .
+ git commit -m "Add GitHub Branch Analyzer with GitHub Actions deployment"
+ git push
+ ```
+
+2. This will automatically trigger the workflow for the first time
+
+3. You can also manually trigger the workflow:
+ - Go to your repository on GitHub
+ - Click on "Actions"
+ - Select the "Branch Comparison Analysis and Deployment" workflow
+ - Click "Run workflow"
+
+## Step 7: Access Your Deployed Visualization
+
+1. After the workflow completes successfully:
+ - Go to your repository on GitHub
+ - Click on "Settings" → "Pages"
+ - You'll see a message like "Your site is published at https://yourusername.github.io/your-repository/"
+
+2. Visit the URL to access your branch comparison visualization
+
+3. The visualization will be automatically updated according to your workflow schedule
+
+## Troubleshooting
+
+### Workflow Failures
+
+If the workflow fails:
+1. Go to "Actions" in your repository
+2. Click on the failed workflow run
+3. Examine the logs to identify the issue
+
+Common issues include:
+- Invalid Personal Access Token
+- Insufficient permissions
+- Repository not found
+- Branch names don't exist in your repository
+
+### GitHub Pages Not Deploying
+
+If GitHub Pages doesn't deploy:
+1. Verify that GitHub Pages is enabled for your repository
+2. Check that the workflow has the correct permissions
+3. Ensure the `gh-pages` branch was created by the workflow
+
+### Visualization Not Showing Data
+
+If the visualization loads but doesn't show data:
+1. Check that the analysis scripts ran successfully
+2. Verify that JSON files were generated and copied to the visualization directory
+3. Check browser console for JavaScript errors
+
+## Advanced Configuration
+
+### Custom Domain
+
+To use a custom domain:
+1. Go to "Settings" → "Pages"
+2. Under "Custom domain", enter your domain
+3. Update DNS settings as instructed
+
+### Additional Analysis Dates
+
+To analyze more historical dates:
+1. Edit the workflow file
+2. Modify the loop that generates historical dates
+3. Consider performance implications for very large repositories
+
+### Automatic Updates
+
+The visualization will automatically update based on your workflow schedule. To change this:
+1. Edit the `schedule` section in the workflow file
+2. Use cron syntax to define your preferred schedule
+
+## Conclusion
+
+Your GitHub Branch Analyzer is now deployed to GitHub Pages and will automatically update according to your schedule. The visualization provides insights into your branch merging patterns over time, helping you understand your development workflow better.
diff --git a/README copy.md b/README copy.md
new file mode 100644
index 0000000..56e5a23
--- /dev/null
+++ b/README copy.md
@@ -0,0 +1,85 @@
+# GitHub Branch Analyzer with Date Filtering
+
+A tool to compare branches in GitHub repositories at specific points in time, visualizing which feature branches were merged into main and release branches.
+
+## Features
+
+- **Date-Based Analysis**: Compare branches as they existed on any specific date
+- **Interactive Visualization**: See branch relationships through an interactive Venn diagram
+- **Timeline View**: Track how branches evolved over time with a visual timeline
+- **Branch Details**: Get detailed information about each merged branch
+- **Search & Filter**: Easily find specific branches across your repository
+- **History Navigation**: Jump between different analysis dates to see changes
+- **GitHub Pages Deployment**: Automatically deploy to GitHub Pages using GitHub Actions
+
+## Repository Structure
+
+```
+.
+├── .github/workflows/ # GitHub Actions workflow files
+│ └── branch-analysis.yml # Workflow for branch analysis and deployment
+├── _config.yml # GitHub Pages configuration
+├── github_branch_analyzer_for_actions.py # Script to analyze GitHub branches
+├── analyze_branch_data_for_actions.py # Script to process branch data
+├── visualization/ # Web visualization files (copied during deployment)
+├── DEPLOYMENT_GUIDE.md # Guide for deploying to GitHub Pages
+└── README.md # This file
+```
+
+## Quick Start
+
+1. **Fork or clone this repository**
+
+2. **Create a GitHub Personal Access Token**
+ - Go to GitHub → Settings → Developer settings → Personal access tokens
+ - Create a token with `repo` permissions
+
+3. **Add the token as a repository secret**
+ - Go to your repository → Settings → Secrets → Actions
+ - Add a secret named `GH_PAT` with your token as the value
+
+4. **Configure GitHub Pages**
+ - Go to your repository → Settings → Pages
+ - Set source to "GitHub Actions"
+
+5. **Trigger the workflow**
+ - Go to Actions → "Branch Comparison Analysis and Deployment" → Run workflow
+
+6. **Access your visualization**
+ - Once the workflow completes, your visualization will be available at:
+ - `https://[your-username].github.io/[repository-name]/`
+
+## Configuration
+
+You can customize the analysis by editing the workflow file:
+
+- Change the branches to analyze (default: main and release)
+- Modify the analysis schedule (default: daily at midnight)
+- Adjust the number of historical dates to analyze
+
+## Local Development
+
+To run the analysis locally:
+
+```bash
+# Install dependencies
+pip install PyGithub
+
+# Run the analysis
+export GITHUB_TOKEN=your_token
+export GITHUB_REPO=owner/repo
+python github_branch_analyzer_for_actions.py
+python analyze_branch_data_for_actions.py
+```
+
+## Documentation
+
+For detailed deployment instructions, see [DEPLOYMENT_GUIDE.md](DEPLOYMENT_GUIDE.md).
+
+## License
+
+MIT
+
+## Acknowledgements
+
+This tool was created to help development teams better understand their branch merging patterns and improve their release processes.
diff --git a/README.md b/README.md
index 05d6866..b0e849b 100644
--- a/README.md
+++ b/README.md
@@ -13,4 +13,8 @@ If you are developing a production application, we recommend using TypeScript an
-hello all
\ No newline at end of file
+hello all
+
+add sites-41
+
+add feature/sites-42
\ No newline at end of file
diff --git a/USER_GUIDE.md b/USER_GUIDE.md
new file mode 100644
index 0000000..ad30852
--- /dev/null
+++ b/USER_GUIDE.md
@@ -0,0 +1,152 @@
+# GitHub Branch Analyzer - User Guide
+
+This guide will walk you through the process of setting up and using the GitHub Branch Analyzer to visualize branch comparisons from your private GitHub repositories.
+
+## Overview
+
+The GitHub Branch Analyzer allows you to:
+1. Connect to your private GitHub repositories using a Personal Access Token
+2. Extract information about branches merged via pull requests
+3. Compare branches (typically "main" and "release") to see which feature branches were merged where
+4. Visualize the comparison with an interactive web interface
+
+## Prerequisites
+
+- A GitHub account with access to the private repository you want to analyze
+- A Personal Access Token (PAT) with appropriate permissions
+- Basic familiarity with command line operations
+
+## Step 1: Set Up GitHub API Access
+
+1. **Create a Personal Access Token (PAT)**:
+ - Go to GitHub → Settings → Developer settings → Personal access tokens → Fine-grained tokens
+ - Click "Generate new token"
+ - Give it a name like "Branch Comparison Tool"
+ - Set the expiration as needed
+ - Select the specific repository you want to analyze
+ - Under "Repository permissions", grant at least "Read access" to:
+ - Contents
+ - Pull requests
+ - Metadata
+ - Click "Generate token"
+ - **IMPORTANT**: Copy the generated token immediately and store it securely. You won't be able to see it again!
+
+## Step 2: Configure the Branch Analyzer
+
+1. **Run the configuration script**:
+ ```bash
+ cd /path/to/github-branch-analyzer
+ ./configure.sh
+ ```
+
+2. **Enter the requested information**:
+ - GitHub Personal Access Token (the one you created in Step 1)
+ - Repository name in the format "owner/repo-name" (e.g., "myusername/myproject")
+ - Main branch name (default is "main")
+ - Release branch name (default is "release")
+
+## Step 3: Run the Branch Analysis
+
+1. **Execute the analysis script**:
+ ```bash
+ cd /path/to/github-branch-analyzer
+ ./run.sh
+ ```
+
+2. **Review the analysis output**:
+ - The script will display a summary of the analysis results
+ - The complete results are saved to `analyzed_branch_data.json`
+
+## Step 4: View the Visualization
+
+You can view the branch comparison visualization in two ways:
+
+### Option 1: Use the Deployed Website
+
+Visit the permanently deployed visualization website at:
+[https://jmocizgq.manus.space](https://jmocizgq.manus.space)
+
+To use your own data with this website:
+1. Copy your `analyzed_branch_data.json` file to the same directory as the website
+2. Refresh the page to see your data
+
+### Option 2: Run the Visualization Locally
+
+1. **Copy the analysis results to the website directory**:
+ ```bash
+ cp /path/to/github-branch-analyzer/analyzed_branch_data.json /path/to/git-branch-comparison-website/
+ ```
+
+2. **Start a local web server**:
+ ```bash
+ cd /path/to/git-branch-comparison-website
+ python -m http.server 8000
+ ```
+
+3. **Open the visualization in your browser**:
+ - Navigate to `http://localhost:8000` in your web browser
+
+## Understanding the Visualization
+
+The visualization shows:
+- **Blue Circle**: Branches merged into the main branch
+- **Orange Circle**: Branches merged into the release branch
+- **Overlap**: Branches that appear in both main and release
+
+Interactive features:
+- Hover over branches to see details
+- Click on a branch to see more information
+- Use the search box to find specific branches
+- Use the checkboxes to filter which branches are displayed
+
+## Automating the Process
+
+To automate the branch analysis, you can:
+
+1. **Create a cron job** to run the analysis periodically:
+ ```bash
+ # Example: Run analysis daily at 2 AM
+ 0 2 * * * cd /path/to/github-branch-analyzer && ./run.sh
+ ```
+
+2. **Set up a GitHub Action** to run the analysis on push events or on a schedule
+
+## Troubleshooting
+
+### Common Issues:
+
+1. **Authentication Errors**:
+ - Ensure your Personal Access Token has the correct permissions
+ - Check that the token hasn't expired
+ - Verify you're using the correct token
+
+2. **Repository Not Found**:
+ - Confirm the repository name is in the format "owner/repo-name"
+ - Verify that your GitHub account has access to the repository
+
+3. **No Data Displayed**:
+ - Check that the analysis completed successfully
+ - Verify that the JSON file was copied to the correct location
+ - Ensure the JSON file has the expected format
+
+4. **Visualization Not Loading**:
+ - Check your browser console for JavaScript errors
+ - Ensure all files (HTML, CSS, JS) are in the same directory
+ - Try using a different browser
+
+## Getting Help
+
+If you encounter issues not covered in this guide, please:
+1. Check the GitHub repository for updates or known issues
+2. Contact the developer with specific error messages and steps to reproduce the problem
+
+## Security Considerations
+
+- Your GitHub Personal Access Token provides access to your repositories. Keep it secure!
+- The token is stored in the `.env` file with restricted permissions (readable only by you)
+- Consider using a token with the minimum necessary permissions and a short expiration time
+- Regularly rotate your tokens for enhanced security
+
+
+sebas
+correa
\ No newline at end of file
diff --git a/_config.yml b/_config.yml
new file mode 100644
index 0000000..687e588
--- /dev/null
+++ b/_config.yml
@@ -0,0 +1,43 @@
+# GitHub Pages Configuration
+
+# This file configures GitHub Pages for the branch comparison visualization
+
+# Use Jekyll to process the site
+# Set to false if you're not using Jekyll (recommended for this project)
+lsi: false
+safe: true
+incremental: false
+highlighter: rouge
+gist:
+ noscript: false
+kramdown:
+ math_engine: mathjax
+ syntax_highlighter: rouge
+
+# Build settings
+# Use the docs folder for GitHub Pages
+source: ./
+destination: ./_site
+
+# Exclude these files from the build
+exclude:
+ - Gemfile
+ - Gemfile.lock
+ - node_modules
+ - vendor
+ - .github
+ - README.md
+ - LICENSE
+ - .gitignore
+ - "*.py"
+ - "*.sh"
+ - "*.env"
+
+# Site settings
+title: Git Branch Comparison
+description: Visualizing differences between main and release branches with date filtering
+baseurl: "" # the subpath of your site, e.g. /blog
+url: "" # the base hostname & protocol for your site, e.g. http://example.com
+
+# Custom settings for this project
+permalink: pretty
diff --git a/analyze_branch_data.py b/analyze_branch_data.py
new file mode 100644
index 0000000..19192a5
--- /dev/null
+++ b/analyze_branch_data.py
@@ -0,0 +1,152 @@
+#!/usr/bin/env python3
+
+import json
+import os
+import sys
+from datetime import datetime
+
+def load_branch_data(file_path):
+ """
+ Load branch data from JSON file.
+
+ Args:
+ file_path (str): Path to the JSON file containing branch data
+
+ Returns:
+ dict: Branch data
+ """
+ try:
+ with open(file_path, 'r') as f:
+ data = json.load(f)
+ return data
+ except FileNotFoundError:
+ print(f"Error: File {file_path} not found.")
+ sys.exit(1)
+ except json.JSONDecodeError:
+ print(f"Error: File {file_path} is not valid JSON.")
+ sys.exit(1)
+
+def analyze_branch_data(data):
+ """
+ Analyze branch data and generate statistics.
+
+ Args:
+ data (dict): Branch data
+
+ Returns:
+ dict: Analysis results
+ """
+ # Verify data structure
+ required_keys = ['main', 'release', 'common', 'mainOnly', 'releaseOnly']
+ for key in required_keys:
+ if key not in data:
+ print(f"Error: Missing required key '{key}' in branch data.")
+ sys.exit(1)
+
+ # Calculate statistics
+ stats = {
+ 'total_branches': len(set(data['main'] + data['release'])),
+ 'main_count': len(data['main']),
+ 'release_count': len(data['release']),
+ 'common_count': len(data['common']),
+ 'main_only_count': len(data['mainOnly']),
+ 'release_only_count': len(data['releaseOnly']),
+ 'repository': data.get('repository', 'Unknown'),
+ 'timestamp': data.get('timestamp', datetime.now().isoformat()),
+ 'analysis_time': datetime.now().isoformat()
+ }
+
+ # Calculate percentages
+ if stats['total_branches'] > 0:
+ stats['common_percentage'] = round(stats['common_count'] / stats['total_branches'] * 100, 1)
+ stats['main_only_percentage'] = round(stats['main_only_count'] / stats['total_branches'] * 100, 1)
+ stats['release_only_percentage'] = round(stats['release_only_count'] / stats['total_branches'] * 100, 1)
+ else:
+ stats['common_percentage'] = 0
+ stats['main_only_percentage'] = 0
+ stats['release_only_percentage'] = 0
+
+ # Combine with original data
+ result = {**data, 'stats': stats}
+ return result
+
+def save_analysis_results(data, output_file):
+ """
+ Save analysis results to a JSON file.
+
+ Args:
+ data (dict): Analysis results
+ output_file (str): Path to the output JSON file
+ """
+ with open(output_file, 'w') as f:
+ json.dump(data, f, indent=2)
+ print(f"Analysis results saved to {output_file}")
+
+def print_analysis_summary(data):
+ """
+ Print a summary of the analysis results.
+
+ Args:
+ data (dict): Analysis results
+ """
+ stats = data['stats']
+
+ print("\n" + "="*50)
+ print(f"BRANCH ANALYSIS SUMMARY FOR {stats['repository']}")
+ print("="*50)
+ print(f"Total unique branches: {stats['total_branches']}")
+ print(f"Branches in main: {stats['main_count']}")
+ print(f"Branches in release: {stats['release_count']}")
+ print(f"Common branches: {stats['common_count']} ({stats['common_percentage']}%)")
+ print(f"Branches only in main: {stats['main_only_count']} ({stats['main_only_percentage']}%)")
+ print(f"Branches only in release: {stats['release_only_count']} ({stats['release_only_percentage']}%)")
+ print("="*50)
+
+ # Print branch lists
+ print("\nBranches in main:")
+ for branch in data['main']:
+ print(f" - {branch}")
+
+ print("\nBranches in release:")
+ for branch in data['release']:
+ print(f" - {branch}")
+
+ print("\nCommon branches:")
+ for branch in data['common']:
+ print(f" - {branch}")
+
+ print("\nBranches only in main:")
+ for branch in data['mainOnly']:
+ print(f" - {branch}")
+
+ print("\nBranches only in release:")
+ for branch in data['releaseOnly']:
+ print(f" - {branch}")
+
+def main():
+ # Default input and output files
+ input_file = "branch_data.json"
+ output_file = "analyzed_branch_data.json"
+
+ # Check command line arguments
+ if len(sys.argv) > 1:
+ input_file = sys.argv[1]
+ if len(sys.argv) > 2:
+ output_file = sys.argv[2]
+
+ print(f"Loading branch data from {input_file}...")
+ data = load_branch_data(input_file)
+
+ print("Analyzing branch data...")
+ analyzed_data = analyze_branch_data(data)
+
+ # Save analysis results
+ save_analysis_results(analyzed_data, output_file)
+
+ # Print summary
+ print_analysis_summary(analyzed_data)
+
+ print(f"\nAnalysis complete. Results saved to {output_file}")
+
+if __name__ == "__main__":
+ main()
diff --git a/analyze_branch_data_for_actions.py b/analyze_branch_data_for_actions.py
new file mode 100644
index 0000000..03142e5
--- /dev/null
+++ b/analyze_branch_data_for_actions.py
@@ -0,0 +1,188 @@
+#!/usr/bin/env python3
+
+import json
+import os
+import sys
+import argparse
+from datetime import datetime
+
+def load_branch_data(file_path):
+ """
+ Load branch data from JSON file.
+
+ Args:
+ file_path (str): Path to the JSON file containing branch data
+
+ Returns:
+ dict: Branch data
+ """
+ try:
+ with open(file_path, 'r') as f:
+ data = json.load(f)
+ return data
+ except FileNotFoundError:
+ print(f"Error: File {file_path} not found.")
+ sys.exit(1)
+ except json.JSONDecodeError:
+ print(f"Error: File {file_path} is not valid JSON.")
+ sys.exit(1)
+
+def analyze_branch_data(data):
+ """
+ Analyze branch data and generate statistics.
+
+ Args:
+ data (dict): Branch data
+
+ Returns:
+ dict: Analysis results
+ """
+ # Verify data structure
+ required_keys = ['main', 'release', 'common', 'mainOnly', 'releaseOnly']
+ for key in required_keys:
+ if key not in data:
+ print(f"Error: Missing required key '{key}' in branch data.")
+ sys.exit(1)
+
+ # Calculate statistics
+ stats = {
+ 'total_branches': len(set(data['main'] + data['release'])),
+ 'main_count': len(data['main']),
+ 'release_count': len(data['release']),
+ 'common_count': len(data['common']),
+ 'main_only_count': len(data['mainOnly']),
+ 'release_only_count': len(data['releaseOnly']),
+ 'repository': data.get('repository', 'Unknown'),
+ 'timestamp': data.get('timestamp', datetime.now().isoformat()),
+ 'analysis_time': datetime.now().isoformat(),
+ 'analysis_date': data.get('analysisDate', 'latest')
+ }
+
+ # Calculate percentages
+ if stats['total_branches'] > 0:
+ stats['common_percentage'] = round(stats['common_count'] / stats['total_branches'] * 100, 1)
+ stats['main_only_percentage'] = round(stats['main_only_count'] / stats['total_branches'] * 100, 1)
+ stats['release_only_percentage'] = round(stats['release_only_count'] / stats['total_branches'] * 100, 1)
+ else:
+ stats['common_percentage'] = 0
+ stats['main_only_percentage'] = 0
+ stats['release_only_percentage'] = 0
+
+ # Add GitHub Actions specific metadata
+ if 'GITHUB_REPOSITORY' in os.environ:
+ stats['github_repository'] = os.environ['GITHUB_REPOSITORY']
+ if 'GITHUB_WORKFLOW' in os.environ:
+ stats['github_workflow'] = os.environ['GITHUB_WORKFLOW']
+ if 'GITHUB_RUN_ID' in os.environ:
+ stats['github_run_id'] = os.environ['GITHUB_RUN_ID']
+ if 'GITHUB_SHA' in os.environ:
+ stats['github_commit'] = os.environ['GITHUB_SHA']
+
+ # Combine with original data
+ result = {**data, 'stats': stats}
+ return result
+
+def save_analysis_results(data, output_file):
+ """
+ Save analysis results to a JSON file.
+
+ Args:
+ data (dict): Analysis results
+ output_file (str): Path to the output JSON file
+ """
+ try:
+ with open(output_file, 'w') as f:
+ json.dump(data, f, indent=2)
+ print(f"Analysis results saved to {output_file}")
+ except Exception as e:
+ print(f"Error saving results to {output_file}: {e}")
+ sys.exit(1)
+
+def print_analysis_summary(data):
+ """
+ Print a summary of the analysis results.
+
+ Args:
+ data (dict): Analysis results
+ """
+ stats = data['stats']
+
+ print("\n" + "="*50)
+ print(f"BRANCH ANALYSIS SUMMARY FOR {stats['repository']}")
+ if stats.get('analysis_date') and stats['analysis_date'] != 'latest':
+ print(f"Analysis Date: {stats['analysis_date']}")
+ print("="*50)
+ print(f"Total unique branches: {stats['total_branches']}")
+ print(f"Branches in main: {stats['main_count']}")
+ print(f"Branches in release: {stats['release_count']}")
+ print(f"Common branches: {stats['common_count']} ({stats['common_percentage']}%)")
+ print(f"Branches only in main: {stats['main_only_count']} ({stats['main_only_percentage']}%)")
+ print(f"Branches only in release: {stats['release_only_count']} ({stats['release_only_percentage']}%)")
+ print("="*50)
+
+ # Print branch lists (limited to first 5 for brevity in CI logs)
+ print("\nBranches in main (first 5):")
+ for branch in data['main'][:5]:
+ print(f" - {branch}")
+ if len(data['main']) > 5:
+ print(f" ... and {len(data['main']) - 5} more")
+
+ print("\nBranches in release (first 5):")
+ for branch in data['release'][:5]:
+ print(f" - {branch}")
+ if len(data['release']) > 5:
+ print(f" ... and {len(data['release']) - 5} more")
+
+ print("\nCommon branches (first 5):")
+ for branch in data['common'][:5]:
+ print(f" - {branch}")
+ if len(data['common']) > 5:
+ print(f" ... and {len(data['common']) - 5} more")
+
+def main():
+ # Parse command line arguments
+ parser = argparse.ArgumentParser(description='Analyze branch data with date filtering for GitHub Actions')
+ parser.add_argument('input_file', nargs='?', default='branch_data.json', help='Input JSON file path')
+ parser.add_argument('output_file', nargs='?', help='Output JSON file path')
+ parser.add_argument('--verbose', action='store_true', help='Print verbose output')
+ args = parser.parse_args()
+
+ # Default input and output files
+ input_file = args.input_file
+
+ # Determine output filename if not provided
+ if args.output_file:
+ output_file = args.output_file
+ else:
+ # If input file has date in name, use same pattern for output
+ if '_' in input_file and input_file.startswith('branch_data_'):
+ date_part = input_file.split('branch_data_')[1]
+ output_file = f"analyzed_branch_data_{date_part}"
+ else:
+ output_file = "analyzed_branch_data.json"
+
+ print(f"Loading branch data from {input_file}...")
+ data = load_branch_data(input_file)
+
+ print("Analyzing branch data...")
+ analyzed_data = analyze_branch_data(data)
+
+ # Save analysis results
+ save_analysis_results(analyzed_data, output_file)
+
+ # Print summary
+ if args.verbose:
+ print_analysis_summary(analyzed_data)
+ else:
+ stats = analyzed_data['stats']
+ print(f"\nAnalysis complete for {stats['repository']}")
+ if stats.get('analysis_date') and stats['analysis_date'] != 'latest':
+ print(f"Analysis Date: {stats['analysis_date']}")
+ print(f"Total branches: {stats['total_branches']}")
+ print(f"Main: {stats['main_count']}, Release: {stats['release_count']}, Common: {stats['common_count']}")
+
+ print(f"\nResults saved to {output_file}")
+ return 0
+
+if __name__ == "__main__":
+ exit(main())
diff --git a/analyze_branch_data_with_date.py b/analyze_branch_data_with_date.py
new file mode 100644
index 0000000..0dbc0c7
--- /dev/null
+++ b/analyze_branch_data_with_date.py
@@ -0,0 +1,166 @@
+#!/usr/bin/env python3
+
+import json
+import os
+import sys
+import argparse
+from datetime import datetime
+
+def load_branch_data(file_path):
+ """
+ Load branch data from JSON file.
+
+ Args:
+ file_path (str): Path to the JSON file containing branch data
+
+ Returns:
+ dict: Branch data
+ """
+ try:
+ with open(file_path, 'r') as f:
+ data = json.load(f)
+ return data
+ except FileNotFoundError:
+ print(f"Error: File {file_path} not found.")
+ sys.exit(1)
+ except json.JSONDecodeError:
+ print(f"Error: File {file_path} is not valid JSON.")
+ sys.exit(1)
+
+def analyze_branch_data(data):
+ """
+ Analyze branch data and generate statistics.
+
+ Args:
+ data (dict): Branch data
+
+ Returns:
+ dict: Analysis results
+ """
+ # Verify data structure
+ required_keys = ['main', 'release', 'common', 'mainOnly', 'releaseOnly']
+ for key in required_keys:
+ if key not in data:
+ print(f"Error: Missing required key '{key}' in branch data.")
+ sys.exit(1)
+
+ # Calculate statistics
+ stats = {
+ 'total_branches': len(set(data['main'] + data['release'])),
+ 'main_count': len(data['main']),
+ 'release_count': len(data['release']),
+ 'common_count': len(data['common']),
+ 'main_only_count': len(data['mainOnly']),
+ 'release_only_count': len(data['releaseOnly']),
+ 'repository': data.get('repository', 'Unknown'),
+ 'timestamp': data.get('timestamp', datetime.now().isoformat()),
+ 'analysis_time': datetime.now().isoformat(),
+ 'analysis_date': data.get('analysisDate', 'latest')
+ }
+
+ # Calculate percentages
+ if stats['total_branches'] > 0:
+ stats['common_percentage'] = round(stats['common_count'] / stats['total_branches'] * 100, 1)
+ stats['main_only_percentage'] = round(stats['main_only_count'] / stats['total_branches'] * 100, 1)
+ stats['release_only_percentage'] = round(stats['release_only_count'] / stats['total_branches'] * 100, 1)
+ else:
+ stats['common_percentage'] = 0
+ stats['main_only_percentage'] = 0
+ stats['release_only_percentage'] = 0
+
+ # Combine with original data
+ result = {**data, 'stats': stats}
+ return result
+
+def save_analysis_results(data, output_file):
+ """
+ Save analysis results to a JSON file.
+
+ Args:
+ data (dict): Analysis results
+ output_file (str): Path to the output JSON file
+ """
+ with open(output_file, 'w') as f:
+ json.dump(data, f, indent=2)
+ print(f"Analysis results saved to {output_file}")
+
+def print_analysis_summary(data):
+ """
+ Print a summary of the analysis results.
+
+ Args:
+ data (dict): Analysis results
+ """
+ stats = data['stats']
+
+ print("\n" + "="*50)
+ print(f"BRANCH ANALYSIS SUMMARY FOR {stats['repository']}")
+ if stats.get('analysis_date') and stats['analysis_date'] != 'latest':
+ print(f"Analysis Date: {stats['analysis_date']}")
+ print("="*50)
+ print(f"Total unique branches: {stats['total_branches']}")
+ print(f"Branches in main: {stats['main_count']}")
+ print(f"Branches in release: {stats['release_count']}")
+ print(f"Common branches: {stats['common_count']} ({stats['common_percentage']}%)")
+ print(f"Branches only in main: {stats['main_only_count']} ({stats['main_only_percentage']}%)")
+ print(f"Branches only in release: {stats['release_only_count']} ({stats['release_only_percentage']}%)")
+ print("="*50)
+
+ # Print branch lists
+ print("\nBranches in main:")
+ for branch in data['main']:
+ print(f" - {branch}")
+
+ print("\nBranches in release:")
+ for branch in data['release']:
+ print(f" - {branch}")
+
+ print("\nCommon branches:")
+ for branch in data['common']:
+ print(f" - {branch}")
+
+ print("\nBranches only in main:")
+ for branch in data['mainOnly']:
+ print(f" - {branch}")
+
+ print("\nBranches only in release:")
+ for branch in data['releaseOnly']:
+ print(f" - {branch}")
+
+def main():
+ # Parse command line arguments
+ parser = argparse.ArgumentParser(description='Analyze branch data with date filtering')
+ parser.add_argument('input_file', nargs='?', default='branch_data.json', help='Input JSON file path')
+ parser.add_argument('output_file', nargs='?', help='Output JSON file path')
+ args = parser.parse_args()
+
+ # Default input and output files
+ input_file = args.input_file
+
+ # Determine output filename if not provided
+ if args.output_file:
+ output_file = args.output_file
+ else:
+ # If input file has date in name, use same pattern for output
+ if '_' in input_file and input_file.startswith('branch_data_'):
+ date_part = input_file.split('branch_data_')[1]
+ output_file = f"analyzed_branch_data_{date_part}"
+ else:
+ output_file = "analyzed_branch_data.json"
+
+ print(f"Loading branch data from {input_file}...")
+ data = load_branch_data(input_file)
+
+ print("Analyzing branch data...")
+ analyzed_data = analyze_branch_data(data)
+
+ # Save analysis results
+ save_analysis_results(analyzed_data, output_file)
+
+ # Print summary
+ print_analysis_summary(analyzed_data)
+
+ print(f"\nAnalysis complete. Results saved to {output_file}")
+
+if __name__ == "__main__":
+ main()
diff --git a/analyzed_branch_data.json b/analyzed_branch_data.json
new file mode 100644
index 0000000..db7c16d
--- /dev/null
+++ b/analyzed_branch_data.json
@@ -0,0 +1,57 @@
+{
+ "main": [
+ "feature/sites-40"
+ ],
+ "release": [
+ "feature/sites-42",
+ "feature/sites-41"
+ ],
+ "common": [],
+ "mainOnly": [
+ "feature/sites-40"
+ ],
+ "releaseOnly": [
+ "feature/sites-41",
+ "feature/sites-42"
+ ],
+ "mainDetails": [
+ {
+ "name": "feature/sites-40",
+ "merged_at": "2025-04-08T18:55:00+00:00",
+ "pr_number": 1,
+ "pr_title": "hello"
+ }
+ ],
+ "releaseDetails": [
+ {
+ "name": "feature/sites-42",
+ "merged_at": "2025-04-09T01:10:20+00:00",
+ "pr_number": 3,
+ "pr_title": "add sites-42"
+ },
+ {
+ "name": "feature/sites-41",
+ "merged_at": "2025-04-08T18:58:45+00:00",
+ "pr_number": 2,
+ "pr_title": "using grid areas"
+ }
+ ],
+ "timestamp": "2025-04-08T20:48:59.461936",
+ "repository": "correasebastian/layers",
+ "analysisDate": "latest",
+ "stats": {
+ "total_branches": 3,
+ "main_count": 1,
+ "release_count": 2,
+ "common_count": 0,
+ "main_only_count": 1,
+ "release_only_count": 2,
+ "repository": "correasebastian/layers",
+ "timestamp": "2025-04-08T20:48:59.461936",
+ "analysis_time": "2025-04-08T20:49:32.478997",
+ "analysis_date": "latest",
+ "common_percentage": 0.0,
+ "main_only_percentage": 33.3,
+ "release_only_percentage": 66.7
+ }
+}
\ No newline at end of file
diff --git a/branch_comparison_results.md b/branch_comparison_results.md
new file mode 100644
index 0000000..b7dbca8
--- /dev/null
+++ b/branch_comparison_results.md
@@ -0,0 +1,27 @@
+# Branch Comparison Results
+
+## Feature Branches in Main
+
+- feature/profile-789
+- feature/payment-456
+- feature/auth-123
+
+## Feature Branches in Release
+
+- feature/notification-202
+- feature/search-101
+- feature/auth-123
+
+## Common Feature Branches
+
+- feature/auth-123
+
+## Feature Branches Only in Main
+
+- feature/profile-789
+- feature/payment-456
+
+## Feature Branches Only in Release
+
+- feature/search-101
+- feature/notification-202
diff --git a/branch_data.json b/branch_data.json
new file mode 100644
index 0000000..ab18f50
--- /dev/null
+++ b/branch_data.json
@@ -0,0 +1,42 @@
+{
+ "main": [
+ "feature/sites-40"
+ ],
+ "release": [
+ "feature/sites-42",
+ "feature/sites-41"
+ ],
+ "common": [],
+ "mainOnly": [
+ "feature/sites-40"
+ ],
+ "releaseOnly": [
+ "feature/sites-41",
+ "feature/sites-42"
+ ],
+ "mainDetails": [
+ {
+ "name": "feature/sites-40",
+ "merged_at": "2025-04-08T18:55:00+00:00",
+ "pr_number": 1,
+ "pr_title": "hello"
+ }
+ ],
+ "releaseDetails": [
+ {
+ "name": "feature/sites-42",
+ "merged_at": "2025-04-09T01:10:20+00:00",
+ "pr_number": 3,
+ "pr_title": "add sites-42"
+ },
+ {
+ "name": "feature/sites-41",
+ "merged_at": "2025-04-08T18:58:45+00:00",
+ "pr_number": 2,
+ "pr_title": "using grid areas"
+ }
+ ],
+ "timestamp": "2025-04-08T20:48:59.461936",
+ "repository": "correasebastian/layers",
+ "analysisDate": "latest"
+}
\ No newline at end of file
diff --git a/configure.sh b/configure.sh
new file mode 100755
index 0000000..7a7d748
--- /dev/null
+++ b/configure.sh
@@ -0,0 +1,57 @@
+#!/bin/bash
+
+# Configuration script for GitHub Branch Analyzer
+# This script sets up the environment variables needed for the analyzer
+
+# Check if .env file exists and source it if it does
+if [ -f .env ]; then
+ source .env
+fi
+
+# Function to prompt for input with a default value
+prompt_with_default() {
+ local prompt=$1
+ local default=$2
+ local input
+
+ echo -n "$prompt [$default]: "
+ read input
+ echo "${input:-$default}"
+}
+
+# Prompt for GitHub token if not set
+if [ -z "$GITHUB_TOKEN" ]; then
+ echo "GitHub Personal Access Token is required to access private repositories."
+ echo "You can create one at: https://github.com/settings/tokens"
+ echo "Ensure it has 'repo' permissions to access private repositories."
+ read -p "Enter your GitHub Personal Access Token: " GITHUB_TOKEN
+fi
+
+# Prompt for repository name if not set
+if [ -z "$GITHUB_REPO" ]; then
+ GITHUB_REPO=$(prompt_with_default "Enter repository name (format: owner/repo)" "")
+fi
+
+# Prompt for branch names
+MAIN_BRANCH=$(prompt_with_default "Enter main branch name" "main")
+RELEASE_BRANCH=$(prompt_with_default "Enter release branch name" "release")
+
+# Save to .env file
+cat > .env << EOF
+GITHUB_TOKEN=$GITHUB_TOKEN
+GITHUB_REPO=$GITHUB_REPO
+MAIN_BRANCH=$MAIN_BRANCH
+RELEASE_BRANCH=$RELEASE_BRANCH
+EOF
+
+echo "Configuration saved to .env file"
+echo "To run the analyzer, use: python github_branch_analyzer.py"
+
+# Make the .env file readable only by the owner
+chmod 600 .env
+
+# Export variables for immediate use
+export GITHUB_TOKEN
+export GITHUB_REPO
+export MAIN_BRANCH
+export RELEASE_BRANCH
diff --git a/configure_with_date.sh b/configure_with_date.sh
new file mode 100644
index 0000000..14af831
--- /dev/null
+++ b/configure_with_date.sh
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+# Configuration script for GitHub Branch Analyzer with date filtering
+# This script sets up the environment variables needed for the analyzer
+
+# Check if .env file exists and source it if it does
+if [ -f .env ]; then
+ source .env
+fi
+
+# Function to prompt for input with a default value
+prompt_with_default() {
+ local prompt=$1
+ local default=$2
+ local input
+
+ echo -n "$prompt [$default]: "
+ read input
+ echo "${input:-$default}"
+}
+
+# Prompt for GitHub token if not set
+if [ -z "$GITHUB_TOKEN" ]; then
+ echo "GitHub Personal Access Token is required to access private repositories."
+ echo "You can create one at: https://github.com/settings/tokens"
+ echo "Ensure it has 'repo' permissions to access private repositories."
+ read -p "Enter your GitHub Personal Access Token: " GITHUB_TOKEN
+fi
+
+# Prompt for repository name if not set
+if [ -z "$GITHUB_REPO" ]; then
+ GITHUB_REPO=$(prompt_with_default "Enter repository name (format: owner/repo)" "")
+fi
+
+# Prompt for branch names
+MAIN_BRANCH=$(prompt_with_default "Enter main branch name" "main")
+RELEASE_BRANCH=$(prompt_with_default "Enter release branch name" "release")
+
+# Prompt for analysis date (optional)
+DEFAULT_DATE=$(date +%Y-%m-%d) # Today's date in YYYY-MM-DD format
+ANALYSIS_DATE=$(prompt_with_default "Enter analysis date (YYYY-MM-DD format, leave empty for latest)" "$DEFAULT_DATE")
+
+# Save to .env file
+cat > .env << EOF
+GITHUB_TOKEN=$GITHUB_TOKEN
+GITHUB_REPO=$GITHUB_REPO
+MAIN_BRANCH=$MAIN_BRANCH
+RELEASE_BRANCH=$RELEASE_BRANCH
+ANALYSIS_DATE=$ANALYSIS_DATE
+EOF
+
+echo "Configuration saved to .env file"
+echo "To run the analyzer, use: ./run_with_date.sh"
+
+# Make the .env file readable only by the owner
+chmod 600 .env
+
+# Export variables for immediate use
+export GITHUB_TOKEN
+export GITHUB_REPO
+export MAIN_BRANCH
+export RELEASE_BRANCH
+export ANALYSIS_DATE
diff --git a/generate_sample_data.sh b/generate_sample_data.sh
new file mode 100755
index 0000000..3d59c3d
--- /dev/null
+++ b/generate_sample_data.sh
@@ -0,0 +1,39 @@
+#!/bin/bash
+
+# Example data generator for GitHub Branch Analyzer
+# This script creates sample data for demonstration purposes
+
+echo "Generating sample branch data for demonstration..."
+
+# Create sample data that matches the format of the real analyzer output
+cat > branch_data.json << EOF
+{
+ "main": [
+ "feature/profile-789",
+ "feature/payment-456",
+ "feature/auth-123"
+ ],
+ "release": [
+ "feature/notification-202",
+ "feature/search-101",
+ "feature/auth-123"
+ ],
+ "common": [
+ "feature/auth-123"
+ ],
+ "mainOnly": [
+ "feature/profile-789",
+ "feature/payment-456"
+ ],
+ "releaseOnly": [
+ "feature/notification-202",
+ "feature/search-101"
+ ],
+ "timestamp": "$(date -Iseconds)",
+ "repository": "example/private-repo"
+}
+EOF
+
+echo "Sample data generated in branch_data.json"
+echo "This file can be used for testing the visualization website."
+echo "For real data, you'll need to run the analyzer with your GitHub credentials."
diff --git a/github_branch_analyzer.py b/github_branch_analyzer.py
new file mode 100644
index 0000000..33342e8
--- /dev/null
+++ b/github_branch_analyzer.py
@@ -0,0 +1,124 @@
+import os
+import json
+from github import Github
+from datetime import datetime
+
+class GitHubBranchAnalyzer:
+ def __init__(self, token, repo_name, main_branch="main", release_branch="release"):
+ """
+ Initialize the GitHub Branch Analyzer.
+
+ Args:
+ token (str): GitHub Personal Access Token
+ repo_name (str): Repository name in format "owner/repo"
+ main_branch (str): Name of the main branch (default: "main")
+ release_branch (str): Name of the release branch (default: "release")
+ """
+ self.token = token
+ self.repo_name = repo_name
+ self.main_branch = main_branch
+ self.release_branch = release_branch
+ self.g = Github(token)
+ self.repo = self.g.get_repo(repo_name)
+
+ def get_merged_branches(self, target_branch):
+ """
+ Get all branches that were merged into the target branch via pull requests.
+
+ Args:
+ target_branch (str): The target branch to check (e.g., "main" or "release")
+
+ Returns:
+ list: List of branch names that were merged into the target branch
+ """
+ print(f"Fetching pull requests merged into {target_branch}...")
+
+ # Get all closed pull requests that were merged into the target branch
+ pulls = self.repo.get_pulls(state='closed', base=target_branch)
+
+ # Extract the source branch names from the merged pull requests
+ merged_branches = []
+ for pr in pulls:
+ if pr.merged:
+ # Extract the source branch name
+ source_branch = pr.head.ref
+ # Check if it matches the feature/xxx-123 pattern
+ if source_branch.startswith("feature/"):
+ merged_branches.append(source_branch)
+ print(f"Found merged branch: {source_branch}")
+
+ return merged_branches
+
+ def analyze_branches(self):
+ """
+ Analyze branches and generate comparison data.
+
+ Returns:
+ dict: Branch comparison data
+ """
+ # Get branches merged into main
+ main_branches = self.get_merged_branches(self.main_branch)
+
+ # Get branches merged into release
+ release_branches = self.get_merged_branches(self.release_branch)
+
+ # Find common branches
+ common_branches = list(set(main_branches) & set(release_branches))
+
+ # Find branches only in main
+ main_only = list(set(main_branches) - set(release_branches))
+
+ # Find branches only in release
+ release_only = list(set(release_branches) - set(main_branches))
+
+ # Create result data structure
+ result = {
+ "main": main_branches,
+ "release": release_branches,
+ "common": common_branches,
+ "mainOnly": main_only,
+ "releaseOnly": release_only,
+ "timestamp": datetime.now().isoformat(),
+ "repository": self.repo_name
+ }
+
+ return result
+
+ def save_results(self, output_file="branch_data.json"):
+ """
+ Analyze branches and save results to a JSON file.
+
+ Args:
+ output_file (str): Path to the output JSON file
+
+ Returns:
+ dict: Branch comparison data
+ """
+ result = self.analyze_branches()
+
+ # Save to JSON file
+ with open(output_file, 'w') as f:
+ json.dump(result, f, indent=2)
+
+ print(f"Results saved to {output_file}")
+ return result
+
+def main():
+ # Check if environment variables are set
+ token = os.environ.get("GITHUB_TOKEN")
+ repo_name = os.environ.get("GITHUB_REPO")
+ main_branch = os.environ.get("MAIN_BRANCH", "main")
+ release_branch = os.environ.get("RELEASE_BRANCH", "release")
+
+ if not token or not repo_name:
+ print("Error: GITHUB_TOKEN and GITHUB_REPO environment variables must be set.")
+ print("Example usage:")
+ print(" GITHUB_TOKEN=your_token GITHUB_REPO=owner/repo python github_branch_analyzer.py")
+ return
+
+ # Create analyzer and run analysis
+ analyzer = GitHubBranchAnalyzer(token, repo_name, main_branch, release_branch)
+ analyzer.save_results()
+
+if __name__ == "__main__":
+ main()
diff --git a/github_branch_analyzer_for_actions.py b/github_branch_analyzer_for_actions.py
new file mode 100644
index 0000000..f09a197
--- /dev/null
+++ b/github_branch_analyzer_for_actions.py
@@ -0,0 +1,198 @@
+#!/usr/bin/env python3
+
+import os
+import json
+import argparse
+from datetime import datetime
+from github import Github, GithubException
+
+class GitHubBranchAnalyzer:
+ def __init__(self, token, repo_name, main_branch="main", release_branch="release"):
+ """
+ Initialize the GitHub Branch Analyzer.
+
+ Args:
+ token (str): GitHub Personal Access Token
+ repo_name (str): Repository name in format "owner/repo"
+ main_branch (str): Name of the main branch (default: "main")
+ release_branch (str): Name of the release branch (default: "release")
+ """
+ self.token = token
+ self.repo_name = repo_name
+ self.main_branch = main_branch
+ self.release_branch = release_branch
+
+ # Initialize GitHub API client
+ try:
+ self.g = Github(token)
+ self.repo = self.g.get_repo(repo_name)
+ print(f"Successfully connected to repository: {repo_name}")
+ except GithubException as e:
+ print(f"Error connecting to GitHub: {e}")
+ raise
+
+ def get_merged_branches(self, target_branch, before_date=None):
+ """
+ Get all branches that were merged into the target branch via pull requests.
+
+ Args:
+ target_branch (str): The target branch to check (e.g., "main" or "release")
+ before_date (str, optional): ISO format date string (YYYY-MM-DD) to filter PRs merged before this date
+
+ Returns:
+ list: List of branch names that were merged into the target branch
+ """
+ print(f"Fetching pull requests merged into {target_branch}...")
+ if before_date:
+ print(f"Filtering for PRs merged before {before_date}")
+ before_datetime = datetime.fromisoformat(before_date)
+ else:
+ before_datetime = None
+
+ # Get all closed pull requests that were merged into the target branch
+ try:
+ pulls = self.repo.get_pulls(state='closed', base=target_branch)
+ print(f"Found {pulls.totalCount} closed pull requests for {target_branch}")
+ except GithubException as e:
+ print(f"Error fetching pull requests: {e}")
+ return [], []
+
+ # Extract the source branch names from the merged pull requests
+ merged_branches = []
+ branch_details = []
+
+ for pr in pulls:
+ try:
+ if not pr.merged:
+ continue
+
+ # Skip if PR was merged after the specified date
+ if before_datetime and pr.merged_at > before_datetime:
+ continue
+
+ # Extract the source branch name
+ source_branch = pr.head.ref
+
+ # Check if it matches the feature/xxx-123 pattern
+ if source_branch.startswith("feature/"):
+ branch_info = {
+ "name": source_branch,
+ "merged_at": pr.merged_at.isoformat(),
+ "pr_number": pr.number,
+ "pr_title": pr.title,
+ "pr_url": pr.html_url,
+ "author": pr.user.login if pr.user else "Unknown"
+ }
+ branch_details.append(branch_info)
+ merged_branches.append(source_branch)
+ print(f"Found merged branch: {source_branch} (merged on {pr.merged_at})")
+ except GithubException as e:
+ print(f"Error processing pull request #{pr.number}: {e}")
+ continue
+
+ return merged_branches, branch_details
+
+ def analyze_branches(self, before_date=None):
+ """
+ Analyze branches and generate comparison data.
+
+ Args:
+ before_date (str, optional): ISO format date string (YYYY-MM-DD) to filter PRs merged before this date
+
+ Returns:
+ dict: Branch comparison data
+ """
+ # Get branches merged into main
+ main_branch_names, main_branches_details = self.get_merged_branches(self.main_branch, before_date)
+
+ # Get branches merged into release
+ release_branch_names, release_branches_details = self.get_merged_branches(self.release_branch, before_date)
+
+ # Find common branches
+ common_branches = list(set(main_branch_names) & set(release_branch_names))
+
+ # Find branches only in main
+ main_only = list(set(main_branch_names) - set(release_branch_names))
+
+ # Find branches only in release
+ release_only = list(set(release_branch_names) - set(main_branch_names))
+
+ # Create result data structure
+ result = {
+ "main": main_branch_names,
+ "release": release_branch_names,
+ "common": common_branches,
+ "mainOnly": main_only,
+ "releaseOnly": release_only,
+ "mainDetails": main_branches_details,
+ "releaseDetails": release_branches_details,
+ "timestamp": datetime.now().isoformat(),
+ "repository": self.repo_name,
+ "analysisDate": before_date if before_date else "latest"
+ }
+
+ return result
+
+ def save_results(self, before_date=None, output_file=None):
+ """
+ Analyze branches and save results to a JSON file.
+
+ Args:
+ before_date (str, optional): ISO format date string (YYYY-MM-DD) to filter PRs merged before this date
+ output_file (str, optional): Path to the output JSON file
+
+ Returns:
+ dict: Branch comparison data
+ """
+ result = self.analyze_branches(before_date)
+
+ # Determine output filename if not provided
+ if output_file is None:
+ if before_date:
+ date_str = before_date.replace("-", "")
+ output_file = f"branch_data_{date_str}.json"
+ else:
+ output_file = "branch_data.json"
+
+ # Save to JSON file
+ with open(output_file, 'w') as f:
+ json.dump(result, f, indent=2)
+
+ print(f"Results saved to {output_file}")
+ return result
+
+def main():
+ # Parse command line arguments
+ parser = argparse.ArgumentParser(description='Analyze GitHub repository branches with date filtering')
+ parser.add_argument('--date', type=str, help='Analysis date in ISO format (YYYY-MM-DD)')
+ parser.add_argument('--output', type=str, help='Output file path')
+ parser.add_argument('--token', type=str, help='GitHub Personal Access Token')
+ parser.add_argument('--repo', type=str, help='Repository name (owner/repo)')
+ parser.add_argument('--main-branch', type=str, help='Main branch name')
+ parser.add_argument('--release-branch', type=str, help='Release branch name')
+ args = parser.parse_args()
+
+ # Check for token and repo in args or environment variables
+ token = args.token or os.environ.get("GITHUB_TOKEN")
+ repo_name = args.repo or os.environ.get("GITHUB_REPO")
+ main_branch = args.main_branch or os.environ.get("MAIN_BRANCH", "main")
+ release_branch = args.release_branch or os.environ.get("RELEASE_BRANCH", "release")
+
+ if not token or not repo_name:
+ print("Error: GitHub token and repository name must be provided.")
+ print("You can provide them as command line arguments or environment variables:")
+ print(" --token TOKEN or GITHUB_TOKEN environment variable")
+ print(" --repo OWNER/REPO or GITHUB_REPO environment variable")
+ return 1
+
+ try:
+ # Create analyzer and run analysis
+ analyzer = GitHubBranchAnalyzer(token, repo_name, main_branch, release_branch)
+ analyzer.save_results(args.date, args.output)
+ return 0
+ except Exception as e:
+ print(f"Error during analysis: {e}")
+ return 1
+
+if __name__ == "__main__":
+ exit(main())
diff --git a/github_branch_analyzer_with_date.py b/github_branch_analyzer_with_date.py
new file mode 100644
index 0000000..c9bf6d4
--- /dev/null
+++ b/github_branch_analyzer_with_date.py
@@ -0,0 +1,164 @@
+import os
+import json
+import argparse
+from datetime import datetime
+from github import Github
+
+class GitHubBranchAnalyzer:
+ def __init__(self, token, repo_name, main_branch="main", release_branch="release"):
+ """
+ Initialize the GitHub Branch Analyzer.
+
+ Args:
+ token (str): GitHub Personal Access Token
+ repo_name (str): Repository name in format "owner/repo"
+ main_branch (str): Name of the main branch (default: "main")
+ release_branch (str): Name of the release branch (default: "release")
+ """
+ self.token = token
+ self.repo_name = repo_name
+ self.main_branch = main_branch
+ self.release_branch = release_branch
+ self.g = Github(token)
+ self.repo = self.g.get_repo(repo_name)
+
+ def get_merged_branches(self, target_branch, before_date=None):
+ """
+ Get all branches that were merged into the target branch via pull requests.
+
+ Args:
+ target_branch (str): The target branch to check (e.g., "main" or "release")
+ before_date (str, optional): ISO format date string (YYYY-MM-DD) to filter PRs merged before this date
+
+ Returns:
+ list: List of branch names that were merged into the target branch
+ """
+ print(f"Fetching pull requests merged into {target_branch}...")
+ if before_date:
+ print(f"Filtering for PRs merged before {before_date}")
+
+ # Get all closed pull requests that were merged into the target branch
+ pulls = self.repo.get_pulls(state='closed', base=target_branch)
+
+ # Extract the source branch names from the merged pull requests
+ merged_branches = []
+ for pr in pulls:
+ if pr.merged:
+ # Skip if PR was merged after the specified date
+ if before_date and pr.merged_at > datetime.fromisoformat(before_date):
+ continue
+
+ # Extract the source branch name
+ source_branch = pr.head.ref
+ # Check if it matches the feature/xxx-123 pattern
+ if source_branch.startswith("feature/"):
+ merged_branches.append({
+ "name": source_branch,
+ "merged_at": pr.merged_at.isoformat(),
+ "pr_number": pr.number,
+ "pr_title": pr.title
+ })
+ print(f"Found merged branch: {source_branch} (merged on {pr.merged_at})")
+
+ # Extract just the branch names for backward compatibility
+ branch_names = [branch["name"] for branch in merged_branches]
+ return branch_names, merged_branches
+
+ def analyze_branches(self, before_date=None):
+ """
+ Analyze branches and generate comparison data.
+
+ Args:
+ before_date (str, optional): ISO format date string (YYYY-MM-DD) to filter PRs merged before this date
+
+ Returns:
+ dict: Branch comparison data
+ """
+ # Get branches merged into main
+ main_branch_names, main_branches_details = self.get_merged_branches(self.main_branch, before_date)
+
+ # Get branches merged into release
+ release_branch_names, release_branches_details = self.get_merged_branches(self.release_branch, before_date)
+
+ # Find common branches
+ common_branches = list(set(main_branch_names) & set(release_branch_names))
+
+ # Find branches only in main
+ main_only = list(set(main_branch_names) - set(release_branch_names))
+
+ # Find branches only in release
+ release_only = list(set(release_branch_names) - set(main_branch_names))
+
+ # Create result data structure
+ result = {
+ "main": main_branch_names,
+ "release": release_branch_names,
+ "common": common_branches,
+ "mainOnly": main_only,
+ "releaseOnly": release_only,
+ "mainDetails": main_branches_details,
+ "releaseDetails": release_branches_details,
+ "timestamp": datetime.now().isoformat(),
+ "repository": self.repo_name,
+ "analysisDate": before_date if before_date else "latest"
+ }
+
+ return result
+
+ def save_results(self, before_date=None, output_file=None):
+ """
+ Analyze branches and save results to a JSON file.
+
+ Args:
+ before_date (str, optional): ISO format date string (YYYY-MM-DD) to filter PRs merged before this date
+ output_file (str, optional): Path to the output JSON file
+
+ Returns:
+ dict: Branch comparison data
+ """
+ result = self.analyze_branches(before_date)
+
+ # Determine output filename if not provided
+ if output_file is None:
+ if before_date:
+ date_str = before_date.replace("-", "")
+ output_file = f"branch_data_{date_str}.json"
+ else:
+ output_file = "branch_data.json"
+
+ # Save to JSON file
+ with open(output_file, 'w') as f:
+ json.dump(result, f, indent=2)
+
+ print(f"Results saved to {output_file}")
+ return result
+
+def main():
+ # Parse command line arguments
+ parser = argparse.ArgumentParser(description='Analyze GitHub repository branches with date or timestamp filtering')
+ parser.add_argument('--date', type=str, help='Analysis date in ISO format (YYYY-MM-DD)')
+ parser.add_argument('--timestamp', type=str, help='Analysis timestamp in ISO format (YYYY-MM-DDTHH:MM:SS)')
+ parser.add_argument('--output', type=str, help='Output file path')
+ args = parser.parse_args()
+
+ # Check if environment variables are set
+ token = os.environ.get("GITHUB_TOKEN")
+ repo_name = os.environ.get("GITHUB_REPO")
+ main_branch = os.environ.get("MAIN_BRANCH", "main")
+ release_branch = os.environ.get("RELEASE_BRANCH", "release")
+
+ if not token or not repo_name:
+ print("Error: GITHUB_TOKEN and GITHUB_REPO environment variables must be set.")
+ print("Example usage:")
+ print(" GITHUB_TOKEN=your_token GITHUB_REPO=owner/repo python github_branch_analyzer.py")
+ return
+
+ # Determine the filtering date or timestamp
+ filter_date = args.timestamp if args.timestamp else args.date
+
+ # Create analyzer and run analysis
+ analyzer = GitHubBranchAnalyzer(token, repo_name, main_branch, release_branch)
+ analyzer.save_results(filter_date, args.output)
+
+if __name__ == "__main__":
+ main()
diff --git a/index copy.html b/index copy.html
new file mode 100644
index 0000000..69f127e
--- /dev/null
+++ b/index copy.html
@@ -0,0 +1,94 @@
+
+
+
+
+
+ Git Branch Comparison
+
+
+
+
+
+
Git Branch Comparison
+
Visualizing differences between main and release branches
+
+
+
+
+
Branch Overview
+
+
+
Main Branch
+
Total merged branches: 0
+
+
+
Release Branch
+
Total merged branches: 0
+
+
+
Common Branches
+
Branches in both: 0
+
+
+
+
+
+
Branch Visualization
+
+
+
+
+
Branch Details
+
+
+
+
Main Branch
+
+
+
+
+
+
+
Release Branch
+
+
+
+
+
+
+
+
+
Unique Branches
+
+
+
+
Only in Main
+
+
+
+
+
+
+
Only in Release
+
+
+
+
+
+
+
+
+
Common Branches
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main_merged_branches.txt b/main_merged_branches.txt
new file mode 100644
index 0000000..e277b4b
--- /dev/null
+++ b/main_merged_branches.txt
@@ -0,0 +1,3 @@
+6042bde Merge pull request #3 from feature/profile-789
+028f70e Merge pull request #2 from feature/payment-456
+0afb34d Merge pull request #1 from feature/auth-123
\ No newline at end of file
diff --git a/release_merged_branches.txt b/release_merged_branches.txt
new file mode 100644
index 0000000..600bd8f
--- /dev/null
+++ b/release_merged_branches.txt
@@ -0,0 +1,3 @@
+1b929f3 Merge pull request #6 from feature/notification-202
+a9b25b9 Merge pull request #5 from feature/search-101
+ff98c7d Merge pull request #4 from feature/auth-123
\ No newline at end of file
diff --git a/run.sh b/run.sh
new file mode 100755
index 0000000..411d768
--- /dev/null
+++ b/run.sh
@@ -0,0 +1,41 @@
+#!/bin/bash
+
+# Run script for GitHub Branch Analyzer
+# This script runs the analyzer with the configured settings
+
+# Check if .env file exists and source it
+if [ -f .env ]; then
+ source .env
+else
+ echo "Error: .env file not found. Please run ./configure.sh first."
+ exit 1
+fi
+
+# Check if required variables are set
+if [ -z "$GITHUB_TOKEN" ] || [ -z "$GITHUB_REPO" ]; then
+ echo "Error: GITHUB_TOKEN and GITHUB_REPO must be set in .env file."
+ echo "Please run ./configure.sh to configure these variables."
+ exit 1
+fi
+
+echo "Running GitHub Branch Analyzer for repository: $GITHUB_REPO"
+echo "Comparing branches: $MAIN_BRANCH and $RELEASE_BRANCH"
+
+# Run the Python script
+python github_branch_analyzer.py
+
+# Check if the analysis was successful
+if [ $? -eq 0 ]; then
+ echo "Analysis completed successfully!"
+ echo "Results saved to branch_data.json"
+
+ # Copy the data to the visualization directory
+ if [ -d "../git-branch-comparison-website" ]; then
+ cp branch_data.json ../git-branch-comparison-website/
+ echo "Data copied to visualization website directory."
+ else
+ echo "Note: Visualization website directory not found."
+ fi
+else
+ echo "Error: Analysis failed. Please check the error messages above."
+fi
diff --git a/run_with_date.sh b/run_with_date.sh
new file mode 100644
index 0000000..34b9538
--- /dev/null
+++ b/run_with_date.sh
@@ -0,0 +1,70 @@
+#!/bin/bash
+
+# Run script for GitHub Branch Analyzer with date filtering
+# This script runs the analyzer with the configured settings and date parameter
+
+# Check if .env file exists and source it
+if [ -f .env ]; then
+ source .env
+else
+ echo "Error: .env file not found. Please run ./configure_with_date.sh first."
+ exit 1
+fi
+
+# Check if required variables are set
+if [ -z "$GITHUB_TOKEN" ] || [ -z "$GITHUB_REPO" ]; then
+ echo "Error: GITHUB_TOKEN and GITHUB_REPO must be set in .env file."
+ echo "Please run ./configure_with_date.sh to configure these variables."
+ exit 1
+fi
+
+echo "Running GitHub Branch Analyzer for repository: $GITHUB_REPO"
+echo "Comparing branches: $MAIN_BRANCH and $RELEASE_BRANCH"
+
+# Determine output filename based on date
+if [ -n "$ANALYSIS_DATE" ]; then
+ DATE_STR=$(echo $ANALYSIS_DATE | tr -d '-')
+ OUTPUT_FILE="branch_data_${DATE_STR}.json"
+ ANALYZED_OUTPUT_FILE="analyzed_branch_data_${DATE_STR}.json"
+ echo "Analysis date: $ANALYSIS_DATE"
+ echo "Output will be saved to: $OUTPUT_FILE"
+else
+ OUTPUT_FILE="branch_data.json"
+ ANALYZED_OUTPUT_FILE="analyzed_branch_data.json"
+ echo "Analysis date: latest (no date filter)"
+ echo "Output will be saved to: $OUTPUT_FILE"
+fi
+
+# Run the Python script with date parameter if provided
+if [ -n "$ANALYSIS_DATE" ]; then
+ python github_branch_analyzer_with_date.py --date "$ANALYSIS_DATE" --output "$OUTPUT_FILE"
+else
+ python github_branch_analyzer_with_date.py --output "$OUTPUT_FILE"
+fi
+
+# Check if the analysis was successful
+if [ $? -eq 0 ]; then
+ echo "Analysis completed successfully!"
+ echo "Results saved to $OUTPUT_FILE"
+
+ # Run the analysis script to add statistics
+ echo "Adding statistics to the data..."
+ python analyze_branch_data_with_date.py "$OUTPUT_FILE" "$ANALYZED_OUTPUT_FILE"
+
+ if [ $? -eq 0 ]; then
+ echo "Statistics added successfully!"
+ echo "Final results saved to $ANALYZED_OUTPUT_FILE"
+
+ # Copy the data to the visualization directory
+ if [ -d "../git-branch-comparison-website" ]; then
+ cp "$ANALYZED_OUTPUT_FILE" ../git-branch-comparison-website/
+ echo "Data copied to visualization website directory."
+ else
+ echo "Note: Visualization website directory not found."
+ fi
+ else
+ echo "Error: Failed to add statistics. Please check the error messages above."
+ fi
+else
+ echo "Error: Analysis failed. Please check the error messages above."
+fi
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..1a671c7
--- /dev/null
+++ b/script.js
@@ -0,0 +1,816 @@
+// Load data from JSON file
+async function loadBranchData() {
+ try {
+ const response = await fetch('analyzed_branch_data.json');
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`);
+ }
+ const data = await response.json();
+ return data;
+ } catch (error) {
+ console.error('Error loading branch data:', error);
+ // Fall back to sample data if loading fails
+ return {
+ main: ["feature/profile-789", "feature/payment-456", "feature/auth-123"],
+ release: ["feature/notification-202", "feature/search-101", "feature/auth-123"],
+ common: ["feature/auth-123"],
+ mainOnly: ["feature/profile-789", "feature/payment-456"],
+ releaseOnly: ["feature/notification-202", "feature/search-101"],
+ stats: {
+ total_branches: 5,
+ main_count: 3,
+ release_count: 3,
+ common_count: 1,
+ main_only_count: 2,
+ release_only_count: 2,
+ repository: "example/private-repo",
+ common_percentage: 20.0,
+ main_only_percentage: 40.0,
+ release_only_percentage: 40.0
+ }
+ };
+ }
+}
+
+// Create Venn Diagram visualization
+async function createVennDiagram() {
+ // Load data from JSON file
+ const branchData = await loadBranchData();
+
+ // Update counts in the UI
+ document.getElementById('main-count').textContent = branchData.stats?.main_count;
+ document.getElementById('release-count').textContent = branchData.stats?.release_count;
+ document.getElementById('common-count').textContent = branchData.stats?.common_count;
+
+ // Update repository name if available
+ if (branchData.stats && branchData.stats?.repository) {
+ const repoName = branchData.stats?.repository;
+ document.querySelector('header p').textContent = `Visualizing differences between main and release branches in ${repoName}`;
+ }
+
+ const container = document.getElementById('venn-diagram');
+ const width = container.clientWidth;
+ const height = 450;
+
+ // Clear any existing SVG
+ container.innerHTML = '';
+
+ // Create SVG
+ const svg = d3.select("#venn-diagram")
+ .append("svg")
+ .attr("width", width)
+ .attr("height", height)
+ .append("g")
+ .attr("transform", `translate(${width/2}, ${height/2})`);
+
+ // Circle properties
+ const radius = Math.min(width, height) / 4;
+ const offset = radius * 0.7;
+
+ // Create gradient for main circle
+ const mainGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "mainGradient")
+ .attr("x1", "0%")
+ .attr("y1", "0%")
+ .attr("x2", "100%")
+ .attr("y2", "100%");
+
+ mainGradient.append("stop")
+ .attr("offset", "0%")
+ .attr("stop-color", "#4299e1")
+ .attr("stop-opacity", 0.7);
+
+ mainGradient.append("stop")
+ .attr("offset", "100%")
+ .attr("stop-color", "#3182ce")
+ .attr("stop-opacity", 0.7);
+
+ // Create gradient for release circle
+ const releaseGradient = svg.append("defs")
+ .append("linearGradient")
+ .attr("id", "releaseGradient")
+ .attr("x1", "0%")
+ .attr("y1", "0%")
+ .attr("x2", "100%")
+ .attr("y2", "100%");
+
+ releaseGradient.append("stop")
+ .attr("offset", "0%")
+ .attr("stop-color", "#ed8936")
+ .attr("stop-opacity", 0.7);
+
+ releaseGradient.append("stop")
+ .attr("offset", "100%")
+ .attr("stop-color", "#dd6b20")
+ .attr("stop-opacity", 0.7);
+
+ // Draw main branch circle
+ const mainCircle = svg.append("circle")
+ .attr("cx", -offset)
+ .attr("cy", 0)
+ .attr("r", radius)
+ .style("fill", "url(#mainGradient)")
+ .style("stroke", "#2b6cb0")
+ .style("stroke-width", 2)
+ .attr("class", "main-circle");
+
+ // Draw release branch circle
+ const releaseCircle = svg.append("circle")
+ .attr("cx", offset)
+ .attr("cy", 0)
+ .attr("r", radius)
+ .style("fill", "url(#releaseGradient)")
+ .style("stroke", "#c05621")
+ .style("stroke-width", 2)
+ .attr("class", "release-circle");
+
+ // Add labels with background
+ // Main label
+ svg.append("rect")
+ .attr("x", -offset - 40)
+ .attr("y", -radius - 30)
+ .attr("width", 80)
+ .attr("height", 24)
+ .attr("rx", 12)
+ .attr("ry", 12)
+ .style("fill", "#2b6cb0");
+
+ svg.append("text")
+ .attr("x", -offset)
+ .attr("y", -radius - 14)
+ .attr("text-anchor", "middle")
+ .style("font-size", "14px")
+ .style("font-weight", "bold")
+ .style("fill", "white")
+ .text("Main");
+
+ // Release label
+ svg.append("rect")
+ .attr("x", offset - 40)
+ .attr("y", -radius - 30)
+ .attr("width", 80)
+ .attr("height", 24)
+ .attr("rx", 12)
+ .attr("ry", 12)
+ .style("fill", "#c05621");
+
+ svg.append("text")
+ .attr("x", offset)
+ .attr("y", -radius - 14)
+ .attr("text-anchor", "middle")
+ .style("font-size", "14px")
+ .style("font-weight", "bold")
+ .style("fill", "white")
+ .text("Release");
+
+ // Add branch counts with circles
+ // Main only count
+ svg.append("circle")
+ .attr("cx", -offset)
+ .attr("cy", -20)
+ .attr("r", 25)
+ .style("fill", "white")
+ .style("fill-opacity", 0.8)
+ .style("stroke", "#2b6cb0")
+ .style("stroke-width", 2);
+
+ svg.append("text")
+ .attr("x", -offset)
+ .attr("y", -15)
+ .attr("text-anchor", "middle")
+ .style("font-size", "18px")
+ .style("font-weight", "bold")
+ .style("fill", "#2b6cb0")
+ .text(branchData.mainOnly.length);
+
+ // Release only count
+ svg.append("circle")
+ .attr("cx", offset)
+ .attr("cy", -20)
+ .attr("r", 25)
+ .style("fill", "white")
+ .style("fill-opacity", 0.8)
+ .style("stroke", "#c05621")
+ .style("stroke-width", 2);
+
+ svg.append("text")
+ .attr("x", offset)
+ .attr("y", -15)
+ .attr("text-anchor", "middle")
+ .style("font-size", "18px")
+ .style("font-weight", "bold")
+ .style("fill", "#c05621")
+ .text(branchData.releaseOnly.length);
+
+ // Common count
+ svg.append("circle")
+ .attr("cx", 0)
+ .attr("cy", 0)
+ .attr("r", 25)
+ .style("fill", "white")
+ .style("fill-opacity", 0.8)
+ .style("stroke", "#38b2ac")
+ .style("stroke-width", 2);
+
+ svg.append("text")
+ .attr("x", 0)
+ .attr("y", 5)
+ .attr("text-anchor", "middle")
+ .style("font-size", "18px")
+ .style("font-weight", "bold")
+ .style("fill", "#38b2ac")
+ .text(branchData.common.length);
+
+ // Add intersection label
+ svg.append("rect")
+ .attr("x", -60)
+ .attr("y", radius + 10)
+ .attr("width", 120)
+ .attr("height", 24)
+ .attr("rx", 12)
+ .attr("ry", 12)
+ .style("fill", "#38b2ac");
+
+ svg.append("text")
+ .attr("x", 0)
+ .attr("y", radius + 26)
+ .attr("text-anchor", "middle")
+ .style("font-size", "14px")
+ .style("font-weight", "bold")
+ .style("fill", "white")
+ .text("Common Branches");
+
+ // Add branch names to the diagram
+ // Main only branches
+ let mainOnlyY = 40;
+ branchData.mainOnly.forEach((branch, i) => {
+ svg.append("text")
+ .attr("x", -offset)
+ .attr("y", mainOnlyY + i * 20)
+ .attr("text-anchor", "middle")
+ .style("font-size", "12px")
+ .style("fill", "#2d3748")
+ .text(branch.split('/')[1]);
+ });
+
+ // Release only branches
+ let releaseOnlyY = 40;
+ branchData.releaseOnly.forEach((branch, i) => {
+ svg.append("text")
+ .attr("x", offset)
+ .attr("y", releaseOnlyY + i * 20)
+ .attr("text-anchor", "middle")
+ .style("font-size", "12px")
+ .style("fill", "#2d3748")
+ .text(branch.split('/')[1]);
+ });
+
+ // Common branches
+ let commonY = -50;
+ branchData.common.forEach((branch, i) => {
+ svg.append("text")
+ .attr("x", 0)
+ .attr("y", commonY + i * 20)
+ .attr("text-anchor", "middle")
+ .style("font-size", "12px")
+ .style("fill", "#2d3748")
+ .text(branch.split('/')[1]);
+ });
+
+ // Add hover effects
+ mainCircle.on("mouseover", function() {
+ d3.select(this)
+ .transition()
+ .duration(300)
+ .style("fill-opacity", 0.9)
+ .attr("r", radius * 1.05);
+
+ // Highlight main branch items
+ document.querySelectorAll('#main-branches li, #main-only li').forEach(item => {
+ item.classList.add('highlight');
+ });
+ }).on("mouseout", function() {
+ d3.select(this)
+ .transition()
+ .duration(300)
+ .style("fill-opacity", 0.7)
+ .attr("r", radius);
+
+ // Remove highlight from main branch items
+ document.querySelectorAll('#main-branches li, #main-only li').forEach(item => {
+ item.classList.remove('highlight');
+ });
+ });
+
+ releaseCircle.on("mouseover", function() {
+ d3.select(this)
+ .transition()
+ .duration(300)
+ .style("fill-opacity", 0.9)
+ .attr("r", radius * 1.05);
+
+ // Highlight release branch items
+ document.querySelectorAll('#release-branches li, #release-only li').forEach(item => {
+ item.classList.add('highlight');
+ });
+ }).on("mouseout", function() {
+ d3.select(this)
+ .transition()
+ .duration(300)
+ .style("fill-opacity", 0.7)
+ .attr("r", radius);
+
+ // Remove highlight from release branch items
+ document.querySelectorAll('#release-branches li, #release-only li').forEach(item => {
+ item.classList.remove('highlight');
+ });
+ });
+
+ // Add statistics to the visualization
+ if (branchData.stats) {
+ const stats = branchData.stats;
+
+ // Add percentage labels
+ svg.append("text")
+ .attr("x", -offset)
+ .attr("y", -radius - 50)
+ .attr("text-anchor", "middle")
+ .style("font-size", "12px")
+ .style("fill", "#4a5568")
+ .text(`${stats.main_only_percentage}% of total`);
+
+ svg.append("text")
+ .attr("x", offset)
+ .attr("y", -radius - 50)
+ .attr("text-anchor", "middle")
+ .style("font-size", "12px")
+ .style("fill", "#4a5568")
+ .text(`${stats.release_only_percentage}% of total`);
+
+ svg.append("text")
+ .attr("x", 0)
+ .attr("y", -60)
+ .attr("text-anchor", "middle")
+ .style("font-size", "12px")
+ .style("fill", "#4a5568")
+ .text(`${stats.common_percentage}% of total`);
+ }
+}
+
+// Update branch lists in the UI
+async function updateBranchLists() {
+ const branchData = await loadBranchData();
+
+ // Update main branches list
+ const mainBranchesList = document.getElementById('main-branches');
+ mainBranchesList.innerHTML = '';
+ branchData.main.forEach(branch => {
+ const li = document.createElement('li');
+ li.textContent = branch;
+ mainBranchesList.appendChild(li);
+ });
+
+ // Update release branches list
+ const releaseBranchesList = document.getElementById('release-branches');
+ releaseBranchesList.innerHTML = '';
+ branchData.release.forEach(branch => {
+ const li = document.createElement('li');
+ li.textContent = branch;
+ releaseBranchesList.appendChild(li);
+ });
+
+ // Update main only branches list
+ const mainOnlyList = document.getElementById('main-only');
+ mainOnlyList.innerHTML = '';
+ branchData.mainOnly.forEach(branch => {
+ const li = document.createElement('li');
+ li.textContent = branch;
+ mainOnlyList.appendChild(li);
+ });
+
+ // Update release only branches list
+ const releaseOnlyList = document.getElementById('release-only');
+ releaseOnlyList.innerHTML = '';
+ branchData.releaseOnly.forEach(branch => {
+ const li = document.createElement('li');
+ li.textContent = branch;
+ releaseOnlyList.appendChild(li);
+ });
+
+ // Update common branches list
+ const commonBranchesList = document.getElementById('common-branches');
+ commonBranchesList.innerHTML = '';
+ branchData.common.forEach(branch => {
+ const li = document.createElement('li');
+ li.textContent = branch;
+ commonBranchesList.appendChild(li);
+ });
+}
+
+// Add repository information section
+function addRepositoryInfo() {
+ loadBranchData().then(data => {
+ if (data.stats && data.stats.repository) {
+ const repoInfo = document.createElement('section');
+ repoInfo.className = 'repository-info';
+
+ const timestamp = data.stats.timestamp ? new Date(data.stats.timestamp).toLocaleString() : 'Unknown';
+
+ repoInfo.innerHTML = `
+