summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG2
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/merge_requests_controller.rb4
-rw-r--r--doc/api/repositories.md74
-rw-r--r--doc/api/tags.md106
-rw-r--r--doc/workflow/README.md1
-rw-r--r--doc/workflow/releases.md20
-rw-r--r--doc/workflow/releases/new_tag.pngbin0 -> 154755 bytes
-rw-r--r--doc/workflow/releases/tags.pngbin0 -> 165449 bytes
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb48
-rw-r--r--lib/api/repositories.rb35
-rw-r--r--lib/api/tags.rb61
-rw-r--r--lib/tasks/grape.rake8
-rw-r--r--spec/requests/api/repositories_spec.rb75
-rw-r--r--spec/requests/api/tags_spec.rb135
16 files changed, 369 insertions, 205 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 5d22e950914..98066668335 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -39,7 +39,9 @@ v 8.2.0 (unreleased)
- Improve Continuous Integration graphs page
- Make color of "Accept Merge Request" button consistent with current build status
- Add ignore white space option in merge request diff and commit and compare view
+ - Ability to add release notes (markdown text and attachments) to git tags (aka Releases)
- Relative links from a repositories README.md now link to the default branch
+ - Fix trailing whitespace issue in merge request/issue title
v 8.1.4
- Fix bug where manually merged branches in a MR would end up with an empty diff (Stan Hu)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index e767efbdc0c..e74c2905e48 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -158,10 +158,12 @@ class Projects::IssuesController < Projects::ApplicationController
end
def issue_params
- params.require(:issue).permit(
+ permitted = params.require(:issue).permit(
:title, :assignee_id, :position, :description,
:milestone_id, :state_event, :task_num, label_ids: []
)
+ params[:issue][:title].strip! if params[:issue][:title]
+ permitted
end
def bulk_update_params
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index b0788a2d073..188f0cc4cea 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -276,11 +276,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_request_params
- params.require(:merge_request).permit(
+ permitted = params.require(:merge_request).permit(
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, :task_num, label_ids: []
)
+ params[:merge_request][:title].strip! if params[:merge_request][:title]
+ permitted
end
# Make sure merge requests created before 8.0
diff --git a/doc/api/repositories.md b/doc/api/repositories.md
index 33167453802..b6cca5d4e2a 100644
--- a/doc/api/repositories.md
+++ b/doc/api/repositories.md
@@ -1,79 +1,5 @@
# Repositories
-## List project repository tags
-
-Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
-
-```
-GET /projects/:id/repository/tags
-```
-
-Parameters:
-
-- `id` (required) - The ID of a project
-
-```json
-[
- {
- "commit": {
- "author_name": "John Smith",
- "author_email": "john@example.com",
- "authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00",
- "committer_name": "Jack Smith",
- "committer_email": "jack@example.com",
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": "Initial commit",
- "parents_ids": [
- "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
- ]
- },
- "name": "v1.0.0",
- "message": null
- }
-]
-```
-
-## Create a new tag
-
-Creates new tag in the repository that points to the supplied ref.
-
-```
-POST /projects/:id/repository/tags
-```
-
-Parameters:
-
-- `id` (required) - The ID of a project
-- `tag_name` (required) - The name of a tag
-- `ref` (required) - Create tag using commit SHA, another tag name, or branch name.
-- `message` (optional) - Creates annotated tag.
-
-```json
-{
- "commit": {
- "author_name": "John Smith",
- "author_email": "john@example.com",
- "authored_date": "2012-05-28T04:42:42-07:00",
- "committed_date": "2012-05-28T04:42:42-07:00",
- "committer_name": "Jack Smith",
- "committer_email": "jack@example.com",
- "id": "2695effb5807a22ff3d138d593fd856244e155e7",
- "message": "Initial commit",
- "parents_ids": [
- "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
- ]
- },
- "name": "v1.0.0",
- "message": null
-}
-```
-The message will be `nil` when creating a lightweight tag otherwise
-it will contain the annotation.
-
-It returns 200 if the operation succeed. In case of an error,
-405 with an explaining error message is returned.
-
## List repository tree
Get a list of repository files and directories in a project.
diff --git a/doc/api/tags.md b/doc/api/tags.md
new file mode 100644
index 00000000000..b5b90cf6b82
--- /dev/null
+++ b/doc/api/tags.md
@@ -0,0 +1,106 @@
+# Tags
+
+## List project repository tags
+
+Get a list of repository tags from a project, sorted by name in reverse alphabetical order.
+
+```
+GET /projects/:id/repository/tags
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+
+```json
+[
+ {
+ "commit": {
+ "author_name": "John Smith",
+ "author_email": "john@example.com",
+ "authored_date": "2012-05-28T04:42:42-07:00",
+ "committed_date": "2012-05-28T04:42:42-07:00",
+ "committer_name": "Jack Smith",
+ "committer_email": "jack@example.com",
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "message": "Initial commit",
+ "parents_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ]
+ },
+ "release": {
+ "tag": "1.0.0",
+ "description": "Amazing release. Wow"
+ },
+ "name": "v1.0.0",
+ "message": null
+ }
+]
+```
+
+## Create a new tag
+
+Creates a new tag in the repository that points to the supplied ref.
+
+```
+POST /projects/:id/repository/tags
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag_name` (required) - The name of a tag
+- `ref` (required) - Create tag using commit SHA, another tag name, or branch name.
+- `message` (optional) - Creates annotated tag.
+- `release_description` (optional) - Add release notes to the git tag and store it in the GitLab database.
+
+```json
+{
+ "commit": {
+ "author_name": "John Smith",
+ "author_email": "john@example.com",
+ "authored_date": "2012-05-28T04:42:42-07:00",
+ "committed_date": "2012-05-28T04:42:42-07:00",
+ "committer_name": "Jack Smith",
+ "committer_email": "jack@example.com",
+ "id": "2695effb5807a22ff3d138d593fd856244e155e7",
+ "message": "Initial commit",
+ "parents_ids": [
+ "2a4b78934375d7f53875269ffd4f45fd83a84ebe"
+ ]
+ },
+ "release": {
+ "tag": "1.0.0",
+ "description": "Amazing release. Wow"
+ },
+ "name": "v1.0.0",
+ "message": null
+}
+```
+The message will be `nil` when creating a lightweight tag otherwise
+it will contain the annotation.
+
+It returns 200 if the operation succeed. In case of an error,
+405 with an explaining error message is returned.
+
+
+## New release
+
+Add release notes to the existing git tag
+
+```
+PUT /projects/:id/repository/:tag/release
+```
+
+Parameters:
+
+- `id` (required) - The ID of a project
+- `tag` (required) - The name of a tag
+- `description` (required) - Release notes with markdown support
+
+```json
+{
+ "tag": "1.0.0",
+ "description": "Amazing release. Wow"
+}
+```
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 5b8d72dfd34..a2653704c33 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -13,5 +13,6 @@
- [Project users](add-user/add-user.md)
- [Protected branches](protected_branches.md)
- [Web Editor](web_editor.md)
+- [Releases](releases.md)
- [Merge Requests](merge_requests.md)
- ["Work In Progress" Merge Requests](wip_merge_requests.md)
diff --git a/doc/workflow/releases.md b/doc/workflow/releases.md
new file mode 100644
index 00000000000..6176784fc57
--- /dev/null
+++ b/doc/workflow/releases.md
@@ -0,0 +1,20 @@
+# Releases
+
+You can turn any git tag into a release, by adding a note to it.
+Release notes behave like any other markdown form in GitLab so you can write text and drag-n-drop files to it.
+Release notes are stored in the database of GitLab.
+
+There are several ways to add release notes:
+
+* In the interface, when you create a new git tag with GitLab
+* In the interface, by adding a note to an existing git tag
+* with the GitLab API
+
+## New tag page with release notes text area
+
+![new_tag](releases/new_tag.png)
+
+## Tags page with button to add or edit release notes for existing git tag
+
+![tags](releases/tags.png)
+
diff --git a/doc/workflow/releases/new_tag.png b/doc/workflow/releases/new_tag.png
new file mode 100644
index 00000000000..e2b64bfe17f
--- /dev/null
+++ b/doc/workflow/releases/new_tag.png
Binary files differ
diff --git a/doc/workflow/releases/tags.png b/doc/workflow/releases/tags.png
new file mode 100644
index 00000000000..aca91906c68
--- /dev/null
+++ b/doc/workflow/releases/tags.png
Binary files differ
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 40671e2517c..fe1bf8a4816 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -52,5 +52,6 @@ module API
mount Labels
mount Settings
mount Keys
+ mount Tags
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 73acf66935a..d6aec03d7f5 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -95,25 +95,6 @@ module API
end
end
- class RepoTag < Grape::Entity
- expose :name
- expose :message do |repo_obj, _options|
- if repo_obj.respond_to?(:message)
- repo_obj.message
- else
- nil
- end
- end
-
- expose :commit do |repo_obj, options|
- if repo_obj.respond_to?(:commit)
- repo_obj.commit
- elsif options[:project]
- options[:project].repository.commit(repo_obj.target)
- end
- end
- end
-
class RepoObject < Grape::Entity
expose :name
@@ -341,5 +322,34 @@ module API
expose :user_oauth_applications
expose :after_sign_out_path
end
+
+ class Release < Grape::Entity
+ expose :tag, :description
+ end
+
+ class RepoTag < Grape::Entity
+ expose :name
+ expose :message do |repo_obj, _options|
+ if repo_obj.respond_to?(:message)
+ repo_obj.message
+ else
+ nil
+ end
+ end
+
+ expose :commit do |repo_obj, options|
+ if repo_obj.respond_to?(:commit)
+ repo_obj.commit
+ elsif options[:project]
+ options[:project].repository.commit(repo_obj.target)
+ end
+ end
+
+ expose :release, using: Entities::Release do |repo_obj, options|
+ if options[:project]
+ options[:project].releases.find_by(tag: repo_obj.name)
+ end
+ end
+ end
end
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index 20d568cf462..d7c48639eba 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -16,41 +16,6 @@ module API
end
end
- # Get a project repository tags
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/repository/tags
- get ":id/repository/tags" do
- present user_project.repo.tags.sort_by(&:name).reverse,
- with: Entities::RepoTag, project: user_project
- end
-
- # Create tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # ref (required) - Create tag from commit sha or branch
- # message (optional) - Specifying a message creates an annotated tag.
- # Example Request:
- # POST /projects/:id/repository/tags
- post ':id/repository/tags' do
- authorize_push_project
- message = params[:message] || nil
- result = CreateTagService.new(user_project, current_user).
- execute(params[:tag_name], params[:ref], message)
-
- if result[:status] == :success
- present result[:tag],
- with: Entities::RepoTag,
- project: user_project
- else
- render_api_error!(result[:message], 400)
- end
- end
-
# Get a project repository tree
#
# Parameters:
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
new file mode 100644
index 00000000000..673342dd447
--- /dev/null
+++ b/lib/api/tags.rb
@@ -0,0 +1,61 @@
+module API
+ # Git Tags API
+ class Tags < Grape::API
+ before { authenticate! }
+ before { authorize! :download_code, user_project }
+
+ resource :projects do
+ # Get a project repository tags
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # GET /projects/:id/repository/tags
+ get ":id/repository/tags" do
+ present user_project.repo.tags.sort_by(&:name).reverse,
+ with: Entities::RepoTag, project: user_project
+ end
+
+ # Create tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag_name (required) - The name of the tag
+ # ref (required) - Create tag from commit sha or branch
+ # message (optional) - Specifying a message creates an annotated tag.
+ # Example Request:
+ # POST /projects/:id/repository/tags
+ post ':id/repository/tags' do
+ authorize_push_project
+ message = params[:message] || nil
+ result = CreateTagService.new(user_project, current_user).
+ execute(params[:tag_name], params[:ref], message, params[:release_description])
+
+ if result[:status] == :success
+ present result[:tag],
+ with: Entities::RepoTag,
+ project: user_project
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
+ # Add release notes to tag
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # tag (required) - The name of the tag
+ # description (required) - Release notes with markdown support
+ # Example Request:
+ # PUT /projects/:id/repository/tags
+ put ':id/repository/:tag/release', requirements: { tag: /.*/ } do
+ authorize_push_project
+ required_attributes! [:description]
+ release = user_project.releases.find_or_initialize_by(tag: params[:tag])
+ release.update_attributes(description: params[:description])
+
+ present release, with: Entities::Release
+ end
+ end
+ end
+end
diff --git a/lib/tasks/grape.rake b/lib/tasks/grape.rake
new file mode 100644
index 00000000000..9980e0b7984
--- /dev/null
+++ b/lib/tasks/grape.rake
@@ -0,0 +1,8 @@
+namespace :grape do
+ desc 'Print compiled grape routes'
+ task routes: :environment do
+ API::API.routes.each do |route|
+ puts route
+ end
+ end
+end
diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb
index faf6b77a462..4911cdd9da6 100644
--- a/spec/requests/api/repositories_spec.rb
+++ b/spec/requests/api/repositories_spec.rb
@@ -11,81 +11,6 @@ describe API::API, api: true do
let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
- describe "GET /projects/:id/repository/tags" do
- it "should return an array of project tags" do
- get api("/projects/#{project.id}/repository/tags", user)
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.first['name']).to eq(project.repo.tags.sort_by(&:name).reverse.first.name)
- end
- end
-
- describe 'POST /projects/:id/repository/tags' do
- context 'lightweight tags' do
- it 'should create a new tag' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.0.1',
- ref: 'master'
-
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq('v7.0.1')
- end
- end
-
- context 'annotated tag' do
- it 'should create a new annotated tag' do
- # Identity must be set in .gitconfig to create annotated tag.
- repo_path = project.repository.path_to_repo
- system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
- system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email}))
-
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v7.1.0',
- ref: 'master',
- message: 'Release 7.1.0'
-
- expect(response.status).to eq(201)
- expect(json_response['name']).to eq('v7.1.0')
- expect(json_response['message']).to eq('Release 7.1.0')
- end
- end
-
- it 'should deny for user without push access' do
- post api("/projects/#{project.id}/repository/tags", user2),
- tag_name: 'v1.9.0',
- ref: '621491c677087aa243f165eab467bfdfbee00be1'
- expect(response.status).to eq(403)
- end
-
- it 'should return 400 if tag name is invalid' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v 1.0.0',
- ref: 'master'
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Tag name invalid')
- end
-
- it 'should return 400 if tag already exists' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v8.0.0',
- ref: 'master'
- expect(response.status).to eq(201)
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'v8.0.0',
- ref: 'master'
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Tag already exists')
- end
-
- it 'should return 400 if ref name is invalid' do
- post api("/projects/#{project.id}/repository/tags", user),
- tag_name: 'mytag',
- ref: 'foo'
- expect(response.status).to eq(400)
- expect(json_response['message']).to eq('Invalid reference name')
- end
- end
-
describe "GET /projects/:id/repository/tree" do
context "authorized user" do
before { project.team << [user2, :reporter] }
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
new file mode 100644
index 00000000000..cc9a5f47582
--- /dev/null
+++ b/spec/requests/api/tags_spec.rb
@@ -0,0 +1,135 @@
+require 'spec_helper'
+require 'mime/types'
+
+describe API::API, api: true do
+ include ApiHelpers
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let!(:project) { create(:project, creator_id: user.id) }
+ let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) }
+ let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) }
+
+ describe "GET /projects/:id/repository/tags" do
+ let(:tag_name) { project.repository.tag_names.sort.reverse.first }
+ let(:description) { 'Awesome release!' }
+
+ context 'without releases' do
+ it "should return an array of project tags" do
+ get api("/projects/#{project.id}/repository/tags", user)
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ end
+ end
+
+ context 'with releases' do
+ before do
+ release = project.releases.find_or_initialize_by(tag: tag_name)
+ release.update_attributes(description: description)
+ get api("/projects/#{project.id}/repository/tags", user)
+ end
+
+ it "should return an array of project tags with release info" do
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).to eq(tag_name)
+ expect(json_response.first['release']['description']).to eq(description)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/repository/tags' do
+ context 'lightweight tags' do
+ it 'should create a new tag' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.0.1',
+ ref: 'master'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.0.1')
+ end
+ end
+
+ context 'lightweight tags with release notes' do
+ it 'should create a new tag' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.0.1',
+ ref: 'master',
+ release_description: 'Wow'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.0.1')
+ expect(json_response['release']['description']).to eq('Wow')
+ end
+ end
+
+ context 'annotated tag' do
+ it 'should create a new annotated tag' do
+ # Identity must be set in .gitconfig to create annotated tag.
+ repo_path = project.repository.path_to_repo
+ system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.name #{user.name}))
+ system(*%W(#{Gitlab.config.git.bin_path} --git-dir=#{repo_path} config user.email #{user.email}))
+
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v7.1.0',
+ ref: 'master',
+ message: 'Release 7.1.0'
+
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq('v7.1.0')
+ expect(json_response['message']).to eq('Release 7.1.0')
+ end
+ end
+
+ it 'should deny for user without push access' do
+ post api("/projects/#{project.id}/repository/tags", user2),
+ tag_name: 'v1.9.0',
+ ref: '621491c677087aa243f165eab467bfdfbee00be1'
+ expect(response.status).to eq(403)
+ end
+
+ it 'should return 400 if tag name is invalid' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v 1.0.0',
+ ref: 'master'
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('Tag name invalid')
+ end
+
+ it 'should return 400 if tag already exists' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v8.0.0',
+ ref: 'master'
+ expect(response.status).to eq(201)
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'v8.0.0',
+ ref: 'master'
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('Tag already exists')
+ end
+
+ it 'should return 400 if ref name is invalid' do
+ post api("/projects/#{project.id}/repository/tags", user),
+ tag_name: 'mytag',
+ ref: 'foo'
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('Invalid reference name')
+ end
+ end
+
+ describe 'PUT /projects/:id/repository/:tag/release' do
+ let(:tag_name) { project.repository.tag_names.first }
+ let(:description) { 'Awesome release!' }
+
+ it 'should create description for existing git tag' do
+ put api("/projects/#{project.id}/repository/#{tag_name}/release", user),
+ description: description
+
+ expect(response.status).to eq(200)
+ expect(json_response['tag']).to eq(tag_name)
+ expect(json_response['description']).to eq(description)
+ end
+ end
+end