Commit cb4f15a3 authored by Sean Carroll's avatar Sean Carroll Committed by Mayra Cabrera

Add links to commit SHA and tag

Modify the Release API endpoint and Vue components to add live
links on the Releases page.

Closes https://gitlab.com/gitlab-org/gitlab-ce/issues/61061
parent 1b95b8b6
...@@ -42,6 +42,12 @@ export default { ...@@ -42,6 +42,12 @@ export default {
commit() { commit() {
return this.release.commit || {}; return this.release.commit || {};
}, },
commitUrl() {
return this.release.commit_path;
},
tagUrl() {
return this.release.tag_path;
},
assets() { assets() {
return this.release.assets || {}; return this.release.assets || {};
}, },
...@@ -81,12 +87,18 @@ export default { ...@@ -81,12 +87,18 @@ export default {
<div class="card-subtitle d-flex flex-wrap text-secondary"> <div class="card-subtitle d-flex flex-wrap text-secondary">
<div class="append-right-8"> <div class="append-right-8">
<icon name="commit" class="align-middle" /> <icon name="commit" class="align-middle" />
<span v-gl-tooltip.bottom :title="commit.title">{{ commit.short_id }}</span> <gl-link v-if="commitUrl" v-gl-tooltip.bottom :title="commit.title" :href="commitUrl">
{{ commit.short_id }}
</gl-link>
<span v-else v-gl-tooltip.bottom :title="commit.title">{{ commit.short_id }}</span>
</div> </div>
<div class="append-right-8"> <div class="append-right-8">
<icon name="tag" class="align-middle" /> <icon name="tag" class="align-middle" />
<span v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span> <gl-link v-if="tagUrl" v-gl-tooltip.bottom :title="__('Tag')" :href="tagUrl">
{{ release.tag_name }}
</gl-link>
<span v-else v-gl-tooltip.bottom :title="__('Tag')">{{ release.tag_name }}</span>
</div> </div>
<milestone-list <milestone-list
......
---
title: Links on Releases page to commits and tags
merge_request: 16128
author:
type: changed
...@@ -85,6 +85,8 @@ Example response: ...@@ -85,6 +85,8 @@ Example response:
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2"
} }
], ],
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
"assets":{ "assets":{
"count":6, "count":6,
"sources":[ "sources":[
...@@ -261,6 +263,8 @@ Example response: ...@@ -261,6 +263,8 @@ Example response:
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2"
} }
], ],
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
"assets":{ "assets":{
"count":4, "count":4,
"sources":[ "sources":[
...@@ -379,6 +383,8 @@ Example response: ...@@ -379,6 +383,8 @@ Example response:
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2" "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/2"
} }
], ],
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
"assets":{ "assets":{
"count":5, "count":5,
"sources":[ "sources":[
...@@ -483,6 +489,8 @@ Example response: ...@@ -483,6 +489,8 @@ Example response:
"web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/3" "web_url":"https://gitlab.example.com/root/awesome-app/-/milestones/3"
} }
], ],
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
"assets":{ "assets":{
"count":4, "count":4,
"sources":[ "sources":[
...@@ -563,6 +571,8 @@ Example response: ...@@ -563,6 +571,8 @@ Example response:
"committer_email":"admin@example.com", "committer_email":"admin@example.com",
"committed_date":"2019-01-03T01:53:28.000Z" "committed_date":"2019-01-03T01:53:28.000Z"
}, },
"commit_path":"/root/awesome-app/commit/588440f66559714280628a4f9799f0c4eb880a4a",
"tag_path":"/root/awesome-app/-/tags/v0.11.1",
"assets":{ "assets":{
"count":4, "count":4,
"sources":[ "sources":[
......
...@@ -1276,7 +1276,7 @@ module API ...@@ -1276,7 +1276,7 @@ module API
class Release < Grape::Entity class Release < Grape::Entity
expose :name expose :name
expose :tag, as: :tag_name, if: lambda { |_, _| can_download_code? } expose :tag, as: :tag_name, if: ->(_, _) { can_download_code? }
expose :description expose :description
expose :description_html do |entity| expose :description_html do |entity|
MarkupHelper.markdown_field(entity, :description) MarkupHelper.markdown_field(entity, :description)
...@@ -1284,16 +1284,17 @@ module API ...@@ -1284,16 +1284,17 @@ module API
expose :created_at expose :created_at
expose :released_at expose :released_at
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? } expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
expose :commit, using: Entities::Commit, if: lambda { |_, _| can_download_code? } expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
expose :upcoming_release?, as: :upcoming_release expose :upcoming_release?, as: :upcoming_release
expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? } expose :milestones, using: Entities::Milestone, if: -> (release, _) { release.milestones.present? }
expose :commit_path, if: ->(_, _) { can_download_code? }
expose :tag_path, if: ->(_, _) { can_download_code? }
expose :assets do expose :assets do
expose :assets_count, as: :count do |release, _| expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources] assets_to_exclude = can_download_code? ? [] : [:sources]
release.assets_count(except: assets_to_exclude) release.assets_count(except: assets_to_exclude)
end end
expose :sources, using: Entities::Releases::Source, if: lambda { |_, _| can_download_code? } expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? }
expose :links, using: Entities::Releases::Link do |release, options| expose :links, using: Entities::Releases::Link do |release, options|
release.links.sorted release.links.sorted
end end
...@@ -1304,6 +1305,16 @@ module API ...@@ -1304,6 +1305,16 @@ module API
def can_download_code? def can_download_code?
Ability.allowed?(options[:current_user], :download_code, object.project) Ability.allowed?(options[:current_user], :download_code, object.project)
end end
def commit_path
return unless object.commit
Gitlab::Routing.url_helpers.project_commit_path(object.project, object.commit.id)
end
def tag_path
Gitlab::Routing.url_helpers.project_tag_path(object.project, object.tag)
end
end end
class Tag < Grape::Entity class Tag < Grape::Entity
......
...@@ -19,6 +19,9 @@ ...@@ -19,6 +19,9 @@
"type": "array", "type": "array",
"items": { "$ref": "milestone.json" } "items": { "$ref": "milestone.json" }
}, },
"commit_path": { "type": "string" },
"tag_path": { "type": "string" },
"name": { "type": "string" },
"assets": { "assets": {
"required": ["count", "links", "sources"], "required": ["count", "links", "sources"],
"properties": { "properties": {
......
...@@ -8,6 +8,12 @@ ...@@ -8,6 +8,12 @@
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"released_at": { "type": "date" }, "released_at": { "type": "date" },
"upcoming_release": { "type": "boolean" }, "upcoming_release": { "type": "boolean" },
"milestones": {
"type": "array",
"items": { "$ref": "../milestone.json" }
},
"commit_path": { "type": "string" },
"tag_path": { "type": "string" },
"author": { "author": {
"oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }] "oneOf": [{ "type": "null" }, { "$ref": "../user/basic.json" }]
}, },
......
...@@ -12,7 +12,6 @@ describe('Release block', () => { ...@@ -12,7 +12,6 @@ describe('Release block', () => {
propsData: { propsData: {
release: releaseProp, release: releaseProp,
}, },
sync: false,
}); });
}; };
...@@ -37,10 +36,16 @@ describe('Release block', () => { ...@@ -37,10 +36,16 @@ describe('Release block', () => {
it('renders commit sha', () => { it('renders commit sha', () => {
expect(wrapper.text()).toContain(release.commit.short_id); expect(wrapper.text()).toContain(release.commit.short_id);
wrapper.setProps({ release: { ...release, commit_path: '/commit/example' } });
expect(wrapper.find('a[href="/commit/example"]').exists()).toBe(true);
}); });
it('renders tag name', () => { it('renders tag name', () => {
expect(wrapper.text()).toContain(release.tag_name); expect(wrapper.text()).toContain(release.tag_name);
wrapper.setProps({ release: { ...release, tag_path: '/tag/example' } });
expect(wrapper.find('a[href="/tag/example"]').exists()).toBe(true);
}); });
it('renders release date', () => { it('renders release date', () => {
......
import Vue from 'vue';
import component from '~/releases/components/release_block.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Release block', () => {
const Component = Vue.extend(component);
const release = {
name: 'Bionic Beaver',
tag_name: '18.04',
description: '## changelog\n\n* line 1\n* line2',
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
author_email: 'release-bot@example.com',
released_at: '2012-05-28T05:00:00-07:00',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
commit: {
id: '2695effb5807a22ff3d138d593fd856244e155e7',
short_id: '2695effb',
title: 'Initial commit',
created_at: '2017-07-26T11:08:53.000+02:00',
parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
message: 'Initial commit',
author_name: 'John Smith',
author_email: 'john@example.com',
authored_date: '2012-05-28T04:42:42-07:00',
committer_name: 'Jack Smith',
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
},
assets: {
count: 6,
sources: [
{
format: 'zip',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip',
},
{
format: 'tar.gz',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
},
{
format: 'tar.bz2',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
},
{
format: 'tar',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
},
],
links: [
{
name: 'release-18.04.dmg',
url: 'https://my-external-hosting.example.com/scrambled-url/',
external: true,
},
{
name: 'binary-linux-amd64',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
external: false,
},
],
},
};
let vm;
const factory = props => mountComponent(Component, { release: props });
beforeEach(() => {
vm = factory(release);
});
afterEach(() => {
vm.$destroy();
});
it("renders the block with an id equal to the release's tag name", () => {
expect(vm.$el.id).toBe('18.04');
});
it('renders release name', () => {
expect(vm.$el.textContent).toContain(release.name);
});
it('renders commit sha', () => {
expect(vm.$el.textContent).toContain(release.commit.short_id);
});
it('renders tag name', () => {
expect(vm.$el.textContent).toContain(release.tag_name);
});
it('renders release date', () => {
expect(vm.$el.textContent).toContain(timeagoMixin.methods.timeFormated(release.released_at));
});
it('renders number of assets provided', () => {
expect(vm.$el.querySelector('.js-assets-count').textContent).toContain(release.assets.count);
});
it('renders dropdown with the sources', () => {
expect(vm.$el.querySelectorAll('.js-sources-dropdown li').length).toEqual(
release.assets.sources.length,
);
expect(vm.$el.querySelector('.js-sources-dropdown li a').getAttribute('href')).toEqual(
release.assets.sources[0].url,
);
expect(vm.$el.querySelector('.js-sources-dropdown li a').textContent).toContain(
release.assets.sources[0].format,
);
});
it('renders list with the links provided', () => {
expect(vm.$el.querySelectorAll('.js-assets-list li').length).toEqual(
release.assets.links.length,
);
expect(vm.$el.querySelector('.js-assets-list li a').getAttribute('href')).toEqual(
release.assets.links[0].url,
);
expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain(
release.assets.links[0].name,
);
});
it('renders author avatar', () => {
expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
});
describe('external label', () => {
it('renders external label when link is external', () => {
expect(vm.$el.querySelector('.js-assets-list li a').textContent).toContain('external source');
});
it('does not render external label when link is not external', () => {
expect(vm.$el.querySelector('.js-assets-list li:nth-child(2) a').textContent).not.toContain(
'external source',
);
});
});
describe('with upcoming_release flag', () => {
beforeEach(() => {
vm = factory(Object.assign({}, release, { upcoming_release: true }));
});
it('renders upcoming release badge', () => {
expect(vm.$el.textContent).toContain('Upcoming Release');
});
});
});
...@@ -54,6 +54,15 @@ describe API::Releases do ...@@ -54,6 +54,15 @@ describe API::Releases do
expect(response).to match_response_schema('public_api/v4/releases') expect(response).to match_response_schema('public_api/v4/releases')
end end
it 'returns rendered helper paths' do
get api("/projects/#{project.id}/releases", maintainer)
expect(json_response.first['commit_path']).to eq("/#{release_2.project.full_path}/commit/#{release_2.commit.id}")
expect(json_response.first['tag_path']).to eq("/#{release_2.project.full_path}/-/tags/#{release_2.tag}")
expect(json_response.second['commit_path']).to eq("/#{release_1.project.full_path}/commit/#{release_1.commit.id}")
expect(json_response.second['tag_path']).to eq("/#{release_1.project.full_path}/-/tags/#{release_1.tag}")
end
end end
it 'returns an upcoming_release status for a future release' do it 'returns an upcoming_release status for a future release' do
...@@ -103,11 +112,13 @@ describe API::Releases do ...@@ -103,11 +112,13 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it "does not expose tag, commit and source code" do it "does not expose tag, commit, source code or helper paths" do
get api("/projects/#{project.id}/releases", guest) get api("/projects/#{project.id}/releases", guest)
expect(response).to match_response_schema('public_api/v4/release/releases_for_guest') expect(response).to match_response_schema('public_api/v4/release/releases_for_guest')
expect(json_response[0]['assets']['count']).to eq(release.links.count) expect(json_response[0]['assets']['count']).to eq(release.links.count)
expect(json_response[0]['commit_path']).to be_nil
expect(json_response[0]['tag_path']).to be_nil
end end
context 'when project is public' do context 'when project is public' do
...@@ -119,11 +130,13 @@ describe API::Releases do ...@@ -119,11 +130,13 @@ describe API::Releases do
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
end end
it "exposes tag, commit and source code" do it "exposes tag, commit, source code and helper paths" do
get api("/projects/#{project.id}/releases", guest) get api("/projects/#{project.id}/releases", guest)
expect(response).to match_response_schema('public_api/v4/releases') expect(response).to match_response_schema('public_api/v4/releases')
expect(json_response[0]['assets']['count']).to eq(release.links.count + release.sources.count) expect(json_response.first['assets']['count']).to eq(release.links.count + release.sources.count)
expect(json_response.first['commit_path']).to eq("/#{release.project.full_path}/commit/#{release.commit.id}")
expect(json_response.first['tag_path']).to eq("/#{release.project.full_path}/-/tags/#{release.tag}")
end end
end end
end end
...@@ -172,6 +185,8 @@ describe API::Releases do ...@@ -172,6 +185,8 @@ describe API::Releases do
expect(json_response['author']['name']).to eq(maintainer.name) expect(json_response['author']['name']).to eq(maintainer.name)
expect(json_response['commit']['id']).to eq(commit.id) expect(json_response['commit']['id']).to eq(commit.id)
expect(json_response['assets']['count']).to eq(4) expect(json_response['assets']['count']).to eq(4)
expect(json_response['commit_path']).to eq("/#{release.project.full_path}/commit/#{release.commit.id}")
expect(json_response['tag_path']).to eq("/#{release.project.full_path}/-/tags/#{release.tag}")
end end
it 'matches response schema' do it 'matches response schema' do
......
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