From fca69adc423db115a9b7c7e1b208703b4595d836 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 13:57:56 -0500 Subject: [PATCH 01/13] add sites 41 meesga --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 05d6866..68f707f 100644 --- a/README.md +++ b/README.md @@ -13,4 +13,6 @@ 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 \ No newline at end of file From 2d07d6322808466bddc8d0a11e719c1d9ae66d61 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 20:09:45 -0500 Subject: [PATCH 02/13] add sites-42 --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 68f707f..b0e849b 100644 --- a/README.md +++ b/README.md @@ -15,4 +15,6 @@ If you are developing a production application, we recommend using TypeScript an hello all -add sites-41 \ No newline at end of file +add sites-41 + +add feature/sites-42 \ No newline at end of file From bf1b0c9e1bf6e09c6ba003ccc6b3ec44d1979b02 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 20:24:28 -0500 Subject: [PATCH 03/13] add actions files --- .env | 4 + .gitignore copy | 1 + DEPLOYMENT_GUIDE.md | 168 ++++ README copy.md | 85 ++ USER_GUIDE.md | 148 ++++ _config.yml | 43 + analyze_branch_data.py | 152 ++++ analyze_branch_data_for_actions.py | 188 +++++ analyzed_branch_data.json | 33 + branch_comparison_results.md | 27 + branch_data.json | 19 + configure.sh | 57 ++ generate_sample_data.sh | 39 + github_branch_analyzer.py | 124 +++ github_branch_analyzer_for_actions.py | 198 +++++ index copy.html | 94 +++ main_merged_branches.txt | 3 + release_merged_branches.txt | 3 + run.sh | 41 + script.js | 816 ++++++++++++++++++ styles.css | 532 ++++++++++++ todo.md | 10 + visualization/analyzed_branch_data.json | 39 + visualization/index.html | 121 +++ visualization/script.js | 1006 +++++++++++++++++++++++ visualization/styles.css | 156 ++++ visualization/timeline.js | 361 ++++++++ 27 files changed, 4468 insertions(+) create mode 100644 .env create mode 100644 .gitignore copy create mode 100644 DEPLOYMENT_GUIDE.md create mode 100644 README copy.md create mode 100644 USER_GUIDE.md create mode 100644 _config.yml create mode 100644 analyze_branch_data.py create mode 100644 analyze_branch_data_for_actions.py create mode 100644 analyzed_branch_data.json create mode 100644 branch_comparison_results.md create mode 100644 branch_data.json create mode 100755 configure.sh create mode 100755 generate_sample_data.sh create mode 100644 github_branch_analyzer.py create mode 100644 github_branch_analyzer_for_actions.py create mode 100644 index copy.html create mode 100644 main_merged_branches.txt create mode 100644 release_merged_branches.txt create mode 100755 run.sh create mode 100644 script.js create mode 100644 styles.css create mode 100644 todo.md create mode 100644 visualization/analyzed_branch_data.json create mode 100644 visualization/index.html create mode 100644 visualization/script.js create mode 100644 visualization/styles.css create mode 100644 visualization/timeline.js diff --git a/.env b/.env new file mode 100644 index 0000000..c8397aa --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +GITHUB_TOKEN=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 +GITHUB_REPO=correasebastian/layers +MAIN_BRANCH=main +RELEASE_BRANCH=release \ No newline at end of file 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/USER_GUIDE.md b/USER_GUIDE.md new file mode 100644 index 0000000..74a477f --- /dev/null +++ b/USER_GUIDE.md @@ -0,0 +1,148 @@ +# 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 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/analyzed_branch_data.json b/analyzed_branch_data.json new file mode 100644 index 0000000..6240f56 --- /dev/null +++ b/analyzed_branch_data.json @@ -0,0 +1,33 @@ +{ + "main": [ + "feature/sites-40" + ], + "release": [ + "feature/sites-42", + "feature/sites-41" + ], + "common": [], + "mainOnly": [ + "feature/sites-40" + ], + "releaseOnly": [ + "feature/sites-42", + "feature/sites-41" + ], + "timestamp": "2025-04-08T20:15:31.377187", + "repository": "correasebastian/layers", + "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:15:31.377187", + "analysis_time": "2025-04-08T20:18:41.090555", + "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..027913a --- /dev/null +++ b/branch_data.json @@ -0,0 +1,19 @@ +{ + "main": [ + "feature/sites-40" + ], + "release": [ + "feature/sites-42", + "feature/sites-41" + ], + "common": [], + "mainOnly": [ + "feature/sites-40" + ], + "releaseOnly": [ + "feature/sites-42", + "feature/sites-41" + ], + "timestamp": "2025-04-08T20:15:31.377187", + "repository": "correasebastian/layers" +} \ 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/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/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

+
    + +
+
+
+ +
+

Last updated: Loading...

+
+ + + + 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/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 = ` +

Repository Information

+
+
+

Repository

+

${data.stats.repository}

+
+
+

Last Updated

+

${timestamp}

+
+
+

Total Branches

+

${data.stats.total_branches}

+
+
+ `; + + // Insert after the overview section + const overviewSection = document.querySelector('.overview'); + overviewSection.parentNode.insertBefore(repoInfo, overviewSection.nextSibling); + } + }); +} + +// Add event listeners for interactive features +function addInteractiveFeatures() { + // Add hover effects for branch items + const branchItems = document.querySelectorAll('li'); + branchItems.forEach(item => { + item.addEventListener('mouseenter', function() { + this.classList.add('hover'); + }); + item.addEventListener('mouseleave', function() { + this.classList.remove('hover'); + }); + + // Add click event to show branch details + item.addEventListener('click', function() { + const branchName = this.textContent.trim(); + showBranchDetails(branchName); + }); + }); + + // Highlight related items when hovering over a branch + const mainBranches = document.querySelectorAll('#main-branches li'); + const releaseBranches = document.querySelectorAll('#release-branches li'); + const commonBranches = document.querySelectorAll('#common-branches li'); + const mainOnlyBranches = document.querySelectorAll('#main-only li'); + const releaseOnlyBranches = document.querySelectorAll('#release-only li'); + + // Function to find matching elements + function findMatchingElements(text) { + const matches = []; + document.querySelectorAll('li').forEach(li => { + if (li.textContent.trim() === text) { + matches.push(li); + } + }); + return matches; + } + + // Add highlighting for all branch items + document.querySelectorAll('li').forEach(item => { + item.addEventListener('mouseenter', function() { + const branchText = this.textContent.trim(); + const matchingElements = findMatchingElements(branchText); + + matchingElements.forEach(el => { + if (el !== this) { + el.classList.add('related'); + } + }); + }); + + item.addEventListener('mouseleave', function() { + const branchText = this.textContent.trim(); + const matchingElements = findMatchingElements(branchText); + + matchingElements.forEach(el => { + if (el !== this) { + el.classList.remove('related'); + } + }); + }); + }); + + // Make the visualization responsive + window.addEventListener('resize', function() { + createVennDiagram(); + }); +} + +// Setup filter and search functionality +function setupFilterAndSearch() { + // Create filter and search elements + const overviewSection = document.querySelector('.overview'); + const filterContainer = document.createElement('div'); + filterContainer.className = 'filter-container'; + filterContainer.innerHTML = ` + +
+ + + +
+ `; + + overviewSection.appendChild(filterContainer); + + // Add search functionality + const searchInput = document.getElementById('branch-search'); + const searchButton = document.getElementById('search-button'); + + function performSearch() { + const searchTerm = searchInput.value.toLowerCase(); + const allBranches = document.querySelectorAll('li'); + + allBranches.forEach(branch => { + const branchText = branch.textContent.toLowerCase(); + if (branchText.includes(searchTerm) || searchTerm === '') { + branch.style.display = ''; + } else { + branch.style.display = 'none'; + } + }); + } + + searchButton.addEventListener('click', performSearch); + searchInput.addEventListener('keyup', function(e) { + if (e.key === 'Enter') { + performSearch(); + } + }); + + // Add filter functionality + const filterMain = document.getElementById('filter-main'); + const filterRelease = document.getElementById('filter-release'); + const filterCommon = document.getElementById('filter-common'); + + function applyFilters() { + loadBranchData().then(branchData => { + const showMain = filterMain.checked; + const showRelease = filterRelease.checked; + const showCommon = filterCommon.checked; + + // Main branches + document.querySelectorAll('#main-branches li').forEach(item => { + const isCommon = branchData.common.includes(item.textContent.trim()); + if ((showMain && !isCommon) || (showCommon && isCommon)) { + item.style.display = ''; + } else { + item.style.display = 'none'; + } + }); + + // Release branches + document.querySelectorAll('#release-branches li').forEach(item => { + const isCommon = branchData.common.includes(item.textContent.trim()); + if ((showRelease && !isCommon) || (showCommon && isCommon)) { + item.style.display = ''; + } else { + item.style.display = 'none'; + } + }); + + // Main only branches + document.querySelectorAll('#main-only li').forEach(item => { + item.style.display = showMain ? '' : 'none'; + }); + + // Release only branches + document.querySelectorAll('#release-only li').forEach(item => { + item.style.display = showRelease ? '' : 'none'; + }); + + // Common branches + document.querySelectorAll('#common-branches li').forEach(item => { + item.style.display = showCommon ? '' : 'none'; + }); + }); + } + + filterMain.addEventListener('change', applyFilters); + filterRelease.addEventListener('change', applyFilters); + filterCommon.addEventListener('change', applyFilters); +} + +// Setup animations +function setupAnimations() { + // Add animation class to sections + document.querySelectorAll('section').forEach((section, index) => { + section.classList.add('animate-section'); + section.style.animationDelay = `${index * 0.1}s`; + }); + + // Add animation to branch items + document.querySelectorAll('li').forEach((item, index) => { + item.classList.add('animate-item'); + item.style.animationDelay = `${0.5 + (index * 0.05)}s`; + }); +} + +// Setup tooltips +function setupTooltips() { + // Create tooltip element + const tooltip = document.createElement('div'); + tooltip.className = 'tooltip'; + tooltip.style.display = 'none'; + document.body.appendChild(tooltip); + + // Add tooltips to branch items + loadBranchData().then(branchData => { + document.querySelectorAll('li').forEach(item => { + item.addEventListener('mouseenter', function(e) { + const branchName = this.textContent.trim(); + const branchType = getBranchType(branchName, branchData); + + tooltip.innerHTML = ` +
${branchName}
+
+
Type: ${branchType}
+
ID: ${getBranchId(branchName)}
+
+ `; + + tooltip.style.display = 'block'; + positionTooltip(e); + }); + + item.addEventListener('mousemove', positionTooltip); + + item.addEventListener('mouseleave', function() { + tooltip.style.display = 'none'; + }); + }); + }); + + function positionTooltip(e) { + const x = e.clientX + 10; + const y = e.clientY + 10; + + tooltip.style.left = `${x}px`; + tooltip.style.top = `${y}px`; + } + + function getBranchType(branchName, branchData) { + if (branchData.common.includes(branchName)) { + return 'Common (in both main and release)'; + } else if (branchData.mainOnly.includes(branchName)) { + return 'Main only'; + } else if (branchData.releaseOnly.includes(branchName)) { + return 'Release only'; + } + return 'Unknown'; + } + + function getBranchId(branchName) { + const match = branchName.match(/\d+$/); + return match ? match[0] : 'N/A'; + } +} + +// Show branch details in a modal +function showBranchDetails(branchName) { + loadBranchData().then(branchData => { + // Check if modal already exists + let modal = document.getElementById('branch-modal'); + + if (!modal) { + // Create modal + modal = document.createElement('div'); + modal.id = 'branch-modal'; + modal.className = 'modal'; + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + + // Add close button functionality + const closeButton = modal.querySelector('.close-button'); + closeButton.addEventListener('click', function() { + modal.style.display = 'none'; + }); + + // Close modal when clicking outside + window.addEventListener('click', function(event) { + if (event.target === modal) { + modal.style.display = 'none'; + } + }); + } + + // Update modal content + const modalTitle = document.getElementById('modal-title'); + const modalBody = document.getElementById('modal-body'); + + modalTitle.textContent = branchName; + + // Determine branch type and details + let branchType = ''; + let branchDetails = ''; + + if (branchData.common.includes(branchName)) { + branchType = 'Common Branch'; + branchDetails = ` +

This branch appears in both main and release branches.

+
+

Branch Information

+
    +
  • ID: ${branchName.split('-')[1] || 'N/A'}
  • +
  • Type: ${branchName.split('/')[0] || 'N/A'}
  • +
  • Feature: ${branchName.split('/')[1]?.split('-')[0] || 'N/A'}
  • +
+
+
+

Merge Status

+
    +
  • Merged to Main: Yes
  • +
  • Merged to Release: Yes
  • +
+
+ `; + } else if (branchData.mainOnly.includes(branchName)) { + branchType = 'Main Only Branch'; + branchDetails = ` +

This branch appears only in the main branch.

+
+

Branch Information

+
    +
  • ID: ${branchName.split('-')[1] || 'N/A'}
  • +
  • Type: ${branchName.split('/')[0] || 'N/A'}
  • +
  • Feature: ${branchName.split('/')[1]?.split('-')[0] || 'N/A'}
  • +
+
+
+

Merge Status

+
    +
  • Merged to Main: Yes
  • +
  • Merged to Release: No
  • +
+
+ `; + } else if (branchData.releaseOnly.includes(branchName)) { + branchType = 'Release Only Branch'; + branchDetails = ` +

This branch appears only in the release branch.

+
+

Branch Information

+
    +
  • ID: ${branchName.split('-')[1] || 'N/A'}
  • +
  • Type: ${branchName.split('/')[0] || 'N/A'}
  • +
  • Feature: ${branchName.split('/')[1]?.split('-')[0] || 'N/A'}
  • +
+
+
+

Merge Status

+
    +
  • Merged to Main: No
  • +
  • Merged to Release: Yes
  • +
+
+ `; + } + + modalBody.innerHTML = ` +
${branchType}
+ ${branchDetails} + `; + + // Show modal + modal.style.display = 'block'; + }); +} + +// Initialize the application +document.addEventListener('DOMContentLoaded', async function() { + // Load data and update UI + await createVennDiagram(); + await updateBranchLists(); + + // Add repository information + addRepositoryInfo(); + + // Setup interactive features + addInteractiveFeatures(); + setupFilterAndSearch(); + setupAnimations(); + setupTooltips(); +}); \ No newline at end of file diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..5b0f470 --- /dev/null +++ b/styles.css @@ -0,0 +1,532 @@ +/* Global Styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +:root { + --primary-color: #3182ce; + --secondary-color: #ed8936; + --common-color: #38b2ac; + --dark-bg: #2d3748; + --light-bg: #f8f9fa; + --card-bg: white; + --border-color: #edf2f7; + --text-color: #333; + --heading-color: #2d3748; + --subheading-color: #4a5568; +} + +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + line-height: 1.6; + color: var(--text-color); + background-color: var(--light-bg); + padding: 0; + margin: 0; +} + +header { + background-color: var(--dark-bg); + color: white; + text-align: center; + padding: 2.5rem 1rem; + margin-bottom: 2rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + position: relative; + overflow: hidden; +} + +header::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(135deg, rgba(49, 130, 206, 0.2) 0%, rgba(237, 137, 54, 0.2) 100%); + z-index: 1; +} + +header h1, header p { + position: relative; + z-index: 2; +} + +header h1 { + margin-bottom: 0.5rem; + font-size: 2.5rem; + letter-spacing: 0.05em; +} + +header p { + font-size: 1.2rem; + opacity: 0.9; +} + +main { + max-width: 1200px; + margin: 0 auto; + padding: 0 1.5rem; +} + +section { + background-color: var(--card-bg); + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05), 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 2.5rem; + margin-bottom: 2.5rem; + transition: transform 0.3s ease, box-shadow 0.3s ease; +} + +section:hover { + transform: translateY(-5px); + box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1); +} + +h2 { + color: var(--heading-color); + margin-bottom: 1.5rem; + border-bottom: 2px solid var(--border-color); + padding-bottom: 0.8rem; + font-size: 1.8rem; + position: relative; +} + +h2::after { + content: ''; + position: absolute; + bottom: -2px; + left: 0; + width: 60px; + height: 2px; + background-color: var(--primary-color); +} + +h3 { + color: var(--subheading-color); + margin-bottom: 1rem; + font-size: 1.4rem; +} + +ul { + list-style-type: none; +} + +li { + padding: 0.75rem 0.5rem; + border-bottom: 1px solid var(--border-color); + transition: all 0.2s ease; + cursor: pointer; +} + +li:last-child { + border-bottom: none; +} + +footer { + text-align: center; + padding: 2.5rem; + background-color: var(--dark-bg); + color: white; + margin-top: 3rem; + position: relative; +} + +footer::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(to right, var(--primary-color), var(--secondary-color)); +} + +/* Branch Stats */ +.branch-stats { + display: flex; + flex-wrap: wrap; + gap: 1.5rem; + justify-content: space-between; + margin-bottom: 2rem; +} + +.stat-card { + flex: 1; + min-width: 200px; + background-color: var(--border-color); + padding: 1.8rem; + border-radius: 12px; + text-align: center; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); + transition: transform 0.3s ease, box-shadow 0.3s ease; + position: relative; + overflow: hidden; +} + +.stat-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.1); +} + +.stat-card:nth-child(1)::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background-color: var(--primary-color); +} + +.stat-card:nth-child(2)::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background-color: var(--secondary-color); +} + +.stat-card:nth-child(3)::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 4px; + height: 100%; + background-color: var(--common-color); +} + +.stat-card h3 { + margin-bottom: 0.5rem; +} + +.stat-card p { + font-size: 1.1rem; +} + +.stat-card span { + font-size: 1.8rem; + font-weight: bold; + display: block; + margin-top: 0.5rem; + color: var(--heading-color); +} + +/* Branch Lists */ +.branch-list { + display: flex; + flex-wrap: wrap; + gap: 2.5rem; +} + +.branch-column { + flex: 1; + min-width: 250px; +} + +.branch-column h3 { + margin-bottom: 1rem; + padding-left: 0.5rem; + border-left: 4px solid; +} + +.branch-column:nth-child(1) h3 { + border-color: var(--primary-color); +} + +.branch-column:nth-child(2) h3 { + border-color: var(--secondary-color); +} + +.branch-column ul { + background-color: #f7fafc; + border-radius: 8px; + padding: 1rem; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); +} + +/* Visualization */ +.visualization { + padding: 3rem 2rem; +} + +#venn-diagram { + height: 450px; + margin: 0 auto; + transition: all 0.3s ease; +} + +/* Responsive Design */ +@media (max-width: 768px) { + header h1 { + font-size: 2rem; + } + + .branch-stats { + flex-direction: column; + } + + .branch-list { + flex-direction: column; + } + + .stat-card, .branch-column { + width: 100%; + } + + section { + padding: 1.5rem; + } +} + +/* Branch Item Styling */ +li { + display: flex; + align-items: center; + padding: 0.75rem 0.8rem; + transition: all 0.2s ease; + border-radius: 4px; + margin-bottom: 0.5rem; +} + +li.hover, li:hover { + background-color: #edf2f7; + transform: translateX(5px); +} + +li.highlight { + background-color: rgba(49, 130, 206, 0.2); + transform: translateX(5px); +} + +li.related { + background-color: rgba(56, 178, 172, 0.2); + transform: translateX(5px); +} + +li::before { + content: '→'; + margin-right: 0.5rem; + color: #718096; +} + +/* Common Branch Highlight */ +#common-branches li { + background-color: rgba(56, 178, 172, 0.1); + border-left: 4px solid var(--common-color); + padding-left: 1rem; + margin-bottom: 0.5rem; + border-radius: 4px; +} + +#common-branches li::before { + content: '✓'; + color: var(--common-color); +} + +/* Main Only Branch Styling */ +#main-only li { + border-left: 4px solid var(--primary-color); + padding-left: 1rem; + margin-bottom: 0.5rem; + border-radius: 4px; + background-color: rgba(49, 130, 206, 0.05); +} + +#main-only li::before { + color: var(--primary-color); +} + +/* Release Only Branch Styling */ +#release-only li { + border-left: 4px solid var(--secondary-color); + padding-left: 1rem; + margin-bottom: 0.5rem; + border-radius: 4px; + background-color: rgba(237, 137, 54, 0.05); +} + +#release-only li::before { + color: var(--secondary-color); +} + +/* Animation */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.animate-section { + opacity: 0; + animation: fadeIn 0.5s ease-out forwards; +} + +.animate-item { + opacity: 0; + animation: fadeIn 0.3s ease-out forwards; +} + +/* Filter and Search */ +.filter-container { + margin-top: 1.5rem; + padding: 1rem; + background-color: #f7fafc; + border-radius: 8px; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); +} + +.search-box { + display: flex; + margin-bottom: 1rem; +} + +.search-box input { + flex: 1; + padding: 0.5rem; + border: 1px solid #e2e8f0; + border-radius: 4px 0 0 4px; + font-size: 1rem; +} + +.search-box button { + padding: 0.5rem 1rem; + background-color: var(--primary-color); + color: white; + border: none; + border-radius: 0 4px 4px 0; + cursor: pointer; + transition: background-color 0.2s; +} + +.search-box button:hover { + background-color: #2b6cb0; +} + +.filter-options { + display: flex; + gap: 1.5rem; + flex-wrap: wrap; +} + +.filter-options label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; +} + +.filter-options input[type="checkbox"] { + width: 16px; + height: 16px; +} + +/* Tooltip */ +.tooltip { + position: fixed; + background-color: white; + border-radius: 4px; + padding: 0.75rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 1000; + max-width: 300px; + pointer-events: none; +} + +.tooltip-title { + font-weight: bold; + margin-bottom: 0.5rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-color); +} + +.tooltip-content { + font-size: 0.9rem; +} + +/* Modal */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1001; + justify-content: center; + align-items: center; +} + +.modal-content { + background-color: white; + border-radius: 8px; + padding: 2rem; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + position: relative; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.close-button { + position: absolute; + top: 1rem; + right: 1rem; + font-size: 1.5rem; + cursor: pointer; + color: #718096; + transition: color 0.2s; +} + +.close-button:hover { + color: #2d3748; +} + +#modal-title { + margin-bottom: 1.5rem; + padding-bottom: 0.5rem; + border-bottom: 2px solid var(--border-color); +} + +.branch-type { + display: inline-block; + padding: 0.25rem 0.75rem; + background-color: #edf2f7; + border-radius: 4px; + margin-bottom: 1rem; + font-weight: bold; +} + +.branch-details-section { + margin-bottom: 1.5rem; +} + +.branch-details-section h3 { + margin-bottom: 0.5rem; + font-size: 1.2rem; +} + +.branch-details-section ul { + background-color: #f7fafc; + border-radius: 4px; + padding: 0.75rem; +} + +.branch-details-section li { + padding: 0.5rem; + border-bottom: 1px solid #e2e8f0; +} + +.branch-details-section li:last-child { + border-bottom: none; +} + +.branch-details-section li::before { + content: none; +} diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..55c338d --- /dev/null +++ b/todo.md @@ -0,0 +1,10 @@ +# GitHub Branch Analyzer for Private Repositories + +- [x] Set up GitHub API access +- [x] Create data extraction script +- [x] Fetch private repo branch data +- [x] Process and analyze branch data +- [x] Adapt visualization website +- [x] Test solution locally +- [x] Deploy solution +- [x] Provide usage instructions diff --git a/visualization/analyzed_branch_data.json b/visualization/analyzed_branch_data.json new file mode 100644 index 0000000..0c27419 --- /dev/null +++ b/visualization/analyzed_branch_data.json @@ -0,0 +1,39 @@ +{ + "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": "2025-04-08T15:18:44-04:00", + "repository": "example/private-repo", + "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", + "timestamp": "2025-04-08T15:18:44-04:00", + "analysis_time": "2025-04-08T15:19:23.331491", + "common_percentage": 20.0, + "main_only_percentage": 40.0, + "release_only_percentage": 40.0 + } +} \ No newline at end of file diff --git a/visualization/index.html b/visualization/index.html new file mode 100644 index 0000000..3da99f7 --- /dev/null +++ b/visualization/index.html @@ -0,0 +1,121 @@ + + + + + + Git Branch Comparison + + + + +
+

Git Branch Comparison

+

Visualizing differences between main and release branches

+
+ +
+
+

Analysis Date

+
+ + +
+
+

Current analysis: Latest

+

Select a date to see branch status as of that date

+
+
+ +
+

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

+
    + +
+
+ +
+

Historical Analysis

+
+ +
+ +
+
+
+ +
+
+
+ +
+

Created with ❤️ by Manus AI

+

Last updated: Loading...

+
+ + + + + diff --git a/visualization/script.js b/visualization/script.js new file mode 100644 index 0000000..3d83a1f --- /dev/null +++ b/visualization/script.js @@ -0,0 +1,1006 @@ +// Global variables for date handling +let currentAnalysisDate = 'latest'; +let availableDates = []; + +// Load data from JSON file with date parameter +async function loadBranchData(date = null) { + try { + // Construct URL with date parameter if provided + let url = 'analyzed_branch_data.json'; + if (date && date !== 'latest') { + // Format date for filename (remove dashes) + const dateStr = date.replace(/-/g, ''); + url = `analyzed_branch_data_${dateStr}.json`; + } + + console.log(`Loading data from: ${url}`); + + const response = await fetch(url); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + + // Update current date display + updateCurrentDateDisplay(data.stats?.analysis_date || 'latest'); + + 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, + analysis_date: 'latest' + } + }; + } +} + +// Update the current date display +function updateCurrentDateDisplay(date) { + const currentDateElement = document.getElementById('current-date'); + currentAnalysisDate = date; + + if (date === 'latest') { + currentDateElement.textContent = 'Latest'; + } else { + // Format date for display (YYYY-MM-DD) + const displayDate = new Date(date); + currentDateElement.textContent = displayDate.toLocaleDateString(); + } +} + +// Set up date picker functionality +function setupDatePicker() { + const datePicker = document.getElementById('analysis-date'); + const updateButton = document.getElementById('update-date-button'); + + // Set default date to today + const today = new Date(); + datePicker.valueAsDate = today; + + // Load data for the selected date when button is clicked + updateButton.addEventListener('click', async function() { + // Show loading indicator + const loadingIndicator = document.createElement('div'); + loadingIndicator.className = 'loading'; + updateButton.appendChild(loadingIndicator); + updateButton.disabled = true; + + try { + const selectedDate = datePicker.value; + const branchData = await loadBranchData(selectedDate); + + // Update visualization and lists + createVennDiagram(branchData); + updateBranchLists(branchData); + + // Add this date to available dates if not already there + if (!availableDates.includes(selectedDate)) { + availableDates.push(selectedDate); + updateHistoryDates(); + } + } catch (error) { + console.error('Error updating data:', error); + alert('Failed to load data for the selected date. Please try another date.'); + } finally { + // Remove loading indicator + updateButton.removeChild(loadingIndicator); + updateButton.disabled = false; + } + }); +} + +// Create Venn Diagram visualization +async function createVennDiagram(branchData = null) { + // Load data if not provided + if (!branchData) { + 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}`; + } + + // Update last updated timestamp + if (branchData.stats && branchData.stats.timestamp) { + const timestamp = new Date(branchData.stats.timestamp).toLocaleString(); + document.getElementById('last-updated').textContent = timestamp; + } + + 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`); + } + + // Add analysis date if available + if (branchData.stats && branchData.stats.analysis_date && branchData.stats.analysis_date !== 'latest') { + const analysisDate = new Date(branchData.stats.analysis_date).toLocaleDateString(); + + svg.append("text") + .attr("x", 0) + .attr("y", -radius - 70) + .attr("text-anchor", "middle") + .style("font-size", "14px") + .style("font-weight", "bold") + .style("fill", "#4a5568") + .text(`Analysis Date: ${analysisDate}`); + } +} + +// Update branch lists in the UI +async function updateBranchLists(branchData = null) { + // Load data if not provided + if (!branchData) { + 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 event listeners to branch items + addInteractiveFeatures(); +} + +// Add repository information section +function addRepositoryInfo(branchData = null) { + // Use provided data or load it + const dataPromise = branchData ? Promise.resolve(branchData) : loadBranchData(); + + dataPromise.then(data => { + if (data.stats && data.stats.repository) { + // Check if repository info section already exists + let repoInfo = document.querySelector('.repository-info'); + + if (!repoInfo) { + repoInfo = document.createElement('section'); + repoInfo.className = 'repository-info'; + + // Insert after the overview section + const overviewSection = document.querySelector('.overview'); + overviewSection.parentNode.insertBefore(repoInfo, overviewSection.nextSibling); + } + + const timestamp = data.stats.timestamp ? new Date(data.stats.timestamp).toLocaleString() : 'Unknown'; + const analysisDate = data.stats.analysis_date && data.stats.analysis_date !== 'latest' + ? new Date(data.stats.analysis_date).toLocaleDateString() + : 'Latest'; + + repoInfo.innerHTML = ` +

Repository Information

+
+
+

Repository

+

${data.stats.repository}

+
+
+

Analysis Date

+

${analysisDate}

+
+
+

Last Updated

+

${timestamp}

+
+
+

Total Branches

+

${data.stats.total_branches}

+
+
+ `; + } + }); +} + +// Add event listeners for interactive features +function addInteractiveFeatures() { + // Add hover effects for branch items + const branchItems = document.querySelectorAll('li'); + branchItems.forEach(item => { + item.addEventListener('mouseenter', function() { + this.classList.add('hover'); + }); + item.addEventListener('mouseleave', function() { + this.classList.remove('hover'); + }); + + // Add click event to show branch details + item.addEventListener('click', function() { + const branchName = this.textContent.trim(); + showBranchDetails(branchName); + }); + }); + + // Function to find matching elements + function findMatchingElements(text) { + const matches = []; + document.querySelectorAll('li').forEach(li => { + if (li.textContent.trim() === text) { + matches.push(li); + } + }); + return matches; + } + + // Add highlighting for all branch items + document.querySelectorAll('li').forEach(item => { + item.addEventListener('mouseenter', function() { + const branchText = this.textContent.trim(); + const matchingElements = findMatchingElements(branchText); + + matchingElements.forEach(el => { + if (el !== this) { + el.classList.add('related'); + } + }); + }); + + item.addEventListener('mouseleave', function() { + const branchText = this.textContent.trim(); + const matchingElements = findMatchingElements(branchText); + + matchingElements.forEach(el => { + if (el !== this) { + el.classList.remove('related'); + } + }); + }); + }); +} + +// Setup filter and search functionality +function setupFilterAndSearch() { + // Create filter and search elements + const overviewSection = document.querySelector('.overview'); + const filterContainer = document.createElement('div'); + filterContainer.className = 'filter-container'; + filterContainer.innerHTML = ` + +
+ + + +
+ `; + + overviewSection.appendChild(filterContainer); + + // Add search functionality + const searchInput = document.getElementById('branch-search'); + const searchButton = document.getElementById('search-button'); + + function performSearch() { + const searchTerm = searchInput.value.toLowerCase(); + const allBranches = document.querySelectorAll('li'); + + allBranches.forEach(branch => { + const branchText = branch.textContent.toLowerCase(); + if (branchText.includes(searchTerm) || searchTerm === '') { + branch.style.display = ''; + } else { + branch.style.display = 'none'; + } + }); + } + + searchButton.addEventListener('click', performSearch); + searchInput.addEventListener('keyup', function(e) { + if (e.key === 'Enter') { + performSearch(); + } + }); + + // Add filter functionality + const filterMain = document.getElementById('filter-main'); + const filterRelease = document.getElementById('filter-release'); + const filterCommon = document.getElementById('filter-common'); + + function applyFilters() { + loadBranchData().then(branchData => { + const showMain = filterMain.checked; + const showRelease = filterRelease.checked; + const showCommon = filterCommon.checked; + + // Main branches + document.querySelectorAll('#main-branches li').forEach(item => { + const isCommon = branchData.common.includes(item.textContent.trim()); + if ((showMain && !isCommon) || (showCommon && isCommon)) { + item.style.display = ''; + } else { + item.style.display = 'none'; + } + }); + + // Release branches + document.querySelectorAll('#release-branches li').forEach(item => { + const isCommon = branchData.common.includes(item.textContent.trim()); + if ((showRelease && !isCommon) || (showCommon && isCommon)) { + item.style.display = ''; + } else { + item.style.display = 'none'; + } + }); + + // Main only branches + document.querySelectorAll('#main-only li').forEach(item => { + item.style.display = showMain ? '' : 'none'; + }); + + // Release only branches + document.querySelectorAll('#release-only li').forEach(item => { + item.style.display = showRelease ? '' : 'none'; + }); + + // Common branches + document.querySelectorAll('#common-branches li').forEach(item => { + item.style.display = showCommon ? '' : 'none'; + }); + }); + } + + filterMain.addEventListener('change', applyFilters); + filterRelease.addEventListener('change', applyFilters); + filterCommon.addEventListener('change', applyFilters); +} + +// Setup history view functionality +function setupHistoryView() { + const showHistoryButton = document.getElementById('show-history-button'); + const historyDatesContainer = document.getElementById('history-dates'); + const historyChartContainer = document.getElementById('history-chart'); + + // Initially hide the history dates + historyDatesContainer.style.display = 'none'; + + // Toggle history dates visibility + showHistoryButton.addEventListener('click', function() { + if (historyDatesContainer.style.display === 'none') { + historyDatesContainer.style.display = 'flex'; + showHistoryButton.textContent = 'Hide History'; + + // Discover available dates + discoverAvailableDates(); + } else { + historyDatesContainer.style.display = 'none'; + showHistoryButton.textContent = 'Show History'; + } + }); +} + +// Discover available date files +async function discoverAvailableDates() { + const historyDatesContainer = document.getElementById('history-dates'); + historyDatesContainer.innerHTML = '
'; + + try { + // In a real implementation, you would fetch a list of available dates from the server + // For this demo, we'll simulate discovering dates by trying to load a few recent dates + const today = new Date(); + const potentialDates = []; + + // Try the last 30 days + for (let i = 0; i < 30; i++) { + const date = new Date(today); + date.setDate(date.getDate() - i); + potentialDates.push(date.toISOString().split('T')[0]); + } + + // Add any dates we already know about + availableDates.forEach(date => { + if (!potentialDates.includes(date)) { + potentialDates.push(date); + } + }); + + // Clear existing dates + availableDates = []; + + // Check each date + const checkPromises = potentialDates.map(async date => { + try { + const dateStr = date.replace(/-/g, ''); + const url = `analyzed_branch_data_${dateStr}.json`; + const response = await fetch(url, { method: 'HEAD' }); + + if (response.ok) { + availableDates.push(date); + } + } catch (error) { + // Ignore errors for files that don't exist + } + }); + + await Promise.all(checkPromises); + + // Always add 'latest' + if (!availableDates.includes('latest')) { + availableDates.push('latest'); + } + + // Sort dates (latest first) + availableDates.sort((a, b) => { + if (a === 'latest') return -1; + if (b === 'latest') return 1; + return new Date(b) - new Date(a); + }); + + // Update the UI + updateHistoryDates(); + } catch (error) { + console.error('Error discovering dates:', error); + historyDatesContainer.innerHTML = 'Failed to load history dates.'; + } +} + +// Update history dates in the UI +function updateHistoryDates() { + const historyDatesContainer = document.getElementById('history-dates'); + historyDatesContainer.innerHTML = ''; + + if (availableDates.length === 0) { + historyDatesContainer.innerHTML = 'No historical data available.'; + return; + } + + availableDates.forEach(date => { + const dateButton = document.createElement('button'); + dateButton.className = 'history-date-button'; + + if (date === currentAnalysisDate) { + dateButton.classList.add('active'); + } + + if (date === 'latest') { + dateButton.textContent = 'Latest'; + } else { + const displayDate = new Date(date); + dateButton.textContent = displayDate.toLocaleDateString(); + } + + dateButton.addEventListener('click', async () => { + try { + // Update active button + document.querySelectorAll('.history-date-button').forEach(btn => { + btn.classList.remove('active'); + }); + dateButton.classList.add('active'); + + // Load data for this date + const branchData = await loadBranchData(date); + + // Update visualization and lists + createVennDiagram(branchData); + updateBranchLists(branchData); + addRepositoryInfo(branchData); + + // Update date picker to match selected date + if (date !== 'latest') { + document.getElementById('analysis-date').value = date; + } + } catch (error) { + console.error('Error loading historical data:', error); + alert('Failed to load data for the selected date.'); + } + }); + + historyDatesContainer.appendChild(dateButton); + }); +} + +// Show branch details in a modal +function showBranchDetails(branchName) { + loadBranchData().then(branchData => { + // Check if modal already exists + let modal = document.getElementById('branch-modal'); + + if (!modal) { + // Create modal + modal = document.createElement('div'); + modal.id = 'branch-modal'; + modal.className = 'modal'; + modal.innerHTML = ` + + `; + document.body.appendChild(modal); + + // Add close button functionality + const closeButton = modal.querySelector('.close-button'); + closeButton.addEventListener('click', function() { + modal.style.display = 'none'; + }); + + // Close modal when clicking outside + window.addEventListener('click', function(event) { + if (event.target === modal) { + modal.style.display = 'none'; + } + }); + } + + // Update modal content + const modalTitle = document.getElementById('modal-title'); + const modalBody = document.getElementById('modal-body'); + + modalTitle.textContent = branchName; + + // Determine branch type and details + let branchType = ''; + let branchDetails = ''; + + if (branchData.common.includes(branchName)) { + branchType = 'Common Branch'; + branchDetails = ` +

This branch appears in both main and release branches.

+
+

Branch Information

+
    +
  • ID: ${branchName.split('-')[1] || 'N/A'}
  • +
  • Type: ${branchName.split('/')[0] || 'N/A'}
  • +
  • Feature: ${branchName.split('/')[1]?.split('-')[0] || 'N/A'}
  • +
+
+
+

Merge Status

+
    +
  • Merged to Main: Yes
  • +
  • Merged to Release: Yes
  • +
+
+ `; + } else if (branchData.mainOnly.includes(branchName)) { + branchType = 'Main Only Branch'; + branchDetails = ` +

This branch appears only in the main branch.

+
+

Branch Information

+
    +
  • ID: ${branchName.split('-')[1] || 'N/A'}
  • +
  • Type: ${branchName.split('/')[0] || 'N/A'}
  • +
  • Feature: ${branchName.split('/')[1]?.split('-')[0] || 'N/A'}
  • +
+
+
+

Merge Status

+
    +
  • Merged to Main: Yes
  • +
  • Merged to Release: No
  • +
+
+ `; + } else if (branchData.releaseOnly.includes(branchName)) { + branchType = 'Release Only Branch'; + branchDetails = ` +

This branch appears only in the release branch.

+
+

Branch Information

+
    +
  • ID: ${branchName.split('-')[1] || 'N/A'}
  • +
  • Type: ${branchName.split('/')[0] || 'N/A'}
  • +
  • Feature: ${branchName.split('/')[1]?.split('-')[0] || 'N/A'}
  • +
+
+
+

Merge Status

+
    +
  • Merged to Main: No
  • +
  • Merged to Release: Yes
  • +
+
+ `; + } + + // Add analysis date information + let dateInfo = ''; + if (currentAnalysisDate && currentAnalysisDate !== 'latest') { + const displayDate = new Date(currentAnalysisDate).toLocaleDateString(); + dateInfo = `

Analysis as of: ${displayDate}

`; + } + + modalBody.innerHTML = ` +
${branchType}
+ ${dateInfo} + ${branchDetails} + `; + + // Show modal + modal.style.display = 'block'; + }); +} + +// Initialize the application +document.addEventListener('DOMContentLoaded', async function() { + // Set up date picker + setupDatePicker(); + + // Load data and update UI + const branchData = await loadBranchData(); + await createVennDiagram(branchData); + await updateBranchLists(branchData); + + // Add repository information + addRepositoryInfo(branchData); + + // Setup interactive features + addInteractiveFeatures(); + setupFilterAndSearch(); + setupHistoryView(); + + // Make the visualization responsive + window.addEventListener('resize', function() { + createVennDiagram(); + }); +}); diff --git a/visualization/styles.css b/visualization/styles.css new file mode 100644 index 0000000..6db2241 --- /dev/null +++ b/visualization/styles.css @@ -0,0 +1,156 @@ +/* Add styles for the date selector section */ +.date-selector { + background-color: #f8f9fa; + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.date-control { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 10px; +} + +#analysis-date { + padding: 8px 12px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 16px; +} + +#update-date-button { + padding: 8px 16px; + background-color: #4299e1; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s; +} + +#update-date-button:hover { + background-color: #3182ce; +} + +.date-info { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 14px; + color: #4a5568; +} + +#current-date { + font-weight: bold; + color: #2d3748; +} + +.date-help { + font-style: italic; +} + +/* Styles for the history view section */ +.history-view { + background-color: #f8f9fa; + border-radius: 8px; + padding: 20px; + margin-top: 30px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.history-controls { + display: flex; + flex-direction: column; + gap: 15px; + margin-bottom: 20px; +} + +#show-history-button { + padding: 8px 16px; + background-color: #38b2ac; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 16px; + transition: background-color 0.3s; + align-self: flex-start; +} + +#show-history-button:hover { + background-color: #319795; +} + +.history-dates { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 10px; +} + +.history-date-button { + padding: 6px 12px; + background-color: #e2e8f0; + border: 1px solid #cbd5e0; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.history-date-button:hover { + background-color: #cbd5e0; +} + +.history-date-button.active { + background-color: #4299e1; + color: white; + border-color: #3182ce; +} + +.history-chart { + height: 300px; + margin-top: 20px; + border: 1px solid #e2e8f0; + border-radius: 4px; + padding: 10px; + background-color: white; +} + +/* Loading indicator */ +.loading { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid rgba(0, 0, 0, 0.1); + border-radius: 50%; + border-top-color: #4299e1; + animation: spin 1s ease-in-out infinite; + margin-left: 10px; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .date-control { + flex-direction: column; + align-items: stretch; + } + + .date-info { + flex-direction: column; + align-items: flex-start; + gap: 5px; + } + + .history-dates { + flex-direction: column; + } +} diff --git a/visualization/timeline.js b/visualization/timeline.js new file mode 100644 index 0000000..1387830 --- /dev/null +++ b/visualization/timeline.js @@ -0,0 +1,361 @@ +// Timeline visualization component for branch history +function createTimelineVisualization() { + const historyChartContainer = document.getElementById('history-chart'); + + // Clear any existing content + historyChartContainer.innerHTML = ''; + + // If no dates available, show message + if (availableDates.length <= 1) { + historyChartContainer.innerHTML = '

Not enough historical data available to create a timeline. Run analysis for multiple dates to see branch evolution over time.

'; + return; + } + + // Create SVG container for timeline + const margin = {top: 40, right: 30, bottom: 50, left: 50}; + const width = historyChartContainer.clientWidth - margin.left - margin.right; + const height = 250 - margin.top - margin.bottom; + + const svg = d3.select("#history-chart") + .append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", `translate(${margin.left},${margin.top})`); + + // Filter out 'latest' and convert dates to Date objects + const dateObjects = availableDates + .filter(d => d !== 'latest') + .map(d => ({date: new Date(d), dateStr: d})) + .sort((a, b) => a.date - b.date); + + // If less than 2 dates, show message + if (dateObjects.length < 2) { + historyChartContainer.innerHTML = '

Not enough historical data available to create a timeline. Run analysis for multiple dates to see branch evolution over time.

'; + return; + } + + // Load data for each date + Promise.all(dateObjects.map(d => loadBranchData(d.dateStr))) + .then(results => { + // Prepare data for visualization + const timelineData = results.map((data, i) => ({ + date: dateObjects[i].date, + dateStr: dateObjects[i].dateStr, + mainCount: data.stats.main_count, + releaseCount: data.stats.release_count, + commonCount: data.stats.common_count + })); + + // Create scales + const xScale = d3.scaleTime() + .domain(d3.extent(timelineData, d => d.date)) + .range([0, width]); + + const yScale = d3.scaleLinear() + .domain([0, d3.max(timelineData, d => Math.max(d.mainCount, d.releaseCount))]) + .nice() + .range([height, 0]); + + // Add X axis + svg.append("g") + .attr("transform", `translate(0,${height})`) + .call(d3.axisBottom(xScale).ticks(5).tickFormat(d3.timeFormat("%b %d"))) + .selectAll("text") + .style("text-anchor", "end") + .attr("dx", "-.8em") + .attr("dy", ".15em") + .attr("transform", "rotate(-45)"); + + // Add Y axis + svg.append("g") + .call(d3.axisLeft(yScale)); + + // Add X axis label + svg.append("text") + .attr("text-anchor", "middle") + .attr("x", width / 2) + .attr("y", height + margin.bottom - 5) + .text("Date"); + + // Add Y axis label + svg.append("text") + .attr("text-anchor", "middle") + .attr("transform", "rotate(-90)") + .attr("y", -margin.left + 15) + .attr("x", -height / 2) + .text("Branch Count"); + + // Add title + svg.append("text") + .attr("x", width / 2) + .attr("y", -margin.top / 2) + .attr("text-anchor", "middle") + .style("font-size", "16px") + .style("font-weight", "bold") + .text("Branch Evolution Over Time"); + + // Create line generators + const mainLine = d3.line() + .x(d => xScale(d.date)) + .y(d => yScale(d.mainCount)) + .curve(d3.curveMonotoneX); + + const releaseLine = d3.line() + .x(d => xScale(d.date)) + .y(d => yScale(d.releaseCount)) + .curve(d3.curveMonotoneX); + + const commonLine = d3.line() + .x(d => xScale(d.date)) + .y(d => yScale(d.commonCount)) + .curve(d3.curveMonotoneX); + + // Add the lines + svg.append("path") + .datum(timelineData) + .attr("fill", "none") + .attr("stroke", "#3182ce") + .attr("stroke-width", 2) + .attr("d", mainLine); + + svg.append("path") + .datum(timelineData) + .attr("fill", "none") + .attr("stroke", "#dd6b20") + .attr("stroke-width", 2) + .attr("d", releaseLine); + + svg.append("path") + .datum(timelineData) + .attr("fill", "none") + .attr("stroke", "#38b2ac") + .attr("stroke-width", 2) + .attr("d", commonLine); + + // Add dots for each data point + svg.selectAll(".main-dot") + .data(timelineData) + .enter() + .append("circle") + .attr("class", "main-dot") + .attr("cx", d => xScale(d.date)) + .attr("cy", d => yScale(d.mainCount)) + .attr("r", 5) + .attr("fill", "#3182ce") + .on("mouseover", function(event, d) { + d3.select(this).attr("r", 8); + showTooltip(event, d, "main"); + }) + .on("mouseout", function() { + d3.select(this).attr("r", 5); + hideTooltip(); + }) + .on("click", function(event, d) { + loadBranchData(d.dateStr).then(data => { + createVennDiagram(data); + updateBranchLists(data); + addRepositoryInfo(data); + updateCurrentDateDisplay(d.dateStr); + + // Update date picker to match selected date + document.getElementById('analysis-date').value = d.dateStr; + + // Update active button in history dates + document.querySelectorAll('.history-date-button').forEach(btn => { + btn.classList.remove('active'); + if (btn.textContent === new Date(d.dateStr).toLocaleDateString()) { + btn.classList.add('active'); + } + }); + }); + }); + + svg.selectAll(".release-dot") + .data(timelineData) + .enter() + .append("circle") + .attr("class", "release-dot") + .attr("cx", d => xScale(d.date)) + .attr("cy", d => yScale(d.releaseCount)) + .attr("r", 5) + .attr("fill", "#dd6b20") + .on("mouseover", function(event, d) { + d3.select(this).attr("r", 8); + showTooltip(event, d, "release"); + }) + .on("mouseout", function() { + d3.select(this).attr("r", 5); + hideTooltip(); + }) + .on("click", function(event, d) { + loadBranchData(d.dateStr).then(data => { + createVennDiagram(data); + updateBranchLists(data); + addRepositoryInfo(data); + updateCurrentDateDisplay(d.dateStr); + + // Update date picker to match selected date + document.getElementById('analysis-date').value = d.dateStr; + + // Update active button in history dates + document.querySelectorAll('.history-date-button').forEach(btn => { + btn.classList.remove('active'); + if (btn.textContent === new Date(d.dateStr).toLocaleDateString()) { + btn.classList.add('active'); + } + }); + }); + }); + + svg.selectAll(".common-dot") + .data(timelineData) + .enter() + .append("circle") + .attr("class", "common-dot") + .attr("cx", d => xScale(d.date)) + .attr("cy", d => yScale(d.commonCount)) + .attr("r", 5) + .attr("fill", "#38b2ac") + .on("mouseover", function(event, d) { + d3.select(this).attr("r", 8); + showTooltip(event, d, "common"); + }) + .on("mouseout", function() { + d3.select(this).attr("r", 5); + hideTooltip(); + }) + .on("click", function(event, d) { + loadBranchData(d.dateStr).then(data => { + createVennDiagram(data); + updateBranchLists(data); + addRepositoryInfo(data); + updateCurrentDateDisplay(d.dateStr); + + // Update date picker to match selected date + document.getElementById('analysis-date').value = d.dateStr; + + // Update active button in history dates + document.querySelectorAll('.history-date-button').forEach(btn => { + btn.classList.remove('active'); + if (btn.textContent === new Date(d.dateStr).toLocaleDateString()) { + btn.classList.add('active'); + } + }); + }); + }); + + // Add legend + const legend = svg.append("g") + .attr("class", "legend") + .attr("transform", `translate(${width - 120}, 0)`); + + // Main branch + legend.append("circle") + .attr("cx", 0) + .attr("cy", 0) + .attr("r", 5) + .attr("fill", "#3182ce"); + + legend.append("text") + .attr("x", 10) + .attr("y", 5) + .text("Main") + .style("font-size", "12px"); + + // Release branch + legend.append("circle") + .attr("cx", 0) + .attr("cy", 20) + .attr("r", 5) + .attr("fill", "#dd6b20"); + + legend.append("text") + .attr("x", 10) + .attr("y", 25) + .text("Release") + .style("font-size", "12px"); + + // Common branches + legend.append("circle") + .attr("cx", 0) + .attr("cy", 40) + .attr("r", 5) + .attr("fill", "#38b2ac"); + + legend.append("text") + .attr("x", 10) + .attr("y", 45) + .text("Common") + .style("font-size", "12px"); + + // Create tooltip + const tooltip = d3.select("body") + .append("div") + .attr("class", "timeline-tooltip") + .style("position", "absolute") + .style("visibility", "hidden") + .style("background-color", "white") + .style("border", "1px solid #ddd") + .style("border-radius", "4px") + .style("padding", "8px") + .style("box-shadow", "0 2px 4px rgba(0,0,0,0.1)") + .style("font-size", "12px") + .style("pointer-events", "none"); + + function showTooltip(event, d, type) { + let content = `Date: ${d.date.toLocaleDateString()}
`; + + if (type === "main") { + content += `Main branches: ${d.mainCount}`; + } else if (type === "release") { + content += `Release branches: ${d.releaseCount}`; + } else if (type === "common") { + content += `Common branches: ${d.commonCount}`; + } + + tooltip + .html(content) + .style("visibility", "visible") + .style("left", (event.pageX + 10) + "px") + .style("top", (event.pageY - 28) + "px"); + } + + function hideTooltip() { + tooltip.style("visibility", "hidden"); + } + }) + .catch(error => { + console.error("Error creating timeline:", error); + historyChartContainer.innerHTML = `

Error creating timeline: ${error.message}

`; + }); +} + +// Update setupHistoryView function to include timeline creation +function setupHistoryView() { + const showHistoryButton = document.getElementById('show-history-button'); + const historyDatesContainer = document.getElementById('history-dates'); + const historyChartContainer = document.getElementById('history-chart'); + + // Initially hide the history dates and chart + historyDatesContainer.style.display = 'none'; + historyChartContainer.style.display = 'none'; + + // Toggle history visibility + showHistoryButton.addEventListener('click', function() { + if (historyDatesContainer.style.display === 'none') { + historyDatesContainer.style.display = 'flex'; + historyChartContainer.style.display = 'block'; + showHistoryButton.textContent = 'Hide History'; + + // Discover available dates and create timeline + discoverAvailableDates().then(() => { + createTimelineVisualization(); + }); + } else { + historyDatesContainer.style.display = 'none'; + historyChartContainer.style.display = 'none'; + showHistoryButton.textContent = 'Show History'; + } + }); +} From 6ae664cb65f335d46e41eae89a32324fdab3aa82 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 20:28:46 -0500 Subject: [PATCH 04/13] after running locally --- analyzed_branch_data.json | 40 ++++++++++++++++++++++++++++++++++----- branch_data.json | 37 ++++++++++++++++++++++++++++++++---- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/analyzed_branch_data.json b/analyzed_branch_data.json index 6240f56..38a063d 100644 --- a/analyzed_branch_data.json +++ b/analyzed_branch_data.json @@ -11,11 +11,40 @@ "feature/sites-40" ], "releaseOnly": [ - "feature/sites-42", - "feature/sites-41" + "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", + "pr_url": "https://github.com/correasebastian/layers/pull/1", + "author": "correasebastian" + } + ], + "releaseDetails": [ + { + "name": "feature/sites-42", + "merged_at": "2025-04-09T01:10:20+00:00", + "pr_number": 3, + "pr_title": "add sites-42", + "pr_url": "https://github.com/correasebastian/layers/pull/3", + "author": "correasebastian" + }, + { + "name": "feature/sites-41", + "merged_at": "2025-04-08T18:58:45+00:00", + "pr_number": 2, + "pr_title": "using grid areas", + "pr_url": "https://github.com/correasebastian/layers/pull/2", + "author": "correasebastian" + } ], - "timestamp": "2025-04-08T20:15:31.377187", + "timestamp": "2025-04-08T20:27:33.243559", "repository": "correasebastian/layers", + "analysisDate": "latest", "stats": { "total_branches": 3, "main_count": 1, @@ -24,8 +53,9 @@ "main_only_count": 1, "release_only_count": 2, "repository": "correasebastian/layers", - "timestamp": "2025-04-08T20:15:31.377187", - "analysis_time": "2025-04-08T20:18:41.090555", + "timestamp": "2025-04-08T20:27:33.243559", + "analysis_time": "2025-04-08T20:27:57.149519", + "analysis_date": "latest", "common_percentage": 0.0, "main_only_percentage": 33.3, "release_only_percentage": 66.7 diff --git a/branch_data.json b/branch_data.json index 027913a..648dd15 100644 --- a/branch_data.json +++ b/branch_data.json @@ -11,9 +11,38 @@ "feature/sites-40" ], "releaseOnly": [ - "feature/sites-42", - "feature/sites-41" + "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", + "pr_url": "https://github.com/correasebastian/layers/pull/1", + "author": "correasebastian" + } + ], + "releaseDetails": [ + { + "name": "feature/sites-42", + "merged_at": "2025-04-09T01:10:20+00:00", + "pr_number": 3, + "pr_title": "add sites-42", + "pr_url": "https://github.com/correasebastian/layers/pull/3", + "author": "correasebastian" + }, + { + "name": "feature/sites-41", + "merged_at": "2025-04-08T18:58:45+00:00", + "pr_number": 2, + "pr_title": "using grid areas", + "pr_url": "https://github.com/correasebastian/layers/pull/2", + "author": "correasebastian" + } ], - "timestamp": "2025-04-08T20:15:31.377187", - "repository": "correasebastian/layers" + "timestamp": "2025-04-08T20:27:33.243559", + "repository": "correasebastian/layers", + "analysisDate": "latest" } \ No newline at end of file From 0bd5f3e0cc5af0fe7c06c22cf4b0f5151fef98db Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 20:45:08 -0500 Subject: [PATCH 05/13] trigger on release changes --- .github/workflows/branch-analysis.yml | 80 +++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 .github/workflows/branch-analysis.yml diff --git a/.github/workflows/branch-analysis.yml b/.github/workflows/branch-analysis.yml new file mode 100644 index 0000000..433d0a4 --- /dev/null +++ b/.github/workflows/branch-analysis.yml @@ -0,0 +1,80 @@ +name: Branch Comparison Analysis and Deployment + +on: + schedule: + - cron: '0 0 * * *' # Run daily at midnight + 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: Run branch analysis for past dates + env: + GITHUB_TOKEN: ${{ secrets.GH_PAT }} + GITHUB_REPO: ${{ github.repository }} + MAIN_BRANCH: main + RELEASE_BRANCH: release + run: | + # Run analysis for past 7 days + for i in {1..7}; do + date=$(date -d "$i days ago" +%Y-%m-%d) + echo "Running analysis for $date" + python github_branch_analyzer_with_date.py --date $date --output "branch_data_$(date -d "$date" +%Y%m%d).json" + python analyze_branch_data_with_date.py "branch_data_$(date -d "$date" +%Y%m%d).json" "analyzed_branch_data_$(date -d "$date" +%Y%m%d).json" + done + + - name: Setup visualization directory + run: | + # Create visualization directory if it doesn't exist + mkdir -p visualization + + # Copy visualization files + cp -r git-branch-comparison-website/* 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 From fa989c8e3d6025e9b6b5d134be9ba00e22c706e4 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 20:46:37 -0500 Subject: [PATCH 06/13] every 5 minutes --- .github/workflows/branch-analysis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/branch-analysis.yml b/.github/workflows/branch-analysis.yml index 433d0a4..be5fadf 100644 --- a/.github/workflows/branch-analysis.yml +++ b/.github/workflows/branch-analysis.yml @@ -2,7 +2,7 @@ name: Branch Comparison Analysis and Deployment on: schedule: - - cron: '0 0 * * *' # Run daily at midnight + - cron: '*/5 * * * *' # Run every 5 minutes workflow_dispatch: # Allow manual trigger push: branches: From f29992c0d38dc34a9b12d9070559dac652577fdc Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 20:54:40 -0500 Subject: [PATCH 07/13] working locally --- analyze_branch_data_with_date.py | 166 ++++++++++++++++++++++++ analyzed_branch_data.json | 18 +-- branch_data.json | 14 +- configure_with_date.sh | 63 +++++++++ github_branch_analyzer_with_date.py | 160 +++++++++++++++++++++++ run_with_date.sh | 70 ++++++++++ visualization/analyzed_branch_data.json | 70 ++++++---- 7 files changed, 513 insertions(+), 48 deletions(-) create mode 100644 analyze_branch_data_with_date.py create mode 100644 configure_with_date.sh create mode 100644 github_branch_analyzer_with_date.py create mode 100644 run_with_date.sh 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 index 38a063d..db7c16d 100644 --- a/analyzed_branch_data.json +++ b/analyzed_branch_data.json @@ -19,9 +19,7 @@ "name": "feature/sites-40", "merged_at": "2025-04-08T18:55:00+00:00", "pr_number": 1, - "pr_title": "hello", - "pr_url": "https://github.com/correasebastian/layers/pull/1", - "author": "correasebastian" + "pr_title": "hello" } ], "releaseDetails": [ @@ -29,20 +27,16 @@ "name": "feature/sites-42", "merged_at": "2025-04-09T01:10:20+00:00", "pr_number": 3, - "pr_title": "add sites-42", - "pr_url": "https://github.com/correasebastian/layers/pull/3", - "author": "correasebastian" + "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", - "pr_url": "https://github.com/correasebastian/layers/pull/2", - "author": "correasebastian" + "pr_title": "using grid areas" } ], - "timestamp": "2025-04-08T20:27:33.243559", + "timestamp": "2025-04-08T20:48:59.461936", "repository": "correasebastian/layers", "analysisDate": "latest", "stats": { @@ -53,8 +47,8 @@ "main_only_count": 1, "release_only_count": 2, "repository": "correasebastian/layers", - "timestamp": "2025-04-08T20:27:33.243559", - "analysis_time": "2025-04-08T20:27:57.149519", + "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, diff --git a/branch_data.json b/branch_data.json index 648dd15..ab18f50 100644 --- a/branch_data.json +++ b/branch_data.json @@ -19,9 +19,7 @@ "name": "feature/sites-40", "merged_at": "2025-04-08T18:55:00+00:00", "pr_number": 1, - "pr_title": "hello", - "pr_url": "https://github.com/correasebastian/layers/pull/1", - "author": "correasebastian" + "pr_title": "hello" } ], "releaseDetails": [ @@ -29,20 +27,16 @@ "name": "feature/sites-42", "merged_at": "2025-04-09T01:10:20+00:00", "pr_number": 3, - "pr_title": "add sites-42", - "pr_url": "https://github.com/correasebastian/layers/pull/3", - "author": "correasebastian" + "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", - "pr_url": "https://github.com/correasebastian/layers/pull/2", - "author": "correasebastian" + "pr_title": "using grid areas" } ], - "timestamp": "2025-04-08T20:27:33.243559", + "timestamp": "2025-04-08T20:48:59.461936", "repository": "correasebastian/layers", "analysisDate": "latest" } \ No newline at end of file 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/github_branch_analyzer_with_date.py b/github_branch_analyzer_with_date.py new file mode 100644 index 0000000..9a1dd33 --- /dev/null +++ b/github_branch_analyzer_with_date.py @@ -0,0 +1,160 @@ +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 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') + 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 + + # Create analyzer and run analysis + analyzer = GitHubBranchAnalyzer(token, repo_name, main_branch, release_branch) + analyzer.save_results(args.date, args.output) + +if __name__ == "__main__": + main() 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/visualization/analyzed_branch_data.json b/visualization/analyzed_branch_data.json index 0c27419..db7c16d 100644 --- a/visualization/analyzed_branch_data.json +++ b/visualization/analyzed_branch_data.json @@ -1,39 +1,57 @@ { "main": [ - "feature/profile-789", - "feature/payment-456", - "feature/auth-123" + "feature/sites-40" ], "release": [ - "feature/notification-202", - "feature/search-101", - "feature/auth-123" - ], - "common": [ - "feature/auth-123" + "feature/sites-42", + "feature/sites-41" ], + "common": [], "mainOnly": [ - "feature/profile-789", - "feature/payment-456" + "feature/sites-40" ], "releaseOnly": [ - "feature/notification-202", - "feature/search-101" + "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-08T15:18:44-04:00", - "repository": "example/private-repo", + "timestamp": "2025-04-08T20:48:59.461936", + "repository": "correasebastian/layers", + "analysisDate": "latest", "stats": { - "total_branches": 5, - "main_count": 3, - "release_count": 3, - "common_count": 1, - "main_only_count": 2, + "total_branches": 3, + "main_count": 1, + "release_count": 2, + "common_count": 0, + "main_only_count": 1, "release_only_count": 2, - "repository": "example/private-repo", - "timestamp": "2025-04-08T15:18:44-04:00", - "analysis_time": "2025-04-08T15:19:23.331491", - "common_percentage": 20.0, - "main_only_percentage": 40.0, - "release_only_percentage": 40.0 + "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 From 1f14f4702965076484a09c2a2e593d89bc310e42 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 20:55:34 -0500 Subject: [PATCH 08/13] delete .env --- .env | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index c8397aa..0000000 --- a/.env +++ /dev/null @@ -1,4 +0,0 @@ -GITHUB_TOKEN=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 -GITHUB_REPO=correasebastian/layers -MAIN_BRANCH=main -RELEASE_BRANCH=release \ No newline at end of file From 81d26656534cd1bb0698e3ebc680872c95630bfb Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 21:02:39 -0500 Subject: [PATCH 09/13] handle timestamp --- .github/workflows/branch-analysis.yml | 12 ++++++------ github_branch_analyzer_with_date.py | 14 +++++++++----- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/.github/workflows/branch-analysis.yml b/.github/workflows/branch-analysis.yml index be5fadf..41aa613 100644 --- a/.github/workflows/branch-analysis.yml +++ b/.github/workflows/branch-analysis.yml @@ -46,19 +46,19 @@ jobs: python github_branch_analyzer_with_date.py python analyze_branch_data_with_date.py - - name: Run branch analysis for past dates + - name: Run branch analysis for past intervals env: GITHUB_TOKEN: ${{ secrets.GH_PAT }} GITHUB_REPO: ${{ github.repository }} MAIN_BRANCH: main RELEASE_BRANCH: release run: | - # Run analysis for past 7 days + # Run analysis for the last 35 minutes with 5-minute intervals for i in {1..7}; do - date=$(date -d "$i days ago" +%Y-%m-%d) - echo "Running analysis for $date" - python github_branch_analyzer_with_date.py --date $date --output "branch_data_$(date -d "$date" +%Y%m%d).json" - python analyze_branch_data_with_date.py "branch_data_$(date -d "$date" +%Y%m%d).json" "analyzed_branch_data_$(date -d "$date" +%Y%m%d).json" + timestamp=$(date -d "$((i * 5)) minutes ago" +%Y-%m-%dT%H:%M:%S) + echo "Running analysis for $timestamp" + python github_branch_analyzer_with_date.py --timestamp $timestamp --output "branch_data_$(date -d "$timestamp" +%Y%m%d%H%M%S).json" + python analyze_branch_data_with_date.py "branch_data_$(date -d "$timestamp" +%Y%m%d%H%M%S).json" "analyzed_branch_data_$(date -d "$timestamp" +%Y%m%d%H%M%S).json" done - name: Setup visualization directory diff --git a/github_branch_analyzer_with_date.py b/github_branch_analyzer_with_date.py index 9a1dd33..c9bf6d4 100644 --- a/github_branch_analyzer_with_date.py +++ b/github_branch_analyzer_with_date.py @@ -135,26 +135,30 @@ def save_results(self, before_date=None, output_file=None): def main(): # Parse command line arguments - parser = argparse.ArgumentParser(description='Analyze GitHub repository branches with date filtering') + 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(args.date, args.output) + analyzer.save_results(filter_date, args.output) if __name__ == "__main__": main() From 4126492f59bd961bf069564cb74e17690b1b0d47 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 21:04:17 -0500 Subject: [PATCH 10/13] remove interval job for now --- .github/workflows/branch-analysis.yml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/.github/workflows/branch-analysis.yml b/.github/workflows/branch-analysis.yml index 41aa613..96749ef 100644 --- a/.github/workflows/branch-analysis.yml +++ b/.github/workflows/branch-analysis.yml @@ -46,21 +46,6 @@ jobs: python github_branch_analyzer_with_date.py python analyze_branch_data_with_date.py - - name: Run branch analysis for past intervals - env: - GITHUB_TOKEN: ${{ secrets.GH_PAT }} - GITHUB_REPO: ${{ github.repository }} - MAIN_BRANCH: main - RELEASE_BRANCH: release - run: | - # Run analysis for the last 35 minutes with 5-minute intervals - for i in {1..7}; do - timestamp=$(date -d "$((i * 5)) minutes ago" +%Y-%m-%dT%H:%M:%S) - echo "Running analysis for $timestamp" - python github_branch_analyzer_with_date.py --timestamp $timestamp --output "branch_data_$(date -d "$timestamp" +%Y%m%d%H%M%S).json" - python analyze_branch_data_with_date.py "branch_data_$(date -d "$timestamp" +%Y%m%d%H%M%S).json" "analyzed_branch_data_$(date -d "$timestamp" +%Y%m%d%H%M%S).json" - done - - name: Setup visualization directory run: | # Create visualization directory if it doesn't exist From 567bffd93af9bb7bfd73a7dce9a11b3a33c0f055 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 21:05:46 -0500 Subject: [PATCH 11/13] remove the copy --- .github/workflows/branch-analysis.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/branch-analysis.yml b/.github/workflows/branch-analysis.yml index 96749ef..57903cf 100644 --- a/.github/workflows/branch-analysis.yml +++ b/.github/workflows/branch-analysis.yml @@ -51,9 +51,6 @@ jobs: # Create visualization directory if it doesn't exist mkdir -p visualization - # Copy visualization files - cp -r git-branch-comparison-website/* visualization/ - # Copy analysis results cp analyzed_branch_data*.json visualization/ From 15dc32c8236ce538318541e498d04210c73b3014 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 21:11:31 -0500 Subject: [PATCH 12/13] just to retrigger --- USER_GUIDE.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 74a477f..57c4bca 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -146,3 +146,6 @@ If you encounter issues not covered in this guide, please: - 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 \ No newline at end of file From 46d05938fc231eec31148984fb74784a6af9eee4 Mon Sep 17 00:00:00 2001 From: sebastian correa Date: Tue, 8 Apr 2025 21:20:32 -0500 Subject: [PATCH 13/13] lastname --- USER_GUIDE.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 57c4bca..ad30852 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -148,4 +148,5 @@ If you encounter issues not covered in this guide, please: - Regularly rotate your tokens for enhanced security -sebas \ No newline at end of file +sebas +correa \ No newline at end of file