Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG2
-rw-r--r--app/assets/javascripts/blob/template_selector.js7
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss81
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/controllers/projects/git_http_client_controller.rb71
-rw-r--r--app/helpers/lfs_helper.rb2
-rw-r--r--app/models/commit_status.rb20
-rw-r--r--app/models/concerns/has_status.rb9
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml25
-rw-r--r--app/views/projects/commit/_pipeline.html.haml3
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml14
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml11
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml17
-rw-r--r--db/fixtures/development/14_pipelines.rb10
-rw-r--r--db/migrate/20140502125220_migrate_repo_size.rb5
-rw-r--r--doc/ci/quick_start/README.md3
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/workflow/lfs/lfs_administration.md4
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md8
-rw-r--r--lib/api/internal.rb13
-rw-r--r--lib/gitlab/auth.rb45
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb4
-rw-r--r--lib/gitlab/lfs_token.rb50
-rw-r--r--spec/features/projects/issuable_templates_spec.rb13
-rw-r--r--spec/features/todos/todos_filtering_spec.rb20
-rw-r--r--spec/lib/gitlab/auth_spec.rb20
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb51
-rw-r--r--spec/models/ci/pipeline_spec.rb28
-rw-r--r--spec/models/commit_status_spec.rb43
-rw-r--r--spec/requests/api/internal_spec.rb46
-rw-r--r--spec/requests/lfs_http_spec.rb16
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb50
32 files changed, 585 insertions, 112 deletions
diff --git a/CHANGELOG b/CHANGELOG
index e410d73d1f6..9458413669d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -37,6 +37,7 @@ v 8.12.0 (unreleased)
- Request only the LDAP attributes we need !6187
- Center build stage columns in pipeline overview (ClemMakesApps)
- Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
+ - Fix bug stopping issue description being scrollable after selecting issue template
- Remove suggested colors hover underline (ClemMakesApps)
- Shorten task status phrase (ClemMakesApps)
- Fix project visibility level fields on settings
@@ -92,6 +93,7 @@ v 8.12.0 (unreleased)
- Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
- Fix repo title alignment (ClemMakesApps)
- Change update interval of contacted_at
+ - Add LFS support to SSH !6043
- Fix branch title trailing space on hover (ClemMakesApps)
- Don't include 'Created By' tag line when importing from GitHub if there is a linked GitLab account (EspadaV8)
- Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index b18b6962382..95352164d76 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -13,6 +13,9 @@
this.buildDropdown();
this.bindEvents();
this.onFilenameUpdate();
+
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
}
TemplateSelector.prototype.buildDropdown = function() {
@@ -72,6 +75,10 @@
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
if (!skipFocus) this.editor.focus();
+
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+ }
};
TemplateSelector.prototype.startLoadingSpinner = function() {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 2d66ab25da6..cc71b8eb045 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -318,9 +318,17 @@
.build-content {
width: 130px;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+
+ .ci-status-text {
+ width: 110px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ display: inline-block;
+ position: relative;
+ top: -1px;
+ }
a {
color: $layout-link-gray;
@@ -331,13 +339,74 @@
text-decoration: underline;
}
}
+ }
+
+ .dropdown-menu-toggle {
+ border: none;
+ width: auto;
+ padding: 0;
+ color: $layout-link-gray;
+
+ .ci-status-text {
+ width: 80px;
+ }
+ }
+
+ .grouped-pipeline-dropdown {
+ padding: 8px 0;
+ width: 200px;
+ left: auto;
+ right: -214px;
+ top: -9px;
+
+ a:hover {
+ .ci-status-text {
+ text-decoration: none;
+ }
+ }
+
+ .ci-status-text {
+ width: 145px;
+ }
+
+ .arrow {
+ &:before,
+ &:after {
+ content: '';
+ display: inline-block;
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid;
+ top: 18px;
+ }
+
+ &:before {
+ left: -5px;
+ margin-top: -6px;
+ border-width: 7px 5px 7px 0;
+ border-right-color: $border-color;
+ }
+ &:after {
+ left: -4px;
+ margin-top: -9px;
+ border-width: 10px 7px 10px 0;
+ border-right-color: $white-light;
+ }
+ }
+ }
+
+ .badge {
+ background-color: $gray-dark;
+ color: $layout-link-gray;
+ font-weight: normal;
}
}
svg {
- position: relative;
- top: 2px;
+ vertical-align: middle;
margin-right: 5px;
}
@@ -442,7 +511,7 @@
width: 21px;
height: 25px;
position: absolute;
- top: -28.5px;
+ top: -29px;
border-top: 2px solid $border-color;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 3e6e50375f6..db46d8072ce 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -334,6 +334,10 @@ a.deploy-project-label {
a {
color: $gl-dark-link-color;
}
+
+ .dropdown-menu {
+ width: 240px;
+ }
}
.last-push-widget {
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index c2a298fe37f..14e83ddda04 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper
- attr_reader :user, :capabilities
+ attr_reader :actor, :capabilities
# Git clients will not know what authenticity token to send along
skip_before_action :verify_authenticity_token
@@ -21,31 +21,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
- auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
-
- if auth_result.type == :ci && !download_request?
- # Not allowed
- auth_result = Gitlab::Auth::Result.new
- elsif auth_result.type == :oauth && !download_request?
- # Not allowed
- auth_result = Gitlab::Auth::Result.new
- elsif auth_result.type == :missing_personal_token
- render_missing_personal_token
- return # Render above denied access, nothing left to do
- else
- @user = auth_result.user
- end
-
- @capabilities = auth_result.capabilities || []
- @ci = auth_result.type == :ci
- if auth_result.succeeded?
+ if handle_basic_authentication(login, password)
return # Allow access
end
elsif allow_kerberos_spnego_auth? && spnego_provided?
- @user = find_kerberos_user
+ @actor = find_kerberos_user
- if user
+ if actor
send_final_spnego_response
return # Allow access
end
@@ -53,6 +36,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
send_challenges
render plain: "HTTP Basic: Access denied\n", status: 401
+ rescue Gitlab::Auth::MissingPersonalTokenError
+ render_missing_personal_token
end
def basic_auth_provided?
@@ -120,7 +105,49 @@ class Projects::GitHttpClientController < Projects::ApplicationController
end
def ci?
- @ci.present?
+ @ci
+ end
+
+ def user
+ @actor
+ end
+
+ def handle_basic_authentication(login, password)
+ auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
+
+ case auth_result.type
+ when :ci
+ if download_request?
+ @ci = true
+ else
+ return false
+ end
+ when :oauth
+ if download_request?
+ @actor = auth_result.actor
+ @capabilities = auth_result.capabilities
+ else
+ return false
+ end
+ when :lfs_deploy_token
+ if download_request?
+ @lfs_deploy_key = true
+ @actor = auth_result.actor
+ @capabilities = auth_result.capabilities
+ end
+ when :lfs_token, :personal_token, :gitlab_or_ldap, :build
+ @actor = auth_result.actor
+ @capabilities = auth_result.capabilities
+ else
+ # Not allowed
+ return false
+ end
+
+ true
+ end
+
+ def lfs_deploy_key?
+ @lfs_deploy_key && actor && actor.projects.include?(project)
end
def has_capability?(capability)
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index d27b87ed5e4..0d6b72ff746 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -25,7 +25,7 @@ module LfsHelper
def lfs_download_access?
return false unless project.lfs_enabled?
- project.public? || ci? || user_can_download_code? || build_can_download_code?
+ project.public? || ci? || lfs_deploy_key? || user_can_download_code? || build_can_download_code?
end
def user_can_download_code?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 4a628924499..c85561291c8 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -69,17 +69,15 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
- # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
- around_transition any => [:success, :failed, :canceled] do |commit_status, block|
- block.call
-
- commit_status.pipeline.try(:process!)
- end
-
after_transition do |commit_status, transition|
commit_status.pipeline.try(:build_updated) unless transition.loopback?
end
+ after_transition any => [:success, :failed, :canceled] do |commit_status|
+ commit_status.pipeline.try(:process!)
+ true
+ end
+
after_transition [:created, :pending, :running] => :success do |commit_status|
MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
end
@@ -95,6 +93,10 @@ class CommitStatus < ActiveRecord::Base
pipeline.before_sha || Gitlab::Git::BLANK_SHA
end
+ def group_name
+ name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
+ end
+
def self.stages
# We group by stage name, but order stages by theirs' index
unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
@@ -113,6 +115,10 @@ class CommitStatus < ActiveRecord::Base
allow_failure? && (failed? || canceled?)
end
+ def playable?
+ false
+ end
+
def duration
calculate_duration
end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index f7b8352405c..d658552f695 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -8,8 +8,9 @@ module HasStatus
class_methods do
def status_sql
- scope = all.relevant
+ scope = all
builds = scope.select('count(*)').to_sql
+ created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql
ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
ignored ||= '0'
@@ -19,12 +20,12 @@ module HasStatus
skipped = scope.skipped.select('count(*)').to_sql
deduce_status = "(CASE
- WHEN (#{builds})=0 THEN NULL
+ WHEN (#{builds})=(#{created}) THEN NULL
WHEN (#{builds})=(#{skipped}) THEN 'skipped'
WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
- WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending'
+ WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
- WHEN (#{running})+(#{pending})>0 THEN 'running'
+ WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
ELSE 'failed'
END)"
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 36fb0300aeb..547bc0c9c19 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,15 +1,12 @@
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
-%li.build{class: ("playable" if is_playable)}
- .curve
- .build-content
- - if is_playable
- = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
- = render_status_with_link('build', 'play')
- %span.ci-status-text= subject.name
- - elsif can?(current_user, :read_build, @project)
- = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
- = render_status_with_link('build', subject.status)
- %span.ci-status-text= subject.name
- - else
- = render_status_with_link('build', subject.status)
- = ci_icon_for_status(subject.status)
+- if is_playable
+ = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
+ = render_status_with_link('build', 'play')
+ .ci-status-text= subject.name
+- elsif can?(current_user, :read_build, @project)
+ = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
+ = render_status_with_link('build', subject.status)
+ .ci-status-text= subject.name
+- else
+ = render_status_with_link('build', subject.status)
+ = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 20a85148ab5..9258f4b3c25 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -39,8 +39,7 @@
= stage.titleize
.builds-container
%ul
- - statuses.each do |status|
- = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ = render "projects/commit/pipeline_stage", statuses: statuses
- if pipeline.yaml_errors.present?
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
new file mode 100644
index 00000000000..23c5c51fbc2
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_stage.html.haml
@@ -0,0 +1,14 @@
+- status_groups = statuses.group_by(&:group_name)
+- status_groups.each do |group_name, grouped_statuses|
+ - if grouped_statuses.one?
+ - status = grouped_statuses.first
+ - is_playable = status.playable? && can?(current_user, :update_build, @project)
+ %li.build{ class: ("playable" if is_playable) }
+ .curve
+ .build-content
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
+ - else
+ %li.build
+ .curve
+ .build-content
+ = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
new file mode 100644
index 00000000000..4e7a6f1af08
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -0,0 +1,11 @@
+- group_status = CommitStatus.where(id: subject).status
+= render_status_with_link('build', group_status)
+.dropdown.inline
+ %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %span.ci-status-text
+ = name
+ %span.badge= subject.size
+ %ul.dropdown-menu.grouped-pipeline-dropdown
+ .arrow
+ - subject.each do |status|
+ = render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 576d0bec51b..409f4701e4b 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -1,10 +1,7 @@
-%li.build
- .curve
- .build-content
- - if subject.target_url
- - link_to subject.target_url do
- = render_status_with_link('commit status', subject.status)
- %span.ci-status-text= subject.name
- - else
- = render_status_with_link('commit status', subject.status)
- %span.ci-status-text= subject.name
+- if subject.target_url
+ = link_to subject.target_url do
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
+- else
+ = render_status_with_link('commit status', subject.status)
+ %span.ci-status-text= subject.name
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 49e6e2361b1..650b410595c 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -3,9 +3,13 @@ class Gitlab::Seeder::Pipelines
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
{ name: 'build:osx', stage: 'build', status: :success },
- { name: 'rspec:linux', stage: 'test', status: :success },
- { name: 'rspec:windows', stage: 'test', status: :success },
- { name: 'rspec:windows', stage: 'test', status: :success },
+ { name: 'rspec:linux 0 3', stage: 'test', status: :success },
+ { name: 'rspec:linux 1 3', stage: 'test', status: :success },
+ { name: 'rspec:linux 2 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 0 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 1 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 2 3', stage: 'test', status: :success },
+ { name: 'rspec:windows 2 3', stage: 'test', status: :success },
{ name: 'rspec:osx', stage: 'test', status_event: :success },
{ name: 'spinach:linux', stage: 'test', status: :success },
{ name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index 84463727b3b..e8de7ccf3db 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -1,12 +1,15 @@
# rubocop:disable all
class MigrateRepoSize < ActiveRecord::Migration
+ DOWNTIME = false
+
def up
project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id')
project_data.each do |project|
id = project['id']
namespace_path = project['namespace_path'] || ''
- path = File.join(Gitlab.config.gitlab_shell.repos_path, namespace_path, project['project_path'] + '.git')
+ repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default
+ path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
begin
repo = Gitlab::Git::Repository.new(path)
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index c835ebc2d44..c40cdd55ea5 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -105,7 +105,8 @@ What is important is that each job is run independently from each other.
If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
-the link under **Settings > CI settings** in your project.
+a "CI Lint" button to go to this page under **Pipelines > Pipelines** and
+**Pipelines > Builds** in your project.
For more information and a complete `.gitlab-ci.yml` syntax, please read
[the documentation on .gitlab-ci.yml](../yaml/README.md).
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 1498cb361c8..f1b75298180 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -63,7 +63,7 @@ The following table depicts the various user permission levels in a project.
| Force push to protected branches [^2] | | | | | |
| Remove protected branches [^2] | | | | | |
-[^1]: If **Allow guest to access builds** is enabled in CI settings
+[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
[^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
## Group
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 9dc1e9b47e3..b3c73e947f0 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -45,5 +45,5 @@ In `config/gitlab.yml`:
* Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
is not supported
* Currently, removing LFS objects from GitLab Git LFS storage is not supported
-* LFS authentications via SSH is not supported for the time being
-* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2.
+* LFS authentications via SSH was added with GitLab 8.12
+* Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2.
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 9fe065fa680..1a4f213a792 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -35,6 +35,10 @@ Documentation for GitLab instance administrators is under [LFS administration do
credentials store is recommended
* Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
to add the URL to Git config manually (see #troubleshooting)
+
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
## Using Git LFS
@@ -132,6 +136,10 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
### Credentials are always required when pushing an object
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
+
Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
the LFS object on every push for every object, user HTTPS credentials are required.
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 2610fd329d6..865379c51c4 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -82,6 +82,19 @@ module API
response
end
+ post "/lfs_authenticate" do
+ status 200
+
+ key = Key.find(params[:key_id])
+ token_handler = Gitlab::LfsToken.new(key)
+
+ {
+ username: token_handler.actor_name,
+ lfs_token: token_handler.generate,
+ repository_http_path: project.http_url_to_repo
+ }
+ end
+
get "/merge_request_urls" do
::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index b1427f412b0..b14c4e565d5 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,23 +1,28 @@
module Gitlab
module Auth
- Result = Struct.new(:user, :project, :type, :capabilities) do
- def succeeded?
- user.present? || [:ci].include?(type)
+ Result = Struct.new(:actor, :project, :type, :capabilities) do
+ def success?
+ actor.present? || type == :ci
end
end
+ class MissingPersonalTokenError < StandardError; end
+
class << self
def find_for_git_client(login, password, project:, ip:)
raise "Must provide an IP for rate limiting" if ip.nil?
- result = service_access_token_check(login, password, project) ||
+ result =
+ service_request_check(login, password, project) ||
build_access_token_check(login, password) ||
user_with_password_for_git(login, password) ||
oauth_access_token_check(login, password) ||
+ lfs_token_check(login, password) ||
personal_access_token_check(login, password) ||
Result.new
- rate_limit!(ip, success: result.succeeded?, login: login)
+ rate_limit!(ip, success: result.success?, login: login)
+
result
end
@@ -59,7 +64,7 @@ module Gitlab
private
- def service_access_token_check(login, password, project)
+ def service_request_check(login, password, project)
matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
return unless project && matched_login.present?
@@ -81,14 +86,9 @@ module Gitlab
user = find_with_user_password(login, password)
return unless user
- type =
- if user.two_factor_enabled?
- :missing_personal_token
- else
- :gitlab_or_ldap
- end
+ raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
- Result.new(user, nil, type, full_capabilities)
+ Result.new(user, nil, :gitlab_or_ldap, full_capabilities)
end
def oauth_access_token_check(login, password)
@@ -105,9 +105,24 @@ module Gitlab
if login && password
user = User.find_by_personal_access_token(password)
validation = User.by_login(login)
- if user && user == validation
- Result.new(user, nil, :personal_token, full_capabilities)
+ Result.new(user, nil, :personal_token, full_capabilities) if user.present? && user == validation
+ end
+ end
+
+ def lfs_token_check(login, password)
+ deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
+
+ actor =
+ if deploy_key_matches
+ DeployKey.find(deploy_key_matches[1])
+ else
+ User.by_login(login)
end
+
+ if actor
+ token_handler = Gitlab::LfsToken.new(actor)
+
+ Result.new(actor, nil, token_handler.type, read_capabilities) if Devise.secure_compare(token_handler.value, password)
end
end
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 6d9379acf25..d1e33ea8678 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -22,10 +22,6 @@ module Gitlab
private
- def repos_path
- Gitlab.config.gitlab_shell.repos_path
- end
-
def path_to_repo
@project.repository.path_to_repo
end
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
new file mode 100644
index 00000000000..f492754b1c8
--- /dev/null
+++ b/lib/gitlab/lfs_token.rb
@@ -0,0 +1,50 @@
+module Gitlab
+ class LfsToken
+ attr_accessor :actor
+
+ TOKEN_LENGTH = 50
+ EXPIRY_TIME = 1800
+
+ def initialize(actor)
+ @actor =
+ case actor
+ when DeployKey, User
+ actor
+ when Key
+ actor.user
+ else
+ raise 'Bad Actor'
+ end
+ end
+
+ def generate
+ token = Devise.friendly_token(TOKEN_LENGTH)
+
+ Gitlab::Redis.with do |redis|
+ redis.set(redis_key, token, ex: EXPIRY_TIME)
+ end
+
+ token
+ end
+
+ def value
+ Gitlab::Redis.with do |redis|
+ redis.get(redis_key)
+ end
+ end
+
+ def type
+ actor.is_a?(User) ? :lfs_token : :lfs_deploy_token
+ end
+
+ def actor_name
+ actor.is_a?(User) ? actor.username : "lfs+deploy-key-#{actor.id}"
+ end
+
+ private
+
+ def redis_key
+ "gitlab:lfs_token:#{actor.class.name.underscore}_#{actor.id}" if actor
+ end
+ end
+end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 4a83740621a..d0f4e5469ed 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -13,10 +13,12 @@ feature 'issuable templates', feature: true, js: true do
context 'user creates an issue using templates' do
let(:template_content) { 'this is a test "bug" template' }
+ let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
let(:issue) { create(:issue, author: user, assignee: user, project: project) }
background do
project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false)
visit edit_namespace_project_issue_path project.namespace, project, issue
fill_in :'issue[title]', with: 'test issue title'
end
@@ -27,6 +29,17 @@ feature 'issuable templates', feature: true, js: true do
preview_template
save_changes
end
+
+ it 'updates height of markdown textarea' do
+ start_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+ select_template 'test'
+ wait_for_ajax
+
+ end_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+ expect(end_height).not_to eq(start_height)
+ end
end
context 'user creates a merge request using templates' do
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
index 83cf306437d..b9e66243d84 100644
--- a/spec/features/todos/todos_filtering_spec.rb
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -29,8 +29,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
fill_in 'Search projects', with: project_1.name_with_namespace
click_link project_1.name_with_namespace
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content project_2.name_with_namespace
+
+ expect(page).to have_content project_1.name_with_namespace
+ expect(page).not_to have_content project_2.name_with_namespace
end
it 'filters by author' do
@@ -39,8 +42,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
fill_in 'Search authors', with: user_1.name
click_link user_1.name
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content user_2.name
+
+ expect(find('.todos-list')).to have_content user_1.name
+ expect(find('.todos-list')).not_to have_content user_2.name
end
it 'filters by type' do
@@ -48,8 +54,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
within '.dropdown-menu-type' do
click_link 'Issue'
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content ' merge request !'
+
+ expect(find('.todos-list')).to have_content issue.to_reference
+ expect(find('.todos-list')).not_to have_content merge_request.to_reference
end
it 'filters by action' do
@@ -57,7 +66,10 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
within '.dropdown-menu-action' do
click_link 'Assigned'
end
+
wait_for_ajax
- expect('.prepend-top-default').not_to have_content ' mentioned '
+
+ expect(find('.todos-list')).to have_content ' assigned you '
+ expect(find('.todos-list')).not_to have_content ' mentioned '
end
end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b665517bbb0..c09ab1dbd57 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -59,6 +59,24 @@ describe Gitlab::Auth, lib: true do
expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_capabilities))
end
+ it 'recognizes user lfs tokens' do
+ user = create(:user)
+ ip = 'ip'
+ token = Gitlab::LfsToken.new(user).generate
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
+ expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :lfs_token))
+ end
+
+ it 'recognizes deploy key lfs tokens' do
+ key = create(:deploy_key)
+ ip = 'ip'
+ token = Gitlab::LfsToken.new(key).generate
+
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
+ expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, :lfs_deploy_token))
+ end
+
it 'recognizes OAuth tokens' do
user = create(:user)
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
@@ -73,7 +91,7 @@ describe Gitlab::Auth, lib: true do
login = 'foo'
ip = 'ip'
- expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
+ expect(gl_auth).to receive(:rate_limit!).with(ip, success: nil, login: login)
expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
new file mode 100644
index 00000000000..9f04f67e0a8
--- /dev/null
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::LfsToken, lib: true do
+ describe '#generate and #value' do
+ shared_examples 'an LFS token generator' do
+ it 'returns a randomly generated token' do
+ token = handler.generate
+
+ expect(token).not_to be_nil
+ expect(token).to be_a String
+ expect(token.length).to eq 50
+ end
+
+ it 'returns the correct token based on the key' do
+ token = handler.generate
+
+ expect(handler.value).to eq(token)
+ end
+ end
+
+ context 'when the actor is a user' do
+ let(:actor) { create(:user) }
+ let(:handler) { described_class.new(actor) }
+
+ it_behaves_like 'an LFS token generator'
+
+ it 'returns the correct username' do
+ expect(handler.actor_name).to eq(actor.username)
+ end
+
+ it 'returns the correct token type' do
+ expect(handler.type).to eq(:lfs_token)
+ end
+ end
+
+ context 'when the actor is a deploy key' do
+ let(:actor) { create(:deploy_key) }
+ let(:handler) { described_class.new(actor) }
+
+ it_behaves_like 'an LFS token generator'
+
+ it 'returns the correct username' do
+ expect(handler.actor_name).to eq("lfs+deploy-key-#{actor.id}")
+ end
+
+ it 'returns the correct token type' do
+ expect(handler.type).to eq(:lfs_deploy_token)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index fbf945c757c..f1857f846dc 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -373,8 +373,8 @@ describe Ci::Pipeline, models: true do
end
describe '#execute_hooks' do
- let!(:build_a) { create_build('a') }
- let!(:build_b) { create_build('b') }
+ let!(:build_a) { create_build('a', 0) }
+ let!(:build_b) { create_build('b', 1) }
let!(:hook) do
create(:project_hook, project: project, pipeline_events: enabled)
@@ -398,7 +398,7 @@ describe Ci::Pipeline, models: true do
build_b.enqueue
end
- it 'receive a pending event once' do
+ it 'receives a pending event once' do
expect(WebMock).to have_requested_pipeline_hook('pending').once
end
end
@@ -411,7 +411,7 @@ describe Ci::Pipeline, models: true do
build_b.run
end
- it 'receive a running event once' do
+ it 'receives a running event once' do
expect(WebMock).to have_requested_pipeline_hook('running').once
end
end
@@ -422,11 +422,21 @@ describe Ci::Pipeline, models: true do
build_b.success
end
- it 'receive a success event once' do
+ it 'receives a success event once' do
expect(WebMock).to have_requested_pipeline_hook('success').once
end
end
+ context 'when stage one failed' do
+ before do
+ build_a.drop
+ end
+
+ it 'receives a failed event once' do
+ expect(WebMock).to have_requested_pipeline_hook('failed').once
+ end
+ end
+
def have_requested_pipeline_hook(status)
have_requested(:post, hook.url).with do |req|
json_body = JSON.parse(req.body)
@@ -450,8 +460,12 @@ describe Ci::Pipeline, models: true do
end
end
- def create_build(name)
- create(:ci_build, :created, pipeline: pipeline, name: name)
+ def create_build(name, stage_idx)
+ create(:ci_build,
+ :created,
+ pipeline: pipeline,
+ name: name,
+ stage_idx: stage_idx)
end
end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index fcfa3138ce5..2f1baff5d66 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -40,7 +40,7 @@ describe CommitStatus, models: true do
it { is_expected.to be_falsey }
end
- %w(running success failed).each do |status|
+ %w[running success failed].each do |status|
context "if commit status is #{status}" do
before { commit_status.status = status }
@@ -48,7 +48,7 @@ describe CommitStatus, models: true do
end
end
- %w(pending canceled).each do |status|
+ %w[pending canceled].each do |status|
context "if commit status is #{status}" do
before { commit_status.status = status }
@@ -60,7 +60,7 @@ describe CommitStatus, models: true do
describe '#active?' do
subject { commit_status.active? }
- %w(pending running).each do |state|
+ %w[pending running].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -68,7 +68,7 @@ describe CommitStatus, models: true do
end
end
- %w(success failed canceled).each do |state|
+ %w[success failed canceled].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -80,7 +80,7 @@ describe CommitStatus, models: true do
describe '#complete?' do
subject { commit_status.complete? }
- %w(success failed canceled).each do |state|
+ %w[success failed canceled].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -88,7 +88,7 @@ describe CommitStatus, models: true do
end
end
- %w(pending running).each do |state|
+ %w[pending running].each do |state|
context "if commit_status.status is #{state}" do
before { commit_status.status = state }
@@ -187,7 +187,7 @@ describe CommitStatus, models: true do
subject { CommitStatus.where(pipeline: pipeline).stages }
it 'returns ordered list of stages' do
- is_expected.to eq(%w(build test deploy))
+ is_expected.to eq(%w[build test deploy])
end
end
@@ -223,4 +223,33 @@ describe CommitStatus, models: true do
expect(commit_status.commit).to eq project.commit
end
end
+
+ describe '#group_name' do
+ subject { commit_status.group_name }
+
+ tests = {
+ 'rspec:windows' => 'rspec:windows',
+ 'rspec:windows 0' => 'rspec:windows 0',
+ 'rspec:windows 0 test' => 'rspec:windows 0 test',
+ 'rspec:windows 0 1' => 'rspec:windows',
+ 'rspec:windows 0 1 name' => 'rspec:windows name',
+ 'rspec:windows 0/1' => 'rspec:windows',
+ 'rspec:windows 0/1 name' => 'rspec:windows name',
+ 'rspec:windows 0:1' => 'rspec:windows',
+ 'rspec:windows 0:1 name' => 'rspec:windows name',
+ 'rspec:windows 10000 20000' => 'rspec:windows',
+ 'rspec:windows 0 : / 1' => 'rspec:windows',
+ 'rspec:windows 0 : / 1 name' => 'rspec:windows name',
+ '0 1 name ruby' => 'name ruby',
+ '0 :/ 1 name ruby' => 'name ruby'
+ }
+
+ tests.each do |name, group_name|
+ it "'#{name}' puts in '#{group_name}'" do
+ commit_status.name = name
+
+ is_expected.to eq(group_name)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 46d1b868782..46e8e6f1169 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -100,6 +100,43 @@ describe API::API, api: true do
end
end
+ describe "POST /internal/lfs_authenticate" do
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'user key' do
+ it 'returns the correct information about the key' do
+ lfs_auth(key.id, project)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['username']).to eq(user.username)
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
+
+ expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+ end
+
+ it 'returns a 404 when the wrong key is provided' do
+ lfs_auth(nil, project)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'deploy key' do
+ let(:key) { create(:deploy_key) }
+
+ it 'returns the correct information about the key' do
+ lfs_auth(key.id, project)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
+ expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+ end
+ end
+ end
+
describe "GET /internal/discover" do
it do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
@@ -389,4 +426,13 @@ describe API::API, api: true do
protocol: 'ssh'
)
end
+
+ def lfs_auth(key_id, project)
+ post(
+ api("/internal/lfs_authenticate"),
+ key_id: key_id,
+ secret_token: secret_token,
+ project: project.path_with_namespace
+ )
+ end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 7bf43a03f23..8ead97efb01 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -246,6 +246,18 @@ describe 'Git LFS API and storage' do
end
end
+ context 'when deploy key is authorized' do
+ let(:key) { create(:deploy_key) }
+ let(:authorization) { authorize_deploy_key }
+
+ let(:update_permissions) do
+ project.deploy_keys << key
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
@@ -906,6 +918,10 @@ describe 'Git LFS API and storage' do
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
+ def authorize_deploy_key
+ ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).generate)
+ end
+
def fork_project(project, user, object = nil)
allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
Projects::ForkService.new(project, user, {}).execute
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
new file mode 100644
index 00000000000..c5b16c1c304
--- /dev/null
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'projects/pipelines/show' do
+ include Devise::TestHelpers
+
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
+
+ before do
+ controller.prepend_view_path('app/views/projects')
+
+ create_build('build', 0, 'build')
+ create_build('test', 1, 'rspec 0:2')
+ create_build('test', 1, 'rspec 1:2')
+ create_build('test', 1, 'audit')
+ create_build('deploy', 2, 'production')
+
+ create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
+
+ assign(:project, project)
+ assign(:pipeline, pipeline)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ it 'shows a graph with grouped stages' do
+ render
+
+ expect(rendered).to have_css('.pipeline-graph')
+ expect(rendered).to have_css('.grouped-pipeline-dropdown')
+
+ # stages
+ expect(rendered).to have_text('Build')
+ expect(rendered).to have_text('Test')
+ expect(rendered).to have_text('Deploy')
+ expect(rendered).to have_text('External')
+
+ # builds
+ expect(rendered).to have_text('rspec')
+ expect(rendered).to have_text('rspec 0:2')
+ expect(rendered).to have_text('production')
+ expect(rendered).to have_text('jenkins')
+ end
+
+ private
+
+ def create_build(stage, stage_idx, name)
+ create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
+ end
+end