mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	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  ### Selecting a commit range  ### Show changes of a single commit only  ### Show changes of a commit range  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>
This commit is contained in:
		
							
								
								
									
										299
									
								
								web_src/js/components/DiffCommitSelector.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										299
									
								
								web_src/js/components/DiffCommitSelector.vue
									
									
									
									
									
										Normal file
									
								
							@@ -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>
 | 
			
		||||
		Reference in New Issue
	
	Block a user