feat: add support for pull requests (#156)

This commit is contained in:
Thomas Durieux
2023-01-22 12:54:14 +01:00
committed by GitHub
parent 3091b13776
commit 73e46f926f
23 changed files with 2479 additions and 28 deletions

View File

@@ -563,7 +563,27 @@ loc .lang {
}
.highlighted-line {
position:absolute;
background:rgba(100,200,100,0.5);
z-index:20
position: absolute;
background: rgba(100, 200, 100, 0.5);
z-index: 20
}
pre,
code {
font-family: "Fira Code", "Courier New", Courier, monospace;
line-height: 1.1;
}
.diff-lines,
.diff-file,
.diff-index {
background: rgba(172, 172, 172, 0.5);
}
.diff-add {
background: rgba(100, 200, 100, 0.5);
}
.diff-remove {
background: rgba(200, 100, 100, 0.5);
}

View File

@@ -0,0 +1,506 @@
<div class="container-fluid h-100">
<div class="row h-100">
<div
class="col sidePanel shadow overflow-auto h-100 d-flex align-content-end"
>
<div
class="p-0 py-2 m-auto"
ng-class="{'card': !pullRequestUrl,'container': pullRequestUrl}"
>
<form
class="form needs-validation"
ng-class="{'card-body': !pullRequestUrl}"
name="anonymizeForm"
novalidate
>
<h5 class="card-title">Anonymize a pull request</h5>
<h6 class="card-subtitle mb-2 text-muted">
Fill the information to anonymize! It will only take 5min.
</h6>
<h2>Source</h2>
<!-- pullRequestUrl -->
<div class="form-group">
<label for="pullRequestUrl"
>Type the url of your pull request</label
>
<input
type="text"
class="form-control"
name="pullRequestUrl"
id="pullRequestUrl"
ng-class="{'is-invalid': anonymize.pullRequestUrl.$invalid}"
ng-model="pullRequestUrl"
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
ng-change="pullRequestSelected()"
/>
<div
class="invalid-feedback"
ng-show="anonymize.pullRequestUrl.$error.access"
>
{{pullRequestUrl}} is not accessible. Some organizations are
restricting the access to the repositories.
</div>
<div
class="invalid-feedback"
ng-show="anonymize.pullRequestUrl.$error.missing"
>
{{pullRequestUrl}} does not exist or is not accessible
</div>
<div
class="invalid-feedback"
ng-show="anonymize.pullRequestUrl.$error.used"
>
{{pullRequestUrl}} is already anonymized
</div>
</div>
<div ng-show="pullRequestUrl">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="update"
name="update"
ng-model="options.update"
/>
<label class="form-check-label" for="update">Auto update</label>
<small id="updateHelp" class="form-text text-muted"
>Automatically update the anonymized pull request with the
latest updates. The pull request is updated once per day
maximum.</small
>
</div>
</div>
<h2>Conference ID</h2>
<!-- Conference -->
<div class="form-group">
<label for="conference"
>Conference ID<span class="text-muted">Optional</span></label
>
<input
class="form-control"
id="conference"
name="conference"
ng-model="conference"
ng-class="{'is-invalid': anonymize.conference.$invalid}"
/>
<small class="form-text text-muted" ng-show="conference_data"
><a ng-href="{{conference_data.url}}" target="_target"
>{{conference_data.name}}</a
>
will expire on {{conference_data.endDate | date}}.</small
>
<div
class="invalid-feedback"
ng-show="anonymize.conference.$error.activated"
>
The conference is not activated.
</div>
<small class="form-text text-muted" ng-show="!conference_data">
Use the Conference ID that your conference provided you. This
will update automatically the anonymization options based on the
conference preferences.
</small>
</div>
<h2>Anonymization Options</h2>
<!-- Pull Request ID -->
<div class="form-group">
<label for="pullRequestId">Anonymize pull request id</label>
<input
type="text"
class="form-control"
name="pullRequestId"
id="pullRequestId"
ng-class="{'is-invalid': anonymize.pullRequestId.$invalid}"
ng-model="pullRequestId"
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
/>
<small id="idHelp" class="form-text text-muted"
>Id used in the url:
https://anonymous.4open.science/r/{{pullRequestId}}</small
>
<div
class="invalid-feedback"
ng-show="anonymize.pullRequestId.$error.format"
>
Repository id can only contain letters and numbers
</div>
<div
class="invalid-feedback"
ng-show="anonymize.pullRequestId.$error.used"
>
{{pullRequestId}} is already used
</div>
</div>
<!-- Terms -->
<div class="form-group">
<label for="terms">Terms to anonymize</label>
<textarea
class="form-control"
id="terms"
name="terms"
rows="3"
ng-model="terms"
ng-model-options="{ debounce: 250 }"
ng-class="{'is-invalid': anonymize.terms.$invalid}"
></textarea>
<small id="termsHelp" class="form-text text-muted"
>One term per line. Each term will be replaced by XXX.</small
>
<div
class="invalid-feedback"
ng-show="anonymize.terms.$error.format"
>
Terms are in an invalid format
</div>
</div>
<div class="form-group">
<label for="expiration">Expiration strategy</label>
<select
class="form-control"
id="expiration"
name="expiration"
ng-model="options.expirationMode"
>
<option value="never" selected>Never expire</option>
<option value="redirect">
Redirect to GitHub when expired
</option>
<option value="remove">Remove when expired</option>
</select>
<small class="form-text text-muted"
>Define the expiration strategy for the anonymized
repository.</small
>
</div>
<div
class="form-group"
id="expiration-date-form"
ng-hide="options.expirationMode=='never'"
>
<label for="expirationDate"
>Expiration date of the anonymized repository</label
>
<input
class="form-control"
type="date"
name="expirationDate"
id="expirationDate"
ng-model="options.expirationDate"
/>
<small
class="form-text text-muted"
ng-show="options.expirationMode=='remove'"
>After {{options.expirationDate | date}}, the repository will be
removed and the visitor will not be able to see the content of
the repository.</small
>
<small
class="form-text text-muted"
ng-show="options.expirationMode=='redirect'"
>After {{options.expirationDate | date}}, the visitors of the
anonymized repository will be redirected to
{{pullRequestUrl}}.</small
>
</div>
<div class="accordion mb-3" id="options">
<div class="card">
<div class="card-header" id="headingOne">
<h2 class="mb-0">
<button
class="btn btn-block text-left"
type="button"
data-toggle="collapse"
data-target="#collapseOne"
aria-expanded="true"
aria-controls="collapseOne"
>
Advance options
</button>
</h2>
</div>
<div
id="collapseOne"
class="collapse show"
aria-labelledby="headingOne"
data-parent="#options"
>
<div class="card-body">
<div class="form-group">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="link"
name="link"
ng-model="options.link"
/>
<label class="form-check-label" for="link"
>Keep links</label
>
<small id="termsHelp" class="form-text text-muted"
>Keep or remove all the links.</small
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="image"
name="image"
ng-model="options.image"
/>
<label class="form-check-label" for="image"
>Display images</label
>
<small id="termsHelp" class="form-text text-muted"
>Images are not anonymized</small
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="date"
name="date"
ng-model="options.date"
/>
<label class="form-check-label" for="date"
>Display dates</label
>
<small id="termsHelp" class="form-text text-muted"
>Display the date of the Pull Request and the date of
the comments.</small
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="username"
name="username"
ng-model="options.username"
/>
<label class="form-check-label" for="username"
>Display username</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="comments"
name="comments"
ng-model="options.comments"
/>
<label class="form-check-label" for="comments"
>Display comments</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="diff"
name="diff"
ng-model="options.diff"
/>
<label class="form-check-label" for="diff"
>Display diff</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="origin"
name="origin"
ng-model="options.origin"
/>
<label class="form-check-label" for="origin"
>Display the project name</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="title"
name="title"
ng-model="options.title"
/>
<label class="form-check-label" for="title"
>Display the PR title</label
>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="body"
name="body"
ng-model="options.body"
/>
<label class="form-check-label" for="body"
>Display the PR body and comment bodies</label
>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="alert alert-danger"
role="alert"
ng-if="error"
ng-bind="error"
></div>
<button
id="submit"
type="submit"
class="btn btn-primary"
ng-click="anonymizePullRequest($event)"
ng-show="pullRequestUrl"
ng-if="!isUpdate"
>
Anonymize
</button>
<button
id="submit"
type="submit"
class="btn btn-primary"
ng-click="updatePullRequest($event)"
ng-show="pullRequestUrl"
ng-if="isUpdate"
>
Update
</button>
</form>
</div>
</div>
<div class="col-9 p-2 h-100 overflow-auto" ng-if="details">
<div class="d-flex w-100 justify-content-between align-items-center">
<h2 class="pr-title">
<span ng-if="options.title"
>{{anonymize(details.pullRequest.title)}}</span
>
<span
class="badge"
ng-class="{'badge-success':details.pullRequest.merged, 'badge-warning':details.pullRequest.state=='open', 'badge-danger':details.pullRequest.state=='closed' &&!details.pullRequest.merged}"
>
{{details.pullRequest.merged?"merged":details.pullRequest.state |
title}}
</span>
</h2>
<small
ng-bind="details.pullRequest.updatedDate | date"
ng-if="options.date"
></small>
</div>
<small ng-if="options.origin"
>Pull Request on {{details.pullRequest.baseRepositoryFullName}}</small
>
<div
class="pr-body shadow-sm p-3 mb-5 bg-white rounded"
ng-if="options.body"
>
<markdown
content="anonymize(details.pullRequest.body)"
options="options"
terms="terms"
></markdown>
</div>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation" ng-if="options.diff">
<button
class="nav-link active"
id="pills-diff-tab"
data-toggle="pill"
data-target="#pills-diff"
type="button"
role="tab"
aria-controls="pills-diff"
aria-selected="true"
>
Diff
</button>
</li>
<li class="nav-item" role="presentation" ng-if="options.comments">
<button
class="nav-link"
ng-class="{'active':!options.diff}"
id="pills-comments-tab"
data-toggle="pill"
data-target="#pills-comments"
type="button"
role="tab"
aria-controls="pills-comments"
aria-selected="false"
>
<ng-pluralize
count="details.pullRequest.comments.length"
when="{'0': 'No comment',
'one': 'One Comment',
'other': '{} Comments'}"
>
</ng-pluralize>
</button>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
<div
class="tab-pane show active"
id="pills-diff"
role="tabpanel"
aria-labelledby="pills-diff-tab"
>
<div
class="pr-diff shadow-sm p-3 mb-5 bg-white rounded"
ng-if="options.diff"
>
<pre><code ng-bind-html="anonymize(details.pullRequest.diff) | diff"></code></pre>
</div>
</div>
<div
class="tab-pane"
ng-class="{'show active':!options.diff}"
id="pills-comments"
role="tabpanel"
aria-labelledby="pills-comments-tab"
>
<ul class="pr-comments list-group" ng-if="options.comments">
<li
class="pr-comment list-group-item"
ng-repeat="comment in details.pullRequest.comments"
>
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1" ng-if="options.username">
@{{anonymize(comment.author)}}
</h5>
<small
ng-bind="comment.updatedDate | date"
ng-if="options.date"
></small>
</div>
<p class="mb-1">
<markdown
class="pr-comment-body"
ng-if="options.body"
content="anonymize(comment.body)"
options="options"
terms="terms"
></markdown>
</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>

View File

@@ -1,9 +1,9 @@
<div class="container page">
<div class="row">
<div class="border-bottom color-border-secondary py-3 w-100">
<div class="d-flex flex-items-start w-100">
<div class="d-flex align-items-center w-100">
<form class="w-100" aria-label="Repositories" accept-charset="UTF-8">
<div class="d-flex flex-column flex-lg-row flex-auto">
<div class="d-flex flex-column flex-lg-row flex-auto ">
<div class="mb-1 mb-md-0 mr-md-3">
<input
type="search"
@@ -244,7 +244,10 @@
</form>
<div class="d-none d-md-flex flex-md-items-center flex-md-justify-end">
<a href="/anonymize" class="text-center btn btn-primary ml-3">
<i class="fa fa-plus-circle" aria-hidden="true"></i> Anonymize
<i class="fa fa-plus-circle" aria-hidden="true"></i> Anonymize Repo
</a>
<a href="/pull-request-anonymize" class="text-center btn btn-primary ml-3">
<i class="fa fa-plus-circle" aria-hidden="true"></i> Anonymize PR
</a>
<a
title="Claim the ownership of an existing anonymized repository."

View File

@@ -31,6 +31,15 @@
Repositories
</a>
</li>
<li class="nav-item" ng-if="user">
<a
class="nav-link"
ng-class="{'active': path == '/pr-dashboard'}"
href="/pr-dashboard"
>
Pull Requests
</a>
</li>
<li class="nav-item" ng-if="user">
<a
class="nav-link"
@@ -47,6 +56,14 @@
>Anonymize</a
>
</li>
<li class="nav-item" ng-if="user">
<a
class="nav-link"
ng-class="{'active':path == '/pull-request-anonymize'}"
href="/pull-request-anonymize"
>Anonymize PR</a
>
</li>
<li class="nav-item dropdown" ng-if="user">
<a
class="nav-link dropdown-toggle"

View File

@@ -0,0 +1,316 @@
<div class="container page">
<div class="row">
<div class="border-bottom color-border-secondary py-3 w-100">
<div class="d-flex align-items-center w-100">
<form class="w-100" aria-label="Pull Requests" accept-charset="UTF-8">
<div class="d-flex flex-column flex-lg-row flex-auto">
<div class="mb-1 mb-md-0 mr-md-3">
<input
type="search"
id="search"
class="form-control"
aria-label="Find a pull request…"
placeholder="Find a pull request…"
autocomplete="off"
ng-model="search"
/>
</div>
<div class="d-flex flex-wrap">
<div class="dropdown mt-1 mt-lg-0 mr-1">
<button
class="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownSort"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
Sort
</button>
<div class="dropdown-menu" aria-labelledby="dropdownSort">
<h6 class="dropdown-header">Select order</h6>
<div class="form-check dropdown-item">
<input
class="form-check-input"
type="radio"
name="sort"
id="sortFullName"
value="fullName"
ng-model="orderBy"
/>
<label class="form-check-label" for="sortFullName">
Pull Request
</label>
</div>
<div class="form-check dropdown-item">
<input
class="form-check-input"
type="radio"
name="sort"
id="sortAnonymizeDate"
value="-anonymizeDate"
ng-model="orderBy"
/>
<label class="form-check-label" for="sortAnonymizeDate">
Anonymize Date
</label>
</div>
<div class="form-check dropdown-item">
<input
class="form-check-input"
type="radio"
name="sort"
id="sortStatus"
value="-status"
ng-model="orderBy"
/>
<label class="form-check-label" for="sortStatus">
Status
</label>
</div>
<div class="form-check dropdown-item">
<input
class="form-check-input"
type="radio"
name="sort"
id="sortLastView"
value="-lastView"
ng-model="orderBy"
/>
<label class="form-check-label" for="sortLastView">
Last View
</label>
</div>
<div class="form-check dropdown-item">
<input
class="form-check-input"
type="radio"
name="sort"
id="sortPageView"
value="-pageView"
ng-model="orderBy"
/>
<label class="form-check-label" for="sortPageView">
Page View
</label>
</div>
</div>
</div>
<div class="dropdown mt-1 mt-lg-0 mr-1">
<button
class="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownStatus"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
Status
</button>
<div class="dropdown-menu" aria-labelledby="dropdownStatus">
<h6 class="dropdown-header">Select status</h6>
<div class="form-check dropdown-item">
<input
class="form-check-input"
type="checkbox"
name="sort"
id="statusReady"
value="ready"
ng-model="filters.status.ready"
/>
<label class="form-check-label" for="statusReady">
Ready
</label>
</div>
<div class="form-check dropdown-item">
<input
class="form-check-input"
type="checkbox"
name="sort"
id="statusExpired"
value="expired"
ng-model="filters.status.expired"
/>
<label class="form-check-label" for="statusExpired">
Expired
</label>
</div>
<div class="form-check dropdown-item">
<input
class="form-check-input"
type="checkbox"
name="sort"
id="statusRemoved"
value="removed"
ng-model="filters.status.removed"
/>
<label class="form-check-label" for="statusRemoved">
Removed
</label>
</div>
</div>
</div>
</div>
</div>
</form>
<div class="d-none d-md-flex flex-md-items-center flex-md-justify-end">
<a href="/anonymize" class="text-center btn btn-primary ml-3">
<i class="fa fa-plus-circle" aria-hidden="true"></i> Anonymize Repo
</a>
<a
href="/pull-request-anonymize"
class="text-center btn btn-primary ml-3"
>
<i class="fa fa-plus-circle" aria-hidden="true"></i> Anonymize PR
</a>
<a
title="Claim the ownership of an existing anonymized repository."
data-toggle="tooltip"
data-placement="bottom"
href="/claim"
class="text-center btn btn-secondary ml-3"
>
Claim
</a>
</div>
</div>
</div>
<ul class="p-0 m-0 w-100">
<li
class="col-12 d-flex px-0 py-3 border-bottom color-border-secondary"
ng-class="{'expired': pr.status == 'expired','removed': pr.status == 'removed','error': pr.status == 'error' }"
ng-repeat="pr in pullRequests| filter:pullRequestFilter| orderBy:orderBy as filteredPullRequests"
>
<div class="w-100">
<div class="">
<h3>
<a
ng-href="/pr/{{pr.pullRequestId}}"
ng-bind="pr.pullRequestId"
></a>
<span
class="badge"
ng-class="{'badge-warning': pr.status == 'removed' || pr.status == 'expired' || pr.status == 'removing' || pr.status == 'expiring', 'badge-info': pr.status == 'preparing' || pr.status == 'download', 'badge-success': pr.status == 'ready', 'badge-danger': pr.status == 'error'}"
><span ng-bind="pr.status | title"></span>
<span
ng-if="pr.status == 'error'"
ng-bind="': ' + pr.statusMessage"
></span
></span>
</h3>
</div>
<div class="color-text-secondary mt-t">
<span class="pull-request">
<i class="fab fa-github" aria-hidden="true"></i>
<a
href="https://github.com/{{pr.source.repositoryFullName}}/pull/{{pr.source.pullRequestId}}"
class="fullName"
>{{pr.source.repositoryFullName}}@{{pr.source.pullRequestId}}</a
>
</span>
anonymized {{pr.anonymizeDate | humanTime}}
</div>
<div class="color-text-secondary mt-2">
<span class="ml-0 mr-3" ng-if="::pr.conference">
<i class="fas fa-chalkboard-teacher"></i>
{{pr.conference}}
</span>
<span
class="ml-0 mr-3"
class="terms"
title="Terms: {{::pr.options.terms.join(', ')}}"
data-toggle="tooltip"
data-placement="bottom"
>
<i class="fas fa-shield-alt"></i>
{{::pr.options.terms.length | number}}
</span>
<span
class="ml-0 mr-3"
title="View: {{::pr.pageView | number}}"
data-toggle="tooltip"
data-placement="bottom"
>
<i class="far fa-eye" aria-hidden="true"></i>
{{::pr.pageView | number}}
</span>
<span
class="ml-0 mr-3"
title="Last view: {{::pr.lastView | date}}"
data-toggle="tooltip"
data-placement="bottom"
>
<i class="far fa-calendar-alt" aria-hidden="true"></i>
Last view: {{::pr.lastView | humanTime}}</span
>
<span
class="ml-0 mr-3"
ng-if="pr.options.expirationMode!='never' && pr.status == 'ready'"
>
<i class="far fa-clock" aria-hidden="true"></i>
Expire: {{pr.options.expirationDate | humanTime}}</span
>
</div>
</div>
<div class="d-flex">
<div class="dropdown">
<button
class="btn black_border dropdown-toggle btn-sm"
type="button"
id="dropdownMenuButton"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false"
>
Actions
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a
class="dropdown-item"
href="/pull-request-anonymize/{{pr.pullRequestId}}"
>
<i class="far fa-edit" aria-hidden="true"></i> Edit
</a>
<a
class="dropdown-item"
href="#"
ng-show="pr.status == 'ready' || pr.status == 'error'"
ng-click="updatePullRequest(pr)"
>
<i class="fas fa-sync"></i> Force update
</a>
<a
class="dropdown-item"
href="#"
ng-show="pr.status == 'removed'"
ng-click="updatePullRequest(pr)"
>
<i class="fas fa-check-circle"></i>
Enable
</a>
<a
class="dropdown-item"
href="#"
ng-show="pr.status == 'ready'"
ng-click="removePullRequest(pr)"
>
<i class="fas fa-trash-alt"></i> Remove
</a>
<a class="dropdown-item" href="/pr/{{pr.pullRequestId}}/">
<i class="fa fa-eye" aria-hidden="true"></i> View PR
</a>
</div>
</div>
</div>
</li>
<li
class="col-12 d-flex px-0 py-3 border-bottom color-border-secondary"
ng-if="filteredPullRequests.length == 0"
>
There is no pull request to display.
</li>
</ul>
</div>
</div>

View File

@@ -0,0 +1,118 @@
<div class="container-fluid h-100">
<div class="row h-100">
<div class="col-md h-100 overflow-auto p-0 d-flex flex-column">
<div class="d-flex align-content-between status-bar shadow">
<div class="last-update">
Last Update: {{details.anonymizeDate|date}}
</div>
</div>
<div class="overflow-auto mx-3">
<div class="d-flex w-100 justify-content-between align-items-center">
<h2 class="pr-title">
<span ng-if="details.title">{{details.title}}</span>
<span
class="badge"
ng-class="{'badge-success':details.merged, 'badge-warning':details.state=='open', 'badge-danger':details.state=='closed' &&!details.merged}"
>
{{details.merged?"merged":details.state | title}}
</span>
</h2>
<small
ng-if="details.updatedDate"
ng-bind="details.updatedDate | date"
></small>
</div>
<small ng-if="details.baseRepositoryFullName"
>Pull Request on {{details.baseRepositoryFullName}}</small
>
<div
class="pr-body shadow-sm p-3 mb-4 rounded border"
ng-if="details.body"
>
<markdown content="details.body"></markdown>
</div>
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation" ng-if="details.diff">
<button
class="nav-link active"
id="pills-diff-tab"
data-toggle="pill"
data-target="#pills-diff"
type="button"
role="tab"
aria-controls="pills-diff"
aria-selected="true"
>
Diff
</button>
</li>
<li class="nav-item" role="presentation" ng-if="details.comments">
<button
class="nav-link"
ng-class="{'active':!details.diff}"
id="pills-comments-tab"
data-toggle="pill"
data-target="#pills-comments"
type="button"
role="tab"
aria-controls="pills-comments"
aria-selected="false"
>
<ng-pluralize
count="details.comments.length"
when="{'0': 'No comment',
'one': 'One Comment',
'other': '{} Comments'}"
>
</ng-pluralize>
</button>
</li>
</ul>
<div class="tab-content" id="pills-tabContent">
<div
class="tab-pane show active"
id="pills-diff"
role="tabpanel"
aria-labelledby="pills-diff-tab"
ng-if="details.diff"
>
<div class="pr-diff shadow-sm p-3 mb-5 bg-white rounded">
<pre><code ng-bind-html="details.diff | diff"></code></pre>
</div>
</div>
<div
class="tab-pane"
ng-class="{'show active':!details.diff}"
id="pills-comments"
role="tabpanel"
aria-labelledby="pills-comments-tab"
ng-if="details.comments"
>
<ul class="pr-comments list-group">
<li
class="pr-comment list-group-item"
ng-repeat="comment in details.comments"
>
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1" ng-if="comment.author">
@{{comment.author}}
</h5>
<small
ng-bind="comment.updatedDate | date"
ng-if="comment.updatedDate"
></small>
</div>
<p class="mb-1" ng-if="comment.body">
<markdown
class="pr-comment-body"
content="comment.body"
></markdown>
</p>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -34,11 +34,21 @@ angular
controller: "dashboardController",
title: "Dashboard - Anonymous GitHub",
})
.when("/pr-dashboard", {
templateUrl: "/partials/pr-dashboard.htm",
controller: "prDashboardController",
title: "Pull Request Dashboard - Anonymous GitHub",
})
.when("/anonymize/:repoId?", {
templateUrl: "/partials/anonymize.htm",
controller: "anonymizeController",
title: "Anonymize - Anonymous GitHub",
})
.when("/pull-request-anonymize/:pullRequestId?", {
templateUrl: "/partials/anonymizePullRequest.htm",
controller: "anonymizePullRequestController",
title: "Anonymize - Anonymous GitHub",
})
.when("/status/:repoId", {
templateUrl: "/partials/status.htm",
controller: "statusController",
@@ -79,6 +89,12 @@ angular
controller: "claimController",
title: "Claim repository - Anonymous GitHub",
})
.when("/pr/:pullRequestId", {
templateUrl: "/partials/pullRequest.htm",
controller: "pullRequestController",
title: "Anonymized Pull Request - Anonymous GitHub",
reloadOnUrl: false,
})
.when("/r/:repoId/:path*?", {
templateUrl: "/partials/explorer.htm",
controller: "exploreController",
@@ -205,6 +221,53 @@ angular
return capitalized.join(" ");
};
})
.filter("diff", function ($sce) {
return function (str) {
if (!str) return str;
const lines = str.split("\n");
const o = [];
for (let i = 1; i < lines.length; i++) {
if (lines[i].startsWith("+++")) {
o.push(`<span class="diff-file">${lines[i]}</span>`);
} else if (lines[i].startsWith("---")) {
o.push(`<span class="diff-file">${lines[i]}</span>`);
} else if (lines[i].startsWith("@@")) {
o.push(`<span class="diff-lines">${lines[i]}</span>`);
} else if (lines[i].startsWith("index")) {
o.push(`<span class="diff-index">${lines[i]}</span>`);
} else if (lines[i].startsWith("+")) {
o.push(`<span class="diff-add">${lines[i]}</span>`);
} else if (lines[i].startsWith("-")) {
o.push(`<span class="diff-remove">${lines[i]}</span>`);
} else {
o.push(`<span class="diff-line">${lines[i]}</span>`);
}
}
return $sce.trustAsHtml(o.join("\n"));
};
})
.directive("markdown", [
"$location",
function ($location) {
return {
restrict: "E",
scope: {
terms: "=",
options: "=",
content: "=",
},
link: function (scope, elem, attrs) {
function update() {
elem.html(marked(scope.content, { baseUrl: $location.url() }));
}
scope.$watch(attrs.terms, update);
scope.$watch("terms", update);
scope.$watch("options", update);
scope.$watch("content", update);
},
};
},
])
.directive("tree", [
function () {
return {
@@ -748,6 +811,123 @@ angular
};
},
])
.controller("prDashboardController", [
"$scope",
"$http",
"$location",
function ($scope, $http, $location) {
$scope.$on("$routeChangeStart", function () {
// remove tooltip
$('[data-toggle="tooltip"]').tooltip("dispose");
});
$scope.$watch("user.status", () => {
if ($scope.user == null) {
$location.url("/");
}
});
if ($scope.user == null) {
$location.url("/");
}
setTimeout(() => {
$('[data-toggle="tooltip"]').tooltip();
}, 250);
$scope.pullRequests = [];
$scope.search = "";
$scope.filters = {
status: { ready: true, expired: true, removed: false },
};
$scope.orderBy = "-anonymizeDate";
function getPullRequests() {
$http.get("/api/user/anonymized_pull_requests").then(
(res) => {
$scope.pullRequests = res.data;
for (const pr of $scope.pullRequests) {
if (!pr.pageView) {
pr.pageView = 0;
}
if (!pr.lastView) {
pr.lastView = "";
}
pr.options.terms = pr.options.terms.filter((f) => f);
}
},
(err) => {
console.error(err);
}
);
}
getPullRequests();
$scope.removePullRequest = (pr) => {
if (
confirm(
`Are you sure that you want to remove the pull request ${pr.pullRequestId}?`
)
) {
const toast = {
title: `Removing ${pr.pullRequestId}...`,
date: new Date(),
body: `The pull request ${pr.pullRequestId} is going to be removed.`,
};
$scope.toasts.push(toast);
$http.delete(`/api/pr/${pr.pullRequestId}`).then(
() => {
toast.title = `${pr.pullRequestId} is removed.`;
toast.body = `The pull request ${pr.pullRequestId} is removed.`;
getPullRequests();
},
(error) => {
toast.title = `Error during the removal of ${pr.pullRequestId}.`;
toast.body = error.body;
getPullRequests();
}
);
}
};
$scope.updatePullRequest = (pr) => {
const toast = {
title: `Refreshing ${pr.pullRequestId}...`,
date: new Date(),
body: `The pull request ${pr.pullRequestId} is going to be refreshed.`,
};
$scope.toasts.push(toast);
$http.post(`/api/pr/${pr.pullRequestId}/refresh`).then(
() => {
toast.title = `${pr.pullRequestId} is refreshed.`;
toast.body = `The pull request ${pr.pullRequestId} is refreshed.`;
getPullRequests();
},
(error) => {
toast.title = `Error during the refresh of ${pr.pullRequestId}.`;
toast.body = error.body;
getPullRequests();
}
);
};
$scope.pullRequestFilter = (pr) => {
if ($scope.filters.status[pr.status] == false) return false;
if ($scope.search.trim().length == 0) return true;
if ((pr.source.pullRequestId + "").indexOf($scope.search) > -1)
return true;
if (pr.source.repositoryFullName.indexOf($scope.search) > -1)
return true;
if (pr.pullRequestId.indexOf($scope.search) > -1) return true;
return false;
};
},
])
.controller("statusController", [
"$scope",
"$http",
@@ -778,7 +958,10 @@ angular
} else if ($scope.repo.status == "anonymizing") {
$scope.progress = 75;
}
if ($scope.repo.status != "ready" && $scope.repo.status != "error") {
if (
$scope.repo.status != "ready" &&
$scope.repo.status != "error"
) {
setTimeout($scope.getStatus, 2000);
}
},
@@ -1267,11 +1450,25 @@ angular
ts: "typescript",
};
const textFiles = ["license", "txt"];
const imageFiles = ["png", "jpg", "jpeg", "gif", "svg", "ico", "bmp", "tiff", "tif", "webp", "avif", "heif", "heic"];
const imageFiles = [
"png",
"jpg",
"jpeg",
"gif",
"svg",
"ico",
"bmp",
"tiff",
"tif",
"webp",
"avif",
"heif",
"heic",
];
$scope.$on("$routeUpdate", function (event, current) {
if (($routeParams.path || "") == $scope.filePath) {
return
return;
}
$scope.filePath = $routeParams.path || "";
$scope.paths = $scope.filePath.split("/");
@@ -1409,7 +1606,9 @@ angular
if ($scope.type == "md") {
const md = contentAbs2Relative(res.data);
$scope.content = $sce.trustAsHtml(marked(md, { baseUrl: $location.url() }));
$scope.content = $sce.trustAsHtml(
marked(md, { baseUrl: $location.url() })
);
$scope.type = "html";
}
if ($scope.type == "org") {
@@ -1481,7 +1680,7 @@ angular
if (window.location.hash && window.location.hash.match(/^#L\d+/)) {
let from = 0;
let to = 0;
if (window.location.hash.indexOf('-') > -1) {
if (window.location.hash.indexOf("-") > -1) {
const match = window.location.hash.match(/^#L(\d+)-L(\d+)/);
from = parseInt(match[1]) - 1;
to = parseInt(match[2]) - 1;
@@ -1489,9 +1688,13 @@ angular
from = parseInt(window.location.hash.substring(2)) - 1;
to = from;
}
const Range = ace.require('ace/range').Range;
_editor.session.addMarker(new Range(from, 0, to, 1), "highlighted-line", "fullLine");
const Range = ace.require("ace/range").Range;
_editor.session.addMarker(
new Range(from, 0, to, 1),
"highlighted-line",
"fullLine"
);
setTimeout(() => {
_editor.scrollToLine(from, true, true, function () {});
}, 100);
@@ -1559,6 +1762,375 @@ angular
init();
},
])
.controller("anonymizePullRequestController", [
"$scope",
"$http",
"$sce",
"$routeParams",
"$location",
"$translate",
function ($scope, $http, $sce, $routeParams, $location, $translate) {
$scope.pullRequestUrl = "";
$scope.pullRequestId = "";
$scope.terms = "";
$scope.defaultTerms = "";
$scope.options = {
expirationMode: "remove",
expirationDate: new Date(),
update: false,
image: true,
link: true,
body: true,
title: true,
origin: false,
diff: true,
comments: true,
username: true,
date: true,
};
$scope.options.expirationDate.setMonth(
$scope.options.expirationDate.getMonth() + 4
);
$scope.isUpdate = false;
function getDefault(cb) {
$http.get("/api/user/default").then((res) => {
const data = res.data;
if (data.terms) {
$scope.defaultTerms = data.terms.join("\n");
}
$scope.options = Object.assign({}, $scope.options, data.options);
$scope.options.expirationDate = new Date(
$scope.options.expirationDate
);
$scope.options.expirationDate.setDate(
$scope.options.expirationDate.getDate() + 90
);
if (cb) cb();
});
}
getDefault(() => {
if ($routeParams.pullRequestId && $routeParams.pullRequestId != "") {
$scope.isUpdate = true;
$scope.pullRequestId = $routeParams.pullRequestId;
$http.get("/api/pr/" + $scope.pullRequestId).then(
async (res) => {
$scope.pullRequestUrl =
"https://github.com/" +
res.data.source.repositoryFullName +
"/pull/" +
res.data.source.pullRequestId;
$scope.terms = res.data.options.terms.filter((f) => f).join("\n");
$scope.source = res.data.source;
$scope.options = res.data.options;
$scope.conference = res.data.conference;
if (res.data.options.expirationDate) {
$scope.options.expirationDate = new Date(
res.data.options.expirationDate
);
} else {
$scope.options.expirationDate = new Date();
$scope.options.expirationDate.setDate(
$scope.options.expirationDate.getDate() + 90
);
}
$scope.details = (
await $http.get(
`/api/pr/${res.data.source.repositoryFullName}/${res.data.source.pullRequestId}`
)
).data;
$scope.$apply();
},
(err) => {
$location.url("/404");
}
);
$scope.$watch("anonymize", () => {
$scope.anonymizeForm.pullRequestId.$$element[0].disabled = true;
$scope.anonymizeForm.pullRequestUrl.$$element[0].disabled = true;
});
}
});
$scope.pullRequestSelected = async () => {
$scope.terms = $scope.defaultTerms;
$scope.pullRequestId = "";
$scope.source = {};
try {
const o = parseGithubUrl($scope.pullRequestUrl);
if (!o.pullRequestId) {
$scope.anonymizeForm.pullRequestUrl.$setValidity("github", false);
return;
}
$scope.anonymizeForm.pullRequestUrl.$setValidity("github", true);
} catch (error) {
$scope.anonymizeForm.pullRequestUrl.$setValidity("github", false);
return;
}
try {
await getDetails();
} catch (error) {}
$scope.$apply();
$('[data-toggle="tooltip"]').tooltip();
};
$('[data-toggle="tooltip"]').tooltip();
$scope.$watch("options.update", (v) => {});
async function getDetails() {
const o = parseGithubUrl($scope.pullRequestUrl);
try {
resetValidity();
const res = await $http.get(
`/api/pr/${o.owner}/${o.repo}/${o.pullRequestId}`
);
$scope.details = res.data;
if ($scope.options.origin) {
$scope.pullRequestId = o.repo + "-" + generateRandomId(4);
} else {
$scope.pullRequestId = generateRandomId(4);
}
} catch (error) {
if (error.data) {
$translate("ERRORS." + error.data.error).then((translation) => {
$scope.error = translation;
}, console.error);
displayErrorMessage(error.data.error);
}
$scope.anonymizeForm.pullRequestUrl.$setValidity("missing", false);
throw error;
}
}
function getConference() {
if (!$scope.conference) return;
$http.get("/api/conferences/" + $scope.conference).then(
(res) => {
$scope.conference_data = res.data;
$scope.conference_data.startDate = new Date(
$scope.conference_data.startDate
);
$scope.conference_data.endDate = new Date(
$scope.conference_data.endDate
);
$scope.options.expirationDate = new Date(
$scope.conference_data.endDate
);
$scope.options.expirationMode = "remove";
$scope.options.update = $scope.conference_data.options.update;
$scope.options.image = $scope.conference_data.options.image;
$scope.options.pdf = $scope.conference_data.options.pdf;
$scope.options.notebook = $scope.conference_data.options.notebook;
$scope.options.link = $scope.conference_data.options.link;
},
(err) => {
$scope.conference_data = null;
}
);
}
$scope.anonymize = function (content) {
const urlRegex =
/<?\b((https?|ftp|file):\/\/)[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]\b\/?>?/g;
if (!$scope.options.image) {
// remove images
content = content.replace(
/!\[[^\]]*\]\((?<filename>.*?)(?=\"|\))(?<optionalpart>\".*\")?\)/g,
""
);
}
if (!$scope.options.link) {
content = content.replace(
urlRegex,
$scope.site_options.ANONYMIZATION_MASK
);
}
const terms = $scope.terms.split("\n");
for (let i = 0; i < terms.length; i++) {
const term = terms[i];
if (term.trim() == "") {
continue;
}
// remove whole url if it contains the term
content = content.replace(urlRegex, (match) => {
if (new RegExp(`\\b${term}\\b`, "gi").test(match))
return $scope.site_options.ANONYMIZATION_MASK + "-" + (i + 1);
return match;
});
// remove the term in the text
content = content.replace(
new RegExp(`\\b${term}\\b`, "gi"),
$scope.site_options.ANONYMIZATION_MASK + "-" + (i + 1)
);
}
return content;
};
function resetValidity() {
$scope.anonymizeForm.pullRequestId.$setValidity("used", true);
$scope.anonymizeForm.pullRequestId.$setValidity("format", true);
$scope.anonymizeForm.pullRequestUrl.$setValidity("used", true);
$scope.anonymizeForm.pullRequestUrl.$setValidity("missing", true);
$scope.anonymizeForm.pullRequestUrl.$setValidity("access", true);
$scope.anonymizeForm.conference.$setValidity("activated", true);
$scope.anonymizeForm.terms.$setValidity("format", true);
$scope.anonymizeForm.terms.$setValidity("format", true);
}
function displayErrorMessage(message) {
switch (message) {
case "repoId_already_used":
$scope.anonymizeForm.repoId.$setValidity("used", false);
break;
case "invalid_repoId":
$scope.anonymizeForm.repoId.$setValidity("format", false);
break;
case "options_not_provided":
$scope.anonymizeForm.repoId.$setValidity("format", false);
break;
case "repo_already_anonymized":
$scope.anonymizeForm.repoUrl.$setValidity("used", false);
break;
case "invalid_terms_format":
$scope.anonymizeForm.terms.$setValidity("format", false);
break;
case "invalid_terms_format":
$scope.anonymizeForm.terms.$setValidity("format", false);
break;
case "repo_not_found":
$scope.anonymizeForm.repoUrl.$setValidity("missing", false);
break;
case "repo_not_accessible":
$scope.anonymizeForm.repoUrl.$setValidity("access", false);
break;
case "conf_not_activated":
$scope.anonymizeForm.conference.$setValidity("activated", false);
break;
default:
$scope.anonymizeForm.$setValidity("error", false);
break;
}
}
function getPullRequest() {
const o = parseGithubUrl($scope.pullRequestUrl);
return {
pullRequestId: $scope.pullRequestId,
terms: $scope.terms
.trim()
.split("\n")
.filter((f) => f),
source: {
repositoryFullName: `${o.owner}/${o.repo}`,
pullRequestId: o.pullRequestId,
},
options: $scope.options,
conference: $scope.conference,
};
}
async function sendPullRequest(url) {
resetValidity();
try {
const newPR = getPullRequest();
await $http.post(url, newPR, {
headers: { "Content-Type": "application/json" },
});
window.location.href = "/pr/" + $scope.pullRequestId;
} catch (error) {
if (error.data) {
$translate("ERRORS." + error.data.error).then((translation) => {
$scope.error = translation;
}, console.error);
displayErrorMessage(error.data.error);
} else {
console.error(error);
}
}
}
$scope.anonymizePullRequest = (event) => {
event.target.disabled = true;
sendPullRequest("/api/pr/").finally(() => {
event.target.disabled = false;
$scope.$apply();
});
};
$scope.updatePullRequest = async (event) => {
event.target.disabled = true;
sendPullRequest("/api/pr/" + $scope.pullRequestId).finally(() => {
event.target.disabled = false;
$scope.$apply();
});
};
$scope.$watch("conference", async (v) => {
getConference();
});
},
])
.controller("pullRequestController", [
"$scope",
"$http",
"$location",
"$routeParams",
"$sce",
function ($scope, $http, $location, $routeParams, $sce) {
async function getOption(callback) {
$http.get(`/api/pr/${$scope.pullRequestId}/options`).then(
(res) => {
$scope.options = res.data;
if ($scope.options.url) {
// the repository is expired with redirect option
window.location = $scope.options.url;
return;
}
if (callback) {
callback(res.data);
}
},
(err) => {
$scope.type = "error";
$scope.content = err.data.error;
}
);
}
async function getPullRequest(callback) {
$http.get(`/api/pr/${$scope.pullRequestId}/content`).then(
(res) => {
$scope.details = res.data;
if (callback) {
callback(res.data);
}
},
(err) => {
$scope.type = "error";
$scope.content = err.data.error;
}
);
}
function init() {
$scope.pullRequestId = $routeParams.pullRequestId;
$scope.type = "loading";
getOption((_) => {
getPullRequest();
});
}
init();
},
])
.controller("conferencesController", [
"$scope",
"$http",

View File

@@ -8,7 +8,7 @@ function urlRel2abs(url) {
return url; //Url is already absolute
}
var base_url = location.href.match(/^(.+)\/?(?:#.+)?$/)[0] + "/";
if (url.substring(0, 2) == "//") return location.protocol + url;
else if (url.charAt(0) == "/")
return location.protocol + "//" + location.host + url;
@@ -28,7 +28,7 @@ function urlRel2abs(url) {
.replace(/'/g, "%27")
.replace(/</g, "%3C")
.replace(/>/g, "%3E");
return url;
}
@@ -93,11 +93,17 @@ function generateRandomId(length) {
}
function parseGithubUrl(url) {
var matches = url.replace(".git", "").match(/.*?github.com\/([\w-\._]+)\/([\w-\._]+)/);
if (matches && matches.length == 3) {
if (!url) throw "Invalid url";
const matches = url
.replace(".git", "")
.match(
/.*?github.com\/(?<owner>[\w-\._]+)\/(?<repo>[\w-\._]+)(\/pull\/(?<PR>[0-9]+))?/
);
if (matches && matches.groups.owner && matches.groups.repo) {
return {
owner: matches[1],
repo: matches[2],
owner: matches.groups.owner,
repo: matches.groups.repo,
pullRequestId: matches.groups.PR,
};
} else {
throw "Invalid url";

311
src/PullRequest.ts Normal file
View File

@@ -0,0 +1,311 @@
import { RepositoryStatus, Source, Tree, TreeElement, TreeFile } from "./types";
import User from "./User";
import { anonymizeContent, anonymizePath } from "./anonymize-utils";
import UserModel from "./database/users/users.model";
import Conference from "./Conference";
import ConferenceModel from "./database/conference/conferences.model";
import AnonymousError from "./AnonymousError";
import { IAnonymizedPullRequestDocument } from "./database/anonymizedPullRequests/anonymizedPullRequests.types";
import config from "../config";
import { Octokit } from "@octokit/rest";
import got from "got";
export default class PullRequest {
private _model: IAnonymizedPullRequestDocument;
owner: User;
constructor(data: IAnonymizedPullRequestDocument) {
this._model = data;
this.owner = new User(new UserModel({ _id: data.owner }));
}
getToken() {
if (this.owner && this.owner.accessToken) {
return this.owner.accessToken;
}
if (this._model.source.accessToken) {
try {
return this._model.source.accessToken;
} catch (error) {
console.debug("[ERROR] Token is invalid", this.pullRequestId);
}
}
return config.GITHUB_TOKEN;
}
async download() {
console.debug("[INFO] Downloading pull request", this.pullRequestId);
const auth = this.getToken();
const octokit = new Octokit({ auth });
const [owner, repo] = this._model.source.repositoryFullName.split("/");
const pull_number = this._model.source.pullRequestId;
const prInfo = await octokit.rest.pulls.get({
owner,
repo,
pull_number,
});
prInfo.data.updated_at;
prInfo.data.draft;
prInfo.data.merged;
prInfo.data.merged_at;
prInfo.data.state;
prInfo.data.base.repo.full_name;
prInfo.data.head.repo.full_name;
const comments = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: pull_number,
per_page: 100,
});
// const commits = await octokit.rest.pulls.listCommits({
// owner,
// repo,
// pull_number,
// per_page: 100,
// });
// const files = await octokit.rest.pulls.listFiles({
// owner,
// repo,
// pull_number,
// per_page: 100,
// });
const diff = await got(prInfo.data.diff_url);
this._model.pullRequest = {
diff: diff.body,
title: prInfo.data.title,
body: prInfo.data.body,
creationDate: new Date(prInfo.data.created_at),
updatedDate: new Date(prInfo.data.updated_at),
draft: prInfo.data.draft,
merged: prInfo.data.merged,
mergedDate: prInfo.data.merged_at
? new Date(prInfo.data.merged_at)
: null,
state: prInfo.data.state,
baseRepositoryFullName: prInfo.data.base.repo.full_name,
headRepositoryFullName: prInfo.data.head.repo.full_name,
comments: comments.data.map((comment) => ({
body: comment.body,
creationDate: new Date(comment.created_at),
updatedDate: new Date(comment.updated_at),
author: comment.user.login,
})),
};
}
/**
* Check the status of the pullRequest
*/
check() {
if (
this._model.options.expirationMode !== "never" &&
this.status == "ready"
) {
if (this._model.options.expirationDate <= new Date()) {
this.expire();
}
}
if (
this.status == "expired" ||
this.status == "expiring" ||
this.status == "removing" ||
this.status == "removed"
) {
throw new AnonymousError("pullRequest_expired", {
object: this,
httpStatus: 410,
});
}
const fiveMinuteAgo = new Date();
fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5);
if (
this.status == "preparing" ||
(this.status == "download" && this._model.statusDate > fiveMinuteAgo)
) {
throw new AnonymousError("pullRequest_not_ready", {
object: this,
});
}
}
/**
* Update the pullRequest if a new commit exists
*
* @returns void
*/
async updateIfNeeded(opt?: { force: boolean }): Promise<void> {
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if (
opt?.force ||
(this._model.options.update && this._model.lastView < yesterday)
) {
await this.download();
this._model.lastView = new Date();
await this._model.save();
}
}
/**
* Download the require state for the pullRequest to work
*
* @returns void
*/
async anonymize() {
if (this.status == "ready") return;
await this.updateStatus("preparing");
await this.updateIfNeeded({ force: true });
return this.updateStatus("ready");
}
/**
* Update the last view and view count
*/
async countView() {
this._model.lastView = new Date();
this._model.pageView = (this._model.pageView || 0) + 1;
return this._model.save();
}
/**
* Update the status of the pullRequest
* @param status the new status
* @param errorMessage a potential error message to display
*/
async updateStatus(status: RepositoryStatus, statusMessage?: string) {
this._model.status = status;
this._model.statusDate = new Date();
this._model.statusMessage = statusMessage;
return this._model.save();
}
/**
* Expire the pullRequest
*/
async expire() {
await this.updateStatus("expiring");
await this.resetSate();
await this.updateStatus("expired");
}
/**
* Remove the pullRequest
*/
async remove() {
await this.updateStatus("removing");
await this.resetSate();
await this.updateStatus("removed");
}
/**
* Reset/delete the state of the pullRequest
*/
async resetSate(status?: RepositoryStatus, statusMessage?: string) {
if (status) this._model.status = status;
if (statusMessage) this._model.statusMessage = statusMessage;
// remove cache
this._model.pullRequest = null;
return Promise.all([this._model.save()]);
}
/**
* Returns the conference of the pullRequest
*
* @returns conference of the pullRequest
*/
async conference(): Promise<Conference | null> {
if (!this._model.conference) {
return null;
}
const conference = await ConferenceModel.findOne({
conferenceID: this._model.conference,
});
if (conference) return new Conference(conference);
return null;
}
/***** Getters ********/
get pullRequestId() {
return this._model.pullRequestId;
}
get options() {
return this._model.options;
}
get source() {
return this._model.source;
}
get model() {
return this._model;
}
get status() {
return this._model.status;
}
content() {
const output: any = {
anonymizeDate: this._model.anonymizeDate,
merged: this._model.pullRequest.merged,
mergedDate: this._model.pullRequest.mergedDate,
state: this._model.pullRequest.state,
draft: this._model.pullRequest.draft,
};
if (this.options.title) {
output.title = anonymizeContent(this._model.pullRequest.title, this);
}
if (this.options.body) {
output.body = anonymizeContent(this._model.pullRequest.body, this);
}
if (this.options.comments) {
output.comments = this._model.pullRequest.comments.map((comment) => {
const o: any = {};
if (this.options.body) o.body = anonymizeContent(comment.body, this);
if (this.options.username)
o.author = anonymizeContent(comment.author, this);
if (this.options.date) {
o.updatedDate = comment.updatedDate;
o.creationDate = comment.creationDate;
}
return o;
});
}
if (this.options.diff) {
output.diff = anonymizeContent(this._model.pullRequest.diff, this);
}
if (this.options.origin) {
output.baseRepositoryFullName =
this._model.pullRequest.baseRepositoryFullName;
}
if (this.options.date) {
output.updatedDate = this.model.pullRequest.updatedDate;
output.creationDate = this.model.pullRequest.creationDate;
}
return output;
}
toJSON() {
return {
pullRequestId: this._model.pullRequestId,
options: this._model.options,
conference: this._model.conference,
anonymizeDate: this._model.anonymizeDate,
status: this._model.status,
state: this.model.pullRequest.state,
merged: this.model.pullRequest.merged,
mergedDate: this.model.pullRequest.mergedDate,
statusMessage: this._model.statusMessage,
source: {
pullRequestId: this._model.source.pullRequestId,
repositoryFullName: this._model.source.repositoryFullName,
},
pullRequest: this._model.pullRequest,
lastView: this._model.lastView,
pageView: this._model.pageView,
};
}
}

View File

@@ -4,6 +4,8 @@ import RepositoryModel from "./database/repositories/repositories.model";
import { IUserDocument } from "./database/users/users.types";
import Repository from "./Repository";
import { GitHubRepository } from "./source/GitHubRepository";
import PullRequest from "./PullRequest";
import AnonymizedPullRequestModel from "./database/anonymizedPullRequests/anonymizedPullRequests.model";
/**
* Model for a user
@@ -136,6 +138,31 @@ export default class User {
await Promise.all(promises);
return repositories;
}
/**
* Get the lost of anonymized repositories
* @returns the list of anonymized repositories
*/
async getPullRequests() {
const pullRequests = (
await AnonymizedPullRequestModel.find({
owner: this.id,
}).exec()
).map((d) => new PullRequest(d));
const promises = [];
for (let repo of pullRequests) {
if (
repo.status == "ready" &&
repo.options.expirationMode != "never" &&
repo.options.expirationDate != null &&
repo.options.expirationDate < new Date()
) {
// expire the repository
promises.push(repo.expire());
}
}
await Promise.all(promises);
return pullRequests;
}
get model() {
return this._model;

View File

@@ -72,7 +72,24 @@ export function anonymizeStream(filename: string, repository: Repository) {
return ts;
}
export function anonymizeContent(content: string, repository: Repository) {
interface Anonymizationptions {
repoId?: string;
source?: {};
options: {
terms: string[];
image: boolean;
link: boolean;
pageSource?: {
branch: string;
path: string;
};
};
}
export function anonymizeContent(
content: string,
repository: Anonymizationptions
) {
if (repository.options?.image === false) {
// remove image in markdown
content = content.replace(

View File

@@ -0,0 +1,14 @@
import { model } from "mongoose";
import AnonymizedPullRequestSchema from "./anonymizedPullRequests.schema";
import {
IAnonymizedPullRequestDocument,
IAnonymizedPullRequestModel,
} from "./anonymizedPullRequests.types";
const AnonymizedPullRequestModel = model<IAnonymizedPullRequestDocument>(
"AnonymizedPullRequest",
AnonymizedPullRequestSchema
) as IAnonymizedPullRequestModel;
export default AnonymizedPullRequestModel;

View File

@@ -0,0 +1,66 @@
import { Schema } from "mongoose";
const AnonymizedPullRequestSchema = new Schema({
pullRequestId: {
type: String,
index: { unique: true },
},
status: {
type: String,
default: "preparing",
},
statusDate: Date,
statusMessage: String,
anonymizeDate: Date,
lastView: Date,
pageView: Number,
owner: Schema.Types.ObjectId,
conference: String,
source: {
pullRequestId: Number,
repositoryFullName: String,
accessToken: String,
},
options: {
terms: [String],
expirationMode: { type: String },
expirationDate: Date,
update: Boolean,
image: Boolean,
link: Boolean,
title: Boolean,
body: Boolean,
comments: Boolean,
diff: Boolean,
origin: Boolean,
username: Boolean,
date: Boolean,
},
dateOfEntry: {
type: Date,
default: new Date(),
},
pullRequest: {
diff: String,
title: String,
body: String,
creationDate: Date,
updatedDate: Date,
draft: Boolean,
merged: Boolean,
mergedDate: Date,
state: String,
baseRepositoryFullName: String,
headRepositoryFullName: String,
comments: [
{
body: String,
creationDate: Date,
updatedDate: Date,
author: String,
},
],
},
});
export default AnonymizedPullRequestSchema;

View File

@@ -0,0 +1,61 @@
import { Document, Model } from "mongoose";
import { RepositoryStatus } from "../../types";
export interface IAnonymizedPullRequest {
pullRequestId: string;
status?: RepositoryStatus;
statusMessage?: string;
statusDate: Date;
anonymizeDate: Date;
source: {
pullRequestId: number;
repositoryFullName?: string;
accessToken?: string;
};
owner: string;
conference: string;
options: {
terms: string[];
expirationMode: "never" | "redirect" | "remove";
expirationDate?: Date;
update: boolean;
image: boolean;
link: boolean;
title: boolean;
body: boolean;
comments: boolean;
diff: boolean;
origin: boolean;
username: boolean;
date: boolean;
};
pageView: number;
lastView: Date;
pullRequest: {
diff: string;
title: string;
body: string;
creationDate: Date;
updatedDate: Date;
draft?: boolean;
merged?: boolean;
mergedDate?: Date;
state?: string;
baseRepositoryFullName?: string;
headRepositoryFullName?: string;
comments?: {
body: string;
creationDate: Date;
updatedDate: Date;
author: string;
}[];
};
}
export interface IAnonymizedPullRequestDocument
extends IAnonymizedPullRequest,
Document {
setLastUpdated: (this: IAnonymizedPullRequestDocument) => Promise<void>;
}
export interface IAnonymizedPullRequestModel
extends Model<IAnonymizedPullRequestDocument> {}

View File

@@ -3,6 +3,8 @@ import Repository from "../Repository";
import config from "../../config";
import AnonymizedRepositoryModel from "./anonymizedRepositories/anonymizedRepositories.model";
import AnonymousError from "../AnonymousError";
import AnonymizedPullRequestModel from "./anonymizedPullRequests/anonymizedPullRequests.model";
import PullRequest from "../PullRequest";
const MONGO_URL = `mongodb://${config.DB_USERNAME}:${config.DB_PASSWORD}@${config.DB_HOSTNAME}:27017/`;
@@ -17,7 +19,7 @@ export async function connect() {
}
export async function getRepository(repoId: string) {
if (!repoId || repoId == 'undefined') {
if (!repoId || repoId == "undefined") {
throw new AnonymousError("repo_not_found", {
object: repoId,
httpStatus: 404,
@@ -31,3 +33,20 @@ export async function getRepository(repoId: string) {
});
return new Repository(data);
}
export async function getPullRequest(pullRequestId: string) {
if (!pullRequestId || pullRequestId == "undefined") {
throw new AnonymousError("pull_request_not_found", {
object: pullRequestId,
httpStatus: 404,
});
}
const data = await AnonymizedPullRequestModel.findOne({
pullRequestId,
});
if (!data)
throw new AnonymousError("pull_request_not_found", {
object: pullRequestId,
httpStatus: 404,
});
return new PullRequest(data);
}

View File

@@ -1,3 +1,5 @@
import pullRequestPrivate from "./pullRequest-private";
import pullRequestPublic from "./pullRequest-public";
import repositoryPrivate from "./repository-private";
import repositoryPublic from "./repository-public";
import conference from "./conference";
@@ -8,6 +10,8 @@ import option from "./option";
import admin from "./admin";
export default {
pullRequestPrivate,
pullRequestPublic,
repositoryPrivate,
repositoryPublic,
file,

View File

@@ -0,0 +1,245 @@
import * as express from "express";
import { ensureAuthenticated } from "./connection";
import {
getPullRequest,
getUser,
handleError,
isOwnerOrAdmin,
} from "./route-utils";
import AnonymousError from "../AnonymousError";
import { IAnonymizedPullRequestDocument } from "../database/anonymizedPullRequests/anonymizedPullRequests.types";
import PullRequest from "../PullRequest";
import AnonymizedPullRequestModel from "../database/anonymizedPullRequests/anonymizedPullRequests.model";
const router = express.Router();
// user needs to be connected for all user API
router.use(ensureAuthenticated);
// refresh pullRequest
router.post(
"/:pullRequestId/refresh",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
if (
pullRequest.status == "preparing" ||
pullRequest.status == "removing" ||
pullRequest.status == "expiring"
)
return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
await pullRequest.anonymize()
res.json({ status: pullRequest.status });
} catch (error) {
handleError(error, res, req);
}
}
);
// delete a pullRequest
router.delete(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
try {
if (pullRequest.status == "removed")
throw new AnonymousError("is_removed", {
object: req.params.pullRequestId,
httpStatus: 410,
});
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
await pullRequest.remove();
return res.json({ status: pullRequest.status });
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:owner/:repository/:pullRequestId",
async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
try {
const pullRequest = new PullRequest(
new AnonymizedPullRequestModel({
owner: user.id,
source: {
pullRequestId: parseInt(req.params.pullRequestId),
repositoryFullName: `${req.params.owner}/${req.params.repository}`,
},
})
);
await pullRequest.download();
res.json(pullRequest.toJSON());
} catch (error) {
handleError(error, res, req);
}
}
);
// get pullRequest information
router.get(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
res.json(pullRequest.toJSON());
} catch (error) {
handleError(error, res, req);
}
}
);
function validateNewPullRequest(pullRequestUpdate): void {
const validCharacters = /^[0-9a-zA-Z\-\_]+$/;
if (
!pullRequestUpdate.pullRequestId.match(validCharacters) ||
pullRequestUpdate.pullRequestId.length < 3
) {
throw new AnonymousError("invalid_pullRequestId", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.source.repositoryFullName) {
throw new AnonymousError("repository_not_specified", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.source.pullRequestId) {
throw new AnonymousError("pullRequestId_not_specified", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (
parseInt(pullRequestUpdate.source.pullRequestId) !=
pullRequestUpdate.source.pullRequestId
) {
throw new AnonymousError("pullRequestId_is_not_a_number", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!pullRequestUpdate.options) {
throw new AnonymousError("options_not_provided", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
if (!Array.isArray(pullRequestUpdate.terms)) {
throw new AnonymousError("invalid_terms_format", {
object: pullRequestUpdate,
httpStatus: 400,
});
}
}
function updatePullRequestModel(
model: IAnonymizedPullRequestDocument,
pullRequestUpdate: any
) {
model.options = {
terms: pullRequestUpdate.terms,
expirationMode: pullRequestUpdate.options.expirationMode,
expirationDate: pullRequestUpdate.options.expirationDate
? new Date(pullRequestUpdate.options.expirationDate)
: null,
update: pullRequestUpdate.options.update,
image: pullRequestUpdate.options.image,
link: pullRequestUpdate.options.link,
body: pullRequestUpdate.options.body,
title: pullRequestUpdate.options.title,
username: pullRequestUpdate.options.username,
origin: pullRequestUpdate.options.origin,
diff: pullRequestUpdate.options.diff,
comments: pullRequestUpdate.options.comments,
date: pullRequestUpdate.options.date,
};
}
// update a pullRequest
router.post(
"/:pullRequestId/",
async (req: express.Request, res: express.Response) => {
try {
const pullRequest = await getPullRequest(req, res, { nocheck: true });
if (!pullRequest) return;
const user = await getUser(req);
isOwnerOrAdmin([pullRequest.owner.id], user);
const pullRequestUpdate = req.body;
validateNewPullRequest(pullRequestUpdate);
pullRequest.model.anonymizeDate = new Date();
updatePullRequestModel(pullRequest.model, pullRequestUpdate);
// TODO handle conference
pullRequest.model.conference = pullRequestUpdate.conference;
await pullRequest.updateIfNeeded({ force: true });
res.json(pullRequest.toJSON());
} catch (error) {
return handleError(error, res, req);
}
}
);
// add pullRequest
router.post("/", async (req: express.Request, res: express.Response) => {
const user = await getUser(req);
const pullRequestUpdate = req.body;
try {
validateNewPullRequest(pullRequestUpdate);
const pullRequest = new PullRequest(
new AnonymizedPullRequestModel({
owner: user.id,
options: pullRequestUpdate.options,
})
);
pullRequest.model.pullRequestId = pullRequestUpdate.pullRequestId;
pullRequest.model.anonymizeDate = new Date();
pullRequest.model.owner = user.id;
updatePullRequestModel(pullRequest.model, pullRequestUpdate);
pullRequest.source.accessToken = user.accessToken;
pullRequest.source.pullRequestId = pullRequestUpdate.source.pullRequestId;
pullRequest.source.repositoryFullName =
pullRequestUpdate.source.repositoryFullName;
pullRequest.conference = pullRequestUpdate.conference;
await pullRequest.anonymize()
res.send(pullRequest.toJSON());
} catch (error) {
if (error.message?.indexOf(" duplicate key") > -1) {
return handleError(
new AnonymousError("pullRequestId_already_used", {
httpStatus: 400,
cause: error,
object: pullRequestUpdate,
}),
res,
req
);
}
return handleError(error, res, req);
}
});
export default router;

View File

@@ -0,0 +1,84 @@
import * as express from "express";
import { getPullRequest, handleError } from "./route-utils";
import AnonymousError from "../AnonymousError";
const router = express.Router();
router.get(
"/:pullRequestId/options",
async (req: express.Request, res: express.Response) => {
try {
res.header("Cache-Control", "no-cache");
const pr = await getPullRequest(req, res, { nocheck: true });
if (!pr) return;
let redirectURL = null;
if (pr.status == "expired" && pr.options.expirationMode == "redirect") {
redirectURL = `https://github.com/${pr.source.repositoryFullName}/pull/${pr.source.pullRequestId}`;
} else {
if (
pr.status == "expired" ||
pr.status == "expiring" ||
pr.status == "removing" ||
pr.status == "removed"
) {
throw new AnonymousError("pull_request_expired", {
object: pr,
httpStatus: 410,
});
}
const fiveMinuteAgo = new Date();
fiveMinuteAgo.setMinutes(fiveMinuteAgo.getMinutes() - 5);
if (pr.status != "ready") {
if (
pr.model.statusDate < fiveMinuteAgo
// && repo.status != "preparing"
) {
await pr.updateIfNeeded({ force: true });
}
if (pr.status == "error") {
throw new AnonymousError(
pr.model.statusMessage
? pr.model.statusMessage
: "pull_request_not_available",
{
object: pr,
httpStatus: 500,
}
);
}
throw new AnonymousError("pull_request_not_ready", {
httpStatus: 404,
object: pr,
});
}
await pr.updateIfNeeded();
}
res.json({
url: redirectURL,
lastUpdateDate: pr.model.statusDate,
});
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/:pullRequestId/content",
async (req: express.Request, res: express.Response) => {
const pullRequest = await getPullRequest(req, res);
if (!pullRequest) return;
try {
await pullRequest.countView();
res.header("Cache-Control", "no-cache");
res.json(pullRequest.content());
} catch (error) {
handleError(error, res, req);
}
}
);
export default router;

View File

@@ -60,11 +60,10 @@ router.get(
router.get(
"/:repoId/files",
async (req: express.Request, res: express.Response) => {
res.header("Cache-Control", "no-cache");
const repo = await getRepo(req, res);
if (!repo) return;
try {
res.header("Cache-Control", "no-cache");
res.json(await repo.anonymizedFiles({ includeSha: false }));
} catch (error) {
handleError(error, res, req);
@@ -76,6 +75,7 @@ router.get(
"/:repoId/options",
async (req: express.Request, res: express.Response) => {
try {
res.header("Cache-Control", "no-cache");
const repo = await getRepo(req, res, { nocheck: true });
if (!repo) return;
let redirectURL = null;
@@ -146,7 +146,6 @@ router.get(
download = true;
}
res.header("Cache-Control", "no-cache");
res.json({
url: redirectURL,
download,

View File

@@ -5,6 +5,35 @@ import UserModel from "../database/users/users.model";
import User from "../User";
import * as io from "@pm2/io";
export async function getPullRequest(
req: express.Request,
res: express.Response,
opt?: { nocheck?: boolean }
) {
try {
const pullRequest = await db.getPullRequest(req.params.pullRequestId);
if (opt?.nocheck == true) {
} else {
// redirect if the repository is expired
if (
pullRequest.status == "expired" &&
pullRequest.options.expirationMode == "redirect"
) {
res.redirect(
`http://github.com/${pullRequest.source.repositoryFullName}/pull/${pullRequest.source.pullRequestId}`
);
return null;
}
pullRequest.check();
}
return pullRequest;
} catch (error) {
handleError(error, res, req);
return null;
}
}
export async function getRepo(
req: express.Request,
res: express.Response,
@@ -50,7 +79,7 @@ function printError(error: any, req?: express.Request) {
if (req) {
message += ` ${req.originalUrl}`;
// ignore common error
if (req.originalUrl === '/api/repo/undefined/options') return
if (req.originalUrl === "/api/repo/undefined/options") return;
}
console.error(message);
} else if (error instanceof Error) {

View File

@@ -97,6 +97,21 @@ router.get(
}
}
);
router.get(
"/anonymized_pull_requests",
async (req: express.Request, res: express.Response) => {
try {
const user = await getUser(req);
res.json(
(await user.getPullRequests()).map((x) => {
return x.toJSON();
})
);
} catch (error) {
handleError(error, res, req);
}
}
);
router.get(
"/all_repositories",

View File

@@ -6,7 +6,7 @@ import GitHubDownload from "../source/GitHubDownload";
import AnonymousError from "../AnonymousError";
import { TreeElement } from "../types";
import * as marked from "marked";
import { anonymizeContent, streamToString } from "../anonymize-utils";
import { streamToString } from "../anonymize-utils";
const router = express.Router();
@@ -103,7 +103,7 @@ async function webView(req: express.Request, res: express.Response) {
}
if ((await f.extension()) == "md") {
const content = await streamToString(await f.anonymizedContent());
res.send(marked.marked(content));
res.contentType("html").send(marked.marked(content));
} else {
f.send(res);
}

View File

@@ -90,6 +90,8 @@ export default async function start() {
apiRouter.use("/repo", router.repositoryPublic);
apiRouter.use("/repo", speedLimiter, router.file);
apiRouter.use("/repo", speedLimiter, router.repositoryPrivate);
apiRouter.use("/pr", speedLimiter, router.pullRequestPrivate);
apiRouter.use("/pr", speedLimiter, router.pullRequestPublic);
apiRouter.get("/message", async (_, res) => {
if (ofs.existsSync("./message.txt")) {