summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDouwe Maan <douwe@gitlab.com>2016-01-14 13:11:12 +0100
committerDouwe Maan <douwe@gitlab.com>2016-01-14 13:11:12 +0100
commit6b000bee8cdc7005358c2a5cf3a1f12af57dc862 (patch)
treed5a2c566c4106cc359a2f438a81c3e586ac9df29
parent05187f0fc671e1ddcbe6d7b0f76971d792acaada (diff)
parentf981da44ab88012db984e1457170067b345660c1 (diff)
downloadgitlab-ce-diff-parse-first-commit.tar.gz
Merge branch 'master' into diff-parse-first-commitdiff-parse-first-commit
-rw-r--r--CHANGELOG2
-rw-r--r--app/assets/stylesheets/pages/branches.scss3
-rw-r--r--app/assets/stylesheets/pages/commit.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss5
-rw-r--r--app/assets/stylesheets/pages/issues.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss2
-rw-r--r--app/assets/stylesheets/pages/tags.scss3
-rw-r--r--app/models/ci/variable.rb6
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml4
-rw-r--r--app/views/shared/groups/_group.html.haml3
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/build_variables.md128
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/variables.rb95
-rw-r--r--spec/factories/ci/variables.rb22
-rw-r--r--spec/requests/api/variables_spec.rb182
20 files changed, 463 insertions, 10 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 4bd871826f6..be998c18e01 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,7 @@ Please view this file on the master branch, on stable branches it's out of date.
v 8.4.0 (unreleased)
- Properly generate diff of orphan commits, like the first commit in a repository
+ - Improve the consistency of commit titles, branch names, tag names, issue/MR titles, on their respective project pages
- Autocomplete data is now always loaded, instead of when focusing a comment text area (Yorick Peterse)
- Improved performance of finding issues for an entire group (Yorick Peterse)
- Added custom application performance measuring system powered by InfluxDB (Yorick Peterse)
@@ -45,6 +46,7 @@ v 8.4.0 (unreleased)
- Allow subsequent validations in CI Linter
- Show referenced MRs & Issues only when the current viewer can access them
- Fix Encoding::CompatibilityError bug when markdown content has some complex URL (Jason Lee)
+ - Add API support for managing build variables of project
- Allow broadcast messages to be edited
v 8.3.4
diff --git a/app/assets/stylesheets/pages/branches.scss b/app/assets/stylesheets/pages/branches.scss
new file mode 100644
index 00000000000..abae5c3d0a5
--- /dev/null
+++ b/app/assets/stylesheets/pages/branches.scss
@@ -0,0 +1,3 @@
+.branch-name{
+ font-weight: 600;
+}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 17245d3be7b..05d2f306d0f 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -2,6 +2,10 @@
display: block;
}
+.commit-row-title .commit-title {
+ font-weight: 600;
+}
+
.commit-author, .commit-committer{
display: block;
color: #999;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 263993f59a5..3404c2631e1 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -11,3 +11,8 @@
height: 42px;
}
}
+
+.content-list .group-name {
+ font-weight: 600;
+ color: #4c4e54;
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 1e1af662850..ad92cc22815 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -6,7 +6,7 @@
.issue-title {
margin-bottom: 5px;
font-size: $list-font-size;
- font-weight: bold;
+ font-weight: 600;
}
.issue-info {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 82effde0bf3..efd33df2e99 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -150,7 +150,7 @@
.merge-request-title {
margin-bottom: 5px;
font-size: $list-font-size;
- font-weight: bold;
+ font-weight: 600;
}
.merge-request-info {
diff --git a/app/assets/stylesheets/pages/tags.scss b/app/assets/stylesheets/pages/tags.scss
new file mode 100644
index 00000000000..e9cd6dc6c5e
--- /dev/null
+++ b/app/assets/stylesheets/pages/tags.scss
@@ -0,0 +1,3 @@
+.tag-name{
+ font-weight: 600;
+}
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 7f6f497f325..e786bd7dd93 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -18,8 +18,12 @@ module Ci
belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
- validates_presence_of :key
validates_uniqueness_of :key, scope: :gl_project_id
+ validates :key,
+ presence: true,
+ length: { within: 0..255 },
+ format: { with: /\A[a-zA-Z0-9_]+\z/,
+ message: "can contain only letters, digits and '_'." }
attr_encrypted :value, mode: :per_attribute_iv_and_salt, key: Gitlab::Application.secrets.db_key_base
end
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index a234536723e..d276e5932d1 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -6,7 +6,7 @@
%li(class="js-branch-#{branch.name}")
%div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
- %strong.str-truncated= branch.name
+ .branch-name.str-truncated= branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 012825f0fdb..4d4b410ee29 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -11,7 +11,7 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
.commit-row-title
- %strong.str-truncated
+ .commit-title.str-truncated
= link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- if commit.description?
%a.text-expander.js-toggle-button ...
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 28b706c5c7e..56a7ced1236 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -3,7 +3,7 @@
%li
%div
= link_to namespace_project_tag_path(@project.namespace, @project, tag.name) do
- %strong
+ .tag-name
= icon('tag')
= tag.name
- if tag.message.present?
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index b594d4f1f27..dbb20347860 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -17,8 +17,8 @@
.pull-right
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has_tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
%i.fa.fa-trash-o
- .title
- %strong= @tag.name
+ .tag-name.title
+ = @tag.name
- if @tag.message.present?
%span.light
&nbsp;
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index a54c5fa8c33..f4cfa29ae56 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -10,8 +10,7 @@
%i.fa.fa-sign-out
= image_tag group_icon(group), class: "avatar s46 hidden-xs"
- = link_to group, class: 'group-name' do
- %strong= group.name
+ = link_to group.name, group, class: 'group-name'
- if group_member
as
diff --git a/doc/api/README.md b/doc/api/README.md
index 25a31b235cc..c3401bcbc9d 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -23,6 +23,7 @@
- [Namespaces](namespaces.md)
- [Settings](settings.md)
- [Keys](keys.md)
+- [Build Variables](build_variables.md)
## Clients
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
new file mode 100644
index 00000000000..b96f1bdac8a
--- /dev/null
+++ b/doc/api/build_variables.md
@@ -0,0 +1,128 @@
+# Build Variables
+
+## List project variables
+
+Get list of a project's build variables.
+
+```
+GET /projects/:id/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|---------------------|
+| `id` | integer | yes | The ID of a project |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables"
+```
+
+```json
+[
+ {
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+ },
+ {
+ "key": "TEST_VARIABLE_2",
+ "value": "TEST_2"
+ }
+]
+```
+
+## Show variable details
+
+Get the details of a project's specific build variable.
+
+```
+GET /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+
+```
+curl -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/TEST_VARIABLE_1"
+```
+
+```json
+{
+ "key": "TEST_VARIABLE_1",
+ "value": "TEST_1"
+}
+```
+
+## Create variable
+
+Create a new build variable.
+
+```
+POST /projects/:id/variables
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-----------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+
+```
+curl -X POST -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables" -F "key=NEW_VARIABLE" -F "value=new value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "new value"
+}
+```
+
+## Update variable
+
+Update a project's build variable.
+
+```
+PUT /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+
+```
+curl -X PUT -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/NEW_VARIABLE" -F "value=updated value"
+```
+
+```json
+{
+ "key": "NEW_VARIABLE",
+ "value": "updated value"
+}
+```
+
+## Remove variable
+
+Remove a project's build variable.
+
+```
+DELETE /projects/:id/variables/:key
+```
+
+| Attribute | Type | required | Description |
+|-----------|---------|----------|-------------------------|
+| `id` | integer | yes | The ID of a project |
+| `key` | string | yes | The `key` of a variable |
+
+```
+curl -X DELETE -H "PRIVATE_TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/1/variables/VARIABLE_1"
+```
+
+```json
+{
+ "key": "VARIABLE_1",
+ "value": "VALUE_1"
+}
+```
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7834262d612..098dd975840 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -54,5 +54,6 @@ module API
mount Keys
mount Tags
mount Triggers
+ mount Variables
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index e3bc3316ce5..e2feb7bb6d9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -366,5 +366,9 @@ module API
class TriggerRequest < Grape::Entity
expose :id, :variables
end
+
+ class Variable < Grape::Entity
+ expose :key, :value
+ end
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
new file mode 100644
index 00000000000..d9a055f6c92
--- /dev/null
+++ b/lib/api/variables.rb
@@ -0,0 +1,95 @@
+module API
+ # Projects variables API
+ class Variables < Grape::API
+ before { authenticate! }
+ before { authorize_admin_project }
+
+ resource :projects do
+ # Get project variables
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # page (optional) - The page number for pagination
+ # per_page (optional) - The value of items per page to show
+ # Example Request:
+ # GET /projects/:id/variables
+ get ':id/variables' do
+ variables = user_project.variables
+ present paginate(variables), with: Entities::Variable
+ end
+
+ # Get specific variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The `key` of variable
+ # Example Request:
+ # GET /projects/:id/variables/:key
+ get ':id/variables/:key' do
+ key = params[:key]
+ variable = user_project.variables.find_by(key: key.to_s)
+
+ return not_found!('Variable') unless variable
+
+ present variable, with: Entities::Variable
+ end
+
+ # Create a new variable in project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The key of variable
+ # value (required) - The value of variable
+ # Example Request:
+ # POST /projects/:id/variables
+ post ':id/variables' do
+ required_attributes! [:key, :value]
+
+ variable = user_project.variables.create(key: params[:key], value: params[:value])
+
+ if variable.valid?
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ # Update existing variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (optional) - The `key` of variable
+ # value (optional) - New value for `value` field of variable
+ # Example Request:
+ # PUT /projects/:id/variables/:key
+ put ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key].to_s)
+
+ return not_found!('Variable') unless variable
+
+ attrs = attributes_for_keys [:value]
+ if variable.update(attrs)
+ present variable, with: Entities::Variable
+ else
+ render_validation_error!(variable)
+ end
+ end
+
+ # Delete existing variable of a project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # key (required) - The ID of a variable
+ # Example Request:
+ # DELETE /projects/:id/variables/:key
+ delete ':id/variables/:key' do
+ variable = user_project.variables.find_by(key: params[:key].to_s)
+
+ return not_found!('Variable') unless variable
+ variable.destroy
+
+ present variable, with: Entities::Variable
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
new file mode 100644
index 00000000000..8f62d64411b
--- /dev/null
+++ b/spec/factories/ci/variables.rb
@@ -0,0 +1,22 @@
+# == Schema Information
+#
+# Table name: ci_variables
+#
+# id :integer not null, primary key
+# project_id :integer not null
+# key :string(255)
+# value :text
+# encrypted_value :text
+# encrypted_value_salt :string(255)
+# encrypted_value_iv :string(255)
+# gl_project_id :integer
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_variable, class: Ci::Variable do
+ sequence(:key) { |n| "VARIABLE_#{n}" }
+ value 'VARIABLE_VALUE'
+ end
+end
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
new file mode 100644
index 00000000000..9744729ba0c
--- /dev/null
+++ b/spec/requests/api/variables_spec.rb
@@ -0,0 +1,182 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ 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!(:developer) { create(:project_member, user: user2, project: project, access_level: ProjectMember::DEVELOPER) }
+ let!(:variable) { create(:ci_variable, project: project) }
+
+ describe 'GET /projects/:id/variables' do
+ context 'authorized user with proper permissions' do
+ it 'should return project variables' do
+ get api("/projects/#{project.id}/variables", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_a(Array)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not return project variables' do
+ get api("/projects/#{project.id}/variables", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project variables' do
+ get api("/projects/#{project.id}/variables")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}", user)
+
+ expect(response.status).to eq(200)
+ expect(json_response['value']).to eq(variable.value)
+ end
+
+ it 'should respond with 404 Not Found if requesting non-existing variable' do
+ get api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not return project variable details' do
+ get api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/variables' do
+ context 'authorized user with proper permissions' do
+ it 'should create variable' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
+ end.to change{project.variables.count}.by(1)
+
+ expect(response.status).to eq(201)
+ expect(json_response['key']).to eq('TEST_VARIABLE_2')
+ expect(json_response['value']).to eq('VALUE_2')
+ end
+
+ it 'should not allow to duplicate variable key' do
+ expect do
+ post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2'
+ end.to change{project.variables.count}.by(0)
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not create variable' do
+ post api("/projects/#{project.id}/variables", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not create variable' do
+ post api("/projects/#{project.id}/variables")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should update variable data' do
+ initial_variable = project.variables.first
+ value_before = initial_variable.value
+
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP'
+
+ updated_variable = project.variables.first
+
+ expect(response.status).to eq(200)
+ expect(value_before).to eq(variable.value)
+ expect(updated_variable.value).to eq('VALUE_1_UP')
+ end
+
+ it 'should responde with 404 Not Found if requesting non-existing variable' do
+ put api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not update variable' do
+ put api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not update variable' do
+ put api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/variables/:key' do
+ context 'authorized user with proper permissions' do
+ it 'should delete variable' do
+ expect do
+ delete api("/projects/#{project.id}/variables/#{variable.key}", user)
+ end.to change{project.variables.count}.by(-1)
+ expect(response.status).to eq(200)
+ end
+
+ it 'should responde with 404 Not Found if requesting non-existing variable' do
+ delete api("/projects/#{project.id}/variables/non_existing_variable", user)
+
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'authorized user with invalid permissions' do
+ it 'should not delete variable' do
+ delete api("/projects/#{project.id}/variables/#{variable.key}", user2)
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'unauthorized user' do
+ it 'should not delete variable' do
+ delete api("/projects/#{project.id}/variables/#{variable.key}")
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end