Skip to content

Commit

Permalink
Automated pr review (OWASP-BLT#3132)
Browse files Browse the repository at this point in the history
* added views and templates to collect, analyze and display pr analysis

* Formats

* pre

* merge migrations
  • Loading branch information
SahilDhillon21 authored Dec 19, 2024
1 parent 01ce0fb commit 526b385
Show file tree
Hide file tree
Showing 10 changed files with 311 additions and 1 deletion.
4 changes: 4 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@
set_vote_status,
sitemap,
sponsor_view,
submit_roadmap_pr,
view_pr_analysis,
view_suggestions,
vote_suggestions,
)
Expand Down Expand Up @@ -756,6 +758,8 @@
user_sizzle_report,
name="user_sizzle_report",
),
path("submit-roadmap-pr/", submit_roadmap_pr, name="submit-roadmap-pr"),
path("view-pr-analysis/", view_pr_analysis, name="view_pr_analysis"),
path("delete_time_entry/", delete_time_entry, name="delete_time_entry"),
path("assign-badge/<str:username>/", assign_badge, name="assign_badge"),
path("github-webhook/", github_webhook, name="github-webhook"),
Expand Down
2 changes: 2 additions & 0 deletions website/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
Payment,
Points,
Post,
PRAnalysisReport,
Project,
SlackIntegration,
Subscription,
Expand Down Expand Up @@ -465,4 +466,5 @@ class PostAdmin(admin.ModelAdmin):
admin.site.register(Integration)
admin.site.register(SlackIntegration)
admin.site.register(Activity)
admin.site.register(PRAnalysisReport)
admin.site.register(Post, PostAdmin)
32 changes: 32 additions & 0 deletions website/migrations/0173_pranalysisreport.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 5.1.3 on 2024-12-19 12:10

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("website", "0172_merge_20241218_0505"),
]

operations = [
migrations.CreateModel(
name="PRAnalysisReport",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("pr_link", models.URLField()),
("issue_link", models.URLField()),
("priority_alignment_score", models.IntegerField()),
("revision_score", models.IntegerField()),
("recommendations", models.TextField()),
("created_at", models.DateTimeField(auto_now_add=True)),
],
),
]
12 changes: 12 additions & 0 deletions website/migrations/0174_merge_20241219_1305.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Generated by Django 5.1.3 on 2024-12-19 13:05

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("website", "0173_pranalysisreport"),
("website", "0173_remove_company_admin_remove_company_integrations_and_more"),
]

operations = []
12 changes: 12 additions & 0 deletions website/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1212,6 +1212,18 @@ def get_absolute_url(self):
return reverse("post_detail", kwargs={"slug": self.slug})


class PRAnalysisReport(models.Model):
pr_link = models.URLField()
issue_link = models.URLField()
priority_alignment_score = models.IntegerField()
revision_score = models.IntegerField()
recommendations = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)

def __str__(self):
return self.pr_link


@receiver(post_save, sender=Post)
def verify_file_upload(sender, instance, **kwargs):
from django.core.files.storage import default_storage
Expand Down
9 changes: 9 additions & 0 deletions website/templates/includes/sidenav.html
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,15 @@
<span>GSOC</span>
</a>
</li>
<li class="mb-2 {% if '/submit-roadmap-pr/' in request.path %}bg-gray-200{% endif %}">
<a href="{% url 'submit-roadmap-pr' %}"
class="flex items-center w-full no-underline p-2 {% if '/submit-roadmap-pr/' in request.path %}text-black{% else %}hover:text-red-500 text-black{% endif %}">
<div class="icon text-red-500">
<i class="fas fa-code-branch fa-lg"></i>
</div>
<span>Submit PR for review</span>
</a>
</li>
</ul>
<!-- Spacer -->
<div class="my-4 border-t border-gray-300"></div>
Expand Down
44 changes: 44 additions & 0 deletions website/templates/submit_roadmap_pr.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block content %}
{% include "includes/sidenav.html" %}
<div class="container mx-auto px-4 py-8 mt-10">
<h1 class="text-3xl font-bold mb-8">Submit Pull Request For Analysis</h1>
{% if request.user.is_superuser %}
<a href="/view-pr-analysis/" class="inline-block">
<button class="bg-red-500 hover:bg-red-700 text-white font-bold py-3 px-6 rounded focus:outline-none focus:shadow-outline">
View PR analysis
</button>
</a>
{% endif %}
<form method="post"
action="{% url 'submit-roadmap-pr' %}"
class="bg-white shadow-md rounded px-8 pt-8 pb-8 mb-6">
{% csrf_token %}
<div class="mb-6">
<label for="pr-link" class="block text-gray-700 text-2xl font-semibold mb-3">GitHub PR Link</label>
<input type="url"
id="pr-link"
name="pr_link"
placeholder="Enter GitHub PR link"
class="shadow appearance-none border rounded w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
required>
</div>
<div class="mb-6">
<label for="issue-link"
class="block text-gray-700 text-2xl font-semibold mb-3">GitHub Issue Link (Roadmap)</label>
<input type="url"
id="issue-link"
name="issue_link"
placeholder="Enter GitHub issue link"
class="shadow appearance-none border rounded w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
required>
</div>
<div class="flex items-center justify-center">
<button type="submit"
class="bg-red-500 hover:bg-red-700 text-white font-bold py-3 px-6 rounded focus:outline-none focus:shadow-outline">
Submit
</button>
</div>
</form>
</div>
{% endblock %}
83 changes: 83 additions & 0 deletions website/templates/view_pr_analysis.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
{% extends "base.html" %}
{% block content %}
{% include "includes/sidenav.html" %}
<title>PR Analysis Reports</title>
<style>
.container {
font-family: Arial, sans-serif;
background-color: #f9f9f9;
padding: 20px;
max-width: 800px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}
.report {
background-color: #ffffff;
padding: 15px;
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 5px;
}
.report h3 {
color: #555;
font-size: 20px;
margin-bottom: 10px;
}
.report p {
color: #666;
line-height: 1.6;
}
.report a {
color: #007bff;
text-decoration: none;
}
.report a:hover {
text-decoration: underline;
}
.report hr {
border: 0;
height: 1px;
background: #ddd;
margin: 20px 0;
}
.no-reports {
text-align: center;
color: #999;
}
</style>
<div class="container mt-20">
<h1 class="text-3xl font-bold">PR Analysis Reports</h1>
{% for report in reports %}
<div class="report">
<h3>PR Analysis</h3>
<p>
<strong>PR Link:</strong> <a href="{{ report.pr_link }}" target="_blank">{{ report.pr_link }}</a>
</p>
<p>
<strong>Issue Link:</strong> <a href="{{ report.issue_link }}" target="_blank">{{ report.issue_link }}</a>
</p>
<p>
<strong>Priority Alignment Score:</strong> {{ report.priority_alignment_score }}
</p>
<p>
<strong>Revision Score:</strong> {{ report.revision_score }}
</p>
<p>
<strong>Recommendations:</strong>
</p>
<p>{{ report.recommendations }}</p>
<p>
<small><em>Created on: {{ report.created_at }}</em></small>
</p>
</div>
<hr>
{% empty %}
<p class="no-reports">No analysis reports available.</p>
{% endfor %}
</div>
{% endblock content %}
63 changes: 63 additions & 0 deletions website/utils.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
import re
import time
from collections import deque
Expand All @@ -10,6 +11,8 @@
from django.http import HttpRequest, HttpResponseBadRequest
from django.shortcuts import redirect

from .models import PRAnalysisReport

WHITELISTED_IMAGE_TYPES = {
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
Expand Down Expand Up @@ -169,3 +172,63 @@ def format_timedelta(td):
hours, remainder = divmod(total_seconds, 3600)
minutes, seconds = divmod(remainder, 60)
return f"{hours}h {minutes}m {seconds}s"


import openai

openai.api_key = os.getenv("OPENAI_API_KEY")
GITHUB_API_TOKEN = os.getenv("GITHUB_ACCESS_TOKEN")


def fetch_github_data(owner, repo, endpoint, number):
"""
Fetch data from GitHub API for a given repository endpoint.
"""
url = f"https://api.github.com/repos/{owner}/{repo}/{endpoint}/{number}"
headers = {
"Authorization": f"Bearer {GITHUB_API_TOKEN}",
"Accept": "application/vnd.github.v3+json",
}
response = requests.get(url, headers=headers)
if response.status_code == 200:
return response.json()
return {"error": f"Failed to fetch data: {response.status_code}"}


def analyze_pr_content(pr_data, roadmap_data):
"""
Use OpenAI API to analyze PR content against roadmap priorities.
"""
prompt = f"""
Compare the following pull request details with the roadmap priorities and provide:
1. A priority alignment score (1-10) with reasoning.
2. Key recommendations for improvement.
3. Assess the quality of the pull request based on its description, structure, and potential impact.
### PR Data:
{pr_data}
### Roadmap Data:
{roadmap_data}
"""
response = openai.ChatCompletion.create(
model="gpt-4", messages=[{"role": "user", "content": prompt}], temperature=0.7
)
return response["choices"][0]["message"]["content"]


def save_analysis_report(pr_link, issue_link, analysis):
"""
Save the analysis report into the database.
"""
priority_score = analysis.get("priority_score", 0)
revision_score = analysis.get("revision_score", 0)
recommendations = analysis.get("recommendations", "")

PRAnalysisReport.objects.create(
pr_link=pr_link,
issue_link=issue_link,
priority_alignment_score=priority_score,
revision_score=revision_score,
recommendations=recommendations,
)
51 changes: 50 additions & 1 deletion website/views/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,18 @@
ChatBotLog,
Domain,
Issue,
PRAnalysisReport,
Suggestion,
SuggestionVotes,
UserProfile,
Wallet,
)
from website.utils import safe_redirect_allowed
from website.utils import (
analyze_pr_content,
fetch_github_data,
safe_redirect_allowed,
save_analysis_report,
)

vector_store = None
DAILY_REQUEST_LIMIT = 10
Expand Down Expand Up @@ -646,6 +652,49 @@ def get_last_commit_date():
return "Not available"


def submit_roadmap_pr(request):
if request.method == "POST":
pr_link = request.POST.get("pr_link")
issue_link = request.POST.get("issue_link")

if not pr_link or not issue_link:
return JsonResponse({"error": "Both PR and issue links are required."}, status=400)

pr_parts = pr_link.split("/")
issue_parts = issue_link.split("/")
owner, repo = pr_parts[3], pr_parts[4]
pr_number, issue_number = pr_parts[-1], issue_parts[-1]

print(pr_parts)
print(issue_parts)

print(f"rrepo: {repo}")
print(f"pr_number: {pr_number}")

pr_data = fetch_github_data(owner, repo, "pulls", pr_number)
roadmap_data = fetch_github_data(owner, repo, "issues", issue_number)

if "error" in pr_data or "error" in roadmap_data:
return JsonResponse(
{
"error": f"Failed to fetch PR or roadmap data: {pr_data.get('error', 'Unknown error')}"
},
status=500,
)

analysis = analyze_pr_content(pr_data, roadmap_data)

save_analysis_report(pr_link, issue_link, analysis)
return JsonResponse({"message": "PR submitted successfully"})

return render(request, "submit_roadmap_pr.html")


def view_pr_analysis(request):
reports = PRAnalysisReport.objects.all()
return render(request, "view_pr_analysis.html", {"reports": reports})


def home(request):
last_commit = get_last_commit_date()
return render(request, "home.html", {"last_commit": last_commit})
Expand Down

0 comments on commit 526b385

Please sign in to comment.