Commit 4dddaef8 authored by Douwe Maan's avatar Douwe Maan

Automatically link commit ranges to compare page.

parent 0625d68f
......@@ -27,6 +27,7 @@ v 7.9.0 (unreleased)
- Add Bitbucket omniauth provider.
- Add Bitbucket importer.
- Support referencing issues to a project whose name starts with a digit
- Automatically link commit ranges to compare page: sha1...sha4 or sha1..sha4 (includes sha1 in comparison)
v 7.8.2
- Fix service migration issue when upgrading from versions prior to 7.3
......
......@@ -14,6 +14,7 @@ module Gitlab
# * !123 for merge requests
# * $123 for snippets
# * 123456 for commits
# * 123456...7890123 for commit ranges (comparisons)
#
# It also parses Emoji codes to insert images. See
# http://www.emoji-cheat-sheet.com/ for a list of the supported icons.
......@@ -133,13 +134,14 @@ def parse(text, project = @project)
|#{PROJ_STR}?\#(?<issue>([a-zA-Z\-]+-)?\d+) # Issue ID
|#{PROJ_STR}?!(?<merge_request>\d+) # MR ID
|\$(?<snippet>\d+) # Snippet ID
|(#{PROJ_STR}@)?(?<commit_range>[\h]{6,40}\.{2,3}[\h]{6,40}) # Commit range
|(#{PROJ_STR}@)?(?<commit>[\h]{6,40}) # Commit ID
|(?<skip>gfm-extraction-[\h]{6,40}) # Skip gfm extractions. Otherwise will be parsed as commit
)
(?<suffix>\W)? # Suffix
}x.freeze
TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit].freeze
TYPES = [:user, :issue, :label, :merge_request, :snippet, :commit, :commit_range].freeze
def parse_references(text, project = @project)
# parse reference links
......@@ -290,6 +292,30 @@ def reference_commit(identifier, project = @project, prefix_text = nil)
end
end
def reference_commit_range(identifier, project = @project, prefix_text = nil)
from_id, to_id = identifier.split(/\.{2,3}/, 2)
inclusive = identifier !~ /\.{3}/
from_id << "^" if inclusive
if project.valid_repo? &&
from = project.repository.commit(from_id) &&
to = project.repository.commit(to_id)
options = html_options.merge(
title: "Commits #{from_id} through #{to_id}",
class: "gfm gfm-commit_range #{html_options[:class]}"
)
prefix_text = "#{prefix_text}@" if prefix_text
link_to(
"#{prefix_text}#{identifier}",
namespace_project_compare_url(project.namespace, project, from: from_id, to: to_id),
options
)
end
end
def reference_external_issue(identifier, project = @project,
prefix_text = nil)
url = url_for_issue(identifier, project)
......
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor
attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits
attr_accessor :users, :labels, :issues, :merge_requests, :snippets, :commits, :commit_ranges
include Markdown
def initialize
@users, @labels, @issues, @merge_requests, @snippets, @commits =
[], [], [], [], [], []
@users, @labels, @issues, @merge_requests, @snippets, @commits, @commit_ranges =
[], [], [], [], [], [], []
end
def analyze(string, project)
......@@ -60,6 +60,16 @@ def commits_for(project = nil)
end.reject(&:nil?)
end
def commit_ranges_for(project = nil)
commit_ranges.map do |entry|
repo = entry[:project].repository if entry[:project]
if repo && should_lookup?(project, entry[:project])
from_id, to_id = entry[:id].split(/\.{2,3}/, 2)
[repo.commit(from_id), repo.commit(to_id)]
end
end.reject(&:nil?)
end
private
def reference_link(type, identifier, project, _)
......
......@@ -9,6 +9,7 @@
let(:user) { create(:user, username: 'gfm') }
let(:commit) { project.repository.commit }
let(:earlier_commit){ project.repository.commit("HEAD~2") }
let(:issue) { create(:issue, project: project) }
let(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let(:snippet) { create(:project_snippet, project: project) }
......@@ -53,6 +54,53 @@ def url_helper(image_name)
to have_selector('a.gfm.foo')
end
describe "referencing a commit range" do
let(:expected) { namespace_project_compare_path(project.namespace, project, from: earlier_commit.id, to: commit.id) }
it "should link using a full id" do
actual = "What happened in #{earlier_commit.id}...#{commit.id}"
expect(gfm(actual)).to match(expected)
end
it "should link using a short id" do
actual = "What happened in #{earlier_commit.short_id}...#{commit.short_id}"
expected = namespace_project_compare_path(project.namespace, project, from: earlier_commit.short_id, to: commit.short_id)
expect(gfm(actual)).to match(expected)
end
it "should link inclusively" do
actual = "What happened in #{earlier_commit.id}..#{commit.id}"
expected = namespace_project_compare_path(project.namespace, project, from: "#{earlier_commit.id}^", to: commit.id)
expect(gfm(actual)).to match(expected)
end
it "should link with adjacent text" do
actual = "(see #{earlier_commit.id}...#{commit.id})"
expect(gfm(actual)).to match(expected)
end
it "should keep whitespace intact" do
actual = "Changes #{earlier_commit.id}...#{commit.id} dramatically"
expected = /Changes <a.+>#{earlier_commit.id}...#{commit.id}<\/a> dramatically/
expect(gfm(actual)).to match(expected)
end
it "should not link with an invalid id" do
actual = expected = "What happened in #{earlier_commit.id.reverse}...#{commit.id.reverse}"
expect(gfm(actual)).to eq(expected)
end
it "should include a title attribute" do
actual = "What happened in #{earlier_commit.id}...#{commit.id}"
expect(gfm(actual)).to match(/title="Commits #{earlier_commit.id} through #{commit.id}"/)
end
it "should include standard gfm classes" do
actual = "What happened in #{earlier_commit.id}...#{commit.id}"
expect(gfm(actual)).to match(/class="\s?gfm gfm-commit_range\s?"/)
end
end
describe "referencing a commit" do
let(:expected) { namespace_project_commit_path(project.namespace, project, commit) }
......
......@@ -31,6 +31,11 @@
expect(subject.commits).to eq([{ project: nil, id: '98cf0ae3' }])
end
it 'extracts commit ranges' do
subject.analyze('here you go, a commit range: 98cf0ae3...98cf0ae4', nil)
expect(subject.commit_ranges).to eq([{ project: nil, id: '98cf0ae3...98cf0ae4' }])
end
it 'extracts multiple references and preserves their order' do
subject.analyze('@me and @you both care about this', nil)
expect(subject.users).to eq([
......@@ -100,5 +105,19 @@
expect(extracted[0].sha).to eq(commit.sha)
expect(extracted[0].message).to eq(commit.message)
end
it 'accesses valid commit ranges' do
commit = project.repository.commit('master')
earlier_commit = project.repository.commit('master~2')
subject.analyze("this references commits #{earlier_commit.sha[0..6]}...#{commit.sha[0..6]}",
project)
extracted = subject.commit_ranges_for(project)
expect(extracted.size).to eq(1)
expect(extracted[0][0].sha).to eq(earlier_commit.sha)
expect(extracted[0][0].message).to eq(earlier_commit.message)
expect(extracted[0][1].sha).to eq(commit.sha)
expect(extracted[0][1].message).to eq(commit.message)
end
end
end
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment