aboutsummaryrefslogtreecommitdiffstats
path: root/web_src/js
diff options
context:
space:
mode:
authorsebastian-sauer <sauer.sebastian@gmail.com>2023-07-28 21:18:12 +0200
committerGitHub <noreply@github.com>2023-07-28 21:18:12 +0200
commit55532061c83d38d33ef48bdc5eeac0f652844e8a (patch)
treed6d21c0afb8b6df197e6eb618274dd16948f3753 /web_src/js
parent4971a1054317ae68b1eb59a3dc30f61c7503dadc (diff)
downloadgitea-55532061c83d38d33ef48bdc5eeac0f652844e8a.tar.gz
gitea-55532061c83d38d33ef48bdc5eeac0f652844e8a.zip
Add commits dropdown in PR files view and allow commit by commit review (#25528)
This PR adds a new dropdown to select a commit or a commit range (shift-click like github) of a Pull Request. After selection of a commit only the changes of this commit will be shown. When selecting a range of commits the diff of this range is shown. This allows to review a PR commit by commit or by viewing only commit ranges. The "Show changes since your last review" mechanism github uses is implemented, too. When reviewing a single commit or a commit range the "Viewed" functionality is disabled. ## Screenshots ### The commit dropdown ![image](https://github.com/go-gitea/gitea/assets/51889757/0db3ae62-1272-436c-be64-4730c5d611e3) ### Selecting a commit range ![image](https://github.com/go-gitea/gitea/assets/51889757/ad81eedb-8437-42b0-8073-2d940c25fe8f) ### Show changes of a single commit only ![image](https://github.com/go-gitea/gitea/assets/51889757/6b1a113b-73ef-4ecc-adf6-bc2340bb8f97) ### Show changes of a commit range ![image](https://github.com/go-gitea/gitea/assets/51889757/6401b358-cd66-4c09-8baa-6cf6177f23a7) Fixes https://github.com/go-gitea/gitea/issues/20989 Fixes https://github.com/go-gitea/gitea/issues/19263 --------- Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: KN4CK3R <admin@oldschoolhack.me> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: delvh <dev.lh@web.de>
Diffstat (limited to 'web_src/js')
-rw-r--r--web_src/js/components/DiffCommitSelector.vue299
-rw-r--r--web_src/js/features/repo-diff-commitselect.js10
-rw-r--r--web_src/js/features/repo-diff.js2
-rw-r--r--web_src/js/svg.js2
4 files changed, 313 insertions, 0 deletions
diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue
new file mode 100644
index 0000000000..a0fc4b2a91
--- /dev/null
+++ b/web_src/js/components/DiffCommitSelector.vue
@@ -0,0 +1,299 @@
+<template>
+ <div class="ui scrolling dropdown custom">
+ <button
+ class="ui basic button"
+ id="diff-commit-list-expand"
+ @click.stop="toggleMenu()"
+ :data-tooltip-content="locale.filter_changes_by_commit"
+ aria-haspopup="true"
+ tabindex="0"
+ aria-controls="diff-commit-selector-menu"
+ :aria-label="locale.filter_changes_by_commit"
+ aria-activedescendant="diff-commit-list-show-all"
+ >
+ <svg-icon name="octicon-git-commit"/>
+ </button>
+ <div class="menu left transition" id="diff-commit-selector-menu" :class="{visible: menuVisible}" v-show="menuVisible" v-cloak :aria-expanded="menuVisible ? 'true': 'false'">
+ <div class="loading-indicator is-loading" v-if="isLoading"/>
+ <div v-if="!isLoading" class="vertical item gt-df gt-fc gt-gap-2" id="diff-commit-list-show-all" role="menuitem" tabindex="-1" @keydown.enter="showAllChanges()" @click="showAllChanges()">
+ <div class="gt-ellipsis">
+ {{ locale.show_all_commits }}
+ </div>
+ <div class="gt-ellipsis text light-2 gt-mb-0">
+ {{ locale.stats_num_commits }}
+ </div>
+ </div>
+ <!-- only show the show changes since last review if there is a review AND we are commits ahead of the last review -->
+ <div
+ v-if="lastReviewCommitSha != null" role="menuitem" tabindex="-1"
+ class="vertical item gt-df gt-fc gt-gap-2 gt-border-secondary-top"
+ :class="{disabled: commitsSinceLastReview === 0}"
+ @keydown.enter="changesSinceLastReviewClick()"
+ @click="changesSinceLastReviewClick()"
+ >
+ <div class="gt-ellipsis">
+ {{ locale.show_changes_since_your_last_review }}
+ </div>
+ <div class="gt-ellipsis text light-2">
+ {{ commitsSinceLastReview }} commits
+ </div>
+ </div>
+ <span v-if="!isLoading" class="info gt-border-secondary-top text light-2">{{ locale.select_commit_hold_shift_for_range }}</span>
+ <template v-for="commit in commits" :key="commit.id">
+ <div
+ class="vertical item gt-df gt-gap-2 gt-border-secondary-top" role="menuitem" tabindex="-1"
+ :class="{selection: commit.selected, hovered: commit.hovered}"
+ @keydown.enter.exact="commitClicked(commit.id)"
+ @keydown.enter.shift.exact="commitClickedShift(commit)"
+ @mouseover.shift="highlight(commit)"
+ @click.exact="commitClicked(commit.id)"
+ @click.ctrl.exact="commitClicked(commit.id, true)"
+ @click.meta.exact="commitClicked(commit.id, true)"
+ @click.shift.exact.stop.prevent="commitClickedShift(commit)"
+ >
+ <div class="gt-f1 gt-df gt-fc gt-gap-2">
+ <div class="gt-ellipsis commit-list-summary">
+ {{ commit.summary }}
+ </div>
+ <div class="gt-ellipsis text light-2">
+ {{ commit.committer_or_author_name }}
+ <span class="text right">
+ <relative-time class="time-since" prefix="" :datetime="commit.time" data-tooltip-content data-tooltip-interactive="true">{{ commit.time }}</relative-time>
+ </span>
+ </div>
+ </div>
+ <div class="gt-mono">
+ {{ commit.short_sha }}
+ </div>
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
+
+<script>
+import {SvgIcon} from '../svg.js';
+
+export default {
+ components: {SvgIcon},
+ data: () => {
+ return {
+ menuVisible: false,
+ isLoading: false,
+ locale: {},
+ commits: [],
+ hoverActivated: false,
+ lastReviewCommitSha: null
+ };
+ },
+ computed: {
+ commitsSinceLastReview() {
+ if (this.lastReviewCommitSha) {
+ return this.commits.length - this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) - 1;
+ }
+ return 0;
+ },
+ queryParams() {
+ return this.$el.parentNode.getAttribute('data-queryparams');
+ },
+ issueLink() {
+ return this.$el.parentNode.getAttribute('data-issuelink');
+ }
+ },
+ mounted() {
+ document.body.addEventListener('click', this.onBodyClick);
+ this.$el.addEventListener('keydown', this.onKeyDown);
+ this.$el.addEventListener('keyup', this.onKeyUp);
+ },
+ unmounted() {
+ document.body.removeEventListener('click', this.onBodyClick);
+ this.$el.removeEventListener('keydown', this.onKeyDown);
+ this.$el.removeEventListener('keyup', this.onKeyUp);
+ },
+ methods: {
+ onBodyClick(event) {
+ // close this menu on click outside of this element when the dropdown is currently visible opened
+ if (this.$el.contains(event.target)) return;
+ if (this.menuVisible) {
+ this.toggleMenu();
+ }
+ },
+ onKeyDown(event) {
+ if (!this.menuVisible) return;
+ const item = document.activeElement;
+ if (!this.$el.contains(item)) return;
+ switch (event.key) {
+ case 'ArrowDown': // select next element
+ event.preventDefault();
+ this.focusElem(item.nextElementSibling, item);
+ break;
+ case 'ArrowUp': // select previous element
+ event.preventDefault();
+ this.focusElem(item.previousElementSibling, item);
+ break;
+ case 'Escape': // close menu
+ event.preventDefault();
+ item.tabIndex = -1;
+ this.toggleMenu();
+ break;
+ }
+ },
+ onKeyUp(event) {
+ if (!this.menuVisible) return;
+ const item = document.activeElement;
+ if (!this.$el.contains(item)) return;
+ if (event.key === 'Shift' && this.hoverActivated) {
+ // shift is not pressed anymore -> deactivate hovering and reset hovered and selected
+ this.hoverActivated = false;
+ for (const commit of this.commits) {
+ commit.hovered = false;
+ commit.selected = false;
+ }
+ }
+ },
+ highlight(commit) {
+ if (!this.hoverActivated) return;
+ const indexSelected = this.commits.findIndex((x) => x.selected);
+ const indexCurrentElem = this.commits.findIndex((x) => x.id === commit.id);
+ for (const [idx, commit] of this.commits.entries()) {
+ commit.hovered = Math.min(indexSelected, indexCurrentElem) <= idx && idx <= Math.max(indexSelected, indexCurrentElem);
+ }
+ },
+ /** Focus given element */
+ focusElem(elem, prevElem) {
+ if (elem) {
+ elem.tabIndex = 0;
+ prevElem.tabIndex = -1;
+ elem.focus();
+ }
+ },
+ /** Opens our menu, loads commits before opening */
+ async toggleMenu() {
+ this.menuVisible = !this.menuVisible;
+ // load our commits when the menu is not yet visible (it'll be toggled after loading)
+ // and we got no commits
+ if (this.commits.length === 0 && this.menuVisible && !this.isLoading) {
+ this.isLoading = true;
+ try {
+ await this.fetchCommits();
+ } finally {
+ this.isLoading = false;
+ }
+ }
+ // set correct tabindex to allow easier navigation
+ this.$nextTick(() => {
+ const expandBtn = this.$el.querySelector('#diff-commit-list-expand');
+ const showAllChanges = this.$el.querySelector('#diff-commit-list-show-all');
+ if (this.menuVisible) {
+ this.focusElem(showAllChanges, expandBtn);
+ } else {
+ this.focusElem(expandBtn, showAllChanges);
+ }
+ });
+ },
+ /** Load the commits to show in this dropdown */
+ async fetchCommits() {
+ const resp = await fetch(`${this.issueLink}/commits/list`);
+ const results = await resp.json();
+ this.commits.push(...results.commits.map((x) => {
+ x.hovered = false;
+ return x;
+ }));
+ this.commits.reverse();
+ this.lastReviewCommitSha = results.last_review_commit_sha || null;
+ if (this.lastReviewCommitSha && this.commits.findIndex((x) => x.id === this.lastReviewCommitSha) === -1) {
+ // the lastReviewCommit is not available (probably due to a force push)
+ // reset the last review commit sha
+ this.lastReviewCommitSha = null;
+ }
+ Object.assign(this.locale, results.locale);
+ },
+ showAllChanges() {
+ window.location = `${this.issueLink}/files${this.queryParams}`;
+ },
+ /** Called when user clicks on since last review */
+ changesSinceLastReviewClick() {
+ window.location = `${this.issueLink}/files/${this.lastReviewCommitSha}..${this.commits.at(-1).id}${this.queryParams}`;
+ },
+ /** Clicking on a single commit opens this specific commit */
+ commitClicked(commitId, newWindow = false) {
+ const url = `${this.issueLink}/commits/${commitId}${this.queryParams}`;
+ if (newWindow) {
+ window.open(url);
+ } else {
+ window.location = url;
+ }
+ },
+ /**
+ * When a commit is clicked with shift this enables the range
+ * selection. Second click (with shift) defines the end of the
+ * range. This opens the diff of this range
+ * Exception: first commit is the first commit of this PR. Then
+ * the diff from beginning of PR up to the second clicked commit is
+ * opened
+ */
+ commitClickedShift(commit) {
+ this.hoverActivated = !this.hoverActivated;
+ commit.selected = true;
+ // Second click -> determine our range and open links accordingly
+ if (!this.hoverActivated) {
+ // find all selected commits and generate a link
+ if (this.commits[0].selected) {
+ // first commit is selected - generate a short url with only target sha
+ const lastCommitIdx = this.commits.findLastIndex((x) => x.selected);
+ if (lastCommitIdx === this.commits.length - 1) {
+ // user selected all commits - just show the normal diff page
+ window.location = `${this.issueLink}/files${this.queryParams}`;
+ } else {
+ window.location = `${this.issueLink}/files/${this.commits[lastCommitIdx].id}${this.queryParams}`;
+ }
+ } else {
+ const start = this.commits[this.commits.findIndex((x) => x.selected) - 1].id;
+ const end = this.commits.findLast((x) => x.selected).id;
+ window.location = `${this.issueLink}/files/${start}..${end}${this.queryParams}`;
+ }
+ }
+ },
+ }
+};
+</script>
+<style scoped>
+ .hovered:not(.selection) {
+ background-color: var(--color-small-accent) !important;
+ }
+ .selection {
+ background-color: var(--color-accent) !important;
+ }
+
+ .info {
+ display: inline-block;
+ padding: 7px 14px !important;
+ line-height: 1.4;
+ width: 100%;
+ }
+
+ #diff-commit-selector-menu {
+ overflow-x: hidden;
+ max-height: 450px;
+ }
+
+ #diff-commit-selector-menu .loading-indicator {
+ height: 200px;
+ width: 350px;
+ }
+
+ #diff-commit-selector-menu .item {
+ flex-direction: row;
+ line-height: 1.4;
+ padding: 7px 14px !important;
+ }
+
+ #diff-commit-selector-menu .item:focus {
+ color: var(--color-text);
+ background: var(--color-hover);
+ }
+
+ #diff-commit-selector-menu .commit-list-summary {
+ max-width: min(380px, 96vw);
+ }
+</style>
diff --git a/web_src/js/features/repo-diff-commitselect.js b/web_src/js/features/repo-diff-commitselect.js
new file mode 100644
index 0000000000..ebac64e855
--- /dev/null
+++ b/web_src/js/features/repo-diff-commitselect.js
@@ -0,0 +1,10 @@
+import {createApp} from 'vue';
+import DiffCommitSelector from '../components/DiffCommitSelector.vue';
+
+export function initDiffCommitSelect() {
+ const el = document.getElementById('diff-commit-select');
+ if (!el) return;
+
+ const commitSelect = createApp(DiffCommitSelector);
+ commitSelect.mount(el);
+}
diff --git a/web_src/js/features/repo-diff.js b/web_src/js/features/repo-diff.js
index f4bb724fe5..b79ca0f5b1 100644
--- a/web_src/js/features/repo-diff.js
+++ b/web_src/js/features/repo-diff.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import {initCompReactionSelector} from './comp/ReactionSelector.js';
import {initRepoIssueContentHistory} from './repo-issue-content.js';
import {initDiffFileTree} from './repo-diff-filetree.js';
+import {initDiffCommitSelect} from './repo-diff-commitselect.js';
import {validateTextareaNonEmpty} from './comp/ComboMarkdownEditor.js';
import {initViewedCheckboxListenerFor, countAndUpdateViewedFiles, initExpandAndCollapseFilesButton} from './pull-view-file.js';
import {initImageDiff} from './imagediff.js';
@@ -188,6 +189,7 @@ export function initRepoDiffView() {
const diffFileList = $('#diff-file-list');
if (diffFileList.length === 0) return;
initDiffFileTree();
+ initDiffCommitSelect();
initRepoDiffShowMore();
initRepoDiffReviewButton();
initRepoDiffFileViewToggle();
diff --git a/web_src/js/svg.js b/web_src/js/svg.js
index 2ef839aa21..46372e7d62 100644
--- a/web_src/js/svg.js
+++ b/web_src/js/svg.js
@@ -29,6 +29,7 @@ import octiconFileDirectoryFill from '../../public/assets/img/svg/octicon-file-d
import octiconFilter from '../../public/assets/img/svg/octicon-filter.svg';
import octiconGear from '../../public/assets/img/svg/octicon-gear.svg';
import octiconGitBranch from '../../public/assets/img/svg/octicon-git-branch.svg';
+import octiconGitCommit from '../../public/assets/img/svg/octicon-git-commit.svg';
import octiconGitMerge from '../../public/assets/img/svg/octicon-git-merge.svg';
import octiconGitPullRequest from '../../public/assets/img/svg/octicon-git-pull-request.svg';
import octiconHeading from '../../public/assets/img/svg/octicon-heading.svg';
@@ -99,6 +100,7 @@ const svgs = {
'octicon-filter': octiconFilter,
'octicon-gear': octiconGear,
'octicon-git-branch': octiconGitBranch,
+ 'octicon-git-commit': octiconGitCommit,
'octicon-git-merge': octiconGitMerge,
'octicon-git-pull-request': octiconGitPullRequest,
'octicon-heading': octiconHeading,