Files
anonymous_github/public/partials/anonymize.htm
T
tdurieux d9104c2ec2 Update commit on branch refresh and validate commit exists on save
Refresh button now always updates the commit to the latest SHA instead
of preserving the stale one in edit mode. Both create and update routes
verify the commit still exists on GitHub before persisting.
2026-05-06 21:14:53 +03:00

476 lines
28 KiB
HTML

<div class="anonymize-page h-100">
<!-- ===== STATE 1: No URL — centered input ===== -->
<div class="anonymize-landing" ng-hide="sourceUrl">
<div class="anonymize-landing-inner">
<div class="paper-crumbs">My work &nbsp;/&nbsp; <span class="here">New anonymization</span></div>
<h1 class="paper-page-title">New <em>anonymization</em></h1>
<p class="paper-page-lede">
Paste a GitHub repository, pull-request, or gist URL. We&rsquo;ll fetch
it, strip every trace of identity, and hand you back a stable link.
</p>
<div class="form-group mt-4 mb-2">
<label class="paper-field-label" for="sourceUrl-landing">Source URL</label>
<input
id="sourceUrl-landing"
type="text"
class="form-control form-control-lg"
ng-model="sourceUrl"
placeholder="https://github.com/owner/repo, https://github.com/owner/repo/pull/42, or https://gist.github.com/owner/abcdef…"
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
ng-change="urlSelected()"
/>
</div>
<small class="form-text" style="color: var(--ink-muted);">
Paste a repository URL to anonymize a repo, a pull-request URL for a PR, or a gist URL for a gist.
</small>
</div>
</div>
<!-- ===== STATE 2: URL provided — form (left) + preview (right) ===== -->
<div class="anonymize-workspace" ng-show="sourceUrl">
<header class="anonymize-topbar">
<div class="anonymize-topbar-inner">
<div class="paper-crumbs">
<a href="/dashboard">My work</a> &nbsp;/&nbsp;
<span class="here">{{ isUpdate ? 'Edit anonymization' : 'New anonymization' }}</span>
</div>
<div class="anonymize-topbar-head">
<h1 class="paper-page-title anonymize-topbar-title">
<span ng-if="!isUpdate">New <em>anonymization</em></span>
<span ng-if="isUpdate">Edit <em>anonymization</em></span>
</h1>
<span class="type-badge" ng-show="detectedType" ng-class="{'type-repo': detectedType === 'repo', 'type-pr': detectedType === 'pr', 'type-gist': detectedType === 'gist'}">
{{detectedType === 'repo' ? 'Repo' : detectedType === 'pr' ? 'PR' : 'Gist'}}
</span>
</div>
</div>
</header>
<div class="anonymize-split">
<!-- Form column (left) -->
<div class="anonymize-form-col overflow-auto">
<form class="form needs-validation paper-settings-main" name="anonymize" novalidate>
<section class="paper-settings-section">
<div class="paper-section-eyebrow">Source</div>
<div class="form-group">
<label class="paper-field-label" for="sourceUrl">GitHub URL</label>
<input
type="text"
class="form-control"
name="sourceUrl"
id="sourceUrl"
ng-class="{'is-invalid': anonymize.sourceUrl.$invalid}"
ng-model="sourceUrl"
placeholder="Paste a GitHub repo or pull request URL"
ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }"
ng-change="urlSelected()"
/>
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.github">
Please provide a valid GitHub URL.
</div>
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.access">
Not accessible. The organization may restrict access.
</div>
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.missing">
Does not exist or is not accessible.
</div>
<div class="invalid-feedback" ng-show="anonymize.sourceUrl.$error.used">
Already anonymized.
</div>
</div>
<div ng-show="detectedType === 'repo'" class="form-grid-2">
<div class="form-group">
<label class="paper-field-label" for="branch">Branch</label>
<div class="input-group">
<select class="form-control" id="branch" name="branch" ng-model="source.branch">
<option ng-repeat="b in branches" ng-bind="b.name" value="{{b.name}}"></option>
</select>
<div class="input-group-append">
<button class="btn" type="button" ng-click="getBranches(true)" title="Refresh" data-toggle="tooltip" data-placement="bottom">
<i class="fa fa-undo"></i>
</button>
</div>
</div>
</div>
<div class="form-group">
<label class="paper-field-label" for="commit">Commit</label>
<input class="form-control" id="commit" name="commit" pattern="[a-fA-Z0-9]{6,}" ng-model="source.commit" required ng-class="{'is-invalid': anonymize.commit.$invalid}" />
<div class="invalid-feedback" ng-show="anonymize.commit.$error.pattern || anonymize.commit.$error.required">
The commit SHA is not valid.
</div>
<div class="invalid-feedback" ng-show="anonymize.commit.$error.exists">
This commit no longer exists in the repository. Click refresh to get the latest.
</div>
</div>
</div>
<div class="form-check" ng-show="detectedType">
<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 class="form-text text-muted">Automatically update with the latest changes (hourly max).</small>
</div>
</section>
<section class="paper-settings-section" ng-show="detectedType">
<div class="paper-section-eyebrow">Identity</div>
<div class="form-group" ng-show="detectedType === 'repo'">
<label class="paper-field-label" for="repoId">Anonymized repository ID</label>
<input type="text" class="form-control" name="repoId" id="repoId" ng-class="{'is-invalid': anonymize.repoId.$invalid}" ng-model="repoId" ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }" />
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/r/{{repoId}}</code>.</small>
<div class="invalid-feedback" ng-show="anonymize.repoId.$error.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" ng-show="anonymize.repoId.$error.used">{{repoId}} is already used.</div>
</div>
<div class="form-group" ng-show="detectedType === 'pr'">
<label class="paper-field-label" for="pullRequestId">Anonymized 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 class="form-text text-muted">Your share link will be <code>anonymous.4open.science/pr/{{pullRequestId}}</code>.</small>
<div class="invalid-feedback" ng-show="anonymize.pullRequestId.$error.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" ng-show="anonymize.pullRequestId.$error.used">{{pullRequestId}} is already used.</div>
</div>
<div class="form-group" ng-show="detectedType === 'gist'">
<label class="paper-field-label" for="gistId">Anonymized gist ID</label>
<input type="text" class="form-control" name="gistId" id="gistId" ng-class="{'is-invalid': anonymize.gistId.$invalid}" ng-model="gistId" ng-model-options="{ debounce: {default: 1000, blur: 0, click: 0}, updateOn: 'default blur click' }" />
<small class="form-text text-muted">Your share link will be <code>anonymous.4open.science/gist/{{gistId}}</code>.</small>
<div class="invalid-feedback" ng-show="anonymize.gistId.$error.format">ID can only contain letters and numbers.</div>
<div class="invalid-feedback" ng-show="anonymize.gistId.$error.used">{{gistId}} is already used.</div>
</div>
<div class="form-group">
<label class="paper-field-label" for="conference">Conference <span class="paper-optional">(optional)</span></label>
<input class="form-control" id="conference" name="conference" ng-model="conference" ng-model-options="{ debounce: { default: 800, blur: 0 }, updateOn: 'default blur' }" ng-class="{'is-invalid': anonymize.conference.$invalid}" />
<small class="form-text text-muted" ng-show="conference_data">
<a ng-href="{{conference_data.url}}" target="_blank">{{conference_data.name}}</a> expires {{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">Link to a conference to apply its shared defaults.</small>
</div>
</section>
<section class="paper-settings-section" ng-show="detectedType">
<div class="paper-section-eyebrow">Anonymization</div>
<div class="form-group">
<label class="paper-field-label" for="terms">Terms to redact</label>
<textarea class="form-control" id="terms" name="terms" rows="4" ng-model="terms" ng-model-options="{ debounce: 250 }" ng-class="{'is-invalid': anonymize.terms.$invalid}"></textarea>
<small class="form-text text-muted">One term per line (regex allowed). Replaced by <code>{{site_options.ANONYMIZATION_MASK}}-[N]</code>, or use <code>term=&gt;replacement</code> to pick your own (e.g. <code>Anonymous=&gt;ABC</code>).</small>
<div class="warning-feedback" ng-show="termsRegexWarning">Regex characters detected. Escape them if unintentional.</div>
<div class="invalid-feedback" ng-show="anonymize.terms.$error.format">Terms are in an invalid format.</div>
</div>
</section>
<section class="paper-settings-section" ng-show="detectedType">
<div class="paper-section-eyebrow">Display</div>
<div ng-show="detectedType === 'repo'">
<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>
</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>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="pdf" name="pdf" ng-model="options.pdf" />
<label class="form-check-label" for="pdf">Display PDFs</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="notebook" name="notebook" ng-model="options.notebook" />
<label class="form-check-label" for="notebook">Display Notebooks</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="page" name="page" ng-model="options.page" ng-disabled="!details.hasPage || (details.pageSource && details.pageSource.branch !== source.branch)" />
<label class="form-check-label" for="page">GitHub Pages</label>
<small class="form-text text-muted d-block" ng-show="!details.hasPage">
{{ 'WARNINGS.page_not_enabled_on_repo' | translate }}
</small>
<small class="form-text text-muted d-block" ng-show="details.hasPage && details.pageSource && details.pageSource.branch !== source.branch">
{{ 'WARNINGS.page_branch_mismatch' | translate:{ pageBranch: details.pageSource.branch, selectedBranch: source.branch } }}
</small>
</div>
</div>
<div ng-show="detectedType === 'gist'">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="title-gist" name="title-gist" ng-model="options.title" />
<label class="form-check-label" for="title-gist">Gist description</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="content-gist" name="content-gist" ng-model="options.content" />
<label class="form-check-label" for="content-gist">File contents</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="comments-gist" name="comments-gist" ng-model="options.comments" />
<label class="form-check-label" for="comments-gist">Comments</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="username-gist" name="username-gist" ng-model="options.username" />
<label class="form-check-label" for="username-gist">Usernames</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="date-gist" name="date-gist" ng-model="options.date" />
<label class="form-check-label" for="date-gist">Dates</label>
</div>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="origin-gist" name="origin-gist" ng-model="options.origin" />
<label class="form-check-label" for="origin-gist">Source gist ID</label>
</div>
</div>
<div ng-show="detectedType === 'pr'">
<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">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">PR body</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">Diff</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">Comments</label>
</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">Usernames</label>
</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">Dates</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">Project name</label>
</div>
</div>
</section>
<section class="paper-settings-section" ng-show="detectedType">
<div class="paper-section-eyebrow">Expiration</div>
<div class="form-grid-2">
<div class="form-group">
<label class="paper-field-label" for="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>
</div>
<div class="form-group" ng-show="options.expirationMode!='never'">
<label class="paper-field-label" for="expirationDate">Expiration date</label>
<input class="form-control" type="date" name="expirationDate" id="expirationDate" ng-model="options.expirationDate" />
</div>
</div>
<small class="form-text text-muted" ng-show="options.expirationMode=='remove'">After {{options.expirationDate | date}}, the content will be removed.</small>
<small class="form-text text-muted" ng-show="options.expirationMode=='redirect'">After {{options.expirationDate | date}}, visitors will be redirected to GitHub.</small>
</section>
<section class="paper-settings-section" ng-show="isUpdate && detectedType === 'repo'">
<div class="paper-section-eyebrow">Co-authors</div>
<p class="form-text text-muted" style="margin-bottom: 8px;">
Co-authors can view and edit these settings. They cannot delete the anonymization or manage co-authors.
</p>
<div class="form-group" ng-show="role === 'owner' || role === 'admin'">
<label class="paper-field-label" for="coauthorSearch">Add a GitHub user</label>
<div style="position: relative;">
<input
type="text"
id="coauthorSearch"
class="form-control"
placeholder="Search GitHub username…"
ng-model="coauthorSearch"
ng-change="searchCoauthors()"
ng-model-options="{ debounce: 300 }"
autocomplete="off"
/>
<div class="dropdown-menu show" style="display: block; max-height: 220px; overflow-y: auto; width: 100%;" ng-show="coauthorResults.length > 0">
<a href="#" class="dropdown-item d-flex align-items-center" ng-repeat="u in coauthorResults" ng-click="addCoauthor(u, $event)">
<img ng-src="{{u.photo}}" alt="" style="width: 22px; height: 22px; border-radius: 50%; margin-right: 8px;" />
<span ng-bind="u.username"></span>
</a>
</div>
</div>
<small class="form-text text-muted" ng-show="coauthorError" ng-bind="coauthorError"></small>
</div>
<div class="coauthor-list">
<div class="coauthor-row d-flex align-items-center" ng-repeat="c in coauthors" style="padding: 6px 0; gap: 8px;">
<img ng-src="{{c.photo}}" alt="" style="width: 24px; height: 24px; border-radius: 50%;" ng-if="c.photo" />
<a ng-href="https://github.com/{{c.username}}" target="_blank" ng-bind="c.username"></a>
<span class="type-badge type-coauthor">Co-author</span>
<button type="button" class="btn btn-sm" ng-click="removeCoauthor(c)" ng-show="role === 'owner' || role === 'admin'" style="margin-left: auto;" title="Remove co-author">
<i class="fas fa-times"></i>
</button>
</div>
<div class="form-text text-muted" ng-show="!coauthors || coauthors.length === 0">
No co-authors yet.
</div>
</div>
</section>
<div class="alert alert-danger" role="alert" ng-if="error" ng-bind="error"></div>
<div class="anonymize-submit-bar" ng-show="detectedType">
<button type="submit" class="btn btn-ink" ng-click="anonymizeRepo($event)" ng-if="detectedType === 'repo' && !isUpdate">
<i class="fas fa-user-secret mr-1"></i> Anonymize Repository
</button>
<button type="submit" class="btn btn-ink" ng-click="anonymizeRepo($event)" ng-if="detectedType === 'repo' && isUpdate">
<i class="fas fa-save mr-1"></i> Update Repository
</button>
<button type="submit" class="btn btn-ink" ng-click="anonymizePullRequest($event)" ng-if="detectedType === 'pr' && !isUpdate">
<i class="fas fa-user-secret mr-1"></i> Anonymize Pull Request
</button>
<button type="submit" class="btn btn-ink" ng-click="anonymizePullRequest($event)" ng-if="detectedType === 'pr' && isUpdate">
<i class="fas fa-save mr-1"></i> Update Pull Request
</button>
<button type="submit" class="btn btn-ink" ng-click="anonymizeGist($event)" ng-if="detectedType === 'gist' && !isUpdate">
<i class="fas fa-user-secret mr-1"></i> Anonymize Gist
</button>
<button type="submit" class="btn btn-ink" ng-click="anonymizeGist($event)" ng-if="detectedType === 'gist' && isUpdate">
<i class="fas fa-save mr-1"></i> Update Gist
</button>
</div>
</form>
</div>
<!-- Preview column (right) -->
<div
class="anonymize-preview-col"
ng-if="detectedType === 'repo' && html_readme"
>
<div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">README with redactions applied</span>
</div>
<div class="anonymize-preview-body markdown-body body" ng-bind-html="html_readme"></div>
</div>
<div class="anonymize-preview-col" ng-if="detectedType === 'gist' && details">
<div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">Gist with redactions applied</span>
</div>
<div class="anonymize-preview-body">
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
<h2 class="pr-title mb-1">
<span ng-if="options.title">{{anonymizeGistContent(details.gist.description) || 'Untitled gist'}}</span>
<span class="badge" ng-class="{'badge-success': details.gist.isPublic, 'badge-secondary': !details.gist.isPublic}">
{{details.gist.isPublic ? 'public' : 'secret'}}
</span>
</h2>
<small ng-bind="details.gist.updatedDate | date" ng-if="options.date"></small>
</div>
<small ng-if="options.origin">Gist ID: {{details.source.gistId}}</small>
<small ng-if="options.username && details.gist.ownerLogin">By @{{anonymizeGistContent(details.gist.ownerLogin)}}</small>
<div ng-if="options.content && previewGistFiles.length">
<ul class="pr-comments mt-3">
<li class="pr-comment" ng-repeat="file in previewGistFiles">
<div class="pr-comment-head">
<strong ng-bind="file.filename"></strong>
<span class="pr-comment-date" ng-if="file.language">{{file.language}}</span>
</div>
<gist-file file="file" terms="terms" options="options"></gist-file>
</li>
</ul>
</div>
<div ng-if="options.comments && details.gist.comments && details.gist.comments.length">
<h3 class="paper-section-eyebrow mt-3">Comments</h3>
<ul class="pr-comments">
<li class="pr-comment" ng-repeat="comment in details.gist.comments">
<div class="pr-comment-head">
<span class="pr-comment-author" ng-if="options.username">
<i class="far fa-user"></i> @<span ng-bind="anonymizeGistContent(comment.author)"></span>
</span>
<span class="pr-comment-date" ng-if="options.date" ng-bind="comment.updatedDate | date"></span>
</div>
<div class="pr-comment-body" ng-if="options.body">
<markdown content="anonymizeGistContent(comment.body)" options="options" terms="terms"></markdown>
</div>
</li>
</ul>
</div>
</div>
</div>
<div class="anonymize-preview-col" ng-if="detectedType === 'pr' && details"
ng-init="prTabState = { active: options.diff ? 'diff' : 'comments' }">
<div class="anonymize-preview-head">
<span class="paper-eyebrow">Live preview</span>
<span class="anonymize-preview-sub">Pull request with redactions applied</span>
</div>
<div class="anonymize-preview-body">
<div class="d-flex w-100 justify-content-between align-items-center flex-wrap">
<h2 class="pr-title mb-1">
<span ng-if="options.title">{{anonymizePrContent(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-4 rounded" style="background: var(--paper-bg-alt)" ng-if="options.body">
<markdown content="anonymizePrContent(details.pullRequest.body)" options="options" terms="terms"></markdown>
</div>
<nav class="paper-tabs" ng-if="options.diff || options.comments" role="tablist">
<button
class="paper-tab"
ng-if="options.diff"
ng-class="{'active': prTabState.active == 'diff'}"
ng-click="prTabState.active = 'diff'"
type="button"
role="tab"
>
<i class="fas fa-code"></i> Diff
</button>
<button
class="paper-tab"
ng-if="options.comments"
ng-class="{'active': prTabState.active == 'comments'}"
ng-click="prTabState.active = 'comments'"
type="button"
role="tab"
>
<i class="far fa-comment-dots"></i>
<ng-pluralize count="details.pullRequest.comments.length" when="{'0': 'No comments', 'one': '1 comment', 'other': '{} comments'}"></ng-pluralize>
</button>
</nav>
<div class="paper-tab-content">
<div ng-if="options.diff && prTabState.active == 'diff'">
<div class="pr-diff" ng-bind-html="anonymizePrContent(details.pullRequest.diff) | diff"></div>
</div>
<div ng-if="options.comments && prTabState.active == 'comments'">
<ul class="pr-comments">
<li class="pr-comment" ng-repeat="comment in details.pullRequest.comments">
<div class="pr-comment-head">
<span class="pr-comment-author" ng-if="options.username">
<i class="far fa-user"></i> @<span ng-bind="anonymizePrContent(comment.author)"></span>
</span>
<span class="pr-comment-date" ng-if="options.date" ng-bind="comment.updatedDate | date"></span>
</div>
<div class="pr-comment-body" ng-if="options.body">
<markdown content="anonymizePrContent(comment.body)" options="options" terms="terms"></markdown>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>