Commit 5afc10ad authored by Jan Beckmann's avatar Jan Beckmann Committed by Matthias Käppler

Add support for fetching merge requests via RSS / Atom

Closes https://gitlab.com/gitlab-org/gitlab/-/issues/29521

Changelog: added
parent 0b5df713
......@@ -8,7 +8,6 @@ module SessionlessAuthentication
# This filter handles personal access tokens, atom requests with rss tokens, and static object tokens
def authenticate_sessionless_user!(request_format)
user = request_authenticator.find_sessionless_user(request_format)
sessionless_sign_in(user) if user
end
......
......@@ -13,6 +13,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include DiffHelper
include Gitlab::Cache::Helpers
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
skip_before_action :merge_request, only: [:index, :bulk_update, :export_csv]
before_action :apply_diff_view_cookie!, only: [:show]
before_action :disable_query_limiting, only: [:assign_related_issues, :update]
......@@ -85,6 +86,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
respond_to do |format|
format.html
format.atom { render layout: 'xml.atom' }
format.json do
render json: {
html: view_to_html_string("projects/merge_requests/_merge_requests")
......
......@@ -3,42 +3,7 @@
xml.entry do
xml.id project_issue_url(issue.project, issue)
xml.link href: project_issue_url(issue.project, issue)
xml.title truncate(issue.title, length: 80)
xml.updated issue.updated_at.xmlschema
xml.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon_for_user(issue.author))
xml.author do
xml.name issue.author_name
xml.email issue.author_public_email
end
xml.summary issue.title
xml.description issue.description if issue.description
xml.content issue.description if issue.description
xml.milestone issue.milestone.title if issue.milestone
# using the shovel operator (xml <<) would make us lose indentation, so we do this (https://github.com/rails/rails/issues/7036)
render(partial: 'shared/issuable/issuable', object: issue, locals: { builder: xml })
xml.due_date issue.due_date if issue.due_date
unless issue.labels.empty?
xml.labels do
issue.labels.each do |label|
xml.label label.name
end
end
end
if issue.assignees.any?
xml.assignees do
issue.assignees.each do |assignee|
xml.assignee do
xml.name assignee.name
xml.email assignee.public_email
end
end
end
xml.assignee do
xml.name issue.assignees.first.name
xml.email issue.assignees.first.public_email
end
end
end
# frozen_string_literal: true
xml.entry do
xml.id project_merge_request_url(merge_request.project, merge_request)
xml.link href: project_merge_request_url(merge_request.project, merge_request)
# using the shovel operator (xml <<) would make us lose indentation, so we do this (https://github.com/rails/rails/issues/7036)
render(partial: 'shared/issuable/issuable', object: merge_request, locals: { builder: xml })
end
- issuable_type = 'merge-requests'
- notification_email = @current_user.present? ? @current_user.notification_email : nil
= render 'shared/issuable/feed_buttons', show_calendar_button: false
.js-csv-import-export-buttons{ data: { show_export_button: "true", issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_merge_requests_path(@project, request.query_parameters), container_class: 'gl-mr-3' } }
- if @can_bulk_update
......
# frozen_string_literal: true
# rubocop: disable CodeReuse/ActiveRecord
xml.title "#{@project.name} merge requests"
xml.link href: url_for(safe_params), rel: "self", type: "application/atom+xml"
xml.link href: project_merge_requests_url(@project), rel: "alternate", type: "text/html"
xml.id project_merge_requests_url(@project)
xml.updated @merge_requests.first.updated_at.xmlschema if @merge_requests.reorder(nil).any?
xml << render(partial: 'projects/merge_requests/merge_request', collection: @merge_requests) if @merge_requests.reorder(nil).any?
# rubocop: enable CodeReuse/ActiveRecord
......@@ -6,6 +6,9 @@
- page_title _("Merge requests")
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} merge requests")
= render 'projects/last_push'
- if @project.merge_requests.exists?
......
= link_to safe_params.merge(rss_url_options), class: 'btn gl-button btn-default btn-icon has-tooltip', data: { container: 'body', testid: 'rss-feed-link' }, title: _('Subscribe to RSS feed') do
- show_calendar_button = local_assigns.fetch(:show_calendar_button, true)
= link_to safe_params.merge(rss_url_options), class: 'btn gl-button btn-default btn-icon has-tooltip', data: { container: 'body', testid: 'rss-feed-link' }, title: _('Subscribe to RSS feed') , 'aria-label': _('Subscribe to RSS feed') do
= sprite_icon('rss', css_class: 'qa-rss-icon')
= link_to safe_params.merge(calendar_url_options), class: 'btn gl-button btn-default btn-icon has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do
- if show_calendar_button
= link_to safe_params.merge(calendar_url_options), class: 'btn gl-button btn-default btn-icon has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar'), 'aria-label': _('Subscribe to calendar') do
= sprite_icon('calendar')
# frozen_string_literal: true
builder.title truncate(issuable.title, length: 80)
builder.updated issuable.updated_at.xmlschema
builder.media :thumbnail, width: "40", height: "40", url: image_url(avatar_icon_for_user(issuable.author))
builder.author do
builder.name issuable.author_name
builder.email issuable.author_public_email
end
builder.summary issuable.title
builder.description truncate(issuable.description, length: 240) if issuable.description
builder.content issuable.description if issuable.description
builder.milestone issuable.milestone.title if issuable.milestone
unless issuable.labels.empty?
builder.labels do
issuable.labels.each do |label|
builder.label label.name
end
end
end
if issuable.assignees.any?
builder.assignees do
issuable.assignees.each do |assignee|
builder.assignee do
builder.name assignee.name
builder.email assignee.public_email
end
end
end
builder.assignee do
builder.name issuable.assignees.first.name
builder.email issuable.assignees.first.public_email
end
end
......@@ -96,6 +96,14 @@ You can filter issues and merge requests by specific terms included in titles or
![filter issues by specific terms](img/issue_search_by_term.png)
### Retrieving search results as feed
> Feeds for merge requests were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66336) in GitLab 14.3.
You can subscribe to the results of your search query for issues or merge requests within a project as an Atom feed by clicking on the feed symbol **{rss}**.
This will generate a feed URL containing both a feed token and your search query, which can be added to your feed reader.
### Filtering by ID
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/39908) in GitLab 12.1.
......
......@@ -4,61 +4,75 @@ require 'spec_helper'
RSpec.describe 'Issues Feed' do
describe 'GET /issues' do
let!(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let!(:group) { create(:group) }
let!(:project) { create(:project) }
let!(:issue) { create(:issue, author: user, assignees: [assignee], project: project) }
before do
let_it_be_with_reload(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let_it_be(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project) }
let_it_be(:issue) { create(:issue, author: user, assignees: [assignee], project: project, due_date: Date.today) }
let_it_be(:issuable) { issue } # "alias" for shared examples
before_all do
project.add_developer(user)
group.add_developer(user)
end
RSpec.shared_examples 'an authenticated issue atom feed' do
it 'renders atom feed with additional issue information' do
expect(body).to have_selector('title', text: "#{project.name} issues")
expect(body).to have_selector('due_date', text: issue.due_date)
end
end
context 'when authenticated' do
it 'renders atom feed' do
before do
sign_in user
visit project_issues_path(project, :atom)
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
expect(body).to have_selector('author email', text: issue.author_public_email)
expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email)
expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
it_behaves_like 'an authenticated issuable atom feed'
it_behaves_like 'an authenticated issue atom feed'
end
context 'when authenticated via personal access token' do
it 'renders atom feed' do
before do
personal_access_token = create(:personal_access_token, user: user)
visit project_issues_path(project, :atom,
private_token: personal_access_token.token)
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
expect(body).to have_selector('author email', text: issue.author_public_email)
expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email)
expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email)
expect(body).to have_selector('entry summary', text: issue.title)
end
it_behaves_like 'an authenticated issuable atom feed'
it_behaves_like 'an authenticated issue atom feed'
end
context 'when authenticated via feed token' do
it 'renders atom feed' do
before do
visit project_issues_path(project, :atom,
feed_token: user.feed_token)
end
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
expect(body).to have_selector('title', text: "#{project.name} issues")
expect(body).to have_selector('author email', text: issue.author_public_email)
expect(body).to have_selector('assignees assignee email', text: issue.assignees.first.public_email)
expect(body).to have_selector('assignee email', text: issue.assignees.first.public_email)
expect(body).to have_selector('entry summary', text: issue.title)
it_behaves_like 'an authenticated issuable atom feed'
it_behaves_like 'an authenticated issue atom feed'
end
context 'when not authenticated' do
before do
visit project_issues_path(project, :atom)
end
context 'and the project is private' do
it 'redirects to login page' do
expect(page).to have_current_path(new_user_session_path)
end
end
context 'and the project is public' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:issue) { create(:issue, author: user, assignees: [assignee], project: project, due_date: Date.today) }
let_it_be(:issuable) { issue } # "alias" for shared examples
it_behaves_like 'an authenticated issuable atom feed'
it_behaves_like 'an authenticated issue atom feed'
end
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Merge Requests Feed' do
describe 'GET /merge_requests' do
let_it_be_with_reload(:user) { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
let_it_be(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, assignees: [assignee]) }
let_it_be(:issuable) { merge_request } # "alias" for shared examples
before_all do
project.add_developer(user)
group.add_developer(user)
end
RSpec.shared_examples 'an authenticated merge request atom feed' do
it 'renders atom feed with additional merge request information' do
expect(body).to have_selector('title', text: "#{project.name} merge requests")
end
end
context 'when authenticated' do
before do
sign_in user
visit project_merge_requests_path(project, :atom)
end
it_behaves_like 'an authenticated issuable atom feed'
it_behaves_like 'an authenticated merge request atom feed'
context 'but the use can not see the project' do
let_it_be(:other_project) { create(:project) }
it 'renders 404 page' do
visit project_issues_path(other_project, :atom)
expect(page).to have_gitlab_http_status(:not_found)
end
end
end
context 'when authenticated via personal access token' do
before do
personal_access_token = create(:personal_access_token, user: user)
visit project_merge_requests_path(project, :atom,
private_token: personal_access_token.token)
end
it_behaves_like 'an authenticated issuable atom feed'
it_behaves_like 'an authenticated merge request atom feed'
end
context 'when authenticated via feed token' do
before do
visit project_merge_requests_path(project, :atom,
feed_token: user.feed_token)
end
it_behaves_like 'an authenticated issuable atom feed'
it_behaves_like 'an authenticated merge request atom feed'
end
context 'when not authenticated' do
before do
visit project_merge_requests_path(project, :atom)
end
context 'and the project is private' do
it 'redirects to login page' do
expect(page).to have_current_path(new_user_session_path)
end
end
context 'and the project is public' do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, assignees: [assignee]) }
let_it_be(:issuable) { merge_request } # "alias" for shared examples
it_behaves_like 'an authenticated issuable atom feed'
it_behaves_like 'an authenticated merge request atom feed'
end
end
end
end
......@@ -3,21 +3,24 @@
require 'spec_helper'
RSpec.describe 'Project Issues RSS' do
let!(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, group: group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let(:path) { project_issues_path(project) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let_it_be(:path) { project_issues_path(project) }
let_it_be(:issue) { create(:issue, project: project, assignees: [user]) }
before do
create(:issue, project: project, assignees: [user])
before_all do
group.add_developer(user)
end
context 'when signed in' do
let(:user) { create(:user) }
let_it_be(:user) { create(:user) }
before do
before_all do
project.add_developer(user)
end
before do
sign_in(user)
visit path
end
......@@ -36,26 +39,6 @@ RSpec.describe 'Project Issues RSS' do
end
describe 'feeds' do
shared_examples 'updates atom feed link' do |type|
it "for #{type}" do
sign_in(user)
visit path
link = find_link('Subscribe to RSS feed')
params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find('link[type="application/atom+xml"]', visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
expected = {
'feed_token' => [user.feed_token],
'assignee_id' => [user.id.to_s]
}
expect(params).to include(expected)
expect(auto_discovery_params).to include(expected)
end
end
it_behaves_like 'updates atom feed link', :project do
let(:path) { project_issues_path(project, assignee_id: user.id) }
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Project Merge Requests RSS' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
let_it_be(:path) { project_merge_requests_path(project) }
before_all do
group.add_developer(user)
end
context 'when signed in' do
let_it_be(:user) { create(:user) }
before_all do
project.add_developer(user)
end
before do
sign_in(user)
visit path
end
it_behaves_like "it has an RSS button with current_user's feed token"
it_behaves_like "an autodiscoverable RSS feed with current_user's feed token"
end
context 'when signed out' do
before do
visit path
end
it_behaves_like "it has an RSS button without a feed token"
it_behaves_like "an autodiscoverable RSS feed without a feed token"
end
describe 'feeds' do
it_behaves_like 'updates atom feed link', :project do
let(:path) { project_merge_requests_path(project, assignee_id: user.id) }
end
end
end
# frozen_string_literal: true
RSpec.shared_examples "an authenticated issuable atom feed" do
it "renders atom feed with common issuable information" do
expect(response_headers['Content-Type'])
.to have_content('application/atom+xml')
expect(body).to have_selector('author email', text: issuable.author_public_email)
expect(body).to have_selector('assignees assignee email', text: issuable.assignees.first.public_email)
expect(body).to have_selector('assignee email', text: issuable.assignees.first.public_email)
expect(body).to have_selector('entry summary', text: issuable.title)
end
end
......@@ -25,3 +25,23 @@ RSpec.shared_examples "it has an RSS button without a feed token" do
.to have_css("a:has(.qa-rss-icon):not([href*='feed_token'])") # rubocop:disable QA/SelectorUsage
end
end
RSpec.shared_examples "updates atom feed link" do |type|
it "for #{type}" do
sign_in(user)
visit path
link = find_link('Subscribe to RSS feed')
params = CGI.parse(URI.parse(link[:href]).query)
auto_discovery_link = find("link[type='application/atom+xml']", visible: false)
auto_discovery_params = CGI.parse(URI.parse(auto_discovery_link[:href]).query)
expected = {
'feed_token' => [user.feed_token],
'assignee_id' => [user.id.to_s]
}
expect(params).to include(expected)
expect(auto_discovery_params).to include(expected)
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